rtp-connect 1.6 → 1.11

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