rtp-connect 1.1 → 1.2

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.
@@ -0,0 +1,503 @@
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
+ def to_dcm
9
+ #
10
+ # FIXME: This method is rather big, with a few sections of somewhat similar, repeating code.
11
+ # Refactoring and simplifying it at some stage might be a good idea.
12
+ #
13
+ require 'dicom'
14
+ DICOM.logger.level = Logger::FATAL
15
+ p = @prescriptions.first
16
+ # If no prescription is present, we are not going to be able to make a valid DICOM object:
17
+ logger.error("No Prescription Record present. Unable to build a valid RTPLAN DICOM object.") unless p
18
+ dcm = DICOM::DObject.new
19
+ #
20
+ # TOP LEVEL TAGS:
21
+ #
22
+ # Specific Character Set:
23
+ DICOM::Element.new('0008,0005', 'ISO_IR 100', :parent => dcm)
24
+ # Instance Creation Date
25
+ DICOM::Element.new('0008,0012', Time.now.strftime("%Y%m%d"), :parent => dcm)
26
+ # Instance Creation Time:
27
+ DICOM::Element.new('0008,0013', Time.now.strftime("%H%M%S"), :parent => dcm)
28
+ # SOP Class UID:
29
+ DICOM::Element.new('0008,0016', '1.2.840.10008.5.1.4.1.1.481.5', :parent => dcm)
30
+ # SOP Instance UID (if an original UID is not present, we make up a UID):
31
+ begin
32
+ sop_uid = p.fields.first.extended_field.original_plan_uid.empty? ? DICOM.generate_uid : p.fields.first.extended_field.original_plan_uid
33
+ rescue
34
+ sop_uid = DICOM.generate_uid
35
+ end
36
+ DICOM::Element.new('0008,0018', sop_uid, :parent => dcm)
37
+ # Study Date
38
+ DICOM::Element.new('0008,0020', Time.now.strftime("%Y%m%d"), :parent => dcm)
39
+ # Study Time:
40
+ DICOM::Element.new('0008,0030', Time.now.strftime("%H%M%S"), :parent => dcm)
41
+ # Accession Number:
42
+ DICOM::Element.new('0008,0050', '', :parent => dcm)
43
+ # Modality:
44
+ DICOM::Element.new('0008,0060', 'RTPLAN', :parent => dcm)
45
+ # Manufacturer:
46
+ DICOM::Element.new('0008,0070', 'rtp-connect', :parent => dcm)
47
+ # Referring Physician's Name:
48
+ DICOM::Element.new('0008,0090', "#{@md_last_name}^#{@md_first_name}^#{@md_middle_name}^^", :parent => dcm)
49
+ # Operator's Name:
50
+ DICOM::Element.new('0008,1070', "#{@author_last_name}^#{@author_first_name}^#{@author_middle_name}^^", :parent => dcm)
51
+ # Patient's Name:
52
+ DICOM::Element.new('0010,0010', "#{@patient_last_name}^#{@patient_first_name}^#{@patient_middle_name}^^", :parent => dcm)
53
+ # Patient ID:
54
+ DICOM::Element.new('0010,0020', @patient_id, :parent => dcm)
55
+ # Patient's Birth Date:
56
+ DICOM::Element.new('0010,0030', '', :parent => dcm)
57
+ # Patient's Sex:
58
+ DICOM::Element.new('0010,0040', '', :parent => dcm)
59
+ # Manufacturer's Model Name:
60
+ DICOM::Element.new('0008,1090', 'RTP-to-DICOM', :parent => dcm)
61
+ # Software Version(s):
62
+ DICOM::Element.new('0018,1020', "RubyRTP#{VERSION}", :parent => dcm)
63
+ # Study Instance UID:
64
+ DICOM::Element.new('0020,000D', DICOM.generate_uid, :parent => dcm)
65
+ # Series Instance UID:
66
+ DICOM::Element.new('0020,000E', DICOM.generate_uid, :parent => dcm)
67
+ # Study ID:
68
+ DICOM::Element.new('0020,0010', '1', :parent => dcm)
69
+ # Series Number:
70
+ DICOM::Element.new('0020,0011', '1', :parent => dcm)
71
+ # Frame of Reference UID (if an original UID is not present, we make up a UID):
72
+ begin
73
+ for_uid = p.site_setup.frame_of_ref_uid.empty? ? DICOM.generate_uid : p.site_setup.frame_of_ref_uid
74
+ rescue
75
+ for_uid = DICOM.generate_uid
76
+ end
77
+ DICOM::Element.new('0020,0052', for_uid, :parent => dcm)
78
+ # Position Reference Indicator:
79
+ DICOM::Element.new('0020,1040', '', :parent => dcm)
80
+ # RT Plan Label (max 16 characters):
81
+ plan_label = p ? p.rx_site_name[0..15] : @course_id
82
+ DICOM::Element.new('300A,0002', plan_label, :parent => dcm)
83
+ # RT Plan Name:
84
+ plan_name = p ? p.rx_site_name : @course_id
85
+ DICOM::Element.new('300A,0003', plan_name, :parent => dcm)
86
+ # RT Plan Description:
87
+ plan_desc = p ? p.technique : @diagnosis
88
+ DICOM::Element.new('300A,0004', plan_desc, :parent => dcm)
89
+ # RT Plan Date:
90
+ plan_date = @plan_date.empty? ? Time.now.strftime("%Y%m%d") : @plan_date
91
+ DICOM::Element.new('300A,0006', plan_date, :parent => dcm)
92
+ # RT Plan Time:
93
+ plan_time = @plan_time.empty? ? Time.now.strftime("%H%M%S") : @plan_time
94
+ DICOM::Element.new('300A,0007', plan_time, :parent => dcm)
95
+ # RT Plan Geometry:
96
+ DICOM::Element.new('300A,000C', 'PATIENT', :parent => dcm)
97
+ # Approval Status:
98
+ DICOM::Element.new('300E,0002', 'UNAPPROVED', :parent => dcm)
99
+ #
100
+ # SEQUENCES:
101
+ #
102
+ #
103
+ # Referenced Structure Set Sequence:
104
+ #
105
+ ss_seq = DICOM::Sequence.new('300C,0060', :parent => dcm)
106
+ ss_item = DICOM::Item.new(:parent => ss_seq)
107
+ # Referenced SOP Class UID:
108
+ DICOM::Element.new('0008,1150', '1.2.840.10008.5.1.4.1.1.481.3', :parent => ss_item)
109
+ # Referenced SOP Instance UID (if an original UID is not present, we make up a UID):
110
+ begin
111
+ ref_ss_uid = p.site_setup.structure_set_uid.empty? ? DICOM.generate_uid : p.site_setup.structure_set_uid
112
+ rescue
113
+ ref_ss_uid = DICOM.generate_uid
114
+ end
115
+ DICOM::Element.new('0008,1155', ref_ss_uid, :parent => ss_item)
116
+ #
117
+ # Patient Setup Sequence:
118
+ #
119
+ ps_seq = DICOM::Sequence.new('300A,0180', :parent => dcm)
120
+ ps_item = DICOM::Item.new(:parent => ps_seq)
121
+ # Patient Position:
122
+ begin
123
+ pat_pos = p.site_setup.patient_orientation.empty? ? 'HFS' : p.site_setup.patient_orientation
124
+ rescue
125
+ pat_pos = 'HFS'
126
+ end
127
+ DICOM::Element.new('0018,5100', pat_pos, :parent => ps_item)
128
+ # Patient Setup Number:
129
+ DICOM::Element.new('300A,0182', '1', :parent => ps_item)
130
+ # Setup Technique (assume Isocentric):
131
+ DICOM::Element.new('300A,01B0', 'ISOCENTRIC', :parent => ps_item)
132
+ #
133
+ # Dose Reference Sequence:
134
+ #
135
+ dr_seq = DICOM::Sequence.new('300A,0010', :parent => dcm)
136
+ dr_item = DICOM::Item.new(:parent => dr_seq)
137
+ # Dose Reference Number:
138
+ DICOM::Element.new('300A,0012', '1', :parent => dr_item)
139
+ # Dose Reference Structure Type:
140
+ DICOM::Element.new('300A,0014', 'SITE', :parent => dr_item)
141
+ # Dose Reference Description:
142
+ DICOM::Element.new('300A,0016', plan_name, :parent => dr_item)
143
+ # Dose Reference Type:
144
+ DICOM::Element.new('300A,0020', 'TARGET', :parent => dr_item)
145
+ #
146
+ # Fraction Group Sequence:
147
+ #
148
+ fg_seq = DICOM::Sequence.new('300A,0070', :parent => dcm)
149
+ fg_item = DICOM::Item.new(:parent => fg_seq)
150
+ # Fraction Group Number:
151
+ DICOM::Element.new('300A,0071', '1', :parent => fg_item)
152
+ # Number of Fractions Planned (try to derive from total dose/fraction dose, or use 1 as default):
153
+ begin
154
+ num_frac = p.dose_ttl.empty? || p.dose_tx.empty? ? '1' : (p.dose_ttl.to_i / p.dose_tx.to_f).round.to_s
155
+ rescue
156
+ num_frac = '0'
157
+ end
158
+ DICOM::Element.new('300A,0078', num_frac, :parent => fg_item)
159
+ # Number of Beams:
160
+ num_beams = p ? p.fields.length : 0
161
+ DICOM::Element.new('300A,0080', "#{num_beams}", :parent => fg_item)
162
+ # Number of Brachy Application Setups:
163
+ DICOM::Element.new('300A,00A0', "0", :parent => fg_item)
164
+ # Referenced Beam Sequence (items created for each beam below):
165
+ rb_seq = DICOM::Sequence.new('300C,0004', :parent => fg_item)
166
+ #
167
+ # Beam Sequence:
168
+ #
169
+ b_seq = DICOM::Sequence.new('300A,00B0', :parent => dcm)
170
+ if p
171
+ # If no fields are present, we are not going to be able to make a valid DICOM object:
172
+ logger.error("No Field Record present. Unable to build a valid RTPLAN DICOM object.") unless p.fields.length > 0
173
+ p.fields.each_with_index do |field, i|
174
+ # If this is an electron beam, a warning should be printed, as these are less reliably converted:
175
+ 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'
176
+ # Beam number and name:
177
+ beam_number = field.extended_field ? field.extended_field.original_beam_number : (i + 1).to_s
178
+ beam_name = field.extended_field ? field.extended_field.original_beam_name : field.field_name
179
+ # Ref Beam Item:
180
+ rb_item = DICOM::Item.new(:parent => rb_seq)
181
+ # Beam Dose (convert from cGy to Gy):
182
+ field_dose = field.field_dose.empty? ? '' : (field.field_dose.to_f * 0.01).round(4).to_s
183
+ DICOM::Element.new('300A,0084', field_dose, :parent => rb_item)
184
+ # Beam Meterset:
185
+ DICOM::Element.new('300A,0086', field.field_monitor_units, :parent => rb_item)
186
+ # Referenced Beam Number:
187
+ DICOM::Element.new('300C,0006', beam_number, :parent => rb_item)
188
+ # Beam Item:
189
+ b_item = DICOM::Item.new(:parent => b_seq)
190
+ # Treatment Machine Name (max 16 characters):
191
+ DICOM::Element.new('300A,00B2', field.treatment_machine[0..15], :parent => b_item)
192
+ # Primary Dosimeter Unit:
193
+ DICOM::Element.new('300A,00B3', 'MU', :parent => b_item)
194
+ # Source-Axis Distance (convert to mm):
195
+ DICOM::Element.new('300A,00B4', "#{field.sad.to_f * 10}", :parent => b_item)
196
+ # Beam Number:
197
+ DICOM::Element.new('300A,00C0', beam_number, :parent => b_item)
198
+ # Beam Name:
199
+ DICOM::Element.new('300A,00C2', beam_name, :parent => b_item)
200
+ # Beam Description:
201
+ DICOM::Element.new('300A,00C3', field.field_note, :parent => b_item)
202
+ # Beam Type:
203
+ beam_type = case field.treatment_type
204
+ when 'Static' then 'STATIC'
205
+ when 'StepNShoot' then 'STATIC'
206
+ else logger.error("The beam type (treatment type) #{field.treatment_type} is not yet supported.")
207
+ end
208
+ DICOM::Element.new('300A,00C4', beam_type, :parent => b_item)
209
+ # Radiation Type:
210
+ rad_type = case field.modality
211
+ when 'Elect' then 'ELECTRON'
212
+ when 'Xrays' then 'PHOTON'
213
+ else logger.error("The radiation type (modality) #{field.modality} is not yet supported.")
214
+ end
215
+ DICOM::Element.new('300A,00C6', rad_type, :parent => b_item)
216
+ # Treatment Delivery Type:
217
+ DICOM::Element.new('300A,00CE', 'TREATMENT', :parent => b_item)
218
+ # Number of Wedges:
219
+ DICOM::Element.new('300A,00D0', (field.wedge.empty? ? '0' : '1'), :parent => b_item)
220
+ # Number of Compensators:
221
+ DICOM::Element.new('300A,00E0', (field.compensator.empty? ? '0' : '1'), :parent => b_item)
222
+ # Number of Boli:
223
+ DICOM::Element.new('300A,00ED', (field.bolus.empty? ? '0' : '1'), :parent => b_item)
224
+ # Number of Blocks:
225
+ DICOM::Element.new('300A,00F0', (field.block.empty? ? '0' : '1'), :parent => b_item)
226
+ # Final Cumulative Meterset Weight:
227
+ DICOM::Element.new('300A,010E', field.field_monitor_units, :parent => b_item)
228
+ # Number of Control Points:
229
+ DICOM::Element.new('300A,0110', "#{field.control_points.length}", :parent => b_item)
230
+ # Referenced Patient Setup Number:
231
+ DICOM::Element.new('300C,006A', '1', :parent => b_item)
232
+ #
233
+ # Beam Limiting Device Sequence:
234
+ #
235
+ bl_seq = DICOM::Sequence.new('300A,00B6', :parent => b_item)
236
+ # Always create one ASYMX and one ASYMY item:
237
+ bl_item_x = DICOM::Item.new(:parent => bl_seq)
238
+ bl_item_y = DICOM::Item.new(:parent => bl_seq)
239
+ # RT Beam Limiting Device Type:
240
+ DICOM::Element.new('300A,00B8', "ASYMX", :parent => bl_item_x)
241
+ DICOM::Element.new('300A,00B8', "ASYMY", :parent => bl_item_y)
242
+ # Number of Leaf/Jaw Pairs:
243
+ DICOM::Element.new('300A,00BC', "1", :parent => bl_item_x)
244
+ DICOM::Element.new('300A,00BC', "1", :parent => bl_item_y)
245
+ # MLCX item is only created if leaves are defined:
246
+ # (NB: The RTP file doesn't specify leaf position boundaries, so for now we estimate these positions
247
+ # based on the (even) number of leaves and the assumptions of a 200 mm position of the outer leaf)
248
+ # FIXME: In the future, the MLCX leaf position boundary should be configurable - i.e. an option argument of to_dcm().
249
+ if field.control_points.length > 0
250
+ bl_item_mlcx = DICOM::Item.new(:parent => bl_seq)
251
+ DICOM::Element.new('300A,00B8', "MLCX", :parent => bl_item_mlcx)
252
+ num_leaves = field.control_points.first.mlc_leaves.to_i
253
+ logger.warn("Support for odd number of leaves (#{num_leaves}) is not implemented yet. Leaf Position Boundaries tag will be incorrect.") if num_leaves.odd?
254
+ logger.warn("Untested number of leaves encountered: #{num_leaves} Leaf Position Boundaries tag may be incorrect.") if num_leaves.even? && ![40, 80].include?(num_leaves)
255
+ DICOM::Element.new('300A,00BC', num_leaves.to_s, :parent => bl_item_mlcx)
256
+ pos_boundaries = Array.new(num_leaves) {|i| i * 400 / num_leaves.to_f - 200}
257
+ DICOM::Element.new('300A,00BE', "#{pos_boundaries.join("\\")}", :parent => bl_item_mlcx)
258
+ end
259
+ #
260
+ # Block Sequence (if any):
261
+ # FIXME: It seems that the Block Sequence (300A,00F4) may be
262
+ # difficult (impossible?) to reconstruct based on the RTP file's
263
+ # information, and thus it is skipped altogether.
264
+ #
265
+ #
266
+ # Applicator Sequence (if any):
267
+ #
268
+ unless field.e_applicator.empty?
269
+ app_seq = DICOM::Sequence.new('300A,0107', :parent => b_item)
270
+ app_item = DICOM::Item.new(:parent => app_seq)
271
+ # Applicator ID:
272
+ DICOM::Element.new('300A,0108', field.e_field_def_aperture, :parent => app_item)
273
+ # Applicator Type:
274
+ DICOM::Element.new('300A,0109', "ELECTRON_#{field.e_applicator.upcase}", :parent => app_item)
275
+ # Applicator Description:
276
+ DICOM::Element.new('300A,010A', "Appl. #{field.e_field_def_aperture}", :parent => app_item)
277
+ end
278
+ #
279
+ # Control Point Sequence:
280
+ #
281
+ # A field may have 0 (no MLC), 1 (conventional beam with MLC) or 2n (IMRT) control points.
282
+ # The DICOM file shall always contain 2n control points (minimum 2).
283
+ #
284
+ cp_seq = DICOM::Sequence.new('300A,0111', :parent => b_item)
285
+ if field.control_points.length < 2
286
+ # When we have 0 or 1 control point, use settings from field, and insert MLC settings if present:
287
+ # First CP:
288
+ cp_item = DICOM::Item.new(:parent => cp_seq)
289
+ # Control Point Index:
290
+ DICOM::Element.new('300A,0112', "0", :parent => cp_item)
291
+ # Nominal Beam Energy:
292
+ DICOM::Element.new('300A,0114', "#{field.energy.to_f}", :parent => cp_item)
293
+ # Dose Rate Set:
294
+ DICOM::Element.new('300A,0115', field.doserate, :parent => cp_item)
295
+ # Gantry Angle:
296
+ DICOM::Element.new('300A,011E', field.gantry_angle, :parent => cp_item)
297
+ # Gantry Rotation Direction:
298
+ DICOM::Element.new('300A,011F', (field.arc_direction.empty? ? 'NONE' : field.arc_direction), :parent => cp_item)
299
+ # Beam Limiting Device Angle:
300
+ DICOM::Element.new('300A,0120', field.collimator_angle, :parent => cp_item)
301
+ # Beam Limiting Device Rotation Direction:
302
+ DICOM::Element.new('300A,0121', 'NONE', :parent => cp_item)
303
+ # Patient Support Angle:
304
+ DICOM::Element.new('300A,0122', field.couch_pedestal, :parent => cp_item)
305
+ # Patient Support Rotation Direction:
306
+ DICOM::Element.new('300A,0123', 'NONE', :parent => cp_item)
307
+ # Table Top Eccentric Angle:
308
+ DICOM::Element.new('300A,0125', field.couch_angle, :parent => cp_item)
309
+ # Table Top Eccentric Rotation Direction:
310
+ DICOM::Element.new('300A,0126', 'NONE', :parent => cp_item)
311
+ # Table Top Vertical Position:
312
+ couch_vert = field.couch_vertical.empty? ? '' : (field.couch_vertical.to_f * 10).to_s
313
+ DICOM::Element.new('300A,0128', couch_vert, :parent => cp_item)
314
+ # Table Top Longitudinal Position:
315
+ couch_long = field.couch_longitudinal.empty? ? '' : (field.couch_longitudinal.to_f * 10).to_s
316
+ DICOM::Element.new('300A,0129', couch_long, :parent => cp_item)
317
+ # Table Top Lateral Position:
318
+ couch_lat = field.couch_lateral.empty? ? '' : (field.couch_lateral.to_f * 10).to_s
319
+ DICOM::Element.new('300A,012A', couch_lat, :parent => cp_item)
320
+ # Isocenter Position (x\y\z):
321
+ 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)
322
+ # Source to Surface Distance:
323
+ DICOM::Element.new('300A,0130', "#{field.ssd.to_f * 10}", :parent => cp_item)
324
+ # Cumulative Meterset Weight:
325
+ DICOM::Element.new('300A,0134', "0.0", :parent => cp_item)
326
+ # Beam Limiting Device Position Sequence:
327
+ dp_seq = DICOM::Sequence.new('300A,011A', :parent => cp_item)
328
+ # Always create one ASYMX and one ASYMY item:
329
+ dp_item_x = DICOM::Item.new(:parent => dp_seq)
330
+ dp_item_y = DICOM::Item.new(:parent => dp_seq)
331
+ # RT Beam Limiting Device Type:
332
+ DICOM::Element.new('300A,00B8', "ASYMX", :parent => dp_item_x)
333
+ DICOM::Element.new('300A,00B8', "ASYMY", :parent => dp_item_y)
334
+ # Leaf/Jaw Positions:
335
+ DICOM::Element.new('300A,011C', "#{field.collimator_x1.to_f * 10}\\#{field.collimator_x2.to_f * 10}", :parent => dp_item_x)
336
+ DICOM::Element.new('300A,011C', "#{field.collimator_y1.to_f * 10}\\#{field.collimator_y2.to_f * 10}", :parent => dp_item_y)
337
+ # MLCX:
338
+ if field.control_points.length > 0
339
+ dp_item_mlcx = DICOM::Item.new(:parent => dp_seq)
340
+ # RT Beam Limiting Device Type:
341
+ DICOM::Element.new('300A,00B8', "MLCX", :parent => dp_item_mlcx)
342
+ # Leaf/Jaw Positions:
343
+ pos_a = field.control_points.first.mlc_lp_a.collect{|p| (p.to_f * 10).round(2) unless p.empty?}.compact
344
+ pos_b = field.control_points.first.mlc_lp_b.collect{|p| (p.to_f * 10).round(2) unless p.empty?}.compact
345
+ leaf_pos = "#{pos_a.join("\\")}\\#{pos_b.join("\\")}"
346
+ DICOM::Element.new('300A,011C', leaf_pos, :parent => dp_item_mlcx)
347
+ end
348
+ # Referenced Dose Reference Sequence:
349
+ rd_seq = DICOM::Sequence.new('300C,0050', :parent => cp_item)
350
+ rd_item = DICOM::Item.new(:parent => rd_seq)
351
+ # Cumulative Dose Reference Coeffecient:
352
+ DICOM::Element.new('300A,010C', '', :parent => rd_item)
353
+ # Referenced Dose Reference Number:
354
+ DICOM::Element.new('300C,0051', '1', :parent => rd_item)
355
+ # Second CP:
356
+ cp_item = DICOM::Item.new(:parent => cp_seq)
357
+ # Control Point Index:
358
+ DICOM::Element.new('300A,0112', "1", :parent => cp_item)
359
+ # Cumulative Meterset Weight:
360
+ DICOM::Element.new('300A,0134', field.field_monitor_units, :parent => cp_item)
361
+ else
362
+ # When we have multiple (2n) control points, iterate and pick settings from the CPs:
363
+ field.control_points.each_slice(2) do |cp1, cp2|
364
+ cp_item1 = DICOM::Item.new(:parent => cp_seq)
365
+ cp_item2 = DICOM::Item.new(:parent => cp_seq)
366
+ # First control point:
367
+ # Control Point Index:
368
+ DICOM::Element.new('300A,0112', "#{cp1.index}", :parent => cp_item1)
369
+ # Nominal Beam Energy:
370
+ DICOM::Element.new('300A,0114', "#{cp1.energy.to_f}", :parent => cp_item1)
371
+ # Dose Rate Set:
372
+ DICOM::Element.new('300A,0115', cp1.doserate, :parent => cp_item1)
373
+ # Gantry Angle:
374
+ DICOM::Element.new('300A,011E', cp1.gantry_angle, :parent => cp_item1)
375
+ # Gantry Rotation Direction:
376
+ DICOM::Element.new('300A,011F', (cp1.gantry_dir.empty? ? 'NONE' : cp1.gantry_dir), :parent => cp_item1)
377
+ # Beam Limiting Device Angle:
378
+ DICOM::Element.new('300A,0120', cp1.collimator_angle, :parent => cp_item1)
379
+ # Beam Limiting Device Rotation Direction:
380
+ DICOM::Element.new('300A,0121', (cp1.collimator_dir.empty? ? 'NONE' : cp1.collimator_dir), :parent => cp_item1)
381
+ # Patient Support Angle:
382
+ DICOM::Element.new('300A,0122', cp1.couch_pedestal, :parent => cp_item1)
383
+ # Patient Support Rotation Direction:
384
+ DICOM::Element.new('300A,0123', (cp1.couch_ped_dir.empty? ? 'NONE' : cp1.couch_ped_dir), :parent => cp_item1)
385
+ # Table Top Eccentric Angle:
386
+ DICOM::Element.new('300A,0125', cp1.couch_angle, :parent => cp_item1)
387
+ # Table Top Eccentric Rotation Direction:
388
+ DICOM::Element.new('300A,0126', (cp1.couch_dir.empty? ? 'NONE' : cp1.couch_dir), :parent => cp_item1)
389
+ # Table Top Vertical Position:
390
+ couch_vert = cp1.couch_vertical.empty? ? '' : (cp1.couch_vertical.to_f * 10).to_s
391
+ DICOM::Element.new('300A,0128', couch_vert, :parent => cp_item1)
392
+ # Table Top Longitudinal Position:
393
+ couch_long = cp1.couch_longitudinal.empty? ? '' : (cp1.couch_longitudinal.to_f * 10).to_s
394
+ DICOM::Element.new('300A,0129', couch_long, :parent => cp_item1)
395
+ # Table Top Lateral Position:
396
+ couch_lat = cp1.couch_lateral.empty? ? '' : (cp1.couch_lateral.to_f * 10).to_s
397
+ DICOM::Element.new('300A,012A', couch_lat, :parent => cp_item1)
398
+ # Isocenter Position (x\y\z):
399
+ DICOM::Element.new('300A,012C', "#{(p.site_setup.iso_pos_x.to_f * 10).round(2)}\\#{(p.site_setup.iso_pos_y.to_f * 10).round(2)}\\#{(p.site_setup.iso_pos_z.to_f * 10).round(2)}", :parent => cp_item1)
400
+ # Source to Surface Distance:
401
+ DICOM::Element.new('300A,0130', "#{cp1.ssd.to_f * 10}", :parent => cp_item1)
402
+ # Cumulative Meterset Weight:
403
+ mu_weight = (cp1.monitor_units.to_f * field.field_monitor_units.to_f).round(4)
404
+ DICOM::Element.new('300A,0134', "#{mu_weight}", :parent => cp_item1)
405
+ # Beam Limiting Device Position Sequence:
406
+ dp_seq = DICOM::Sequence.new('300A,011A', :parent => cp_item1)
407
+ # Always create one ASYMX and one ASYMY item:
408
+ dp_item_x = DICOM::Item.new(:parent => dp_seq)
409
+ dp_item_y = DICOM::Item.new(:parent => dp_seq)
410
+ # RT Beam Limiting Device Type:
411
+ DICOM::Element.new('300A,00B8', "ASYMX", :parent => dp_item_x)
412
+ DICOM::Element.new('300A,00B8', "ASYMY", :parent => dp_item_y)
413
+ # Leaf/Jaw Positions:
414
+ DICOM::Element.new('300A,011C', "#{field.collimator_x1.to_f * 10}\\#{field.collimator_x2.to_f * 10}", :parent => dp_item_x)
415
+ DICOM::Element.new('300A,011C', "#{field.collimator_y1.to_f * 10}\\#{field.collimator_y2.to_f * 10}", :parent => dp_item_y)
416
+ # MLCX:
417
+ dp_item_mlcx = DICOM::Item.new(:parent => dp_seq)
418
+ # RT Beam Limiting Device Type:
419
+ DICOM::Element.new('300A,00B8', "MLCX", :parent => dp_item_mlcx)
420
+ # Leaf/Jaw Positions:
421
+ pos_a = cp1.mlc_lp_a.collect{|p| (p.to_f * 10).round(2) unless p.empty?}.compact
422
+ pos_b = cp1.mlc_lp_b.collect{|p| (p.to_f * 10).round(2) unless p.empty?}.compact
423
+ leaf_pos = "#{pos_a.join("\\")}\\#{pos_b.join("\\")}"
424
+ DICOM::Element.new('300A,011C', leaf_pos, :parent => dp_item_mlcx)
425
+ # Referenced Dose Reference Sequence:
426
+ rd_seq = DICOM::Sequence.new('300C,0050', :parent => cp_item1)
427
+ rd_item = DICOM::Item.new(:parent => rd_seq)
428
+ # Cumulative Dose Reference Coeffecient:
429
+ DICOM::Element.new('300A,010C', '', :parent => rd_item)
430
+ # Referenced Dose Reference Number:
431
+ DICOM::Element.new('300C,0051', '1', :parent => rd_item)
432
+ # Second control point:
433
+ # Always include index and cumulative weight:
434
+ DICOM::Element.new('300A,0112', "#{cp2.index}", :parent => cp_item2)
435
+ mu_weight = (cp2.monitor_units.to_f * field.field_monitor_units.to_f).round(4)
436
+ DICOM::Element.new('300A,0134', "#{mu_weight}", :parent => cp_item2)
437
+ # The other parameters are only included if they have changed from the previous control point:
438
+ # Nominal Beam Energy:
439
+ DICOM::Element.new('300A,0114', "#{cp2.energy.to_f}", :parent => cp_item2) if cp2.energy != cp1.energy
440
+ # Dose Rate Set:
441
+ DICOM::Element.new('300A,0115', cp1.doserate, :parent => cp_item2) if cp2.doserate != cp1.doserate
442
+ # Gantry Angle:
443
+ DICOM::Element.new('300A,011E', cp2.gantry_angle, :parent => cp_item2) if cp2.gantry_angle != cp1.gantry_angle
444
+ # Gantry Rotation Direction:
445
+ DICOM::Element.new('300A,011F', (cp2.gantry_dir.empty? ? 'NONE' : cp2.gantry_dir), :parent => cp_item2) if cp2.gantry_dir != cp1.gantry_dir
446
+ # Beam Limiting Device Angle:
447
+ DICOM::Element.new('300A,0120', cp2.collimator_angle, :parent => cp_item2) if cp2.collimator_angle != cp1.collimator_angle
448
+ # Beam Limiting Device Rotation Direction:
449
+ DICOM::Element.new('300A,0121', (cp2.collimator_dir.empty? ? 'NONE' : cp2.collimator_dir), :parent => cp_item2) if cp2.collimator_dir != cp1.collimator_dir
450
+ # Patient Support Angle:
451
+ DICOM::Element.new('300A,0122', cp2.couch_pedestal, :parent => cp_item2) if cp2.couch_pedestal != cp1.couch_pedestal
452
+ # Patient Support Rotation Direction:
453
+ DICOM::Element.new('300A,0123', (cp2.couch_ped_dir.empty? ? 'NONE' : cp2.couch_ped_dir), :parent => cp_item2) if cp2.couch_ped_dir != cp1.couch_ped_dir
454
+ # Table Top Eccentric Angle:
455
+ DICOM::Element.new('300A,0125', cp2.couch_angle, :parent => cp_item2) if cp2.couch_angle != cp1.couch_angle
456
+ # Table Top Eccentric Rotation Direction:
457
+ DICOM::Element.new('300A,0126', (cp2.couch_dir.empty? ? 'NONE' : cp2.couch_dir), :parent => cp_item2) if cp2.couch_dir != cp1.couch_dir
458
+ # Table Top Vertical Position:
459
+ couch_vert = cp2.couch_vertical.empty? ? '' : (cp2.couch_vertical.to_f * 10).to_s
460
+ DICOM::Element.new('300A,0128', couch_vert, :parent => cp_item2) if cp2.couch_vertical != cp1.couch_vertical
461
+ # Table Top Longitudinal Position:
462
+ couch_long = cp2.couch_longitudinal.empty? ? '' : (cp2.couch_longitudinal.to_f * 10).to_s
463
+ DICOM::Element.new('300A,0129', couch_long, :parent => cp_item2) if cp2.couch_longitudinal != cp1.couch_longitudinal
464
+ # Table Top Lateral Position:
465
+ couch_lat = cp2.couch_lateral.empty? ? '' : (cp2.couch_lateral.to_f * 10).to_s
466
+ DICOM::Element.new('300A,012A', couch_lat, :parent => cp_item2) if cp2.couch_lateral != cp1.couch_lateral
467
+ # Source to Surface Distance:
468
+ DICOM::Element.new('300A,0130', "#{cp2.ssd.to_f * 10}", :parent => cp_item2) if cp2.ssd != cp1.ssd
469
+ # Beam Limiting Device Position Sequence:
470
+ dp_seq = DICOM::Sequence.new('300A,011A', :parent => cp_item2)
471
+ # ASYMX:
472
+ if cp2.collimator_x1 != cp1.collimator_x1
473
+ dp_item_x = DICOM::Item.new(:parent => dp_seq)
474
+ DICOM::Element.new('300A,00B8', "ASYMX", :parent => dp_item_x)
475
+ DICOM::Element.new('300A,011C', "#{field.collimator_x1.to_f * 10}\\#{field.collimator_x2.to_f * 10}", :parent => dp_item_x)
476
+ end
477
+ # ASYMY:
478
+ if cp2.collimator_y1 != cp1.collimator_y1
479
+ dp_item_y = DICOM::Item.new(:parent => dp_seq)
480
+ DICOM::Element.new('300A,00B8', "ASYMY", :parent => dp_item_y)
481
+ DICOM::Element.new('300A,011C', "#{field.collimator_y1.to_f * 10}\\#{field.collimator_y2.to_f * 10}", :parent => dp_item_y)
482
+ end
483
+ # MLCX:
484
+ if cp2.mlc_lp_a != cp1.mlc_lp_a or cp2.mlc_lp_b != cp1.mlc_lp_b
485
+ dp_item_mlcx = DICOM::Item.new(:parent => dp_seq)
486
+ # RT Beam Limiting Device Type:
487
+ DICOM::Element.new('300A,00B8', "MLCX", :parent => dp_item_mlcx)
488
+ # Leaf/Jaw Positions:
489
+ pos_a = cp2.mlc_lp_a.collect{|p| (p.to_f * 10).round(2) unless p.empty?}.compact
490
+ pos_b = cp2.mlc_lp_b.collect{|p| (p.to_f * 10).round(2) unless p.empty?}.compact
491
+ leaf_pos = "#{pos_a.join("\\")}\\#{pos_b.join("\\")}"
492
+ DICOM::Element.new('300A,011C', leaf_pos, :parent => dp_item_mlcx)
493
+ end
494
+ end
495
+ end
496
+ end
497
+ end
498
+ return dcm
499
+ end
500
+
501
+ end
502
+
503
+ end