예제 #1
0
def main(dir, args):

    energies = [90 + i*5 for i in range(0,29)] # in MeV, they go from 90 to 230 in steps of 5 MeV
    lat_sigma = [6 for energy in energies] # in mm
    print(energies, lat_sigma)

    parser = argparse.ArgumentParser(description='Generate an ideal CT of a water cube and a dummy RTplan with certain energy layers, defined in script. Every energy layer has a single spot at (0,0)')

    parser.add_argument('--outdir', dest='outdir'   , type=str, required=True )
    parser.add_argument('--inDate', dest='inDate'   , type=str, required=False, default='20191011')
    parser.add_argument('--stDate', dest='stDate'   , type=str, required=False, default='20191011')
    parser.add_argument('--seDate', dest='seDate'   , type=str, required=False, default='20191011')
    parser.add_argument('--acDate', dest='acDate'   , type=str, required=False, default='20191011')
    parser.add_argument('--coDate', dest='coDate'   , type=str, required=False, default='20191011')
    parser.add_argument('--inTime', dest='inTime'   , type=str, required=False, default='150000')
    parser.add_argument('--stTime', dest='stTime'   , type=str, required=False, default='150000')
    parser.add_argument('--seTime', dest='seTime'   , type=str, required=False, default='150000')
    parser.add_argument('--acTime', dest='acTime'   , type=str, required=False, default='150000')
    parser.add_argument('--coTime', dest='coTime'   , type=str, required=False, default='150000')
    parser.add_argument('--access', dest='access'   , type=str, required=False, default='0')
    parser.add_argument('--manuf' , dest='manuf'    , type=str, required=False, default='MGH Physics Research')
    parser.add_argument('--station',dest='station'  , type=str, required=False, default='Nashua')
    parser.add_argument('--institution' , dest='institution', type=str, required=False, default='rbe')
    parser.add_argument('--instaddr' , dest='instaddr', type=str, required=False, default='30 Fruit St, Boston MA 02114, USA')
    parser.add_argument('--physician' , dest='physician', type=str, required=False, default='')
    parser.add_argument('--stDesc', dest='stDesc'  , type=str, required=False, default='For commissioning')
    parser.add_argument('--seDesc', dest='seDesc'  , type=str, required=False, default='Water cube 50x50x50cm3')
    parser.add_argument('--operat', dest='operat'  , type=str, required=False, default='FHG')
    parser.add_argument('--manmod', dest='manmod'  , type=str, required=False, default='2019')
    parser.add_argument('--patname',dest='patname' , type=str, required=False, default='Cube^Water')
    parser.add_argument('--patid' , dest='patid'   , type=str, required=False, default='WaterCube50')
    parser.add_argument('--birth' , dest='birth'   , type=str, required=False, default='N/A')
    parser.add_argument('--age'   , dest='age'     , type=str, required=False, default='N/A')
    parser.add_argument('--sex'   , dest='sex'     , type=str, required=False, default='N/A')
    parser.add_argument('--anon'  , dest='anon'    , type=str, required=False, default='NO')
    parser.add_argument('--sw'    , dest='sw'      , type=str, required=False, default='pydicom1.2.2')
    parser.add_argument('--lastCal',dest='lastCal' , type=str, required=False, default='')
    parser.add_argument('--stuid' , dest='stuid'   , type=str, required=False, default=uid.generate_uid())
    parser.add_argument('--seuid' , dest='seuid'   , type=str, required=False, default=uid.generate_uid())
    parser.add_argument('--stid'  , dest='stid'    , type=str, required=False, default='1')
    parser.add_argument('--sen'   , dest='sen'     , type=str, required=False, default='1')
    parser.add_argument('--acqn'  , dest='acqn'    , type=str, required=False, default='1')
    parser.add_argument('--forUID', dest='forUID'  , type=str, required=False, default=uid.generate_uid())
    parser.add_argument('--RTlabel',dest='RTlabel' , type=str, required=False, default='Test')
    parser.add_argument('--RTname', dest='RTname'  , type=str, required=False, default='Pristine test')
    parser.add_argument('--RTdesc', dest='RTdesc'  , type=str, required=False, default='Dummy energies')
    parser.add_argument('--RTdate', dest='RTdate'  , type=str, required=False, default='20191011')
    parser.add_argument('--RTtime', dest='RTtime'  , type=str, required=False, default='150000')
    parser.add_argument('--RTgeom', dest='RTgeom'  , type=str, required=False, default='PATIENT')
    parser.add_argument('--sadX'  , dest='sadX'    , type=float, required=False, default=2000.0)
    parser.add_argument('--sadY'  , dest='sadY'    , type=float, required=False, default=1800.0)
    parser.add_argument('--nBlocks',dest='nBlocks' , type=int, required=False, default=0)
    parser.add_argument('--isoX'  , dest='isoX'    , type=float, required=False, default=0.0)
    parser.add_argument('--isoY'  , dest='isoY'    , type=float, required=False, default=0.0)
    parser.add_argument('--isoZ'  , dest='isoZ'    , type=float, required=False, default=0.0)
    parser.add_argument('--snout' , dest='snout'   , type=str, required=False, default='')
    parser.add_argument('--nRS'   , dest='nRS'     , type=int, required=False, default=0)
    parser.add_argument('--rsID'  , dest='rsID'    , type=str, required=False, default='')
    parser.add_argument('--snPos' , dest='snPos'   , type=float, required=False, default=200)
    parser.add_argument('--seX'   , dest='seX'     , type=float, required=False, default=0.0)
    parser.add_argument('--seY'   , dest='seY'     , type=float, required=False, default=0.0)
    parser.add_argument('--seZ'   , dest='seZ'     , type=float, required=False, default=0.0)
    parser.add_argument('--ssd'   , dest='ssd'     , type=float, required=False, default=2000.0)
    parser.add_argument('--beam'        , dest='beam'       , type=str  , required=False, nargs='?', default='G000'  )
    parser.add_argument('--machine'     , dest='machine'    , type=str  , required=False, nargs='?', default='1.1')
    parser.add_argument('--dosimeter'   , dest='dosimeter'  , type=str  , required=False, nargs='?', default='NP'    )
    parser.add_argument('--tuneid'      , dest='tuneid'     , type=str  , required=False, nargs='?', default='Tune'  )
    parser.add_argument('--gangle'      , dest='gangle'     , type=int  , required=False, nargs='?', default=0       )
    args = parser.parse_args(args)

    # Check output dir
    if not os.path.exists(args.outdir):
        print('Creating output folder',args.outdir)
        os.mkdir(args.outdir)
        os.mkdir(os.path.join(args.outdir,'ct'))
    else:
        print('Writing to preexisting output folder',args.outdir)

    # Define geometry of water cube
    rows = 512
    columns = 512
    slices = 512
    wrows = 1 # mm
    wcolumns = 1 # mm
    wslices = 1 # mm
    margin = 6 # pixels of air around water cube, so that water cube is 50cm * 50cm * 50cm

    cube = np.full((slices, rows, columns), -1000, dtype='int16') # -1000 HU as initialization value (air)
    cube[margin:-margin,margin:-margin,margin:-margin] = 0 # 0 HU cube (water)

    # Generate CT image
    for sl in range(slices):
        # ~ break
        output_file = os.path.join(args.outdir,'ct',str(sl+1)+'.dcm')
        image = cube[sl]
        print(output_file,image.shape)

        meta = Dataset()
        meta.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.2'#'CT Image Storage'
        meta.MediaStorageSOPInstanceUID = uid.generate_uid()
        meta.ImplementationClassUID = uid.PYDICOM_IMPLEMENTATION_UID
        meta.TransferSyntaxUID = uid.ImplicitVRLittleEndian
        ds = FileDataset(output_file, {}, file_meta=meta, preamble=b"\0" * 128)

        ds.SpecificCharacterSet     = 'ISO_IR 100'
        ds.ImageType                = ['DERIVED','SECONDARY','AXIAL']
        ds.InstanceCreationDate     = args.inDate
        ds.InstanceCreationTime     = args.inTime
        ds.SOPClassUID              = ds.file_meta.MediaStorageSOPClassUID
        ds.SOPInstanceUID           = ds.file_meta.MediaStorageSOPInstanceUID
        ds.StudyDate                = args.stDate
        ds.SeriesDate               = args.seDate
        ds.AcquisitionDate          = args.acDate
        ds.ContentDate              = args.coDate
        ds.StudyTime                = args.stTime
        ds.SeriesTime               = args.seTime
        ds.AcquisitionTime          = args.acTime
        ds.ContentTime              = args.coTime
        ds.AccessionNumber          = args.access
        ds.Modality                 = 'CT'
        ds.Manufacturer             = args.manuf
        ds.InstitutionName          = args.institution
        ds.InstitutionAddress       = args.instaddr
        ds.ReferringPhysicianName   = args.physician
        ds.StationName              = args.station
        ds.StudyDescription         = args.stDesc
        ds.SeriesDescription        = args.seDesc
        ds.OperatorsName            = args.operat
        ds.ManufacturerModelName    = args.manmod
        ds.PatientName              = args.patname
        ds.PatientID                = args.patid
        ds.PatientBirthDate         = args.birth
        ds.PatientSex               = args.sex
        ds.PatientAge               = args.age
        ds.PatientIdentityRemoved   = args.anon
        ds.AdditionalPatientHistory = 'Pseudo-CT'
        if args.anon == "YES":
            ds.DeidentificationMethod   = "Manual"
        ds.SliceThickness           = wslices
        # ~ ds.FocalSpots
        # ~ ds.KVP
        # ~ ds.DataCollectionDiameter
        # ~ ds.ReconstructionDiameter
        ds.SoftwareVersions         = args.sw
        # ~ ds.DistanceSourceToDetector
        # ~ ds.DistanceSourceToPatient
        # ~ ds.GantryDetectorTilt
        # ~ ds.ExposureTime
        # ~ ds.XRayTubeCurrent
        # ~ ds.RotationDirection
        # ~ ds.ConvolutionKerne
        # ~ ds.FilterType
        ds.ProtocolName             = 'RESEARCH'
        # ~ ds.ScanOptions = 'AXIAL MODE'
        ds.DateOfLastCalibration    = args.lastCal
        ds.PatientPosition          = 'HFS'
        ds.StudyInstanceUID         = args.stuid
        ds.SeriesInstanceUID        = args.seuid
        ds.StudyID                  = args.stid
        ds.SeriesNumber             = args.sen
        ds.AcquisitionNumber        = args.acqn
        ds.InstanceNumber           = sl + 1
        # Zero (origin) is at surface of water on anterior side, and centered in the other axes. Image position refers to center point of corner voxel.
        ds.ImagePositionPatient     = [(-columns/2+0.5)*wcolumns,(-margin+0.5)*wrows, (slices/2-0.5-sl)*wslices]
        ds.ImageOrientationPatient  = ['1', '0', '0', '0', '1', '0']
        ds.FrameOfReferenceUID      = args.forUID
        ds.PositionReferenceIndicator = ''#'OM'
        ds.SliceLocation            = ds.ImagePositionPatient[2]

        ds.SamplesPerPixel          = 1
        ds.PhotometricInterpretation= 'MONOCHROME2'
        ds.Rows                     = rows
        ds.Columns                  = columns
        ds.PixelSpacing             = [wcolumns, wrows]
        ds.BitsAllocated            = 16
        ds.BitsStored               = 16
        ds.HighBit                  = 15
        ds.PixelRepresentation      = 1
        ds.SmallestImagePixelValue  = np.amin(image).tobytes()
        ds.LargestImagePixelValue   = np.amax(image).tobytes()
        ds.WindowCenter             = 0
        ds.WindowWidth              = 1000
        ds.RescaleIntercept         = 0
        ds.RescaleSlope             = 1
        ds.RescaleType              = "HU"
        ds.PixelData                = image.tobytes()
        # ~ ds.PixelPaddingValue        = -2000

        ds.save_as(output_file, False)

    # Generate RTplan with certain energies
    meta = Dataset()
    meta.MediaStorageSOPClassUID    = '1.2.840.10008.5.1.4.1.1.481.8' #RT Ion Plan Storage
    meta.MediaStorageSOPInstanceUID = uid.generate_uid()
    meta.ImplementationClassUID     = uid.PYDICOM_IMPLEMENTATION_UID
    meta.TransferSyntaxUID = uid.ImplicitVRLittleEndian
    output_file = os.path.join(args.outdir,'rtplan.dcm')
    ds = FileDataset(output_file, {}, file_meta=meta, preamble=b"\0" * 128)

    ds.SpecificCharacterSet     = 'ISO_IR 100'
    ds.InstanceCreationDate     = args.inDate
    ds.InstanceCreationTime     = args.inTime
    ds.SOPClassUID              = ds.file_meta.MediaStorageSOPClassUID
    ds.SOPInstanceUID           = ds.file_meta.MediaStorageSOPInstanceUID
    ds.StudyDate                = args.stDate
    ds.SeriesDate               = args.seDate
    ds.StudyTime                = args.stTime
    ds.SeriesTime               = args.seTime
    ds.AccessionNumber          = ''
    ds.Modality                 = 'RTPLAN'
    ds.Manufacturer             = args.manuf
    ds.InstitutionName          = args.institution
    ds.ReferringPhysicianName   = args.physician
    ds.StudyDescription         = args.stDesc
    ds.SeriesDescription        = args.seDesc
    ds.OperatorsName            = args.operat
    ds.ManufacturerModelName    = args.manmod
    ds.PatientName              = args.patname
    ds.PatientID                = args.patid
    ds.PatientBirthDate         = args.birth
    ds.PatientSex               = args.sex
    ds.PatientAge               = args.age
    ds.PatientIdentityRemoved   = args.anon
    if args.anon=="YES":
        ds.DeidentificationMethod   = "Manual"
    ds.SoftwareVersions         = args.sw
    ds.DateOfLastCalibration    = args.lastCal
    ds.StudyInstanceUID         = args.stuid
    ds.SeriesInstanceUID        = args.seuid
    ds.StudyID                  = args.stid
    ds.SeriesNumber             = args.sen
    ds.InstanceNumber           = args.acqn
    ds.FrameOfReferenceUID      = args.forUID
    ds.PositionReferenceIndicator = ''
    ds.RTPlanLabel              = args.RTlabel
    ds.RTPlanName               = args.RTname
    ds.RTPlanDescription        = args.RTdesc
    ds.RTPlanDate               = args.RTdate
    ds.RTPlanTime               = args.RTtime
    ds.RTPlanGeometry           = args.RTgeom

    ds.FractionGroupSequence = Sequence()
    dsfx = Dataset()
    dsfx.FractionGroupNumber      = 1
    dsfx.FractionGroupDescription = ''
    dsfx.NumberOfFractionsPlanned = 1
    dsfx.NumberOfBeams            = 1
    dsfx.NumberOfBrachyApplicationSetups = 0
    dsfx.ReferencedBeamSequence = Sequence()
    dsfx_b = Dataset()
    dsfx_b.BeamDoseSpecificationPoint = [args.isoX,args.isoY,args.isoZ]
    dsfx_b.BeamDose = 1 #dummy
    dsfx_b.BeamMeterset = float(len(energies)*1e9)#1e9 protons per spot
    dsfx_b.ReferencedBeamNumber = 1
    dsfx.ReferencedBeamSequence.append(dsfx_b)
    ds.FractionGroupSequence.append(dsfx)

    ds.PatientSetupSequence = Sequence()
    pss = Dataset()
    pss.PatientPosition = 'HFS'
    pss.PatientSetupNumber = 1
    pss.PatientSetupLabel = 'Standard'
    ds.PatientSetupSequence.append(pss)

    ds.IonBeamSequence = Sequence()
    be = Dataset()
    ds.IonBeamSequence.append(be)
    be.BeamName = args.beam
    be.IonControlPointSequence = Sequence()
    be.TreatmentMachineName = args.machine
    be.InstitutionName = args.institution
    be.PrimaryDosimeterUnit = args.dosimeter
    be.Manufacturer = args.manuf
    be.InstitutionName  = args.institution
    be.ManufacturerModelName  = args.machine
    be.InstitutionAddress = args.instaddr
    be.TreatmentMachineName   = args.machine
    be.PrimaryDosimeterUnit   = args.dosimeter
    be.BeamNumber             = '1'
    be.BeamName               = args.beam
    be.BeamDescription        = 'Gantry from top'
    be.BeamType               = 'STATIC'
    be.RadiationType          = 'PROTON'
    be.TreatmentDeliveryType  = 'TREATMENT'
    be.NumberOfWedges         = 0
    be.NumberOfCompensators   = 0
    be.NumberOfBoli           = 0
    be.NumberOfBlocks         = args.nBlocks
    be.FinalCumulativeMetersetWeight = int(1e9*len(energies)) # 1e9 protons per spot (energy)
    be.NumberOfControlPoints  = 2*len(energies)
    be.ScanMode                   = 'MODULATED'
    be.VirtualSourceAxisDistances = [args.sadX, args.sadY]
    if not args.snout == '':
        be.SnoutSequence = Sequence()
        sds = Dataset()
        sds.AccessoryCode = args.snout
        sds.SnoutID = args.snout
        be.SnoutSequence.append(sds)
    be.NumberOfRangeShifters   = args.nRS
    if args.nRS == 1:
        be.RangeShifterSequence = Sequence()
        rsds = Dataset()
        rsds.AccessoryCode = 'Undefined Accessory Code'
        rsds.RangeShifterNumber = 1
        rsds.RangeShifterID = args.rsID
        rsds.RangeShifterType = 'BINARY'
        be.RangeShifterSequence.append(rsds)
    be.NumberOfLateralSpreadingDevices = 0
    be.NumberOfRangeModulators = 0
    be.PatientSupportType = 'TABLE'
    cweight = 0
    for i,energy in enumerate(energies[::-1]):
        for j in range(2):
            icpoi = Dataset()
            icpoi.NominalBeamEnergyUnit = 'MEV'
            icpoi.ControlPointIndex = i*2 + j
            icpoi.NominalBeamEnergy = str(energy)
            if j == 0:
                icpoi.GantryAngle = args.gangle
                icpoi.GantryRotationDirection = 'NONE'
                icpoi.BeamLimitingDeviceAngle = 0
                icpoi.BeamLimitingDeviceRotationDirection = 'NONE'
                icpoi.PatientSupportAngle = 0
                icpoi.PatientSupportRotationDirection = 'NONE'
                icpoi.TableTopVerticalPosition     = 0
                icpoi.TableTopLongitudinalPosition = 0
                icpoi.TableTopLateralPosition      = 0
                icpoi.IsocenterPosition            = [args.isoX,args.isoY,args.isoZ]
                icpoi.SurfaceEntryPoint            = [args.seX,args.seY,args.seZ]
                icpoi.SourceToSurfaceDistance      = args.ssd
            icpoi.CumulativeMetersetWeight = cweight
            if j == 0:
                icpoi.TableTopPitchAngle = 0
                icpoi.TableTopPitchRotationDirection = 'NONE'
                icpoi.TableTopRollAngle  = 0
                icpoi.TableTopRollRotationDirection = 'NONE'
                icpoi.GantryPitchAngle = 0.0
                icpoi.GantryPitchRotationDirection = 'NONE'
                icpoi.SnoutPosition      = args.snPos
            icpoi.ScanSpotTuneID = args.tuneid
            icpoi.NumberOfScanSpotPositions = 1
            icpoi.ScanSpotPositionMap = [0.0, 0.0]
            icpoi.ScanSpotMetersetWeights = 1e9 if j == 0 else 0
            cweight += icpoi.ScanSpotMetersetWeights
            icpoi.ScanningSpotSize = [lat_sigma[len(energies)-i-1],lat_sigma[len(energies)-i-1]]
            icpoi.NumberOfPaintings = 1
            be.IonControlPointSequence.append(icpoi)
    be.ReferencedPatientSetupNumber = 1
    be.ReferencedToleranceTableNumber = 0

    ds.ApprovalStatus = 'UNAPPROVED'
    # ~ ds.ReviewDate     = args.inDate
    # ~ ds.ReviewTime     = args.inTime
    # ~ ds.ReviewerName   = 'You'

    ds.save_as(output_file, False)
    print('Done, saved to',output_file)
예제 #2
0
def generate_rtplan_beam(field):

    beam = Dataset()
    beam.BeamNumber = 1  # TODO: Required, need to auto-set
    beam.BeamName = "3x3 J"  # TODO: Optional, want to auto-set
    beam.BeamDescription = "3x3 J - generated by PyMedPhys"  # Optional, want to auto-set
    beam.BeamType = "STATIC"
    beam.RadiationType = "PHOTON"  # TODO: Handle electrons

    # Primary Fluence Mode Sequence & Fluence Mode (assume 1)
    primary_fluence_mode_sequence = Sequence()
    beam.PrimaryFluenceModeSequence = primary_fluence_mode_sequence
    primary_fluence_mode = Dataset()
    primary_fluence_mode.FluenceMode = 'STANDARD'
    primary_fluence_mode_sequence.append(primary_fluence_mode)

    # beam.HighDoseTechniqueType = "HDR" # TODO: Check this - might be needed for FFF?

    # Treatment Machine Required for ARIA/Eclipse (Must match existing machine in
    # database). Should we handle this in our TPS Dose Toolbox?
    beam.TreatmentMachineName = "TS2_TBHD"
    beam.PrimaryDosimeterUnit = 'MU'

    # Optional, but probably should set (user?)
    beam.SourceAxisDistance = "1000"

    # ----- Beam Limiting Device Sequence -----
    beam_limiting_device_sequence = Sequence()
    beam.BeamLimitingDeviceSequence = beam_limiting_device_sequence

    # Beam Limiting Device Sequence: Beam Limiting Device 1 ( X Jaws)
    beam_limiting_device1 = Dataset()
    beam_limiting_device1.RTBeamLimitingDeviceType = 'X'  # or "ASYMX"
    beam_limiting_device1.NumberOfLeafJawPairs = "1"
    beam_limiting_device_sequence.append(beam_limiting_device1)

    # Beam Limiting Device Sequence: Beam Limiting Device 2 ( Y Jaws)
    beam_limiting_device2 = Dataset()
    beam_limiting_device2.RTBeamLimitingDeviceType = 'Y'  # or "ASYMY"
    beam_limiting_device2.NumberOfLeafJawPairs = "1"
    beam_limiting_device_sequence.append(beam_limiting_device2)

    if HAS_MLC:
        # Beam Limiting Device Sequence: Beam Limiting Device 3 ( X MLC)
        beam_limiting_device3 = Dataset()
        beam_limiting_device3.RTBeamLimitingDeviceType = 'MLCX'
        beam_limiting_device3.NumberOfLeafJawPairs = "1"
        beam_limiting_device_sequence.append(beam_limiting_device3)

        # Beam Limiting Device Sequence: Beam Limiting Device 4 ( Y MLC)
        beam_limiting_device4 = Dataset()
        beam_limiting_device4.RTBeamLimitingDeviceType = 'MLCY'
        beam_limiting_device4.NumberOfLeafJawPairs = "1"
        beam_limiting_device_sequence.append(beam_limiting_device4)

    # Optional but might be best to set.
    beam.TreatmentDeliveryType = 'TREATMENT'

    # ----- Wedge Sequence TODO: Handle wedges -----

    # Assume no wedge or same wedge throughout beam (0 or 1)
    beam.NumberOfWedges = 0
    if HAS_WEDGE:
        wedge_sequence = Sequence()
        beam.WedgeSequence = wedge_sequence

        # Wedge Sequence: Wedge
        wedge = Dataset()
        wedge.WedgeNumber = 1  # Unique within beam
        wedge.WedgeType = "DYNAMIC"  # or "STANDARD" or "MOTORIZED"
        wedge.WedgeID = ""  # Optional, TODO: check if needed
        wedge.AccessoryCode = ""  # Optional TODO: check if needed
        wedge.WedgeAngle = 60
        wedge.WedgeFactor = ""  # Required but can leave empty
        # Degrees relative to Beam Limiting Device TODO: (always 0 for EDW?)
        wedge.WedgeOrientation = 0
        wedge_sequence.append(wedge)

    beam.NumberOfCompensators = '0'  # ASSUME NO COMPENSATORS
    beam.NumberOfBoli = '0'  # ASSUME NO BOLI

    # ASSUME NO BLOCKS (TODO: check if used for electrons)
    beam.NumberOfBlocks = '0'

    # ----- Applicator Sequence (electrons & SRS) TODO: Handle cones -----
    if HAS_APPLICATOR:
        applicator_sequence = Sequence()
        beam.ApplicatorSequence = applicator_sequence

        # Applicator Sequence: Applicator
        applicator = Dataset()
        applicator.ApplicatorID = "<machine supplied ID>"  # TODO: check cone ID
        applicator.AccessoryCode = ""  # Optional TODO: check if needed
        applicator.ApplicatorType = "ELECTRON_SQUARE"  # many others possible
        applicator.ApplicatorGeometrySequence = [Dataset()]
        # RECTANGLE and CIRCULAR also possible
        applicator.ApplicatorGeometrySequence[
            0].ApplicatorApertureShape = "SYM_SQUARE"
        # Required for SQUARE and CIRCLE = length of side or diameter
        applicator.ApplicatorGeometrySequence[0].ApplicatorOpening = 10
        # Required if RECTANGLE
        applicator.ApplicatorGeometrySequence[0].ApplicatorOpeningX
        # Required if RECTANGLE
        applicator.ApplicatorGeometrySequence[0].ApplicatorOpeningY
        # Optional TODO: decide whether to set cone desc
        applicator.ApplicatorDescription = ""
        applicator_sequence.append(applicator)

    # ASSUME NO ACCESSORIES? TODO: check if this includes cutouts

    beam.FinalCumulativeMetersetWeight = "1"
    beam.NumberOfControlPoints = "2"

    # ----- Control Point Sequence -----
    cp_sequence = Sequence()
    beam.ControlPointSequence = cp_sequence

    # Control Point Sequence: Control Point 0
    cp0 = Dataset()
    cp0.ControlPointIndex = "0"
    cp0.CumulativeMetersetWeight = "0"
    cp0.NominalBeamEnergy = "6"  # TODO: User supplied
    cp0.DoseRateSet = "600"  # TODO: User supplied

    # Wedge Position Sequence
    if HAS_WEDGE:
        wedge_position_sequence = Sequence()
        cp0.WedgePositionSequence = wedge_position_sequence

        # Wedge Position Sequence: Wedge
        wedge_position = Dataset()
        wedge_position.ReferencedWedgeNumber = 1  # Assume never more than 1 wedge
        wedge_position.WedgePosition = "IN"  # Also "OUT"
        wedge_position_sequence.append(wedge_position)

    # Beam Limiting Device Position Sequence
    beam_limiting_device_position_sequence = Sequence()
    cp0.BeamLimitingDevicePositionSequence = beam_limiting_device_position_sequence

    # Beam Limiting Device Position Sequence: Beam Limiting Device Position 1
    beam_limiting_device_position1 = Dataset()
    # or "ASYMX" TODO: User supplied
    beam_limiting_device_position1.RTBeamLimitingDeviceType = 'X'
    beam_limiting_device_position1.LeafJawPositions = ['-15', '15'
                                                       ]  # TODO: User supplied
    beam_limiting_device_position_sequence.append(
        beam_limiting_device_position1)

    # Beam Limiting Device Position Sequence: Beam Limiting Device Position 2
    beam_limiting_device_position2 = Dataset()
    # or "ASYMY" TODO: User supplied
    beam_limiting_device_position2.RTBeamLimitingDeviceType = 'Y'
    beam_limiting_device_position2.LeafJawPositions = ['-15', '15'
                                                       ]  # TODO: User supplied
    beam_limiting_device_position_sequence.append(
        beam_limiting_device_position2)

    if HAS_MLC:  # TODO: Handle MLCs
        # Beam Limiting Device Position Sequence: Beam Limiting Device Position 3
        beam_limiting_device_position3 = Dataset()
        beam_limiting_device_position3.RTBeamLimitingDeviceType = 'MLCX'
        beam_limiting_device_position3.LeafJawPositions = [
        ]  # TODO: Tricksy MLC stuff
        beam_limiting_device_position_sequence.append(
            beam_limiting_device_position3)

        # Beam Limiting Device Position Sequence: Beam Limiting Device Position 4
        beam_limiting_device_position4 = Dataset()
        beam_limiting_device_position4.RTBeamLimitingDeviceType = 'MLCY'
        beam_limiting_device_position4.LeafJawPositions = [
        ]  # TODO: Tricksy MLC stuff
        beam_limiting_device_position_sequence.append(
            beam_limiting_device_position4)

    cp0.GantryAngle = "0"  # TODO: User supplied, default to 0
    cp0.GantryRotationDirection = 'NONE'
    cp0.BeamLimitingDeviceAngle = "0"  # TODO: User supplied, default to 0
    cp0.BeamLimitingDeviceRotationDirection = 'NONE'
    cp0.PatientSupportAngle = "0"
    cp0.PatientSupportRotationDirection = 'NONE'
    cp0.TableTopEccentricAngle = "0"
    cp0.TableTopEccentricRotationDirection = 'NONE'
    cp0.TableTopPitchAngle = 0.0
    cp0.TableTopPitchRotationDirection = 'NONE'
    cp0.TableTopRollAngle = 0.0
    cp0.TableTopRollRotationDirection = 'NONE'
    cp0.TableTopVerticalPosition = ''  # Required but can leave empty
    cp0.TableTopLongitudinalPosition = ''  # Required but can leave empty
    cp0.TableTopLateralPosition = ''  # Required but can leave empty
    # Required but can leave empty TODO: not according to RS!
    cp0.IsocenterPosition = ['0', '0', '0']
    cp0.SourceToSurfaceDistance = 900  # TODO: User supplied

    cp_sequence.append(cp0)

    # Control Point Sequence: Control Point 1
    cp1 = Dataset()
    cp1.ControlPointIndex = "1"
    cp1.CumulativeMetersetWeight = "1"

    cp_sequence.append(cp1)

    # Referenced Beam Sequence: Referenced Beam 1
    refd_beam = Dataset()
    refd_beam.ReferencedBeamNumber = "1"
    refd_beam.BeamMeterset = "100"  # TODO: Set upon plan setup

    return beam, refd_beam