rtp-connect 1.6 → 1.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/{CHANGELOG.rdoc → CHANGELOG.md} +29 -14
- data/Gemfile.lock +27 -21
- data/{README.rdoc → README.md} +62 -50
- data/lib/rtp-connect.rb +1 -0
- data/lib/rtp-connect/constants.rb +1 -0
- data/lib/rtp-connect/control_point.rb +142 -100
- data/lib/rtp-connect/dose_tracking.rb +31 -36
- data/lib/rtp-connect/extended_field.rb +15 -51
- data/lib/rtp-connect/extended_plan.rb +133 -0
- data/lib/rtp-connect/field.rb +101 -128
- data/lib/rtp-connect/methods.rb +31 -16
- data/lib/rtp-connect/plan.rb +80 -98
- data/lib/rtp-connect/plan_to_dcm.rb +68 -106
- data/lib/rtp-connect/prescription.rb +18 -56
- data/lib/rtp-connect/record.rb +62 -1
- data/lib/rtp-connect/ruby_extensions.rb +34 -3
- data/lib/rtp-connect/simulation_field.rb +58 -136
- data/lib/rtp-connect/site_setup.rb +51 -62
- data/lib/rtp-connect/version.rb +1 -1
- data/rakefile.rb +0 -1
- data/rtp-connect.gemspec +7 -7
- metadata +51 -41
data/lib/rtp-connect/methods.rb
CHANGED
@@ -16,37 +16,52 @@ module RTP
|
|
16
16
|
def leaf_boundaries(nr_leaves)
|
17
17
|
case nr_leaves
|
18
18
|
when 29
|
19
|
-
|
20
|
-
-15, -5, 5, 15, 25, 35, 45, 55, 65, 75, 85, 95, 105, 115, 125, 135, 200
|
21
|
-
]
|
19
|
+
leaf_boundaries_odd(29)
|
22
20
|
when 40
|
23
|
-
|
21
|
+
leaf_boundaries_even(40)
|
24
22
|
when 41
|
25
|
-
|
26
|
-
-105, -95, -85, -75, -65, -55, -45, -35, -25, -15, -5, 5, 15, 25, 35, 45,
|
27
|
-
55, 65, 75, 85, 95, 105, 115, 125, 135, 145, 155, 165, 175, 185, 195, 200
|
28
|
-
]
|
23
|
+
leaf_boundaries_odd(41)
|
29
24
|
when 60
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
70, 75, 80, 85, 90, 95, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200
|
34
|
-
]
|
25
|
+
Array.new(10) {|i| (i * 10 - 200).to_i}
|
26
|
+
.concat(Array.new(41) {|i| (i * 5 - 100).to_i})
|
27
|
+
.concat(Array.new(10) {|i| (i * 10 + 110).to_i})
|
35
28
|
when 80
|
36
|
-
|
29
|
+
leaf_boundaries_even(80)
|
37
30
|
else
|
38
31
|
raise ArgumentError, "Unsupported number of leaves: #{nr_leaves}"
|
39
32
|
end
|
40
33
|
end
|
41
34
|
|
35
|
+
# Gives an array of MLC leaf position boundaries for ordinary even numbered
|
36
|
+
# multi leaf collimators.
|
37
|
+
#
|
38
|
+
# @param [Fixnum] nr_leaves the number of leaves (in one leaf bank)
|
39
|
+
# @return [Array<Fixnum>] the leaf boundary positions
|
40
|
+
#
|
41
|
+
def leaf_boundaries_even(nr_leaves)
|
42
|
+
Array.new(nr_leaves+1) {|i| (i * 400 / nr_leaves.to_f - 200).to_i}
|
43
|
+
end
|
44
|
+
|
45
|
+
# Gives an array of MLC leaf position boundaries for ordinary odd numbered
|
46
|
+
# multi leaf collimators.
|
47
|
+
#
|
48
|
+
# @param [Fixnum] nr_leaves the number of leaves (in one leaf bank)
|
49
|
+
# @return [Array<Fixnum>] the leaf boundary positions
|
50
|
+
#
|
51
|
+
def leaf_boundaries_odd(nr_leaves)
|
52
|
+
Array.new(nr_leaves-1) {|i| (10 * (i - (0.5 * nr_leaves - 1))).to_i}.unshift(-200).push(200)
|
53
|
+
end
|
54
|
+
|
42
55
|
# Computes the CRC checksum of the given line and verifies that
|
43
56
|
# this value corresponds with the checksum given at the end of the line.
|
44
57
|
#
|
45
58
|
# @param [String] line a single line string from an RTPConnect ascii file
|
59
|
+
# @param [Hash] options the options to use for verifying the RTP record
|
60
|
+
# @option options [Boolean] :ignore_crc if true, the verification method will return true even if the checksum is invalid
|
46
61
|
# @return [Boolean] true
|
47
62
|
# @raise [ArgumentError] if an invalid line/record is given or the string contains an invalid checksum
|
48
63
|
#
|
49
|
-
def verify(line)
|
64
|
+
def verify(line, options={})
|
50
65
|
last_comma_pos = line.rindex(',')
|
51
66
|
raise ArgumentError, "Invalid line encountered; No comma present in the string: #{line}" unless last_comma_pos
|
52
67
|
string_to_check = line[0..last_comma_pos]
|
@@ -54,7 +69,7 @@ module RTP
|
|
54
69
|
raise ArgumentError, "Invalid line encountered; Valid checksum missing at end of string: #{string_remaining}" unless string_remaining.length >= 3
|
55
70
|
checksum_extracted = string_remaining.value.to_i
|
56
71
|
checksum_computed = string_to_check.checksum
|
57
|
-
raise ArgumentError, "Invalid line encountered: Specified
|
72
|
+
raise ArgumentError, "Invalid line encountered: Specified checksum #{checksum_extracted} deviates from the computed checksum #{checksum_computed}." if checksum_extracted != checksum_computed && !options[:ignore_crc]
|
58
73
|
return true
|
59
74
|
end
|
60
75
|
|
data/lib/rtp-connect/plan.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2011-
|
1
|
+
# Copyright 2011-2014 Christoffer Lervag
|
2
2
|
#
|
3
3
|
# This program is free software: you can redistribute it and/or modify
|
4
4
|
# it under the terms of the GNU General Public License as published by
|
@@ -63,70 +63,48 @@ module RTP
|
|
63
63
|
# @note This method does not perform crc verification on the given string.
|
64
64
|
# If such verification is desired, use methods ::parse or ::read instead.
|
65
65
|
# @param [#to_s] string the plan definition record string line
|
66
|
+
# @param [Hash] options the options to use for loading the plan definition string
|
67
|
+
# @option options [Boolean] :repair if true, a record containing invalid CSV will be attempted fixed and loaded
|
66
68
|
# @return [Plan] the created Plan instance
|
67
69
|
# @raise [ArgumentError] if given a string containing an invalid number of elements
|
68
70
|
#
|
69
|
-
def self.load(string)
|
70
|
-
# Get the quote-less values:
|
71
|
-
values = string.to_s.values
|
72
|
-
low_limit = 10
|
73
|
-
high_limit = 28
|
74
|
-
raise ArgumentError, "Invalid argument 'string': Expected at least #{low_limit} elements, got #{values.length}." if values.length < low_limit
|
75
|
-
RTP.logger.warn "The number of elements (#{values.length}) for this Plan record exceeds the known number of data items for this record (#{high_limit}). This may indicate an invalid record or that the RTP format has recently been expanded with new items." if values.length > high_limit
|
71
|
+
def self.load(string, options={})
|
76
72
|
rtp = self.new
|
77
|
-
|
78
|
-
rtp.keyword = values[0]
|
79
|
-
rtp.patient_id = values[1]
|
80
|
-
rtp.patient_last_name = values[2]
|
81
|
-
rtp.patient_first_name = values[3]
|
82
|
-
rtp.patient_middle_initial = values[4]
|
83
|
-
rtp.plan_id = values[5]
|
84
|
-
rtp.plan_date = values[6]
|
85
|
-
rtp.plan_time = values[7]
|
86
|
-
rtp.course_id = values[8]
|
87
|
-
rtp.diagnosis = values[9]
|
88
|
-
rtp.md_last_name = values[10]
|
89
|
-
rtp.md_first_name = values[11]
|
90
|
-
rtp.md_middle_initial = values[12]
|
91
|
-
rtp.md_approve_last_name = values[13]
|
92
|
-
rtp.md_approve_first_name = values[14]
|
93
|
-
rtp.md_approve_middle_initial = values[15]
|
94
|
-
rtp.phy_approve_last_name = values[16]
|
95
|
-
rtp.phy_approve_first_name = values[17]
|
96
|
-
rtp.phy_approve_middle_initial = values[18]
|
97
|
-
rtp.author_last_name = values[19]
|
98
|
-
rtp.author_first_name = values[20]
|
99
|
-
rtp.author_middle_initial = values[21]
|
100
|
-
rtp.rtp_mfg = values[22]
|
101
|
-
rtp.rtp_model = values[23]
|
102
|
-
rtp.rtp_version = values[24]
|
103
|
-
rtp.rtp_if_protocol = values[25]
|
104
|
-
rtp.rtp_if_version = values[26]
|
105
|
-
rtp.crc = values[-1]
|
106
|
-
return rtp
|
73
|
+
rtp.load(string, options)
|
107
74
|
end
|
108
75
|
|
109
76
|
# Creates a Plan instance by parsing an RTPConnect string.
|
110
77
|
#
|
111
78
|
# @param [#to_s] string an RTPConnect ascii string (with single or multiple lines/records)
|
79
|
+
# @param [Hash] options the options to use for parsing the RTP string
|
80
|
+
# @option options [Boolean] :ignore_crc if true, the RTP records will be successfully loaded even if their checksums are invalid
|
81
|
+
# @option options [Boolean] :repair if true, any RTP records containing invalid CSV will be attempted fixed and loaded
|
82
|
+
# @option options [Boolean] :skip_unknown if true, unknown records will be skipped, and record instances will be built from the remaining recognized string records
|
112
83
|
# @return [Plan] the created Plan instance
|
113
84
|
# @raise [ArgumentError] if given an invalid string record
|
114
85
|
#
|
115
|
-
def self.parse(string)
|
86
|
+
def self.parse(string, options={})
|
116
87
|
lines = string.to_s.split("\r\n")
|
117
88
|
# Create the Plan object:
|
118
89
|
line = lines.first
|
119
|
-
RTP::verify(line)
|
120
|
-
rtp = self.load(line)
|
90
|
+
RTP::verify(line, options)
|
91
|
+
rtp = self.load(line, options)
|
121
92
|
lines[1..-1].each do |line|
|
122
93
|
# Validate, determine type, and process the line accordingly to
|
123
94
|
# build the hierarchy of records:
|
124
|
-
RTP::verify(line)
|
125
|
-
values = line.values
|
95
|
+
RTP::verify(line, options)
|
96
|
+
values = line.values(options[:repair])
|
126
97
|
keyword = values.first
|
127
98
|
method = RTP::PARSE_METHOD[keyword]
|
128
|
-
|
129
|
-
|
99
|
+
if method
|
100
|
+
rtp.send(method, line)
|
101
|
+
else
|
102
|
+
if options[:skip_unknown]
|
103
|
+
logger.warn("Skipped unknown record definition: #{keyword}")
|
104
|
+
else
|
105
|
+
raise ArgumentError, "Unknown keyword #{keyword} extracted from string."
|
106
|
+
end
|
107
|
+
end
|
130
108
|
end
|
131
109
|
return rtp
|
132
110
|
end
|
@@ -134,10 +112,14 @@ module RTP
|
|
134
112
|
# Creates an Plan instance by reading and parsing an RTPConnect file.
|
135
113
|
#
|
136
114
|
# @param [String] file a string which specifies the path of the RTPConnect file to be loaded
|
115
|
+
# @param [Hash] options the options to use for reading the RTP file
|
116
|
+
# @option options [Boolean] :ignore_crc if true, the RTP records will be successfully loaded even if their checksums are invalid
|
117
|
+
# @option options [Boolean] :repair if true, any RTP records containing invalid CSV will be attempted fixed and loaded
|
118
|
+
# @option options [Boolean] :skip_unknown if true, unknown records will be skipped, and record instances will be built from the remaining recognized string records
|
137
119
|
# @return [Plan] the created Plan instance
|
138
120
|
# @raise [ArgumentError] if given an invalid file or the file given contains an invalid record
|
139
121
|
#
|
140
|
-
def self.read(file)
|
122
|
+
def self.read(file, options={})
|
141
123
|
raise ArgumentError, "Invalid argument 'file'. Expected String, got #{file.class}." unless file.is_a?(String)
|
142
124
|
# Read the file content:
|
143
125
|
str = nil
|
@@ -160,7 +142,7 @@ module RTP
|
|
160
142
|
end
|
161
143
|
# Parse the file contents and create the RTP::Connect object:
|
162
144
|
if str
|
163
|
-
rtp = self.parse(str)
|
145
|
+
rtp = self.parse(str, options)
|
164
146
|
else
|
165
147
|
raise "An RTP::Plan object could not be created from the specified file. Check the log for more details."
|
166
148
|
end
|
@@ -170,13 +152,45 @@ module RTP
|
|
170
152
|
# Creates a new Plan.
|
171
153
|
#
|
172
154
|
def initialize
|
155
|
+
super('PLAN_DEF', 10, 28)
|
173
156
|
@current_parent = self
|
174
157
|
# Child records:
|
158
|
+
@extended_plan = nil
|
175
159
|
@prescriptions = Array.new
|
176
160
|
@dose_trackings = Array.new
|
177
161
|
# No parent (by definition) for the Plan record:
|
178
162
|
@parent = nil
|
179
|
-
@
|
163
|
+
@attributes = [
|
164
|
+
# Required:
|
165
|
+
:keyword,
|
166
|
+
:patient_id,
|
167
|
+
:patient_last_name,
|
168
|
+
:patient_first_name,
|
169
|
+
:patient_middle_initial,
|
170
|
+
:plan_id,
|
171
|
+
:plan_date,
|
172
|
+
:plan_time,
|
173
|
+
:course_id,
|
174
|
+
# Optional:
|
175
|
+
:diagnosis,
|
176
|
+
:md_last_name,
|
177
|
+
:md_first_name,
|
178
|
+
:md_middle_initial,
|
179
|
+
:md_approve_last_name,
|
180
|
+
:md_approve_first_name,
|
181
|
+
:md_approve_middle_initial,
|
182
|
+
:phy_approve_last_name,
|
183
|
+
:phy_approve_first_name,
|
184
|
+
:phy_approve_middle_initial,
|
185
|
+
:author_last_name,
|
186
|
+
:author_first_name,
|
187
|
+
:author_middle_initial,
|
188
|
+
:rtp_mfg,
|
189
|
+
:rtp_model,
|
190
|
+
:rtp_version,
|
191
|
+
:rtp_if_protocol,
|
192
|
+
:rtp_if_version
|
193
|
+
]
|
180
194
|
end
|
181
195
|
|
182
196
|
# Checks for equality.
|
@@ -203,6 +217,14 @@ module RTP
|
|
203
217
|
@dose_trackings << child.to_dose_tracking
|
204
218
|
end
|
205
219
|
|
220
|
+
# Adds an extended plan record to this instance.
|
221
|
+
#
|
222
|
+
# @param [ExtendedPlan] child an ExtendedPlan instance which is to be associated with self
|
223
|
+
#
|
224
|
+
def add_extended_plan(child)
|
225
|
+
@extended_plan = child.to_extended_plan
|
226
|
+
end
|
227
|
+
|
206
228
|
# Adds a prescription site record to this instance.
|
207
229
|
#
|
208
230
|
# @param [Prescription] child a Prescription instance which is to be associated with self
|
@@ -216,7 +238,7 @@ module RTP
|
|
216
238
|
# @return [Array<Prescription, DoseTracking>] a sorted array of self's child records
|
217
239
|
#
|
218
240
|
def children
|
219
|
-
return [@prescriptions, @dose_trackings].flatten.compact
|
241
|
+
return [@extended_plan, @prescriptions, @dose_trackings].flatten.compact
|
220
242
|
end
|
221
243
|
|
222
244
|
# Computes a hash code for this object.
|
@@ -229,43 +251,6 @@ module RTP
|
|
229
251
|
state.hash
|
230
252
|
end
|
231
253
|
|
232
|
-
# Collects the values (attributes) of this instance.
|
233
|
-
#
|
234
|
-
# @note The CRC is not considered part of the actual values and is excluded.
|
235
|
-
# @return [Array<String>] an array of attributes (in the same order as they appear in the RTP string)
|
236
|
-
#
|
237
|
-
def values
|
238
|
-
return [
|
239
|
-
@keyword,
|
240
|
-
@patient_id,
|
241
|
-
@patient_last_name,
|
242
|
-
@patient_first_name,
|
243
|
-
@patient_middle_initial,
|
244
|
-
@plan_id,
|
245
|
-
@plan_date,
|
246
|
-
@plan_time,
|
247
|
-
@course_id,
|
248
|
-
@diagnosis,
|
249
|
-
@md_last_name,
|
250
|
-
@md_first_name,
|
251
|
-
@md_middle_initial,
|
252
|
-
@md_approve_last_name,
|
253
|
-
@md_approve_first_name,
|
254
|
-
@md_approve_middle_initial,
|
255
|
-
@phy_approve_last_name,
|
256
|
-
@phy_approve_first_name,
|
257
|
-
@phy_approve_middle_initial,
|
258
|
-
@author_last_name,
|
259
|
-
@author_first_name,
|
260
|
-
@author_middle_initial,
|
261
|
-
@rtp_mfg,
|
262
|
-
@rtp_model,
|
263
|
-
@rtp_version,
|
264
|
-
@rtp_if_protocol,
|
265
|
-
@rtp_if_version
|
266
|
-
]
|
267
|
-
end
|
268
|
-
|
269
254
|
# Returns self.
|
270
255
|
#
|
271
256
|
# @return [Plan] self
|
@@ -308,18 +293,6 @@ module RTP
|
|
308
293
|
f.close
|
309
294
|
end
|
310
295
|
|
311
|
-
# Sets the keyword attribute.
|
312
|
-
#
|
313
|
-
# @note Since only a specific string is accepted, this is more of an argument check than a traditional setter method
|
314
|
-
# @param [#to_s] value the new attribute value
|
315
|
-
# @raise [ArgumentError] if given an unexpected keyword
|
316
|
-
#
|
317
|
-
def keyword=(value)
|
318
|
-
value = value.to_s.upcase
|
319
|
-
raise ArgumentError, "Invalid keyword. Expected 'PLAN_DEF', got #{value}." unless value == "PLAN_DEF"
|
320
|
-
@keyword = value
|
321
|
-
end
|
322
|
-
|
323
296
|
# Sets the patient_id attribute.
|
324
297
|
#
|
325
298
|
# @param [nil, #to_s] value the new attribute value
|
@@ -548,6 +521,15 @@ module RTP
|
|
548
521
|
@current_parent = dt
|
549
522
|
end
|
550
523
|
|
524
|
+
# Creates an extended plan record from the given string.
|
525
|
+
#
|
526
|
+
# @param [String] string a string line containing an extended plan definition
|
527
|
+
#
|
528
|
+
def extended_plan(string)
|
529
|
+
ep = ExtendedPlan.load(string, @current_parent)
|
530
|
+
@current_parent = ep
|
531
|
+
end
|
532
|
+
|
551
533
|
# Creates an extended treatment field record from the given string.
|
552
534
|
#
|
553
535
|
# @param [String] string a string line containing an extended treatment field definition
|
@@ -2,6 +2,14 @@ module RTP
|
|
2
2
|
|
3
3
|
class Plan < Record
|
4
4
|
|
5
|
+
attr_accessor :current_gantry
|
6
|
+
attr_accessor :current_collimator
|
7
|
+
attr_accessor :current_couch_angle
|
8
|
+
attr_accessor :current_couch_pedestal
|
9
|
+
attr_accessor :current_couch_lateral
|
10
|
+
attr_accessor :current_couch_longitudinal
|
11
|
+
attr_accessor :current_couch_vertical
|
12
|
+
|
5
13
|
# Converts the Plan (and child) records to a
|
6
14
|
# DICOM::DObject of modality RTPLAN.
|
7
15
|
#
|
@@ -13,6 +21,7 @@ module RTP
|
|
13
21
|
# @option options [Boolean] :dose_ref if set, Dose Reference & Referenced Dose Reference sequences will be included in the generated DICOM file
|
14
22
|
# @option options [String] :manufacturer the value used for the manufacturer tag (0008,0070) in the beam sequence
|
15
23
|
# @option options [String] :model the value used for the manufacturer's model name tag (0008,1090) in the beam sequence
|
24
|
+
# @option options [Symbol] :scale if set, relevant device parameters are converted from native readout format to IEC1217 (supported values are :elekta & :varian)
|
16
25
|
# @option options [String] :serial_number the value used for the device serial number tag (0018,1000) in the beam sequence
|
17
26
|
# @return [DICOM::DObject] the converted DICOM object
|
18
27
|
#
|
@@ -323,7 +332,11 @@ module RTP
|
|
323
332
|
# Cumulative Meterset Weight:
|
324
333
|
DICOM::Element.new('300A,0134', '0', :parent => cp_item)
|
325
334
|
# Beam Limiting Device Position Sequence:
|
326
|
-
|
335
|
+
if field.control_points.length > 0
|
336
|
+
create_beam_limiting_device_positions(cp_item, field.control_points.first, options)
|
337
|
+
else
|
338
|
+
create_beam_limiting_device_positions_from_field(cp_item, field, options)
|
339
|
+
end
|
327
340
|
# Referenced Dose Reference Sequence:
|
328
341
|
create_referenced_dose_reference(cp_item) if options[:dose_ref]
|
329
342
|
# Second CP:
|
@@ -355,93 +368,39 @@ module RTP
|
|
355
368
|
private
|
356
369
|
|
357
370
|
|
358
|
-
# Adds
|
371
|
+
# Adds an angular type value to a Control Point Item, by creating the
|
372
|
+
# necessary DICOM elements.
|
359
373
|
# Note that the element is only added if there is no 'current' attribute
|
360
374
|
# defined, or the given value is different form the current attribute.
|
361
375
|
#
|
362
|
-
# @param [
|
363
|
-
# @param [String
|
364
|
-
# @param [
|
365
|
-
#
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
376
|
+
# @param [DICOM::Item] item the DICOM control point item in which to create the elements
|
377
|
+
# @param [String] angle_tag the DICOM tag of the angle element
|
378
|
+
# @param [String] direction_tag the DICOM tag of the direction element
|
379
|
+
# @param [String, NilClass] angle the collimator angle attribute
|
380
|
+
# @param [String, NilClass] direction the collimator rotation direction attribute
|
381
|
+
# @param [Symbol] current_angle the instance variable that keeps track of the current value of this attribute
|
382
|
+
#
|
383
|
+
def add_angle(item, angle_tag, direction_tag, angle, direction, current_angle)
|
384
|
+
if !self.send(current_angle) || angle != self.send(current_angle)
|
385
|
+
self.send("#{current_angle}=", angle)
|
386
|
+
DICOM::Element.new(angle_tag, angle, :parent => item)
|
387
|
+
DICOM::Element.new(direction_tag, (direction.empty? ? 'NONE' : direction), :parent => item)
|
371
388
|
end
|
372
389
|
end
|
373
390
|
|
374
|
-
# Adds Table Top
|
391
|
+
# Adds a Table Top Position element to a Control Point Item.
|
375
392
|
# Note that the element is only added if there is no 'current' attribute
|
376
393
|
# defined, or the given value is different form the current attribute.
|
377
394
|
#
|
378
|
-
# @param [
|
379
|
-
# @param [String
|
380
|
-
# @param [
|
395
|
+
# @param [DICOM::Item] item the DICOM control point item in which to create the element
|
396
|
+
# @param [String] tag the DICOM tag of the couch position element
|
397
|
+
# @param [String, NilClass] value the couch position
|
398
|
+
# @param [Symbol] current the instance variable that keeps track of the current value of this attribute
|
381
399
|
#
|
382
|
-
def
|
383
|
-
if
|
384
|
-
|
385
|
-
DICOM::Element.new(
|
386
|
-
DICOM::Element.new('300A,0126', (value2.empty? ? 'NONE' : value2), :parent => item)
|
387
|
-
end
|
388
|
-
end
|
389
|
-
|
390
|
-
# Adds a Table Top Lateral Position element to a Control Point Item.
|
391
|
-
# Note that the element is only added if there is no 'current' attribute
|
392
|
-
# defined, or the given value is different form the current attribute.
|
393
|
-
#
|
394
|
-
# @param [String, NilClass] value the couch lateral attribute
|
395
|
-
# @param [DICOM::Item] item the DICOM control point item in which to create an element
|
396
|
-
#
|
397
|
-
def add_couch_lateral(value, item)
|
398
|
-
if !@current_couch_lateral || value != @current_couch_lateral
|
399
|
-
@current_couch_lateral = value
|
400
|
-
DICOM::Element.new('300A,012A', (value.empty? ? '' : value.to_f * 10), :parent => item)
|
401
|
-
end
|
402
|
-
end
|
403
|
-
|
404
|
-
# Adds a Table Top Longitudinal Position element to a Control Point Item.
|
405
|
-
# Note that the element is only added if there is no 'current' attribute
|
406
|
-
# defined, or the given value is different form the current attribute.
|
407
|
-
#
|
408
|
-
# @param [String, NilClass] value the couch longitudinal attribute
|
409
|
-
# @param [DICOM::Item] item the DICOM control point item in which to create an element
|
410
|
-
#
|
411
|
-
def add_couch_longitudinal(value, item)
|
412
|
-
if !@current_couch_longitudinal || value != @current_couch_longitudinal
|
413
|
-
@current_couch_longitudinal = value
|
414
|
-
DICOM::Element.new('300A,0129', (value.empty? ? '' : value.to_f * 10), :parent => item)
|
415
|
-
end
|
416
|
-
end
|
417
|
-
|
418
|
-
# Adds Patient Support Angle elements to a Control Point Item.
|
419
|
-
# Note that the element is only added if there is no 'current' attribute
|
420
|
-
# defined, or the given value is different form the current attribute.
|
421
|
-
#
|
422
|
-
# @param [String, NilClass] value1 the patient support angle attribute
|
423
|
-
# @param [String, NilClass] value2 the patient support rotation direction attribute
|
424
|
-
# @param [DICOM::Item] item the DICOM control point item in which to create an element
|
425
|
-
#
|
426
|
-
def add_couch_pedestal(value1, value2, item)
|
427
|
-
if !@current_couch_pedestal || value1 != @current_couch_pedestal
|
428
|
-
@current_couch_pedestal = value1
|
429
|
-
DICOM::Element.new('300A,0122', value1, :parent => item)
|
430
|
-
DICOM::Element.new('300A,0123', (value2.empty? ? 'NONE' : value2), :parent => item)
|
431
|
-
end
|
432
|
-
end
|
433
|
-
|
434
|
-
# Adds a Table Top Vertical Position element to a Control Point Item.
|
435
|
-
# Note that the element is only added if there is no 'current' attribute
|
436
|
-
# defined, or the given value is different form the current attribute.
|
437
|
-
#
|
438
|
-
# @param [String, NilClass] value the couch vertical attribute
|
439
|
-
# @param [DICOM::Item] item the DICOM control point item in which to create an element
|
440
|
-
#
|
441
|
-
def add_couch_vertical(value, item)
|
442
|
-
if !@current_couch_vertical || value != @current_couch_vertical
|
443
|
-
@current_couch_vertical = value
|
444
|
-
DICOM::Element.new('300A,0128', (value.empty? ? '' : value.to_f * 10), :parent => item)
|
400
|
+
def add_couch_position(item, tag, value, current)
|
401
|
+
if !self.send(current) || value != self.send(current)
|
402
|
+
self.send("#{current}=", value)
|
403
|
+
DICOM::Element.new(tag, (value.empty? ? '' : value.to_f * 10), :parent => item)
|
445
404
|
end
|
446
405
|
end
|
447
406
|
|
@@ -473,22 +432,6 @@ module RTP
|
|
473
432
|
end
|
474
433
|
end
|
475
434
|
|
476
|
-
# Adds Gantry Angle elements to a Control Point Item.
|
477
|
-
# Note that the element is only added if there is no 'current' attribute
|
478
|
-
# defined, or the given value is different form the current attribute.
|
479
|
-
#
|
480
|
-
# @param [String, NilClass] value1 the gantry angle attribute
|
481
|
-
# @param [String, NilClass] value2 the gantry rotation direction attribute
|
482
|
-
# @param [DICOM::Item] item the DICOM control point item in which to create an element
|
483
|
-
#
|
484
|
-
def add_gantry(value1, value2, item)
|
485
|
-
if !@current_gantry || value1 != @current_gantry
|
486
|
-
@current_gantry = value1
|
487
|
-
DICOM::Element.new('300A,011E', value1, :parent => item)
|
488
|
-
DICOM::Element.new('300A,011F', (value2.empty? ? 'NONE' : value2), :parent => item)
|
489
|
-
end
|
490
|
-
end
|
491
|
-
|
492
435
|
# Adds an Isosenter element to a Control Point Item.
|
493
436
|
# Note that the element is only added if there is a Site Setup record present,
|
494
437
|
# and it contains a real (non-empty) value. Also, the element is only added if there
|
@@ -542,7 +485,7 @@ module RTP
|
|
542
485
|
# Control Point Index:
|
543
486
|
DICOM::Element.new('300A,0112', "#{cp.index}", :parent => cp_item)
|
544
487
|
# Beam Limiting Device Position Sequence:
|
545
|
-
create_beam_limiting_device_positions(cp_item, cp)
|
488
|
+
create_beam_limiting_device_positions(cp_item, cp, options)
|
546
489
|
# Source to Surface Distance:
|
547
490
|
add_ssd(cp.ssd, cp_item)
|
548
491
|
# Cumulative Meterset Weight:
|
@@ -555,19 +498,19 @@ module RTP
|
|
555
498
|
# Dose Rate Set:
|
556
499
|
add_doserate(cp.doserate, cp_item)
|
557
500
|
# Gantry Angle & Rotation Direction:
|
558
|
-
|
501
|
+
add_angle(cp_item, '300A,011E', '300A,011F', cp.gantry_angle, cp.gantry_dir, :current_gantry)
|
559
502
|
# Beam Limiting Device Angle & Rotation Direction:
|
560
|
-
|
503
|
+
add_angle(cp_item, '300A,0120', '300A,0121', cp.collimator_angle, cp.collimator_dir, :current_collimator)
|
561
504
|
# Patient Support Angle & Rotation Direction:
|
562
|
-
|
505
|
+
add_angle(cp_item, '300A,0122', '300A,0123', cp.couch_pedestal, cp.couch_ped_dir, :current_couch_pedestal)
|
563
506
|
# Table Top Eccentric Angle & Rotation Direction:
|
564
|
-
|
507
|
+
add_angle(cp_item, '300A,0125', '300A,0126', cp.couch_angle, cp.couch_dir, :current_couch_angle)
|
565
508
|
# Table Top Vertical Position:
|
566
|
-
|
509
|
+
add_couch_position(cp_item, '300A,0128', cp.couch_vertical, :current_couch_vertical)
|
567
510
|
# Table Top Longitudinal Position:
|
568
|
-
|
511
|
+
add_couch_position(cp_item, '300A,0129', cp.couch_longitudinal, :current_couch_longitudinal)
|
569
512
|
# Table Top Lateral Position:
|
570
|
-
|
513
|
+
add_couch_position(cp_item, '300A,012A', cp.couch_lateral, :current_couch_lateral)
|
571
514
|
# Isocenter Position (x\y\z):
|
572
515
|
add_isosenter(cp.parent.parent.site_setup, cp_item)
|
573
516
|
cp_item
|
@@ -613,26 +556,45 @@ module RTP
|
|
613
556
|
# @param [ControlPoint] cp the RTP control point to fetch device parameters from
|
614
557
|
# @return [DICOM::Sequence] the constructed beam limiting device positions sequence
|
615
558
|
#
|
616
|
-
def create_beam_limiting_device_positions(cp_item, cp)
|
559
|
+
def create_beam_limiting_device_positions(cp_item, cp, options={})
|
617
560
|
dp_seq = DICOM::Sequence.new('300A,011A', :parent => cp_item)
|
618
561
|
# The ASYMX item ('backup jaws') doesn't exist on all models:
|
619
562
|
if ['SYM', 'ASY'].include?(cp.parent.field_x_mode.upcase)
|
620
563
|
dp_item_x = DICOM::Item.new(:parent => dp_seq)
|
621
564
|
DICOM::Element.new('300A,00B8', "ASYMX", :parent => dp_item_x)
|
622
|
-
DICOM::Element.new('300A,011C', "#{cp.dcm_collimator_x1}\\#{cp.dcm_collimator_x2}", :parent => dp_item_x)
|
565
|
+
DICOM::Element.new('300A,011C', "#{cp.dcm_collimator_x1(options[:scale])}\\#{cp.dcm_collimator_x2(options[:scale])}", :parent => dp_item_x)
|
623
566
|
end
|
624
567
|
# Always create one ASYMY item:
|
625
568
|
dp_item_y = DICOM::Item.new(:parent => dp_seq)
|
626
569
|
# RT Beam Limiting Device Type:
|
627
570
|
DICOM::Element.new('300A,00B8', "ASYMY", :parent => dp_item_y)
|
628
571
|
# Leaf/Jaw Positions:
|
629
|
-
DICOM::Element.new('300A,011C', "#{cp.dcm_collimator_y1}\\#{cp.dcm_collimator_y2}", :parent => dp_item_y)
|
572
|
+
DICOM::Element.new('300A,011C', "#{cp.dcm_collimator_y1(options[:scale])}\\#{cp.dcm_collimator_y2(options[:scale])}", :parent => dp_item_y)
|
630
573
|
# MLCX:
|
631
574
|
dp_item_mlcx = DICOM::Item.new(:parent => dp_seq)
|
632
575
|
# RT Beam Limiting Device Type:
|
633
576
|
DICOM::Element.new('300A,00B8', "MLCX", :parent => dp_item_mlcx)
|
634
577
|
# Leaf/Jaw Positions:
|
635
|
-
DICOM::Element.new('300A,011C', cp.dcm_mlc_positions, :parent => dp_item_mlcx)
|
578
|
+
DICOM::Element.new('300A,011C', cp.dcm_mlc_positions(options[:scale]), :parent => dp_item_mlcx)
|
579
|
+
dp_seq
|
580
|
+
end
|
581
|
+
|
582
|
+
# Creates a beam limiting device positions sequence in the given DICOM object.
|
583
|
+
#
|
584
|
+
# @param [DICOM::Item] cp_item the DICOM control point item in which to insert the sequence
|
585
|
+
# @param [Field] field the RTP treatment field to fetch device parameters from
|
586
|
+
# @return [DICOM::Sequence] the constructed beam limiting device positions sequence
|
587
|
+
#
|
588
|
+
def create_beam_limiting_device_positions_from_field(cp_item, field, options={})
|
589
|
+
dp_seq = DICOM::Sequence.new('300A,011A', :parent => cp_item)
|
590
|
+
# ASYMX:
|
591
|
+
dp_item_x = DICOM::Item.new(:parent => dp_seq)
|
592
|
+
DICOM::Element.new('300A,00B8', "ASYMX", :parent => dp_item_x)
|
593
|
+
DICOM::Element.new('300A,011C', "#{field.dcm_collimator_x1}\\#{field.dcm_collimator_x2}", :parent => dp_item_x)
|
594
|
+
# ASYMY:
|
595
|
+
dp_item_y = DICOM::Item.new(:parent => dp_seq)
|
596
|
+
DICOM::Element.new('300A,00B8', "ASYMY", :parent => dp_item_y)
|
597
|
+
DICOM::Element.new('300A,011C', "#{field.dcm_collimator_y1}\\#{field.dcm_collimator_y2}", :parent => dp_item_y)
|
636
598
|
dp_seq
|
637
599
|
end
|
638
600
|
|