rtp-connect 1.1 → 1.2

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