├── .gitignore ├── .gitmodules ├── CODEOWNERS ├── LICENSE ├── README.md ├── ScriptSelector.py ├── Script_Selector_py3.py ├── Script_Selector_py3_11.py ├── development ├── BeamTemplateConstruction.py ├── Couch_Query.py ├── DITTO_APTR.py ├── MultipatientPlanning.py ├── PlanDialog.py ├── automatic_otv_maker.py ├── autoplan_tomo_vmat_tbi.py ├── get_implanted_device_metrics.py └── standard_cutout.py ├── general ├── AutoPlan.py ├── ExportMenu.py ├── UpdateScripts.py ├── automated_plan_optimization.py ├── create_goals.py ├── electron_qa_plan.py ├── planning_structures.py └── run_dosimetry_safety_review.py ├── helper_scripts ├── CompareContours.py ├── DITTO_APTR.py ├── DisplayTomoSinogram.py ├── PRDR_CalcSheet.py ├── Visual_Settings.py ├── beavis_run_physics_review_script.py ├── parse_log_file.py ├── run_dosimetry_safety_review.py └── test_modules.py ├── ide.general.xml ├── legacy ├── CreateMobius3DDLGPlans.py ├── CreateMobius3DValidationPlans.py ├── CreateReferenceCT.py ├── CreateTPO.py ├── CreateWaterTankPlans.py ├── FilterLeaves.py ├── GenerateGoalTemplates.py ├── ImportRecalcDICOMPlans.py ├── MakeRandoPlanElectrons.py ├── PlanningStudy.py ├── SelectCaseWhenMultipleCases.py ├── ShoulderBlock.py ├── Tomo3D.py ├── Tomo_PlanTransfer.py ├── UpdateTomoDQAPlan.py ├── VMATTestPlanAutoOptimize.py ├── VerifyDicomExport.py ├── autoplan_tomo_TBI.py ├── autoplan_tomo_vmat_TBI.py ├── check_isocenter_clearance.py ├── initial_physics_Jun20.py ├── select_element.py ├── test_pydicom.py └── test_userinterface.py ├── library ├── AutoPlanOperations.py ├── BeamOperations.py ├── Beams.py ├── ContouringAnalysis │ └── contouring_metrics.py ├── CreatePrvs.py ├── DITTO │ ├── AriaPlanTransferReviewChecks.py │ ├── AriaRTPlanQR.py │ ├── DicomIntegrityTool_APTR.py │ ├── DicomPairClasses.py │ ├── DicomPairTreeFunctions.py │ ├── DicomTagExpectedResults.md │ ├── ProcessingFunctions.py │ ├── __init__.py │ ├── images │ │ ├── blue_circle.png │ │ ├── blue_circle_icon.png │ │ ├── green_circle.png │ │ ├── green_circle_icon.png │ │ ├── red_circle.png │ │ ├── red_circle_icon.png │ │ ├── yellow_circle.png │ │ └── yellow_circle_icon.png │ ├── resizer.py │ └── testgui.py ├── DicomDestinations.xml ├── DicomExport.py ├── DicomFilters.xml ├── FinalDose.py ├── GeneralOperations.py ├── Goals.py ├── ImplantedDeviceOperations.py ├── Objectives.py ├── OldPlanReview │ ├── BeamSetReviewTests.py │ ├── ExamTests.py │ ├── Icons │ │ ├── UW_Health_Logo.png │ │ ├── Yellow_Circle_icon.png │ │ ├── blue_circle_icon.png │ │ ├── green_circle_icon.png │ │ └── red_circle_icon.png │ ├── PlanReviewTests.py │ ├── ReviewDefinitions.py │ ├── init_physics_19Jun2023.py │ └── retrieve_logs.py ├── OptimizationOperations.py ├── PlanOperations.py ├── PlanQualityAssuranceTests.py ├── PlanReview │ ├── __init__.py │ ├── automated_review.py │ ├── documentation │ │ ├── __init__.py │ │ ├── generate_dosimetry_safety_pdf.py │ │ ├── generate_physics_document.py │ │ └── generate_physics_pdf.py │ ├── generate_test_mapping_wikitable.py │ ├── guis │ │ ├── __init__.py │ │ ├── build_tree.py │ │ ├── comment_to_clipboard.py │ │ ├── create_physics_manual_tab.py │ │ ├── create_preplan_tab.py │ │ ├── create_side_panel.py │ │ ├── gui_bottom_buttons.py │ │ ├── gui_ditto_wrapper.py │ │ ├── gui_physics_review.py │ │ ├── gui_qa_form.py │ │ ├── gui_report_script_error.py │ │ ├── gui_top_buttons.py │ │ ├── icons │ │ │ ├── Thumbs.db │ │ │ ├── UW_Health_Logo.png │ │ │ ├── blue_circle_icon.png │ │ │ ├── cancel_icon.png │ │ │ ├── cancel_icon_small.png │ │ │ ├── checker_icon.png │ │ │ ├── error_icon.png │ │ │ ├── error_icon_small.png │ │ │ ├── final_icon.png │ │ │ ├── final_icon_small.png │ │ │ ├── green_circle_icon.png │ │ │ ├── load_icon.png │ │ │ ├── load_icon_small.png │ │ │ ├── material_icon.png │ │ │ ├── material_icon_small.png │ │ │ ├── pause_icon.png │ │ │ ├── pause_icon_small.png │ │ │ ├── print_icon.png │ │ │ ├── print_icon_small.png │ │ │ ├── red_circle_icon.png │ │ │ ├── resume_icon_small.png │ │ │ ├── save_icon.png │ │ │ ├── save_icon_small.png │ │ │ ├── scale_icon.png │ │ │ ├── scale_icon_small.png │ │ │ ├── start_icon.png │ │ │ ├── start_icon_small.png │ │ │ ├── submit_icon.png │ │ │ ├── submit_icon_small.png │ │ │ ├── windowlevel_icon.png │ │ │ ├── windowlevel_icon_small.png │ │ │ └── yellow_circle_icon.png │ │ ├── isodose_visualization.py │ │ ├── parse_gui_values.py │ │ ├── progress_bar_tests.py │ │ ├── review_loader.py │ │ └── save_create_preplan_tab.py │ ├── physics_review.py │ ├── qa_tests │ │ ├── __init__.py │ │ ├── analyze_logs │ │ │ ├── __init__.py │ │ │ └── retrieve_logs.py │ │ ├── test_beamset │ │ │ ├── __init__.py │ │ │ ├── check_beamset_approved.py │ │ │ ├── check_bolus_included.py │ │ │ ├── check_common_isocenter.py │ │ │ ├── check_control_point_spacing.py │ │ │ ├── check_couch_type.py │ │ │ ├── check_dose_grid.py │ │ │ ├── check_edw_field_size.py │ │ │ ├── check_edw_mu.py │ │ │ ├── check_emc_statistics.py │ │ │ ├── check_fraction_size.py │ │ │ ├── check_isocenter_clearance.py │ │ │ ├── check_mod_factor.py │ │ │ ├── check_no_fly.py │ │ │ ├── check_pacemaker.py │ │ │ ├── check_prv_status.py │ │ │ ├── check_slice_thickness.py │ │ │ ├── check_tomo_isocenter.py │ │ │ ├── check_tomo_mod_factor.py │ │ │ ├── check_transfer_approved.py │ │ │ ├── compare_rx_to_preplan.py │ │ │ ├── get_beamset_level_tests.py │ │ │ └── parse_beamset_selection.py │ │ ├── test_examination │ │ │ ├── __init__.py │ │ │ ├── check_axial_orientation.py │ │ │ ├── check_contour_gaps.py │ │ │ ├── check_couch_extent.py │ │ │ ├── check_exam_data.py │ │ │ ├── check_exam_date.py │ │ │ ├── check_fov_overlap_external.py │ │ │ ├── check_high_z.py │ │ │ ├── check_image_extent.py │ │ │ ├── check_localization.py │ │ │ ├── check_support_material.py │ │ │ ├── compare_exam_data_to_preplan.py │ │ │ ├── compare_patient_orientation_to_preplan.py │ │ │ ├── exam_review_tests.py │ │ │ ├── get_exam_level_tests.py │ │ │ ├── get_roi_list.py │ │ │ ├── get_si_extent.py │ │ │ └── get_targets_si_extent.py │ │ ├── test_plan │ │ │ ├── __init__.py │ │ │ ├── check_plan_approved.py │ │ │ ├── get_plan_level_tests.py │ │ │ └── parse_order_selection.py │ │ └── test_sandbox │ │ │ ├── __init__.py │ │ │ ├── check_c_arm_modulation.py │ │ │ ├── check_max_dose_point.py │ │ │ ├── compute_vmat_beam_properties.py │ │ │ └── get_sandbox_level_tests.py │ ├── review_definitions.py │ ├── testing │ │ ├── __init__.py │ │ ├── automated_review_testing.py │ │ ├── test_create_manual_tab.py │ │ └── test_doc_generate_physics_pdf.py │ └── utils │ │ ├── __init__.py │ │ ├── comment_to_clipboard.py │ │ ├── constants.py │ │ ├── contour_utilities.py │ │ ├── email_results.py │ │ ├── get_approval_info.py │ │ ├── get_machine.py │ │ ├── get_roi_list.py │ │ ├── get_roi_names_from_type.py │ │ ├── get_user_display_parameters.py │ │ ├── get_user_name.py │ │ ├── io_file_utils.py │ │ ├── match_roi_name.py │ │ ├── perform_automated_checks.py │ │ ├── protocol_loading.py │ │ ├── python_utilities.py │ │ ├── review_api_utils.py │ │ ├── review_api_versions.py │ │ ├── review_dispatcher.py │ │ └── subtract_roi_sources.py ├── StructureOperations.py ├── TomoExport.py ├── TrackingDSP_UWmod.py ├── UW_Definitions.py ├── UserInterface │ ├── ButtonList.py │ ├── CommonDialog.py │ ├── InputDialog.py │ ├── MatchDialog.py │ ├── MessageBox.py │ ├── PSInputDialog.py │ ├── PlanDialog.py │ ├── ProgressBar.py │ ├── ScriptStatus.py │ ├── TpoDialog.py │ └── __init__.py ├── WriteTpo.py ├── Xml_Out.py ├── api │ ├── api_beamsets.py │ ├── api_case.py │ ├── api_objectives.py │ ├── api_optimization_settings.py │ ├── api_qa_preparation.py │ ├── api_structures.py │ ├── api_ui.py │ ├── api_user_functions.py │ ├── api_utils.py │ └── dispatcher.py ├── autoplan_whole_brain.py └── report_logo.jpg ├── plan_setup ├── AddBolus.py ├── LoadBeamSet.py ├── RenameBeamsInBeamset.py ├── RenderingSettings.py ├── RoundJaws.py ├── SetTreatJawsCurrent.py └── dry_run.py ├── protocols ├── FakeTemplate.csv ├── QUANTEC.xml ├── Retired │ ├── UWBrainCNS.xml │ ├── UWLung.xml │ └── UWProstate.xml ├── SampleGoal.xml ├── TG-101.xml ├── TG-263.xml ├── Tests.xml ├── UW │ ├── AutoPlans │ │ ├── QUANTEC.xml │ │ ├── Tomo3DLarge.xml │ │ ├── Tomo3DStandard.xml │ │ ├── TomoTBI.xml │ │ ├── UWBrainPRDR.xml │ │ ├── UWBreastPRDR.xml │ │ ├── UW_Abdomen.xml │ │ ├── UW_AutoBreastSBRT.xml │ │ ├── UW_AutoGyn.xml │ │ ├── UW_AutoHeadNeck.xml │ │ ├── UW_AutoProstateSBRT.xml │ │ ├── UW_AutoSRS.xml │ │ ├── UW_AutoSpineSBRT.xml │ │ ├── UW_AutoWB.xml │ │ ├── UW_AutoWBHA.xml │ │ └── UW_VMAT_TBI.xml │ ├── Electron_Rendering.jpg │ ├── HyTEC.xml │ ├── QUANTEC.xml │ ├── TG101.xml │ ├── Testing │ │ ├── QUANTEC.xml │ │ ├── TomoPRDRTest.xml │ │ ├── TrialProtocol_Esophagus.xml │ │ ├── TrialProtocol_HeadNeck.xml │ │ ├── UWVMAT_Beamsets.xml │ │ ├── UW_AutoJawTrackingHeadNeck.xml │ │ ├── UW_AutoJawTracking_Breast.xml │ │ └── UW_AutoJawTracking_LungSBRT.xml │ ├── Thumbs.db │ ├── Tomo3DTest.xml │ ├── TomoTBI.xml │ ├── UWAbdomen.xml │ ├── UWAbdomen_SBRT.xml │ ├── UWAnorectal.xml │ ├── UWBrainCNS.xml │ ├── UWBrainCNS_SABR.xml │ ├── UWBrainCNS_SRS.xml │ ├── UWBrainPRDR.xml │ ├── UWBreast.xml │ ├── UWBreastPRDR.xml │ ├── UWBreastSBRT.xml │ ├── UWEsophagus.xml │ ├── UWGeneric.xml │ ├── UWHeadNeck.xml │ ├── UWLung.xml │ ├── UWLungSBRT.xml │ ├── UWPelvis.xml │ ├── UWPlanNaming.xml │ ├── UWProstate.xml │ ├── UWProstateSBRT.xml │ ├── UWSpineSBRT.xml │ ├── UW_WBHA.xml │ ├── beamset_templates │ │ ├── UWConformalArc.xml │ │ ├── UWStaticGantry.xml │ │ ├── UWTomoHelical.xml │ │ └── UWVMAT.xml │ ├── objectives │ │ ├── Ablative_Fractionation.xml │ │ ├── Standard_Fractionation.xml │ │ └── UWChestwall.xml │ └── readme.txt ├── UW_AutoPlanTesting │ ├── UW_LungSBRT.xml │ └── UW_ProstateNodes.xml ├── UW_HFP_BeamTemplates.csv ├── UW_HFS_3DSnS_BeamTemplates.csv ├── UW_HFS_BeamTemplates.csv ├── UW_prvs_and_logical.xml ├── dicom_verification.xml └── icd10cm_codes_2018.txt ├── qa_preparation ├── AutoDQA.py └── Tomo DQA.py ├── rayscripts_reqs_python3.txt ├── requirements.txt ├── requirements_17Jan2021.txt ├── structure_definition ├── AddSupportStructures.py ├── CreateExternalAlignRTSU.py ├── CreateExternalDIBHandFB.py ├── FiducialPlacement.py └── StructureMatching.py └── testing ├── AI_Contouring.py ├── AutoPlan_UpdateInterface.py ├── CasePreparation.py ├── Couch_check.py ├── DeformShoulders.py ├── ExportDosimetricData.py ├── Find_ROI_Info.py ├── Icons ├── blue_circle_icon.png ├── green_circle_icon.png └── red_circle_icon.png ├── ScreenGrab.py ├── Test_AutoPlan.py ├── Test_Plan_Checks.py ├── Test_VMAT_PRDR.py ├── UpgradeScripts.py ├── copy_clinical_goals_prep_review.py ├── copy_plan_to_new_ct.py ├── run_automated_review.py ├── run_dosimetry_safety_review.py └── run_physics_review.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | .idea/ 3 | *_tmp_* 4 | *.swp 5 | *.pyc 6 | certificates.xml 7 | colors.scheme.xml 8 | debugger.xml 9 | editor.* 10 | filetypes.xml 11 | find.xml 12 | vim* 13 | markdown.xml 14 | vcs.xml 15 | ui.lnf.xml 16 | github.xml 17 | diff.xml 18 | code.style.schemes.xml 19 | _windows/ 20 | _windows/* 21 | codestyles/ 22 | codestyles/* 23 | .vscode 24 | .vscode/* 25 | *.history 26 | .env 27 | .pylintrc 28 | Thumbs.db 29 | conf.py 30 | index.rst 31 | make.bat 32 | Makefile 33 | docs/ 34 | docs/* 35 | _build/ 36 | _static/ 37 | _templates/!/Script_Selector_py3_11.py 38 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "pydicom"] 2 | path = pydicom 3 | url = https://github.com/pydicom/pydicom 4 | [submodule "numpy"] 5 | path = numpy 6 | url = https://github.com/numpy/numpy.git 7 | [submodule "requests"] 8 | path = requests 9 | url = https://github.com/requests/requests.git 10 | [submodule "XlsxWriter"] 11 | path = XlsxWriter 12 | url = https://github.com/jmcnamara/XlsxWriter 13 | [submodule "pynetdicom3"] 14 | path = pynetdicom3 15 | url = https://github.com/pydicom/pynetdicom3 16 | [submodule "reportlab"] 17 | path = reportlab 18 | url = https://github.com/Distrotech/reportlab 19 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @rabayliss 2 | /general/ @rabayliss 3 | /plan_setup/ @rabayliss 4 | /library/ @rabayliss 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python scripts for RayStation treatment planning system 2 | 3 | by Mark Geurts and Adam Bayliss 4 |
Copyright © 2018, University of Wisconsin Board of Regents 5 | 6 | ## Description 7 | 8 | This repository contains a collection of python scripts that were developed 9 | for use with the [RayStation treatment planning system](https://www.raysearchlabs.com/raystation/). The application of these scripts varies from efficient creation of QA plans to automated planning and plan checks. This repository is actively being developed, so please see our [Projects](../../projects) page for more information on what is coming soon! 10 | 11 | ## Installation and Use 12 | 13 | A full guide to installation is avaiable on the the [Installation](../../wiki/dependencies) wiki page. This repository is set up so that only one "selector" script, [ScriptSelector.py](../../blob/master/ScriptSelector.py), actually needs to be imported into RayStation. Once imported, you can change how scripts are loaded through two variables specified at the top of the script: `local` and `module`. 14 | 15 | If `local` is left empty, each time the selector is called from RayStation it will use the [requests](http://docs.python-requests.org/en/master/) library to connect to this repository and prompt the user to select a branch to run scripts from (the [master](../../) branch contains all validated Production versions, but other feature development branches can be selected to test new features). After selecting a branch, the selector will download all scripts and prompt the user with a list to select one to execute. Alternatively, users can enter a local directory into the `local` variable that contains a local copy of this repository. Instead of downloading a fresh copy each time from GitHub, the local scripts will be displayed to the user for execution. 16 | 17 | The `module` variable is used to only show scripts from a particular directory in this repository. For example, setting `module = 'general'` will only show the [general scripts](general) to the user. In this way, multiple copies of [ScriptSelector.py](../../blob/master/ScriptSelector.py) can be loaded into RayStation with each pointing to a single focus of scripts. 18 | 19 | Packages not included as part of the standard library have been included as submodules in this repository, and should be installed from these references or [pip](https://packaging.python.org/tutorials/installing-packages/) during RayStation setup. A description of each package and how it is used is available on the [Dependencies](../../wiki/dependencies) wiki page. 20 | 21 | ## Documentation and Help 22 | 23 | With the exception of the library folder, each python script in this repository performs a standalone function in RayStation. See the [wiki](../../wiki) for information on what each script does, how it is used, and to see troubleshooting tips. If you experience issues with a script or have improvements to suggest please submit an [Issue](../../issues), otherwise contact the script author for questions or feedback. 24 | 25 | ## License 26 | 27 | Released under the GNU GPL v3.0 License. See the [LICENSE](LICENSE) file for further details. 28 | -------------------------------------------------------------------------------- /Script_Selector_py3_11.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/Script_Selector_py3_11.py -------------------------------------------------------------------------------- /development/Couch_Query.py: -------------------------------------------------------------------------------- 1 | """ Check Couch Position 2 | 3 | -Check for a patient 4 | -Load patient 5 | -Check for s frame 6 | -Load targets and check sup/inf extent of targets 7 | -If within boundaries of couch edge check beam angles 8 | -If angles posterior write out patient MRN 9 | 10 | This program is free software: you can redistribute it and/or modify it under 11 | the terms of the GNU General Public License as published by the Free Software 12 | Foundation, either version 3 of the License, or (at your option) any later 13 | version. 14 | 15 | This program is distributed in the hope that it will be useful, but WITHOUT 16 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 17 | FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 18 | 19 | You should have received a copy of the GNU General Public License along with 20 | this program. If not, see . 21 | """ 22 | import sys 23 | import os 24 | import csv 25 | import logging 26 | import connect 27 | 28 | def main(): 29 | # configs 30 | roi_name = 'S-frame' 31 | shift_to_couch_edge = 21.17 32 | db = connect.get_current('PatientDB') 33 | male_patients = db.QueryPatientInfo(Filter={'Gender':'Male'}) 34 | female_patients = db.QueryPatientInfo(Filter={'Gender':'Female'}) 35 | all_patients = male_patients + female_patients 36 | failed_patient_open =[] 37 | for p in all_patients: 38 | if p['PatientId'] != 'xx': 39 | continue 40 | try: 41 | patient = db.LoadPatient(PatientInfo=p) 42 | except: 43 | failed_patient_open.append(p) 44 | 45 | # Look at all cases 46 | for c in patient.Cases: 47 | try: 48 | c.PatientModel.RegionsOfInterest[roi_name] 49 | case_name = c.CaseName 50 | except SystemError: 51 | # Go on to next case 52 | continue 53 | # case_name has an instance of the structure we are looking for 54 | # now find the examination 55 | for s in c.PatientModel.StructureSets: 56 | try: 57 | if s.RoiGeometries[roi_name].HasContours(): 58 | # Get the center of the roi in question in Dicom Coordinates. 59 | center_roi = s.RoiGeometries[roi_name].GetCenterOfRoi() 60 | exam = s.OnExamination 61 | else: 62 | continue 63 | except SystemError: 64 | # roi_name is not in this exam 65 | continue 66 | c.PatientModel.CreatePoi(Examination=exam, 67 | Point={'x':center_roi.x, 68 | 'y':center_roi.y, 69 | 'z':center_roi.z}, 70 | Name='Bob') 71 | shifted = center_roi + shift_to_couch_edge 72 | c.PatientModel.CreatePoi(Examination=exam, 73 | Point={'x':center_roi.x, 74 | 'y':shifted, 75 | 'z':center_roi.z}, 76 | Name='Bob2') -------------------------------------------------------------------------------- /development/DITTO_APTR.py: -------------------------------------------------------------------------------- 1 | """ DICOM Integrity Tool - Aria Plan Transfer Review 2 | 3 | """ 4 | 5 | import sys 6 | from pathlib import Path 7 | 8 | ditto_path = Path(__file__).parent.parent / "library" / "DITTO" 9 | sys.path.insert(1, str(ditto_path)) 10 | import AriaRTPlanQR 11 | import DicomIntegrityTool_APTR 12 | import logging 13 | 14 | 15 | aria_file_location, rs_file_location, selected_rs = AriaRTPlanQR.aria_qr() 16 | 17 | logging.debug(f"Aria file location: {aria_file_location}") 18 | logging.debug(f"RayStation file location: {rs_file_location}") 19 | 20 | DicomIntegrityTool_APTR.run_dicom_integrity_tool( 21 | rs_file_location, 22 | aria_file_location, 23 | file_label1="RayStation", 24 | file_label2="Aria", 25 | ) 26 | -------------------------------------------------------------------------------- /development/get_implanted_device_metrics.py: -------------------------------------------------------------------------------- 1 | """ Implanted Medical Device Metrics 2 | 3 | """ 4 | 5 | __author__ = "Dustin Jacqmin" 6 | __contact__ = "djjacqmin_humanswillremovethis@wisc.edu" 7 | __date__ = "2023-06-19" 8 | __version__ = "0.1.0" 9 | __status__ = "Developmemt" 10 | __deprecated__ = False 11 | __reviewer__ = "" 12 | __reviewed__ = "" 13 | __raystation__ = "11B" 14 | __maintainer__ = "Dustin Jacqmin" 15 | __contact__ = "djjacqmin_humanswillremovethis@wisc.edu" 16 | __license__ = "GPLv3" 17 | __help__ = None 18 | __copyright__ = "Copyright (C) 2023, University of Wisconsin Board of Regents" 19 | 20 | from connect import get_current 21 | import PySimpleGUI as sg 22 | import sys 23 | from pathlib import Path 24 | 25 | ditto_path = Path(__file__).parent.parent / "library" 26 | sys.path.insert(1, str(ditto_path)) 27 | import ImplantedDeviceOperations as ido 28 | 29 | 30 | def main(): 31 | case = get_current("Case") 32 | plan = get_current("Plan") 33 | beam_set = get_current("BeamSet") 34 | examination = get_current("Examination") 35 | 36 | list_of_ROIs = [roi.Name for roi in case.PatientModel.RegionsOfInterest] 37 | if "Pacemaker" in list_of_ROIs: 38 | default_value = "Pacemaker" 39 | else: 40 | default_value = None 41 | 42 | list_of_plans = [p.Name for p in case.TreatmentPlans] 43 | default_plan = plan.Name 44 | 45 | layout = [ 46 | [ 47 | sg.Text("Select Plan:"), 48 | sg.Combo( 49 | values=list_of_plans, default_value=default_plan, key="-SELECTED PLAN-" 50 | ), 51 | ], 52 | [ 53 | sg.Text("Select Implanted Device ROI:"), 54 | sg.Combo( 55 | values=list_of_ROIs, default_value=default_value, key="-SELECTED ROI-" 56 | ), 57 | ], 58 | [ 59 | sg.Text("Maximum Dose (D0.03cc):"), 60 | sg.Text("Default Text", size=(20, 1), key="-MAX DOSE TEXT-"), 61 | ], 62 | [ 63 | sg.Text("Distance from Device to Nearest Collimated Field Edge:"), 64 | sg.Text("Default Text", size=(20, 1), key="-MIN DIST TEXT-"), 65 | ], 66 | [ 67 | sg.Text("Neutron-generating Beams?:"), 68 | sg.Text("Default Text", size=(20, 1), key="-NEUTRONS TEXT-"), 69 | ], 70 | [ 71 | sg.Text("Diagnostics:"), 72 | sg.Multiline("Default Text", size=(40, 6), key="-DIAG TEXT-"), 73 | ], 74 | [ 75 | sg.Button("Calculate"), 76 | sg.Cancel(), 77 | ], 78 | ] 79 | 80 | window = sg.Window( 81 | "Implanted Medical Device Metrics", 82 | layout, 83 | default_element_size=(40, 1), 84 | grab_anywhere=False, 85 | ) 86 | 87 | while True: 88 | event, values = window.read() 89 | 90 | if event == sg.WIN_CLOSED or event == "Cancel": 91 | break 92 | elif event == "Calculate": 93 | roi_name = values["-SELECTED ROI-"] 94 | 95 | max_dose = ido.get_device_D0_03cc( 96 | case=case, beam_set=beam_set, examination=examination, roi_name=roi_name 97 | ) 98 | window["-MAX DOSE TEXT-"].update(f"{max_dose:.3f} Gy") 99 | 100 | min_dist, dist_desc = ido.get_device_dist_to_field_edge( 101 | case=case, beam_set=beam_set, examination=examination, roi_name=roi_name 102 | ) 103 | window["-MIN DIST TEXT-"].update(f"{min_dist:.3f} cm") 104 | window["-DIAG TEXT-"].update(dist_desc) 105 | 106 | quality_df = ido.get_beamset_beam_quality(beam_set) 107 | if quality_df["Beam Has Neutrons"].any(): 108 | window["-NEUTRONS TEXT-"].update("Yes") 109 | else: 110 | window["-NEUTRONS TEXT-"].update("No") 111 | 112 | window.close() 113 | 114 | 115 | if __name__ == "__main__": 116 | main() 117 | -------------------------------------------------------------------------------- /general/create_goals.py: -------------------------------------------------------------------------------- 1 | """ Create Clinical Goals and Objectives 2 | 3 | Add clinical goals and objectives in RayStation given user supplied inputs 4 | At this time, we are looking in the UW protocols directory for a 5 | list of approved protocols 6 | 7 | We may want to extend this main function to a simple function which would potentially 8 | take the path as an argument. 9 | 10 | Script will ask user for a protocol and potentially an order. It will then find the 11 | doses that are to be used. If protocol defined doses exist and matches are found to 12 | target names it will load those first. 13 | 14 | Inputs:: 15 | None at this time 16 | 17 | Dependencies:: 18 | Note that protocols are assumed to have even priorities describing targets 19 | 20 | Validation Notes: 21 | Test Patient: MR# ZZUWQA_ScTest_05Jan2020, 22 | Name: Script_testing^Planning Structures Clinical Goals and Objectives 23 | -Anal_THI - sets clinical goals and objectives for AnoRectal-TPO 24 | Test Patient: 11B: MR# ZZUWQA_ScTest_06Jun2022_11B_GoalsObjectives, 25 | Name: Script_testing^Planning Structures Clinical Goals and Objectives 26 | -HN: Head and neck template 27 | -ChwL: Leftchestwall 28 | -Lung_SR: Knowledge based goals in Lung SBR template 29 | 30 | 31 | TODO: Add goal loop for secondary - unspecified target goals 32 | :versions 33 | 1.0.0 initial release supporting HN, Prostate, and lung (non-SBRT) 34 | 1.0.1 supporting SBRT, brain, and knowledge-based goals for RTOG-SBRT Lung 35 | 2.0.0 Adding the clinical objectives for IMRT 36 | 2.1.0 Python 3.6 conversion and update to RS 10A SP1 and converted to use the 37 | new create_goals_and_objectives function 38 | 2.1.1 Python 3.8 Conversion and 11B Update 39 | 40 | """ 41 | __author__ = 'Adam Bayliss' 42 | __contact__ = 'rabayliss@wisc.edu' 43 | __version__ = '2.1.1' 44 | __license__ = 'GPLv3' 45 | __help__ = 'https://github.com/wrssc/ray_scripts/wiki/CreateGoals' 46 | __copyright__ = 'Copyright (C) 2022, University of Wisconsin Board of Regents' 47 | 48 | import logging 49 | import Objectives 50 | import GeneralOperations 51 | 52 | 53 | def main(): 54 | 55 | # Get current patient, case, exam, and plan 56 | # note that the interpreter handles a missing plan as an Exception 57 | patient = GeneralOperations.find_scope(level='Patient') 58 | case = GeneralOperations.find_scope(level='Case') 59 | exam = GeneralOperations.find_scope(level='Examination') 60 | plan = GeneralOperations.find_scope(level='Plan') 61 | beamset = GeneralOperations.find_scope(level='BeamSet') 62 | ui = GeneralOperations.find_scope(level='ui') 63 | # TODO put in more sophisticated InvalidOperationException Catch here. 64 | try: 65 | ui.TitleBar.MenuItem['Plan Optimization'].Button_Plan_Optimization.Click() 66 | except: 67 | logging.debug('Unable to change viewing windows') 68 | 69 | error_message = Objectives.add_goals_and_objectives_from_protocol( 70 | case=case, plan=plan, beamset=beamset, exam=exam 71 | ) 72 | if not error_message: 73 | logging.info('Clinical goals/objectives added successfully') 74 | else: 75 | output_message ='' 76 | for e in error_message: 77 | output_message.append('{} '.format(e)) 78 | logging.warning('Error adding clinical goals. {}'.format(output_message)) 79 | 80 | 81 | 82 | if __name__ == '__main__': 83 | main() 84 | -------------------------------------------------------------------------------- /general/electron_qa_plan.py: -------------------------------------------------------------------------------- 1 | """ Create Electron QA Plan 2 | 3 | Creates a QA plan for an electron beam onto a water phantom in order to compare RayStation vs. 4 | Mobius MU. 5 | 6 | This script will create a QA plan in the Plan preparation workspace, using a 50cm cube water 7 | phantom. This phantom has POIs spaced every 1mm on the CAX. The patient-specific cutout will 8 | be used but the couch and and gantry angles will be set to zero. The default SSD is 105cm so 9 | the user will need to manually edit the QA plan for a different SSD. The dose grid resolution 10 | is 2.5mm and 500K histories will be used. 11 | 12 | Validation Notes: The script relies on the status of the QA phantom being verifiable as 13 | read-only.Due to a programming error in RayStation 6.0 this status validation 14 | fails when QA plans are generated as part of a python script. Therefore, 15 | unless the user has, without a script, validated the read-only status of the 16 | plan by opening a "dummy" QA plan, the script will fail to execute. 17 | 18 | Version Notes: 1.0.0 Original 19 | 1.0.1 Added a relabeling of the plan to include the Beamset name to avoid 20 | ambiguity when the "Mobius MU" plan name is taken. Also changed the 21 | active phantom to 50cmCube for the clinical institution. Added 22 | autorecompute and deleted incorrect plan recomputation syntax. 23 | 1.0.2 Added main function call, eliminated blanket connect import, 24 | autoclick QA prep 25 | 1.1.0 Update to Rs 10A 26 | 27 | This program is free software: you can redistribute it and/or modify it under 28 | the terms of the GNU General Public License as published by the Free Software 29 | Foundation, either version 3 of the License, or (at your option) any later 30 | version. 31 | 32 | This program is distributed in the hope that it will be useful, but WITHOUT 33 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 34 | FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 35 | 36 | You should have received a copy of the GNU General Public License along with 37 | this program. If not, see . 38 | """ 39 | 40 | __author__ = 'Jessie Huang-Vredevoogd' 41 | __contact__ = 'jyhuang4@wisc.edu' 42 | __date__ = '2021-01-17' 43 | __version__ = '1.0.2' 44 | __status__ = 'Production' 45 | __deprecated__ = False 46 | __reviewer__ = 'Adam Bayliss' 47 | __reviewed__ = '2018-01-26' 48 | __raystation__ = '10A SP1' 49 | __maintainer__ = 'Huang-Vredevoogd and Bayliss' 50 | __email__ = 'jyhuang4@wisc.edu' 51 | __license__ = 'GPLv3' 52 | __copyright__ = 'Copyright (C) 2021, University of Wisconsin Board of Regents' 53 | __credits__ = [] 54 | 55 | 56 | def main(): 57 | import connect 58 | import UserInterface 59 | import sys 60 | 61 | try: 62 | beam_set = connect.get_current("BeamSet") 63 | except Exception: 64 | UserInterface.WarningBox('This script requires a Beam Set to be loaded') 65 | sys.exit('This script requires a Beam Set to be loaded') 66 | 67 | QA_Plan_Name = beam_set.DicomPlanLabel + "QA" 68 | # Click magic 69 | ui = connect.get_current('ui') 70 | ui.TitleBar.Navigation.MenuItem['QA preparation'].Button_QA_preparation.Click() 71 | 72 | try: 73 | beam_set.CreateQAPlan( 74 | PhantomName="50cmCube", 75 | PhantomId="WaterPhantom", 76 | QAPlanName=QA_Plan_Name, 77 | IsoCenter={'x': 0, 'y': -30, 'z': 25}, 78 | DoseGrid={'x': 0.25, 'y': 0.25, 'z': 0.25}, 79 | GantryAngle=0, 80 | CollimatorAngle=None, 81 | CouchRotationAngle=0, 82 | ComputeDoseWhenPlanIsCreated=True, 83 | NumberOfMonteCarloHistories=500000) 84 | except Exception as e: 85 | UserInterface.WarningBox('QA Plan failed to create: {}'.format(e)) 86 | sys.exit('QA Plan failed to create {}'.format(e)) 87 | 88 | 89 | if __name__ == '__main__': 90 | main() 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /general/planning_structures.py: -------------------------------------------------------------------------------- 1 | """ Generate Planning Structures 2 | 3 | This script is designed to help you make planning structures. Prior to starting you should determine: 4 | All structures to be treated, and their doses 5 | All structures with priority 1 goals (they are going to be selected for UnderDose) 6 | All structures where hot-spots are undesirable but underdosing is not desired. They will be placed in 7 | the UniformDose ROI. 8 | 9 | 10 | Raystation script to make structures used for planning. 11 | 12 | Note: 13 | Using the Standard InputDialog 14 | We have several calls 15 | The first will determine the target doses and whether we are uniform or underdosing 16 | Based on those responses: 17 | Select and Approve underdose selections 18 | Select and Approve uniform dose selections 19 | The second non-optional call prompts the user to use: 20 | -Target-specific rings 21 | -Specify desired standoffs in the rings closest to the target 22 | 23 | Inputs: 24 | None, though eventually the common uniform and underdose should be dumped into xml files 25 | and stored in protocols 26 | 27 | Usage: 28 | 29 | Version History: 30 | 1.0.1: Hot fix to repair inconsistency when underdose is not used but uniform dose is. 31 | 1.0.2: Adding "inner air" as an optional feature 32 | 1.0.3 Hot fix to repair error in definition of sOTVu: Currently taking union of PTV and 33 | not_OTV - should be intersection. 34 | 1.0.4 Bug fix for upgrade to RS 8 - replaced the toggling of the exclude from export with 35 | the required method. 36 | 1.0.4b Save the user mapping for this structure set as an xml file to be loaded by create_goals 37 | 1.0.5 Exclude InnerAir and FOV from Export, add IGRT Alignment Structure 38 | 1.0.6 Added the Normal_1cm structure to the list 39 | 40 | 1.1.0 Updated to python3 41 | 42 | 43 | This program is free software: you can redistribute it and/or modify it under 44 | the terms of the GNU General Public License as published by the Free Software 45 | Foundation, either version 3 of the License, or (at your option) any later version. 46 | 47 | This program is distributed in the hope that it will be useful, but WITHOUT 48 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 49 | FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 50 | 51 | You should have received a copy of the GNU General Public License along with 52 | this program. If not, see . 53 | """ 54 | from pickle import FALSE 55 | 56 | # from typing import List, Any 57 | 58 | __author__ = 'Adam Bayliss' 59 | __contact__ = 'rabayliss@wisc.edu' 60 | __version__ = '1.1.0' 61 | __license__ = 'GPLv3' 62 | __help__ = 'https://github.com/wrrsc/ray_scripts/wiki/Planning_Structures' 63 | __copyright__ = 'Copyright (C) 2021, University of Wisconsin Board of Regents' 64 | 65 | import StructureOperations 66 | 67 | 68 | def main(): 69 | StructureOperations.planning_structures() 70 | 71 | 72 | if __name__ == '__main__': 73 | main() 74 | -------------------------------------------------------------------------------- /general/run_dosimetry_safety_review.py: -------------------------------------------------------------------------------- 1 | """ Final Dosimetry Safety Review 2 | --version 1.0.3--Run basic plan integrity checks and parse the log file. 3 | 4 | """ 5 | 6 | import sys 7 | from pathlib import Path 8 | # This function is pointed to the PlanReview folder where the real magic happens 9 | plan_review_path = Path(__file__).parent.parent / "library" / "PlanReview" 10 | sys.path.insert(1, str(plan_review_path)) 11 | from PlanReview.physics_review import physics_review 12 | 13 | # Similarly, point to the DITTO folder where more magic happens 14 | ditto_path = Path(__file__).parent.parent / "library" / "DITTO" 15 | sys.path.insert(1, str(ditto_path)) 16 | 17 | physics_review(do_physics_review=True, review_type='Dosimetry') 18 | -------------------------------------------------------------------------------- /helper_scripts/CompareContours.py: -------------------------------------------------------------------------------- 1 | """ Compare Contours 2 | Compare two contours selected by the user and return the 3 | Dice Coefficient, Precision, Sensitivity, Specificity, Mean DTA, Max DTA 4 | 5 | Version: 6 | 0.0 Compares two contours using the RayStation method Test 7 | 1.0 Release 8 | 9 | This program is free software: you can redistribute it and/or modify it under 10 | the terms of the GNU General Public License as published by the Free Software 11 | Foundation, either version 3 of the License, or (at your option) any later 12 | version. 13 | 14 | This program is distributed in the hope that it will be useful, but WITHOUT 15 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 16 | FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License along with 19 | this program. If not, see . 20 | """ 21 | 22 | __author__ = 'Adam Bayliss' 23 | __contact__ = 'rabayliss@wisc.edu' 24 | __date__ = '04-Jun-2021' 25 | __version__ = '1.0.0' 26 | __status__ = 'Production' 27 | __deprecated__ = False 28 | __reviewer__ = '' 29 | __reviewed__ = '' 30 | __raystation__ = '10.A' 31 | __maintainer__ = 'One maintainer' 32 | __email__ = 'rabayliss@wisc.edu' 33 | __license__ = 'GPLv3' 34 | __copyright__ = 'Copyright (C) 2018, University of Wisconsin Board of Regents' 35 | __help__ = '' 36 | __credits__ = [] 37 | 38 | import logging 39 | import sys 40 | from collections import namedtuple 41 | import connect 42 | import UserInterface 43 | import StructureOperations 44 | import GeneralOperations 45 | 46 | 47 | 48 | 49 | def main(): 50 | # 51 | # Initialize return variable 52 | Pd = namedtuple('Pd', ['error','db', 'case', 'patient', 'exam', 'plan', 'beamset']) 53 | # Get current patient, case, exam 54 | pd = Pd(error = [], 55 | patient = GeneralOperations.find_scope(level='Patient'), 56 | case = GeneralOperations.find_scope(level='Case'), 57 | exam = GeneralOperations.find_scope(level='Examination'), 58 | db = GeneralOperations.find_scope(level='PatientDB'), 59 | plan = None, 60 | beamset = None) 61 | defined_rois = [] 62 | for r in pd.case.PatientModel.RegionsOfInterest: 63 | defined_rois.append(r.Name) 64 | rois_1 = defined_rois.copy() 65 | rois_2 = defined_rois.copy() 66 | 67 | # Declare the dialog 68 | dialog = UserInterface.InputDialog( 69 | inputs={'1': 'Select Roi 1', '2':'Select Roi 2'}, 70 | title='Roi Compare', 71 | datatype={'1': 'combo', '2':'combo'}, 72 | initial={}, 73 | options={'1': rois_1, '2':rois_2}, 74 | required=[]) 75 | # Launch the dialog 76 | response = dialog.show() 77 | # Close on cancel 78 | if response == {}: 79 | logging.info('Autoload cancelled by user. Protocol not selected') 80 | sys.exit('Protocol not selected. Process cancelled.') 81 | stats = StructureOperations.compute_comparison_statistics( 82 | patient = pd.patient, 83 | case = pd.case, 84 | exam = pd.exam, 85 | rois_a = [dialog.values['1']], 86 | rois_b = [dialog.values['2']], 87 | compute_distances = True 88 | ) 89 | user_message = "" 90 | for k, v in stats.items(): 91 | user_message += "{} : {}\n".format(k,v) 92 | user_message += "{}".format("""\n 93 | DiceSimilarityCoefficient: | ROIA intersect ROIB | / | ROIA | + | ROIB |\n 94 | Precision: ROIA intersect ROIB | / | ROIA union ROIB |\n 95 | Sensitivity: 1 - | ROIB not ROIA | / | ROIA |\n 96 | Specificity: | ROIA intersect ROIB | / | ROIA |\n 97 | MeanDistanceToAgreement: Mean distance to agreement computed 98 | from average of minimum voxel distances\n 99 | MaxDistanceToAgreement: Maximum of minimum distances\n 100 | between voxels (Hausdorff)""") 101 | UserInterface.MessageBox(user_message) 102 | 103 | if __name__ == '__main__': 104 | main() 105 | -------------------------------------------------------------------------------- /helper_scripts/DITTO_APTR.py: -------------------------------------------------------------------------------- 1 | """ DICOM Integrity Tool - Aria Plan Transfer Review 2 | 3 | """ 4 | 5 | import sys 6 | from pathlib import Path 7 | 8 | ditto_path = Path(__file__).parent.parent / "library" / "DITTO" 9 | sys.path.insert(1, str(ditto_path)) 10 | # import DITTO.AriaRTPlanQR as AriaRTPlanQR 11 | # import DITTO.DicomIntegrityTool_APTR as DicomIntegrityTool_APTR 12 | import AriaRTPlanQR as AriaRTPlanQR 13 | import DicomIntegrityTool_APTR as DicomIntegrityTool_APTR 14 | import logging 15 | 16 | 17 | aria_file_location, rs_file_location, selected_rs = AriaRTPlanQR.aria_qr() 18 | 19 | logging.debug(f"Aria file location: {aria_file_location}") 20 | logging.debug(f"RayStation file location: {rs_file_location}") 21 | 22 | DicomIntegrityTool_APTR.run_dicom_integrity_tool( 23 | rs_file_location, 24 | aria_file_location, 25 | file_label1="RayStation", 26 | file_label2="Aria", 27 | ) 28 | -------------------------------------------------------------------------------- /helper_scripts/DisplayTomoSinogram.py: -------------------------------------------------------------------------------- 1 | """ Display TomoTherapy Sinogram 2 | Use matplotlin to show the sinogram of the current TomoPlan 3 | Only works for TomoHelical. 4 | 5 | Version: 6 | 0.0 Testing 7 | 8 | This program is free software: you can redistribute it and/or modify it under 9 | the terms of the GNU General Public License as published by the Free Software 10 | Foundation, either version 3 of the License, or (at your option) any later 11 | version. 12 | 13 | This program is distributed in the hope that it will be useful, but WITHOUT 14 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 | FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License along with 18 | this program. If not, see . 19 | """ 20 | 21 | __author__ = 'Adam Bayliss' 22 | __contact__ = 'rabayliss@wisc.edu' 23 | __date__ = '12-Dec-2021' 24 | __version__ = '0.0.0' 25 | __status__ = 'Testing' 26 | __deprecated__ = False 27 | __reviewer__ = '' 28 | __reviewed__ = '' 29 | __raystation__ = '10.A' 30 | __maintainer__ = 'One maintainer' 31 | __email__ = 'rabayliss@wisc.edu' 32 | __license__ = 'GPLv3' 33 | __copyright__ = 'Copyright (C) 2021, University of Wisconsin Board of Regents' 34 | __help__ = '' 35 | __credits__ = [] 36 | 37 | import logging 38 | import sys 39 | import matplotlib.pyplot as plt 40 | from matplotlib.pyplot import figure 41 | from collections import namedtuple 42 | import connect 43 | #import UserInterface 44 | import BeamOperations 45 | import GeneralOperations 46 | 47 | def main(): 48 | # 49 | # Initialize return variable 50 | PatientData = namedtuple('Pd', ['error','db', 'case', 'patient', 'exam', 'plan', 'beamset']) 51 | # Get current patient, case, exam 52 | ptdat =PatientData(error = [], 53 | patient = GeneralOperations.find_scope(level='Patient'), 54 | case = GeneralOperations.find_scope(level='Case'), 55 | exam = GeneralOperations.find_scope(level='Examination'), 56 | db = GeneralOperations.find_scope(level='PatientDB'), 57 | plan = GeneralOperations.find_scope(level='Plan'), 58 | beamset = GeneralOperations.find_scope(level='BeamSet'), 59 | ) 60 | if ptdat.beamset.DeliveryTechnique == 'TomoHelical': 61 | beam_params = BeamOperations.gather_tomo_beam_params(ptdat.beamset) 62 | else: 63 | sys.exit('Unable to show a sinogram for this kind of plan') 64 | sino_i = beam_params.loc[0].sinogram * beam_params.loc[0].proj_time *1000. #ms per s 65 | num_proj_i = beam_params.iloc[0].sinogram.shape[0] 66 | num_mlc_i = beam_params.iloc[0].sinogram.shape[1] 67 | extent_i = [0, num_mlc_i, 0 , num_proj_i] 68 | plt.style.use('grayscale') 69 | plt.imshow(1-sino_i, interpolation='none',extent=extent_i) 70 | plt.show() 71 | 72 | if __name__ == '__main__': 73 | main() 74 | -------------------------------------------------------------------------------- /helper_scripts/beavis_run_physics_review_script.py: -------------------------------------------------------------------------------- 1 | """ BEAmset reVIew Script 2 | --version 1.0.3--Run basic plan integrity checks and parse the log file. 3 | 4 | """ 5 | 6 | import sys 7 | from pathlib import Path 8 | # This function is pointed to the PlanReview folder where the real magic happens 9 | plan_review_path = Path(__file__).parent.parent / "library" / "PlanReview" 10 | sys.path.insert(1, str(plan_review_path)) 11 | from PlanReview.physics_review import physics_review 12 | 13 | # Similarly, point to the DITTO folder where more magic happens 14 | ditto_path = Path(__file__).parent.parent / "library" / "DITTO" 15 | sys.path.insert(1, str(ditto_path)) 16 | 17 | physics_review(do_physics_review=True) 18 | -------------------------------------------------------------------------------- /helper_scripts/run_dosimetry_safety_review.py: -------------------------------------------------------------------------------- 1 | """ Dosimetry Safety Review 2 | --version 1.0.3--Run basic plan integrity checks and parse the log file. 3 | 4 | """ 5 | 6 | import sys 7 | from pathlib import Path 8 | # This function is pointed to the PlanReview folder where the real magic happens 9 | plan_review_path = Path(__file__).parent.parent / "library" / "PlanReview" 10 | sys.path.insert(1, str(plan_review_path)) 11 | from PlanReview.physics_review import physics_review 12 | 13 | # Similarly, point to the DITTO folder where more magic happens 14 | ditto_path = Path(__file__).parent.parent / "library" / "DITTO" 15 | sys.path.insert(1, str(ditto_path)) 16 | 17 | physics_review(do_physics_review=True, review_type='Dosimetry') 18 | -------------------------------------------------------------------------------- /helper_scripts/test_modules.py: -------------------------------------------------------------------------------- 1 | """Test modules and dependencies for RS""" 2 | 3 | import pandas 4 | import scipy 5 | import requests 6 | import numpy 7 | import clr 8 | import pydicom 9 | import reportlab 10 | import xlsxwriter 11 | #import scikit-learn 12 | 13 | help('modules') 14 | #help('modules scipy') 15 | #help('modules numpy') 16 | #help('modules pydicom') 17 | #help('modules requests') 18 | #help('modules pandas') 19 | 20 | #print '\n' 21 | #print 'scipy package location: ', (scipy.__file__) -------------------------------------------------------------------------------- /ide.general.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /legacy/FilterLeaves.py: -------------------------------------------------------------------------------- 1 | """ Fix Dynamic Leaf Gaps 2 | 3 | Loop through the beams in this beamset. Look for leaves that are closed more than 4 | the minimum leaf gap for the commissioned machine. Increment the gap until the 5 | minimum leaf gap condition is met. 6 | 7 | Version History: 8 | 1.0.0 Released in 8.0 b 9 | 1.1.0 Released in 10A SP1 10 | 11 | This program is free software: you can redistribute it and/or modify it under 12 | the terms of the GNU General Public License as published by the Free Software 13 | Foundation, either version 3 of the License, or (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, but WITHOUT 16 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 17 | FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 18 | 19 | You should have received a copy of the GNU General Public License along with 20 | this program. If not, see . 21 | """ 22 | 23 | __author__ = 'Adam Bayliss' 24 | __contact__ = 'rabayliss@wisc.edu' 25 | __date__ = '2021-01-17' 26 | 27 | __version__ = '1.1.0' 28 | __status__ = 'Production' 29 | __deprecated__ = False 30 | __reviewer__ = 'Adam Bayliss' 31 | 32 | __reviewed__ = '' 33 | __raystation__ = '10.A' 34 | __maintainer__ = 'Adam Bayliss' 35 | 36 | __email__ = 'rabayliss@wisc.edu' 37 | __license__ = 'GPLv3' 38 | __copyright__ = 'Copyright (C) 2018, University of Wisconsin Board of Regents' 39 | 40 | 41 | import GeneralOperations 42 | import BeamOperations 43 | import logging 44 | 45 | 46 | def main(): 47 | beamset = GeneralOperations.find_scope(level='BeamSet') 48 | for b in beamset.Beams: 49 | error = BeamOperations.repair_leaf_gap(b) 50 | if error is not None: 51 | logging.debug(error) 52 | else: 53 | logging.debug('Beam {} filtered'.format(b.Name)) 54 | 55 | if __name__ == '__main__': 56 | main() -------------------------------------------------------------------------------- /legacy/GenerateGoalTemplates.py: -------------------------------------------------------------------------------- 1 | '''Generate Goal Templates 2 | This function is a stub for what will eventually be all goals relevant to 3 | standard TPO's 4 | ''' 5 | import sys 6 | import os 7 | import xml.etree.ElementTree 8 | import connect 9 | import Goals 10 | 11 | def main(): 12 | protocol = r'../protocols/SampleGoal.xml' 13 | #protocol_file = r'SampleGoal.xml' 14 | protocol_name = os.path.join(os.path.dirname(__file__), protocol) 15 | 16 | tree = xml.etree.ElementTree.parse(protocol_name) 17 | for g in tree.findall('//goals/roi'): 18 | print('Adding goal ' + Goals.print_goal(g, 'xml')) 19 | Goals.add_goal(g, connect.get_current('Plan')) 20 | ui = connect.get_current('ui') 21 | ui.TitleBar.MenuItem['Plan Optimization'].Click() 22 | ui.TitleBar.MenuItem['Plan Optimization'].Popup.MenuItem['Plan Optimization'].Click() 23 | ui.Workspace.TabControl['DVH'].TabItem['Clinical Goals'].Select() 24 | #ui.ToolPanel.TabItem._Scripting.Select() 25 | 26 | if __name__ == 'main': 27 | main() 28 | -------------------------------------------------------------------------------- /legacy/SelectCaseWhenMultipleCases.py: -------------------------------------------------------------------------------- 1 | import wpf 2 | from connect import * 3 | from System.Windows import * 4 | from System.Windows.Controls import * 5 | 6 | def open_case( name ): 7 | "Opens the case having given name." 8 | 9 | if name is None or len(name) <= 0: 10 | raise Exception('No case has been selected.') 11 | 12 | case = [c for c in cases if name == c.CaseName] 13 | if len(case) > 1: 14 | raise Exception('Name is not giving a unique identification.') 15 | 16 | case[0].SetCurrent() 17 | 18 | class OpenCaseWindow(Window): 19 | 20 | def __init__(self): 21 | self.Title = "Open case" 22 | self.Width = 200 23 | self.Height = 400 24 | 25 | scroll = ScrollViewer() 26 | scroll.HorizontalAlignment = HorizontalAlignment.Stretch 27 | scroll.VerticalAlignment = VerticalAlignment.Stretch 28 | scroll.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto 29 | self.Content = scroll 30 | 31 | stack = StackPanel() 32 | scroll.Content = stack 33 | 34 | self.box = ListBox() 35 | self.box.ItemsSource = [case.CaseName for case in cases] 36 | stack.Children.Add(self.box) 37 | 38 | button = Button() 39 | button.Width = 50 40 | button.Margin = Thickness(10) 41 | button.Content = 'OK' 42 | button.Click += self.click_ok 43 | stack.Children.Add(button) 44 | 45 | def click_ok(self, sender, args): 46 | try: 47 | open_case(self.box.SelectedItem) 48 | except Exception as error: 49 | MessageBox.Show('Error: {0}'.format(error)) 50 | else: 51 | self.Close() 52 | 53 | 54 | if __name__ == "__main__": 55 | patient = get_current('Patient') 56 | cases = patient.Cases 57 | 58 | app = Application() 59 | app.Run(OpenCaseWindow()) 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /legacy/UpdateTomoDQAPlan.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pydicom 3 | from pydicom.tag import Tag 4 | 5 | # extract dicom file and gantry period from directory files 6 | filepath = 'W:\\rsconvert\\' 7 | hlist = os.listdir(filepath) 8 | flist = filter(lambda x: '.dcm' in x, hlist) 9 | filename = flist[0] 10 | GPlist = filter(lambda x: '.txt' in x, hlist) 11 | GPval = GPlist[0][3:8]+' ' 12 | 13 | # format and set tag to change 14 | t1 = Tag('300d1040') 15 | 16 | # read file 17 | ds = pydicom.read_file(filepath+filename) 18 | 19 | # add attribute to beam sequence 20 | ds.BeamSequence[0].add_new(t1, 'UN', GPval) 21 | 22 | # output file 23 | ds.save_as(filepath+'new_'+filename, write_like_original=True) 24 | -------------------------------------------------------------------------------- /legacy/VerifyDicomExport.py: -------------------------------------------------------------------------------- 1 | """ Verification of DICOM Export 2 | 3 | You must have TPL_000 open for this to work as this iterates through all 4 | patient orientations 5 | 6 | Scope: Requires RayStation "Case" and "Examination" to be loaded. They are 7 | passed to the function as an argument 8 | 9 | Example Usage: 10 | 11 | This program is free software: you can redistribute it and/or modify it under 12 | the terms of the GNU General Public License as published by the Free Software 13 | Foundation, either version 3 of the License, or (at your option) any later 14 | version. 15 | 16 | This program is distributed in the hope that it will be useful, but WITHOUT 17 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 18 | FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License along with 21 | this program. If not, see . 22 | """ 23 | 24 | __author__ = 'Adam Bayliss' 25 | __contact__ = 'rabayliss@wisc.edu' 26 | __date__ = '2018-Apr-10' 27 | __version__ = '1.0.2' 28 | __status__ = 'Development' 29 | __deprecated__ = False 30 | __reviewer__ = 'Someone else' 31 | __reviewed__ = 'YYYY-MM-DD' 32 | __raystation__ = '7.0.0' 33 | __maintainer__ = 'One maintainer' 34 | __email__ = 'rabayliss@wisc.edu' 35 | __license__ = 'GPLv3' 36 | __help__ = 'https://github.com/mwgeurts/ray_scripts/wiki/xxxxx' 37 | __copyright__ = 'Copyright (C) 2018, University of Wisconsin Board of Regents' 38 | __credits__ = [''] 39 | 40 | import connect 41 | import xml.etree.ElementTree 42 | 43 | # Define the protocol XML directory 44 | protocol_folder = r'../protocols' 45 | 46 | tree = xml.etree.ElementTree.parse('dicom_verification.xml') 47 | case = connect.get_current("Case") 48 | for p in tree.findall('//plan'): 49 | plan_name = p.find('plan_name') 50 | plan_comment = p.find('plan_comment') 51 | examination_name = p.find('examination_name') 52 | patient_position = p.find('patient_position') 53 | plan = case.AddNewPlan(PlanName=plan_name, 54 | PlannedBy="", 55 | Comment=plan_comment, 56 | ExaminationName=examination_name, 57 | AllowDuplicateNames=False) 58 | for b in tree.findall('//plan/beam_set'): 59 | beam_set_name = b.find('beam_set_name') 60 | machine_name = b.find('machine_name') 61 | modality = b.find ('modality') 62 | treatment_technique = b.find ('treatment_technique') 63 | patient_position = b.find('patient_position') 64 | number_of_fractions = b.find('number_of_fractions') 65 | beam_set_comment = b.find('beam_set_comment') 66 | plan.AddNewBeamSet( 67 | Name=beam_set_name, 68 | ExaminationName=examination_name, 69 | MachineName=machine_name, 70 | Modality=modality, 71 | TreatmentTechnique=treatment_technique, 72 | PatientPosition=patient_position, 73 | NumberOfFractions=number_of_fractions, 74 | CreateSetupBeams=True, 75 | UseLocalizationPointAsSetupIsocenter=False, 76 | Comment=beam_set_comment, 77 | RbeModelReference=None, 78 | EnableDynamicTrackingForVero=False, 79 | NewDoseSpecificationPointNames=[], 80 | NewDoseSpecificationPoints=[], 81 | RespiratoryMotionCompensationTechnique="Disabled", 82 | RespiratorySignalSource="Disabled") 83 | 84 | 85 | patient.Save() 86 | #plan = case.TreatmentPlans[plan_name] 87 | #plan.SetCurrent() 88 | #connect.get_current('Plan') 89 | 90 | #plan.SetDefaultDoseGrid(VoxelSize={'x': 0.2, 'y': 0.2, 'z': 0.2}) 91 | 92 | #retval_1.AddDosePrescriptionToRoi(RoiName="PTV1", DoseVolume=95, PrescriptionType="DoseAtVolume", DoseValue=2500, 93 | # RelativePrescriptionLevel=1, AutoScaleDose=True) 94 | 95 | -------------------------------------------------------------------------------- /legacy/test_pydicom.py: -------------------------------------------------------------------------------- 1 | """ Testing the loading of pydicom 2 | 3 | """ 4 | 5 | import pydicom 6 | import pynetdicom3 7 | 8 | ae = pynetdicom3.AE(scu_sop_class=['1.2.840.10008.1.1'], 9 | ae_title=local_AET, 10 | port=local_port) 11 | -------------------------------------------------------------------------------- /legacy/test_userinterface.py: -------------------------------------------------------------------------------- 1 | """ Test the UserInterface Module 2 | Opens a simple dialog to test the user-interface library""" 3 | 4 | import UserInterface 5 | import sys 6 | 7 | 8 | def test_simple_interface(check_list=True): 9 | if check_list: 10 | inputs = ['1', '2', '3', '4'] 11 | color_inputs = ['Red', 'Blue', 'Green', 'Yellow'] 12 | quest_inputs = ['To buy an argument.', 'To find the Holy Grail.', 'To beat Confucious in a foot race.'] 13 | name_inputs = ['Arthur', 'Tarquin Fin-tim-lin-bin-whin-bim-lim-bus-stop-Ftang-Ftang-Ole-Biscuitbarrel'] 14 | sample_dialog = UserInterface.InputDialog( 15 | title='Sample Interface', 16 | inputs={ 17 | inputs[0]: 'What is your favorite color?', 18 | inputs[1]: 'What is your quest?', 19 | inputs[2]: 'What is your Name?', 20 | inputs[3]: 'What Is the Airspeed Velocity of an Unladen Swallow?'}, 21 | datatype={ 22 | inputs[0]: 'check', 23 | inputs[1]: 'combo', 24 | inputs[2]: 'combo'}, 25 | initial={color_inputs[0]: '40'}, 26 | options={ 27 | inputs[0]: color_inputs, 28 | inputs[1]: quest_inputs, 29 | inputs[2]: name_inputs}, 30 | required=[inputs[0], inputs[1]]) 31 | options_response = sample_dialog.show() 32 | 33 | if options_response == {}: 34 | sys.exit('Selection of planning structure options was cancelled') 35 | 36 | message = '' 37 | for k, v in sample_dialog.values.items(): 38 | message += k + ' : ' + str(v) + '\n' 39 | 40 | UserInterface.MessageBox(message) 41 | 42 | 43 | def main(): 44 | test_simple_interface(check_list=True) 45 | 46 | 47 | if __name__ == '__main__': 48 | main() 49 | -------------------------------------------------------------------------------- /library/DITTO/DicomTagExpectedResults.md: -------------------------------------------------------------------------------- 1 | ### AccessionNumber 2 | Typically blank from both RayStation and Aria, and will match. 3 | 4 | ### ApprovalStatus 5 | Both are expected to have status "APPROVAL" (what about when a plan is changed in Aria?) 6 | 7 | ### BeamSequence : BeamDescription 8 | Expect perfect match 9 | 10 | ### BeamSequence : BeamLimitingDeviceSequence : NumberOfLeafJawPairs 11 | Expect perfect match 12 | 13 | ### BeamSequence : BeamLimitingDeviceSequence : RTBeamLimitingDeviceType 14 | Expect perfect match 15 | 16 | ### BeamSequence : BeamName 17 | Expect perfect match 18 | 19 | ### BeamSequence : BeamNumber 20 | Expect perfect match 21 | 22 | ### BeamSequence : BeamType 23 | Expect perfect match 24 | 25 | ### BeamSequence : ControlPointSequence : BeamLimitingDeviceAngle 26 | Expect perfect match 27 | 28 | ### FrameOfReferenceUID 29 | ? 30 | -------------------------------------------------------------------------------- /library/DITTO/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/DITTO/__init__.py -------------------------------------------------------------------------------- /library/DITTO/images/blue_circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/DITTO/images/blue_circle.png -------------------------------------------------------------------------------- /library/DITTO/images/blue_circle_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/DITTO/images/blue_circle_icon.png -------------------------------------------------------------------------------- /library/DITTO/images/green_circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/DITTO/images/green_circle.png -------------------------------------------------------------------------------- /library/DITTO/images/green_circle_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/DITTO/images/green_circle_icon.png -------------------------------------------------------------------------------- /library/DITTO/images/red_circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/DITTO/images/red_circle.png -------------------------------------------------------------------------------- /library/DITTO/images/red_circle_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/DITTO/images/red_circle_icon.png -------------------------------------------------------------------------------- /library/DITTO/images/yellow_circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/DITTO/images/yellow_circle.png -------------------------------------------------------------------------------- /library/DITTO/images/yellow_circle_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/DITTO/images/yellow_circle_icon.png -------------------------------------------------------------------------------- /library/DITTO/resizer.py: -------------------------------------------------------------------------------- 1 | # Import required Image library 2 | from PIL import Image 3 | 4 | factor = 0.25 5 | 6 | in_images = [ 7 | "library\\DITTO\\images\\red_circle.png", 8 | "library\\DITTO\\images\\green_circle.png", 9 | "library\\DITTO\\images\\blue_circle.png", 10 | "library\\DITTO\\images\\yellow_circle.png", 11 | ] 12 | 13 | out_images = [ 14 | "library\\DITTO\\images\\red_circle_icon.png", 15 | "library\\DITTO\\images\\green_circle_icon.png", 16 | "library\\DITTO\\images\\blue_circle_icon.png", 17 | "library\\DITTO\\images\\yellow_circle_icon.png", 18 | ] 19 | 20 | params = zip(in_images, out_images) 21 | 22 | for img_in, img_out in params: 23 | 24 | # Create an Image Object from an Image 25 | im = Image.open(img_in) 26 | 27 | # Make the new image half the width and half the height of the original image 28 | resized_im = im.resize((round(im.size[0] * factor), round(im.size[1] * factor))) 29 | 30 | resized_im = im.resize((15, 15)) 31 | 32 | # Save the cropped image 33 | resized_im.save(img_out) 34 | -------------------------------------------------------------------------------- /library/DITTO/testgui.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import PySimpleGUI as sg 4 | 5 | treedata = sg.TreeData() 6 | 7 | treedata.Insert(">", '_A_', 'A', [1, 2, 3]) 8 | treedata.Insert(">", '_B_', 'B', [4, 5, 6]) 9 | treedata.Insert("_A_", '_A1_', 'A1', ['can', 'be', 'anything']) 10 | 11 | layout = [ 12 | [sg.Tree( 13 | data=treedata, 14 | headings=['Size', ], 15 | auto_size_columns=True, 16 | num_rows=20, 17 | col0_width=40, 18 | key='-TREE-', 19 | show_expanded=False, 20 | enable_events=True), 21 | ], 22 | [sg.Button('Ok'), sg.Button('Cancel')] 23 | ] 24 | 25 | window = sg.Window('Tree Element Test', layout) 26 | 27 | while True: # Event Loop 28 | event, values = window.read() 29 | if event in (sg.WIN_CLOSED, 'Cancel'): 30 | break 31 | print(event, values) 32 | window.close() 33 | -------------------------------------------------------------------------------- /library/DicomDestinations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | MIM 4 | mim.uwhealth.wisc.edu 5 | AE_MIM 6 | 4008 7 | 8 | 9 | Mobius3D 10 | mobius.uwhealth.wisc.edu 11 | MOBIUS3D 12 | 104 13 | 14 | 15 | RayGateway 16 | 10.151.28.149 17 | RAYGATEWAY 18 | 7809 19 | 20 | 21 | ViewRay 22 | 10.105.10.25 23 | VIEWRAY_STORAGE 24 | 11115 25 | 26 | 27 | ARIA 28 | ariadicom.uwhealth.wisc.edu 29 | ARIA 30 | 106 31 | 32 | 33 | Delta4 34 | delta4.uwhis.hosp.wisc.edu 35 | Delta4 36 | 104 37 | 38 | 39 | Transfer Folder 40 | \\uwhis.hosp.wisc.edu\UFS\UWHealth\RadOnc\ShareAll\Transfer 41 | False 42 | 43 | -------------------------------------------------------------------------------- /library/GeneralOperations.py: -------------------------------------------------------------------------------- 1 | """ General Operations 2 | GeneralOperations is a set of functions that operate on the patient and API level 3 | within RayStation. 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, but WITHOUT 10 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 | 13 | You should have received a copy of the GNU General Public License along with 14 | this program. If not, see . 15 | """ 16 | 17 | __author__ = "Adam Bayliss" 18 | __contact__ = "rabayliss@wisc.edu" 19 | __date__ = "2024-04-22" 20 | 21 | __version__ = "1.0.1" 22 | __status__ = "Production" 23 | __deprecated__ = False 24 | __reviewer__ = "Adam Bayliss" 25 | 26 | __reviewed__ = "" 27 | __raystation__ = "2024A" 28 | __maintainer__ = "Adam Bayliss" 29 | 30 | __email__ = "rabayliss@wisc.edu" 31 | __license__ = "GPLv3" 32 | __copyright__ = "Copyright (C) 2024, University of Wisconsin Board of Regents" 33 | 34 | import logging 35 | from api.api_utils import find_scope 36 | from api.api_beamsets import get_unique_id_beamset, get_unique_id_plan 37 | 38 | 39 | class InvalidDataException(Exception): 40 | pass 41 | 42 | 43 | def logcrit(message): 44 | # Determine deepest scope 45 | current_scope = find_scope() 46 | level = "" 47 | 48 | # Construct the log message with '|' separator 49 | plan_uuid = get_unique_id_plan(current_scope["Plan"]) 50 | beamset_uuid = get_unique_id_beamset(current_scope["BeamSet"]) 51 | if current_scope["Case"] is not None: 52 | level += "Case: " + current_scope["Case"].CaseName + " | " 53 | if current_scope["Examination"] is not None: 54 | level += "Exam: " + current_scope["Examination"].Name + " | " 55 | if current_scope["Plan"] is not None: 56 | level += f"Plan: {current_scope['Plan'].Name} | PlanId: {plan_uuid} | " 57 | if current_scope["BeamSet"] is not None: 58 | level += f"Beamset: {current_scope['BeamSet'].DicomPlanLabel} | BeamsetId: {beamset_uuid} | " 59 | 60 | # Append the actual message to the log level information 61 | message = level + message 62 | 63 | # Log the message at critical level 64 | logging.critical(message) 65 | -------------------------------------------------------------------------------- /library/OldPlanReview/Icons/UW_Health_Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/OldPlanReview/Icons/UW_Health_Logo.png -------------------------------------------------------------------------------- /library/OldPlanReview/Icons/Yellow_Circle_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/OldPlanReview/Icons/Yellow_Circle_icon.png -------------------------------------------------------------------------------- /library/OldPlanReview/Icons/blue_circle_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/OldPlanReview/Icons/blue_circle_icon.png -------------------------------------------------------------------------------- /library/OldPlanReview/Icons/green_circle_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/OldPlanReview/Icons/green_circle_icon.png -------------------------------------------------------------------------------- /library/OldPlanReview/Icons/red_circle_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/OldPlanReview/Icons/red_circle_icon.png -------------------------------------------------------------------------------- /library/OldPlanReview/PlanReviewTests.py: -------------------------------------------------------------------------------- 1 | # Plan Checks 2 | import BeamSetReviewTests 3 | from ReviewDefinitions import * 4 | 5 | 6 | def check_plan_approved(rso, **kwargs): 7 | """ 8 | Check if a plan is approved 9 | Args: rso: Named Tuple of RS script objects 10 | do_physics_review: Bool: True if expected status of plan is approved 11 | 12 | Returns: 13 | message: [str1, ...]: [parent_key, child_key, child_key display, result_value] 14 | 15 | """ 16 | physics_review = kwargs.get('do_physics_review') 17 | approval_status = BeamSetReviewTests.approval_info(rso.plan, rso.beamset) 18 | if approval_status.plan_approved: 19 | message_str = "Plan: {} was approved by {} on {}".format( 20 | rso.plan.Name, 21 | approval_status.plan_reviewer, 22 | approval_status.plan_approval_time 23 | ) 24 | pass_result = PASS 25 | else: 26 | message_str = "Plan: {} is not approved".format( 27 | rso.plan.Name) 28 | if physics_review: 29 | pass_result = FAIL 30 | else: 31 | pass_result = ALERT 32 | return pass_result, message_str 33 | -------------------------------------------------------------------------------- /library/PlanReview/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/__init__.py -------------------------------------------------------------------------------- /library/PlanReview/documentation/__init__.py: -------------------------------------------------------------------------------- 1 | from PlanReview.documentation.generate_physics_document import generate_doc 2 | -------------------------------------------------------------------------------- /library/PlanReview/guis/__init__.py: -------------------------------------------------------------------------------- 1 | from PlanReview.guis.comment_to_clipboard import comment_to_clipboard 2 | from PlanReview.guis.build_tree import build_tree_element 3 | from PlanReview.guis.build_tree import build_review_tree 4 | from PlanReview.guis.build_tree import load_rsos 5 | from PlanReview.guis.progress_bar_tests import display_progress_bar 6 | from PlanReview.guis.gui_physics_review import launch_physics_review_gui 7 | from PlanReview.guis.gui_report_script_error import report_script_error 8 | from PlanReview.guis.parse_gui_values import get_review_gui_values, get_header_checklist_qa_values 9 | -------------------------------------------------------------------------------- /library/PlanReview/guis/comment_to_clipboard.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from PlanReview.utils.get_approval_info import get_approval_info 3 | 4 | 5 | def comment_to_clipboard(rso): 6 | # Clear the system clipboard 7 | root = tk.Tk() 8 | root.withdraw() 9 | root.clipboard_clear() 10 | 11 | # Create the beamset comment 12 | approval_status = get_approval_info(rso.plan, rso.beamset) 13 | beamset_comment = approval_status.beamset_approval_time 14 | 15 | # Copy the comment to the system clipboard 16 | root.clipboard_append(beamset_comment) 17 | root.update() 18 | 19 | # Return the root window so it can be destroyed by the caller 20 | return root 21 | -------------------------------------------------------------------------------- /library/PlanReview/guis/gui_report_script_error.py: -------------------------------------------------------------------------------- 1 | import PySimpleGUI as Sg 2 | from PlanReview.utils import get_user_name 3 | from PlanReview.utils.email_results import email_report, save_report, capture_screen 4 | 5 | 6 | def report_script_error(rso): 7 | # Define the layout of the error report dialog 8 | user_name = get_user_name() 9 | 10 | error_report_layout = [ 11 | [Sg.Text('Patient ID'), Sg.Input(default_text=rso.patient.PatientID, 12 | key='patient_id')], 13 | [Sg.Text('Beamset Name:'), 14 | Sg.Input(default_text=rso.beamset.DicomPlanLabel, 15 | key='beamset_name')], 16 | [Sg.Text('User Name:'), 17 | Sg.Input(default_text=user_name, key='user_name')], 18 | [Sg.Text('Description:')], 19 | [Sg.Multiline(key='description', size=(50, 10))], 20 | [Sg.Button("Capture", 21 | tooltip='Capture a screenshot with Snipping Tool: select ' 22 | '"New",' 23 | + ' capture your screen, and press "Ctrl-C" to ' 24 | 'save to clipboard.'), 25 | Sg.Button("Finish")], 26 | ] 27 | 28 | # Create the dialog window 29 | error_report_window = Sg.Window('Error Report', error_report_layout) 30 | img_data = None 31 | # Event loop for the dialog window 32 | while True: 33 | event, values = error_report_window.read() 34 | if event == Sg.WIN_CLOSED: 35 | break 36 | elif event == 'Capture': 37 | # Take a screenshot 38 | img_data = capture_screen(error_report_window) 39 | if img_data: 40 | Sg.popup_ok('Screenshot captured!') 41 | else: 42 | Sg.popup_ok( 43 | 'Oops I missed it. Try hitting Ctrl-C after you capture') 44 | img_data = capture_screen(error_report_window) 45 | elif event == 'Finish': 46 | # Save the report and close the window 47 | patient_id = values['patient_id'] 48 | beamset_name = values["beamset_name"] 49 | description = values["description"] 50 | screenshot = img_data if img_data else None 51 | file_path = save_report('error_report', patient_id=patient_id, beamset_name=beamset_name, 52 | user_name=user_name, report_text=description, screenshot=screenshot) 53 | email_report(file_path, 'error_report', source='manual') 54 | error_report_window.close() 55 | break 56 | -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/Thumbs.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/Thumbs.db -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/UW_Health_Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/UW_Health_Logo.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/blue_circle_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/blue_circle_icon.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/cancel_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/cancel_icon.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/cancel_icon_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/cancel_icon_small.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/checker_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/checker_icon.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/error_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/error_icon.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/error_icon_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/error_icon_small.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/final_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/final_icon.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/final_icon_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/final_icon_small.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/green_circle_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/green_circle_icon.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/load_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/load_icon.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/load_icon_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/load_icon_small.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/material_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/material_icon.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/material_icon_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/material_icon_small.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/pause_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/pause_icon.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/pause_icon_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/pause_icon_small.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/print_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/print_icon.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/print_icon_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/print_icon_small.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/red_circle_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/red_circle_icon.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/resume_icon_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/resume_icon_small.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/save_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/save_icon.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/save_icon_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/save_icon_small.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/scale_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/scale_icon.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/scale_icon_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/scale_icon_small.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/start_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/start_icon.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/start_icon_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/start_icon_small.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/submit_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/submit_icon.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/submit_icon_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/submit_icon_small.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/windowlevel_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/windowlevel_icon.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/windowlevel_icon_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/windowlevel_icon_small.png -------------------------------------------------------------------------------- /library/PlanReview/guis/icons/yellow_circle_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/guis/icons/yellow_circle_icon.png -------------------------------------------------------------------------------- /library/PlanReview/guis/parse_gui_values.py: -------------------------------------------------------------------------------- 1 | from PlanReview.guis.create_preplan_tab import extract_values_preplan_tab, create_tuple_key 2 | from PlanReview.guis.create_side_panel import extract_values_side_panel 3 | from PlanReview.guis.create_physics_manual_tab import ( 4 | extract_values_manual_tab, get_tests_from_tree, process_check_box_values, process_auto_tests) 5 | from PlanReview.guis.gui_qa_form import extract_values_qa_form, build_qa_form 6 | from PlanReview.utils.python_utilities import merge_dicts 7 | from PlanReview.utils.constants import KEY_BEAMSET_COUNT, KEY_BEAMSET_SELECT 8 | 9 | 10 | def get_review_gui_values(gui_state_manager, values): 11 | """ 12 | Extracts the values entered into the PySimpleGUI dialog and sorts them by keys. 13 | This is used for saving the review to file and for the report 14 | 15 | Parameters: 16 | - window: PySimpleGUI Window object representing the GUI 17 | - passing_tests: list of passing tests from the review_definitions module 18 | - failed_tests: list of failed tests from the review_definitions module 19 | - check_boxes: dictionary of completed check boxes the user has filled in 20 | 21 | Returns: 22 | - sorted_values: dictionary of values sorted by keys 23 | """ 24 | 25 | # Get any data from the first tab 26 | preplan_values = extract_values_preplan_tab(gui_state_manager.window) 27 | 28 | 29 | # Get values from the side tab 30 | if gui_state_manager.side_panel: 31 | side_frame_values = extract_values_side_panel(gui_state_manager.window, gui_state_manager.review_type) 32 | else: 33 | side_frame_values = {} 34 | 35 | # Get the data from the first tab 36 | if gui_state_manager.manual_tabs: 37 | manual_values = extract_values_manual_tab(values, gui_state_manager.passing_tests, 38 | gui_state_manager.failed_tests, gui_state_manager.check_box_copy) 39 | else: 40 | manual_values = {} 41 | 42 | # Merge them into a single dictionary 43 | sorted_values = merge_dicts(side_frame_values, preplan_values) 44 | sorted_values = merge_dicts(sorted_values, manual_values) 45 | # Get values from the qa form 46 | if gui_state_manager.qa_form_accessible: 47 | qa_form_values = extract_values_qa_form(gui_state_manager.window) 48 | sorted_values = merge_dicts(sorted_values, qa_form_values) 49 | 50 | return sorted_values 51 | 52 | 53 | def get_header_checklist_qa_values(gui_state_manager, values): 54 | """ 55 | Extracts the values entered into the PySimpleGUI dialog and sorts them by keys. 56 | This is used for saving the review to file and for the report 57 | 58 | Parameters: 59 | - window: PySimpleGUI Window object representing the GUI 60 | - passing_tests: list of passing tests from the review_definitions module 61 | - failed_tests: list of failed tests from the review_definitions module 62 | - check_boxes: dictionary of completed check boxes the user has filled in 63 | 64 | Returns: 65 | """ 66 | 67 | # 68 | # Retrieve data from the check-boxes and automated tests 69 | gui_state_manager.passing_tests, gui_state_manager.failed_tests = get_tests_from_tree( 70 | gui_state_manager.tree_children) 71 | gui_state_manager.check_list = process_check_box_values(gui_state_manager.window, values, 72 | gui_state_manager.check_box_copy) 73 | gui_state_manager.check_list.extend( 74 | process_auto_tests(gui_state_manager.window, gui_state_manager.failed_tests)) 75 | gui_state_manager.check_list.extend( 76 | process_auto_tests(gui_state_manager.window, gui_state_manager.passing_tests)) 77 | # 78 | # Retrieve data from the first tab and side panel 79 | preplan_data = extract_values_preplan_tab(gui_state_manager.window) 80 | if gui_state_manager.qa_form_accessible: 81 | gui_state_manager.qa_form_data = build_qa_form(gui_state_manager.rso, gui_state_manager.window) 82 | else: 83 | gui_state_manager.qa_form_data = {} 84 | sidepanel_data = extract_values_side_panel(gui_state_manager.window, gui_state_manager.review_type) 85 | gui_state_manager.header_data = merge_dicts(preplan_data, sidepanel_data) 86 | -------------------------------------------------------------------------------- /library/PlanReview/guis/progress_bar_tests.py: -------------------------------------------------------------------------------- 1 | import PySimpleGUI as Sg 2 | 3 | 4 | def display_progress_bar(title_text='Progress on tests', progress_bar_text='Running tests...'): 5 | layout = [[Sg.Text(progress_bar_text, key='progress_text')], 6 | [Sg.ProgressBar(max_value=100, orientation='h', size=(30, 20), 7 | key='progressbar')]] 8 | 9 | window = Sg.Window(title_text, layout, no_titlebar=True, 10 | keep_on_top=True, finalize=True) 11 | 12 | progress_bar = window['progressbar'] 13 | progress_text = window['progress_text'] 14 | progress_bar.UpdateBar(0) 15 | 16 | return window, progress_bar, progress_text 17 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/qa_tests/__init__.py -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/analyze_logs/__init__.py: -------------------------------------------------------------------------------- 1 | from PlanReview.qa_tests.analyze_logs.retrieve_logs import retrieve_logs 2 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_beamset/__init__.py: -------------------------------------------------------------------------------- 1 | from PlanReview.qa_tests.test_beamset.parse_beamset_selection \ 2 | import parse_beamset_selection 3 | from PlanReview.qa_tests.test_beamset.check_beamset_approved \ 4 | import check_beamset_approved 5 | from PlanReview.qa_tests.test_beamset.check_transfer_approved \ 6 | import check_transfer_approved 7 | from PlanReview.qa_tests.test_beamset.get_beamset_level_tests \ 8 | import get_beamset_level_tests 9 | from PlanReview.qa_tests.test_beamset.check_control_point_spacing \ 10 | import check_control_point_spacing 11 | from PlanReview.qa_tests.test_beamset.check_tomo_mod_factor \ 12 | import check_tomo_mod_factor 13 | from PlanReview.qa_tests.test_beamset.check_tomo_isocenter \ 14 | import check_tomo_isocenter 15 | from PlanReview.qa_tests.test_beamset.check_fraction_size \ 16 | import check_fraction_size 17 | from PlanReview.qa_tests.test_beamset.check_no_fly import check_no_fly 18 | from PlanReview.qa_tests.test_beamset.check_pacemaker import check_pacemaker 19 | from PlanReview.qa_tests.test_beamset.check_isocenter_clearance \ 20 | import check_isocenter_clearance 21 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_beamset/check_beamset_approved.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, NamedTuple, Optional 2 | from PlanReview.review_definitions import PASS, FAIL, ALERT, STAFF_XML_PATH 3 | from PlanReview.utils import ( 4 | get_approval_info, find_groupname_by_userid, is_valid_approver) 5 | 6 | VALID_APPROVAL_GROUPS = ["Oncologist"] 7 | 8 | 9 | def check_beamset_approved(rso: NamedTuple, **kwargs: Optional[bool]) -> Tuple[str, str]: 10 | """ Check Beamset Approved 11 | Checks whether a given beamset is approved or not. 12 | 13 | Args: 14 | rso (NamedTuple): ScriptObjects in RayStation containing 15 | [case ('RayStation Case Object'), 16 | exam ('RayStation Exam Object'), 17 | plan ('RayStation Plan Object'), 18 | beamset ('RayStation BeamSet Object'), 19 | db ('RayStation Database Object')] 20 | **kwargs: Additional keyword arguments, options include: 21 | - do_physics_review (Optional[bool]): If True, the beamset is expected to be approved. 22 | 23 | Returns: 24 | result, message_string (Tuple[str, str]): First element is the status (PASS/FAIL/ALERT), 25 | Second element is the message string 26 | 27 | Pseudocode: 28 | 1. Extract 'do_physics_review' from kwargs 29 | 2. Retrieve approval status from 'get_approval_info' function 30 | 3. Build the appropriate message string based on approval status 31 | 4. Determine the result (PASS/FAIL/ALERT) 32 | 5. Return the result and message 33 | 34 | Test Patients: 35 | Pass: Script_Testing^FinalDose: ZZUWQA_ScTest_06Jan2021: Case: THI: Plan: Anal_THI 36 | Fail: Script_Testing^FinalDose: ZZUWQA_ScTest_06Jan2021: Case: VMAT: Plan: Pros_VMA also Validation/ZZUWQA_20Jan2021: MultiBeamset Script_Testing 37 | """ 38 | 39 | do_physics_review = kwargs.get('do_physics_review', False) 40 | 41 | approval_status = get_approval_info(rso.plan, rso.beamset) 42 | if approval_status.beamset_approved: 43 | if not approval_status.plan_reviewer: 44 | message_str = f"Beamset: {rso.beamset.DicomPlanLabel} does not have valid approval data" 45 | pass_result = ALERT 46 | return pass_result, message_str 47 | group_name = find_groupname_by_userid(approval_status.beamset_reviewer) 48 | if is_valid_approver(group_name, VALID_APPROVAL_GROUPS): 49 | message_str = f"Beamset: {rso.beamset.DicomPlanLabel} was approved by " \ 50 | f"{approval_status.beamset_reviewer}, (Staff {group_name}) " \ 51 | f"on {approval_status.beamset_approval_time}" 52 | pass_result = PASS 53 | else: 54 | message_str = f"Beamset: {rso.beamset.DicomPlanLabel} approval INVALID. Approved by " \ 55 | f"{approval_status.beamset_reviewer}, ({group_name}) " \ 56 | f"on {approval_status.beamset_approval_time}" 57 | pass_result = FAIL 58 | 59 | else: 60 | message_str = "Beamset: {} is not approved.".format( 61 | rso.plan.Name) 62 | if do_physics_review: 63 | pass_result = FAIL 64 | else: 65 | pass_result = PASS 66 | message_str += " (Dosimetry Safety Review)" 67 | return pass_result, message_str 68 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_beamset/check_bolus_included.py: -------------------------------------------------------------------------------- 1 | from PlanReview.review_definitions import BOLUS_NAMES, PASS, FAIL 2 | from PlanReview.utils import get_roi_list, match_roi_name 3 | 4 | 5 | def check_bolus_included(rso): 6 | """ Check Bolus Included 7 | Checks if the given bolus is applied to any beam in the beamset. 8 | 9 | Args: 10 | rso (Union[NamedTuple, dict]): ScriptObjects in Raystation containing [case,exam,plan,beamset,db] 11 | 12 | Returns: 13 | Tuple[str, str]: First element is the status (PASS/FAIL), 14 | Second element is the message string 15 | 16 | Pseudocode: 17 | 1. Retrieve exam_name and roi_list from rso 18 | 2. Match ROI names to predefined BOLUS_NAMES 19 | 3. Check if any matched bolus names are applied to beams 20 | 4. Construct the message string 21 | 5. Determine the result (PASS/FAIL) 22 | 6. Return the result and message 23 | 24 | Test Patients: 25 | Pass: Name: Script_Testing^Plan_Review,MRN: #ZZUWQA_ScTest_01May2022,Case: ChwL, Bolus_Roi_Check_Pass: ChwL_VMA_R1A0 26 | Fail: Name: Script_Testing^Plan_Review,MRN: #ZZUWQA_ScTest_01May2022,Case: ChwL, Bolus_Roi_Check_Fail: ChwL_VMA_R0A0 27 | """ 28 | child_key = "Bolus Application" 29 | exam_name = rso.exam.Name 30 | roi_list = get_roi_list(rso.case, exam_name=exam_name) 31 | bolus_names = match_roi_name(roi_names=BOLUS_NAMES, roi_list=roi_list) 32 | if bolus_names: 33 | fail_str = "Stucture(s) {} named bolus, ".format(bolus_names) \ 34 | + "but not applied to beams in beamset {}".format(rso.beamset.DicomPlanLabel) 35 | try: 36 | applied_boli = set([bolus.Name for b in rso.beamset.Beams for bolus in b.Boli]) 37 | if any(bn in applied_boli for bn in bolus_names): 38 | bolus_matches = {bn: [] for bn in bolus_names} 39 | for ab in applied_boli: 40 | bolus_matches[ab].extend([b.Name for b in rso.beamset.Beams 41 | for bolus in b.Boli 42 | if bolus.Name == ab]) 43 | pass_result = PASS 44 | message_str = "".join( 45 | ['Roi {0} applied on beams {1}'.format(k, v) for k, v in bolus_matches.items()]) 46 | else: 47 | pass_result = FAIL 48 | message_str = fail_str 49 | except AttributeError: 50 | pass_result = FAIL 51 | message_str = fail_str 52 | else: 53 | message_str = "No rois including {} found for Exam {}".format(BOLUS_NAMES, exam_name) 54 | pass_result = PASS 55 | return pass_result, message_str 56 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_beamset/check_common_isocenter.py: -------------------------------------------------------------------------------- 1 | from math import isclose 2 | from typing import Tuple, Optional 3 | from PlanReview.review_definitions import FAIL, PASS 4 | 5 | 6 | def check_common_isocenter(rso, **kwargs) -> Tuple[str, str]: 7 | """ Check common isocenter 8 | Checks all beams in a given beamset for shared isocenter. 9 | 10 | Args: 11 | rso (NamedTuple): ScriptObjects in Raystation containing [case,exam,plan,beamset,db]. 12 | tolerance (Optional[float]): Largest acceptable difference in isocenter location in mm. 13 | 14 | Returns: 15 | Tuple[str, str]: First element is the status (PASS/FAIL), 16 | Second element is the message string. 17 | 18 | Pseudocode: 19 | 1. Extract 'tolerance' from kwargs 20 | 2. Retrieve isocenter positions for the first beam 21 | 3. Loop through all beams to compare isocenter positions 22 | 4. Build the appropriate message string based on isocenter match or difference 23 | 5. Determine the result (PASS/FAIL) 24 | 6. Return the result and message 25 | 26 | Test Patients: 27 | Pass: (Provide relevant test cases where this check will pass) 28 | Fail: (Provide relevant test cases where this check will fail) 29 | """ 30 | tolerance = kwargs.get('tolerance') 31 | initial_beam_name = rso.beamset.Beams[0].Name 32 | iso_pos_x = rso.beamset.Beams[0].Isocenter.Position.x 33 | iso_pos_y = rso.beamset.Beams[0].Isocenter.Position.y 34 | iso_pos_z = rso.beamset.Beams[0].Isocenter.Position.z 35 | iso_differs = [] 36 | iso_match = [] 37 | for b in rso.beamset.Beams: 38 | b_iso = b.Isocenter.Position 39 | if all([isclose(b_iso.x, iso_pos_x, rel_tol=tolerance, abs_tol=0.0), 40 | isclose(b_iso.y, iso_pos_y, rel_tol=tolerance, abs_tol=0.0), 41 | isclose(b_iso.z, iso_pos_z, rel_tol=tolerance, abs_tol=0.0)]): 42 | iso_match.append(b.Name) 43 | else: 44 | iso_differs.append(b.Name) 45 | if iso_differs: 46 | pass_result = FAIL 47 | message_str = \ 48 | f"Beam(s) {iso_differs} differ in isocenter location" \ 49 | f"from beam {initial_beam_name}" 50 | else: 51 | pass_result = PASS 52 | message_str = \ 53 | f"Beam(s) {iso_match} all share the same isocenter to within " \ 54 | f"{tolerance} mm" 55 | 56 | return pass_result, message_str 57 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_beamset/check_control_point_spacing.py: -------------------------------------------------------------------------------- 1 | from typing import NamedTuple, List, Dict, Tuple, Any, Optional 2 | from PlanReview.review_definitions import PASS, FAIL 3 | 4 | 5 | def message_format_control_point_spacing( 6 | beam_spacing_failures: Dict[str, List[int]], 7 | spacing: int 8 | ) -> Tuple[str, str]: 9 | """ 10 | Formats the control point spacing message and result. 11 | 12 | Args: 13 | beam_spacing_failures (Dict[str, List[int]]): Dictionary containing beam names and the corresponding failing control points. 14 | spacing (int): The maximum allowed gantry angle between control points. 15 | 16 | Returns: 17 | Tuple[str, str]: First element is the message string, 18 | Second element is the status (PASS/FAIL). 19 | """ 20 | # Takes in a message dictionary that is labeled per beam, then parses 21 | if beam_spacing_failures: 22 | for b, v in beam_spacing_failures.items(): 23 | message_str = 'Beam {}: Gantry Spacing Exceeds {} in Control Points {}\n' \ 24 | .format(b, spacing, v) 25 | message_result = FAIL 26 | else: 27 | message_str = "No control points > {} detected".format(spacing) 28 | message_result = PASS 29 | return message_str, message_result 30 | 31 | 32 | def pass_control_point_spacing(s: Any, s0: Optional[Any], spacing: int) -> bool: 33 | """ 34 | Checks whether the gantry angle between control points is within a specified limit. 35 | 36 | Args: 37 | s (Any): The current control point. 38 | s0 (Optional[Any]): The previous control point. 39 | spacing (int): The maximum allowed gantry angle between control points. 40 | 41 | Returns: 42 | bool: True if the spacing is acceptable, False otherwise. 43 | """ 44 | if not s0: 45 | if s.DeltaGantryAngle <= spacing: 46 | return True 47 | else: 48 | return False 49 | else: 50 | if s.DeltaGantryAngle - s0.DeltaGantryAngle <= spacing: 51 | return True 52 | else: 53 | return False 54 | 55 | 56 | def check_control_point_spacing(rso: NamedTuple,**kwargs) -> Tuple[str, str]: 57 | """ Check Control Point Spacing 58 | Checks the gantry angle between control points in a beamset. 59 | 60 | Args: 61 | rso (NamedTuple): ScriptObjects in Raystation containing [case, exam, plan, beamset, db]. 62 | expected (Optional[int]): Expected gantry angle between control points, provided through kwargs. 63 | 64 | Returns: 65 | Tuple[str, str]: First element is the result (PASS/FAIL), 66 | Second element is the message string. 67 | 68 | Pseudocode: 69 | 1. Extract 'expected' from kwargs. 70 | 2. Loop through beams in the beamset. 71 | 3. For each beam, loop through control points and apply 'pass_control_point_spacing' function. 72 | 4. Store failed control points and prepare the message string using 'message_format_control_point_spacing' function. 73 | 5. Return the result and message string. 74 | 75 | Test Patients: 76 | Pass: None provided 77 | Fail: None Provided 78 | """ 79 | expected = kwargs.get('expected') 80 | beam_result = {} 81 | for b in rso.beamset.Beams: 82 | s0 = None 83 | fails = [] 84 | for s in b.Segments: 85 | if not pass_control_point_spacing(s, s0, spacing=expected): 86 | fails.append(s.SegmentNumber + 1) 87 | s0 = s 88 | if fails: 89 | beam_result[b.Name] = fails 90 | message_str, pass_result = message_format_control_point_spacing( 91 | beam_spacing_failures=beam_result, 92 | spacing=expected) 93 | return pass_result, message_str 94 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_beamset/check_couch_type.py: -------------------------------------------------------------------------------- 1 | from PlanReview.review_definitions import TOMO_DATA, TRUEBEAM_DATA, FAIL, \ 2 | PASS, ALERT 3 | from PlanReview.utils import get_machine 4 | from typing import NamedTuple, Tuple 5 | 6 | 7 | def check_couch_type(rso: NamedTuple) -> Tuple[str, str]: 8 | """Check Couch Type 9 | Checks if the correct couch support structures are present in the 10 | patient plan based on the machine type. 11 | 12 | Args: 13 | rso (NamedTuple): ScriptObjects in RayStation containing 14 | [case ('RayStation Case Object'), 15 | exam ('RayStation Exam Object'), 16 | plan ('RayStation Plan Object'), 17 | beamset ('RayStation BeamSet Object'), 18 | db ('RayStation Database Object')] 19 | 20 | Returns: 21 | Tuple[str, str]: First element is the status (PASS/FAIL/ALERT), 22 | Second element is the message string. 23 | 24 | Pseudocode: 25 | 1. Retrieve the list of ROI names from the case 26 | 2. Determine the machine name from the first beam of the beamset 27 | 3. Identify the wrong and correct supports based on the machine type 28 | 4. Build an appropriate message string based on the support structures 29 | found 30 | 5. Determine the result (PASS/FAIL/ALERT) 31 | 6. Return the result and message string 32 | 33 | Test Patients: 34 | Pass: Patient Needed 35 | Fail: Patient Needed 36 | """ 37 | # Abbreviate geometries 38 | rg = rso.case.PatientModel.StructureSets[rso.exam.Name].RoiGeometries 39 | roi_list = [r.OfRoi.Name for r in rg] 40 | beam = rso.beamset.Beams[0] 41 | current_machine = get_machine(machine_name=beam.MachineReference.MachineName) 42 | wrong_supports = [] 43 | correct_supports = [] 44 | if current_machine.Name in TRUEBEAM_DATA['MACHINES']: 45 | wrong_supports = [s for s in TOMO_DATA['SUPPORTS'] if s in roi_list] 46 | correct_supports = [s for s in TRUEBEAM_DATA['SUPPORTS'] if s in roi_list] 47 | elif current_machine.Name in TOMO_DATA['MACHINES']: 48 | wrong_supports = [s for s in TRUEBEAM_DATA['SUPPORTS'] if s in roi_list] 49 | correct_supports = [s for s in TOMO_DATA['SUPPORTS'] if s in roi_list] 50 | if wrong_supports: 51 | message_str = 'Support Structure(s) {} are INCORRECT for machine {}'.format( 52 | wrong_supports, current_machine.Name) 53 | pass_result = FAIL 54 | elif correct_supports: 55 | message_str = 'Support Structure(s) {} are correct for machine {}'.format( 56 | correct_supports, current_machine.Name) 57 | pass_result = PASS 58 | else: 59 | message_str = 'No couch structure found' 60 | pass_result = ALERT 61 | # Prepare output 62 | return pass_result, message_str 63 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_beamset/check_edw_mu.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple, NamedTuple 2 | from PlanReview.review_definitions import TRUEBEAM_DATA, PASS, FAIL 3 | 4 | 5 | def get_edw_beams(beams: List[object]) -> dict: 6 | """ 7 | Helper function to get beams with EDWs. 8 | """ 9 | edw_dict = {} 10 | for b in beams: 11 | try: 12 | if 'EDW' in b.Wedge.WedgeID: 13 | edw_dict[b.Name] = b.BeamMU 14 | except AttributeError: 15 | continue 16 | 17 | return edw_dict 18 | 19 | 20 | def check_edw_mu(rso: NamedTuple) -> Tuple[str, str]: 21 | """ 22 | Check if all Monitor Units (MU) are greater than the EDW limit. 23 | 24 | Args: 25 | rso: NamedTuple containing RayStation script objects. 26 | E.g., rso.beamset refers to the RayStation beamset script object. 27 | 28 | Returns: 29 | Tuple of (pass_result: str, message_str: str) 30 | 31 | Pseudocode: 32 | 1. Extract beams with EDWs from the provided RayStation object 33 | 2. Check if MU values for these beams are within specified limits 34 | 3. Generate a message based on the check results 35 | 4. Return the message and pass/fail status 36 | 37 | Test Patient: 38 | ScriptTesting, #ZZUWQA_SCTest_13May2022, C1 39 | PASS: ChwR_3DC_R0A0 40 | FAIL: ChwR_3DC_R2A0 41 | """ 42 | edws = get_edw_beams(rso.beamset.Beams) 43 | 44 | if edws: 45 | passing, message_str = validate_edw_limits(edws) 46 | else: 47 | passing = True 48 | message_str = "No beams with EDWs found" 49 | 50 | pass_result = PASS if passing else FAIL 51 | return pass_result, message_str 52 | 53 | 54 | def validate_edw_limits(edws: dict) -> Tuple[bool, str]: 55 | """ 56 | Helper function to validate if the MU values for EDW beams meet the limits. 57 | """ 58 | passing = all(mu >= TRUEBEAM_DATA['EDW_LIMITS']['MU_LIMIT'] for mu in edws.values()) 59 | 60 | if passing: 61 | edw_names = ', '.join(edws.keys()) 62 | message_str = f"Beam(s) have EDWs: {edw_names} all with MU > {TRUEBEAM_DATA['EDW_LIMITS']['MU_LIMIT']}" 63 | else: 64 | failing_beams = [(bn, mu) for bn, mu in edws.items() if mu < TRUEBEAM_DATA['EDW_LIMITS']['MU_LIMIT']] 65 | message_str = f"Beam(s) have EDWs: {', '.join(f'{bn}(MU)={mu:.2f}' for bn, mu in failing_beams)} < {TRUEBEAM_DATA['EDW_LIMITS']['MU_LIMIT']}" 66 | 67 | return passing, message_str 68 | 69 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_beamset/check_emc_statistics.py: -------------------------------------------------------------------------------- 1 | from PlanReview.review_definitions import PASS, FAIL, ELECTRON_MC_MIN_HISTORIES, ELECTRON_MC_UNCERTAINTY 2 | from PlanReview.utils.review_api_versions import get_number_of_emc_histories 3 | 4 | 5 | def emc_calc_params(beamset): 6 | """ 7 | For each beam, go through the beam doses, and return the statistical uncertainty and the 8 | MC histories used in beam dose. Return the maximum uncertainty and minimum MC histories. 9 | :param beamset: RS beamset 10 | :return: NormUnc (the maximum normalized uncertainty) and number of histories used in calc 11 | """ 12 | max_uncertainty = 0 13 | # Return electron monte carlo computational parameters 14 | for bd in beamset.FractionDose.BeamDoses: 15 | max_uncertainty = max(max_uncertainty, bd.DoseValues.RelativeStatisticalUncertainty) 16 | min_histories = get_number_of_emc_histories(beamset) 17 | 18 | return {'NormUnc': max_uncertainty, 'MinHist': min_histories} 19 | 20 | 21 | def check_emc_statistics(rso): 22 | """ 23 | Checks the electron monte carlo accuracy to ensure statistical limit is met 24 | :param rso: RS object 25 | :return: result, message: where message is the message to be displayed in the review, and 26 | result is the pass/fail status. 27 | """ 28 | eval_current_emc = emc_calc_params(rso.beamset) 29 | if eval_current_emc['MinHist'] < ELECTRON_MC_MIN_HISTORIES \ 30 | or eval_current_emc['NormUnc'] > ELECTRON_MC_UNCERTAINTY: 31 | stat_limit_hist = int( 32 | eval_current_emc['MinHist'] 33 | * (eval_current_emc['NormUnc'] / ELECTRON_MC_UNCERTAINTY) ** 2.) 34 | recommended_histories = max(ELECTRON_MC_MIN_HISTORIES, stat_limit_hist) 35 | message = f"EMC uncertainty: {round(100 * eval_current_emc['NormUnc'])}% " 36 | f"recommend increasing histories from {eval_current_emc['MinHist']} " 37 | f"to {recommended_histories}" 38 | result = FAIL 39 | else: 40 | message = f'Clinically-acceptable EMC uncertainty: {round( 100 * eval_current_emc["NormUnc"])} ' \ 41 | f'and histories {eval_current_emc["MinHist"]}' 42 | result = PASS 43 | 44 | return result, message 45 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_beamset/check_fraction_size.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, NamedTuple 2 | from PlanReview.review_definitions import ( 3 | PASS, ALERT, DOSE_FRACTION_PAIRS 4 | ) 5 | 6 | 7 | def check_fraction_size(rso: NamedTuple) -> Tuple[int, str]: 8 | """ Check Fraction Size 9 | 10 | Checks the fraction size for common errors based on predefined 11 | often-mixed-up fractionations (DOSE_FRACTION_PAIRS). 12 | 13 | Args: 14 | rso (NamedTuple): RayStation Beamset Object containing 15 | information about the plan's fractionation. 16 | 17 | Returns: 18 | Tuple[int, str]: First element is the result (PASS/ALERT/FAIL), 19 | Second element is the detailed message string. 20 | 21 | Pseudocode: 22 | 1. Extract fractionation details from rso. 23 | 2. Initialize result variables. 24 | 3. Try to get the Prescription dose. 25 | 4. Check for matching dose pairs from the DOSE_FRACTION_PAIRS list. 26 | 5. Build the appropriate message string based on the checks. 27 | 6. Return the result and message. 28 | 29 | Test Patients: 30 | Pass: Script_Testing^Plan_Review, #ZZUWQA_ScTest_01May2022: Pelv_THI_R0A0 31 | Fail: Script_Testing^Plan_Review, #ZZUWQA_ScTest_01May2022: Pelv_T3D_R0A0 32 | """ 33 | 34 | # Number of fractions from the RayStation Beamset Object 35 | num_fx = rso.beamset.FractionationPattern.NumberOfFractions 36 | 37 | # Default result is PASS 38 | pass_result = PASS 39 | 40 | # Default message 41 | message_str = f'Beamset {rso.beamset.DicomPlanLabel} fractionation not flagged' 42 | 43 | # Initialize prescription dose to None 44 | rx_dose = None 45 | 46 | # Try to extract the Prescription dose 47 | try: 48 | rx_dose = rso.beamset.Prescription.PrimaryPrescriptionDoseReference.DoseValue 49 | except AttributeError: 50 | # ALERT if prescription is not defined 51 | pass_result = ALERT 52 | message_str = f'No Prescription is Defined for Beamset: {rso.beamset.DicomPlanLabel}' 53 | 54 | # Constant for converting cGy to Gy 55 | cgy_to_gy_conversion = 100.0 56 | 57 | # Check for matching dose pairs in DOSE_FRACTION_PAIRS, which contains often mixed-up fractionations 58 | if rx_dose is not None: 59 | for dose_fraction_pair in DOSE_FRACTION_PAIRS: 60 | if dose_fraction_pair[0] == num_fx and dose_fraction_pair[1] == rx_dose: 61 | pass_result = ALERT 62 | dose_per_fraction = int(rx_dose / cgy_to_gy_conversion / num_fx) 63 | message_str = f"Verify with MD: {num_fx} fractions at {dose_per_fraction:.2f}" \ 64 | " Gy/fraction due to high risk of transcription error." 65 | 66 | return pass_result, message_str 67 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_beamset/check_no_fly.py: -------------------------------------------------------------------------------- 1 | from typing import NamedTuple, Tuple 2 | from PlanReview.review_definitions import ( 3 | PASS, ALERT, FAIL, NO_FLY_DOSE, NO_FLY_NAME 4 | ) 5 | 6 | 7 | def build_message(dose: float, tolerance: float, label: str, status: str) -> str: 8 | """Builds a message string based on the dose comparison.""" 9 | return f"{label} is {'likely out of field' if status == PASS else 'potentially infield'}. " \ 10 | f"Dose = {dose:.2f} cGy (tolerance {tolerance:.2f} cGy)" 11 | 12 | 13 | def check_no_fly(rso: NamedTuple) -> Tuple[str, str]: 14 | """ Check No Fly 15 | Checks if the 'NO_FLY_NAME' ROI is within acceptable dose limits. 16 | 17 | Args: 18 | rso (NamedTuple): ScriptObjects in RayStation containing 19 | [case ('RayStation Case Object'), 20 | exam ('RayStation Exam Object'), 21 | plan ('RayStation Plan Object'), 22 | beamset ('RayStation BeamSet Object'), 23 | db ('RayStation Database Object')] 24 | 25 | Returns: 26 | pass_result, message_str (Tuple[str, str]): First element is the status (PASS/FAIL/ALERT), 27 | Second element is the message string 28 | 29 | Pseudocode: 30 | 1. Retrieve the dose statistic from RayStation for 'NO_FLY_NAME' 31 | 2. Compare the retrieved dose with 'NO_FLY_DOSE' 32 | 3. Build the appropriate message string based on comparison 33 | 4. Determine the result (PASS/FAIL/ALERT) 34 | 5. Return the result and message 35 | 36 | Test Patients: 37 | PASS: Script_Testing, #ZZUWQA_ScTest_13May2022, ChwR_3DC_R0A0 38 | FAIL: Script_Testing, #ZZUWQA_ScTest_13May2022b, Esop_VMA_R1A0 39 | """ 40 | try: 41 | # Retrieve the maximum dose to the 'NO_FLY_NAME' region of interest (ROI) 42 | plan_no_fly_dose = rso.plan.TreatmentCourse.TotalDose.GetDoseStatistic( 43 | RoiName=NO_FLY_NAME, DoseType='Max') 44 | 45 | # Compare the retrieved dose with the tolerance dose 'NO_FLY_DOSE' 46 | if plan_no_fly_dose > NO_FLY_DOSE: 47 | pass_result = FAIL 48 | else: 49 | pass_result = PASS 50 | 51 | # Build message string 52 | message_str = build_message(plan_no_fly_dose, NO_FLY_DOSE, NO_FLY_NAME, pass_result) 53 | 54 | except Exception as e: 55 | # Check if ROI exists, and handle other exceptions 56 | if "exists no ROI" in str(e): 57 | message_str = f"No ROI {NO_FLY_NAME} found, Incline Board not used" 58 | pass_result = PASS 59 | else: 60 | message_str = f"Unknown error in looking for incline board info: {e}" 61 | pass_result = ALERT 62 | 63 | return pass_result, message_str 64 | 65 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_beamset/check_slice_thickness.py: -------------------------------------------------------------------------------- 1 | from typing import NamedTuple, Tuple 2 | from PlanReview.review_definitions import PASS, FAIL, ALERT, GRID_PREFERENCES 3 | import numpy as np 4 | 5 | 6 | def calculate_slice_thickness(img_series): 7 | slice_positions = np.array(img_series.ImageStack.SlicePositions) 8 | return np.diff(slice_positions) 9 | 10 | 11 | def check_slice_thickness(rso: NamedTuple) -> Tuple[str, str]: 12 | """ Check Slice Thickness 13 | Checks the slice thickness for the current exam and plan type. 14 | 15 | Args: 16 | rso (NamedTuple): ScriptObjects in RayStation containing 17 | [case ('RayStation Case Object'), 18 | exam ('RayStation Exam Object'), 19 | plan ('RayStation Plan Object'), 20 | beamset ('RayStation BeamSet Object'), 21 | db ('RayStation Database Object')] 22 | 23 | Returns: 24 | Tuple[str, str]: First element is the status (PASS/FAIL/ALERT), 25 | Second element is the message string 26 | 27 | Pseudocode: 28 | 1. Initialize empty message string and result variable. 29 | 2. Loop through each plan type in GRID_PREFERENCES. 30 | 3. Check if the current plan label matches any of the PLAN_NAMES in the plan type. 31 | 4. If so, calculate the slice thickness and compare it to the expected slice thickness. 32 | 5. Generate the appropriate message string based on the comparison. 33 | 6. Return the status and message string. 34 | Example Outcome: 35 | ('PASS', 'Slice spacing 0.200 cm appropriate for plan type LUNG_SBRT') 36 | 37 | Test Patients: 38 | Pass: Needed 39 | Fail: Needed 40 | """ 41 | 42 | message_str = "" 43 | pass_result = "" 44 | slice_thickness = np.array([]) # Initialize to empty numpy array 45 | 46 | for plan_type, preferences in GRID_PREFERENCES.items(): 47 | plan_names = preferences.get('PLAN_NAMES', []) 48 | nominal_slice_thickness = preferences.get('SLICE_THICKNESS', 0.0) 49 | 50 | if any(plan_name in rso.beamset.DicomPlanLabel for plan_name in plan_names): 51 | for exam_series in rso.exam.Series: 52 | slice_thickness = calculate_slice_thickness(exam_series) 53 | 54 | if slice_thickness.size > 0: 55 | is_thickness_close = np.isclose(slice_thickness, nominal_slice_thickness).all() 56 | is_thickness_smaller = all(slice_thickness < nominal_slice_thickness) 57 | 58 | if is_thickness_close or is_thickness_smaller: 59 | message_str = f'Slice spacing {np.amax(slice_thickness):.3f} cm ' \ 60 | f'appropriate for plan type {plan_names}' 61 | pass_result = PASS 62 | else: 63 | message_str = f'Slice spacing {np.amax(slice_thickness):.3f} cm ' \ 64 | f'TOO LARGE for plan type {plan_names}' 65 | pass_result = FAIL 66 | else: 67 | message_str = 'Slice thickness data is missing.' 68 | pass_result = ALERT 69 | 70 | if not message_str: 71 | for exam_series in rso.exam.Series: 72 | slice_thickness = calculate_slice_thickness(exam_series) 73 | if slice_thickness.size > 0: 74 | message_str = f'Plan type unknown, check slice spacing ' \ 75 | f'{np.amax(slice_thickness):.3f} cm carefully' 76 | pass_result = ALERT 77 | else: 78 | message_str = 'Slice thickness data is missing for unknown plan type.' 79 | pass_result = ALERT 80 | 81 | return pass_result, message_str 82 | 83 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_beamset/check_tomo_isocenter.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from PlanReview.review_definitions import PASS, FAIL, TOMO_DATA 3 | 4 | 5 | def check_tomo_isocenter(rso): 6 | """ 7 | Checks isocenter for lateral less than 2 cm. 8 | Check also for any 9 | 10 | Args: 11 | rso (namedtuple): Named tuple of ScriptObjects 12 | Returns: 13 | message: List of ['Test Name Key', 'Pass/Fail', 'Detailed Message'] 14 | 15 | """ 16 | isocenter = rso.beamset.Beams[0].Isocenter 17 | iso_pos_x = isocenter.Position.x 18 | if np.less_equal(abs(iso_pos_x), TOMO_DATA['LATERAL_ISO_MARGIN']): 19 | pass_result = PASS 20 | message_str = \ 21 | f"Isocenter [{rso.beamset.Beams[0].Isocenter.Annotation.Name}] " \ 22 | f"lateral shift is acceptable: {iso_pos_x} < " \ 23 | f"{TOMO_DATA['LATERAL_ISO_MARGIN']} cm" 24 | else: 25 | pass_result = FAIL 26 | message_str = \ 27 | f"Isocenter [{isocenter.Annotation.Name}]"\ 28 | f" lateral shift is inconsistent with indexing: {iso_pos_x}"\ 29 | f" > {TOMO_DATA['LATERAL_ISO_MARGIN']} cm!" 30 | return pass_result, message_str 31 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_beamset/check_transfer_approved.py: -------------------------------------------------------------------------------- 1 | from typing import NamedTuple, Tuple, Optional 2 | from PlanReview.review_definitions import TOMO_DATA, PASS, FAIL 3 | from PlanReview.utils import get_approval_info 4 | 5 | def check_transfer_approved(rso: NamedTuple, **kwargs: Optional[bool]) -> Tuple[str, str]: 6 | 7 | """Check Transfer Approved 8 | Checks whether a given transfer beamset is approved or not. 9 | 10 | Args: 11 | rso (NamedTuple): ScriptObjects in RayStation containing 12 | [case ('RayStation Case Object'), 13 | exam ('RayStation Exam Object'), 14 | plan ('RayStation Plan Object'), 15 | beamset ('RayStation BeamSet Object'), 16 | db ('RayStation Database Object')] 17 | **kwargs: Additional keyword arguments (none are utilized in this function). 18 | 19 | Returns: 20 | result, message_string (Tuple[str, str]): First element is the status (PASS/FAIL/ALERT), 21 | Second element is the message string 22 | 23 | Pseudocode: 24 | 1. Derive parent and daughter beamset and plan names 25 | 2. Check if daughter beamset and plan exist in case 26 | 3. If they exist, check their approval status 27 | 4. Return PASS or FAIL based on approval status 28 | 29 | Test Patients: 30 | Pass: Needed 31 | Fail: Needed 32 | 33 | """ 34 | parent_beamset_name = rso.beamset.DicomPlanLabel 35 | daughter_plan_name = rso.plan.Name + TOMO_DATA['PLAN_TR_SUFFIX'] 36 | if TOMO_DATA['MACHINES'][1] in rso.beamset.MachineReference['MachineName']: 37 | daughter_machine = TOMO_DATA['MACHINES'][1] 38 | else: 39 | daughter_machine = TOMO_DATA['MACHINES'][0] 40 | 41 | daughter_beamset_name = f"{parent_beamset_name[:8]}" \ 42 | f"{TOMO_DATA['PLAN_TR_SUFFIX']}" \ 43 | f"{daughter_machine[-3:]}" 44 | 45 | plan_names = [plan.Name for plan in rso.case.TreatmentPlans] 46 | beamset_names = [beamset.DicomPlanLabel 47 | for plan in rso.case.TreatmentPlans 48 | for beamset in plan.BeamSets] 49 | 50 | if daughter_beamset_name not in beamset_names or daughter_plan_name not in plan_names: 51 | message_str = f"Beamset: {rso.beamset.DicomPlanLabel}" \ 52 | " is missing a transfer plan!" 53 | return FAIL, message_str 54 | 55 | transfer_beamset = rso.case.TreatmentPlans[daughter_plan_name] \ 56 | .BeamSets[daughter_beamset_name] 57 | approval_status = get_approval_info(rso.plan, transfer_beamset) 58 | 59 | if approval_status.beamset_approved: 60 | message_str = f"Transfer Beamset: {transfer_beamset.DicomPlanLabel}" \ 61 | f" was approved by {approval_status.beamset_reviewer}" \ 62 | f" on {approval_status.beamset_approval_time}" 63 | return PASS, message_str 64 | else: 65 | message_str = f"Beamset: {transfer_beamset.DicomPlanLabel}" \ 66 | " is not approved" 67 | return FAIL, message_str 68 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_beamset/parse_beamset_selection.py: -------------------------------------------------------------------------------- 1 | import re 2 | from PlanReview.review_definitions import ALERT, PASS 3 | 4 | 5 | def determine_energy(energy_str): 6 | energy_regex = re.compile(r"Energy:\s?([^,]*)") 7 | energy_match = energy_regex.search(energy_str) 8 | if energy_match and energy_match.group(1).strip(): 9 | # If there is some text before the comma, return it 10 | return f'Energy: {energy_match.group(1).strip()}' 11 | else: 12 | # If the field is empty or contains only whitespace 13 | return '' 14 | 15 | 16 | def determine_iso_method(iso_str): 17 | # Regular expressions to extract the values for ISO_0, ROI, and POI 18 | iso_0_regex = re.compile(r"ISO_0':\s*'([^']*)'") 19 | roi_regex = re.compile(r"ROI':\s*'([^']*)'") 20 | poi_regex = re.compile(r"POI':\s*'([^']*)'") 21 | 22 | # Extract values 23 | iso_0_value = iso_0_regex.search(iso_str) 24 | roi_value = roi_regex.search(iso_str) 25 | poi_value = poi_regex.search(iso_str) 26 | 27 | # Check each key and return the method with its value 28 | if iso_0_value and iso_0_value.group(1): 29 | return f'Existing Isocenter: {iso_0_value.group(1)}' 30 | elif roi_value and roi_value.group(1): 31 | return f'Center of Roi: {roi_value.group(1)}' 32 | elif poi_value and poi_value.group(1): 33 | return f'At Poi: {poi_value.group(1)}' 34 | return '' 35 | 36 | 37 | def extract_match(regex, text): 38 | """Helper function to extract match using regex.""" 39 | match = re.search(regex, text) 40 | return match.group(1) if match else None 41 | 42 | 43 | def parse_beamset_selection(rso, **kwargs): 44 | """ 45 | Parse the log_messages for a specific beamset dialog and return the dialog choices as a dictionary. 46 | 47 | Args: 48 | rso (NamedTuple): A namedtuple containing the beamset. 49 | **kwargs: Additional keyword arguments. Options include: 50 | 'LOG_MESSAGES' (Optional[list]): A list of log_messages to search through. 51 | 52 | Returns: 53 | tuple: A tuple containing the pass result and a message. 54 | """ 55 | from PlanReview.utils.review_api_versions import get_unique_id_beamset 56 | log_messages = kwargs.get('LOG_MESSAGES', []) 57 | beamset_name = rso.beamset.DicomPlanLabel 58 | beamset_id = get_unique_id_beamset(rso.beamset) 59 | 60 | # Regular expressions for parsing log messages 61 | regexes = { 62 | 'Dialog': re.compile(r'Dialog:\s?Beamset Template Selection'), 63 | 'Name': re.compile(r'TemplateName:\s?(.*?)(?=\s*,\t)'), 64 | 'Isocenter': re.compile(r'Iso:\s?\{([^}]+)\}'), 65 | 'Energy': re.compile(r'Energy:\s?(.*?)(?=,\t|$)') 66 | } 67 | 68 | # Default result setup 69 | pass_result = ALERT 70 | message = f'Beamset {beamset_name} not set by script' 71 | 72 | # Parsing log messages 73 | for log_line in log_messages: 74 | if 'Beamset Template Selection' in log_line['Message'] and \ 75 | (beamset_name == log_line['Beamset'] or beamset_id in log_line['BeamsetID']): 76 | 77 | template_name = extract_match(regexes['Name'], log_line['Message']) 78 | iso_info = extract_match(regexes['Isocenter'], log_line['Message']) 79 | energy = extract_match(regexes['Energy'], log_line['Message']) 80 | 81 | if template_name: 82 | iso_method = determine_iso_method(iso_info) 83 | energy_string = determine_energy(energy) 84 | pass_result = PASS 85 | message = f'Template: {template_name}, Isocenter: {iso_method}, Energy: {energy_string}' 86 | 87 | return pass_result, message 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_examination/__init__.py: -------------------------------------------------------------------------------- 1 | from PlanReview.qa_tests.test_examination.exam_review_tests import * 2 | from PlanReview.qa_tests.test_examination.get_exam_level_tests \ 3 | import get_exam_level_tests 4 | from PlanReview.qa_tests.test_examination.compare_exam_data_to_preplan import \ 5 | compare_exam_data_to_preplan 6 | from PlanReview.qa_tests.test_examination.check_exam_data import \ 7 | check_exam_data 8 | from PlanReview.qa_tests.test_examination.check_exam_date import \ 9 | check_exam_date 10 | from PlanReview.qa_tests.test_examination.check_localization import \ 11 | check_localization 12 | from PlanReview.qa_tests.test_examination.check_image_extent import \ 13 | check_image_extent 14 | from PlanReview.qa_tests.test_examination.get_targets_si_extent import \ 15 | get_targets_si_extent 16 | from PlanReview.qa_tests.test_examination.get_si_extent import get_si_extent 17 | from PlanReview.qa_tests.test_examination.get_roi_list import get_roi_list 18 | from PlanReview.qa_tests.test_examination.check_contour_gaps import \ 19 | check_contour_gaps 20 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_examination/check_couch_extent.py: -------------------------------------------------------------------------------- 1 | from typing import NamedTuple, Tuple, Optional, List 2 | from PlanReview.review_definitions import ( 3 | FIELD_OF_VIEW_PREFERENCES, TOMO_DATA, TRUEBEAM_DATA, PASS, FAIL) 4 | from .get_si_extent import get_si_extent 5 | 6 | 7 | def format_extent(extent: List[float]) -> str: 8 | """Formats a list of floats as a string representation of extents.""" 9 | return '[' + ('%.2f ' * len(extent)) % tuple(extent) + ']' 10 | 11 | 12 | def fetch_support_rois(rg, supports: List[str]) -> List[str]: 13 | """Fetches the list of support ROIs from rg.""" 14 | return [r.OfRoi.Name for r in rg if r.OfRoi.Name in supports] 15 | 16 | 17 | def check_couch_extent(rso: NamedTuple, **kwargs: Optional[List[float]]) -> Tuple[str, str]: 18 | """Check Couch Extent 19 | Check if PTV volume extents have supports under them. 20 | 21 | Args: 22 | rso (NamedTuple): ScriptObjects in RayStation containing 23 | [case ('RayStation Case Object'), 24 | exam ('RayStation Exam Object'), 25 | plan ('RayStation Plan Object'), 26 | beamset ('RayStation BeamSet Object'), 27 | db ('RayStation Database Object')] 28 | **kwargs: Additional keyword arguments, options include: 29 | 'TARGET_EXTENT' (Optional): Extent of the target to compare with the couch extent. 30 | 31 | Returns: 32 | result, message_string (Tuple[str, str]): First element is the status (PASS/FAIL/ALERT), 33 | Second element is the message string 34 | 35 | Pseudocode: 36 | 1. Retrieve 'TARGET_EXTENT' from kwargs 37 | 2. Get support structure extents 38 | 3. Determine if the couch extent is adequate for the target 39 | 4. Prepare and return message string and result (PASS/FAIL/ALERT) 40 | 41 | Test Patients: 42 | Pass: Plan_Review_Script_Testing, ZZUWQA_SCTest_01May2022: Case THI: Anal_THI: Anal_THI 43 | Fail: (bad couch): Plan_Review_Script_Testing, ZZUWQA_SCTest_01May2022: Case THI: ChwL_3DC: SCV PAB; (no couch): Plan_Review_Script_Testing, ZZUWQA_SCTest_01May2022: Case THI: Pros_VMA: Pros_VMA 44 | """ 45 | target_extent = kwargs.get('TARGET_EXTENT') 46 | buffer = FIELD_OF_VIEW_PREFERENCES['SI_PTV_BUFFER'] 47 | 48 | # Fetch support structure extents 49 | rg = rso.case.PatientModel.StructureSets[rso.exam.Name].RoiGeometries 50 | supports = TOMO_DATA['SUPPORTS'] + TRUEBEAM_DATA['SUPPORTS'] 51 | support_rois = fetch_support_rois(rg, supports) 52 | couch_extent = get_si_extent(rso=rso, roi_list=supports) 53 | 54 | # Use helper function to format extent strings 55 | z_str = format_extent(couch_extent) if couch_extent else None 56 | t_str = format_extent([target_extent[0] - buffer, target_extent[1] + buffer]) if target_extent else None 57 | 58 | # Determine pass/fail status 59 | if not couch_extent: 60 | message_str = 'No support structures found. No couch check possible' 61 | pass_result = FAIL 62 | elif couch_extent[1] >= (target_extent[1] + buffer) and couch_extent[0] <= (target_extent[0] - buffer): 63 | message_str = f'Supports ({", ".join(support_rois)}) span {z_str} and is at least {buffer:.0f} cm larger than S/I target extent {t_str}' 64 | pass_result = PASS 65 | else: 66 | message_str = f'Support extent ({", ".join(support_rois)}): {z_str} is not fully under the target. (SMALLER THAN S/I target extent: {t_str} ± {buffer:.1f} cm)' 67 | pass_result = FAIL 68 | 69 | return pass_result, message_str 70 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_examination/check_exam_date.py: -------------------------------------------------------------------------------- 1 | from typing import NamedTuple, Tuple 2 | from dateutil import parser 3 | import datetime 4 | from PlanReview.review_definitions import DAYS_SINCE_SIM, PASS, FAIL, ALERT 5 | from PlanReview.utils import get_approval_info 6 | 7 | 8 | def check_exam_date(rso: NamedTuple) -> Tuple[str, str]: 9 | """ Check Exam Date 10 | Check if the examination date occurred within the tolerance set by DAYS_SINCE_SIM. 11 | First, it checks for a RayStation approval date, then the last saved by, and 12 | if not found, the current time is used. 13 | 14 | Args: 15 | rso (NamedTuple): ScriptObjects in RayStation containing 16 | [case ('RayStation Case Object'), 17 | exam ('RayStation Exam Object'), 18 | plan ('RayStation Plan Object'), 19 | beamset ('RayStation BeamSet Object'), 20 | db ('RayStation Database Object')] 21 | 22 | Returns: 23 | result, message_string (Tuple[str, str]): First element is the status (PASS/FAIL/ALERT), 24 | Second element is the message string 25 | 26 | Pseudocode: 27 | 1. Extract DICOM data from 'rso' 28 | 2. Retrieve the approval status from 'get_approval_info' function 29 | 3. Parse or determine the DICOM date and current time 30 | 4. Calculate the elapsed days between DICOM date and current time 31 | 5. Determine the result (PASS/FAIL/ALERT) based on elapsed days and tolerance 32 | 6. Build and return the result and message string 33 | 34 | Test Patients: 35 | Pass: ZZ_RayStation^CT_Artifact, 20210408SPF, Case 1: TB_HFS_ArtFilt: Lsha_3DC_R0A0 (all but Gender) 36 | Fail: Script_Testing^Plan_Review, #ZZUWQA_ScTest_01May2022, ChwL: Bolus_Roi_Check_Fail: ChwL_VMA_R0A0 37 | """ 38 | tolerance: int = DAYS_SINCE_SIM # Days since simulation 39 | dcm_data: list = list( 40 | rso.exam.GetStoredDicomTagValueForVerification(Group=0x0008, Element=0x0020).values()) 41 | approval_status = get_approval_info(rso.plan, rso.beamset) 42 | 43 | if dcm_data: 44 | try: 45 | dcm_date: datetime.datetime = parser.parse(dcm_data[0]) 46 | except TypeError: 47 | DEFAULT_DATE: datetime.datetime = datetime.datetime(datetime.MINYEAR, 1, 1) 48 | dcm_date = parser.parse(str(DEFAULT_DATE)) 49 | 50 | if approval_status.beamset_approved: 51 | current_time: datetime.datetime = parser.parse(str(rso.beamset.Review.ReviewTime)) 52 | else: 53 | try: 54 | # Use last saved date if plan not approved 55 | current_time: datetime.datetime = parser.parse( 56 | str(rso.beamset.ModificationInfo.ModificationTime)) 57 | except AttributeError: 58 | current_time: datetime.datetime = datetime.datetime.now() 59 | 60 | elapsed_days: int = (current_time - dcm_date).days 61 | 62 | if elapsed_days <= tolerance: 63 | message_str: str = "Exam {} acquired {} within {} days ({} days) of Plan Date {}" \ 64 | .format(rso.exam.Name, dcm_date.date(), tolerance, elapsed_days, 65 | current_time.date()) 66 | pass_result: str = PASS 67 | else: 68 | message_str: str = "Exam {} acquired {} GREATER THAN {} days ({} days) of Plan Date {}" \ 69 | .format(rso.exam.Name, dcm_date.date(), tolerance, elapsed_days, 70 | current_time.date()) 71 | pass_result: str = FAIL 72 | else: 73 | message_str: str = "Exam {} has no apparent study date!".format(rso.exam.Name) 74 | pass_result: str = ALERT 75 | return pass_result, message_str 76 | 77 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_examination/check_image_extent.py: -------------------------------------------------------------------------------- 1 | from PlanReview.review_definitions import FIELD_OF_VIEW_PREFERENCES,PASS,FAIL 2 | 3 | 4 | def check_image_extent(rso, **kwargs): 5 | """ 6 | Check if the image extent is long enough to cover the target extent and a buffer 7 | 8 | Args: 9 | rso: (namedtuple): Named tuple of ScriptObjects 10 | Returns: 11 | (Pass/Fail/Alert, Message to Display) 12 | Test Patient: 13 | 14 | Pass TODO 15 | Fail: TODO 16 | """ 17 | # 18 | # Target length 19 | target_extent = kwargs.get('TARGET_EXTENT') 20 | # 21 | # Tolerance for SI extent 22 | buffer = FIELD_OF_VIEW_PREFERENCES['SI_PTV_BUFFER'] 23 | # 24 | # Get image slices 25 | bb = rso.exam.Series[0].ImageStack.GetBoundingBox() 26 | bb_z = [bb[0]['z'], bb[1]['z']] 27 | z_extent = [min(bb_z), max(bb_z)] 28 | buffered_target_extent = [target_extent[0] - buffer, target_extent[1] + buffer] 29 | # 30 | # Nice strings for output 31 | z_str = '[' + ('%.2f ' * len(z_extent)) % tuple(z_extent) + ']' 32 | t_str = '[' + ('%.2f ' * len(buffered_target_extent)) % tuple(buffered_target_extent) + ']' 33 | if not target_extent: 34 | message_str = 'No targets found of type Ptv, image extent could not be evaluated' 35 | pass_result = FAIL 36 | elif z_extent[1] >= buffered_target_extent[1] and z_extent[0] <= buffered_target_extent[0]: 37 | message_str = f'Planning image extent {z_str} and is at ' \ 38 | f'least {buffer:.1f} larger than S/I target; extent ' \ 39 | f'{t_str}' 40 | pass_result = PASS 41 | elif z_extent[1] < buffered_target_extent[1] or z_extent[0] > buffered_target_extent[0]: 42 | message_str = f'Planning Image extent:{z_str} is insufficient for' \ 43 | f' accurate calculation. (SMALLER THAN than S/I target' \ 44 | f' extent: {t_str} \xB1 {buffer:.1f} cm)' 45 | pass_result = FAIL 46 | else: 47 | message_str = 'Target length could not be compared to image set' 48 | pass_result = FAIL 49 | return pass_result, message_str 50 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_examination/check_localization.py: -------------------------------------------------------------------------------- 1 | from PlanReview.review_definitions import PASS, FAIL 2 | 3 | 4 | def check_localization(rso): 5 | poi_coord = {} 6 | localization_found = False 7 | for p in rso.case.PatientModel.StructureSets[rso.exam.Name].PoiGeometries: 8 | if p.OfPoi.Type == 'LocalizationPoint': 9 | point = p 10 | poi_coord = p.Point 11 | localization_found = True 12 | break 13 | if poi_coord: 14 | message_str = f"Localization point {point.OfPoi.Name} exists and " \ 15 | f"has coordinates." 16 | pass_result = PASS 17 | elif localization_found: 18 | message_str = f"Localization point {point.OfPoi.Name} does not " \ 19 | f"have coordinates." 20 | pass_result = FAIL 21 | else: 22 | message_str = "No point of type LocalizationPoint found" 23 | pass_result = FAIL 24 | return pass_result, message_str 25 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_examination/check_support_material.py: -------------------------------------------------------------------------------- 1 | 2 | from PlanReview.review_definitions import MATERIALS, FAIL, PASS 3 | from .get_roi_list import get_roi_list 4 | 5 | 6 | def check_support_material(rso): 7 | """ 8 | For the list of accepted supports defined in ReviewDefinitions.py->Materials 9 | assure the correct material name has been assigned 10 | :param rso: NamedTuple of ScriptObjects in Raystation [case,exam,plan,beamset,db] 11 | 12 | :return: message (list str): [Pass_Status, Message String] 13 | """ 14 | rois = get_roi_list(rso.case) 15 | message_str = "" 16 | correct_supports = [] 17 | for r in rois: 18 | try: 19 | correct_material_name = MATERIALS[r] 20 | except KeyError: 21 | continue 22 | try: 23 | material_name = rso.case.PatientModel.RegionsOfInterest[r].RoiMaterial.OfMaterial.Name 24 | if material_name != correct_material_name: 25 | message_str += r + ' incorrectly assigned as ' + material_name 26 | else: 27 | correct_supports.append(r) 28 | except AttributeError: 29 | message_str += r + ' not overriden! ' 30 | if message_str: 31 | pass_result = FAIL 32 | else: 33 | pass_result = PASS 34 | if correct_supports: 35 | message_str = "Supports [" + ",".join(correct_supports) + "] Correctly overriden" 36 | else: 37 | message_str = "No supports found" 38 | return pass_result, message_str -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_examination/compare_patient_orientation_to_preplan.py: -------------------------------------------------------------------------------- 1 | from typing import NamedTuple, Tuple 2 | from PlanReview.review_definitions import PASS, FAIL, PATIENT_ORIENTATIONS 3 | from PlanReview.utils import KEY_PATIENT_ORIENTATION 4 | 5 | 6 | def compare_patient_orientation_to_preplan(rso: NamedTuple, **kwargs) -> Tuple[str, str]: 7 | """ Check Patient Orientation Against Exam Data 8 | For an input patient orientation, make sure the patient orientation matches 9 | the expected orientation. 10 | 11 | Args: 12 | rso (NamedTuple): ScriptObjects in RayStation containing 13 | [case ('RayStation Case Object'), 14 | exam ('RayStation Exam Object'), 15 | plan ('RayStation Plan Object'), 16 | beamset ('RayStation BeamSet Object'), 17 | db ('RayStation Database Object')] 18 | 19 | Returns: 20 | result, message_string (Tuple[str, str]): 21 | First element is the status (PASS/FAIL/ALERT), 22 | Second element is the message string 23 | 24 | Pseudocode: 25 | 1. Extract 'patient_orientation' from user-entered value in preplan tab 26 | 2. Retrieve the patient orientation from the exam in use 27 | 3. Compare the two patient orientations 28 | 29 | Test Patients: 30 | Pass: Any patient with correct orientation. 31 | Fail: Any patient, just deliberately enter the wrong orientation 32 | 33 | """ 34 | values = kwargs.get('VALUES') 35 | simulation_patient_orientation = values[KEY_PATIENT_ORIENTATION] 36 | examination_patient_orientation = rso.exam.PatientPosition 37 | if examination_patient_orientation != simulation_patient_orientation: 38 | return FAIL, f"For exam: {rso.exam.Name}: patient orientation from CT SIM: " \ 39 | f"{simulation_patient_orientation} " \ 40 | f"DOES NOT MATCH RS: {PATIENT_ORIENTATIONS[examination_patient_orientation]}" 41 | return PASS, f"For exam: {rso.exam.Name}: patient orientation from CT SIM: "\ 42 | f"{simulation_patient_orientation} " \ 43 | f"matches RS: {PATIENT_ORIENTATIONS[examination_patient_orientation]}" 44 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_examination/get_exam_level_tests.py: -------------------------------------------------------------------------------- 1 | from PlanReview.review_definitions import REVIEW_LEVELS 2 | from .compare_exam_data_to_preplan import compare_exam_data_to_preplan 3 | from .check_exam_date import check_exam_date 4 | from .check_exam_data import check_exam_data 5 | from .compare_patient_orientation_to_preplan import compare_patient_orientation_to_preplan 6 | from .check_localization import check_localization 7 | from .check_axial_orientation import check_axial_orientation 8 | from .check_image_extent import check_image_extent 9 | from .check_couch_extent import check_couch_extent 10 | from .check_fov_overlap_external import check_fov_overlap_external 11 | from .check_contour_gaps import check_contour_gaps 12 | from .check_support_material import check_support_material 13 | from .get_si_extent import get_si_extent 14 | 15 | 16 | def get_exam_level_tests(rso, values=None): 17 | if not rso.exam: 18 | return {} 19 | # 20 | # Get target length 21 | target_extent = get_si_extent(rso, types=['Ptv']) 22 | patient_checks_dict = { 23 | f"{REVIEW_LEVELS['PREPLAN_DATA']}::DICOM RayStation Comparison": 24 | (check_exam_data, {}), 25 | f"{REVIEW_LEVELS['PREPLAN_DATA']}::Exam Date Is Recent": 26 | (check_exam_date, {}), 27 | f"{REVIEW_LEVELS['PATIENT_MODEL']}::Localization Point Exists": 28 | (check_localization, {}), 29 | f"{REVIEW_LEVELS['PATIENT_MODEL']}::Contours are interpolated": 30 | (check_contour_gaps, {}), 31 | f"{REVIEW_LEVELS['PATIENT_MODEL']}::Supports correctly overriden": 32 | (check_support_material, {}), 33 | f"{REVIEW_LEVELS['PREPLAN_DATA']}::Image Is Axially Oriented": 34 | (check_axial_orientation, {}), 35 | } 36 | # TODO: If the target extent is NONE, then we ought to try and get one 37 | # from dose 38 | if target_extent: 39 | patient_checks_dict.update({ 40 | f"{REVIEW_LEVELS['PREPLAN_DATA']}::Image extent sufficient": 41 | (check_image_extent, {'TARGET_EXTENT': target_extent}), 42 | f"{REVIEW_LEVELS['PATIENT_MODEL']}::Couch extent sufficient": 43 | (check_couch_extent, {'TARGET_EXTENT': target_extent}), 44 | f"{REVIEW_LEVELS['PATIENT_MODEL']}::Edge of scan overlaps patient at key slices": 45 | (check_fov_overlap_external, {'TARGET_EXTENT': target_extent}), 46 | }) 47 | if values: 48 | patient_checks_dict.update({ 49 | f"{REVIEW_LEVELS['PREPLAN_DATA']}::Data Matches CT Document": 50 | (compare_exam_data_to_preplan, {'VALUES': values}), 51 | }) 52 | patient_checks_dict.update({ 53 | f"{REVIEW_LEVELS['PATIENT_MODEL']}::Patient Orientation Matches CT Document": 54 | (compare_patient_orientation_to_preplan, {'VALUES': values}), 55 | }) 56 | return patient_checks_dict 57 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_examination/get_roi_list.py: -------------------------------------------------------------------------------- 1 | def get_roi_list(case, exam_name=None): 2 | """ 3 | Get a list of all rois 4 | Args: 5 | case: RS case object 6 | 7 | Returns: 8 | roi_list: [str1,str2,...]: a list of roi names 9 | """ 10 | roi_list = [] 11 | if exam_name: 12 | structure_sets = [case.PatientModel.StructureSets[exam_name]] 13 | else: 14 | structure_sets = [s for s in case.PatientModel.StructureSets] 15 | 16 | for s in structure_sets: 17 | for r in s.RoiGeometries: 18 | if r.OfRoi.Name not in roi_list: 19 | roi_list.append(r.OfRoi.Name) 20 | return roi_list -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_examination/get_si_extent.py: -------------------------------------------------------------------------------- 1 | def get_si_extent(rso, types=None, roi_list=None): 2 | rg = rso.case.PatientModel.StructureSets[rso.exam.Name].RoiGeometries 3 | initial = [-1000, 1000] 4 | extent = [-1000, 1000] 5 | # Generate a list to search 6 | type_list = [] 7 | rois = [] 8 | if types: 9 | type_list = [r.OfRoi.Name for r in rg if r.OfRoi.Type in types and r.HasContours()] 10 | if roi_list: 11 | rois = [r.OfRoi.Name for r in rg if r.OfRoi.Name in roi_list and r.HasContours()] 12 | check_list = list(set(type_list + rois)) 13 | 14 | for r in rg: 15 | if r.OfRoi.Name in check_list: 16 | bb = r.GetBoundingBox() 17 | rg_max = bb[0]['z'] 18 | rg_min = bb[1]['z'] 19 | if rg_max > extent[0]: 20 | extent[0] = rg_max 21 | if rg_min < extent[1]: 22 | extent[1] = rg_min 23 | if extent == initial: 24 | return None 25 | else: 26 | return extent 27 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_examination/get_targets_si_extent.py: -------------------------------------------------------------------------------- 1 | def get_targets_si_extent(rso): 2 | types = ['Ptv'] 3 | rg = rso.case.PatientModel.StructureSets[rso.exam.Name].RoiGeometries 4 | extent = [-1000., 1000] 5 | for r in rg: 6 | if r.OfRoi.Type in types and r.HasContours(): 7 | bb = r.GetBoundingBox() 8 | rg_max = bb[0]['z'] 9 | rg_min = bb[1]['z'] 10 | if rg_max > extent[0]: 11 | extent[0] = rg_max 12 | if rg_min < extent[1]: 13 | extent[1] = rg_min 14 | return extent -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_plan/__init__.py: -------------------------------------------------------------------------------- 1 | from PlanReview.qa_tests.test_plan.check_plan_approved import check_plan_approved 2 | from PlanReview.qa_tests.test_plan.get_plan_level_tests import get_plan_level_tests 3 | from PlanReview.qa_tests.test_plan.parse_order_selection \ 4 | import parse_order_selection 5 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_plan/check_plan_approved.py: -------------------------------------------------------------------------------- 1 | # Check if the plan is approved by an MD 2 | from PlanReview.utils import get_approval_info 3 | from PlanReview.utils.get_approval_info import find_groupname_by_userid, is_valid_approver, find_username_by_userid 4 | from PlanReview.review_definitions import PASS, FAIL, ALERT 5 | 6 | 7 | VALID_APPROVAL_GROUPS = ["Oncologist"] 8 | 9 | 10 | def check_plan_approved(rso, **kwargs): 11 | """ 12 | Check if a plan is approved 13 | Args: rso: Named Tuple of RS script objects 14 | do_physics_review: Bool: True if expected status of plan is approved 15 | 16 | Returns: 17 | message: [str1, ...]: [parent_key, child_key, child_key display, result_value] 18 | 19 | """ 20 | physics_review = kwargs.get('do_physics_review') 21 | approval_status = get_approval_info(rso.plan, rso.beamset) 22 | if approval_status.plan_approved: 23 | if not approval_status.plan_reviewer: 24 | message_str = f"Plan: {rso.plan.Name} does not have valid approval data" 25 | pass_result = ALERT 26 | return pass_result, message_str 27 | group_name = find_groupname_by_userid(approval_status.plan_reviewer) 28 | user_name = find_username_by_userid(approval_status.plan_reviewer) 29 | if is_valid_approver(group_name, VALID_APPROVAL_GROUPS): 30 | message_str = f"Plan: {rso.plan.Name} was approved by " \ 31 | f"{user_name}, (Staff {group_name}) " \ 32 | f"on {approval_status.plan_approval_time}" 33 | pass_result = PASS 34 | else: 35 | message_str = f"Plan: {rso.plan.Name} approval INVALID. Approved by " \ 36 | f"{user_name}, ({group_name}) " \ 37 | f"on {approval_status.plan_approval_time}" 38 | pass_result = FAIL 39 | else: 40 | message_str = "Plan: {} is not approved".format( 41 | rso.plan.Name) 42 | if physics_review: 43 | pass_result = FAIL 44 | else: 45 | message_str += " (Dosimetry Safety Review)" 46 | pass_result = PASS 47 | return pass_result, message_str 48 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_plan/get_plan_level_tests.py: -------------------------------------------------------------------------------- 1 | from PlanReview.qa_tests.test_plan.check_plan_approved import check_plan_approved 2 | from PlanReview.review_definitions import REVIEW_LEVELS 3 | 4 | 5 | def get_plan_level_tests(rso, physics_review=True): 6 | if not rso.plan: 7 | return {} 8 | plan_checks_dict = { 9 | f"{REVIEW_LEVELS['PLAN_DATA']}::Plan approval status": 10 | (check_plan_approved, {"do_physics_review": physics_review}), 11 | } 12 | return plan_checks_dict 13 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_plan/parse_order_selection.py: -------------------------------------------------------------------------------- 1 | import re 2 | from PlanReview.review_definitions import ALERT, PASS 3 | 4 | 5 | def parse_order_selection(beamset_name, messages, dialog_key): 6 | """ 7 | Using the beamset name and the log file, use regex to find a phrase 8 | identifying the 9 | template name used in the treatment planning order for this beamset. 10 | Args: 11 | beamset_name: name of current beamset 12 | messages: a dictionary of log data. See retrieve_logs.py 13 | Keys are: 'Date', 'Time', 'Log_Level', 'Message', 'Deepest_Context', 14 | 'Case', 'Exam', 'Plan', 'Beamset', 'PlanID', 'BeamsetID', 'Source' 15 | dialog_key: key to be used for top level entry in the tree 16 | 17 | Returns: 18 | 19 | 20 | """ 21 | # Parse the dialogs for specific key phrases related to the beamset dialog 22 | beamset_template_searches = { 23 | 'Dialog': re.compile(r'(Treatment Planning Order selected:\s?)(.*)$')} 24 | template_data = {dialog_key: (ALERT, 25 | f'Treatment Planning for Beamset ' 26 | f'{beamset_name} goals manually defined')} 27 | for m in messages: 28 | template_search = re.search(beamset_template_searches['Dialog'], m['Message']) 29 | 30 | if template_search and beamset_name in m['Beamset']: 31 | # Found the TPO Dialog. Lets display it 32 | # Note that it is a little sloppy, since this will always grab 33 | # the last match in the log file 34 | k, template_name = template_search.groups() 35 | template_data[dialog_key] = (PASS, template_name) 36 | return template_data 37 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_sandbox/__init__.py: -------------------------------------------------------------------------------- 1 | from PlanReview.qa_tests.test_sandbox.get_sandbox_level_tests import get_sandbox_level_tests 2 | from PlanReview.qa_tests.test_sandbox.check_max_dose_point import check_max_dose_point 3 | 4 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_sandbox/check_c_arm_modulation.py: -------------------------------------------------------------------------------- 1 | from PlanReview.review_definitions import (PASS, ALERT, FAIL,) 2 | 3 | def check_c_arm_modulation(rso): 4 | """ 5 | Compute the modulation of the beamset. 6 | 7 | Pseudo-code: 8 | * Determine if the beamset is C-arm based 9 | * Determine if the plan is VMAT or 3D Conformal 10 | * If those conditions are met, determine the dose from the 11 | prescription 12 | * Sum the beam MU 13 | * The ratio of total MU to the prescription in cGy should be 14 | evaluated 15 | 16 | Args: 17 | rso (NamedTuple): RayStation script objects 18 | Returns: 19 | tuple: A tuple containing the status and message 20 | 21 | Test Patient: 22 | PASS: 23 | ALERT: 24 | Note jupyter notebook: 25 | """ 26 | 27 | -------------------------------------------------------------------------------- /library/PlanReview/qa_tests/test_sandbox/get_sandbox_level_tests.py: -------------------------------------------------------------------------------- 1 | from .compute_vmat_beam_properties import compute_vmat_beam_properties 2 | from .check_max_dose_point import check_max_dose_point 3 | from PlanReview.qa_tests.test_beamset.compare_rx_to_preplan import match_rx_to_preplan 4 | from PlanReview.review_definitions import REVIEW_LEVELS 5 | 6 | 7 | def get_sandbox_level_tests(rso, physics_review=True, values=None): 8 | # Don't proceed if no beamset is defined 9 | if not rso.beamset: 10 | return {} 11 | 12 | sandbox_checks_dict = { 13 | 14 | f"{REVIEW_LEVELS['SANDBOX']}::Maximum Dose Inside Targets": 15 | (check_max_dose_point, {}), 16 | f"{REVIEW_LEVELS['SANDBOX']}::Beamset Complexity": 17 | (compute_vmat_beam_properties, {}), 18 | } 19 | if values: 20 | sandbox_checks_dict.update({ 21 | f"{REVIEW_LEVELS['SANDBOX']}::Target Doses Match Treatment Planning Order": 22 | (match_rx_to_preplan, {'VALUES': values}) 23 | }) 24 | return sandbox_checks_dict 25 | -------------------------------------------------------------------------------- /library/PlanReview/testing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/library/PlanReview/testing/__init__.py -------------------------------------------------------------------------------- /library/PlanReview/testing/automated_review_testing.py: -------------------------------------------------------------------------------- 1 | """ 2 | Create a function that will loop over a list of test patient and preplan data and run the review_test_patient function 3 | """ 4 | def review_test_patient(rso, preplan_data, review_type='Physics'): 5 | """ 6 | Function to review a test patient by setting the plan and beamset, launching the review GUI, 7 | filling in preplan data, and interacting with available buttons. 8 | 9 | Parameters: 10 | - rso: NamedTuple of ScriptObjects in RayStation [case, exam, plan, beamset, db] 11 | - preplan_data: Dictionary containing preplan data to fill in the GUI 12 | - review_type: String indicating the type of review, either 'Physics' or 'Dosimetry' 13 | 14 | Returns: None 15 | """ 16 | import connect 17 | import logging 18 | import PySimpleGUI as Sg 19 | 20 | try: 21 | # Step 1: Open test patient and set current plan and beamset 22 | case = rso.case 23 | plan = rso.plan 24 | beamset = rso.beamset 25 | 26 | # Set the current case, plan, and beamset 27 | case.SetCurrent() 28 | plan.SetCurrent() 29 | beamset.SetCurrent() 30 | 31 | # Step 2: Launch the review GUI 32 | gui_state_manager = launch_physics_review_gui(rso, review_type=review_type) 33 | 34 | # Step 3: Fill in preplan data 35 | for key, value in preplan_data.items(): 36 | element = gui_state_manager.window[key] 37 | if element.Type == Sg.ELEM_TYPE_INPUT or element.Type == Sg.ELEM_TYPE_COMBO: 38 | element.Update(value) 39 | elif element.Type == Sg.ELEM_TYPE_RADIO: 40 | element.Update(value) 41 | 42 | # Step 4: Hit all buttons available to a user 43 | for button in gui_state_manager.window.AllKeysDict: 44 | element = gui_state_manager.window[button] 45 | if element.Type == Sg.ELEM_TYPE_BUTTON: 46 | element.Click() 47 | 48 | # Step 5: Check for errors and alert if any are found 49 | if gui_state_manager.window.FindElementWithFocus(): 50 | error_message = gui_state_manager.window.FindElementWithFocus().Get() 51 | if 'Error' in error_message: 52 | raise Exception(error_message) 53 | 54 | except Exception as e: 55 | logging.error(f"An error occurred: {e}") 56 | Sg.Popup(f"An error occurred during the review: {e}") 57 | 58 | # Example usage 59 | from collections import namedtuple 60 | 61 | # Assuming rso is already defined with the required NamedTuple 62 | rso = namedtuple('RSO', ['case', 'exam', 'plan', 'beamset', 'db']) 63 | 64 | # Example preplan data 65 | preplan_data = { 66 | 'KEY_SIM_DATE': '2024-06-01', 67 | 'KEY_SLICES': '100', 68 | 'KEY_PATIENT_ORIENTATION': 'HFS', 69 | 'KEY_IMD+KEY_RADIO-YES': True, 70 | 'KEY_PRIOR_RT+KEY_RADIO-NO': True, 71 | 'KEY_SITE_SELECT': 'Head and Neck', 72 | 'KEY_PROTOCOL_SELECT': 'Protocol A', 73 | 'KEY_ORDER_SELECT': 'Order 1', 74 | 'KEY_IMAGING_FREQ': 'Weekly', 75 | 'KEY_TREAT_FREQ': 'Daily', 76 | # Beamset and target fields 77 | 'KEY_BEAMSET_COUNT': 2, 78 | ('KEY_BEAMSET_SELECT', 0): 'Beamset 1', 79 | ('KEY_BEAMSET_SELECT', 1): 'Beamset 2', 80 | ('KEY_BEAMSET_TARGET_COUNT', 0): 1, 81 | ('KEY_BEAMSET_TARGET_COUNT', 1): 2, 82 | ('KEY_BEAMSET_DOSE', 0, 0): '50', 83 | ('KEY_BEAMSET_DOSE', 1, 0): '60', 84 | ('KEY_BEAMSET_DOSE', 1, 1): '30', 85 | ('KEY_BEAMSET_FRACTION_DOSE', 0, 0): '2.5', 86 | ('KEY_BEAMSET_FRACTION_DOSE', 1, 0): '2.0', 87 | ('KEY_BEAMSET_FRACTION_DOSE', 1, 1): '1.5', 88 | ('KEY_BEAMSET+KEY_FRACTIONS', 0): 20, 89 | ('KEY_BEAMSET+KEY_FRACTIONS', 1): 30, 90 | # Add more preplan data as needed 91 | } 92 | 93 | review_test_patient(rso, preplan_data) 94 | -------------------------------------------------------------------------------- /library/PlanReview/testing/test_create_manual_tab.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from PlanReview.guis.create_physics_manual_tab import ( 3 | is_valid_automated_test, 4 | is_valid_manual_tab, 5 | create_key, 6 | ) 7 | from PlanReview.utils.constants import (KEY_OUT_DOMAIN_NAME, KEY_OUT_DESC, 8 | KEY_OUT_COMMENT, FAILED_AUTOMATED_TEST, 9 | KEY_INPUT_TEXT) 10 | 11 | 12 | @pytest.fixture 13 | def mock_window(): 14 | """ 15 | Mock window for testing. 16 | Replace this with actual window mocking if needed. 17 | """ 18 | return { 19 | create_key('Test1', 'Desc1', KEY_INPUT_TEXT): '', # Add relevant keys and initial values 20 | create_key('Test2', 'Desc2', KEY_INPUT_TEXT): '', 21 | # Add other keys as needed 22 | } 23 | 24 | 25 | def test_is_valid_automated_test(mock_window): 26 | # Test when all comments are 'Script Fail: Comment Needed' 27 | failed_tests = [ 28 | {KEY_OUT_DOMAIN_NAME: 'Test1', KEY_OUT_DESC: 'Desc1', KEY_OUT_COMMENT: FAILED_AUTOMATED_TEST}, 29 | {KEY_OUT_DOMAIN_NAME: 'Test2', KEY_OUT_DESC: 'Desc2', KEY_OUT_COMMENT: FAILED_AUTOMATED_TEST}, 30 | ] 31 | assert is_valid_automated_test(mock_window, failed_tests) is False 32 | 33 | # Test when at least one comment is not 'Script Fail: Comment Needed' 34 | failed_tests[0][KEY_OUT_COMMENT] = 'User Comment' 35 | assert is_valid_automated_test(mock_window, failed_tests) is True 36 | 37 | 38 | def test_is_valid_manual_tab(mock_window): 39 | # Test when all checkboxes are valid 40 | values = { 41 | create_key('Test1', 'Check1', 'RadioYes'): True, 42 | create_key('Test1', 'Check1', 'RadioNo'): False, 43 | create_key('Test1', 'Check1', 'RadioNA'): False, 44 | create_key('Test2', 'Check2', 'RadioYes'): True, 45 | create_key('Test2', 'Check2', 'RadioNo'): False, 46 | create_key('Test2', 'Check2', 'RadioNA'): True, 47 | } 48 | check_boxes = { 49 | 'Test1': [{'KEY_OUT_TEST': 'Check1'}], 50 | 'Test2': [{'KEY_OUT_TEST': 'Check2'}], 51 | } 52 | failed_tests = [] 53 | 54 | assert is_valid_manual_tab(mock_window, values, check_boxes, failed_tests) is True 55 | 56 | # Test when at least one checkbox is not valid 57 | values[create_key('Test2', 'Check2', 'RadioNA')] = False 58 | assert is_valid_manual_tab(mock_window, values, check_boxes, failed_tests) is False 59 | -------------------------------------------------------------------------------- /library/PlanReview/testing/test_doc_generate_physics_pdf.py: -------------------------------------------------------------------------------- 1 | """ 2 | Testing functions for generate_physics_pdf.py 3 | 4 | """ 5 | # Import necessary modules and functions 6 | import pytest 7 | from PlanReview.documentation.generate_physics_pdf import make_paragraph 8 | 9 | # Define test cases 10 | 11 | 12 | def test_make_paragraph_basic(): 13 | # Test with basic input 14 | input_text = "Hello, world!" 15 | expected_output = "

Hello, world!

" 16 | assert make_paragraph(input_text) == expected_output 17 | 18 | 19 | def test_make_paragraph_with_style(): 20 | # Test with custom style 21 | input_text = "Custom style text" 22 | custom_style = {'fontName': 'Helvetica-Bold', 'fontSize': 12} 23 | expected_output = "

Custom style text

" 24 | assert make_paragraph(input_text, style=custom_style) == expected_output 25 | 26 | 27 | def test_make_paragraph_with_special_characters(): 28 | # Test with special characters 29 | input_text = "Special characters: \u00a0 \n \r \t * " 30 | expected_output = "

Special characters:  

     • 

" 31 | assert make_paragraph(input_text) == expected_output 32 | 33 | 34 | # Run the tests 35 | if __name__ == "__main__": 36 | pytest.main() -------------------------------------------------------------------------------- /library/PlanReview/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from PlanReview.utils.get_approval_info import get_approval_info 2 | from PlanReview.utils.get_approval_info import find_groupname_by_userid 3 | from PlanReview.utils.get_approval_info import is_valid_approver 4 | from PlanReview.utils.get_approval_info import find_username_by_userid 5 | from PlanReview.utils.get_roi_names_from_type import get_roi_names_from_type 6 | from PlanReview.utils.subtract_roi_sources import subtract_roi_sources 7 | from PlanReview.utils.get_user_name import get_user_name 8 | from PlanReview.utils.get_roi_list import get_roi_list 9 | from PlanReview.utils.match_roi_name import match_roi_name 10 | from PlanReview.utils.get_machine import get_machine 11 | from PlanReview.utils.constants import * 12 | from PlanReview.utils.comment_to_clipboard import comment_to_clipboard 13 | from PlanReview.utils.get_user_display_parameters import ( 14 | get_user_display_parameters) 15 | from PlanReview.utils.perform_automated_checks import perform_automated_checks 16 | from PlanReview.utils.python_utilities import * 17 | -------------------------------------------------------------------------------- /library/PlanReview/utils/comment_to_clipboard.py: -------------------------------------------------------------------------------- 1 | import tkinter as Tk 2 | from PlanReview.utils.get_approval_info import get_approval_info 3 | def comment_to_clipboard(rso): 4 | # 5 | # Clear the system clipboard 6 | r = Tk.Tk() 7 | r.withdraw() 8 | r.clipboard_clear() 9 | 10 | # 11 | # Add data to the beamset comment 12 | approval_status = get_approval_info(rso.plan, rso.beamset) 13 | beamset_comment = approval_status.beamset_approval_time 14 | # Copy the comment to the system clipboard 15 | r.clipboard_append(beamset_comment) 16 | r.update() # now it stays on the clipboard after the window is closed 17 | return r 18 | -------------------------------------------------------------------------------- /library/PlanReview/utils/get_machine.py: -------------------------------------------------------------------------------- 1 | def get_machine(machine_name): 2 | import connect 3 | """Finds the current machine name from the list of currently commissioned machines 4 | :param: machine_name (name of the machine in raystation, 5 | usually this is machine_name = beamset.MachineReference.MachineName 6 | return: machine (RS object)""" 7 | machine_db = connect.get_current("MachineDB") 8 | machine = machine_db.GetTreatmentMachine(machineName=machine_name, lockMode=None) 9 | return machine 10 | -------------------------------------------------------------------------------- /library/PlanReview/utils/get_roi_list.py: -------------------------------------------------------------------------------- 1 | def get_roi_list(case, exam_name=None): 2 | """ 3 | Get a list of all rois 4 | Args: 5 | case: RS case object 6 | 7 | Returns: 8 | roi_list: [str1,str2,...]: a list of roi names 9 | """ 10 | roi_list = [] 11 | if exam_name: 12 | structure_sets = [case.PatientModel.StructureSets[exam_name]] 13 | else: 14 | structure_sets = [s for s in case.PatientModel.StructureSets] 15 | 16 | for s in structure_sets: 17 | for r in s.RoiGeometries: 18 | if r.OfRoi.Name not in roi_list: 19 | roi_list.append(r.OfRoi.Name) 20 | return roi_list 21 | -------------------------------------------------------------------------------- /library/PlanReview/utils/get_roi_names_from_type.py: -------------------------------------------------------------------------------- 1 | def get_roi_names_from_type(rso, roi_type): 2 | rois = [] 3 | if type(roi_type) is not list: 4 | roi_type = [roi_type] 5 | for r in rso.case.PatientModel.RegionsOfInterest: 6 | for t in roi_type: 7 | if r.Type == t and t == 'External': 8 | return [r.Name] 9 | elif r.Type == t: 10 | rois.append(r.Name) 11 | 12 | return rois 13 | -------------------------------------------------------------------------------- /library/PlanReview/utils/get_user_display_parameters.py: -------------------------------------------------------------------------------- 1 | import PySimpleGUI as Sg 2 | from typing import Tuple 3 | from PIL import ImageFont 4 | 5 | 6 | def pil_get_element_size(text, font_size, font_name): 7 | font = ImageFont.truetype(font_name, font_size) 8 | size = font.getsize(text) 9 | return size 10 | 11 | 12 | def get_text_element_size(text: str) -> Tuple[int, int]: 13 | """ 14 | Given a text, this function creates an invisible window with the text, and 15 | then it calculates and returns the width and height of the text in pixels. 16 | 17 | Args: 18 | text (str): The input text to calculate size for. 19 | 20 | Returns: 21 | size_text (Tuple[int, int]): The width and height of the text in pixels. 22 | """ 23 | layout = [[Sg.Text(text, key='text')]] 24 | window = Sg.Window('Invisible Window', 25 | layout, 26 | alpha_channel=0, finalize=True) 27 | window.read(timeout=0) 28 | size_text = window['text'].get_size() 29 | window.close() 30 | return size_text 31 | 32 | 33 | def get_user_display_parameters(review_type) -> Tuple[int, int, bool, int, int]: 34 | """ 35 | This function determines some parameters related to the user's display. 36 | It calculates the dimensions of the window and the number of pixels per character. 37 | It also checks if the screen height is less than or equal to 1080 to save space. 38 | 39 | Returns: 40 | window_width (int): The width of the window in pixels. 41 | window_height (int): The height of the window in pixels. 42 | save_space (bool): If True, we will save space because the screen height is 43 | less than or equal to 1080. 44 | pixels_per_char_width (int): The width of a character in pixels. 45 | pixels_per_char_height (int): The height of a character in pixels. 46 | """ 47 | minimum_vertical_resolution = 1300 48 | sample_sentence = "A quick brown fox jumps over a lazy dog." 49 | # width_pixels, height_pixels = pil_get_element_size(sample_sentence, 11, 'times.ttf') 50 | width_pixels, height_pixels = get_text_element_size(sample_sentence) 51 | 52 | # calculating the number of pixels per character 53 | pixels_per_char = width_pixels // len(sample_sentence) 54 | screen_width, screen_height = Sg.Window.get_screen_size() 55 | ## # Create a QPoint object with (0, 0) coordinates 56 | ## point = Sg.QtCore.QPoint() 57 | ## screen_width = Sg.QtWidgets.QDesktopWidget().screenGeometry(point).width() 58 | ## screen_height = Sg.QtWidgets.QDesktopWidget().screenGeometry(point).height() 59 | window_height = 800 if screen_height. 30 | """ 31 | 32 | __author__ = 'Adam Bayliss' 33 | __contact__ = 'rabayliss@wisc.edu' 34 | __date__ = '2018-09-05' 35 | 36 | __version__ = '1.1.0' 37 | __status__ = 'Production' 38 | __deprecated__ = False 39 | __reviewer__ = 'Adam Bayliss' 40 | 41 | __reviewed__ = '2022-Jun-27' 42 | __raystation__ = '11B' 43 | __maintainer__ = 'Adam Bayliss' 44 | 45 | __email__ = 'rabayliss@wisc.edu' 46 | __license__ = 'GPLv3' 47 | __copyright__ = 'Copyright (C) 2022, University of Wisconsin Board of Regents' 48 | 49 | import BeamOperations 50 | 51 | 52 | def main(): 53 | BeamOperations.rename_beams() 54 | 55 | 56 | if __name__ == '__main__': 57 | main() -------------------------------------------------------------------------------- /plan_setup/RoundJaws.py: -------------------------------------------------------------------------------- 1 | """ Round Jaws in Current Beamset 2 | 3 | Currently this simply is a wrapper for the round_jaws function. 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, but WITHOUT 10 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 | 13 | You should have received a copy of the GNU General Public License along with 14 | this program. If not, see . 15 | """ 16 | 17 | __author__ = 'Adam Bayliss' 18 | __contact__ = 'rabayliss@wisc.edu' 19 | __date__ = '2020-01-29' 20 | 21 | __version__ = '1.0.0' 22 | __status__ = 'Production' 23 | __deprecated__ = False 24 | __reviewer__ = 'Adam Bayliss' 25 | 26 | __reviewed__ = '' 27 | __raystation__ = '8.B, SP2' 28 | __maintainer__ = 'Adam Bayliss' 29 | 30 | __email__ = 'rabayliss@wisc.edu' 31 | __license__ = 'GPLv3' 32 | __copyright__ = 'Copyright (C) 2018, University of Wisconsin Board of Regents' 33 | 34 | 35 | import GeneralOperations 36 | import BeamOperations 37 | 38 | 39 | def main(): 40 | beamset = GeneralOperations.find_scope(level='BeamSet') 41 | BeamOperations.round_jaws(beamset=beamset) 42 | 43 | 44 | if __name__ == '__main__': 45 | main() -------------------------------------------------------------------------------- /plan_setup/SetTreatJawsCurrent.py: -------------------------------------------------------------------------------- 1 | """ Set Treatment Jaws To Current 2 | 3 | Take the current jaw settings and set them to the current values. 4 | 5 | Version History: 6 | 0.0.0 Development 7 | 8 | This program is free software: you can redistribute it and/or modify it under 9 | the terms of the GNU General Public License as published by the Free Software 10 | Foundation, either version 3 of the License, or (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, but WITHOUT 13 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 14 | FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along with 17 | this program. If not, see . 18 | """ 19 | 20 | __author__ = 'Adam Bayliss' 21 | __contact__ = 'rabayliss@wisc.edu' 22 | __date__ = '2022-09-16' 23 | 24 | __version__ = '0.0.0' 25 | __status__ = 'Production' 26 | __deprecated__ = False 27 | __reviewer__ = 'Adam Bayliss' 28 | 29 | __reviewed__ = '' 30 | __raystation__ = '11.B' 31 | __maintainer__ = 'Adam Bayliss' 32 | 33 | __email__ = 'rabayliss@wisc.edu' 34 | __license__ = 'GPLv3' 35 | __copyright__ = 'Copyright (C) 2022, University of Wisconsin Board of Regents' 36 | 37 | import sys 38 | import logging 39 | import connect 40 | import GeneralOperations 41 | import BeamOperations 42 | from PlanOperations import find_optimization_index 43 | 44 | 45 | def main(): 46 | plan = GeneralOperations.find_scope(level='Plan') 47 | beamset = GeneralOperations.find_scope(level='BeamSet') 48 | # Find the optimization index corresponding to this beamset 49 | opt_index = find_optimization_index(plan=plan, beamset=beamset) 50 | plan_optimization = plan.PlanOptimizations[opt_index] 51 | connect.await_user_input( 52 | 'Note, beams will be reset. Cancel script if this is not intended') 53 | message = BeamOperations.lock_jaws_to_current(plan_optimization) 54 | log_message = message.split("\n") 55 | for l in log_message: 56 | logging.info(l) 57 | sys.exit("Treat settings changed!\n" 58 | + "{}".format(message)) 59 | 60 | 61 | if __name__ == '__main__': 62 | main() 63 | -------------------------------------------------------------------------------- /protocols/FakeTemplate.csv: -------------------------------------------------------------------------------- 1 | PlanName,BeamSetName,TemplateName,TreatmentTechnique,PatientPosition,BeamName,BeamDescription,GantryStart,GantryStop,ArcDirection,CollimatorAngle,CouchAngle 2 | HFS BeamTemplate - Conformal Arc,VMA_ConArc_Full,Conformal Arc Full,ConformalArc,HeadFirstSupine,1__Arc_c000,1 3D: CW Arc,183,178,Clockwise,5,0 3 | HFS BeamTemplate - Conformal Arc,VMA_ConArc_AR,Conformal Arc Avoid Right,ConformalArc,HeadFirstSupine,1a__Arc_c000,1a 3D: CW Arc,182,225,Clockwise,5,0 4 | HFS BeamTemplate - Conformal Arc,VMA_ConArc_AR,Conformal Arc Avoid Right,ConformalArc,HeadFirstSupine,1b__Arc_c000,1b 3D: CW Arc,315,178,Clockwise,5,0 5 | HFS BeamTemplate - Conformal Arc,VMA_ConArc_AA,Conformal Arc Avoid Anterior,ConformalArc,HeadFirstSupine,1a__Arc_c000,1a 3D: CW Arc,182,315,Clockwise,5,0 6 | HFS BeamTemplate - Conformal Arc,VMA_ConArc_AA,Conformal Arc Avoid Anterior,ConformalArc,HeadFirstSupine,1b__Arc_c000,1b 3D: CW Arc,45,178,Clockwise,5,0 7 | HFS BeamTemplate - Conformal Arc,VMA_ConArc_AL,Conformal Arc Avoid Left,ConformalArc,HeadFirstSupine,1a__Arc_c000,1a 3D: CW Arc,182,45,Clockwise,5,0 8 | HFS BeamTemplate - Conformal Arc,VMA_ConArc_AL,Conformal Arc Avoid Left,ConformalArc,HeadFirstSupine,1b__Arc_c000,1b 3D: CW Arc,135,178,Clockwise,5,0 9 | HFS BeamTemplate - Conformal Arc,VMA_ConArc_AP,Conformal Arc Avoid Posterior,ConformalArc,HeadFirstSupine,1__Arc_c000,1 3D: CW Arc,225,135,Clockwise,5,0 -------------------------------------------------------------------------------- /protocols/UW/Electron_Rendering.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/protocols/UW/Electron_Rendering.jpg -------------------------------------------------------------------------------- /protocols/UW/Thumbs.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/protocols/UW/Thumbs.db -------------------------------------------------------------------------------- /protocols/UW/objectives/Standard_Fractionation.xml: -------------------------------------------------------------------------------- 1 | 2 | Standard Fractionation 3 | 4 | 5 | PTV1 6 | DX 7 | 98 8 | 102.5 9 | 10 10 | 11 | 12 | PTV1 13 | Max 14 | 109.7 15 | 10 16 | 17 | 18 | PTV1 19 | UD 20 | 102.5 21 | 10 22 | 23 | 24 | PTV1 25 | MinEud 26 | 102.5 27 | 10 28 | 29 | 30 | PTV1 31 | MaxEud 32 | 50 33 | 10 34 | 35 | 36 | PTV1 37 | TarEud 38 | 50 39 | 10 40 | 41 | 42 | PTV1 43 | DFO 44 | 100 45 | 10 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /protocols/UW/readme.txt: -------------------------------------------------------------------------------- 1 | Goals placed in this folder require a protocol name so as not to break the tpo and goal loading scripts. 2 | 3 | -------------------------------------------------------------------------------- /protocols/UW_HFP_BeamTemplates.csv: -------------------------------------------------------------------------------- 1 | PlanName,BeamSetName,TemplateName,TreatmentTechnique,PatientPosition,BeamName,BeamDescription,GantryStart,GantryStop,ArcDirection,CollimatorAngle,CouchAngle 2 | HFP Beam Template,VMA_AbdPel_Ce_2A,AbdPelv Central 2Arc,VMAT,HeadFirstProne,1__Arc_c000,1 VMA: CW Arc,192,168,Clockwise,15,0 3 | HFP Beam Template,VMA_AbdPel_Ce_2A,AbdPelv Central 2Arc,VMAT,HeadFirstProne,2__Arc_c000,2 VMA: CCW Arc,163,187,CounterClockwise,355,0 4 | HFP Beam Template,VMA_AbdPel_Ce_3A,AbdPelv Central 3Arc,VMAT,HeadFirstProne,1__Arc_c000,1 VMA: CW Arc,192,168,Clockwise,15,0 5 | HFP Beam Template,VMA_AbdPel_Ce_3A,AbdPelv Central 3Arc,VMAT,HeadFirstProne,2__Arc_c000,2 VMA: CCW Arc,163,187,CounterClockwise,355,0 6 | HFP Beam Template,VMA_AbdPel_Ce_3A,AbdPelv Central 3Arc,VMAT,HeadFirstProne,3__Arc_c000,3 VMA: CW Arc,192,168,Clockwise,270,0 7 | HFP Beam Template,VMA_AbdLun_Lt_2A,LungAbd Left 2Arc,VMAT,HeadFirstProne,1__Arc_c000,1 VMA: CW Arc,346,178,Clockwise,15,0 8 | HFP Beam Template,VMA_AbdLun_Lt_2A,LungAbd Left 2Arc,VMAT,HeadFirstProne,2__Arc_c000,2 VMA: CCW Arc,173,341,CounterClockwise,350,0 9 | HFP Beam Template,VMA_AbdLun_Lt_4A,LungAbd Left NORPM 4Arc ,VMAT,HeadFirstProne,1__Arc_c000,1 VMA: CW Arc,346,178,Clockwise,15,0 10 | HFP Beam Template,VMA_AbdLun_Lt_4A,LungAbd Left NORPM 4Arc ,VMAT,HeadFirstProne,2__Arc_c000,2 VMA: CCW Arc,173,341,CounterClockwise,355,0 11 | HFP Beam Template,VMA_AbdLun_Lt_4A,LungAbd Left NORPM 4Arc ,VMAT,HeadFirstProne,3__Arc_c015,3 VMA: CW Arc,344,80,Clockwise,10,15 12 | HFP Beam Template,VMA_AbdLun_Lt_4A,LungAbd Left NORPM 4Arc ,VMAT,HeadFirstProne,4__Arc_c090,4 VMA: CCW Arc,21,333,CounterClockwise,90,90 13 | HFP Beam Template,VMA_AbdLun_Rt_2A,LungAbd Right 2Arc,VMAT,HeadFirstProne,1__Arc_c000,1 VMA: CW Arc,182,14,Clockwise,15,0 14 | HFP Beam Template,VMA_AbdLun_Rt_2A,LungAbd Right 2Arc,VMAT,HeadFirstProne,2__Arc_c000,2 VMA: CCW Arc,19,187,CounterClockwise,355,0 15 | HFP Beam Template,VMA_AbdLun_Rt_4A,LungAbd Right NORPM 4Arc,VMAT,HeadFirstProne,1__Arc_c000,1 VMA: CW Arc,182,14,Clockwise,15,0 16 | HFP Beam Template,VMA_AbdLun_Rt_4A,LungAbd Right NORPM 4Arc,VMAT,HeadFirstProne,2__Arc_c000,2 VMA: CCW Arc,19,187,CounterClockwise,355,0 17 | HFP Beam Template,VMA_AbdLun_Rt_4A,LungAbd Right NORPM 4Arc,VMAT,HeadFirstProne,3__Arc_c345,3 VMA: CW Arc,282,18,Clockwise,10,345 18 | HFP Beam Template,VMA_AbdLun_Rt_4A,LungAbd Right NORPM 4Arc,VMAT,HeadFirstProne,4__Arc_c270,4 VMA: CCW Arc,21,333,CounterClockwise,90,270 19 | HFP Beam Template,3D_HFP_2F_APPA,2Fields (Anterior and Posterior),Conformal,HeadFirstProne,1__AP,1 3DC: MLC Static Field,180,NA,NA,0,0 20 | HFP Beam Template,3D_HFP_2F_APPA,2Fields (Anterior and Posterior),Conformal,HeadFirstProne,2__PA,2 3DC: MLC Static Field,0,NA,NA,0,0 21 | HFP Beam Template,3D_HFP_3F_Obl,3Fields (Anterior and PosteriorObliques),Conformal,HeadFirstProne,1__AP,1 3DC: MLC Static Field,180,NA,NA,0,0 22 | HFP Beam Template,3D_HFP_3F_Obl,3Fields (Anterior and PosteriorObliques),Conformal,HeadFirstProne,2__RPO,2 3DC: MLC Static Field,150,NA,NA,90,0 23 | HFP Beam Template,3D_HFP_3F_Obl,3Fields (Anterior and PosteriorObliques),Conformal,HeadFirstProne,3__LPO,3 3DC: MLC Static Field,210,NA,NA,90,0 24 | HFP Beam Template,3D_HFP_3F_Post,3Fields (Posterior and Right/Left Laterals),Conformal,HeadFirstProne,1__RLAT,1 3DC: MLC Static Field,90,NA,NA,90,0 25 | HFP Beam Template,3D_HFP_3F_Post,3Fields (Posterior and Right/Left Laterals),Conformal,HeadFirstProne,2__LLAT,2 3DC: MLC Static Field,270,NA,NA,90,0 26 | HFP Beam Template,3D_HFP_3F_Post,3Fields (Posterior and Right/Left Laterals),Conformal,HeadFirstProne,3__PA,3 3DC: MLC Static Field,0,NA,NA,0,0 27 | HFP Beam Template,3D_HFP_4F,4Fields (Anterior/Posterior and Right/Left Laterals),Conformal,HeadFirstProne,1__RLAT,1 3DC: MLC Static Field,90,NA,NA,90,0 28 | HFP Beam Template,3D_HFP_4F,4Fields (Anterior/Posterior and Right/Left Laterals),Conformal,HeadFirstProne,2__AP,2 3DC: MLC Static Field,180,NA,NA,0,0 29 | HFP Beam Template,3D_HFP_4F,4Fields (Anterior/Posterior and Right/Left Laterals),Conformal,HeadFirstProne,3__LLAT,3 3DC: MLC Static Field,270,NA,NA,90,0 30 | HFP Beam Template,3D_HFP_4F,4Fields (Anterior/Posterior and Right/Left Laterals),Conformal,HeadFirstProne,4__PA,4 3DC: MLC Static Field,0,NA,NA,0,0 -------------------------------------------------------------------------------- /protocols/UW_HFS_3DSnS_BeamTemplates.csv: -------------------------------------------------------------------------------- 1 | PlanName,BeamSetName,TemplateName,TreatmentTechnique,PatientPosition,BeamName,BeamDescription,GantryStart,GantryStop,ArcDirection,CollimatorAngle,CouchAngle) 2 | HFS BeamTemplate - 3DSnS,SNS_Brai_PRDR,SNS (PRDR): 10Beam Brain,SMLC,HeadFirstSupine,1_Brai_RPO,1 SNS: PRDR,240,NA,NA,15,0 3 | HFS BeamTemplate - 3DSnS,SNS_Brai_PRDR,SNS (PRDR): 10Beam Brain,SMLC,HeadFirstSupine,2_Brai_RLAT,2 SNS: PRDR,270,NA,NA,350,0 4 | HFS BeamTemplate - 3DSnS,SNS_Brai_PRDR,SNS (PRDR): 10Beam Brain,SMLC,HeadFirstSupine,3_Brai_RAO,3 SNS: PRDR,300,NA,NA,355,0 5 | HFS BeamTemplate - 3DSnS,SNS_Brai_PRDR,SNS (PRDR): 10Beam Brain,SMLC,HeadFirstSupine,4_Brai_LAO,4 SNS: PRDR,60,NA,NA,25,0 6 | HFS BeamTemplate - 3DSnS,SNS_Brai_PRDR,SNS (PRDR): 10Beam Brain,SMLC,HeadFirstSupine,5_Brai_LLAT,5 SNS: PRDR,90,NA,NA,345,0 7 | HFS BeamTemplate - 3DSnS,SNS_Brai_PRDR,SNS (PRDR): 10Beam Brain,SMLC,HeadFirstSupine,6_Brai_LPO,6 SNS: PRDR,120,NA,NA,5,0 8 | HFS BeamTemplate - 3DSnS,SNS_Brai_PRDR,SNS (PRDR): 10Beam Brain,SMLC,HeadFirstSupine,7_Brai_g045c315,7 SNS: PRDR,45,NA,NA,5,315 9 | HFS BeamTemplate - 3DSnS,SNS_Brai_PRDR,SNS (PRDR): 10Beam Brain,SMLC,HeadFirstSupine,8_Brai_g135c315,8 SNS: PRDR,135,NA,NA,5,315 10 | HFS BeamTemplate - 3DSnS,SNS_Brai_PRDR,SNS (PRDR): 10Beam Brain,SMLC,HeadFirstSupine,9_Brai_g315c045,9 SNS: PRDR,315,NA,NA,15,45 11 | HFS BeamTemplate - 3DSnS,SNS_Brai_PRDR,SNS (PRDR): 10Beam Brain,SMLC,HeadFirstSupine,10_Brai_g225c045,10 SNS: PRDR,225,NA,NA,5,45 12 | HFS BeamTemplate - 3DSnS,3D_HFS_2F_APPA,2Fields (Anterior and Posterior),Conformal,HeadFirstSupine,1__AP,1 3DC: MLC Static Field,0,NA,NA,0,0 13 | HFS BeamTemplate - 3DSnS,3D_HFS_2F_APPA,2Fields (Anterior and Posterior),Conformal,HeadFirstSupine,2__PA,2 3DC: MLC Static Field,180,NA,NA,0,0 14 | HFS BeamTemplate - 3DSnS,3D_HFS_3F_Obl,3Fields (Anterior and PosteriorObliques),Conformal,HeadFirstSupine,1__AP,1 3DC: MLC Static Field,0,NA,NA,0,0 15 | HFS BeamTemplate - 3DSnS,3D_HFS_3F_Obl,3Fields (Anterior and PosteriorObliques),Conformal,HeadFirstSupine,2__RPO,2 3DC: MLC Static Field,210,NA,NA,210,0 16 | HFS BeamTemplate - 3DSnS,3D_HFS_3F_Obl,3Fields (Anterior and PosteriorObliques),Conformal,HeadFirstSupine,3__LPO,3 3DC: MLC Static Field,150,NA,NA,150,0 17 | HFS BeamTemplate - 3DSnS,3D_HFS_3F_Post,3Fields (Posterior and Right/Left Laterals),Conformal,HeadFirstSupine,1__RLAT,1 3DC: MLC Static Field,270,NA,NA,90,0 18 | HFS BeamTemplate - 3DSnS,3D_HFS_3F_Post,3Fields (Posterior and Right/Left Laterals),Conformal,HeadFirstSupine,2__LLAT,2 3DC: MLC Static Field,90,NA,NA,90,0 19 | HFS BeamTemplate - 3DSnS,3D_HFS_3F_Post,3Fields (Posterior and Right/Left Laterals),Conformal,HeadFirstSupine,3__PA,3 3DC: MLC Static Field,180,NA,NA,0,0 20 | HFS BeamTemplate - 3DSnS,3D_HFS_4F,4Fields (Anterior/Posterior and Right/Left Laterals),Conformal,HeadFirstSupine,1__RLAT,1 3DC: MLC Static Field,270,NA,NA,90,0 21 | HFS BeamTemplate - 3DSnS,3D_HFS_4F,4Fields (Anterior/Posterior and Right/Left Laterals),Conformal,HeadFirstSupine,2__AP,2 3DC: MLC Static Field,0,NA,NA,0,0 22 | HFS BeamTemplate - 3DSnS,3D_HFS_4F,4Fields (Anterior/Posterior and Right/Left Laterals),Conformal,HeadFirstSupine,3__LLAT,3 3DC: MLC Static Field,90,NA,NA,90,0 23 | HFS BeamTemplate - 3DSnS,3D_HFS_4F,4Fields (Anterior/Posterior and Right/Left Laterals),Conformal,HeadFirstSupine,4__PA,4 3DC: MLC Static Field,180,NA,NA,0,0 24 | HFS BeamTemplate - 3DSnS,3D_HFS_WBRT,2Fields (Right/Left Lateral with Couch Adjust),Conformal,HeadFirstSupine,1_Brai_g270c355,1 3DC: MLC Static Field,270,NA,NA,0,355 25 | HFS BeamTemplate - 3DSnS,3D_HFS_WBRT,2Fields (Right/Left Lateral with Couch Adjust),Conformal,HeadFirstSupine,2_Brai_g090c005,2 3DC: MLC Static Field,90,NA,NA,0,5 -------------------------------------------------------------------------------- /protocols/UW_prvs_and_logical.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | BrachialPlexus_L_PRV05 4 | Exp 5 | "255, 0, 255" 6 | Avoidance 7 | 8 | 9 | BrachialPlexus_R_PRV05 10 | Exp 11 | "255, 0, 255" 12 | Avoidance 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /protocols/dicom_verification.xml: -------------------------------------------------------------------------------- 1 | 2 | DICOM Export Test 3 | 4 | HFS_DICOM_Export 5 | Head First Supine Dicom Export Verification" 6 | HFS 7 | 8 | HFS_MeV_3DC_TB 9 | HFS 10 | TrueBeam 11 | Photons 12 | Conformal 13 | HeadFirstSupine 14 | 5 15 | 3D Validation plan for DICOM Export - TrueBeam 16 | 17 | PTV1 18 | 95 19 | DoseAtVolume 20 | 2500 21 | 1 22 | True 23 | 24 | 25 | 26 | HFS_MV_VMA_TB 27 | HFS 28 | TrueBeam 29 | Photons 30 | VMAT 31 | HeadFirstSupine 32 | 5 33 | VMAT Validation plan for DICOM Export - TrueBeam 34 | 35 | PTV1 36 | 95 37 | DoseAtVolume 38 | 2500 39 | 1 40 | True 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /rayscripts_reqs_python3.txt: -------------------------------------------------------------------------------- 1 | ##### Requirements.txt for ray_scripts in Python 3 ##### 2 | # 3 | ### Notes ### 4 | ## 9/20/2020 5 | # * Instantiation 6 | # * I added two libraries to the list given to me by Craig Fitzwilliams 7 | # ** Jupyter notebooks (notebook) for interactive development 8 | # ** Matplotlib (matplotlib) for basic plotting functionality (nice for Jupyter) 9 | # * When an environment is created in RayStation, numpy and pythonnet are installed 10 | # by default but the versions that are installed are outdated. I have added >= for 11 | # these libraries to ensure they are updated when the user installs using pip. 12 | # * pynetdicom has been added, pynetdicom3 has been removed. The latter, which was 13 | # used with our Python 2 ray_scripts, is deprecated. 14 | # * PySimpleGUI was installed with bindings for PySide2 (Qt for Python) and tkinter. 15 | # 16 | ## 1/18/2021 17 | # * Added "requests" explicitly to list 18 | # 19 | # 09/28/2023 20 | # * Based on vulnerabilities, updated our scipy and requests libraries. 21 | ### Use ### 22 | # path/to/python -m pip install -r rayscripts_reqs_python3.txt 23 | # 24 | ##### 25 | 26 | matplotlib 27 | # notebook 28 | numpy>=1.19 29 | pandas 30 | Pillow 31 | pydicom 32 | pynetdicom 33 | PySide2 34 | PySimpleGUI 35 | PySimpleGUIQt 36 | pythonnet>=2.5 37 | reportlab>3.6.13 38 | requests>=2.31 39 | scipy>=1.10 40 | XlsxWriter 41 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | colorama==0.4.6 2 | matplotlib==3.2.2 3 | numpy==1.24.4 4 | pandas==2.0.3 5 | Pillow==10.0.1 6 | pydicom==2.4.3 7 | pynetdicom==2.0.2 8 | pyperclip==1.8.2 9 | PySimpleGUI==4.60.5 10 | pytest==7.4.2 11 | python-dateutil==2.8.2 12 | python-docx==0.8.11 13 | pythonnet==3.0.2 14 | reportlab==3.6.12 15 | requests==2.31.0 16 | scipy>=1.10 17 | XlsxWriter==3.1.6 18 | -------------------------------------------------------------------------------- /requirements_17Jan2021.txt: -------------------------------------------------------------------------------- 1 | pandas==1.0.3 2 | XlsxWriter==1.2.7 3 | pydicom==2.0.0 4 | scipy==1.10.0 5 | numpy==1.22.0 6 | requests==2.31.0 7 | clr==1.0.3 8 | connect==0.2 9 | pynetdicom==1.5.5 10 | reportlab==3.5.59 11 | -------------------------------------------------------------------------------- /testing/Icons/blue_circle_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/testing/Icons/blue_circle_icon.png -------------------------------------------------------------------------------- /testing/Icons/green_circle_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/testing/Icons/green_circle_icon.png -------------------------------------------------------------------------------- /testing/Icons/red_circle_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrssc/ray_scripts/f6a9a2cfc22d11f3949c4406eb8877e44959d84b/testing/Icons/red_circle_icon.png -------------------------------------------------------------------------------- /testing/ScreenGrab.py: -------------------------------------------------------------------------------- 1 | """ Screen grab 2 | 3 | """ 4 | 5 | __author__ = 'Adam Bayliss' 6 | __contact__ = 'rabayliss@wisc.edu' 7 | __version__ = '0.0.0' 8 | __license__ = 'GPLv3' 9 | __help__ = 'https://github.com/wrssc/ray_scripts/wiki/Rendering_Settings' 10 | __copyright__ = 'Copyright (C) 2021, University of Wisconsin Board of Regents' 11 | 12 | import tkinter as tk 13 | # from tkinter import * 14 | from PIL import Image, ImageGrab, ImageTk 15 | import ctypes, sys 16 | import os 17 | 18 | def main(): 19 | 20 | if sys.getwindowsversion().major == 10: 21 | ctypes.windll.shcore.SetProcessDpiAwareness(2) # Set DPI awareness 22 | 23 | 24 | root = tk.Tk() 25 | def area_sel(): 26 | def getPress(event): # get press position 27 | global press_x,press_y 28 | press_x,press_y = event.x,event.y 29 | 30 | def mouseMove(event): # movement 31 | global press_x, press_y, rectangleId 32 | fullCanvas.delete(rectangleId) 33 | rectangleId = fullCanvas.create_rectangle(press_x,press_y,event.x,event.y,width=5) 34 | 35 | def getRelease(event): # get release position 36 | global press_x, press_y, rectangleId 37 | top.withdraw() 38 | img = ImageGrab.grab((press_x, press_y,event.x,event.y)) 39 | img.show() 40 | 41 | top = tk.Toplevel() 42 | top.state('zoomed') 43 | top.overrideredirect(1) 44 | fullCanvas = tk.Canvas(top) 45 | 46 | background = ImageTk.PhotoImage(ImageGrab.grab().convert("L")) 47 | fullCanvas.create_image(0,0,anchor="nw",image=background) 48 | 49 | # bind event for canvas 50 | fullCanvas.bind('',getPress) 51 | fullCanvas.bind('',mouseMove) 52 | fullCanvas.bind('',getRelease) 53 | 54 | fullCanvas.pack(expand="YES",fill="both") 55 | top.mainloop() 56 | file_out=r'Q:\\RadOnc\RayStation\Reports\screenshot.jpg' 57 | background.write(file_out,format='jpg') 58 | 59 | rectangleId = None 60 | sel_btn = tk.Button(root, text='select area', width=20, command=area_sel) 61 | sel_btn.pack() 62 | 63 | root.mainloop() 64 | 65 | if __name__ == '__main__': 66 | main() 67 | -------------------------------------------------------------------------------- /testing/Test_VMAT_PRDR.py: -------------------------------------------------------------------------------- 1 | """ VMAT PRDR 2 | 3 | Version: 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later 8 | version. 9 | 10 | This program is distributed in the hope that it will be useful, but WITHOUT 11 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along with 15 | this program. If not, see . 16 | """ 17 | 18 | __author__ = "Adam Bayliss" 19 | __contact__ = 'rabayliss@wisc.edu' 20 | __date__ = '12-May-2022' 21 | __version__ = '0.0.0' 22 | __status__ = 'Development' 23 | __deprecated__ = False 24 | __reviewer__ = '' 25 | __reviewed__ = '' 26 | __raystation__ = '10A SP2' 27 | __maintainer__ = 'One maintainer' 28 | __email__ = 'rabayliss@wisc.edu' 29 | __license__ = 'GPLv3' 30 | __copyright__ = 'Copyright (C) 2018, University of Wisconsin Board of Regents' 31 | __help__ = 'https://github.com/mwgeurts/ray_scripts/wiki/User-Interface' 32 | __credits__ = [] 33 | 34 | import logging 35 | 36 | 37 | # 38 | # Make plan copy 39 | def make_plan_copy(case, old_plan_name, new_plan_name): 40 | case.CopyPlan(PlanName=old_plan_name, 41 | NewPlanName=new_plan_name, 42 | KeepBeamSetNames=True) 43 | 44 | 45 | def new_col(old_col): 46 | return (old_col + 90) % 360 47 | 48 | 49 | def change_col(beam, old_col): 50 | new_angle = new_col(old_col) 51 | beam.InitialCollimatorAngle = new_angle 52 | 53 | 54 | def reverse_dir(beam, old_dir): 55 | if old_dir == 'CounterClockwise': 56 | new_dir = 'Clockwise' 57 | else: 58 | new_dir = 'CounterClockwise' 59 | beam.ArcDirectionRotation = new_dir 60 | 61 | 62 | def reverse_gantry(beam, start, stop): 63 | new_start = stop 64 | new_stop = start 65 | beam.GantryAngle = new_start 66 | beam.ArcStopGantryAngle = new_stop 67 | beam.ArcRotationDirection = reverse_dir(beam, 68 | old_dir=beam.ArcRotationDirection) 69 | 70 | 71 | def reverse_plan(old_plan, new_plan): 72 | old_data = {} 73 | for bs in old_plan.BeamSets: 74 | old_data[bs.DicomPlanLabel] = {} 75 | for b in bs.Beams: 76 | old_data[bs.DicomPlanLabel][b.Name] = {'Col': b.InitialCollimatorAngle, 77 | 'Gantry_Start': b.GantryAngle, 78 | 'Gantry_Stop': b.ArcStopGantryAngle, 79 | 'Arc_Rot': b.ArcRotationDirection} 80 | # 81 | # Cycle new beamset 82 | for bs in new_plan.BeamSets: 83 | for b in bs.Beams: 84 | col_changed = change_col(b, 85 | old_col=old_data[bs.DicomPlanLabel][b.Name]['Col']) 86 | gan_changed = reverse_gantry(b, 87 | b.GantryAngle, 88 | b.ArcStopGantryAngle) 89 | 90 | 91 | def make_prdr_vmat_companion_plans(): 92 | current_plan_name = plan.Name 93 | new_plan_name = current_plan_name + '_Rvrs' 94 | make_plan_copy(case, current_plan_name, new_plan_name) 95 | # 96 | # Reverse beam directions 97 | old_plan = case.TreatmentPlans[current_plan_name] 98 | new_plan = case.TreatmentPlans[new_plan_name] 99 | new_plan.PlanOptimizations[0].ResetOptimization() 100 | reverse_plan(old_plan, new_plan) 101 | -------------------------------------------------------------------------------- /testing/UpgradeScripts.py: -------------------------------------------------------------------------------- 1 | """ Upgrade Versions 2 | 3 | """ 4 | import os 5 | import connect 6 | from collections import namedtuple 7 | from library.api.api_utils import find_scope 8 | import logging 9 | 10 | STRUCTURE_TEMPLATE_LOCATION = r'U:\\UWHealth\\RadOnc\\ShareAll\\RayStation\\2024A\\Templates\\Structure' 11 | 12 | 13 | def build_file_list(): 14 | file_list = [] 15 | for file in os.listdir(STRUCTURE_TEMPLATE_LOCATION): 16 | if file.endswith(".rsbak"): 17 | file_list.append(os.path.join(STRUCTURE_TEMPLATE_LOCATION, file)) 18 | return file_list 19 | 20 | 21 | def load_structure_templates(pd): 22 | files = build_file_list() 23 | for f in files: 24 | logging.debug("Loading {}".format(f)) 25 | pd.db.ImportStructureTemplate(FileName=f) 26 | 27 | 28 | def main(): 29 | # Initialize return variable 30 | Pd = namedtuple('Pd', ['error', 'db', 'case', 'patient', 'exam', 'plan', 'beamset']) 31 | # Get current patient, case, exam 32 | pd = Pd(error=[], 33 | patient=find_scope(level='Patient'), 34 | case=find_scope(level='Case'), 35 | exam=find_scope(level='Examination'), 36 | db=find_scope(level='PatientDB'), 37 | plan=None, # find_scope(level='Plan'), 38 | beamset=None, ) # find_scope(level='BeamSet')) 39 | load_structure_templates(pd) 40 | 41 | 42 | if __name__ == '__main__': 43 | main() 44 | -------------------------------------------------------------------------------- /testing/run_automated_review.py: -------------------------------------------------------------------------------- 1 | """ Automated Review Only 2 | 3 | """ 4 | 5 | import sys 6 | from pathlib import Path 7 | 8 | plan_review_path = Path(__file__).parent.parent / "library" / "PlanReview" 9 | sys.path.insert(1, str(plan_review_path)) 10 | from PlanReview import automated_tests 11 | 12 | automated_tests() 13 | -------------------------------------------------------------------------------- /testing/run_dosimetry_safety_review.py: -------------------------------------------------------------------------------- 1 | """ Dosimetry Safety Check with pdf 2 | --version 0.0.0--Run basic plan integrity checks and parse the log file. 3 | 4 | """ 5 | 6 | import sys 7 | from pathlib import Path 8 | # This function is pointed to the PlanReview folder where the real magic happens 9 | plan_review_path = Path(__file__).parent.parent / "library" / "PlanReview" 10 | sys.path.insert(1, str(plan_review_path)) 11 | from PlanReview.dosimetry_review import dosimetry_safety_check 12 | dosimetry_safety_check() 13 | -------------------------------------------------------------------------------- /testing/run_physics_review.py: -------------------------------------------------------------------------------- 1 | """ Physics Review with Document 2 | 3 | """ 4 | 5 | import sys 6 | from pathlib import Path 7 | 8 | plan_review_path = Path(__file__).parent.parent / "library" / "PlanReview" 9 | sys.path.insert(1, str(plan_review_path)) 10 | from PlanReview.physics_review import physics_review 11 | 12 | physics_review(rso=None, do_physics_review=True) 13 | --------------------------------------------------------------------------------