rtkit 0.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.
@@ -0,0 +1,213 @@
1
+ module RTKIT
2
+
3
+ # Contains DICOM data and methods related to a Contour.
4
+ # A set of Contours in a set of Slices defines a ROI.
5
+ #
6
+ # === Relations
7
+ #
8
+ # * The Contour belongs to a Slice.
9
+ # * A Contour has many Coordinates.
10
+ #
11
+ # === Resources
12
+ #
13
+ # * ROI Contour Module: PS 3.3, C.8.8.6
14
+ # * Patient Based Coordinate System: PS 3.3, C.7.6.2.1.1
15
+ #
16
+ class Contour
17
+
18
+ # An array of Coordinates (x,y,z - triplets).
19
+ attr_reader :coordinates
20
+ # Contour Number.
21
+ attr_reader :number
22
+ # The Slice that the Contour belongs to.
23
+ attr_reader :slice
24
+ # Contour Geometric Type.
25
+ attr_reader :type
26
+
27
+ # Creates a new Contour instance from x, y and z coordinate arrays.
28
+ # This method also creates and connects any child Coordinates as indicated by the coordinate arrays.
29
+ # Returns the Contour instance.
30
+ #
31
+ # === Parameters
32
+ #
33
+ # * <tt>x</tt> -- An array of x coordinates (Floats).
34
+ # * <tt>y</tt> -- An array of y coordinates (Floats).
35
+ # * <tt>z</tt> -- An array of z coordinates (Floats).
36
+ # * <tt>slice</tt> -- The Slice instance that this Contour belongs to.
37
+ #
38
+ def self.create_from_coordinates(x, y, z, slice)
39
+ raise ArgumentError, "Invalid argument 'x'. Expected Array, got #{x.class}." unless x.is_a?(Array)
40
+ raise ArgumentError, "Invalid argument 'y'. Expected Array, got #{y.class}." unless y.is_a?(Array)
41
+ raise ArgumentError, "Invalid argument 'z'. Expected Array, got #{z.class}." unless z.is_a?(Array)
42
+ raise ArgumentError, "Invalid argument 'slice'. Expected Slice, got #{slice.class}." unless slice.is_a?(Slice)
43
+ raise ArgumentError, "The coordinate arrays are of unequal length [#{x.length}, #{y.length}, #{z.length}]." unless [x.length, y.length, z.length].uniq.length == 1
44
+ number = slice.roi.num_contours + 1
45
+ # Create the Contour:
46
+ c = self.new(slice, :number => number)
47
+ # Create the Coordinates belonging to this Contour:
48
+ x.each_index do |i|
49
+ Coordinate.new(x[i], y[i], z[i], c)
50
+ end
51
+ return c
52
+ end
53
+
54
+ # Creates a new Contour instance from a contour item.
55
+ # This method also creates and connects any Coordinates as indicated by the item.
56
+ # Returns the Contour instance.
57
+ #
58
+ # === Parameters
59
+ #
60
+ # * <tt>contour_item</tt> -- An array of contour items from the Contour Sequence in ROI Contour Sequence, belonging to the same slice.
61
+ # * <tt>slice</tt> -- The Slice instance that this Contour belongs to.
62
+ #
63
+ def self.create_from_item(contour_item, slice)
64
+ raise ArgumentError, "Invalid argument 'contour_item'. Expected Item, got #{contour_item.class}." unless contour_item.is_a?(DICOM::Item)
65
+ raise ArgumentError, "Invalid argument 'slice'. Expected Slice, got #{slice.class}." unless slice.is_a?(Slice)
66
+ raise ArgumentError, "Invalid argument 'contour_item'. The specified Item does not contain a Contour Data Value (Element '3006,0050')." unless contour_item.value(CONTOUR_DATA)
67
+ number = (contour_item.value(CONTOUR_NUMBER) ? contour_item.value(CONTOUR_NUMBER).to_i : nil)
68
+ type = contour_item.value(CONTOUR_GEO_TYPE)
69
+ #size = contour_item.value(NR_CONTOUR_POINTS) # May be used for QA of the content of the item, but not needed in the Contour object.
70
+ # Create the Contour:
71
+ c = self.new(slice, :type => type, :number => number)
72
+ # Create the Coordinates belonging to this Contour:
73
+ c.create_coordinates(contour_item.value(CONTOUR_DATA))
74
+ return c
75
+ end
76
+
77
+ # Creates a new Contour instance.
78
+ #
79
+ # === Parameters
80
+ #
81
+ # * <tt>slice</tt> -- The Slice instance that this Contour belongs to.
82
+ # * <tt>options</tt> -- A hash of parameters.
83
+ #
84
+ # === Options
85
+ #
86
+ # * <tt>:number</tt> -- Integer. The Contour Number.
87
+ # * <tt>:type</tt> -- String. The Contour Geometric Type. Defaults to 'CLOSED_PLANAR'.
88
+ #
89
+ def initialize(slice, options={})
90
+ raise ArgumentError, "Invalid argument 'slice'. Expected Slice, got #{slice.class}." unless slice.is_a?(Slice)
91
+ raise ArgumentError, "Invalid option :number. Expected Integer, got #{options[:number].class}." if options[:number] && !options[:number].is_a?(Integer)
92
+ raise ArgumentError, "Invalid option :type. Expected String, got #{options[:type].class}." if options[:type] && !options[:type].is_a?(String)
93
+ # Key attributes:
94
+ @coordinates = Array.new
95
+ @slice = slice
96
+ @type = options[:type] || 'CLOSED_PLANAR'
97
+ @number = options[:number] # don't need a default value for this attribute
98
+ # Register ourselves with the Slice:
99
+ @slice.add_contour(self)
100
+ end
101
+
102
+ # Returns true if the argument is an instance with attributes equal to self.
103
+ #
104
+ def ==(other)
105
+ if other.respond_to?(:to_contour)
106
+ other.send(:state) == state
107
+ end
108
+ end
109
+
110
+ alias_method :eql?, :==
111
+
112
+ # Adds a Coordinate instance to this Contour.
113
+ #
114
+ def add_coordinate(coordinate)
115
+ raise ArgumentError, "Invalid argument 'coordinate'. Expected Coordinate, got #{coordinate.class}." unless coordinate.is_a?(Coordinate)
116
+ @coordinates << coordinate unless @coordinates.include?(coordinate)
117
+ end
118
+
119
+ # Returns all Coordinates of this Contour, packed to a string in the format
120
+ # used in the Contour Data DICOM Element (3006,0050).
121
+ # Returns an empty string if the Contour contains no coordinates.
122
+ #
123
+ def contour_data
124
+ x, y, z = coords
125
+ return [x, y, z].transpose.flatten.join("\\")
126
+ end
127
+
128
+ # Returns all Coordinates of this Contour, in arrays of x, y and z coordinates.
129
+ #
130
+ def coords
131
+ x, y, z = Array.new, Array.new, Array.new
132
+ @coordinates.each do |coord|
133
+ x << coord.x
134
+ y << coord.y
135
+ z << coord.z
136
+ end
137
+ return x, y, z
138
+ end
139
+
140
+ # Creates and connects Coordinate instances with this Contour instance
141
+ # by processing the value of the Contour Data element.
142
+ #
143
+ # === Parameters
144
+ #
145
+ # * <tt>contour_data</tt> -- The value of the Contour Data Element (A String of backslash-separated of xyz coordinate triplets).
146
+ #
147
+ def create_coordinates(contour_data)
148
+ raise ArgumentError, "Invalid argument 'contour_data'. Expected String (or nil), got #{contour_data.class}." unless [String, NilClass].include?(contour_data.class)
149
+ if contour_data && contour_data != ""
150
+ # Split the number strings, sperated by a '\', into an array:
151
+ string_values = contour_data.split("\\")
152
+ size = string_values.length/3
153
+ # Extract every third value of the string array as x, y, and z, respectively, and collect them as floats instead of strings:
154
+ x = string_values.values_at(*(Array.new(size){|i| i*3 })).collect{|val| val.to_f}
155
+ y = string_values.values_at(*(Array.new(size){|i| i*3+1})).collect{|val| val.to_f}
156
+ z = string_values.values_at(*(Array.new(size){|i| i*3+2})).collect{|val| val.to_f}
157
+ x.each_index do |i|
158
+ Coordinate.new(x[i], y[i], z[i], self)
159
+ end
160
+ end
161
+ end
162
+
163
+ # Generates a Fixnum hash value for this instance.
164
+ #
165
+ def hash
166
+ state.hash
167
+ end
168
+
169
+ =begin
170
+ # Returns the number of Coordinates (Corner Points) belonging to this Contour.
171
+ #
172
+ def length
173
+ @coordinates.length
174
+ end
175
+
176
+ alias_method :size, :length
177
+ =end
178
+
179
+ # Returns self.
180
+ #
181
+ def to_contour
182
+ self
183
+ end
184
+
185
+ # Creates and returns a Contour Sequence Item from the attributes of the Contour.
186
+ #
187
+ def to_item
188
+ # FIXME: We need to decide on how to principally handle the situation when an image series has not been
189
+ # loaded, and how to set up the ROI slices. A possible solution is to create Image instances if they hasn't been loaded.
190
+ item = DICOM::Item.new
191
+ item.add(DICOM::Sequence.new(CONTOUR_IMAGE_SQ))
192
+ item[CONTOUR_IMAGE_SQ].add_item
193
+ item[CONTOUR_IMAGE_SQ][0].add(DICOM::Element.new(REF_SOP_CLASS_UID, @slice.image ? @slice.image.series.class_uid : '1.2.840.10008.5.1.4.1.1.2')) # Deafult to CT if image ref. doesn't exist.
194
+ item[CONTOUR_IMAGE_SQ][0].add(DICOM::Element.new(REF_SOP_UID, @slice.uid))
195
+ item.add(DICOM::Element.new(CONTOUR_GEO_TYPE, @type))
196
+ item.add(DICOM::Element.new(NR_CONTOUR_POINTS, @coordinates.length.to_s))
197
+ item.add(DICOM::Element.new(CONTOUR_NUMBER, @number.to_s))
198
+ item.add(DICOM::Element.new(CONTOUR_DATA, contour_data))
199
+ return item
200
+ end
201
+
202
+
203
+ private
204
+
205
+
206
+ # Returns the attributes of this instance in an array (for comparison purposes).
207
+ #
208
+ def state
209
+ [@coordinates, @number, @type]
210
+ end
211
+
212
+ end
213
+ end
@@ -0,0 +1,371 @@
1
+ module RTKIT
2
+
3
+ # Contains DICOM data and methods related to a ControlPoint.
4
+ #
5
+ # === Notes
6
+ #
7
+ # The first control point in a given beam defines the intial setup, and contains
8
+ # all applicable parameters. The rest of the control points contains the parameters
9
+ # which change at any control point.
10
+ #
11
+ # === Relations
12
+ #
13
+ # * A Beam has many ControlPoints.
14
+ # * A ControlPoint has many Collimators.
15
+ #
16
+ class ControlPoint
17
+
18
+
19
+ # The Beam that the ControlPoint is defined in.
20
+ attr_reader :beam
21
+ # Collimator (beam limiting device) angle (float).
22
+ attr_reader :collimator_angle
23
+ # Collimator (beam limiting device) rotation direction (string).
24
+ attr_reader :collimator_direction
25
+ # An array containing the ControlPoint's collimators.
26
+ attr_reader :collimators
27
+ # Cumulative meterset weight (float).
28
+ attr_reader :cum_meterset
29
+ # Nominal beam energy (float).
30
+ attr_reader :energy
31
+ # Gantry angle (float).
32
+ attr_reader :gantry_angle
33
+ # Gantry rotation direction (string).
34
+ attr_reader :gantry_direction
35
+ # Control point index (integer).
36
+ attr_reader :index
37
+ # Isosenter position (a coordinate triplet of positions x, y, z).
38
+ attr_reader :iso
39
+ # Pedestal angle (float).
40
+ attr_reader :pedestal_angle
41
+ # Pedestal rotation direction (string).
42
+ attr_reader :pedestal_direction
43
+ # Source to surface distance (float).
44
+ attr_reader :ssd
45
+ # Table top angle (float).
46
+ attr_reader :table_top_angle
47
+ # Table top rotation direction (string).
48
+ attr_reader :table_top_direction
49
+ # Table top lateral position (float).
50
+ attr_reader :table_top_lateral
51
+ # Table top longitudinal position (float).
52
+ attr_reader :table_top_longitudinal
53
+ # Table top vertical position (float).
54
+ attr_reader :table_top_vertical
55
+
56
+ # Creates a new control point instance from a Control Point Sequence Item (from an RTPlan file).
57
+ # Returns the ControlPoint instance.
58
+ #
59
+ # === Parameters
60
+ #
61
+ # * <tt>cp_item</tt> -- An item from the Control Point Sequence in the DObject of a RTPlan file.
62
+ # * <tt>beam</tt> -- The Beam instance that this ControlPoint belongs to.
63
+ #
64
+ def self.create_from_item(cp_item, beam)
65
+ raise ArgumentError, "Invalid argument 'cp_item'. Expected DICOM::Item, got #{cp_item.class}." unless cp_item.is_a?(DICOM::Item)
66
+ raise ArgumentError, "Invalid argument 'beam'. Expected Beam, got #{beam.class}." unless beam.is_a?(Beam)
67
+ # Values from the Structure Set ROI Sequence Item:
68
+ index = cp_item.value(CONTROL_POINT_INDEX)
69
+ cum_meterset = cp_item.value(CUM_METERSET_WEIGHT)
70
+ # Create the Beam instance:
71
+ cp = self.new(index, cum_meterset, beam)
72
+ # Set optional values:
73
+ cp.collimator_angle = cp_item.value(COLL_ANGLE)
74
+ cp.collimator_direction = cp_item.value(COLL_DIRECTION)
75
+ cp.energy = cp_item.value(BEAM_ENERGY)
76
+ cp.gantry_angle = cp_item.value(GANTRY_ANGLE)
77
+ cp.gantry_direction = cp_item.value(GANTRY_DIRECTION)
78
+ cp.iso = cp_item.value(ISO_POS)
79
+ cp.pedestal_angle = cp_item.value(PEDESTAL_ANGLE)
80
+ cp.pedestal_direction = cp_item.value(PEDESTAL_DIRECTION)
81
+ cp.ssd = cp_item.value(SSD).to_f if cp_item.exists?(SSD)
82
+ cp.table_top_angle = cp_item.value(TABLE_TOP_ANGLE)
83
+ cp.table_top_direction = cp_item.value(TABLE_TOP_DIRECTION)
84
+ cp.table_top_lateral = cp_item.value(TABLE_TOP_LATERAL)
85
+ cp.table_top_vertical = cp_item.value(TABLE_TOP_VERTICAL)
86
+ cp.table_top_longitudinal = cp_item.value(TABLE_TOP_LONGITUDINAL)
87
+ # Iterate the beam limiting device position items and create Collimator instances:
88
+ if cp_item.exists?(COLL_POS_SQ)
89
+ cp_item[COLL_POS_SQ].each do |coll_item|
90
+ CollimatorSetup.create_from_item(coll_item, cp)
91
+ end
92
+ end
93
+ return cp
94
+ end
95
+
96
+ # Creates a new ControlPoint instance.
97
+ #
98
+ # === Parameters
99
+ #
100
+ # * <tt>index</tt> -- Integer. The control point index.
101
+ # * <tt>meterset</tt> -- The control point's cumulative meterset weight.
102
+ # * <tt>beam</tt> -- The Beam instance that this ControlPoint belongs to.
103
+ # * <tt>options</tt> -- A hash of parameters.
104
+ #
105
+ # === Options
106
+ #
107
+ # * <tt>:type</tt> -- String. Beam type. Defaults to 'STATIC'.
108
+ # * <tt>:delivery_type</tt> -- String. Treatment delivery type. Defaults to 'TREATMENT'.
109
+ # * <tt>:description</tt> -- String. Beam description. Defaults to the 'name' attribute.
110
+ # * <tt>:rad_type</tt> -- String. Radiation type. Defaults to 'PHOTON'.
111
+ # * <tt>:sad</tt> -- Float. Source-axis distance. Defaults to 1000.0.
112
+ # * <tt>:unit</tt> -- String. The primary dosimeter unit. Defaults to 'MU'.
113
+ #
114
+ def initialize(index, cum_meterset, beam)
115
+ raise ArgumentError, "Invalid argument 'beam'. Expected Beam, got #{beam.class}." unless beam.is_a?(Beam)
116
+ # Set values:
117
+ @collimators = Array.new
118
+ @associated_collimators = Hash.new
119
+ self.index = index
120
+ self.cum_meterset = cum_meterset
121
+ # Set references:
122
+ @beam = beam
123
+ # Register ourselves with the Beam:
124
+ @beam.add_control_point(self)
125
+ end
126
+
127
+ # Returns true if the argument is an instance with attributes equal to self.
128
+ #
129
+ def ==(other)
130
+ if other.respond_to?(:to_control_point)
131
+ other.send(:state) == state
132
+ end
133
+ end
134
+
135
+ alias_method :eql?, :==
136
+
137
+ # Adds a CollimatorSetup instance to this ControlPoint.
138
+ #
139
+ def add_collimator(coll)
140
+ raise ArgumentError, "Invalid argument 'coll'. Expected CollimatorSetup, got #{coll.class}." unless coll.is_a?(CollimatorSetup)
141
+ @collimators << coll unless @associated_collimators[coll.type]
142
+ @associated_collimators[coll.type] = coll
143
+ end
144
+
145
+ # Returns the CollimatorSetup instance mathcing the specified device type string (if an argument is used).
146
+ # If a specified type doesn't match, nil is returned.
147
+ # If no argument is passed, the first CollimatorSetup instance associated with the ControlPoint is returned.
148
+ #
149
+ # === Parameters
150
+ #
151
+ # * <tt>type</tt> -- String. The RT Beam Limiting Device Type value.
152
+ #
153
+ def collimator(*args)
154
+ raise ArgumentError, "Expected one or none arguments, got #{args.length}." unless [0, 1].include?(args.length)
155
+ if args.length == 1
156
+ return @associated_collimators[args.first && args.first.to_s]
157
+ else
158
+ # No argument used, therefore we return the first CollimatorSetup instance:
159
+ return @collimators.first
160
+ end
161
+ end
162
+
163
+ # Sets a new beam limiting device angle.
164
+ #
165
+ # === Parameters
166
+ #
167
+ # * <tt>value</tt> -- Float. The beam limiting device angle (300A,0120).
168
+ #
169
+ def collimator_angle=(value)
170
+ @collimator_angle = value && value.to_f
171
+ end
172
+
173
+ # Sets a new beam limiting device rotation direction.
174
+ #
175
+ # === Parameters
176
+ #
177
+ # * <tt>value</tt> -- String. The beam limiting device rotation direction (300A,0121).
178
+ #
179
+ def collimator_direction=(value)
180
+ @collimator_direction = value && value.to_s
181
+ end
182
+
183
+ # Sets a new cumulative meterset weight.
184
+ #
185
+ # === Parameters
186
+ #
187
+ # * <tt>value</tt> -- Float. The cumulative meterset weight (300A,0134).
188
+ #
189
+ def cum_meterset=(value)
190
+ raise ArgumentError, "Argument 'value' must be defined (got #{value.class})." unless value
191
+ @cum_meterset = value && value.to_f
192
+ end
193
+
194
+ # Sets a new nominal beam energy.
195
+ #
196
+ # === Parameters
197
+ #
198
+ # * <tt>value</tt> -- Float. The nominal beam energy (300A,0114).
199
+ #
200
+ def energy=(value)
201
+ @energy = value && value.to_f
202
+ end
203
+
204
+ # Sets a new gantry angle.
205
+ #
206
+ # === Parameters
207
+ #
208
+ # * <tt>value</tt> -- Float. The gantry angle (300A,011E).
209
+ #
210
+ def gantry_angle=(value)
211
+ @gantry_angle = value && value.to_f
212
+ end
213
+
214
+ # Sets a new gantry rotation direction.
215
+ #
216
+ # === Parameters
217
+ #
218
+ # * <tt>value</tt> -- String. The gantry rotation direction (300A,011F).
219
+ #
220
+ def gantry_direction=(value)
221
+ @gantry_direction = value && value.to_s
222
+ end
223
+
224
+ # Generates a Fixnum hash value for this instance.
225
+ #
226
+ def hash
227
+ state.hash
228
+ end
229
+
230
+ # Sets a new control point index.
231
+ #
232
+ # === Parameters
233
+ #
234
+ # * <tt>value</tt> -- Integer. The control point index (300A,0112).
235
+ #
236
+ def index=(value)
237
+ raise ArgumentError, "Argument 'value' must be defined (got #{value.class})." unless value
238
+ @index = value.to_i
239
+ end
240
+
241
+ # Sets a new isosenter position (a coordinate triplet of positions x, y, z).
242
+ #
243
+ # === Parameters
244
+ #
245
+ # * <tt>value</tt> -- Coordinate/String. The isocenter position (300A,0112).
246
+ #
247
+ def iso=(value)
248
+ @iso = value && value.to_coordinate
249
+ end
250
+
251
+ # Sets a new patient support angle.
252
+ #
253
+ # === Parameters
254
+ #
255
+ # * <tt>value</tt> -- Float. The patient support angle (300A,0122).
256
+ #
257
+ def pedestal_angle=(value)
258
+ @pedestal_angle = value && value.to_f
259
+ end
260
+
261
+ # Sets a new patient support rotation direction.
262
+ #
263
+ # === Parameters
264
+ #
265
+ # * <tt>value</tt> -- String. The patient support rotation direction (300A,0123).
266
+ #
267
+ def pedestal_direction=(value)
268
+ @pedestal_direction = value && value.to_s
269
+ end
270
+
271
+ # Sets a new source to surface distance.
272
+ #
273
+ # === Parameters
274
+ #
275
+ # * <tt>value</tt> -- Float. The source to surface distance (300A,012C).
276
+ #
277
+ def ssd=(value)
278
+ @ssd = value && value.to_f
279
+ end
280
+
281
+ # Sets a new table top eccentric angle.
282
+ #
283
+ # === Parameters
284
+ #
285
+ # * <tt>value</tt> -- Float. The table top eccentric angle (300A,0125).
286
+ #
287
+ def table_top_angle=(value)
288
+ @table_top_angle = value && value.to_f
289
+ end
290
+
291
+ # Sets a new table top eccentric rotation direction.
292
+ #
293
+ # === Parameters
294
+ #
295
+ # * <tt>value</tt> -- String. The table top eccentric rotation direction (300A,0126).
296
+ #
297
+ def table_top_direction=(value)
298
+ @table_top_direction = value && value.to_s
299
+ end
300
+
301
+ # Sets a new table top lateral position.
302
+ #
303
+ # === Parameters
304
+ #
305
+ # * <tt>value</tt> -- Float. The table top lateral position (300A,0125).
306
+ #
307
+ def table_top_lateral=(value)
308
+ @table_top_lateral = value && value.to_f
309
+ end
310
+
311
+ # Sets a new table top longitudinal position.
312
+ #
313
+ # === Parameters
314
+ #
315
+ # * <tt>value</tt> -- Float. The table top longitudinal position (300A,0125).
316
+ #
317
+ def table_top_longitudinal=(value)
318
+ @table_top_longitudinal = value && value.to_f
319
+ end
320
+
321
+ # Sets a new table top vertical position.
322
+ #
323
+ # === Parameters
324
+ #
325
+ # * <tt>value</tt> -- Float. The table top vertical position (300A,0125).
326
+ #
327
+ def table_top_vertical=(value)
328
+ @table_top_vertical = value && value.to_f
329
+ end
330
+
331
+ # Returns self.
332
+ #
333
+ def to_control_point
334
+ self
335
+ end
336
+
337
+ # Creates and returns a Control Point Sequence Item from the attributes of the ControlPoint.
338
+ #
339
+ def to_item
340
+ item = DICOM::Item.new
341
+ item.add(DICOM::Element.new(ROI_COLOR, @color))
342
+ s = DICOM::Sequence.new(CONTOUR_SQ)
343
+ item.add(s)
344
+ item.add(DICOM::Element.new(REF_ROI_NUMBER, @number.to_s))
345
+ # Add Contour items to the Contour Sequence (one or several items per Slice):
346
+ @slices.each do |slice|
347
+ slice.contours.each do |contour|
348
+ s.add_item(contour.to_item)
349
+ end
350
+ end
351
+ return item
352
+ end
353
+
354
+
355
+ private
356
+
357
+
358
+ # Returns the attributes of this instance in an array (for comparison purposes).
359
+ #
360
+ def state
361
+ [@collimators, @collimator_angle, @collimator_direction, @cum_meterset, @energy,
362
+ @gantry_angle, @gantry_direction, @index, @iso, @pedestal_angle, @pedestal_direction,
363
+ @ssd, @table_top_angle, @table_top_direction, @table_top_lateral,
364
+ @table_top_longitudinal, @table_top_vertical
365
+ ]
366
+ end
367
+
368
+
369
+ end
370
+
371
+ end