rtp-connect 1.8 → 1.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,657 +1,669 @@
1
- module RTP
2
-
3
- class Plan < Record
4
-
5
- attr_accessor :current_gantry
6
- attr_accessor :current_collimator
7
- attr_accessor :current_couch_angle
8
- attr_accessor :current_couch_pedestal
9
- attr_accessor :current_couch_lateral
10
- attr_accessor :current_couch_longitudinal
11
- attr_accessor :current_couch_vertical
12
-
13
- # Converts the Plan (and child) records to a
14
- # DICOM::DObject of modality RTPLAN.
15
- #
16
- # @note Only photon plans have been tested.
17
- # Electron beams beams may give an invalid DICOM file.
18
- # Also note that, due to limitations in the RTP file format, some original
19
- # values can not be recreated, like e.g. Study UID or Series UID.
20
- # @param [Hash] options the options to use for creating the DICOM object
21
- # @option options [Boolean] :dose_ref if set, Dose Reference & Referenced Dose Reference sequences will be included in the generated DICOM file
22
- # @option options [String] :manufacturer the value used for the manufacturer tag (0008,0070) in the beam sequence
23
- # @option options [String] :model the value used for the manufacturer's model name tag (0008,1090) in the beam sequence
24
- # @option options [Symbol] :scale if set, relevant device parameters are converted from native readout format to IEC1217 (supported values are :elekta & :varian)
25
- # @option options [String] :serial_number the value used for the device serial number tag (0018,1000) in the beam sequence
26
- # @return [DICOM::DObject] the converted DICOM object
27
- #
28
- def to_dcm(options={})
29
- #
30
- # FIXME: This method is rather big, with a few sections of somewhat similar, repeating code.
31
- # Refactoring and simplifying it at some stage might be a good idea.
32
- #
33
- require 'dicom'
34
- original_level = DICOM.logger.level
35
- DICOM.logger.level = Logger::FATAL
36
- p = @prescriptions.first
37
- # If no prescription is present, we are not going to be able to make a valid DICOM object:
38
- logger.error("No Prescription Record present. Unable to build a valid RTPLAN DICOM object.") unless p
39
- dcm = DICOM::DObject.new
40
- #
41
- # TOP LEVEL TAGS:
42
- #
43
- # Specific Character Set:
44
- DICOM::Element.new('0008,0005', 'ISO_IR 100', :parent => dcm)
45
- # Instance Creation Date
46
- DICOM::Element.new('0008,0012', Time.now.strftime("%Y%m%d"), :parent => dcm)
47
- # Instance Creation Time:
48
- DICOM::Element.new('0008,0013', Time.now.strftime("%H%M%S"), :parent => dcm)
49
- # SOP Class UID:
50
- DICOM::Element.new('0008,0016', '1.2.840.10008.5.1.4.1.1.481.5', :parent => dcm)
51
- # SOP Instance UID (if an original UID is not present, we make up a UID):
52
- begin
53
- sop_uid = p.fields.first.extended_field.original_plan_uid.empty? ? DICOM.generate_uid : p.fields.first.extended_field.original_plan_uid
54
- rescue
55
- sop_uid = DICOM.generate_uid
56
- end
57
- DICOM::Element.new('0008,0018', sop_uid, :parent => dcm)
58
- # Study Date
59
- DICOM::Element.new('0008,0020', Time.now.strftime("%Y%m%d"), :parent => dcm)
60
- # Study Time:
61
- DICOM::Element.new('0008,0030', Time.now.strftime("%H%M%S"), :parent => dcm)
62
- # Accession Number:
63
- DICOM::Element.new('0008,0050', '', :parent => dcm)
64
- # Modality:
65
- DICOM::Element.new('0008,0060', 'RTPLAN', :parent => dcm)
66
- # Manufacturer:
67
- DICOM::Element.new('0008,0070', 'rtp-connect', :parent => dcm)
68
- # Referring Physician's Name:
69
- DICOM::Element.new('0008,0090', "#{@md_last_name}^#{@md_first_name}^#{@md_middle_name}^^", :parent => dcm)
70
- # Operator's Name:
71
- DICOM::Element.new('0008,1070', "#{@author_last_name}^#{@author_first_name}^#{@author_middle_name}^^", :parent => dcm)
72
- # Patient's Name:
73
- DICOM::Element.new('0010,0010', "#{@patient_last_name}^#{@patient_first_name}^#{@patient_middle_name}^^", :parent => dcm)
74
- # Patient ID:
75
- DICOM::Element.new('0010,0020', @patient_id, :parent => dcm)
76
- # Patient's Birth Date:
77
- DICOM::Element.new('0010,0030', '', :parent => dcm)
78
- # Patient's Sex:
79
- DICOM::Element.new('0010,0040', '', :parent => dcm)
80
- # Manufacturer's Model Name:
81
- DICOM::Element.new('0008,1090', 'RTP-to-DICOM', :parent => dcm)
82
- # Software Version(s):
83
- DICOM::Element.new('0018,1020', "RubyRTP#{VERSION}", :parent => dcm)
84
- # Study Instance UID:
85
- DICOM::Element.new('0020,000D', DICOM.generate_uid, :parent => dcm)
86
- # Series Instance UID:
87
- DICOM::Element.new('0020,000E', DICOM.generate_uid, :parent => dcm)
88
- # Study ID:
89
- DICOM::Element.new('0020,0010', '1', :parent => dcm)
90
- # Series Number:
91
- DICOM::Element.new('0020,0011', '1', :parent => dcm)
92
- # Frame of Reference UID (if an original UID is not present, we make up a UID):
93
- begin
94
- for_uid = p.site_setup.frame_of_ref_uid.empty? ? DICOM.generate_uid : p.site_setup.frame_of_ref_uid
95
- rescue
96
- for_uid = DICOM.generate_uid
97
- end
98
- DICOM::Element.new('0020,0052', for_uid, :parent => dcm)
99
- # Position Reference Indicator:
100
- DICOM::Element.new('0020,1040', '', :parent => dcm)
101
- # RT Plan Label (max 16 characters):
102
- plan_label = p ? p.rx_site_name[0..15] : @course_id
103
- DICOM::Element.new('300A,0002', plan_label, :parent => dcm)
104
- # RT Plan Name:
105
- plan_name = p ? p.rx_site_name : @course_id
106
- DICOM::Element.new('300A,0003', plan_name, :parent => dcm)
107
- # RT Plan Description:
108
- plan_desc = p ? p.technique : @diagnosis
109
- DICOM::Element.new('300A,0004', plan_desc, :parent => dcm)
110
- # RT Plan Date:
111
- plan_date = @plan_date.empty? ? Time.now.strftime("%Y%m%d") : @plan_date
112
- DICOM::Element.new('300A,0006', plan_date, :parent => dcm)
113
- # RT Plan Time:
114
- plan_time = @plan_time.empty? ? Time.now.strftime("%H%M%S") : @plan_time
115
- DICOM::Element.new('300A,0007', plan_time, :parent => dcm)
116
- # Approval Status:
117
- DICOM::Element.new('300E,0002', 'UNAPPROVED', :parent => dcm)
118
- #
119
- # SEQUENCES:
120
- #
121
- # Tolerance Table Sequence:
122
- if p && p.fields.first && !p.fields.first.tolerance_table.empty?
123
- tt_seq = DICOM::Sequence.new('300A,0040', :parent => dcm)
124
- tt_item = DICOM::Item.new(:parent => tt_seq)
125
- # Tolerance Table Number:
126
- DICOM::Element.new('300A,0042', p.fields.first.tolerance_table, :parent => tt_item)
127
- end
128
- # Structure set information:
129
- if p && p.site_setup && !p.site_setup.structure_set_uid.empty?
130
- #
131
- # Referenced Structure Set Sequence:
132
- #
133
- ss_seq = DICOM::Sequence.new('300C,0060', :parent => dcm)
134
- ss_item = DICOM::Item.new(:parent => ss_seq)
135
- # Referenced SOP Class UID:
136
- DICOM::Element.new('0008,1150', '1.2.840.10008.5.1.4.1.1.481.3', :parent => ss_item)
137
- DICOM::Element.new('0008,1155', p.site_setup.structure_set_uid, :parent => ss_item)
138
- # RT Plan Geometry:
139
- DICOM::Element.new('300A,000C', 'PATIENT', :parent => dcm)
140
- else
141
- # RT Plan Geometry:
142
- DICOM::Element.new('300A,000C', 'TREATMENT_DEVICE', :parent => dcm)
143
- end
144
- #
145
- # Patient Setup Sequence:
146
- #
147
- ps_seq = DICOM::Sequence.new('300A,0180', :parent => dcm)
148
- ps_item = DICOM::Item.new(:parent => ps_seq)
149
- # Patient Position:
150
- begin
151
- pat_pos = p.site_setup.patient_orientation.empty? ? 'HFS' : p.site_setup.patient_orientation
152
- rescue
153
- pat_pos = 'HFS'
154
- end
155
- DICOM::Element.new('0018,5100', pat_pos, :parent => ps_item)
156
- # Patient Setup Number:
157
- DICOM::Element.new('300A,0182', '1', :parent => ps_item)
158
- # Setup Technique (assume Isocentric):
159
- DICOM::Element.new('300A,01B0', 'ISOCENTRIC', :parent => ps_item)
160
- #
161
- # Dose Reference Sequence:
162
- #
163
- create_dose_reference(dcm, plan_name) if options[:dose_ref]
164
- #
165
- # Fraction Group Sequence:
166
- #
167
- fg_seq = DICOM::Sequence.new('300A,0070', :parent => dcm)
168
- fg_item = DICOM::Item.new(:parent => fg_seq)
169
- # Fraction Group Number:
170
- DICOM::Element.new('300A,0071', '1', :parent => fg_item)
171
- # Number of Fractions Planned (try to derive from total dose/fraction dose, or use 1 as default):
172
- begin
173
- num_frac = p.dose_ttl.empty? || p.dose_tx.empty? ? '1' : (p.dose_ttl.to_i / p.dose_tx.to_f).round.to_s
174
- rescue
175
- num_frac = '0'
176
- end
177
- DICOM::Element.new('300A,0078', num_frac, :parent => fg_item)
178
- # Number of Brachy Application Setups:
179
- DICOM::Element.new('300A,00A0', '0', :parent => fg_item)
180
- # Referenced Beam Sequence (items created for each beam below):
181
- rb_seq = DICOM::Sequence.new('300C,0004', :parent => fg_item)
182
- #
183
- # Beam Sequence:
184
- #
185
- b_seq = DICOM::Sequence.new('300A,00B0', :parent => dcm)
186
- if p
187
- # If no fields are present, we are not going to be able to make a valid DICOM object:
188
- logger.error("No Field Record present. Unable to build a valid RTPLAN DICOM object.") unless p.fields.length > 0
189
- p.fields.each_with_index do |field, i|
190
- # Fields with modality 'Unspecified' (e.g. CT or 2dkV) must be skipped:
191
- unless field.modality == 'Unspecified'
192
- # If this is an electron beam, a warning should be printed, as these are less reliably converted:
193
- 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'
194
- # Reset control point 'current value' attributes:
195
- reset_cp_current_attributes
196
- # Beam number and name:
197
- beam_number = field.extended_field ? field.extended_field.original_beam_number : (i + 1).to_s
198
- beam_name = field.extended_field ? field.extended_field.original_beam_name : field.field_name
199
- # Ref Beam Item:
200
- rb_item = DICOM::Item.new(:parent => rb_seq)
201
- # Beam Dose (convert from cGy to Gy):
202
- field_dose = field.field_dose.empty? ? '' : (field.field_dose.to_f * 0.01).round(4).to_s
203
- DICOM::Element.new('300A,0084', field_dose, :parent => rb_item)
204
- # Beam Meterset:
205
- DICOM::Element.new('300A,0086', field.field_monitor_units, :parent => rb_item)
206
- # Referenced Beam Number:
207
- DICOM::Element.new('300C,0006', beam_number, :parent => rb_item)
208
- # Beam Item:
209
- b_item = DICOM::Item.new(:parent => b_seq)
210
- # Optional method values:
211
- # Manufacturer:
212
- DICOM::Element.new('0008,0070', options[:manufacturer], :parent => b_item) if options[:manufacturer]
213
- # Manufacturer's Model Name:
214
- DICOM::Element.new('0008,1090', options[:model], :parent => b_item) if options[:model]
215
- # Device Serial Number:
216
- DICOM::Element.new('0018,1000', options[:serial_number], :parent => b_item) if options[:serial_number]
217
- # Treatment Machine Name (max 16 characters):
218
- DICOM::Element.new('300A,00B2', field.treatment_machine[0..15], :parent => b_item)
219
- # Primary Dosimeter Unit:
220
- DICOM::Element.new('300A,00B3', 'MU', :parent => b_item)
221
- # Source-Axis Distance (convert to mm):
222
- DICOM::Element.new('300A,00B4', "#{field.sad.to_f * 10}", :parent => b_item)
223
- # Beam Number:
224
- DICOM::Element.new('300A,00C0', beam_number, :parent => b_item)
225
- # Beam Name:
226
- DICOM::Element.new('300A,00C2', beam_name, :parent => b_item)
227
- # Beam Description:
228
- DICOM::Element.new('300A,00C3', field.field_note, :parent => b_item)
229
- # Beam Type:
230
- beam_type = case field.treatment_type
231
- when 'Static' then 'STATIC'
232
- when 'StepNShoot' then 'STATIC'
233
- when 'VMAT' then 'DYNAMIC'
234
- else logger.error("The beam type (treatment type) #{field.treatment_type} is not yet supported.")
235
- end
236
- DICOM::Element.new('300A,00C4', beam_type, :parent => b_item)
237
- # Radiation Type:
238
- rad_type = case field.modality
239
- when 'Elect' then 'ELECTRON'
240
- when 'Xrays' then 'PHOTON'
241
- else logger.error("The radiation type (modality) #{field.modality} is not yet supported.")
242
- end
243
- DICOM::Element.new('300A,00C6', rad_type, :parent => b_item)
244
- # Treatment Delivery Type:
245
- DICOM::Element.new('300A,00CE', 'TREATMENT', :parent => b_item)
246
- # Number of Wedges:
247
- DICOM::Element.new('300A,00D0', (field.wedge.empty? ? '0' : '1'), :parent => b_item)
248
- # Number of Compensators:
249
- DICOM::Element.new('300A,00E0', (field.compensator.empty? ? '0' : '1'), :parent => b_item)
250
- # Number of Boli:
251
- DICOM::Element.new('300A,00ED', (field.bolus.empty? ? '0' : '1'), :parent => b_item)
252
- # Number of Blocks:
253
- DICOM::Element.new('300A,00F0', (field.block.empty? ? '0' : '1'), :parent => b_item)
254
- # Final Cumulative Meterset Weight:
255
- DICOM::Element.new('300A,010E', 1, :parent => b_item)
256
- # Referenced Patient Setup Number:
257
- DICOM::Element.new('300C,006A', '1', :parent => b_item)
258
- #
259
- # Beam Limiting Device Sequence:
260
- #
261
- create_beam_limiting_devices(b_item, field)
262
- #
263
- # Block Sequence (if any):
264
- # FIXME: It seems that the Block Sequence (300A,00F4) may be
265
- # difficult (impossible?) to reconstruct based on the RTP file's
266
- # information, and thus it is skipped altogether.
267
- #
268
- #
269
- # Applicator Sequence (if any):
270
- #
271
- unless field.e_applicator.empty?
272
- app_seq = DICOM::Sequence.new('300A,0107', :parent => b_item)
273
- app_item = DICOM::Item.new(:parent => app_seq)
274
- # Applicator ID:
275
- DICOM::Element.new('300A,0108', field.e_field_def_aperture, :parent => app_item)
276
- # Applicator Type:
277
- DICOM::Element.new('300A,0109', "ELECTRON_#{field.e_applicator.upcase}", :parent => app_item)
278
- # Applicator Description:
279
- DICOM::Element.new('300A,010A', "Appl. #{field.e_field_def_aperture}", :parent => app_item)
280
- end
281
- #
282
- # Control Point Sequence:
283
- #
284
- # A field may have 0 (no MLC), 1 (conventional beam with MLC) or 2n (IMRT) control points.
285
- # The DICOM file shall always contain 2n control points (minimum 2).
286
- #
287
- cp_seq = DICOM::Sequence.new('300A,0111', :parent => b_item)
288
- if field.control_points.length < 2
289
- # When we have 0 or 1 control point, use settings from field, and insert MLC settings if present:
290
- # First CP:
291
- cp_item = DICOM::Item.new(:parent => cp_seq)
292
- # Control Point Index:
293
- DICOM::Element.new('300A,0112', "0", :parent => cp_item)
294
- # Nominal Beam Energy:
295
- DICOM::Element.new('300A,0114', "#{field.energy.to_f}", :parent => cp_item)
296
- # Dose Rate Set:
297
- DICOM::Element.new('300A,0115', field.doserate, :parent => cp_item)
298
- # Gantry Angle:
299
- DICOM::Element.new('300A,011E', field.gantry_angle, :parent => cp_item)
300
- # Gantry Rotation Direction:
301
- DICOM::Element.new('300A,011F', (field.arc_direction.empty? ? 'NONE' : field.arc_direction), :parent => cp_item)
302
- # Beam Limiting Device Angle:
303
- DICOM::Element.new('300A,0120', field.collimator_angle, :parent => cp_item)
304
- # Beam Limiting Device Rotation Direction:
305
- DICOM::Element.new('300A,0121', 'NONE', :parent => cp_item)
306
- # Patient Support Angle:
307
- DICOM::Element.new('300A,0122', field.couch_pedestal, :parent => cp_item)
308
- # Patient Support Rotation Direction:
309
- DICOM::Element.new('300A,0123', 'NONE', :parent => cp_item)
310
- # Table Top Eccentric Angle:
311
- DICOM::Element.new('300A,0125', field.couch_angle, :parent => cp_item)
312
- # Table Top Eccentric Rotation Direction:
313
- DICOM::Element.new('300A,0126', 'NONE', :parent => cp_item)
314
- # Table Top Vertical Position:
315
- couch_vert = field.couch_vertical.empty? ? '' : (field.couch_vertical.to_f * 10).to_s
316
- DICOM::Element.new('300A,0128', couch_vert, :parent => cp_item)
317
- # Table Top Longitudinal Position:
318
- couch_long = field.couch_longitudinal.empty? ? '' : (field.couch_longitudinal.to_f * 10).to_s
319
- DICOM::Element.new('300A,0129', couch_long, :parent => cp_item)
320
- # Table Top Lateral Position:
321
- couch_lat = field.couch_lateral.empty? ? '' : (field.couch_lateral.to_f * 10).to_s
322
- DICOM::Element.new('300A,012A', couch_lat, :parent => cp_item)
323
- # Isocenter Position (x\y\z):
324
- if p.site_setup
325
- 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)
326
- else
327
- logger.warn("No Site Setup record exists for this plan. Unable to provide an isosenter position.")
328
- DICOM::Element.new('300A,012C', '', :parent => cp_item)
329
- end
330
- # Source to Surface Distance:
331
- add_ssd(field.ssd, cp_item)
332
- # Cumulative Meterset Weight:
333
- DICOM::Element.new('300A,0134', '0', :parent => cp_item)
334
- # Beam Limiting Device Position Sequence:
335
- if field.control_points.length > 0
336
- create_beam_limiting_device_positions(cp_item, field.control_points.first, options)
337
- else
338
- create_beam_limiting_device_positions_from_field(cp_item, field, options)
339
- end
340
- # Referenced Dose Reference Sequence:
341
- create_referenced_dose_reference(cp_item) if options[:dose_ref]
342
- # Second CP:
343
- cp_item = DICOM::Item.new(:parent => cp_seq)
344
- # Control Point Index:
345
- DICOM::Element.new('300A,0112', "1", :parent => cp_item)
346
- # Cumulative Meterset Weight:
347
- DICOM::Element.new('300A,0134', '1', :parent => cp_item)
348
- else
349
- # When we have multiple (2 or more) control points, iterate each control point:
350
- field.control_points.each { |cp| create_control_point(cp, cp_seq, options) }
351
- # Make sure that hte cumulative meterset weight of the last control
352
- # point is '1' (exactly equal to final cumulative meterset weight):
353
- cp_seq.items.last['300A,0134'].value = '1'
354
- end
355
- # Number of Control Points:
356
- DICOM::Element.new('300A,0110', b_item['300A,0111'].items.length, :parent => b_item)
357
- end
358
- end
359
- # Number of Beams:
360
- DICOM::Element.new('300A,0080', fg_item['300C,0004'].items.length, :parent => fg_item)
361
- end
362
- # Restore the DICOM logger:
363
- DICOM.logger.level = original_level
364
- return dcm
365
- end
366
-
367
-
368
- private
369
-
370
-
371
- # Adds an angular type value to a Control Point Item, by creating the
372
- # necessary DICOM elements.
373
- # Note that the element is only added if there is no 'current' attribute
374
- # defined, or the given value is different form the current attribute.
375
- #
376
- # @param [DICOM::Item] item the DICOM control point item in which to create the elements
377
- # @param [String] angle_tag the DICOM tag of the angle element
378
- # @param [String] direction_tag the DICOM tag of the direction element
379
- # @param [String, NilClass] angle the collimator angle attribute
380
- # @param [String, NilClass] direction the collimator rotation direction attribute
381
- # @param [Symbol] current_angle the instance variable that keeps track of the current value of this attribute
382
- #
383
- def add_angle(item, angle_tag, direction_tag, angle, direction, current_angle)
384
- if !self.send(current_angle) || angle != self.send(current_angle)
385
- self.send("#{current_angle}=", angle)
386
- DICOM::Element.new(angle_tag, angle, :parent => item)
387
- DICOM::Element.new(direction_tag, (direction.empty? ? 'NONE' : direction), :parent => item)
388
- end
389
- end
390
-
391
- # Adds a Table Top Position element to a Control Point Item.
392
- # Note that the element is only added if there is no 'current' attribute
393
- # defined, or the given value is different form the current attribute.
394
- #
395
- # @param [DICOM::Item] item the DICOM control point item in which to create the element
396
- # @param [String] tag the DICOM tag of the couch position element
397
- # @param [String, NilClass] value the couch position
398
- # @param [Symbol] current the instance variable that keeps track of the current value of this attribute
399
- #
400
- def add_couch_position(item, tag, value, current)
401
- if !self.send(current) || value != self.send(current)
402
- self.send("#{current}=", value)
403
- DICOM::Element.new(tag, (value.empty? ? '' : value.to_f * 10), :parent => item)
404
- end
405
- end
406
-
407
- # Adds a Dose Rate Set element to a Control Point Item.
408
- # Note that the element is only added if there is no 'current' attribute
409
- # defined, or the given value is different form the current attribute.
410
- #
411
- # @param [String, NilClass] value the doserate attribute
412
- # @param [DICOM::Item] item the DICOM control point item in which to create an element
413
- #
414
- def add_doserate(value, item)
415
- if !@current_doserate || value != @current_doserate
416
- @current_doserate = value
417
- DICOM::Element.new('300A,0115', value, :parent => item)
418
- end
419
- end
420
-
421
- # Adds a Nominal Beam Energy element to a Control Point Item.
422
- # Note that the element is only added if there is no 'current' attribute
423
- # defined, or the given value is different form the current attribute.
424
- #
425
- # @param [String, NilClass] value the energy attribute
426
- # @param [DICOM::Item] item the DICOM control point item in which to create an element
427
- #
428
- def add_energy(value, item)
429
- if !@current_energy || value != @current_energy
430
- @current_energy = value
431
- DICOM::Element.new('300A,0114', "#{value.to_f}", :parent => item)
432
- end
433
- end
434
-
435
- # Adds an Isosenter element to a Control Point Item.
436
- # Note that the element is only added if there is a Site Setup record present,
437
- # and it contains a real (non-empty) value. Also, the element is only added if there
438
- # is no 'current' attribute defined, or the given value is different form the current attribute.
439
- #
440
- # @param [SiteSetup, NilClass] site_setup the associated site setup record
441
- # @param [DICOM::Item] item the DICOM control point item in which to create an element
442
- #
443
- def add_isosenter(site_setup, item)
444
- if site_setup
445
- # Create an element if the value is new or unique:
446
- if !@current_isosenter
447
- 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)}"
448
- if iso != @current_isosenter
449
- @current_isosenter = iso
450
- DICOM::Element.new('300A,012C', iso, :parent => item)
451
- end
452
- end
453
- else
454
- # Log a warning if this is the first control point:
455
- unless @current_isosenter
456
- logger.warn("No Site Setup record exists for this plan. Unable to provide an isosenter position.")
457
- end
458
- end
459
- end
460
-
461
- # Adds a Source to Surface Distance element to a Control Point Item.
462
- # Note that the element is only added if the SSD attribute contains
463
- # real (non-empty) value.
464
- #
465
- # @param [String, NilClass] value the SSD attribute
466
- # @param [DICOM::Item] item the DICOM control point item in which to create an element
467
- #
468
- def add_ssd(value, item)
469
- DICOM::Element.new('300A,0130', "#{value.to_f * 10}", :parent => item) if value && !value.empty?
470
- end
471
-
472
- # Creates a control point item in the given control point sequence, based
473
- # on an RTP control point record.
474
- #
475
- # @param [ControlPoint] cp the RTP ControlPoint record to convert
476
- # @param [DICOM::Sequence] sequence the DICOM parent sequence of the item to be created
477
- # @param [Hash] options the options to use for creating the control point
478
- # @option options [Boolean] :dose_ref if set, a Referenced Dose Reference sequence will be included in the generated control point item
479
- # @return [DICOM::Item] the constructed control point DICOM item
480
- #
481
- def create_control_point(cp, sequence, options={})
482
- cp_item = DICOM::Item.new(:parent => sequence)
483
- # Some CP attributes will always be written (CP index, BLD positions & Cumulative meterset weight).
484
- # The other attributes are only written if they are different from the previous control point.
485
- # Control Point Index:
486
- DICOM::Element.new('300A,0112', "#{cp.index}", :parent => cp_item)
487
- # Beam Limiting Device Position Sequence:
488
- create_beam_limiting_device_positions(cp_item, cp, options)
489
- # Source to Surface Distance:
490
- add_ssd(cp.ssd, cp_item)
491
- # Cumulative Meterset Weight:
492
- DICOM::Element.new('300A,0134', cp.monitor_units.to_f, :parent => cp_item)
493
- # Referenced Dose Reference Sequence:
494
- create_referenced_dose_reference(cp_item) if options[:dose_ref]
495
- # Attributes that are only added if they carry an updated value:
496
- # Nominal Beam Energy:
497
- add_energy(cp.energy, cp_item)
498
- # Dose Rate Set:
499
- add_doserate(cp.doserate, cp_item)
500
- # Gantry Angle & Rotation Direction:
501
- add_angle(cp_item, '300A,011E', '300A,011F', cp.gantry_angle, cp.gantry_dir, :current_gantry)
502
- # Beam Limiting Device Angle & Rotation Direction:
503
- add_angle(cp_item, '300A,0120', '300A,0121', cp.collimator_angle, cp.collimator_dir, :current_collimator)
504
- # Patient Support Angle & Rotation Direction:
505
- add_angle(cp_item, '300A,0122', '300A,0123', cp.couch_pedestal, cp.couch_ped_dir, :current_couch_pedestal)
506
- # Table Top Eccentric Angle & Rotation Direction:
507
- add_angle(cp_item, '300A,0125', '300A,0126', cp.couch_angle, cp.couch_dir, :current_couch_angle)
508
- # Table Top Vertical Position:
509
- add_couch_position(cp_item, '300A,0128', cp.couch_vertical, :current_couch_vertical)
510
- # Table Top Longitudinal Position:
511
- add_couch_position(cp_item, '300A,0129', cp.couch_longitudinal, :current_couch_longitudinal)
512
- # Table Top Lateral Position:
513
- add_couch_position(cp_item, '300A,012A', cp.couch_lateral, :current_couch_lateral)
514
- # Isocenter Position (x\y\z):
515
- add_isosenter(cp.parent.parent.site_setup, cp_item)
516
- cp_item
517
- end
518
-
519
- # Creates a beam limiting device sequence in the given DICOM object.
520
- #
521
- # @param [DICOM::Item] beam_item the DICOM beam item in which to insert the sequence
522
- # @param [Field] field the RTP field to fetch device parameters from
523
- # @return [DICOM::Sequence] the constructed beam limiting device sequence
524
- #
525
- def create_beam_limiting_devices(beam_item, field)
526
- bl_seq = DICOM::Sequence.new('300A,00B6', :parent => beam_item)
527
- # The ASYMX item ('backup jaws') doesn't exist on all models:
528
- if ['SYM', 'ASY'].include?(field.field_x_mode.upcase)
529
- bl_item_x = DICOM::Item.new(:parent => bl_seq)
530
- DICOM::Element.new('300A,00B8', "ASYMX", :parent => bl_item_x)
531
- DICOM::Element.new('300A,00BC', "1", :parent => bl_item_x)
532
- end
533
- # The ASYMY item is always created:
534
- bl_item_y = DICOM::Item.new(:parent => bl_seq)
535
- # RT Beam Limiting Device Type:
536
- DICOM::Element.new('300A,00B8', "ASYMY", :parent => bl_item_y)
537
- # Number of Leaf/Jaw Pairs:
538
- DICOM::Element.new('300A,00BC', "1", :parent => bl_item_y)
539
- # MLCX item is only created if leaves are defined:
540
- # (NB: The RTP file doesn't specify leaf position boundaries, so we
541
- # have to set these based on a set of known MLC types, their number
542
- # of leaves, and their leaf boundary positions.)
543
- if field.control_points.length > 0
544
- bl_item_mlcx = DICOM::Item.new(:parent => bl_seq)
545
- DICOM::Element.new('300A,00B8', "MLCX", :parent => bl_item_mlcx)
546
- num_leaves = field.control_points.first.mlc_leaves.to_i
547
- DICOM::Element.new('300A,00BC', num_leaves.to_s, :parent => bl_item_mlcx)
548
- DICOM::Element.new('300A,00BE', "#{RTP.leaf_boundaries(num_leaves).join("\\")}", :parent => bl_item_mlcx)
549
- end
550
- bl_seq
551
- end
552
-
553
- # Creates a beam limiting device positions sequence in the given DICOM object.
554
- #
555
- # @param [DICOM::Item] cp_item the DICOM control point item in which to insert the sequence
556
- # @param [ControlPoint] cp the RTP control point to fetch device parameters from
557
- # @return [DICOM::Sequence] the constructed beam limiting device positions sequence
558
- #
559
- def create_beam_limiting_device_positions(cp_item, cp, options={})
560
- dp_seq = DICOM::Sequence.new('300A,011A', :parent => cp_item)
561
- # The ASYMX item ('backup jaws') doesn't exist on all models:
562
- if ['SYM', 'ASY'].include?(cp.parent.field_x_mode.upcase)
563
- dp_item_x = DICOM::Item.new(:parent => dp_seq)
564
- DICOM::Element.new('300A,00B8', "ASYMX", :parent => dp_item_x)
565
- DICOM::Element.new('300A,011C', "#{cp.dcm_collimator_x1(options[:scale])}\\#{cp.dcm_collimator_x2(options[:scale])}", :parent => dp_item_x)
566
- end
567
- # Always create one ASYMY item:
568
- dp_item_y = DICOM::Item.new(:parent => dp_seq)
569
- # RT Beam Limiting Device Type:
570
- DICOM::Element.new('300A,00B8', "ASYMY", :parent => dp_item_y)
571
- # Leaf/Jaw Positions:
572
- DICOM::Element.new('300A,011C', "#{cp.dcm_collimator_y1(options[:scale])}\\#{cp.dcm_collimator_y2(options[:scale])}", :parent => dp_item_y)
573
- # MLCX:
574
- dp_item_mlcx = DICOM::Item.new(:parent => dp_seq)
575
- # RT Beam Limiting Device Type:
576
- DICOM::Element.new('300A,00B8', "MLCX", :parent => dp_item_mlcx)
577
- # Leaf/Jaw Positions:
578
- DICOM::Element.new('300A,011C', cp.dcm_mlc_positions(options[:scale]), :parent => dp_item_mlcx)
579
- dp_seq
580
- end
581
-
582
- # Creates a beam limiting device positions sequence in the given DICOM object.
583
- #
584
- # @param [DICOM::Item] cp_item the DICOM control point item in which to insert the sequence
585
- # @param [Field] field the RTP treatment field to fetch device parameters from
586
- # @return [DICOM::Sequence] the constructed beam limiting device positions sequence
587
- #
588
- def create_beam_limiting_device_positions_from_field(cp_item, field, options={})
589
- dp_seq = DICOM::Sequence.new('300A,011A', :parent => cp_item)
590
- # ASYMX:
591
- dp_item_x = DICOM::Item.new(:parent => dp_seq)
592
- DICOM::Element.new('300A,00B8', "ASYMX", :parent => dp_item_x)
593
- DICOM::Element.new('300A,011C', "#{field.dcm_collimator_x1}\\#{field.dcm_collimator_x2}", :parent => dp_item_x)
594
- # ASYMY:
595
- dp_item_y = DICOM::Item.new(:parent => dp_seq)
596
- DICOM::Element.new('300A,00B8', "ASYMY", :parent => dp_item_y)
597
- DICOM::Element.new('300A,011C', "#{field.dcm_collimator_y1}\\#{field.dcm_collimator_y2}", :parent => dp_item_y)
598
- dp_seq
599
- end
600
-
601
- # Creates a dose reference sequence in the given DICOM object.
602
- #
603
- # @param [DICOM::DObject] dcm the DICOM object in which to insert the sequence
604
- # @param [String] description the value to use for Dose Reference Description
605
- # @return [DICOM::Sequence] the constructed dose reference sequence
606
- #
607
- def create_dose_reference(dcm, description)
608
- dr_seq = DICOM::Sequence.new('300A,0010', :parent => dcm)
609
- dr_item = DICOM::Item.new(:parent => dr_seq)
610
- # Dose Reference Number:
611
- DICOM::Element.new('300A,0012', '1', :parent => dr_item)
612
- # Dose Reference Structure Type:
613
- DICOM::Element.new('300A,0014', 'SITE', :parent => dr_item)
614
- # Dose Reference Description:
615
- DICOM::Element.new('300A,0016', description, :parent => dr_item)
616
- # Dose Reference Type:
617
- DICOM::Element.new('300A,0020', 'TARGET', :parent => dr_item)
618
- dr_seq
619
- end
620
-
621
- # Creates a referenced dose reference sequence in the given DICOM object.
622
- #
623
- # @param [DICOM::Item] cp_item the DICOM item in which to insert the sequence
624
- # @return [DICOM::Sequence] the constructed referenced dose reference sequence
625
- #
626
- def create_referenced_dose_reference(cp_item)
627
- # Referenced Dose Reference Sequence:
628
- rd_seq = DICOM::Sequence.new('300C,0050', :parent => cp_item)
629
- rd_item = DICOM::Item.new(:parent => rd_seq)
630
- # Cumulative Dose Reference Coeffecient:
631
- DICOM::Element.new('300A,010C', '', :parent => rd_item)
632
- # Referenced Dose Reference Number:
633
- DICOM::Element.new('300C,0051', '1', :parent => rd_item)
634
- rd_seq
635
- end
636
-
637
- # Resets the types of control point attributes that are only written to the
638
- # first control point item, and for following control point items only when
639
- # they are different from the 'current' value. When a new field is reached,
640
- # it is essential to reset these attributes, or else we could risk to start
641
- # the field with a control point with missing attributes, if one of its first
642
- # attributes is equal to the last attribute of the previous field.
643
- #
644
- def reset_cp_current_attributes
645
- @current_gantry = nil
646
- @current_collimator = nil
647
- @current_couch_pedestal = nil
648
- @current_couch_angle = nil
649
- @current_couch_vertical = nil
650
- @current_couch_longitudinal = nil
651
- @current_couch_lateral = nil
652
- @current_isosenter = nil
653
- end
654
-
655
- end
656
-
1
+ module RTP
2
+
3
+ class Plan < Record
4
+
5
+ attr_accessor :current_gantry
6
+ attr_accessor :current_collimator
7
+ attr_accessor :current_couch_angle
8
+ attr_accessor :current_couch_pedestal
9
+ attr_accessor :current_couch_lateral
10
+ attr_accessor :current_couch_longitudinal
11
+ attr_accessor :current_couch_vertical
12
+
13
+ # Converts the Plan (and child) records to a
14
+ # DICOM::DObject of modality RTPLAN.
15
+ #
16
+ # @note Only photon plans have been tested.
17
+ # Electron beams beams may give an invalid DICOM file.
18
+ # Also note that, due to limitations in the RTP file format, some original
19
+ # values can not be recreated, like e.g. Study UID or Series UID.
20
+ # @param [Hash] options the options to use for creating the DICOM object
21
+ # @option options [Boolean] :dose_ref if set, Dose Reference & Referenced Dose Reference sequences will be included in the generated DICOM file
22
+ # @option options [String] :manufacturer the value used for the manufacturer tag (0008,0070) in the beam sequence
23
+ # @option options [String] :model the value used for the manufacturer's model name tag (0008,1090) in the beam sequence
24
+ # @option options [Symbol] :scale if set, relevant device parameters are converted from native readout format to IEC1217 (supported values are :elekta & :varian)
25
+ # @option options [String] :serial_number the value used for the device serial number tag (0018,1000) in the beam sequence
26
+ # @return [DICOM::DObject] the converted DICOM object
27
+ #
28
+ def to_dcm(options={})
29
+ #
30
+ # FIXME: This method is rather big, with a few sections of somewhat similar, repeating code.
31
+ # Refactoring and simplifying it at some stage might be a good idea.
32
+ #
33
+ require 'dicom'
34
+ original_level = DICOM.logger.level
35
+ DICOM.logger.level = Logger::FATAL
36
+ p = @prescriptions.first
37
+ # If no prescription is present, we are not going to be able to make a valid DICOM object:
38
+ logger.error("No Prescription Record present. Unable to build a valid RTPLAN DICOM object.") unless p
39
+ dcm = DICOM::DObject.new
40
+ #
41
+ # TOP LEVEL TAGS:
42
+ #
43
+ # Specific Character Set:
44
+ DICOM::Element.new('0008,0005', 'ISO_IR 100', :parent => dcm)
45
+ # Instance Creation Date
46
+ DICOM::Element.new('0008,0012', Time.now.strftime("%Y%m%d"), :parent => dcm)
47
+ # Instance Creation Time:
48
+ DICOM::Element.new('0008,0013', Time.now.strftime("%H%M%S"), :parent => dcm)
49
+ # SOP Class UID:
50
+ DICOM::Element.new('0008,0016', '1.2.840.10008.5.1.4.1.1.481.5', :parent => dcm)
51
+ # SOP Instance UID (if an original UID is not present, we make up a UID):
52
+ begin
53
+ sop_uid = p.fields.first.extended_field.original_plan_uid.empty? ? DICOM.generate_uid : p.fields.first.extended_field.original_plan_uid
54
+ rescue
55
+ sop_uid = DICOM.generate_uid
56
+ end
57
+ DICOM::Element.new('0008,0018', sop_uid, :parent => dcm)
58
+ # Study Date
59
+ DICOM::Element.new('0008,0020', Time.now.strftime("%Y%m%d"), :parent => dcm)
60
+ # Study Time:
61
+ DICOM::Element.new('0008,0030', Time.now.strftime("%H%M%S"), :parent => dcm)
62
+ # Accession Number:
63
+ DICOM::Element.new('0008,0050', '', :parent => dcm)
64
+ # Modality:
65
+ DICOM::Element.new('0008,0060', 'RTPLAN', :parent => dcm)
66
+ # Manufacturer:
67
+ DICOM::Element.new('0008,0070', 'rtp-connect', :parent => dcm)
68
+ # Referring Physician's Name:
69
+ DICOM::Element.new('0008,0090', "#{@md_last_name}^#{@md_first_name}^#{@md_middle_name}^^", :parent => dcm)
70
+ # Operator's Name:
71
+ DICOM::Element.new('0008,1070', "#{@author_last_name}^#{@author_first_name}^#{@author_middle_name}^^", :parent => dcm)
72
+ # Patient's Name:
73
+ DICOM::Element.new('0010,0010', "#{@patient_last_name}^#{@patient_first_name}^#{@patient_middle_name}^^", :parent => dcm)
74
+ # Patient ID:
75
+ DICOM::Element.new('0010,0020', @patient_id, :parent => dcm)
76
+ # Patient's Birth Date:
77
+ DICOM::Element.new('0010,0030', '', :parent => dcm)
78
+ # Patient's Sex:
79
+ DICOM::Element.new('0010,0040', '', :parent => dcm)
80
+ # Manufacturer's Model Name:
81
+ DICOM::Element.new('0008,1090', 'RTP-to-DICOM', :parent => dcm)
82
+ # Software Version(s):
83
+ DICOM::Element.new('0018,1020', "RubyRTP#{VERSION}", :parent => dcm)
84
+ # Study Instance UID:
85
+ DICOM::Element.new('0020,000D', DICOM.generate_uid, :parent => dcm)
86
+ # Series Instance UID:
87
+ DICOM::Element.new('0020,000E', DICOM.generate_uid, :parent => dcm)
88
+ # Study ID:
89
+ DICOM::Element.new('0020,0010', '1', :parent => dcm)
90
+ # Series Number:
91
+ DICOM::Element.new('0020,0011', '1', :parent => dcm)
92
+ # Frame of Reference UID (if an original UID is not present, we make up a UID):
93
+ begin
94
+ for_uid = p.site_setup.frame_of_ref_uid.empty? ? DICOM.generate_uid : p.site_setup.frame_of_ref_uid
95
+ rescue
96
+ for_uid = DICOM.generate_uid
97
+ end
98
+ DICOM::Element.new('0020,0052', for_uid, :parent => dcm)
99
+ # Position Reference Indicator:
100
+ DICOM::Element.new('0020,1040', '', :parent => dcm)
101
+ # RT Plan Label (max 16 characters):
102
+ plan_label = p ? p.rx_site_name[0..15] : @course_id
103
+ DICOM::Element.new('300A,0002', plan_label, :parent => dcm)
104
+ # RT Plan Name:
105
+ plan_name = p ? p.rx_site_name : @course_id
106
+ DICOM::Element.new('300A,0003', plan_name, :parent => dcm)
107
+ # RT Plan Description:
108
+ plan_desc = p ? p.technique : @diagnosis
109
+ DICOM::Element.new('300A,0004', plan_desc, :parent => dcm)
110
+ # RT Plan Date:
111
+ plan_date = @plan_date.empty? ? Time.now.strftime("%Y%m%d") : @plan_date
112
+ DICOM::Element.new('300A,0006', plan_date, :parent => dcm)
113
+ # RT Plan Time:
114
+ plan_time = @plan_time.empty? ? Time.now.strftime("%H%M%S") : @plan_time
115
+ DICOM::Element.new('300A,0007', plan_time, :parent => dcm)
116
+ # Approval Status:
117
+ DICOM::Element.new('300E,0002', 'UNAPPROVED', :parent => dcm)
118
+ #
119
+ # SEQUENCES:
120
+ #
121
+ # Tolerance Table Sequence:
122
+ if p && p.fields.first && !p.fields.first.tolerance_table.empty?
123
+ tt_seq = DICOM::Sequence.new('300A,0040', :parent => dcm)
124
+ tt_item = DICOM::Item.new(:parent => tt_seq)
125
+ # Tolerance Table Number:
126
+ DICOM::Element.new('300A,0042', p.fields.first.tolerance_table, :parent => tt_item)
127
+ end
128
+ # Structure set information:
129
+ if p && p.site_setup && !p.site_setup.structure_set_uid.empty?
130
+ #
131
+ # Referenced Structure Set Sequence:
132
+ #
133
+ ss_seq = DICOM::Sequence.new('300C,0060', :parent => dcm)
134
+ ss_item = DICOM::Item.new(:parent => ss_seq)
135
+ # Referenced SOP Class UID:
136
+ DICOM::Element.new('0008,1150', '1.2.840.10008.5.1.4.1.1.481.3', :parent => ss_item)
137
+ DICOM::Element.new('0008,1155', p.site_setup.structure_set_uid, :parent => ss_item)
138
+ # RT Plan Geometry:
139
+ DICOM::Element.new('300A,000C', 'PATIENT', :parent => dcm)
140
+ else
141
+ # RT Plan Geometry:
142
+ DICOM::Element.new('300A,000C', 'TREATMENT_DEVICE', :parent => dcm)
143
+ end
144
+ #
145
+ # Patient Setup Sequence:
146
+ #
147
+ ps_seq = DICOM::Sequence.new('300A,0180', :parent => dcm)
148
+ ps_item = DICOM::Item.new(:parent => ps_seq)
149
+ # Patient Position:
150
+ begin
151
+ pat_pos = p.site_setup.patient_orientation.empty? ? 'HFS' : p.site_setup.patient_orientation
152
+ rescue
153
+ pat_pos = 'HFS'
154
+ end
155
+ DICOM::Element.new('0018,5100', pat_pos, :parent => ps_item)
156
+ # Patient Setup Number:
157
+ DICOM::Element.new('300A,0182', '1', :parent => ps_item)
158
+ # Setup Technique (assume Isocentric):
159
+ DICOM::Element.new('300A,01B0', 'ISOCENTRIC', :parent => ps_item)
160
+ #
161
+ # Dose Reference Sequence:
162
+ #
163
+ create_dose_reference(dcm, plan_name) if options[:dose_ref]
164
+ #
165
+ # Fraction Group Sequence:
166
+ #
167
+ fg_seq = DICOM::Sequence.new('300A,0070', :parent => dcm)
168
+ fg_item = DICOM::Item.new(:parent => fg_seq)
169
+ # Fraction Group Number:
170
+ DICOM::Element.new('300A,0071', '1', :parent => fg_item)
171
+ # Number of Fractions Planned (try to derive from total dose/fraction dose, or use 1 as default):
172
+ begin
173
+ num_frac = p.dose_ttl.empty? || p.dose_tx.empty? ? '1' : (p.dose_ttl.to_i / p.dose_tx.to_f).round.to_s
174
+ rescue
175
+ num_frac = '0'
176
+ end
177
+ DICOM::Element.new('300A,0078', num_frac, :parent => fg_item)
178
+ # Number of Brachy Application Setups:
179
+ DICOM::Element.new('300A,00A0', '0', :parent => fg_item)
180
+ # Referenced Beam Sequence (items created for each beam below):
181
+ rb_seq = DICOM::Sequence.new('300C,0004', :parent => fg_item)
182
+ #
183
+ # Beam Sequence:
184
+ #
185
+ b_seq = DICOM::Sequence.new('300A,00B0', :parent => dcm)
186
+ if p
187
+ # If no fields are present, we are not going to be able to make a valid DICOM object:
188
+ logger.error("No Field Record present. Unable to build a valid RTPLAN DICOM object.") unless p.fields.length > 0
189
+ p.fields.each_with_index do |field, i|
190
+ # Fields with modality 'Unspecified' (e.g. CT or 2dkV) must be skipped:
191
+ unless field.modality == 'Unspecified'
192
+ # If this is an electron beam, a warning should be printed, as these are less reliably converted:
193
+ 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'
194
+ # Reset control point 'current value' attributes:
195
+ reset_cp_current_attributes
196
+ # Beam number and name:
197
+ beam_number = field.extended_field ? field.extended_field.original_beam_number : (i + 1).to_s
198
+ beam_name = field.extended_field ? field.extended_field.original_beam_name : field.field_name
199
+ # Ref Beam Item:
200
+ rb_item = DICOM::Item.new(:parent => rb_seq)
201
+ # Beam Dose (convert from cGy to Gy):
202
+ field_dose = field.field_dose.empty? ? '' : (field.field_dose.to_f * 0.01).round(4).to_s
203
+ DICOM::Element.new('300A,0084', field_dose, :parent => rb_item)
204
+ # Beam Meterset:
205
+ DICOM::Element.new('300A,0086', field.field_monitor_units, :parent => rb_item)
206
+ # Referenced Beam Number:
207
+ DICOM::Element.new('300C,0006', beam_number, :parent => rb_item)
208
+ # Beam Item:
209
+ b_item = DICOM::Item.new(:parent => b_seq)
210
+ # Optional method values:
211
+ # Manufacturer:
212
+ DICOM::Element.new('0008,0070', options[:manufacturer], :parent => b_item) if options[:manufacturer]
213
+ # Manufacturer's Model Name:
214
+ DICOM::Element.new('0008,1090', options[:model], :parent => b_item) if options[:model]
215
+ # Device Serial Number:
216
+ DICOM::Element.new('0018,1000', options[:serial_number], :parent => b_item) if options[:serial_number]
217
+ # Treatment Machine Name (max 16 characters):
218
+ DICOM::Element.new('300A,00B2', field.treatment_machine[0..15], :parent => b_item)
219
+ # Primary Dosimeter Unit:
220
+ DICOM::Element.new('300A,00B3', 'MU', :parent => b_item)
221
+ # Source-Axis Distance (convert to mm):
222
+ DICOM::Element.new('300A,00B4', "#{field.sad.to_f * 10}", :parent => b_item)
223
+ # Beam Number:
224
+ DICOM::Element.new('300A,00C0', beam_number, :parent => b_item)
225
+ # Beam Name:
226
+ DICOM::Element.new('300A,00C2', beam_name, :parent => b_item)
227
+ # Beam Description:
228
+ DICOM::Element.new('300A,00C3', field.field_note, :parent => b_item)
229
+ # Beam Type:
230
+ beam_type = case field.treatment_type
231
+ when 'Static' then 'STATIC'
232
+ when 'StepNShoot' then 'STATIC'
233
+ when 'VMAT' then 'DYNAMIC'
234
+ else logger.error("The beam type (treatment type) #{field.treatment_type} is not yet supported.")
235
+ end
236
+ DICOM::Element.new('300A,00C4', beam_type, :parent => b_item)
237
+ # Radiation Type:
238
+ rad_type = case field.modality
239
+ when 'Elect' then 'ELECTRON'
240
+ when 'Xrays' then 'PHOTON'
241
+ else logger.error("The radiation type (modality) #{field.modality} is not yet supported.")
242
+ end
243
+ DICOM::Element.new('300A,00C6', rad_type, :parent => b_item)
244
+ # Treatment Delivery Type:
245
+ DICOM::Element.new('300A,00CE', 'TREATMENT', :parent => b_item)
246
+ # Number of Wedges:
247
+ DICOM::Element.new('300A,00D0', (field.wedge.empty? ? '0' : '1'), :parent => b_item)
248
+ # Number of Compensators:
249
+ DICOM::Element.new('300A,00E0', (field.compensator.empty? ? '0' : '1'), :parent => b_item)
250
+ # Number of Boli:
251
+ DICOM::Element.new('300A,00ED', (field.bolus.empty? ? '0' : '1'), :parent => b_item)
252
+ # Number of Blocks:
253
+ DICOM::Element.new('300A,00F0', (field.block.empty? ? '0' : '1'), :parent => b_item)
254
+ # Final Cumulative Meterset Weight:
255
+ DICOM::Element.new('300A,010E', 1, :parent => b_item)
256
+ # Referenced Patient Setup Number:
257
+ DICOM::Element.new('300C,006A', '1', :parent => b_item)
258
+ #
259
+ # Beam Limiting Device Sequence:
260
+ #
261
+ create_beam_limiting_devices(b_item, field)
262
+ #
263
+ # Block Sequence (if any):
264
+ # FIXME: It seems that the Block Sequence (300A,00F4) may be
265
+ # difficult (impossible?) to reconstruct based on the RTP file's
266
+ # information, and thus it is skipped altogether.
267
+ #
268
+ #
269
+ # Applicator Sequence (if any):
270
+ #
271
+ unless field.e_applicator.empty?
272
+ app_seq = DICOM::Sequence.new('300A,0107', :parent => b_item)
273
+ app_item = DICOM::Item.new(:parent => app_seq)
274
+ # Applicator ID:
275
+ DICOM::Element.new('300A,0108', field.e_field_def_aperture, :parent => app_item)
276
+ # Applicator Type:
277
+ DICOM::Element.new('300A,0109', "ELECTRON_#{field.e_applicator.upcase}", :parent => app_item)
278
+ # Applicator Description:
279
+ DICOM::Element.new('300A,010A', "Appl. #{field.e_field_def_aperture}", :parent => app_item)
280
+ end
281
+ #
282
+ # Control Point Sequence:
283
+ #
284
+ # A field may have 0 (no MLC), 1 (conventional beam with MLC) or 2n (IMRT) control points.
285
+ # The DICOM file shall always contain 2n control points (minimum 2).
286
+ #
287
+ cp_seq = DICOM::Sequence.new('300A,0111', :parent => b_item)
288
+ if field.control_points.length < 2
289
+ # When we have 0 or 1 control point, use settings from field, and insert MLC settings if present:
290
+ # First CP:
291
+ cp_item = DICOM::Item.new(:parent => cp_seq)
292
+ # Control Point Index:
293
+ DICOM::Element.new('300A,0112', "0", :parent => cp_item)
294
+ # Nominal Beam Energy:
295
+ DICOM::Element.new('300A,0114', "#{field.energy.to_f}", :parent => cp_item)
296
+ # Dose Rate Set:
297
+ DICOM::Element.new('300A,0115', field.doserate, :parent => cp_item)
298
+ # Gantry Angle:
299
+ DICOM::Element.new('300A,011E', field.gantry_angle, :parent => cp_item)
300
+ # Gantry Rotation Direction:
301
+ DICOM::Element.new('300A,011F', (field.arc_direction.empty? ? 'NONE' : field.arc_direction), :parent => cp_item)
302
+ # Beam Limiting Device Angle:
303
+ DICOM::Element.new('300A,0120', field.collimator_angle, :parent => cp_item)
304
+ # Beam Limiting Device Rotation Direction:
305
+ DICOM::Element.new('300A,0121', 'NONE', :parent => cp_item)
306
+ # Patient Support Angle:
307
+ DICOM::Element.new('300A,0122', field.couch_pedestal, :parent => cp_item)
308
+ # Patient Support Rotation Direction:
309
+ DICOM::Element.new('300A,0123', 'NONE', :parent => cp_item)
310
+ # Table Top Eccentric Angle:
311
+ DICOM::Element.new('300A,0125', field.couch_angle, :parent => cp_item)
312
+ # Table Top Eccentric Rotation Direction:
313
+ DICOM::Element.new('300A,0126', 'NONE', :parent => cp_item)
314
+ # Table Top Vertical Position:
315
+ couch_vert = field.couch_vertical.empty? ? '' : (field.couch_vertical.to_f * 10).to_s
316
+ DICOM::Element.new('300A,0128', couch_vert, :parent => cp_item)
317
+ # Table Top Longitudinal Position:
318
+ couch_long = field.couch_longitudinal.empty? ? '' : (field.couch_longitudinal.to_f * 10).to_s
319
+ DICOM::Element.new('300A,0129', couch_long, :parent => cp_item)
320
+ # Table Top Lateral Position:
321
+ couch_lat = field.couch_lateral.empty? ? '' : (field.couch_lateral.to_f * 10).to_s
322
+ DICOM::Element.new('300A,012A', couch_lat, :parent => cp_item)
323
+ # Isocenter Position (x\y\z):
324
+ if p.site_setup
325
+ 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)
326
+ else
327
+ logger.warn("No Site Setup record exists for this plan. Unable to provide an isosenter position.")
328
+ DICOM::Element.new('300A,012C', '', :parent => cp_item)
329
+ end
330
+ # Source to Surface Distance:
331
+ add_ssd(field.ssd, cp_item)
332
+ # Cumulative Meterset Weight:
333
+ DICOM::Element.new('300A,0134', '0', :parent => cp_item)
334
+ # Beam Limiting Device Position Sequence:
335
+ if field.control_points.length > 0
336
+ create_beam_limiting_device_positions(cp_item, field.control_points.first, options)
337
+ else
338
+ create_beam_limiting_device_positions_from_field(cp_item, field, options)
339
+ end
340
+ # Referenced Dose Reference Sequence:
341
+ create_referenced_dose_reference(cp_item) if options[:dose_ref]
342
+ # Second CP:
343
+ cp_item = DICOM::Item.new(:parent => cp_seq)
344
+ # Control Point Index:
345
+ DICOM::Element.new('300A,0112', "1", :parent => cp_item)
346
+ # Cumulative Meterset Weight:
347
+ DICOM::Element.new('300A,0134', '1', :parent => cp_item)
348
+ else
349
+ # When we have multiple (2 or more) control points, iterate each control point:
350
+ field.control_points.each { |cp| create_control_point(cp, cp_seq, options) }
351
+ # Make sure that hte cumulative meterset weight of the last control
352
+ # point is '1' (exactly equal to final cumulative meterset weight):
353
+ cp_seq.items.last['300A,0134'].value = '1'
354
+ end
355
+ # Number of Control Points:
356
+ DICOM::Element.new('300A,0110', b_item['300A,0111'].items.length, :parent => b_item)
357
+ end
358
+ end
359
+ # Number of Beams:
360
+ DICOM::Element.new('300A,0080', fg_item['300C,0004'].items.length, :parent => fg_item)
361
+ end
362
+ # Restore the DICOM logger:
363
+ DICOM.logger.level = original_level
364
+ return dcm
365
+ end
366
+
367
+
368
+ private
369
+
370
+
371
+ # Adds an angular type value to a Control Point Item, by creating the
372
+ # necessary DICOM elements.
373
+ # Note that the element is only added if there is no 'current' attribute
374
+ # defined, or the given value is different form the current attribute.
375
+ #
376
+ # @param [DICOM::Item] item the DICOM control point item in which to create the elements
377
+ # @param [String] angle_tag the DICOM tag of the angle element
378
+ # @param [String] direction_tag the DICOM tag of the direction element
379
+ # @param [String, NilClass] angle the collimator angle attribute
380
+ # @param [String, NilClass] direction the collimator rotation direction attribute
381
+ # @param [Symbol] current_angle the instance variable that keeps track of the current value of this attribute
382
+ #
383
+ def add_angle(item, angle_tag, direction_tag, angle, direction, current_angle)
384
+ if !self.send(current_angle) || angle != self.send(current_angle)
385
+ self.send("#{current_angle}=", angle)
386
+ DICOM::Element.new(angle_tag, angle, :parent => item)
387
+ DICOM::Element.new(direction_tag, (direction.empty? ? 'NONE' : direction), :parent => item)
388
+ end
389
+ end
390
+
391
+ # Adds a Table Top Position element to a Control Point Item.
392
+ # Note that the element is only added if there is no 'current' attribute
393
+ # defined, or the given value is different form the current attribute.
394
+ #
395
+ # @param [DICOM::Item] item the DICOM control point item in which to create the element
396
+ # @param [String] tag the DICOM tag of the couch position element
397
+ # @param [String, NilClass] value the couch position
398
+ # @param [Symbol] current the instance variable that keeps track of the current value of this attribute
399
+ #
400
+ def add_couch_position(item, tag, value, current)
401
+ if !self.send(current) || value != self.send(current)
402
+ self.send("#{current}=", value)
403
+ DICOM::Element.new(tag, (value.empty? ? '' : value.to_f * 10), :parent => item)
404
+ end
405
+ end
406
+
407
+ # Adds a Dose Rate Set element to a Control Point Item.
408
+ # Note that the element is only added if there is no 'current' attribute
409
+ # defined, or the given value is different form the current attribute.
410
+ #
411
+ # @param [String, NilClass] value the doserate attribute
412
+ # @param [DICOM::Item] item the DICOM control point item in which to create an element
413
+ #
414
+ def add_doserate(value, item)
415
+ if !@current_doserate || value != @current_doserate
416
+ @current_doserate = value
417
+ DICOM::Element.new('300A,0115', value, :parent => item)
418
+ end
419
+ end
420
+
421
+ # Adds a Nominal Beam Energy element to a Control Point Item.
422
+ # Note that the element is only added if there is no 'current' attribute
423
+ # defined, or the given value is different form the current attribute.
424
+ #
425
+ # @param [String, NilClass] value the energy attribute
426
+ # @param [DICOM::Item] item the DICOM control point item in which to create an element
427
+ #
428
+ def add_energy(value, item)
429
+ if !@current_energy || value != @current_energy
430
+ @current_energy = value
431
+ DICOM::Element.new('300A,0114', "#{value.to_f}", :parent => item)
432
+ end
433
+ end
434
+
435
+ # Adds an Isosenter element to a Control Point Item.
436
+ # Note that the element is only added if there is a Site Setup record present,
437
+ # and it contains a real (non-empty) value. Also, the element is only added if there
438
+ # is no 'current' attribute defined, or the given value is different form the current attribute.
439
+ #
440
+ # @param [SiteSetup, NilClass] site_setup the associated site setup record
441
+ # @param [DICOM::Item] item the DICOM control point item in which to create an element
442
+ #
443
+ def add_isosenter(site_setup, item)
444
+ if site_setup
445
+ # Create an element if the value is new or unique:
446
+ if !@current_isosenter
447
+ 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)}"
448
+ if iso != @current_isosenter
449
+ @current_isosenter = iso
450
+ DICOM::Element.new('300A,012C', iso, :parent => item)
451
+ end
452
+ end
453
+ else
454
+ # Log a warning if this is the first control point:
455
+ unless @current_isosenter
456
+ logger.warn("No Site Setup record exists for this plan. Unable to provide an isosenter position.")
457
+ end
458
+ end
459
+ end
460
+
461
+ # Adds a Source to Surface Distance element to a Control Point Item.
462
+ # Note that the element is only added if the SSD attribute contains
463
+ # real (non-empty) value.
464
+ #
465
+ # @param [String, NilClass] value the SSD attribute
466
+ # @param [DICOM::Item] item the DICOM control point item in which to create an element
467
+ #
468
+ def add_ssd(value, item)
469
+ DICOM::Element.new('300A,0130', "#{value.to_f * 10}", :parent => item) if value && !value.empty?
470
+ end
471
+
472
+ # Creates a control point item in the given control point sequence, based
473
+ # on an RTP control point record.
474
+ #
475
+ # @param [ControlPoint] cp the RTP ControlPoint record to convert
476
+ # @param [DICOM::Sequence] sequence the DICOM parent sequence of the item to be created
477
+ # @param [Hash] options the options to use for creating the control point
478
+ # @option options [Boolean] :dose_ref if set, a Referenced Dose Reference sequence will be included in the generated control point item
479
+ # @return [DICOM::Item] the constructed control point DICOM item
480
+ #
481
+ def create_control_point(cp, sequence, options={})
482
+ cp_item = DICOM::Item.new(:parent => sequence)
483
+ # Some CP attributes will always be written (CP index, BLD positions & Cumulative meterset weight).
484
+ # The other attributes are only written if they are different from the previous control point.
485
+ # Control Point Index:
486
+ DICOM::Element.new('300A,0112', "#{cp.index}", :parent => cp_item)
487
+ # Beam Limiting Device Position Sequence:
488
+ create_beam_limiting_device_positions(cp_item, cp, options)
489
+ # Source to Surface Distance:
490
+ add_ssd(cp.ssd, cp_item)
491
+ # Cumulative Meterset Weight:
492
+ DICOM::Element.new('300A,0134', cp.monitor_units.to_f, :parent => cp_item)
493
+ # Referenced Dose Reference Sequence:
494
+ create_referenced_dose_reference(cp_item) if options[:dose_ref]
495
+ # Attributes that are only added if they carry an updated value:
496
+ # Nominal Beam Energy:
497
+ add_energy(cp.energy, cp_item)
498
+ # Dose Rate Set:
499
+ add_doserate(cp.doserate, cp_item)
500
+ # Gantry Angle & Rotation Direction:
501
+ add_angle(cp_item, '300A,011E', '300A,011F', cp.gantry_angle, cp.gantry_dir, :current_gantry)
502
+ # Beam Limiting Device Angle & Rotation Direction:
503
+ add_angle(cp_item, '300A,0120', '300A,0121', cp.collimator_angle, cp.collimator_dir, :current_collimator)
504
+ # Patient Support Angle & Rotation Direction:
505
+ add_angle(cp_item, '300A,0122', '300A,0123', cp.couch_pedestal, cp.couch_ped_dir, :current_couch_pedestal)
506
+ # Table Top Eccentric Angle & Rotation Direction:
507
+ add_angle(cp_item, '300A,0125', '300A,0126', cp.couch_angle, cp.couch_dir, :current_couch_angle)
508
+ # Table Top Vertical Position:
509
+ add_couch_position(cp_item, '300A,0128', cp.couch_vertical, :current_couch_vertical)
510
+ # Table Top Longitudinal Position:
511
+ add_couch_position(cp_item, '300A,0129', cp.couch_longitudinal, :current_couch_longitudinal)
512
+ # Table Top Lateral Position:
513
+ add_couch_position(cp_item, '300A,012A', cp.couch_lateral, :current_couch_lateral)
514
+ # Isocenter Position (x\y\z):
515
+ add_isosenter(cp.parent.parent.site_setup, cp_item)
516
+ cp_item
517
+ end
518
+
519
+ # Creates a beam limiting device sequence in the given DICOM object.
520
+ #
521
+ # @param [DICOM::Item] beam_item the DICOM beam item in which to insert the sequence
522
+ # @param [Field] field the RTP field to fetch device parameters from
523
+ # @return [DICOM::Sequence] the constructed beam limiting device sequence
524
+ #
525
+ def create_beam_limiting_devices(beam_item, field)
526
+ bl_seq = DICOM::Sequence.new('300A,00B6', :parent => beam_item)
527
+ # The ASYMX item ('backup jaws') doesn't exist on all models:
528
+ if ['SYM', 'ASY'].include?(field.field_x_mode.upcase)
529
+ bl_item_x = DICOM::Item.new(:parent => bl_seq)
530
+ DICOM::Element.new('300A,00B8', "ASYMX", :parent => bl_item_x)
531
+ DICOM::Element.new('300A,00BC', "1", :parent => bl_item_x)
532
+ end
533
+ # The ASYMY item is always created:
534
+ bl_item_y = DICOM::Item.new(:parent => bl_seq)
535
+ # RT Beam Limiting Device Type:
536
+ DICOM::Element.new('300A,00B8', "ASYMY", :parent => bl_item_y)
537
+ # Number of Leaf/Jaw Pairs:
538
+ DICOM::Element.new('300A,00BC', "1", :parent => bl_item_y)
539
+ # MLCX item is only created if leaves are defined:
540
+ # (NB: The RTP file doesn't specify leaf position boundaries, so we
541
+ # have to set these based on a set of known MLC types, their number
542
+ # of leaves, and their leaf boundary positions.)
543
+ if field.control_points.length > 0
544
+ bl_item_mlcx = DICOM::Item.new(:parent => bl_seq)
545
+ DICOM::Element.new('300A,00B8', "MLCX", :parent => bl_item_mlcx)
546
+ num_leaves = field.control_points.first.mlc_leaves.to_i
547
+ DICOM::Element.new('300A,00BC', num_leaves.to_s, :parent => bl_item_mlcx)
548
+ DICOM::Element.new('300A,00BE', "#{RTP.leaf_boundaries(num_leaves).join("\\")}", :parent => bl_item_mlcx)
549
+ end
550
+ bl_seq
551
+ end
552
+
553
+ # Creates a beam limiting device positions sequence in the given DICOM object.
554
+ #
555
+ # @param [DICOM::Item] cp_item the DICOM control point item in which to insert the sequence
556
+ # @param [ControlPoint] cp the RTP control point to fetch device parameters from
557
+ # @return [DICOM::Sequence] the constructed beam limiting device positions sequence
558
+ #
559
+ def create_beam_limiting_device_positions(cp_item, cp, options={})
560
+ dp_seq = DICOM::Sequence.new('300A,011A', :parent => cp_item)
561
+ # The ASYMX item ('backup jaws') doesn't exist on all models:
562
+ if ['SYM', 'ASY'].include?(cp.parent.field_x_mode.upcase)
563
+ dp_item_x = create_asym_item(cp, dp_seq, axis=:x, options)
564
+ end
565
+ # Always create one ASYMY item:
566
+ dp_item_y = create_asym_item(cp, dp_seq, axis=:y, options)
567
+ # MLCX:
568
+ dp_item_mlcx = DICOM::Item.new(:parent => dp_seq)
569
+ # RT Beam Limiting Device Type:
570
+ DICOM::Element.new('300A,00B8', "MLCX", :parent => dp_item_mlcx)
571
+ # Leaf/Jaw Positions:
572
+ DICOM::Element.new('300A,011C', cp.dcm_mlc_positions(options[:scale]), :parent => dp_item_mlcx)
573
+ dp_seq
574
+ end
575
+
576
+ # Creates an ASYMX or ASYMY item.
577
+ #
578
+ # @param [ControlPoint] cp the RTP control point to fetch device parameters from
579
+ # @param [DICOM::Sequence] dcm_parent the DICOM sequence in which to insert the item
580
+ # @param [Symbol] axis the axis for the item (:x or :y)
581
+ # @return [DICOM::Item] the constructed ASYMX or ASYMY item
582
+ #
583
+ def create_asym_item(cp, dcm_parent, axis, options={})
584
+ val1 = cp.send("dcm_collimator_#{axis.to_s}1", options[:scale])
585
+ val2 = cp.send("dcm_collimator_#{axis.to_s}2", options[:scale])
586
+ item = DICOM::Item.new(:parent => dcm_parent)
587
+ # RT Beam Limiting Device Type:
588
+ DICOM::Element.new('300A,00B8', "ASYM#{axis.to_s.upcase}", :parent => item)
589
+ # Leaf/Jaw Positions:
590
+ DICOM::Element.new('300A,011C', "#{val1}\\#{val2}", :parent => item)
591
+ item
592
+ end
593
+
594
+ # Creates a beam limiting device positions sequence in the given DICOM object.
595
+ #
596
+ # @param [DICOM::Item] cp_item the DICOM control point item in which to insert the sequence
597
+ # @param [Field] field the RTP treatment field to fetch device parameters from
598
+ # @return [DICOM::Sequence] the constructed beam limiting device positions sequence
599
+ #
600
+ def create_beam_limiting_device_positions_from_field(cp_item, field, options={})
601
+ dp_seq = DICOM::Sequence.new('300A,011A', :parent => cp_item)
602
+ # ASYMX:
603
+ dp_item_x = DICOM::Item.new(:parent => dp_seq)
604
+ DICOM::Element.new('300A,00B8', "ASYMX", :parent => dp_item_x)
605
+ DICOM::Element.new('300A,011C', "#{field.dcm_collimator_x1}\\#{field.dcm_collimator_x2}", :parent => dp_item_x)
606
+ # ASYMY:
607
+ dp_item_y = DICOM::Item.new(:parent => dp_seq)
608
+ DICOM::Element.new('300A,00B8', "ASYMY", :parent => dp_item_y)
609
+ DICOM::Element.new('300A,011C', "#{field.dcm_collimator_y1}\\#{field.dcm_collimator_y2}", :parent => dp_item_y)
610
+ dp_seq
611
+ end
612
+
613
+ # Creates a dose reference sequence in the given DICOM object.
614
+ #
615
+ # @param [DICOM::DObject] dcm the DICOM object in which to insert the sequence
616
+ # @param [String] description the value to use for Dose Reference Description
617
+ # @return [DICOM::Sequence] the constructed dose reference sequence
618
+ #
619
+ def create_dose_reference(dcm, description)
620
+ dr_seq = DICOM::Sequence.new('300A,0010', :parent => dcm)
621
+ dr_item = DICOM::Item.new(:parent => dr_seq)
622
+ # Dose Reference Number:
623
+ DICOM::Element.new('300A,0012', '1', :parent => dr_item)
624
+ # Dose Reference Structure Type:
625
+ DICOM::Element.new('300A,0014', 'SITE', :parent => dr_item)
626
+ # Dose Reference Description:
627
+ DICOM::Element.new('300A,0016', description, :parent => dr_item)
628
+ # Dose Reference Type:
629
+ DICOM::Element.new('300A,0020', 'TARGET', :parent => dr_item)
630
+ dr_seq
631
+ end
632
+
633
+ # Creates a referenced dose reference sequence in the given DICOM object.
634
+ #
635
+ # @param [DICOM::Item] cp_item the DICOM item in which to insert the sequence
636
+ # @return [DICOM::Sequence] the constructed referenced dose reference sequence
637
+ #
638
+ def create_referenced_dose_reference(cp_item)
639
+ # Referenced Dose Reference Sequence:
640
+ rd_seq = DICOM::Sequence.new('300C,0050', :parent => cp_item)
641
+ rd_item = DICOM::Item.new(:parent => rd_seq)
642
+ # Cumulative Dose Reference Coeffecient:
643
+ DICOM::Element.new('300A,010C', '', :parent => rd_item)
644
+ # Referenced Dose Reference Number:
645
+ DICOM::Element.new('300C,0051', '1', :parent => rd_item)
646
+ rd_seq
647
+ end
648
+
649
+ # Resets the types of control point attributes that are only written to the
650
+ # first control point item, and for following control point items only when
651
+ # they are different from the 'current' value. When a new field is reached,
652
+ # it is essential to reset these attributes, or else we could risk to start
653
+ # the field with a control point with missing attributes, if one of its first
654
+ # attributes is equal to the last attribute of the previous field.
655
+ #
656
+ def reset_cp_current_attributes
657
+ @current_gantry = nil
658
+ @current_collimator = nil
659
+ @current_couch_pedestal = nil
660
+ @current_couch_angle = nil
661
+ @current_couch_vertical = nil
662
+ @current_couch_longitudinal = nil
663
+ @current_couch_lateral = nil
664
+ @current_isosenter = nil
665
+ end
666
+
667
+ end
668
+
657
669
  end