rtp-connect 1.6 → 1.7
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.
- 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
|
|