DICOM Files Made for Halcyon QA - Output

Copyright 2019 Nick Harding of Queen's Centre for Oncology, Castle Hill Hospital.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License here.

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

The Halcyon 2.0 will not deliver open FFF beams in Treatment QA mode. There is a requirement for there to be MLC motions (in particular the proximal bank to move). It is possible to deliver open FFF beams in Service mode, however locally we prefer to perform all QA in Treatment mode, to ensure the machine is being tested in the mode used clinically. The following brief code snippet is desgined to make a Step and Shoot plan with 99.9% of MU delivered with a static open square field and the final 0.1% of MU delivered with an out of field proximal leaf pair moved.

The following code imports libraries and defines functions required later in the code.

In [1]:
import pydicom
import tkinter as tk
from tkinter import filedialog

def makeSquare(distal,square):
    """Return list of MLC positions of square cm size
    Keyword arguments
    distal is boolean. True returns list of 28 + 28 MLC positions
    False returns proximal bank i.e. 29 + 29 positions
    square is square size (in cm)
    if square == 28:
        if distal:
            return [-140] * 28 + [140] * 28
            return [-140] * 29 + [140] * 29
    if distal:
        n_of_open = int(square)
        half_closed = int((28 - square)/2)
        return (
                [0.0] * half_closed
                + [float(-square*10 / 2)] * n_of_open
                + [0.0] * half_closed
                +[0.0] * half_closed
                + [float(square*10 / 2)] * n_of_open
                + [0.0] * half_closed
        n_of_open = square + 1
        half_closed = int((29 - n_of_open)/2)
        return (
                [0.0] * half_closed
                + [float(-square*10 / 2)] * n_of_open
                + [0.0] * (half_closed)
                + [0.0] * half_closed
                + [float(square*10 / 2)] * n_of_open
                + [0.0] * (half_closed)

The following cell sets some variables. Change them as required.

In [2]:
req_MU = 100 #no of MU to deliver in plan
req_square = 10 # square field size in whole cm
req_GA = 0 #gantry angle must be in range 0 <= ga < 360 deg
move_out = True # move_out True moves proximal pair away from each other, False together
if req_square >= 27:
    move_out = False
#NB That if preferred the MU and gantry angle can be edited in Eclipse.

Running the cell below opens a File Open dialog to navigate to an existing Step and Shoot Halcyon RP.dcm file. As mentioned in the Snooker Cue notebook, the plan needs to match the calibration of your existing unit. Our Halcyon shipped with a plan labelled StepAndShoot_DLG_30mmGap_Gantry0_Tit_50_SX1.dcm and is the plan we used locally. If another plan is used the code below may need editing as it not designed to be robust for example if the BeamLimitingDevicePositionSequences are in a different order.

In [3]:
root = tk.Tk()
ds = pydicom.dcmread(filedialog.askopenfilename())

The cell below will open a Folder Select Dialog box to choose where you want the Output dcm files saving to.

In [4]:
#set an output directory with a GUI
output_dir = filedialog.askdirectory()
In [5]:
#set the plan MU
fgs = ds.FractionGroupSequence[0]
fgs.ReferencedBeamSequence[0].BeamMeterset = req_MU

beam = ds.BeamSequence[0]
#Set new beam name and plan label
plan_name = 'Output_' + str(req_square)
plan_name = f"O_S{int(req_square)}_M{int(req_MU)}_G{int(req_GA)}"

#Eclipse field names must be 16 characters or less
beam.BeamName = plan_name
if len(beam.BeamName) > 16:
    beam.BeamName = plan_name[:16]

print(f"Beam Name is {beam.BeamName}")

#Eclipse plan names must be 13 characters or less

ds.RTPlanLabel = plan_name
if len(ds.RTPlanLabel)>13:
    ds.RTPlanLabel = ds.RTPlanLabel[:13]
print(f"Plan Name is {ds.RTPlanLabel}")

#original plan will likely contain too many Control Points (CP)
#the DLG plan above contains 16. Only three required for output files. Delete all but first three.
beam.NumberOfControlPoints = 3

#set three CPs
cp0 = beam.ControlPointSequence[0]
cp1 = beam.ControlPointSequence[1]
cp2 = beam.ControlPointSequence[2]

cp0.BeamLimitingDevicePositionSequence[2].LeafJawPositions = (
cp0.BeamLimitingDevicePositionSequence[3].LeafJawPositions = (
cp0.GantryAngle = req_GA

cp1.BeamLimitingDevicePositionSequence[0].LeafJawPositions = (
cp1.BeamLimitingDevicePositionSequence[1].LeafJawPositions = (
#give this CP 99.9% of dose
cp1.CumulativeMetersetWeight = 0.999

cp2.BeamLimitingDevicePositionSequence[0].LeafJawPositions = (
cp2.BeamLimitingDevicePositionSequence[1].LeafJawPositions = (
#move a PROXIMAL leaf pair
if move_out:    
    cp2.BeamLimitingDevicePositionSequence[1].LeafJawPositions[0] += -10
    cp2.BeamLimitingDevicePositionSequence[1].LeafJawPositions[29] += 10
    cp2.BeamLimitingDevicePositionSequence[1].LeafJawPositions[0] += 10
    cp2.BeamLimitingDevicePositionSequence[1].LeafJawPositions[29] += -10
#give this CP final 0.1% of dose
cp2.CumulativeMetersetWeight = 1.0

output_name = f"{output_dir}/{plan_name}.dcm"
print (f"File saved as {output_name}")
Beam Name is O_S10_M100_G0
Plan Name is O_S10_M100_G0
File saved as H:/Python/Output_Files/O_S10_M100_G0.dcm

These output files can be imported into Eclipse after UIDs have been changed. Locally these plans are used in routine QA for output constancy and profile constancy.