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.
- 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
|