rtp-connect 1.4 → 1.5

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 CHANGED
@@ -1,9 +1,35 @@
1
+ = 1.5
2
+
3
+ === 24th October, 2013
4
+
5
+ * Added support for the Simulation Field record.
6
+ * Bumped required Ruby version to 1.9.3.
7
+ * More robust CSV implementation:
8
+ * Properly handle attributes containing a double-quote character.
9
+ * Improved handling of invalid CSV RTP files.
10
+ * Ensure that we don't produce invalid CSV with records containing attributes with the double-quote character.
11
+ * Plan#to_dcm improvements:
12
+ * Exclude CT & 2DkV fields on export.
13
+ * Properly handle the case of a missing Site Setup in the RTP file.
14
+ * Improve handling of logging in the DICOM module.
15
+ * Added logic for deciding whether the plan's machine actually has an X jaw.
16
+ * Ensure that a correct number of fields and control points are specified.
17
+ * Added support for more MLC types:
18
+ * Siemens 58 & 82 leaf
19
+ * Varian 120 leaf
20
+ * Added options for specifying undeterminable machine information such as:
21
+ * Manufacturer
22
+ * Manufacturer's Model Name
23
+ * Device Serial Number
24
+
25
+
1
26
  = 1.4
2
27
 
3
28
  === 10th April, 2013
4
29
 
5
30
  * Support an extended ascii character set (ISO8859-1 encoding) for record values and file read/write.
6
31
 
32
+
7
33
  = 1.3
8
34
 
9
35
  === 12th October, 2012
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rtp-connect (1.4)
4
+ rtp-connect (1.5)
5
5
 
6
6
  GEM
7
7
  remote: http://www.rubygems.org/
@@ -26,10 +26,10 @@ PLATFORMS
26
26
  x86-mingw32
27
27
 
28
28
  DEPENDENCIES
29
- bundler (~> 1.2)
29
+ bundler (~> 1.3)
30
30
  dicom (~> 0.9.5)
31
- mocha (~> 0.12)
32
- rake (~> 0.9.2)
33
- rspec (~> 2.11)
31
+ mocha (~> 0.13)
32
+ rake (~> 0.9.6)
33
+ rspec (~> 2.13)
34
34
  rtp-connect!
35
- yard (~> 0.8.2)
35
+ yard (~> 0.8.5)
data/README.rdoc CHANGED
@@ -13,7 +13,7 @@ external dependencies.
13
13
 
14
14
  == REQUIREMENTS
15
15
 
16
- * Ruby 1.9.2 (or higher)
16
+ * Ruby 1.9.3 (or higher)
17
17
 
18
18
 
19
19
  == BASIC USAGE
@@ -92,6 +92,7 @@ Example:
92
92
  * Plan definition [PLAN_DEF]
93
93
  * Prescription site [RX_DEF]
94
94
  * Site setup [SITE_SETUP_DEF]
95
+ * Simulation field [SIM_DEF]
95
96
  * Treatment field [FIELD_DEF]
96
97
  * Extended treatment field [EXTENDED_FIELD_DEF]
97
98
  * Control point record [CONTROL_PT_DEF]
@@ -99,7 +100,6 @@ Example:
99
100
 
100
101
  === Unsupported records
101
102
 
102
- * Simulation field [SIM_DEF]
103
103
  * Extended plan definition [EXTENDED_PLAN_DEF]
104
104
  * Document based treatment field [PDF_FIELD_DEF]
105
105
  * Multileaf collimator [MLC_DEF]
data/lib/rtp-connect.rb CHANGED
@@ -9,6 +9,7 @@ require_relative 'rtp-connect/plan'
9
9
  require_relative 'rtp-connect/plan_to_dcm'
10
10
  require_relative 'rtp-connect/prescription'
11
11
  require_relative 'rtp-connect/site_setup'
12
+ require_relative 'rtp-connect/simulation_field'
12
13
  require_relative 'rtp-connect/field'
13
14
  require_relative 'rtp-connect/extended_field'
14
15
  require_relative 'rtp-connect/control_point'
@@ -6,6 +6,39 @@ module RTP
6
6
  # Module methods:
7
7
  #++
8
8
 
9
+ # Gives an array of MLC leaf position boundaries for a given type of MLC,
10
+ # specified by its number of leaves at one side.
11
+ #
12
+ # @param [Fixnum] nr_leaves the number of leaves (in one leaf bank)
13
+ # @return [Array<Fixnum>] the leaf boundary positions
14
+ # @raise [ArgumentError] if an unsupported MLC (nr of leaves) is given
15
+ #
16
+ def leaf_boundaries(nr_leaves)
17
+ case nr_leaves
18
+ when 29
19
+ [-200, -135, -125, -115, -105, -95, -85, -75, -65, -55, -45, -35, -25,
20
+ -15, -5, 5, 15, 25, 35, 45, 55, 65, 75, 85, 95, 105, 115, 125, 135, 200
21
+ ]
22
+ when 40
23
+ Array.new(nr_leaves) {|i| (i * 400 / nr_leaves.to_f - 200).to_i}
24
+ when 41
25
+ [-200, -195, -185, -175, -165, -155, -145, -135, -125, -115,
26
+ -105, -95, -85, -75, -65, -55, -45, -35, -25, -15, -5, 5, 15, 25, 35, 45,
27
+ 55, 65, 75, 85, 95, 105, 115, 125, 135, 145, 155, 165, 175, 185, 195, 200
28
+ ]
29
+ when 60
30
+ [-200, -190, -180, -170, -160, -150, -140, -130, -120, -110,
31
+ -100, -95, -90, -85, -80, -75, -70, -65, -60, -55, -50, -45, -40, -35, -30,
32
+ -25, -20, -15, -10, -5, 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65,
33
+ 70, 75, 80, 85, 90, 95, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200
34
+ ]
35
+ when 80
36
+ Array.new(nr_leaves) {|i| (i * 400 / nr_leaves.to_f - 200).to_i}
37
+ else
38
+ raise ArgumentError, "Unsupported number of leaves: #{nr_leaves}"
39
+ end
40
+ end
41
+
9
42
  # Computes the CRC checksum of the given line and verifies that
10
43
  # this value corresponds with the checksum given at the end of the line.
11
44
  #
@@ -623,6 +623,15 @@ module RTP
623
623
  @current_parent = f
624
624
  end
625
625
 
626
+ # Creates a simulation field record from the given string.
627
+ #
628
+ # @param [String] string a string line containing a simulation field definition
629
+ #
630
+ def simulation_field(string)
631
+ sf = SimulationField.load(string, @current_parent)
632
+ @current_parent = sf
633
+ end
634
+
626
635
  end
627
636
 
628
637
  end
@@ -9,14 +9,19 @@ module RTP
9
9
  # Electron beams or dynamic photon 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
+ # @param [Hash] options the options to use for creating the DICOM object
13
+ # @option options [String] :manufacturer the value used for the manufacturer tag (0008,0070) in the beam sequence
14
+ # @option options [String] :model the value used for the manufacturer's model name tag (0008,1090) in the beam sequence
15
+ # @option options [String] :serial_number the value used for the device serial number tag (0018,1000) in the beam sequence
12
16
  # @return [DICOM::DObject] the converted DICOM object
13
17
  #
14
- def to_dcm
18
+ def to_dcm(options={})
15
19
  #
16
20
  # FIXME: This method is rather big, with a few sections of somewhat similar, repeating code.
17
21
  # Refactoring and simplifying it at some stage might be a good idea.
18
22
  #
19
23
  require 'dicom'
24
+ original_level = DICOM.logger.level
20
25
  DICOM.logger.level = Logger::FATAL
21
26
  p = @prescriptions.first
22
27
  # If no prescription is present, we are not going to be able to make a valid DICOM object:
@@ -162,11 +167,8 @@ module RTP
162
167
  num_frac = '0'
163
168
  end
164
169
  DICOM::Element.new('300A,0078', num_frac, :parent => fg_item)
165
- # Number of Beams:
166
- num_beams = p ? p.fields.length : 0
167
- DICOM::Element.new('300A,0080', "#{num_beams}", :parent => fg_item)
168
170
  # Number of Brachy Application Setups:
169
- DICOM::Element.new('300A,00A0', "0", :parent => fg_item)
171
+ DICOM::Element.new('300A,00A0', '0', :parent => fg_item)
170
172
  # Referenced Beam Sequence (items created for each beam below):
171
173
  rb_seq = DICOM::Sequence.new('300C,0004', :parent => fg_item)
172
174
  #
@@ -177,330 +179,360 @@ module RTP
177
179
  # If no fields are present, we are not going to be able to make a valid DICOM object:
178
180
  logger.error("No Field Record present. Unable to build a valid RTPLAN DICOM object.") unless p.fields.length > 0
179
181
  p.fields.each_with_index do |field, i|
180
- # If this is an electron beam, a warning should be printed, as these are less reliably converted:
181
- 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'
182
- # Beam number and name:
183
- beam_number = field.extended_field ? field.extended_field.original_beam_number : (i + 1).to_s
184
- beam_name = field.extended_field ? field.extended_field.original_beam_name : field.field_name
185
- # Ref Beam Item:
186
- rb_item = DICOM::Item.new(:parent => rb_seq)
187
- # Beam Dose (convert from cGy to Gy):
188
- field_dose = field.field_dose.empty? ? '' : (field.field_dose.to_f * 0.01).round(4).to_s
189
- DICOM::Element.new('300A,0084', field_dose, :parent => rb_item)
190
- # Beam Meterset:
191
- DICOM::Element.new('300A,0086', field.field_monitor_units, :parent => rb_item)
192
- # Referenced Beam Number:
193
- DICOM::Element.new('300C,0006', beam_number, :parent => rb_item)
194
- # Beam Item:
195
- b_item = DICOM::Item.new(:parent => b_seq)
196
- # Treatment Machine Name (max 16 characters):
197
- DICOM::Element.new('300A,00B2', field.treatment_machine[0..15], :parent => b_item)
198
- # Primary Dosimeter Unit:
199
- DICOM::Element.new('300A,00B3', 'MU', :parent => b_item)
200
- # Source-Axis Distance (convert to mm):
201
- DICOM::Element.new('300A,00B4', "#{field.sad.to_f * 10}", :parent => b_item)
202
- # Beam Number:
203
- DICOM::Element.new('300A,00C0', beam_number, :parent => b_item)
204
- # Beam Name:
205
- DICOM::Element.new('300A,00C2', beam_name, :parent => b_item)
206
- # Beam Description:
207
- DICOM::Element.new('300A,00C3', field.field_note, :parent => b_item)
208
- # Beam Type:
209
- beam_type = case field.treatment_type
210
- when 'Static' then 'STATIC'
211
- when 'StepNShoot' then 'STATIC'
212
- else logger.error("The beam type (treatment type) #{field.treatment_type} is not yet supported.")
213
- end
214
- DICOM::Element.new('300A,00C4', beam_type, :parent => b_item)
215
- # Radiation Type:
216
- rad_type = case field.modality
217
- when 'Elect' then 'ELECTRON'
218
- when 'Xrays' then 'PHOTON'
219
- else logger.error("The radiation type (modality) #{field.modality} is not yet supported.")
220
- end
221
- DICOM::Element.new('300A,00C6', rad_type, :parent => b_item)
222
- # Treatment Delivery Type:
223
- DICOM::Element.new('300A,00CE', 'TREATMENT', :parent => b_item)
224
- # Number of Wedges:
225
- DICOM::Element.new('300A,00D0', (field.wedge.empty? ? '0' : '1'), :parent => b_item)
226
- # Number of Compensators:
227
- DICOM::Element.new('300A,00E0', (field.compensator.empty? ? '0' : '1'), :parent => b_item)
228
- # Number of Boli:
229
- DICOM::Element.new('300A,00ED', (field.bolus.empty? ? '0' : '1'), :parent => b_item)
230
- # Number of Blocks:
231
- DICOM::Element.new('300A,00F0', (field.block.empty? ? '0' : '1'), :parent => b_item)
232
- # Final Cumulative Meterset Weight:
233
- DICOM::Element.new('300A,010E', field.field_monitor_units, :parent => b_item)
234
- # Number of Control Points:
235
- DICOM::Element.new('300A,0110', "#{field.control_points.length}", :parent => b_item)
236
- # Referenced Patient Setup Number:
237
- DICOM::Element.new('300C,006A', '1', :parent => b_item)
238
- #
239
- # Beam Limiting Device Sequence:
240
- #
241
- bl_seq = DICOM::Sequence.new('300A,00B6', :parent => b_item)
242
- # Always create one ASYMX and one ASYMY item:
243
- bl_item_x = DICOM::Item.new(:parent => bl_seq)
244
- bl_item_y = DICOM::Item.new(:parent => bl_seq)
245
- # RT Beam Limiting Device Type:
246
- DICOM::Element.new('300A,00B8', "ASYMX", :parent => bl_item_x)
247
- DICOM::Element.new('300A,00B8', "ASYMY", :parent => bl_item_y)
248
- # Number of Leaf/Jaw Pairs:
249
- DICOM::Element.new('300A,00BC', "1", :parent => bl_item_x)
250
- DICOM::Element.new('300A,00BC', "1", :parent => bl_item_y)
251
- # MLCX item is only created if leaves are defined:
252
- # (NB: The RTP file doesn't specify leaf position boundaries, so for now we estimate these positions
253
- # based on the (even) number of leaves and the assumptions of a 200 mm position of the outer leaf)
254
- # FIXME: In the future, the MLCX leaf position boundary should be configurable - i.e. an option argument of to_dcm().
255
- if field.control_points.length > 0
256
- bl_item_mlcx = DICOM::Item.new(:parent => bl_seq)
257
- DICOM::Element.new('300A,00B8', "MLCX", :parent => bl_item_mlcx)
258
- num_leaves = field.control_points.first.mlc_leaves.to_i
259
- logger.warn("Support for odd number of leaves (#{num_leaves}) is not implemented yet. Leaf Position Boundaries tag will be incorrect.") if num_leaves.odd?
260
- logger.warn("Untested number of leaves encountered: #{num_leaves} Leaf Position Boundaries tag may be incorrect.") if num_leaves.even? && ![40, 80].include?(num_leaves)
261
- DICOM::Element.new('300A,00BC', num_leaves.to_s, :parent => bl_item_mlcx)
262
- pos_boundaries = Array.new(num_leaves) {|i| i * 400 / num_leaves.to_f - 200}
263
- DICOM::Element.new('300A,00BE', "#{pos_boundaries.join("\\")}", :parent => bl_item_mlcx)
264
- end
265
- #
266
- # Block Sequence (if any):
267
- # FIXME: It seems that the Block Sequence (300A,00F4) may be
268
- # difficult (impossible?) to reconstruct based on the RTP file's
269
- # information, and thus it is skipped altogether.
270
- #
271
- #
272
- # Applicator Sequence (if any):
273
- #
274
- unless field.e_applicator.empty?
275
- app_seq = DICOM::Sequence.new('300A,0107', :parent => b_item)
276
- app_item = DICOM::Item.new(:parent => app_seq)
277
- # Applicator ID:
278
- DICOM::Element.new('300A,0108', field.e_field_def_aperture, :parent => app_item)
279
- # Applicator Type:
280
- DICOM::Element.new('300A,0109', "ELECTRON_#{field.e_applicator.upcase}", :parent => app_item)
281
- # Applicator Description:
282
- DICOM::Element.new('300A,010A', "Appl. #{field.e_field_def_aperture}", :parent => app_item)
283
- end
284
- #
285
- # Control Point Sequence:
286
- #
287
- # A field may have 0 (no MLC), 1 (conventional beam with MLC) or 2n (IMRT) control points.
288
- # The DICOM file shall always contain 2n control points (minimum 2).
289
- #
290
- cp_seq = DICOM::Sequence.new('300A,0111', :parent => b_item)
291
- if field.control_points.length < 2
292
- # When we have 0 or 1 control point, use settings from field, and insert MLC settings if present:
293
- # First CP:
294
- cp_item = DICOM::Item.new(:parent => cp_seq)
295
- # Control Point Index:
296
- DICOM::Element.new('300A,0112', "0", :parent => cp_item)
297
- # Nominal Beam Energy:
298
- DICOM::Element.new('300A,0114', "#{field.energy.to_f}", :parent => cp_item)
299
- # Dose Rate Set:
300
- DICOM::Element.new('300A,0115', field.doserate, :parent => cp_item)
301
- # Gantry Angle:
302
- DICOM::Element.new('300A,011E', field.gantry_angle, :parent => cp_item)
303
- # Gantry Rotation Direction:
304
- DICOM::Element.new('300A,011F', (field.arc_direction.empty? ? 'NONE' : field.arc_direction), :parent => cp_item)
305
- # Beam Limiting Device Angle:
306
- DICOM::Element.new('300A,0120', field.collimator_angle, :parent => cp_item)
307
- # Beam Limiting Device Rotation Direction:
308
- DICOM::Element.new('300A,0121', 'NONE', :parent => cp_item)
309
- # Patient Support Angle:
310
- DICOM::Element.new('300A,0122', field.couch_pedestal, :parent => cp_item)
311
- # Patient Support Rotation Direction:
312
- DICOM::Element.new('300A,0123', 'NONE', :parent => cp_item)
313
- # Table Top Eccentric Angle:
314
- DICOM::Element.new('300A,0125', field.couch_angle, :parent => cp_item)
315
- # Table Top Eccentric Rotation Direction:
316
- DICOM::Element.new('300A,0126', 'NONE', :parent => cp_item)
317
- # Table Top Vertical Position:
318
- couch_vert = field.couch_vertical.empty? ? '' : (field.couch_vertical.to_f * 10).to_s
319
- DICOM::Element.new('300A,0128', couch_vert, :parent => cp_item)
320
- # Table Top Longitudinal Position:
321
- couch_long = field.couch_longitudinal.empty? ? '' : (field.couch_longitudinal.to_f * 10).to_s
322
- DICOM::Element.new('300A,0129', couch_long, :parent => cp_item)
323
- # Table Top Lateral Position:
324
- couch_lat = field.couch_lateral.empty? ? '' : (field.couch_lateral.to_f * 10).to_s
325
- DICOM::Element.new('300A,012A', couch_lat, :parent => cp_item)
326
- # Isocenter Position (x\y\z):
327
- 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_item)
328
- # Source to Surface Distance:
329
- DICOM::Element.new('300A,0130', "#{field.ssd.to_f * 10}", :parent => cp_item)
330
- # Cumulative Meterset Weight:
331
- DICOM::Element.new('300A,0134', "0.0", :parent => cp_item)
332
- # Beam Limiting Device Position Sequence:
333
- dp_seq = DICOM::Sequence.new('300A,011A', :parent => cp_item)
334
- # Always create one ASYMX and one ASYMY item:
335
- dp_item_x = DICOM::Item.new(:parent => dp_seq)
336
- dp_item_y = DICOM::Item.new(:parent => dp_seq)
182
+ # Fields with modality 'Unspecified' (e.g. CT or 2dkV) must be skipped:
183
+ unless field.modality == 'Unspecified'
184
+ # If this is an electron beam, a warning should be printed, as these are less reliably converted:
185
+ 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'
186
+ # Beam number and name:
187
+ beam_number = field.extended_field ? field.extended_field.original_beam_number : (i + 1).to_s
188
+ beam_name = field.extended_field ? field.extended_field.original_beam_name : field.field_name
189
+ # Ref Beam Item:
190
+ rb_item = DICOM::Item.new(:parent => rb_seq)
191
+ # Beam Dose (convert from cGy to Gy):
192
+ field_dose = field.field_dose.empty? ? '' : (field.field_dose.to_f * 0.01).round(4).to_s
193
+ DICOM::Element.new('300A,0084', field_dose, :parent => rb_item)
194
+ # Beam Meterset:
195
+ DICOM::Element.new('300A,0086', field.field_monitor_units, :parent => rb_item)
196
+ # Referenced Beam Number:
197
+ DICOM::Element.new('300C,0006', beam_number, :parent => rb_item)
198
+ # Beam Item:
199
+ b_item = DICOM::Item.new(:parent => b_seq)
200
+ # Optional method values:
201
+ # Manufacturer:
202
+ DICOM::Element.new('0008,0070', options[:manufacturer], :parent => b_item) if options[:manufacturer]
203
+ # Manufacturer's Model Name:
204
+ DICOM::Element.new('0008,1090', options[:model], :parent => b_item) if options[:model]
205
+ # Device Serial Number:
206
+ DICOM::Element.new('0018,1000', options[:serial_number], :parent => b_item) if options[:serial_number]
207
+ # Treatment Machine Name (max 16 characters):
208
+ DICOM::Element.new('300A,00B2', field.treatment_machine[0..15], :parent => b_item)
209
+ # Primary Dosimeter Unit:
210
+ DICOM::Element.new('300A,00B3', 'MU', :parent => b_item)
211
+ # Source-Axis Distance (convert to mm):
212
+ DICOM::Element.new('300A,00B4', "#{field.sad.to_f * 10}", :parent => b_item)
213
+ # Beam Number:
214
+ DICOM::Element.new('300A,00C0', beam_number, :parent => b_item)
215
+ # Beam Name:
216
+ DICOM::Element.new('300A,00C2', beam_name, :parent => b_item)
217
+ # Beam Description:
218
+ DICOM::Element.new('300A,00C3', field.field_note, :parent => b_item)
219
+ # Beam Type:
220
+ beam_type = case field.treatment_type
221
+ when 'Static' then 'STATIC'
222
+ when 'StepNShoot' then 'STATIC'
223
+ else logger.error("The beam type (treatment type) #{field.treatment_type} is not yet supported.")
224
+ end
225
+ DICOM::Element.new('300A,00C4', beam_type, :parent => b_item)
226
+ # Radiation Type:
227
+ rad_type = case field.modality
228
+ when 'Elect' then 'ELECTRON'
229
+ when 'Xrays' then 'PHOTON'
230
+ else logger.error("The radiation type (modality) #{field.modality} is not yet supported.")
231
+ end
232
+ DICOM::Element.new('300A,00C6', rad_type, :parent => b_item)
233
+ # Treatment Delivery Type:
234
+ DICOM::Element.new('300A,00CE', 'TREATMENT', :parent => b_item)
235
+ # Number of Wedges:
236
+ DICOM::Element.new('300A,00D0', (field.wedge.empty? ? '0' : '1'), :parent => b_item)
237
+ # Number of Compensators:
238
+ DICOM::Element.new('300A,00E0', (field.compensator.empty? ? '0' : '1'), :parent => b_item)
239
+ # Number of Boli:
240
+ DICOM::Element.new('300A,00ED', (field.bolus.empty? ? '0' : '1'), :parent => b_item)
241
+ # Number of Blocks:
242
+ DICOM::Element.new('300A,00F0', (field.block.empty? ? '0' : '1'), :parent => b_item)
243
+ # Final Cumulative Meterset Weight:
244
+ DICOM::Element.new('300A,010E', field.field_monitor_units, :parent => b_item)
245
+ # Referenced Patient Setup Number:
246
+ DICOM::Element.new('300C,006A', '1', :parent => b_item)
247
+ #
248
+ # Beam Limiting Device Sequence:
249
+ #
250
+ bl_seq = DICOM::Sequence.new('300A,00B6', :parent => b_item)
251
+ # Always create one ASYMY item:
252
+ bl_item_y = DICOM::Item.new(:parent => bl_seq)
337
253
  # RT Beam Limiting Device Type:
338
- DICOM::Element.new('300A,00B8', "ASYMX", :parent => dp_item_x)
339
- DICOM::Element.new('300A,00B8', "ASYMY", :parent => dp_item_y)
340
- # Leaf/Jaw Positions:
341
- DICOM::Element.new('300A,011C', "#{field.collimator_x1.to_f * 10}\\#{field.collimator_x2.to_f * 10}", :parent => dp_item_x)
342
- DICOM::Element.new('300A,011C', "#{field.collimator_y1.to_f * 10}\\#{field.collimator_y2.to_f * 10}", :parent => dp_item_y)
343
- # MLCX:
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.)
344
267
  if field.control_points.length > 0
345
- dp_item_mlcx = DICOM::Item.new(:parent => dp_seq)
346
- # RT Beam Limiting Device Type:
347
- DICOM::Element.new('300A,00B8', "MLCX", :parent => dp_item_mlcx)
348
- # Leaf/Jaw Positions:
349
- pos_a = field.control_points.first.mlc_lp_a.collect{|p| (p.to_f * 10).round(2) unless p.empty?}.compact
350
- pos_b = field.control_points.first.mlc_lp_b.collect{|p| (p.to_f * 10).round(2) unless p.empty?}.compact
351
- leaf_pos = "#{pos_a.join("\\")}\\#{pos_b.join("\\")}"
352
- DICOM::Element.new('300A,011C', leaf_pos, :parent => dp_item_mlcx)
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
274
+ #
275
+ # Block Sequence (if any):
276
+ # FIXME: It seems that the Block Sequence (300A,00F4) may be
277
+ # difficult (impossible?) to reconstruct based on the RTP file's
278
+ # information, and thus it is skipped altogether.
279
+ #
280
+ #
281
+ # Applicator Sequence (if any):
282
+ #
283
+ unless field.e_applicator.empty?
284
+ app_seq = DICOM::Sequence.new('300A,0107', :parent => b_item)
285
+ app_item = DICOM::Item.new(:parent => app_seq)
286
+ # Applicator ID:
287
+ DICOM::Element.new('300A,0108', field.e_field_def_aperture, :parent => app_item)
288
+ # Applicator Type:
289
+ DICOM::Element.new('300A,0109', "ELECTRON_#{field.e_applicator.upcase}", :parent => app_item)
290
+ # Applicator Description:
291
+ DICOM::Element.new('300A,010A', "Appl. #{field.e_field_def_aperture}", :parent => app_item)
353
292
  end
354
- # Referenced Dose Reference Sequence:
355
- rd_seq = DICOM::Sequence.new('300C,0050', :parent => cp_item)
356
- rd_item = DICOM::Item.new(:parent => rd_seq)
357
- # Cumulative Dose Reference Coeffecient:
358
- DICOM::Element.new('300A,010C', '', :parent => rd_item)
359
- # Referenced Dose Reference Number:
360
- DICOM::Element.new('300C,0051', '1', :parent => rd_item)
361
- # Second CP:
362
- cp_item = DICOM::Item.new(:parent => cp_seq)
363
- # Control Point Index:
364
- DICOM::Element.new('300A,0112', "1", :parent => cp_item)
365
- # Cumulative Meterset Weight:
366
- DICOM::Element.new('300A,0134', field.field_monitor_units, :parent => cp_item)
367
- else
368
- # When we have multiple (2n) control points, iterate and pick settings from the CPs:
369
- field.control_points.each_slice(2) do |cp1, cp2|
370
- cp_item1 = DICOM::Item.new(:parent => cp_seq)
371
- cp_item2 = DICOM::Item.new(:parent => cp_seq)
372
- # First control point:
293
+ #
294
+ # Control Point Sequence:
295
+ #
296
+ # A field may have 0 (no MLC), 1 (conventional beam with MLC) or 2n (IMRT) control points.
297
+ # The DICOM file shall always contain 2n control points (minimum 2).
298
+ #
299
+ cp_seq = DICOM::Sequence.new('300A,0111', :parent => b_item)
300
+ if field.control_points.length < 2
301
+ # When we have 0 or 1 control point, use settings from field, and insert MLC settings if present:
302
+ # First CP:
303
+ cp_item = DICOM::Item.new(:parent => cp_seq)
373
304
  # Control Point Index:
374
- DICOM::Element.new('300A,0112', "#{cp1.index}", :parent => cp_item1)
305
+ DICOM::Element.new('300A,0112', "0", :parent => cp_item)
375
306
  # Nominal Beam Energy:
376
- DICOM::Element.new('300A,0114', "#{cp1.energy.to_f}", :parent => cp_item1)
307
+ DICOM::Element.new('300A,0114', "#{field.energy.to_f}", :parent => cp_item)
377
308
  # Dose Rate Set:
378
- DICOM::Element.new('300A,0115', cp1.doserate, :parent => cp_item1)
309
+ DICOM::Element.new('300A,0115', field.doserate, :parent => cp_item)
379
310
  # Gantry Angle:
380
- DICOM::Element.new('300A,011E', cp1.gantry_angle, :parent => cp_item1)
311
+ DICOM::Element.new('300A,011E', field.gantry_angle, :parent => cp_item)
381
312
  # Gantry Rotation Direction:
382
- DICOM::Element.new('300A,011F', (cp1.gantry_dir.empty? ? 'NONE' : cp1.gantry_dir), :parent => cp_item1)
313
+ DICOM::Element.new('300A,011F', (field.arc_direction.empty? ? 'NONE' : field.arc_direction), :parent => cp_item)
383
314
  # Beam Limiting Device Angle:
384
- DICOM::Element.new('300A,0120', cp1.collimator_angle, :parent => cp_item1)
315
+ DICOM::Element.new('300A,0120', field.collimator_angle, :parent => cp_item)
385
316
  # Beam Limiting Device Rotation Direction:
386
- DICOM::Element.new('300A,0121', (cp1.collimator_dir.empty? ? 'NONE' : cp1.collimator_dir), :parent => cp_item1)
317
+ DICOM::Element.new('300A,0121', 'NONE', :parent => cp_item)
387
318
  # Patient Support Angle:
388
- DICOM::Element.new('300A,0122', cp1.couch_pedestal, :parent => cp_item1)
319
+ DICOM::Element.new('300A,0122', field.couch_pedestal, :parent => cp_item)
389
320
  # Patient Support Rotation Direction:
390
- DICOM::Element.new('300A,0123', (cp1.couch_ped_dir.empty? ? 'NONE' : cp1.couch_ped_dir), :parent => cp_item1)
321
+ DICOM::Element.new('300A,0123', 'NONE', :parent => cp_item)
391
322
  # Table Top Eccentric Angle:
392
- DICOM::Element.new('300A,0125', cp1.couch_angle, :parent => cp_item1)
323
+ DICOM::Element.new('300A,0125', field.couch_angle, :parent => cp_item)
393
324
  # Table Top Eccentric Rotation Direction:
394
- DICOM::Element.new('300A,0126', (cp1.couch_dir.empty? ? 'NONE' : cp1.couch_dir), :parent => cp_item1)
325
+ DICOM::Element.new('300A,0126', 'NONE', :parent => cp_item)
395
326
  # Table Top Vertical Position:
396
- couch_vert = cp1.couch_vertical.empty? ? '' : (cp1.couch_vertical.to_f * 10).to_s
397
- DICOM::Element.new('300A,0128', couch_vert, :parent => cp_item1)
327
+ couch_vert = field.couch_vertical.empty? ? '' : (field.couch_vertical.to_f * 10).to_s
328
+ DICOM::Element.new('300A,0128', couch_vert, :parent => cp_item)
398
329
  # Table Top Longitudinal Position:
399
- couch_long = cp1.couch_longitudinal.empty? ? '' : (cp1.couch_longitudinal.to_f * 10).to_s
400
- DICOM::Element.new('300A,0129', couch_long, :parent => cp_item1)
330
+ couch_long = field.couch_longitudinal.empty? ? '' : (field.couch_longitudinal.to_f * 10).to_s
331
+ DICOM::Element.new('300A,0129', couch_long, :parent => cp_item)
401
332
  # Table Top Lateral Position:
402
- couch_lat = cp1.couch_lateral.empty? ? '' : (cp1.couch_lateral.to_f * 10).to_s
403
- DICOM::Element.new('300A,012A', couch_lat, :parent => cp_item1)
333
+ couch_lat = field.couch_lateral.empty? ? '' : (field.couch_lateral.to_f * 10).to_s
334
+ DICOM::Element.new('300A,012A', couch_lat, :parent => cp_item)
404
335
  # Isocenter Position (x\y\z):
405
- 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)
336
+ if p.site_setup
337
+ 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_item)
338
+ else
339
+ logger.warn("No Site Setup record exists for this plan. Unable to provide an isosenter position.")
340
+ DICOM::Element.new('300A,012C', '', :parent => cp_item)
341
+ end
406
342
  # Source to Surface Distance:
407
- DICOM::Element.new('300A,0130', "#{cp1.ssd.to_f * 10}", :parent => cp_item1)
343
+ DICOM::Element.new('300A,0130', "#{field.ssd.to_f * 10}", :parent => cp_item)
408
344
  # Cumulative Meterset Weight:
409
- mu_weight = (cp1.monitor_units.to_f * field.field_monitor_units.to_f).round(4)
410
- DICOM::Element.new('300A,0134', "#{mu_weight}", :parent => cp_item1)
345
+ DICOM::Element.new('300A,0134', "0.0", :parent => cp_item)
411
346
  # Beam Limiting Device Position Sequence:
412
- dp_seq = DICOM::Sequence.new('300A,011A', :parent => cp_item1)
413
- # Always create one ASYMX and one ASYMY item:
414
- dp_item_x = DICOM::Item.new(:parent => dp_seq)
347
+ dp_seq = DICOM::Sequence.new('300A,011A', :parent => cp_item)
348
+ # Always create one ASYMY item:
415
349
  dp_item_y = DICOM::Item.new(:parent => dp_seq)
416
350
  # RT Beam Limiting Device Type:
417
- DICOM::Element.new('300A,00B8', "ASYMX", :parent => dp_item_x)
418
351
  DICOM::Element.new('300A,00B8', "ASYMY", :parent => dp_item_y)
419
352
  # Leaf/Jaw Positions:
420
- DICOM::Element.new('300A,011C', "#{field.collimator_x1.to_f * 10}\\#{field.collimator_x2.to_f * 10}", :parent => dp_item_x)
421
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
422
360
  # MLCX:
423
- dp_item_mlcx = DICOM::Item.new(:parent => dp_seq)
424
- # RT Beam Limiting Device Type:
425
- DICOM::Element.new('300A,00B8', "MLCX", :parent => dp_item_mlcx)
426
- # Leaf/Jaw Positions:
427
- pos_a = cp1.mlc_lp_a.collect{|p| (p.to_f * 10).round(2) unless p.empty?}.compact
428
- pos_b = cp1.mlc_lp_b.collect{|p| (p.to_f * 10).round(2) unless p.empty?}.compact
429
- leaf_pos = "#{pos_a.join("\\")}\\#{pos_b.join("\\")}"
430
- DICOM::Element.new('300A,011C', leaf_pos, :parent => dp_item_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
431
371
  # Referenced Dose Reference Sequence:
432
- rd_seq = DICOM::Sequence.new('300C,0050', :parent => cp_item1)
372
+ rd_seq = DICOM::Sequence.new('300C,0050', :parent => cp_item)
433
373
  rd_item = DICOM::Item.new(:parent => rd_seq)
434
374
  # Cumulative Dose Reference Coeffecient:
435
375
  DICOM::Element.new('300A,010C', '', :parent => rd_item)
436
376
  # Referenced Dose Reference Number:
437
377
  DICOM::Element.new('300C,0051', '1', :parent => rd_item)
438
- # Second control point:
439
- # Always include index and cumulative weight:
440
- DICOM::Element.new('300A,0112', "#{cp2.index}", :parent => cp_item2)
441
- mu_weight = (cp2.monitor_units.to_f * field.field_monitor_units.to_f).round(4)
442
- DICOM::Element.new('300A,0134', "#{mu_weight}", :parent => cp_item2)
443
- # The other parameters are only included if they have changed from the previous control point:
444
- # Nominal Beam Energy:
445
- DICOM::Element.new('300A,0114', "#{cp2.energy.to_f}", :parent => cp_item2) if cp2.energy != cp1.energy
446
- # Dose Rate Set:
447
- DICOM::Element.new('300A,0115', cp1.doserate, :parent => cp_item2) if cp2.doserate != cp1.doserate
448
- # Gantry Angle:
449
- DICOM::Element.new('300A,011E', cp2.gantry_angle, :parent => cp_item2) if cp2.gantry_angle != cp1.gantry_angle
450
- # Gantry Rotation Direction:
451
- DICOM::Element.new('300A,011F', (cp2.gantry_dir.empty? ? 'NONE' : cp2.gantry_dir), :parent => cp_item2) if cp2.gantry_dir != cp1.gantry_dir
452
- # Beam Limiting Device Angle:
453
- DICOM::Element.new('300A,0120', cp2.collimator_angle, :parent => cp_item2) if cp2.collimator_angle != cp1.collimator_angle
454
- # Beam Limiting Device Rotation Direction:
455
- DICOM::Element.new('300A,0121', (cp2.collimator_dir.empty? ? 'NONE' : cp2.collimator_dir), :parent => cp_item2) if cp2.collimator_dir != cp1.collimator_dir
456
- # Patient Support Angle:
457
- DICOM::Element.new('300A,0122', cp2.couch_pedestal, :parent => cp_item2) if cp2.couch_pedestal != cp1.couch_pedestal
458
- # Patient Support Rotation Direction:
459
- 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
460
- # Table Top Eccentric Angle:
461
- DICOM::Element.new('300A,0125', cp2.couch_angle, :parent => cp_item2) if cp2.couch_angle != cp1.couch_angle
462
- # Table Top Eccentric Rotation Direction:
463
- DICOM::Element.new('300A,0126', (cp2.couch_dir.empty? ? 'NONE' : cp2.couch_dir), :parent => cp_item2) if cp2.couch_dir != cp1.couch_dir
464
- # Table Top Vertical Position:
465
- couch_vert = cp2.couch_vertical.empty? ? '' : (cp2.couch_vertical.to_f * 10).to_s
466
- DICOM::Element.new('300A,0128', couch_vert, :parent => cp_item2) if cp2.couch_vertical != cp1.couch_vertical
467
- # Table Top Longitudinal Position:
468
- couch_long = cp2.couch_longitudinal.empty? ? '' : (cp2.couch_longitudinal.to_f * 10).to_s
469
- DICOM::Element.new('300A,0129', couch_long, :parent => cp_item2) if cp2.couch_longitudinal != cp1.couch_longitudinal
470
- # Table Top Lateral Position:
471
- couch_lat = cp2.couch_lateral.empty? ? '' : (cp2.couch_lateral.to_f * 10).to_s
472
- DICOM::Element.new('300A,012A', couch_lat, :parent => cp_item2) if cp2.couch_lateral != cp1.couch_lateral
473
- # Source to Surface Distance:
474
- DICOM::Element.new('300A,0130', "#{cp2.ssd.to_f * 10}", :parent => cp_item2) if cp2.ssd != cp1.ssd
475
- # Beam Limiting Device Position Sequence:
476
- dp_seq = DICOM::Sequence.new('300A,011A', :parent => cp_item2)
477
- # ASYMX:
478
- if cp2.collimator_x1 != cp1.collimator_x1
479
- dp_item_x = DICOM::Item.new(:parent => dp_seq)
480
- DICOM::Element.new('300A,00B8', "ASYMX", :parent => dp_item_x)
481
- DICOM::Element.new('300A,011C', "#{field.collimator_x1.to_f * 10}\\#{field.collimator_x2.to_f * 10}", :parent => dp_item_x)
482
- end
483
- # ASYMY:
484
- if cp2.collimator_y1 != cp1.collimator_y1
378
+ # Second CP:
379
+ cp_item = DICOM::Item.new(:parent => cp_seq)
380
+ # Control Point Index:
381
+ DICOM::Element.new('300A,0112', "1", :parent => cp_item)
382
+ # Cumulative Meterset Weight:
383
+ DICOM::Element.new('300A,0134', field.field_monitor_units, :parent => cp_item)
384
+ else
385
+ # When we have multiple (2n) control points, iterate and pick settings from the CPs:
386
+ field.control_points.each_slice(2) do |cp1, cp2|
387
+ cp_item1 = DICOM::Item.new(:parent => cp_seq)
388
+ cp_item2 = DICOM::Item.new(:parent => cp_seq)
389
+ # First control point:
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:
485
436
  dp_item_y = DICOM::Item.new(:parent => dp_seq)
437
+ # RT Beam Limiting Device Type:
486
438
  DICOM::Element.new('300A,00B8', "ASYMY", :parent => dp_item_y)
439
+ # Leaf/Jaw Positions:
487
440
  DICOM::Element.new('300A,011C', "#{field.collimator_y1.to_f * 10}\\#{field.collimator_y2.to_f * 10}", :parent => dp_item_y)
488
- end
489
- # MLCX:
490
- if cp2.mlc_lp_a != cp1.mlc_lp_a or cp2.mlc_lp_b != cp1.mlc_lp_b
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:
491
448
  dp_item_mlcx = DICOM::Item.new(:parent => dp_seq)
492
449
  # RT Beam Limiting Device Type:
493
450
  DICOM::Element.new('300A,00B8', "MLCX", :parent => dp_item_mlcx)
494
451
  # Leaf/Jaw Positions:
495
- pos_a = cp2.mlc_lp_a.collect{|p| (p.to_f * 10).round(2) unless p.empty?}.compact
496
- pos_b = cp2.mlc_lp_b.collect{|p| (p.to_f * 10).round(2) unless p.empty?}.compact
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
497
454
  leaf_pos = "#{pos_a.join("\\")}\\#{pos_b.join("\\")}"
498
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
499
525
  end
500
526
  end
527
+ # Number of Control Points:
528
+ DICOM::Element.new('300A,0110', b_item['300A,0111'].items.length, :parent => b_item)
501
529
  end
502
530
  end
531
+ # Number of Beams:
532
+ DICOM::Element.new('300A,0080', fg_item['300C,0004'].items.length, :parent => fg_item)
503
533
  end
534
+ # Restore the DICOM logger:
535
+ DICOM.logger.level = original_level
504
536
  return dcm
505
537
  end
506
538