Python Exercise #1A – MLC shaper

The MLC Shaper application, developed by Varian, allows the creation of .MLC files that can be used to deliver multi-leaf collimated fields in service. The first python exercise, detailed here, was to develop similar software to allow the creation of .MLC files containing rectangular fields.

This problem can be broken up into the following components:

  1. Retrieving user input (the name of the file, the field size, etc.);
  2. Determining MLC position (by identifying whether any given leaf pair is open or closed);
  3. Writing the .MLC file (in the correct format).

This post will discuss the solution to each of these smaller problems. Please note that this discussion assumes the use of Python 3.

Parsing user input

For this exercise we have assumed that the user wants an executable application that prompts for the necessary data. This is not necessarily the case, especially in a medical physics department, where you may prefer to implement methods that are called from within an interactive environment (where these variables can be directly declared). Here, however, we will attempt to implement a command-line interface, similar to the one provided in the exercise description:

Field name: test
Field size, x (cm): 2.0
Field size, y (cm): 3.7

Basic command-line interface prompts can be created using the input() function. To implement the example above, you could use:

name = input("Field name: ")
x_size = input("Field size, x (cm): ")
y_size = input("Field size, y (cm): ")

The input() function will read a line from input and convert it to a string (without the trailing newline character). The int(), float() or eval() functions could be used to construct a number from this string.

Command-line interface programs should validate entered data. For this example, we want to ensure that the field size entered by the user is valid by ensuring that it is a number and that it is reasonable number (not greater than 40 cm, for example). We could do this by handling ValueError exceptions (which will be thrown, for example, when trying to convert a letter into a float) and conditional statements. We can implement this as a method:

def valid_field_size_input(size):
    try:
        return float(size) >= 0 and float(size) <= 40
    except ValueError:
        return False

x_size = input("Field size, x (cm): ")

while not valid_field_size_input(x_size):
    print("Invalid field size")
    x_size = input("Field size, x (cm): ")

Looking at the example .MLC file, it appears as though there are a few variables that the user could be prompted to enter, in addition to field size: treatment type, last name, first name, patient ID, number of fields, MLC model, field names, operator and collimator rotation.

MLC positioning

A simple way to generate leaf positions is to assume that all leaf pairs are closed, and then determine which leaf pairs are open. The Varian MLC Shaper application allows the user to specify the closed position of the leaves (at the left, centre, or right edge of the field). This code will assume that the leaves will be positioned at 0 when closed. An array of 120 closed leaf positions can be created with numpy,

import numpy as np
leaf_positions = np.zeroes(120)

or simply with

leaf_positions = [0] * 120

The y field size allows the determination of whether a leaf pair is open, while the x field size indicates the size of the aperture between the paired leaves. Determining whether a leaf is closed or open requires some information about the leaf boundaries – the location of the leaf sides with respect to the central axis. This data is static for any given collimator system, and can, for example, be found in beam arrangement data produced by treatment planning systems. For this example, a method to determine whether a leaf pair is open will be defined:

def leaf_pair_open(leaf_pair, y_size):
    if leaf_pair > 30:
        return leaf_pair_open(61 - leaf_pair, y_size)
    leaf_boundaries = list(np.arange(-20,-10,1)) + list(np.arange(-10,0.5,0.5))
    return math.fabs(not leaf_boundaries[leaf_pair] > y_size / 2.0)

Since only rectangular symmetric fields are being generated, it can be assumed that if leaf pair 30 is opened, then leaf pair 31 must also be opened. As such, the method can be written to only include half the field. The leaf_boundaries list contains the leaf boundaries located on and before the central axis, and if boundary nearest the central axis is further away than the field edge (y_size / 2.0), the leaf can be closed.
This method can then be called for every leaf pair, like so:

for leaf_pair in range(60):
    if leaf_pair_open(field_size, leaf_pair + 1):
        leaf_positions[leaf_pair] = x_size / 2;
        leaf_positions[leaf_pair + 60] = - x_size / 2;

File output

The .MLC file produced by the Varian MLC Shaper software contains human-readable text (ASCII). A text stream can be created using Python’s in-built open() function:

output_file_name = "output.mlc"
output_file = open(output_file_name, "w", encoding="utf-8")

The “w” here, included as a mode string, indicates the writing mode, as opposed to reading (“r”), appending (“a”) or exclusive creation (“x”). With this file stream, the opening lines of the file can be written using the write() method, like so:

output_file.write("File Rev = H")
output_file.write("Treatment = Static")
output_file.write("Last Name = " + input("Last name: "))
output_file.write("First Name = " + input("First name: "))
output_file.write("Patient ID = " + input("Patient ID: "))

The leaf positions can be written to file iteratively:

for index in range(120):
leaf_name = str(index + 1) + "A"
if (index) > 59:
    leaf_name = str(index - 59) + "B"
output_file.write("Leaf " + leaf_name + " = " + str(leaf_positions[index]))