rtp-connect 1.1 → 1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +30 -19
- data/COPYING +674 -674
- data/Gemfile +3 -0
- data/Gemfile.lock +35 -0
- data/README.rdoc +12 -6
- data/lib/rtp-connect.rb +24 -22
- data/lib/rtp-connect/constants.rb +57 -57
- data/lib/rtp-connect/control_point.rb +176 -86
- data/lib/rtp-connect/dose_tracking.rb +222 -0
- data/lib/rtp-connect/extended_field.rb +56 -26
- data/lib/rtp-connect/field.rb +163 -43
- data/lib/rtp-connect/logging.rb +155 -158
- data/lib/rtp-connect/methods.rb +3 -4
- data/lib/rtp-connect/plan.rb +136 -52
- data/lib/rtp-connect/plan_to_dcm.rb +503 -0
- data/lib/rtp-connect/prescription.rb +71 -23
- data/lib/rtp-connect/record.rb +18 -2
- data/lib/rtp-connect/ruby_extensions.rb +90 -81
- data/lib/rtp-connect/site_setup.rb +78 -28
- data/lib/rtp-connect/version.rb +5 -5
- data/rakefile.rb +29 -0
- data/rtp-connect.gemspec +29 -0
- metadata +105 -53
@@ -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
|