rtp-connect 1.5 → 1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.rdoc +19 -0
- data/Gemfile.lock +1 -1
- data/lib/rtp-connect/control_point.rb +78 -0
- data/lib/rtp-connect/methods.rb +2 -2
- data/lib/rtp-connect/plan_to_dcm.rb +380 -226
- data/lib/rtp-connect/version.rb +1 -1
- metadata +3 -3
    
        data/CHANGELOG.rdoc
    CHANGED
    
    | @@ -1,3 +1,22 @@ | |
| 1 | 
            +
            = 1.6
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            === 12th December, 2013
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            * Plan#to_dcm improvements:
         | 
| 6 | 
            +
              * Added support for VMAT by improving the handling of control point conversion.
         | 
| 7 | 
            +
              * Made dose reference sequences optional.
         | 
| 8 | 
            +
              * Order beam limiting device items alphabetically.
         | 
| 9 | 
            +
              * Added rudimentary support for scale conversion (scale convention = 1 in control point records).
         | 
| 10 | 
            +
              * More robust extraction of jaw position.
         | 
| 11 | 
            +
              * More robust handling of cases with missing structure set in the RTP file.
         | 
| 12 | 
            +
              * Add support for tolerance table sequence.
         | 
| 13 | 
            +
              * Fixed a bug with missing leaf boundary value for 80 and 160 leaf MLCs.
         | 
| 14 | 
            +
              * Switched to using fractional cumulative meterset weight, which seems to be more commonly used in commercial systems.
         | 
| 15 | 
            +
              * Don't create an SSD DICOM element if the SSD attribute in the RTP file is undefined.
         | 
| 16 | 
            +
              * Only write control point attributes which have changed since the previous/initial control point of each beam.
         | 
| 17 | 
            +
              * Make sure that the last cumulative meterset weight exactly equals the final cumulative meterset weight.
         | 
| 18 | 
            +
             | 
| 19 | 
            +
             | 
| 1 20 | 
             
            = 1.5
         | 
| 2 21 |  | 
| 3 22 | 
             
            === 24th October, 2013
         | 
    
        data/Gemfile.lock
    CHANGED
    
    
| @@ -143,6 +143,57 @@ module RTP | |
| 143 143 | 
             
                  return Array.new
         | 
| 144 144 | 
             
                end
         | 
| 145 145 |  | 
| 146 | 
            +
                # Converts the collimator_x1 attribute to proper DICOM format.
         | 
| 147 | 
            +
                #
         | 
| 148 | 
            +
                # @return [Float] the DICOM-formatted collimator_x1 attribute
         | 
| 149 | 
            +
                #
         | 
| 150 | 
            +
                def dcm_collimator_x1
         | 
| 151 | 
            +
                  attribute = (scale_convertion? ? :collimator_y1 : :collimator_x1)
         | 
| 152 | 
            +
                  target = (@field_x_mode && !@field_x_mode.empty? ? self : @parent)
         | 
| 153 | 
            +
                  target.send(attribute).to_f * 10 * scale_factor
         | 
| 154 | 
            +
                end
         | 
| 155 | 
            +
             | 
| 156 | 
            +
                # Converts the collimator_x2 attribute to proper DICOM format.
         | 
| 157 | 
            +
                #
         | 
| 158 | 
            +
                # @return [Float] the DICOM-formatted collimator_x2 attribute
         | 
| 159 | 
            +
                #
         | 
| 160 | 
            +
                def dcm_collimator_x2
         | 
| 161 | 
            +
                  attribute = (scale_convertion? ? :collimator_y2 : :collimator_x2)
         | 
| 162 | 
            +
                  target = (@field_x_mode && !@field_x_mode.empty? ? self : @parent)
         | 
| 163 | 
            +
                  target.send(attribute).to_f * 10
         | 
| 164 | 
            +
                end
         | 
| 165 | 
            +
             | 
| 166 | 
            +
                # Converts the collimator_y1 attribute to proper DICOM format.
         | 
| 167 | 
            +
                #
         | 
| 168 | 
            +
                # @return [Float] the DICOM-formatted collimator_y1 attribute
         | 
| 169 | 
            +
                #
         | 
| 170 | 
            +
                def dcm_collimator_y1
         | 
| 171 | 
            +
                  attribute = (scale_convertion? ? :collimator_x1 : :collimator_y1)
         | 
| 172 | 
            +
                  target = (@field_y_mode && !@field_y_mode.empty? ? self : @parent)
         | 
| 173 | 
            +
                  target.send(attribute).to_f * 10 * scale_factor
         | 
| 174 | 
            +
                end
         | 
| 175 | 
            +
             | 
| 176 | 
            +
                # Converts the collimator_y2 attribute to proper DICOM format.
         | 
| 177 | 
            +
                #
         | 
| 178 | 
            +
                # @return [Float] the DICOM-formatted collimator_y2 attribute
         | 
| 179 | 
            +
                #
         | 
| 180 | 
            +
                def dcm_collimator_y2
         | 
| 181 | 
            +
                  attribute = (scale_convertion? ? :collimator_x2 : :collimator_y2)
         | 
| 182 | 
            +
                  target = (@field_y_mode && !@field_y_mode.empty? ? self : @parent)
         | 
| 183 | 
            +
                  target.send(attribute).to_f * 10
         | 
| 184 | 
            +
                end
         | 
| 185 | 
            +
             | 
| 186 | 
            +
                # Converts the mlc_lp_a & mlc_lp_b attributes to a proper DICOM formatted string.
         | 
| 187 | 
            +
                #
         | 
| 188 | 
            +
                # @return [String] the DICOM-formatted leaf pair positions
         | 
| 189 | 
            +
                #
         | 
| 190 | 
            +
                def dcm_mlc_positions
         | 
| 191 | 
            +
                  # As with the collimators, the first side (1/a) may need scale invertion:
         | 
| 192 | 
            +
                  pos_a = @mlc_lp_a.collect{|p| (p.to_f * 10 * scale_factor).round(1) unless p.empty?}.compact
         | 
| 193 | 
            +
                  pos_b = @mlc_lp_b.collect{|p| (p.to_f * 10).round(1) unless p.empty?}.compact
         | 
| 194 | 
            +
                  (pos_a + pos_b).join("\\")
         | 
| 195 | 
            +
                end
         | 
| 196 | 
            +
             | 
| 146 197 | 
             
                # Computes a hash code for this object.
         | 
| 147 198 | 
             
                #
         | 
| 148 199 | 
             
                # @note Two objects with the same attributes will have the same hash code.
         | 
| @@ -526,6 +577,33 @@ module RTP | |
| 526 577 | 
             
                #
         | 
| 527 578 | 
             
                alias_method :state, :values
         | 
| 528 579 |  | 
| 580 | 
            +
                # Checks whether the contents of the this record indicates that scale
         | 
| 581 | 
            +
                # convertion is to be applied. This convertion entails converting a value
         | 
| 582 | 
            +
                # from IEC1217 format to the target machine's native readout format.
         | 
| 583 | 
            +
                # Note that the scope of this scale conversion is not precisely known (the
         | 
| 584 | 
            +
                # current implementation is based on a few observations made from a single
         | 
| 585 | 
            +
                # RTP file).
         | 
| 586 | 
            +
                #
         | 
| 587 | 
            +
                # @return [Boolean] true if the scale convention attribute indicates scale convertion
         | 
| 588 | 
            +
                #
         | 
| 589 | 
            +
                def scale_convertion?
         | 
| 590 | 
            +
                  # A scale convention of 1 means that geometric parameters are represented
         | 
| 591 | 
            +
                  # in the target machine's native readout format, as opposed to the IEC 1217
         | 
| 592 | 
            +
                  # convention. The consequences of this is not totally clear, but at least for
         | 
| 593 | 
            +
                  # an Elekta device, there are a number of convertions which seems to be indicated.
         | 
| 594 | 
            +
                  @scale_convention.to_i == 1 ? true : false
         | 
| 595 | 
            +
                end
         | 
| 596 | 
            +
             | 
| 597 | 
            +
                # Gives a factor used for scale convertion, which depends on the
         | 
| 598 | 
            +
                # 'scale_convention' attribute.
         | 
| 599 | 
            +
                #
         | 
| 600 | 
            +
                # @param [Numerical] value the value to process
         | 
| 601 | 
            +
                # @return [Numerical] the scale converted value
         | 
| 602 | 
            +
                #
         | 
| 603 | 
            +
                def scale_factor
         | 
| 604 | 
            +
                  scale_convertion? ? -1 : 1
         | 
| 605 | 
            +
                end
         | 
| 606 | 
            +
             | 
| 529 607 | 
             
              end
         | 
| 530 608 |  | 
| 531 609 | 
             
            end
         | 
    
        data/lib/rtp-connect/methods.rb
    CHANGED
    
    | @@ -20,7 +20,7 @@ module RTP | |
| 20 20 | 
             
                      -15, -5, 5, 15, 25, 35, 45, 55, 65, 75, 85, 95, 105, 115, 125, 135, 200
         | 
| 21 21 | 
             
                    ]
         | 
| 22 22 | 
             
                  when 40
         | 
| 23 | 
            -
                    Array.new(nr_leaves) {|i| (i * 400 / nr_leaves.to_f - 200).to_i}
         | 
| 23 | 
            +
                    Array.new(nr_leaves+1) {|i| (i * 400 / nr_leaves.to_f - 200).to_i}
         | 
| 24 24 | 
             
                  when 41
         | 
| 25 25 | 
             
                    [-200, -195, -185, -175, -165, -155, -145, -135, -125, -115,
         | 
| 26 26 | 
             
                      -105, -95, -85, -75, -65, -55, -45, -35, -25, -15, -5, 5, 15, 25, 35, 45,
         | 
| @@ -33,7 +33,7 @@ module RTP | |
| 33 33 | 
             
                      70, 75, 80, 85, 90, 95, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200
         | 
| 34 34 | 
             
                    ]
         | 
| 35 35 | 
             
                  when 80
         | 
| 36 | 
            -
                    Array.new(nr_leaves) {|i| (i * 400 / nr_leaves.to_f - 200).to_i}
         | 
| 36 | 
            +
                    Array.new(nr_leaves+1) {|i| (i * 400 / nr_leaves.to_f - 200).to_i}
         | 
| 37 37 | 
             
                  else
         | 
| 38 38 | 
             
                    raise ArgumentError, "Unsupported number of leaves: #{nr_leaves}"
         | 
| 39 39 | 
             
                  end
         | 
| @@ -5,11 +5,12 @@ module RTP | |
| 5 5 | 
             
                # Converts the Plan (and child) records to a
         | 
| 6 6 | 
             
                # DICOM::DObject of modality RTPLAN.
         | 
| 7 7 | 
             
                #
         | 
| 8 | 
            -
                # @note Only  | 
| 9 | 
            -
                #   Electron beams  | 
| 8 | 
            +
                # @note Only photon plans have been tested.
         | 
| 9 | 
            +
                #   Electron beams beams may give an invalid DICOM file.
         | 
| 10 10 | 
             
                #   Also note that, due to limitations in the RTP file format, some original
         | 
| 11 11 | 
             
                #   values can not be recreated, like e.g. Study UID or Series UID.
         | 
| 12 12 | 
             
                # @param [Hash] options the options to use for creating the DICOM object
         | 
| 13 | 
            +
                # @option options [Boolean] :dose_ref if set, Dose Reference & Referenced Dose Reference sequences will be included in the generated DICOM file
         | 
| 13 14 | 
             
                # @option options [String] :manufacturer the value used for the manufacturer tag (0008,0070) in the beam sequence
         | 
| 14 15 | 
             
                # @option options [String] :model the value used for the manufacturer's model name tag (0008,1090) in the beam sequence
         | 
| 15 16 | 
             
                # @option options [String] :serial_number the value used for the device serial number tag (0018,1000) in the beam sequence
         | 
| @@ -103,27 +104,34 @@ module RTP | |
| 103 104 | 
             
                  # RT Plan Time:
         | 
| 104 105 | 
             
                  plan_time = @plan_time.empty? ? Time.now.strftime("%H%M%S") : @plan_time
         | 
| 105 106 | 
             
                  DICOM::Element.new('300A,0007', plan_time, :parent => dcm)
         | 
| 106 | 
            -
                  # RT Plan Geometry:
         | 
| 107 | 
            -
                  DICOM::Element.new('300A,000C', 'PATIENT', :parent => dcm)
         | 
| 108 107 | 
             
                  # Approval Status:
         | 
| 109 108 | 
             
                  DICOM::Element.new('300E,0002', 'UNAPPROVED', :parent => dcm)
         | 
| 110 109 | 
             
                  #
         | 
| 111 110 | 
             
                  # SEQUENCES:
         | 
| 112 111 | 
             
                  #
         | 
| 113 | 
            -
                  #
         | 
| 114 | 
            -
                   | 
| 115 | 
            -
             | 
| 116 | 
            -
             | 
| 117 | 
            -
             | 
| 118 | 
            -
             | 
| 119 | 
            -
                   | 
| 120 | 
            -
                  #  | 
| 121 | 
            -
                   | 
| 122 | 
            -
                     | 
| 123 | 
            -
             | 
| 124 | 
            -
                     | 
| 112 | 
            +
                  # Tolerance Table Sequence:
         | 
| 113 | 
            +
                  if p && p.fields.first && !p.fields.first.tolerance_table.empty?
         | 
| 114 | 
            +
                    tt_seq = DICOM::Sequence.new('300A,0040', :parent => dcm)
         | 
| 115 | 
            +
                    tt_item = DICOM::Item.new(:parent => tt_seq)
         | 
| 116 | 
            +
                    # Tolerance Table Number:
         | 
| 117 | 
            +
                    DICOM::Element.new('300A,0042', p.fields.first.tolerance_table, :parent => tt_item)
         | 
| 118 | 
            +
                  end
         | 
| 119 | 
            +
                  # Structure set information:
         | 
| 120 | 
            +
                  if p && p.site_setup && !p.site_setup.structure_set_uid.empty?
         | 
| 121 | 
            +
                    #
         | 
| 122 | 
            +
                    # Referenced Structure Set Sequence:
         | 
| 123 | 
            +
                    #
         | 
| 124 | 
            +
                    ss_seq = DICOM::Sequence.new('300C,0060', :parent => dcm)
         | 
| 125 | 
            +
                    ss_item = DICOM::Item.new(:parent => ss_seq)
         | 
| 126 | 
            +
                    # Referenced SOP Class UID:
         | 
| 127 | 
            +
                    DICOM::Element.new('0008,1150', '1.2.840.10008.5.1.4.1.1.481.3', :parent => ss_item)
         | 
| 128 | 
            +
                    DICOM::Element.new('0008,1155', p.site_setup.structure_set_uid, :parent => ss_item)
         | 
| 129 | 
            +
                    # RT Plan Geometry:
         | 
| 130 | 
            +
                    DICOM::Element.new('300A,000C', 'PATIENT', :parent => dcm)
         | 
| 131 | 
            +
                  else
         | 
| 132 | 
            +
                    # RT Plan Geometry:
         | 
| 133 | 
            +
                    DICOM::Element.new('300A,000C', 'TREATMENT_DEVICE', :parent => dcm)
         | 
| 125 134 | 
             
                  end
         | 
| 126 | 
            -
                  DICOM::Element.new('0008,1155', ref_ss_uid, :parent => ss_item)
         | 
| 127 135 | 
             
                  #
         | 
| 128 136 | 
             
                  # Patient Setup Sequence:
         | 
| 129 137 | 
             
                  #
         | 
| @@ -143,16 +151,7 @@ module RTP | |
| 143 151 | 
             
                  #
         | 
| 144 152 | 
             
                  # Dose Reference Sequence:
         | 
| 145 153 | 
             
                  #
         | 
| 146 | 
            -
                   | 
| 147 | 
            -
                  dr_item = DICOM::Item.new(:parent => dr_seq)
         | 
| 148 | 
            -
                  # Dose Reference Number:
         | 
| 149 | 
            -
                  DICOM::Element.new('300A,0012', '1', :parent => dr_item)
         | 
| 150 | 
            -
                  # Dose Reference Structure Type:
         | 
| 151 | 
            -
                  DICOM::Element.new('300A,0014', 'SITE', :parent => dr_item)
         | 
| 152 | 
            -
                  # Dose Reference Description:
         | 
| 153 | 
            -
                  DICOM::Element.new('300A,0016', plan_name, :parent => dr_item)
         | 
| 154 | 
            -
                  # Dose Reference Type:
         | 
| 155 | 
            -
                  DICOM::Element.new('300A,0020', 'TARGET', :parent => dr_item)
         | 
| 154 | 
            +
                  create_dose_reference(dcm, plan_name) if options[:dose_ref]
         | 
| 156 155 | 
             
                  #
         | 
| 157 156 | 
             
                  # Fraction Group Sequence:
         | 
| 158 157 | 
             
                  #
         | 
| @@ -183,6 +182,8 @@ module RTP | |
| 183 182 | 
             
                      unless field.modality == 'Unspecified'
         | 
| 184 183 | 
             
                        # If this is an electron beam, a warning should be printed, as these are less reliably converted:
         | 
| 185 184 | 
             
                        logger.warn("This is not a photon beam (#{field.modality}). Beware that DICOM conversion of Electron beams are experimental, and other modalities are unsupported.") if field.modality != 'Xrays'
         | 
| 185 | 
            +
                        # Reset control point 'current value' attributes:
         | 
| 186 | 
            +
                        reset_cp_current_attributes
         | 
| 186 187 | 
             
                        # Beam number and name:
         | 
| 187 188 | 
             
                        beam_number = field.extended_field ? field.extended_field.original_beam_number : (i + 1).to_s
         | 
| 188 189 | 
             
                        beam_name = field.extended_field ? field.extended_field.original_beam_name : field.field_name
         | 
| @@ -220,6 +221,7 @@ module RTP | |
| 220 221 | 
             
                        beam_type = case field.treatment_type
         | 
| 221 222 | 
             
                          when 'Static' then 'STATIC'
         | 
| 222 223 | 
             
                          when 'StepNShoot' then 'STATIC'
         | 
| 224 | 
            +
                          when 'VMAT' then 'DYNAMIC'
         | 
| 223 225 | 
             
                          else logger.error("The beam type (treatment type) #{field.treatment_type} is not yet supported.")
         | 
| 224 226 | 
             
                        end
         | 
| 225 227 | 
             
                        DICOM::Element.new('300A,00C4', beam_type, :parent => b_item)
         | 
| @@ -241,36 +243,13 @@ module RTP | |
| 241 243 | 
             
                        # Number of Blocks:
         | 
| 242 244 | 
             
                        DICOM::Element.new('300A,00F0', (field.block.empty? ? '0' : '1'), :parent => b_item)
         | 
| 243 245 | 
             
                        # Final Cumulative Meterset Weight:
         | 
| 244 | 
            -
                        DICOM::Element.new('300A,010E',  | 
| 246 | 
            +
                        DICOM::Element.new('300A,010E', 1, :parent => b_item)
         | 
| 245 247 | 
             
                        # Referenced Patient Setup Number:
         | 
| 246 248 | 
             
                        DICOM::Element.new('300C,006A', '1', :parent => b_item)
         | 
| 247 249 | 
             
                        #
         | 
| 248 250 | 
             
                        # Beam Limiting Device Sequence:
         | 
| 249 251 | 
             
                        #
         | 
| 250 | 
            -
                         | 
| 251 | 
            -
                        # Always create one ASYMY item:
         | 
| 252 | 
            -
                        bl_item_y = DICOM::Item.new(:parent => bl_seq)
         | 
| 253 | 
            -
                        # RT Beam Limiting Device Type:
         | 
| 254 | 
            -
                        DICOM::Element.new('300A,00B8', "ASYMY", :parent => bl_item_y)
         | 
| 255 | 
            -
                        # Number of Leaf/Jaw Pairs:
         | 
| 256 | 
            -
                        DICOM::Element.new('300A,00BC', "1", :parent => bl_item_y)
         | 
| 257 | 
            -
                        # The ASYMX item ('backup jaws') only exsists on some models:
         | 
| 258 | 
            -
                        if ['SYM', 'ASY'].include?(field.field_x_mode.upcase)
         | 
| 259 | 
            -
                          bl_item_x = DICOM::Item.new(:parent => bl_seq)
         | 
| 260 | 
            -
                          DICOM::Element.new('300A,00B8', "ASYMX", :parent => bl_item_x)
         | 
| 261 | 
            -
                          DICOM::Element.new('300A,00BC', "1", :parent => bl_item_x)
         | 
| 262 | 
            -
                        end
         | 
| 263 | 
            -
                        # MLCX item is only created if leaves are defined:
         | 
| 264 | 
            -
                        # (NB: The RTP file doesn't specify leaf position boundaries, so we
         | 
| 265 | 
            -
                        # have to set these based on a set of known MLC types, their number
         | 
| 266 | 
            -
                        # of leaves, and their leaf boundary positions.)
         | 
| 267 | 
            -
                        if field.control_points.length > 0
         | 
| 268 | 
            -
                          bl_item_mlcx = DICOM::Item.new(:parent => bl_seq)
         | 
| 269 | 
            -
                          DICOM::Element.new('300A,00B8', "MLCX", :parent => bl_item_mlcx)
         | 
| 270 | 
            -
                          num_leaves = field.control_points.first.mlc_leaves.to_i
         | 
| 271 | 
            -
                          DICOM::Element.new('300A,00BC', num_leaves.to_s, :parent => bl_item_mlcx)
         | 
| 272 | 
            -
                          DICOM::Element.new('300A,00BE', "#{RTP.leaf_boundaries(num_leaves).join("\\")}", :parent => bl_item_mlcx)
         | 
| 273 | 
            -
                        end
         | 
| 252 | 
            +
                        create_beam_limiting_devices(b_item, field)
         | 
| 274 253 | 
             
                        #
         | 
| 275 254 | 
             
                        # Block Sequence (if any):
         | 
| 276 255 | 
             
                        # FIXME: It seems that the Block Sequence (300A,00F4) may be
         | 
| @@ -340,189 +319,25 @@ module RTP | |
| 340 319 | 
             
                            DICOM::Element.new('300A,012C', '', :parent => cp_item)
         | 
| 341 320 | 
             
                          end
         | 
| 342 321 | 
             
                          # Source to Surface Distance:
         | 
| 343 | 
            -
                           | 
| 322 | 
            +
                          add_ssd(field.ssd, cp_item)
         | 
| 344 323 | 
             
                          # Cumulative Meterset Weight:
         | 
| 345 | 
            -
                          DICOM::Element.new('300A,0134',  | 
| 324 | 
            +
                          DICOM::Element.new('300A,0134', '0', :parent => cp_item)
         | 
| 346 325 | 
             
                          # Beam Limiting Device Position Sequence:
         | 
| 347 | 
            -
                           | 
| 348 | 
            -
                          # Always create one ASYMY item:
         | 
| 349 | 
            -
                          dp_item_y = DICOM::Item.new(:parent => dp_seq)
         | 
| 350 | 
            -
                          # RT Beam Limiting Device Type:
         | 
| 351 | 
            -
                          DICOM::Element.new('300A,00B8', "ASYMY", :parent => dp_item_y)
         | 
| 352 | 
            -
                          # Leaf/Jaw Positions:
         | 
| 353 | 
            -
                          DICOM::Element.new('300A,011C', "#{field.collimator_y1.to_f * 10}\\#{field.collimator_y2.to_f * 10}", :parent => dp_item_y)
         | 
| 354 | 
            -
                          # The ASYMX item ('backup jaws') only exsists on some models:
         | 
| 355 | 
            -
                          if ['SYM', 'ASY'].include?(field.field_x_mode.upcase)
         | 
| 356 | 
            -
                            dp_item_x = DICOM::Item.new(:parent => dp_seq)
         | 
| 357 | 
            -
                            DICOM::Element.new('300A,00B8', "ASYMX", :parent => dp_item_x)
         | 
| 358 | 
            -
                            DICOM::Element.new('300A,011C', "#{field.collimator_x1.to_f * 10}\\#{field.collimator_x2.to_f * 10}", :parent => dp_item_x)
         | 
| 359 | 
            -
                          end
         | 
| 360 | 
            -
                          # MLCX:
         | 
| 361 | 
            -
                          if field.control_points.length > 0
         | 
| 362 | 
            -
                            dp_item_mlcx = DICOM::Item.new(:parent => dp_seq)
         | 
| 363 | 
            -
                            # RT Beam Limiting Device Type:
         | 
| 364 | 
            -
                            DICOM::Element.new('300A,00B8', "MLCX", :parent => dp_item_mlcx)
         | 
| 365 | 
            -
                            # Leaf/Jaw Positions:
         | 
| 366 | 
            -
                            pos_a = field.control_points.first.mlc_lp_a.collect{|p| (p.to_f * 10).round(2) unless p.empty?}.compact
         | 
| 367 | 
            -
                            pos_b = field.control_points.first.mlc_lp_b.collect{|p| (p.to_f * 10).round(2) unless p.empty?}.compact
         | 
| 368 | 
            -
                            leaf_pos = "#{pos_a.join("\\")}\\#{pos_b.join("\\")}"
         | 
| 369 | 
            -
                            DICOM::Element.new('300A,011C', leaf_pos, :parent => dp_item_mlcx)
         | 
| 370 | 
            -
                          end
         | 
| 326 | 
            +
                          create_beam_limiting_device_positions(cp_item, field.control_points.first)
         | 
| 371 327 | 
             
                          # Referenced Dose Reference Sequence:
         | 
| 372 | 
            -
                           | 
| 373 | 
            -
                          rd_item = DICOM::Item.new(:parent => rd_seq)
         | 
| 374 | 
            -
                          # Cumulative Dose Reference Coeffecient:
         | 
| 375 | 
            -
                          DICOM::Element.new('300A,010C', '', :parent => rd_item)
         | 
| 376 | 
            -
                          # Referenced Dose Reference Number:
         | 
| 377 | 
            -
                          DICOM::Element.new('300C,0051', '1', :parent => rd_item)
         | 
| 328 | 
            +
                          create_referenced_dose_reference(cp_item) if options[:dose_ref]
         | 
| 378 329 | 
             
                          # Second CP:
         | 
| 379 330 | 
             
                          cp_item = DICOM::Item.new(:parent => cp_seq)
         | 
| 380 331 | 
             
                          # Control Point Index:
         | 
| 381 332 | 
             
                          DICOM::Element.new('300A,0112', "1", :parent => cp_item)
         | 
| 382 333 | 
             
                          # Cumulative Meterset Weight:
         | 
| 383 | 
            -
                          DICOM::Element.new('300A,0134',  | 
| 334 | 
            +
                          DICOM::Element.new('300A,0134', '1', :parent => cp_item)
         | 
| 384 335 | 
             
                        else
         | 
| 385 | 
            -
                          # When we have multiple ( | 
| 386 | 
            -
                          field.control_points. | 
| 387 | 
            -
             | 
| 388 | 
            -
             | 
| 389 | 
            -
             | 
| 390 | 
            -
                            # Control Point Index:
         | 
| 391 | 
            -
                            DICOM::Element.new('300A,0112', "#{cp1.index}", :parent => cp_item1)
         | 
| 392 | 
            -
                            # Nominal Beam Energy:
         | 
| 393 | 
            -
                            DICOM::Element.new('300A,0114', "#{cp1.energy.to_f}", :parent => cp_item1)
         | 
| 394 | 
            -
                            # Dose Rate Set:
         | 
| 395 | 
            -
                            DICOM::Element.new('300A,0115', cp1.doserate, :parent => cp_item1)
         | 
| 396 | 
            -
                            # Gantry Angle:
         | 
| 397 | 
            -
                            DICOM::Element.new('300A,011E', cp1.gantry_angle, :parent => cp_item1)
         | 
| 398 | 
            -
                            # Gantry Rotation Direction:
         | 
| 399 | 
            -
                            DICOM::Element.new('300A,011F', (cp1.gantry_dir.empty? ? 'NONE' : cp1.gantry_dir), :parent => cp_item1)
         | 
| 400 | 
            -
                            # Beam Limiting Device Angle:
         | 
| 401 | 
            -
                            DICOM::Element.new('300A,0120', cp1.collimator_angle, :parent => cp_item1)
         | 
| 402 | 
            -
                            # Beam Limiting Device Rotation Direction:
         | 
| 403 | 
            -
                            DICOM::Element.new('300A,0121', (cp1.collimator_dir.empty? ? 'NONE' : cp1.collimator_dir), :parent => cp_item1)
         | 
| 404 | 
            -
                            # Patient Support Angle:
         | 
| 405 | 
            -
                            DICOM::Element.new('300A,0122', cp1.couch_pedestal, :parent => cp_item1)
         | 
| 406 | 
            -
                            # Patient Support Rotation Direction:
         | 
| 407 | 
            -
                            DICOM::Element.new('300A,0123', (cp1.couch_ped_dir.empty? ? 'NONE' : cp1.couch_ped_dir), :parent => cp_item1)
         | 
| 408 | 
            -
                            # Table Top Eccentric Angle:
         | 
| 409 | 
            -
                            DICOM::Element.new('300A,0125', cp1.couch_angle, :parent => cp_item1)
         | 
| 410 | 
            -
                            # Table Top Eccentric Rotation Direction:
         | 
| 411 | 
            -
                            DICOM::Element.new('300A,0126', (cp1.couch_dir.empty? ? 'NONE' : cp1.couch_dir), :parent => cp_item1)
         | 
| 412 | 
            -
                            # Table Top Vertical Position:
         | 
| 413 | 
            -
                            couch_vert = cp1.couch_vertical.empty? ? '' : (cp1.couch_vertical.to_f * 10).to_s
         | 
| 414 | 
            -
                            DICOM::Element.new('300A,0128', couch_vert, :parent => cp_item1)
         | 
| 415 | 
            -
                            # Table Top Longitudinal Position:
         | 
| 416 | 
            -
                            couch_long = cp1.couch_longitudinal.empty? ? '' : (cp1.couch_longitudinal.to_f * 10).to_s
         | 
| 417 | 
            -
                            DICOM::Element.new('300A,0129', couch_long, :parent => cp_item1)
         | 
| 418 | 
            -
                            # Table Top Lateral Position:
         | 
| 419 | 
            -
                            couch_lat = cp1.couch_lateral.empty? ? '' : (cp1.couch_lateral.to_f * 10).to_s
         | 
| 420 | 
            -
                            DICOM::Element.new('300A,012A', couch_lat, :parent => cp_item1)
         | 
| 421 | 
            -
                            # Isocenter Position (x\y\z):
         | 
| 422 | 
            -
                            if p.site_setup
         | 
| 423 | 
            -
                              DICOM::Element.new('300A,012C', "#{(p.site_setup.iso_pos_x.to_f * 10).round(2)}\\#{(p.site_setup.iso_pos_y.to_f * 10).round(2)}\\#{(p.site_setup.iso_pos_z.to_f * 10).round(2)}", :parent => cp_item1)
         | 
| 424 | 
            -
                            else
         | 
| 425 | 
            -
                              logger.warn("No Site Setup record exists for this plan. Unable to provide an isosenter position.")
         | 
| 426 | 
            -
                              DICOM::Element.new('300A,012C', '', :parent => cp_item1)
         | 
| 427 | 
            -
                            end
         | 
| 428 | 
            -
                            # Source to Surface Distance:
         | 
| 429 | 
            -
                            DICOM::Element.new('300A,0130', "#{cp1.ssd.to_f * 10}", :parent => cp_item1)
         | 
| 430 | 
            -
                            # Cumulative Meterset Weight:
         | 
| 431 | 
            -
                            mu_weight = (cp1.monitor_units.to_f * field.field_monitor_units.to_f).round(4)
         | 
| 432 | 
            -
                            DICOM::Element.new('300A,0134', "#{mu_weight}", :parent => cp_item1)
         | 
| 433 | 
            -
                            # Beam Limiting Device Position Sequence:
         | 
| 434 | 
            -
                            dp_seq = DICOM::Sequence.new('300A,011A', :parent => cp_item1)
         | 
| 435 | 
            -
                            # Always create one ASYMY item:
         | 
| 436 | 
            -
                            dp_item_y = DICOM::Item.new(:parent => dp_seq)
         | 
| 437 | 
            -
                            # RT Beam Limiting Device Type:
         | 
| 438 | 
            -
                            DICOM::Element.new('300A,00B8', "ASYMY", :parent => dp_item_y)
         | 
| 439 | 
            -
                            # Leaf/Jaw Positions:
         | 
| 440 | 
            -
                            DICOM::Element.new('300A,011C', "#{field.collimator_y1.to_f * 10}\\#{field.collimator_y2.to_f * 10}", :parent => dp_item_y)
         | 
| 441 | 
            -
                            # The ASYMX item ('backup jaws') only exsists on some models:
         | 
| 442 | 
            -
                            if ['SYM', 'ASY'].include?(field.field_x_mode.upcase)
         | 
| 443 | 
            -
                              dp_item_x = DICOM::Item.new(:parent => dp_seq)
         | 
| 444 | 
            -
                              DICOM::Element.new('300A,00B8', "ASYMX", :parent => dp_item_x)
         | 
| 445 | 
            -
                              DICOM::Element.new('300A,011C', "#{field.collimator_x1.to_f * 10}\\#{field.collimator_x2.to_f * 10}", :parent => dp_item_x)
         | 
| 446 | 
            -
                            end
         | 
| 447 | 
            -
                            # MLCX:
         | 
| 448 | 
            -
                            dp_item_mlcx = DICOM::Item.new(:parent => dp_seq)
         | 
| 449 | 
            -
                            # RT Beam Limiting Device Type:
         | 
| 450 | 
            -
                            DICOM::Element.new('300A,00B8', "MLCX", :parent => dp_item_mlcx)
         | 
| 451 | 
            -
                            # Leaf/Jaw Positions:
         | 
| 452 | 
            -
                            pos_a = cp1.mlc_lp_a.collect{|p| (p.to_f * 10).round(2) unless p.empty?}.compact
         | 
| 453 | 
            -
                            pos_b = cp1.mlc_lp_b.collect{|p| (p.to_f * 10).round(2) unless p.empty?}.compact
         | 
| 454 | 
            -
                            leaf_pos = "#{pos_a.join("\\")}\\#{pos_b.join("\\")}"
         | 
| 455 | 
            -
                            DICOM::Element.new('300A,011C', leaf_pos, :parent => dp_item_mlcx)
         | 
| 456 | 
            -
                            # Referenced Dose Reference Sequence:
         | 
| 457 | 
            -
                            rd_seq = DICOM::Sequence.new('300C,0050', :parent => cp_item1)
         | 
| 458 | 
            -
                            rd_item = DICOM::Item.new(:parent => rd_seq)
         | 
| 459 | 
            -
                            # Cumulative Dose Reference Coeffecient:
         | 
| 460 | 
            -
                            DICOM::Element.new('300A,010C', '', :parent => rd_item)
         | 
| 461 | 
            -
                            # Referenced Dose Reference Number:
         | 
| 462 | 
            -
                            DICOM::Element.new('300C,0051', '1', :parent => rd_item)
         | 
| 463 | 
            -
                            # Second control point:
         | 
| 464 | 
            -
                            # Always include index and cumulative weight:
         | 
| 465 | 
            -
                            DICOM::Element.new('300A,0112', "#{cp2.index}", :parent => cp_item2)
         | 
| 466 | 
            -
                            mu_weight = (cp2.monitor_units.to_f * field.field_monitor_units.to_f).round(4)
         | 
| 467 | 
            -
                            DICOM::Element.new('300A,0134', "#{mu_weight}", :parent => cp_item2)
         | 
| 468 | 
            -
                            # The other parameters are only included if they have changed from the previous control point:
         | 
| 469 | 
            -
                            # Nominal Beam Energy:
         | 
| 470 | 
            -
                            DICOM::Element.new('300A,0114', "#{cp2.energy.to_f}", :parent => cp_item2) if cp2.energy != cp1.energy
         | 
| 471 | 
            -
                            # Dose Rate Set:
         | 
| 472 | 
            -
                            DICOM::Element.new('300A,0115', cp1.doserate, :parent => cp_item2) if cp2.doserate != cp1.doserate
         | 
| 473 | 
            -
                            # Gantry Angle:
         | 
| 474 | 
            -
                            DICOM::Element.new('300A,011E', cp2.gantry_angle, :parent => cp_item2) if cp2.gantry_angle != cp1.gantry_angle
         | 
| 475 | 
            -
                            # Gantry Rotation Direction:
         | 
| 476 | 
            -
                            DICOM::Element.new('300A,011F', (cp2.gantry_dir.empty? ? 'NONE' : cp2.gantry_dir), :parent => cp_item2) if cp2.gantry_dir != cp1.gantry_dir
         | 
| 477 | 
            -
                            # Beam Limiting Device Angle:
         | 
| 478 | 
            -
                            DICOM::Element.new('300A,0120', cp2.collimator_angle, :parent => cp_item2) if cp2.collimator_angle != cp1.collimator_angle
         | 
| 479 | 
            -
                            # Beam Limiting Device Rotation Direction:
         | 
| 480 | 
            -
                            DICOM::Element.new('300A,0121', (cp2.collimator_dir.empty? ? 'NONE' : cp2.collimator_dir), :parent => cp_item2) if cp2.collimator_dir != cp1.collimator_dir
         | 
| 481 | 
            -
                            # Patient Support Angle:
         | 
| 482 | 
            -
                            DICOM::Element.new('300A,0122', cp2.couch_pedestal, :parent => cp_item2) if cp2.couch_pedestal != cp1.couch_pedestal
         | 
| 483 | 
            -
                            # Patient Support Rotation Direction:
         | 
| 484 | 
            -
                            DICOM::Element.new('300A,0123', (cp2.couch_ped_dir.empty? ? 'NONE' : cp2.couch_ped_dir), :parent => cp_item2) if cp2.couch_ped_dir != cp1.couch_ped_dir
         | 
| 485 | 
            -
                            # Table Top Eccentric Angle:
         | 
| 486 | 
            -
                            DICOM::Element.new('300A,0125', cp2.couch_angle, :parent => cp_item2) if cp2.couch_angle != cp1.couch_angle
         | 
| 487 | 
            -
                            # Table Top Eccentric Rotation Direction:
         | 
| 488 | 
            -
                            DICOM::Element.new('300A,0126', (cp2.couch_dir.empty? ? 'NONE' : cp2.couch_dir), :parent => cp_item2) if cp2.couch_dir != cp1.couch_dir
         | 
| 489 | 
            -
                            # Table Top Vertical Position:
         | 
| 490 | 
            -
                            couch_vert = cp2.couch_vertical.empty? ? '' : (cp2.couch_vertical.to_f * 10).to_s
         | 
| 491 | 
            -
                            DICOM::Element.new('300A,0128', couch_vert, :parent => cp_item2) if cp2.couch_vertical != cp1.couch_vertical
         | 
| 492 | 
            -
                            # Table Top Longitudinal Position:
         | 
| 493 | 
            -
                            couch_long = cp2.couch_longitudinal.empty? ? '' : (cp2.couch_longitudinal.to_f * 10).to_s
         | 
| 494 | 
            -
                            DICOM::Element.new('300A,0129', couch_long, :parent => cp_item2) if cp2.couch_longitudinal != cp1.couch_longitudinal
         | 
| 495 | 
            -
                            # Table Top Lateral Position:
         | 
| 496 | 
            -
                            couch_lat = cp2.couch_lateral.empty? ? '' : (cp2.couch_lateral.to_f * 10).to_s
         | 
| 497 | 
            -
                            DICOM::Element.new('300A,012A', couch_lat, :parent => cp_item2) if cp2.couch_lateral != cp1.couch_lateral
         | 
| 498 | 
            -
                            # Source to Surface Distance:
         | 
| 499 | 
            -
                            DICOM::Element.new('300A,0130', "#{cp2.ssd.to_f * 10}", :parent => cp_item2) if cp2.ssd != cp1.ssd
         | 
| 500 | 
            -
                            # Beam Limiting Device Position Sequence:
         | 
| 501 | 
            -
                            dp_seq = DICOM::Sequence.new('300A,011A', :parent => cp_item2)
         | 
| 502 | 
            -
                            # ASYMX:
         | 
| 503 | 
            -
                            if cp2.collimator_x1 != cp1.collimator_x1
         | 
| 504 | 
            -
                              dp_item_x = DICOM::Item.new(:parent => dp_seq)
         | 
| 505 | 
            -
                              DICOM::Element.new('300A,00B8', "ASYMX", :parent => dp_item_x)
         | 
| 506 | 
            -
                              DICOM::Element.new('300A,011C', "#{field.collimator_x1.to_f * 10}\\#{field.collimator_x2.to_f * 10}", :parent => dp_item_x)
         | 
| 507 | 
            -
                            end
         | 
| 508 | 
            -
                            # ASYMY:
         | 
| 509 | 
            -
                            if cp2.collimator_y1 != cp1.collimator_y1
         | 
| 510 | 
            -
                              dp_item_y = DICOM::Item.new(:parent => dp_seq)
         | 
| 511 | 
            -
                              DICOM::Element.new('300A,00B8', "ASYMY", :parent => dp_item_y)
         | 
| 512 | 
            -
                              DICOM::Element.new('300A,011C', "#{field.collimator_y1.to_f * 10}\\#{field.collimator_y2.to_f * 10}", :parent => dp_item_y)
         | 
| 513 | 
            -
                            end
         | 
| 514 | 
            -
                            # MLCX:
         | 
| 515 | 
            -
                            if cp2.mlc_lp_a != cp1.mlc_lp_a or cp2.mlc_lp_b != cp1.mlc_lp_b
         | 
| 516 | 
            -
                              dp_item_mlcx = DICOM::Item.new(:parent => dp_seq)
         | 
| 517 | 
            -
                              # RT Beam Limiting Device Type:
         | 
| 518 | 
            -
                              DICOM::Element.new('300A,00B8', "MLCX", :parent => dp_item_mlcx)
         | 
| 519 | 
            -
                              # Leaf/Jaw Positions:
         | 
| 520 | 
            -
                              pos_a = cp2.mlc_lp_a.collect{|p| (p.to_f * 10).round(2) unless p.empty?}.compact
         | 
| 521 | 
            -
                              pos_b = cp2.mlc_lp_b.collect{|p| (p.to_f * 10).round(2) unless p.empty?}.compact
         | 
| 522 | 
            -
                              leaf_pos = "#{pos_a.join("\\")}\\#{pos_b.join("\\")}"
         | 
| 523 | 
            -
                              DICOM::Element.new('300A,011C', leaf_pos, :parent => dp_item_mlcx)
         | 
| 524 | 
            -
                            end
         | 
| 525 | 
            -
                          end
         | 
| 336 | 
            +
                          # When we have multiple (2 or more) control points, iterate each control point:
         | 
| 337 | 
            +
                          field.control_points.each { |cp| create_control_point(cp, cp_seq, options) }
         | 
| 338 | 
            +
                          # Make sure that hte cumulative meterset weight of the last control
         | 
| 339 | 
            +
                          # point is '1' (exactly equal to final cumulative meterset weight):
         | 
| 340 | 
            +
                          cp_seq.items.last['300A,0134'].value = '1'
         | 
| 526 341 | 
             
                        end
         | 
| 527 342 | 
             
                        # Number of Control Points:
         | 
| 528 343 | 
             
                        DICOM::Element.new('300A,0110', b_item['300A,0111'].items.length, :parent => b_item)
         | 
| @@ -536,6 +351,345 @@ module RTP | |
| 536 351 | 
             
                  return dcm
         | 
| 537 352 | 
             
                end
         | 
| 538 353 |  | 
| 354 | 
            +
             | 
| 355 | 
            +
                private
         | 
| 356 | 
            +
             | 
| 357 | 
            +
             | 
| 358 | 
            +
                # Adds Collimator Angle elements to a Control Point Item.
         | 
| 359 | 
            +
                # Note that the element is only added if there is no 'current' attribute
         | 
| 360 | 
            +
                # defined, or the given value is different form the current attribute.
         | 
| 361 | 
            +
                #
         | 
| 362 | 
            +
                # @param [String, NilClass] value1 the collimator angle attribute
         | 
| 363 | 
            +
                # @param [String, NilClass] value2 the collimator rotation direction attribute
         | 
| 364 | 
            +
                # @param [DICOM::Item] item the DICOM control point item in which to create an element
         | 
| 365 | 
            +
                #
         | 
| 366 | 
            +
                def add_collimator(value1, value2, item)
         | 
| 367 | 
            +
                  if !@current_collimator || value1 != @current_collimator
         | 
| 368 | 
            +
                    @current_collimator = value1
         | 
| 369 | 
            +
                    DICOM::Element.new('300A,0120', value1, :parent => item)
         | 
| 370 | 
            +
                    DICOM::Element.new('300A,0121', (value2.empty? ? 'NONE' : value2), :parent => item)
         | 
| 371 | 
            +
                  end
         | 
| 372 | 
            +
                end
         | 
| 373 | 
            +
             | 
| 374 | 
            +
                # Adds Table Top Eccentric Angle elements to a Control Point Item.
         | 
| 375 | 
            +
                # Note that the element is only added if there is no 'current' attribute
         | 
| 376 | 
            +
                # defined, or the given value is different form the current attribute.
         | 
| 377 | 
            +
                #
         | 
| 378 | 
            +
                # @param [String, NilClass] value1 the table top eccentric angle attribute
         | 
| 379 | 
            +
                # @param [String, NilClass] value2 the table top eccentric rotation direction attribute
         | 
| 380 | 
            +
                # @param [DICOM::Item] item the DICOM control point item in which to create an element
         | 
| 381 | 
            +
                #
         | 
| 382 | 
            +
                def add_couch_angle(value1, value2, item)
         | 
| 383 | 
            +
                  if !@current_couch_angle || value1 != @current_couch_angle
         | 
| 384 | 
            +
                    @current_couch_angle = value1
         | 
| 385 | 
            +
                    DICOM::Element.new('300A,0125', value1, :parent => item)
         | 
| 386 | 
            +
                    DICOM::Element.new('300A,0126', (value2.empty? ? 'NONE' : value2), :parent => item)
         | 
| 387 | 
            +
                  end
         | 
| 388 | 
            +
                end
         | 
| 389 | 
            +
             | 
| 390 | 
            +
                # Adds a Table Top Lateral Position element to a Control Point Item.
         | 
| 391 | 
            +
                # Note that the element is only added if there is no 'current' attribute
         | 
| 392 | 
            +
                # defined, or the given value is different form the current attribute.
         | 
| 393 | 
            +
                #
         | 
| 394 | 
            +
                # @param [String, NilClass] value the couch lateral attribute
         | 
| 395 | 
            +
                # @param [DICOM::Item] item the DICOM control point item in which to create an element
         | 
| 396 | 
            +
                #
         | 
| 397 | 
            +
                def add_couch_lateral(value, item)
         | 
| 398 | 
            +
                  if !@current_couch_lateral || value != @current_couch_lateral
         | 
| 399 | 
            +
                    @current_couch_lateral = value
         | 
| 400 | 
            +
                    DICOM::Element.new('300A,012A', (value.empty? ? '' : value.to_f * 10), :parent => item)
         | 
| 401 | 
            +
                  end
         | 
| 402 | 
            +
                end
         | 
| 403 | 
            +
             | 
| 404 | 
            +
                # Adds a Table Top Longitudinal Position element to a Control Point Item.
         | 
| 405 | 
            +
                # Note that the element is only added if there is no 'current' attribute
         | 
| 406 | 
            +
                # defined, or the given value is different form the current attribute.
         | 
| 407 | 
            +
                #
         | 
| 408 | 
            +
                # @param [String, NilClass] value the couch longitudinal attribute
         | 
| 409 | 
            +
                # @param [DICOM::Item] item the DICOM control point item in which to create an element
         | 
| 410 | 
            +
                #
         | 
| 411 | 
            +
                def add_couch_longitudinal(value, item)
         | 
| 412 | 
            +
                  if !@current_couch_longitudinal || value != @current_couch_longitudinal
         | 
| 413 | 
            +
                    @current_couch_longitudinal = value
         | 
| 414 | 
            +
                    DICOM::Element.new('300A,0129', (value.empty? ? '' : value.to_f * 10), :parent => item)
         | 
| 415 | 
            +
                  end
         | 
| 416 | 
            +
                end
         | 
| 417 | 
            +
             | 
| 418 | 
            +
                # Adds Patient Support Angle elements to a Control Point Item.
         | 
| 419 | 
            +
                # Note that the element is only added if there is no 'current' attribute
         | 
| 420 | 
            +
                # defined, or the given value is different form the current attribute.
         | 
| 421 | 
            +
                #
         | 
| 422 | 
            +
                # @param [String, NilClass] value1 the patient support angle attribute
         | 
| 423 | 
            +
                # @param [String, NilClass] value2 the patient support rotation direction attribute
         | 
| 424 | 
            +
                # @param [DICOM::Item] item the DICOM control point item in which to create an element
         | 
| 425 | 
            +
                #
         | 
| 426 | 
            +
                def add_couch_pedestal(value1, value2, item)
         | 
| 427 | 
            +
                  if !@current_couch_pedestal || value1 != @current_couch_pedestal
         | 
| 428 | 
            +
                    @current_couch_pedestal = value1
         | 
| 429 | 
            +
                    DICOM::Element.new('300A,0122', value1, :parent => item)
         | 
| 430 | 
            +
                    DICOM::Element.new('300A,0123', (value2.empty? ? 'NONE' : value2), :parent => item)
         | 
| 431 | 
            +
                  end
         | 
| 432 | 
            +
                end
         | 
| 433 | 
            +
             | 
| 434 | 
            +
                # Adds a Table Top Vertical Position element to a Control Point Item.
         | 
| 435 | 
            +
                # Note that the element is only added if there is no 'current' attribute
         | 
| 436 | 
            +
                # defined, or the given value is different form the current attribute.
         | 
| 437 | 
            +
                #
         | 
| 438 | 
            +
                # @param [String, NilClass] value the couch vertical attribute
         | 
| 439 | 
            +
                # @param [DICOM::Item] item the DICOM control point item in which to create an element
         | 
| 440 | 
            +
                #
         | 
| 441 | 
            +
                def add_couch_vertical(value, item)
         | 
| 442 | 
            +
                  if !@current_couch_vertical || value != @current_couch_vertical
         | 
| 443 | 
            +
                    @current_couch_vertical = value
         | 
| 444 | 
            +
                    DICOM::Element.new('300A,0128', (value.empty? ? '' : value.to_f * 10), :parent => item)
         | 
| 445 | 
            +
                  end
         | 
| 446 | 
            +
                end
         | 
| 447 | 
            +
             | 
| 448 | 
            +
                # Adds a Dose Rate Set element to a Control Point Item.
         | 
| 449 | 
            +
                # Note that the element is only added if there is no 'current' attribute
         | 
| 450 | 
            +
                # defined, or the given value is different form the current attribute.
         | 
| 451 | 
            +
                #
         | 
| 452 | 
            +
                # @param [String, NilClass] value the doserate attribute
         | 
| 453 | 
            +
                # @param [DICOM::Item] item the DICOM control point item in which to create an element
         | 
| 454 | 
            +
                #
         | 
| 455 | 
            +
                def add_doserate(value, item)
         | 
| 456 | 
            +
                  if !@current_doserate || value != @current_doserate
         | 
| 457 | 
            +
                    @current_doserate = value
         | 
| 458 | 
            +
                    DICOM::Element.new('300A,0115', value, :parent => item)
         | 
| 459 | 
            +
                  end
         | 
| 460 | 
            +
                end
         | 
| 461 | 
            +
             | 
| 462 | 
            +
                # Adds a Nominal Beam Energy element to a Control Point Item.
         | 
| 463 | 
            +
                # Note that the element is only added if there is no 'current' attribute
         | 
| 464 | 
            +
                # defined, or the given value is different form the current attribute.
         | 
| 465 | 
            +
                #
         | 
| 466 | 
            +
                # @param [String, NilClass] value the energy attribute
         | 
| 467 | 
            +
                # @param [DICOM::Item] item the DICOM control point item in which to create an element
         | 
| 468 | 
            +
                #
         | 
| 469 | 
            +
                def add_energy(value, item)
         | 
| 470 | 
            +
                  if !@current_energy || value != @current_energy
         | 
| 471 | 
            +
                    @current_energy = value
         | 
| 472 | 
            +
                    DICOM::Element.new('300A,0114', "#{value.to_f}", :parent => item)
         | 
| 473 | 
            +
                  end
         | 
| 474 | 
            +
                end
         | 
| 475 | 
            +
             | 
| 476 | 
            +
                # Adds Gantry Angle elements to a Control Point Item.
         | 
| 477 | 
            +
                # Note that the element is only added if there is no 'current' attribute
         | 
| 478 | 
            +
                # defined, or the given value is different form the current attribute.
         | 
| 479 | 
            +
                #
         | 
| 480 | 
            +
                # @param [String, NilClass] value1 the gantry angle attribute
         | 
| 481 | 
            +
                # @param [String, NilClass] value2 the gantry rotation direction attribute
         | 
| 482 | 
            +
                # @param [DICOM::Item] item the DICOM control point item in which to create an element
         | 
| 483 | 
            +
                #
         | 
| 484 | 
            +
                def add_gantry(value1, value2, item)
         | 
| 485 | 
            +
                  if !@current_gantry || value1 != @current_gantry
         | 
| 486 | 
            +
                    @current_gantry = value1
         | 
| 487 | 
            +
                    DICOM::Element.new('300A,011E', value1, :parent => item)
         | 
| 488 | 
            +
                    DICOM::Element.new('300A,011F', (value2.empty? ? 'NONE' : value2), :parent => item)
         | 
| 489 | 
            +
                  end
         | 
| 490 | 
            +
                end
         | 
| 491 | 
            +
             | 
| 492 | 
            +
                # Adds an Isosenter element to a Control Point Item.
         | 
| 493 | 
            +
                # Note that the element is only added if there is a Site Setup record present,
         | 
| 494 | 
            +
                # and it contains a real (non-empty) value. Also, the element is only added if there
         | 
| 495 | 
            +
                # is no 'current' attribute defined, or the given value is different form the current attribute.
         | 
| 496 | 
            +
                #
         | 
| 497 | 
            +
                # @param [SiteSetup, NilClass] site_setup the associated site setup record
         | 
| 498 | 
            +
                # @param [DICOM::Item] item the DICOM control point item in which to create an element
         | 
| 499 | 
            +
                #
         | 
| 500 | 
            +
                def add_isosenter(site_setup, item)
         | 
| 501 | 
            +
                  if site_setup
         | 
| 502 | 
            +
                    # Create an element if the value is new or unique:
         | 
| 503 | 
            +
                    if !@current_isosenter
         | 
| 504 | 
            +
                      iso = "#{(site_setup.iso_pos_x.to_f * 10).round(2)}\\#{(site_setup.iso_pos_y.to_f * 10).round(2)}\\#{(site_setup.iso_pos_z.to_f * 10).round(2)}"
         | 
| 505 | 
            +
                      if iso != @current_isosenter
         | 
| 506 | 
            +
                        @current_isosenter = iso
         | 
| 507 | 
            +
                        DICOM::Element.new('300A,012C', iso, :parent => item)
         | 
| 508 | 
            +
                      end
         | 
| 509 | 
            +
                    end
         | 
| 510 | 
            +
                  else
         | 
| 511 | 
            +
                    # Log a warning if this is the first control point:
         | 
| 512 | 
            +
                    unless @current_isosenter
         | 
| 513 | 
            +
                      logger.warn("No Site Setup record exists for this plan. Unable to provide an isosenter position.")
         | 
| 514 | 
            +
                    end
         | 
| 515 | 
            +
                  end
         | 
| 516 | 
            +
                end
         | 
| 517 | 
            +
             | 
| 518 | 
            +
                # Adds a Source to Surface Distance element to a Control Point Item.
         | 
| 519 | 
            +
                # Note that the element is only added if the SSD attribute contains
         | 
| 520 | 
            +
                # real (non-empty) value.
         | 
| 521 | 
            +
                #
         | 
| 522 | 
            +
                # @param [String, NilClass] value the SSD attribute
         | 
| 523 | 
            +
                # @param [DICOM::Item] item the DICOM control point item in which to create an element
         | 
| 524 | 
            +
                #
         | 
| 525 | 
            +
                def add_ssd(value, item)
         | 
| 526 | 
            +
                  DICOM::Element.new('300A,0130', "#{value.to_f * 10}", :parent => item) if value && !value.empty?
         | 
| 527 | 
            +
                end
         | 
| 528 | 
            +
             | 
| 529 | 
            +
                # Creates a control point item in the given control point sequence, based
         | 
| 530 | 
            +
                # on an RTP control point record.
         | 
| 531 | 
            +
                #
         | 
| 532 | 
            +
                # @param [ControlPoint] cp the RTP ControlPoint record to convert
         | 
| 533 | 
            +
                # @param [DICOM::Sequence] sequence the DICOM parent sequence of the item to be created
         | 
| 534 | 
            +
                # @param [Hash] options the options to use for creating the control point
         | 
| 535 | 
            +
                # @option options [Boolean] :dose_ref if set, a Referenced Dose Reference sequence will be included in the generated control point item
         | 
| 536 | 
            +
                # @return [DICOM::Item] the constructed control point DICOM item
         | 
| 537 | 
            +
                #
         | 
| 538 | 
            +
                def create_control_point(cp, sequence, options={})
         | 
| 539 | 
            +
                  cp_item = DICOM::Item.new(:parent => sequence)
         | 
| 540 | 
            +
                  # Some CP attributes will always be written (CP index, BLD positions & Cumulative meterset weight).
         | 
| 541 | 
            +
                  # The other attributes are only written if they are different from the previous control point.
         | 
| 542 | 
            +
                  # Control Point Index:
         | 
| 543 | 
            +
                  DICOM::Element.new('300A,0112', "#{cp.index}", :parent => cp_item)
         | 
| 544 | 
            +
                  # Beam Limiting Device Position Sequence:
         | 
| 545 | 
            +
                  create_beam_limiting_device_positions(cp_item, cp)
         | 
| 546 | 
            +
                  # Source to Surface Distance:
         | 
| 547 | 
            +
                  add_ssd(cp.ssd, cp_item)
         | 
| 548 | 
            +
                  # Cumulative Meterset Weight:
         | 
| 549 | 
            +
                  DICOM::Element.new('300A,0134', cp.monitor_units.to_f, :parent => cp_item)
         | 
| 550 | 
            +
                  # Referenced Dose Reference Sequence:
         | 
| 551 | 
            +
                  create_referenced_dose_reference(cp_item) if options[:dose_ref]
         | 
| 552 | 
            +
                  # Attributes that are only added if they carry an updated value:
         | 
| 553 | 
            +
                  # Nominal Beam Energy:
         | 
| 554 | 
            +
                  add_energy(cp.energy, cp_item)
         | 
| 555 | 
            +
                  # Dose Rate Set:
         | 
| 556 | 
            +
                  add_doserate(cp.doserate, cp_item)
         | 
| 557 | 
            +
                  # Gantry Angle & Rotation Direction:
         | 
| 558 | 
            +
                  add_gantry(cp.gantry_angle, cp.gantry_dir, cp_item)
         | 
| 559 | 
            +
                  # Beam Limiting Device Angle & Rotation Direction:
         | 
| 560 | 
            +
                  add_collimator(cp.collimator_angle, cp.collimator_dir, cp_item)
         | 
| 561 | 
            +
                  # Patient Support Angle & Rotation Direction:
         | 
| 562 | 
            +
                  add_couch_pedestal(cp.couch_pedestal, cp.couch_ped_dir, cp_item)
         | 
| 563 | 
            +
                  # Table Top Eccentric Angle & Rotation Direction:
         | 
| 564 | 
            +
                  add_couch_angle(cp.couch_angle, cp.couch_dir, cp_item)
         | 
| 565 | 
            +
                  # Table Top Vertical Position:
         | 
| 566 | 
            +
                  add_couch_vertical(cp.couch_vertical, cp_item)
         | 
| 567 | 
            +
                  # Table Top Longitudinal Position:
         | 
| 568 | 
            +
                  add_couch_longitudinal(cp.couch_vertical, cp_item)
         | 
| 569 | 
            +
                  # Table Top Lateral Position:
         | 
| 570 | 
            +
                  add_couch_lateral(cp.couch_vertical, cp_item)
         | 
| 571 | 
            +
                  # Isocenter Position (x\y\z):
         | 
| 572 | 
            +
                  add_isosenter(cp.parent.parent.site_setup, cp_item)
         | 
| 573 | 
            +
                  cp_item
         | 
| 574 | 
            +
                end
         | 
| 575 | 
            +
             | 
| 576 | 
            +
                # Creates a beam limiting device sequence in the given DICOM object.
         | 
| 577 | 
            +
                #
         | 
| 578 | 
            +
                # @param [DICOM::Item] beam_item the DICOM beam item in which to insert the sequence
         | 
| 579 | 
            +
                # @param [Field] field the RTP field to fetch device parameters from
         | 
| 580 | 
            +
                # @return [DICOM::Sequence] the constructed beam limiting device sequence
         | 
| 581 | 
            +
                #
         | 
| 582 | 
            +
                def create_beam_limiting_devices(beam_item, field)
         | 
| 583 | 
            +
                  bl_seq = DICOM::Sequence.new('300A,00B6', :parent => beam_item)
         | 
| 584 | 
            +
                  # The ASYMX item ('backup jaws') doesn't exist on all models:
         | 
| 585 | 
            +
                  if ['SYM', 'ASY'].include?(field.field_x_mode.upcase)
         | 
| 586 | 
            +
                    bl_item_x = DICOM::Item.new(:parent => bl_seq)
         | 
| 587 | 
            +
                    DICOM::Element.new('300A,00B8', "ASYMX", :parent => bl_item_x)
         | 
| 588 | 
            +
                    DICOM::Element.new('300A,00BC', "1", :parent => bl_item_x)
         | 
| 589 | 
            +
                  end
         | 
| 590 | 
            +
                  # The ASYMY item is always created:
         | 
| 591 | 
            +
                  bl_item_y = DICOM::Item.new(:parent => bl_seq)
         | 
| 592 | 
            +
                  # RT Beam Limiting Device Type:
         | 
| 593 | 
            +
                  DICOM::Element.new('300A,00B8', "ASYMY", :parent => bl_item_y)
         | 
| 594 | 
            +
                  # Number of Leaf/Jaw Pairs:
         | 
| 595 | 
            +
                  DICOM::Element.new('300A,00BC', "1", :parent => bl_item_y)
         | 
| 596 | 
            +
                  # MLCX item is only created if leaves are defined:
         | 
| 597 | 
            +
                  # (NB: The RTP file doesn't specify leaf position boundaries, so we
         | 
| 598 | 
            +
                  # have to set these based on a set of known MLC types, their number
         | 
| 599 | 
            +
                  # of leaves, and their leaf boundary positions.)
         | 
| 600 | 
            +
                  if field.control_points.length > 0
         | 
| 601 | 
            +
                    bl_item_mlcx = DICOM::Item.new(:parent => bl_seq)
         | 
| 602 | 
            +
                    DICOM::Element.new('300A,00B8', "MLCX", :parent => bl_item_mlcx)
         | 
| 603 | 
            +
                    num_leaves = field.control_points.first.mlc_leaves.to_i
         | 
| 604 | 
            +
                    DICOM::Element.new('300A,00BC', num_leaves.to_s, :parent => bl_item_mlcx)
         | 
| 605 | 
            +
                    DICOM::Element.new('300A,00BE', "#{RTP.leaf_boundaries(num_leaves).join("\\")}", :parent => bl_item_mlcx)
         | 
| 606 | 
            +
                  end
         | 
| 607 | 
            +
                  bl_seq
         | 
| 608 | 
            +
                end
         | 
| 609 | 
            +
             | 
| 610 | 
            +
                # Creates a beam limiting device positions sequence in the given DICOM object.
         | 
| 611 | 
            +
                #
         | 
| 612 | 
            +
                # @param [DICOM::Item] cp_item the DICOM control point item in which to insert the sequence
         | 
| 613 | 
            +
                # @param [ControlPoint] cp the RTP control point to fetch device parameters from
         | 
| 614 | 
            +
                # @return [DICOM::Sequence] the constructed beam limiting device positions sequence
         | 
| 615 | 
            +
                #
         | 
| 616 | 
            +
                def create_beam_limiting_device_positions(cp_item, cp)
         | 
| 617 | 
            +
                  dp_seq = DICOM::Sequence.new('300A,011A', :parent => cp_item)
         | 
| 618 | 
            +
                  # The ASYMX item ('backup jaws') doesn't exist on all models:
         | 
| 619 | 
            +
                  if ['SYM', 'ASY'].include?(cp.parent.field_x_mode.upcase)
         | 
| 620 | 
            +
                    dp_item_x = DICOM::Item.new(:parent => dp_seq)
         | 
| 621 | 
            +
                    DICOM::Element.new('300A,00B8', "ASYMX", :parent => dp_item_x)
         | 
| 622 | 
            +
                    DICOM::Element.new('300A,011C', "#{cp.dcm_collimator_x1}\\#{cp.dcm_collimator_x2}", :parent => dp_item_x)
         | 
| 623 | 
            +
                  end
         | 
| 624 | 
            +
                  # Always create one ASYMY item:
         | 
| 625 | 
            +
                  dp_item_y = DICOM::Item.new(:parent => dp_seq)
         | 
| 626 | 
            +
                  # RT Beam Limiting Device Type:
         | 
| 627 | 
            +
                  DICOM::Element.new('300A,00B8', "ASYMY", :parent => dp_item_y)
         | 
| 628 | 
            +
                  # Leaf/Jaw Positions:
         | 
| 629 | 
            +
                  DICOM::Element.new('300A,011C', "#{cp.dcm_collimator_y1}\\#{cp.dcm_collimator_y2}", :parent => dp_item_y)
         | 
| 630 | 
            +
                  # MLCX:
         | 
| 631 | 
            +
                  dp_item_mlcx = DICOM::Item.new(:parent => dp_seq)
         | 
| 632 | 
            +
                  # RT Beam Limiting Device Type:
         | 
| 633 | 
            +
                  DICOM::Element.new('300A,00B8', "MLCX", :parent => dp_item_mlcx)
         | 
| 634 | 
            +
                  # Leaf/Jaw Positions:
         | 
| 635 | 
            +
                  DICOM::Element.new('300A,011C', cp.dcm_mlc_positions, :parent => dp_item_mlcx)
         | 
| 636 | 
            +
                  dp_seq
         | 
| 637 | 
            +
                end
         | 
| 638 | 
            +
             | 
| 639 | 
            +
                # Creates a dose reference sequence in the given DICOM object.
         | 
| 640 | 
            +
                #
         | 
| 641 | 
            +
                # @param [DICOM::DObject] dcm the DICOM object in which to insert the sequence
         | 
| 642 | 
            +
                # @param [String] description the value to use for Dose Reference Description
         | 
| 643 | 
            +
                # @return [DICOM::Sequence] the constructed dose reference sequence
         | 
| 644 | 
            +
                #
         | 
| 645 | 
            +
                def create_dose_reference(dcm, description)
         | 
| 646 | 
            +
                  dr_seq = DICOM::Sequence.new('300A,0010', :parent => dcm)
         | 
| 647 | 
            +
                  dr_item = DICOM::Item.new(:parent => dr_seq)
         | 
| 648 | 
            +
                  # Dose Reference Number:
         | 
| 649 | 
            +
                  DICOM::Element.new('300A,0012', '1', :parent => dr_item)
         | 
| 650 | 
            +
                  # Dose Reference Structure Type:
         | 
| 651 | 
            +
                  DICOM::Element.new('300A,0014', 'SITE', :parent => dr_item)
         | 
| 652 | 
            +
                  # Dose Reference Description:
         | 
| 653 | 
            +
                  DICOM::Element.new('300A,0016', description, :parent => dr_item)
         | 
| 654 | 
            +
                  # Dose Reference Type:
         | 
| 655 | 
            +
                  DICOM::Element.new('300A,0020', 'TARGET', :parent => dr_item)
         | 
| 656 | 
            +
                  dr_seq
         | 
| 657 | 
            +
                end
         | 
| 658 | 
            +
             | 
| 659 | 
            +
                # Creates a referenced dose reference sequence in the given DICOM object.
         | 
| 660 | 
            +
                #
         | 
| 661 | 
            +
                # @param [DICOM::Item] cp_item the DICOM item in which to insert the sequence
         | 
| 662 | 
            +
                # @return [DICOM::Sequence] the constructed referenced dose reference sequence
         | 
| 663 | 
            +
                #
         | 
| 664 | 
            +
                def create_referenced_dose_reference(cp_item)
         | 
| 665 | 
            +
                  # Referenced Dose Reference Sequence:
         | 
| 666 | 
            +
                  rd_seq = DICOM::Sequence.new('300C,0050', :parent => cp_item)
         | 
| 667 | 
            +
                  rd_item = DICOM::Item.new(:parent => rd_seq)
         | 
| 668 | 
            +
                  # Cumulative Dose Reference Coeffecient:
         | 
| 669 | 
            +
                  DICOM::Element.new('300A,010C', '', :parent => rd_item)
         | 
| 670 | 
            +
                  # Referenced Dose Reference Number:
         | 
| 671 | 
            +
                  DICOM::Element.new('300C,0051', '1', :parent => rd_item)
         | 
| 672 | 
            +
                  rd_seq
         | 
| 673 | 
            +
                end
         | 
| 674 | 
            +
             | 
| 675 | 
            +
                # Resets the types of control point attributes that are only written to the
         | 
| 676 | 
            +
                # first control point item, and for following control point items only when
         | 
| 677 | 
            +
                # they are different from the 'current' value. When a new field is reached,
         | 
| 678 | 
            +
                # it is essential to reset these attributes, or else we could risk to start
         | 
| 679 | 
            +
                # the field with a control point with missing attributes, if one of its first
         | 
| 680 | 
            +
                # attributes is equal to the last attribute of the previous field.
         | 
| 681 | 
            +
                #
         | 
| 682 | 
            +
                def reset_cp_current_attributes
         | 
| 683 | 
            +
                  @current_gantry = nil
         | 
| 684 | 
            +
                  @current_collimator = nil
         | 
| 685 | 
            +
                  @current_couch_pedestal = nil
         | 
| 686 | 
            +
                  @current_couch_angle = nil
         | 
| 687 | 
            +
                  @current_couch_vertical = nil
         | 
| 688 | 
            +
                  @current_couch_longitudinal = nil
         | 
| 689 | 
            +
                  @current_couch_lateral = nil
         | 
| 690 | 
            +
                  @current_isosenter = nil
         | 
| 691 | 
            +
                end
         | 
| 692 | 
            +
             | 
| 539 693 | 
             
              end
         | 
| 540 694 |  | 
| 541 695 | 
             
            end
         | 
    
        data/lib/rtp-connect/version.rb
    CHANGED
    
    
    
        metadata
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: rtp-connect
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: '1. | 
| 4 | 
            +
              version: '1.6'
         | 
| 5 5 | 
             
              prerelease: 
         | 
| 6 6 | 
             
            platform: ruby
         | 
| 7 7 | 
             
            authors:
         | 
| @@ -9,7 +9,7 @@ authors: | |
| 9 9 | 
             
            autorequire: 
         | 
| 10 10 | 
             
            bindir: bin
         | 
| 11 11 | 
             
            cert_chain: []
         | 
| 12 | 
            -
            date: 2013- | 
| 12 | 
            +
            date: 2013-12-12 00:00:00.000000000 Z
         | 
| 13 13 | 
             
            dependencies:
         | 
| 14 14 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 15 15 | 
             
              name: bundler
         | 
| @@ -159,7 +159,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement | |
| 159 159 | 
             
                  version: '0'
         | 
| 160 160 | 
             
                  segments:
         | 
| 161 161 | 
             
                  - 0
         | 
| 162 | 
            -
                  hash:  | 
| 162 | 
            +
                  hash: -111446803
         | 
| 163 163 | 
             
            requirements: []
         | 
| 164 164 | 
             
            rubyforge_project: rtp-connect
         | 
| 165 165 | 
             
            rubygems_version: 1.8.24
         |