rtp-connect 1.6 → 1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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