rtp-connect 1.4 → 1.5

Sign up to get free protection for your applications and to get access to all the features.
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