rtp-connect 1.6 → 1.11

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/Gemfile CHANGED
@@ -1,3 +1,3 @@
1
- source "http://www.rubygems.org"
2
-
1
+ source "http://www.rubygems.org"
2
+
3
3
  gemspec
@@ -1,35 +1,45 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rtp-connect (1.6)
4
+ rtp-connect (1.11)
5
5
 
6
6
  GEM
7
7
  remote: http://www.rubygems.org/
8
8
  specs:
9
- dicom (0.9.5)
10
- diff-lcs (1.2.1)
11
- metaclass (0.0.1)
12
- mocha (0.13.2)
9
+ dicom (0.9.8)
10
+ diff-lcs (1.2.5)
11
+ metaclass (0.0.4)
12
+ mocha (1.1.0)
13
13
  metaclass (~> 0.0.1)
14
- rake (0.9.6)
15
- rspec (2.13.0)
16
- rspec-core (~> 2.13.0)
17
- rspec-expectations (~> 2.13.0)
18
- rspec-mocks (~> 2.13.0)
19
- rspec-core (2.13.0)
20
- rspec-expectations (2.13.0)
21
- diff-lcs (>= 1.1.3, < 2.0)
22
- rspec-mocks (2.13.0)
23
- yard (0.8.5)
14
+ rake (12.3.1)
15
+ redcarpet (3.4.0)
16
+ rspec (3.7.0)
17
+ rspec-core (~> 3.7.0)
18
+ rspec-expectations (~> 3.7.0)
19
+ rspec-mocks (~> 3.7.0)
20
+ rspec-core (3.7.1)
21
+ rspec-support (~> 3.7.0)
22
+ rspec-expectations (3.7.0)
23
+ diff-lcs (>= 1.2.0, < 2.0)
24
+ rspec-support (~> 3.7.0)
25
+ rspec-mocks (3.7.0)
26
+ diff-lcs (>= 1.2.0, < 2.0)
27
+ rspec-support (~> 3.7.0)
28
+ rspec-support (3.7.1)
29
+ yard (0.9.12)
24
30
 
25
31
  PLATFORMS
26
32
  x86-mingw32
27
33
 
28
34
  DEPENDENCIES
29
- bundler (~> 1.3)
30
- dicom (~> 0.9.5)
31
- mocha (~> 0.13)
32
- rake (~> 0.9.6)
33
- rspec (~> 2.13)
35
+ bundler (~> 1.11)
36
+ dicom (~> 0.9, >= 0.9.8)
37
+ mocha (~> 1.1)
38
+ rake (~> 12.3)
39
+ redcarpet (~> 3.4)
40
+ rspec (~> 3.7)
34
41
  rtp-connect!
35
- yard (~> 0.8.5)
42
+ yard (~> 0.9, >= 0.9.12)
43
+
44
+ BUNDLED WITH
45
+ 1.16.5
@@ -0,0 +1,161 @@
1
+ [![DOI](https://zenodo.org/badge/doi/10.5281/zenodo.11752.png)](http://dx.doi.org/10.5281/zenodo.11752)
2
+
3
+ # RTPConnect
4
+
5
+ The RTPConnect library allows you to read, edit and write RTPConnect files in Ruby.
6
+ RTPConnect is a file format used in radiotherapy (e.g. Mosaiq) for export & import
7
+ of treatment planning data. The library is written entirely in Ruby and has no
8
+ external dependencies.
9
+
10
+
11
+ ## INSTALLATION
12
+
13
+ gem install rtp-connect
14
+
15
+
16
+ ## REQUIREMENTS
17
+
18
+ * Ruby 1.9.3 (or higher)
19
+
20
+
21
+ ## BASIC USAGE
22
+
23
+ ### Load & Include
24
+
25
+ require 'rtp-connect'
26
+ include RTP
27
+
28
+ ### Read, modify and write
29
+
30
+ # Read file:
31
+ rtp = Plan.read('some_file.rtp')
32
+ # Extract the Patient's Name:
33
+ name = rtp.patient_last_name
34
+ # Modify the Patient's Name:
35
+ rtp.patient_last_name = 'Anonymous'
36
+ # Write to file:
37
+ rtp.write('new_file.rtp')
38
+
39
+ ### Create a new Plan Definition Record from scratch
40
+
41
+ # Create the instance:
42
+ rtp = Plan.new
43
+ # Set the Patient's ID attribute:
44
+ rtp.patient_id = '12345'
45
+ # Export the instance to an RTP string (with CRC):
46
+ output = rtp.to_s
47
+
48
+ ### Fix invalid RTP files:
49
+
50
+ # Read an RTP file containing invalid checksum(s):
51
+ rtp = Plan.read('invalid_crc.rtp', ignore_crc: true)
52
+ # Read an RTP file containing unknown record type(s):
53
+ rtp = Plan.read('custom.rtp', skip_unknown: true)
54
+ # Read an RTP file containing invalid CSV format:
55
+ rtp = Plan.read('invalid_csv.rtp', repair: true)
56
+ # Write a corrected RTP file:
57
+ rtp.write('valid.rtp')
58
+
59
+ ### Write RTP files for specific Mosaiq versions:
60
+
61
+ # Mosaiq 2.4:
62
+ rtp.write('treatment_plan.rtp', version: 2.4)
63
+ # Mosaiq 2.5:
64
+ rtp.write('treatment_plan.rtp', version: 2.5)
65
+ # Mosaiq 2.6 (and 2.62):
66
+ rtp.write('treatment_plan.rtp', version: 2.6)
67
+ # By default files are outputted at the latest supported version (currently 2.64) when omitting the version parameter:
68
+ rtp.write('treatment_plan.rtp')
69
+
70
+ ### Convert an RTP file to DICOM:
71
+
72
+ p = Plan.read('some_file.rtp')
73
+ dcm = p.to_dcm
74
+ dcm.write('rtplan.dcm')
75
+
76
+ ### Log settings
77
+
78
+ # Change the log level so that only error messages are displayed:
79
+ RTP.logger.level = Logger::ERROR
80
+ # Setting up a simple file log:
81
+ l = Logger.new('my_logfile.log')
82
+ RTP.logger = l
83
+ # Create a logger which ages logfile daily/monthly:
84
+ RTP.logger = Logger.new('foo.log', 'daily')
85
+ RTP.logger = Logger.new('foo.log', 'monthly')
86
+
87
+ ### Scripts
88
+
89
+ For more comprehensive and useful examples, check out the scripts folder
90
+ which contains various Ruby scripts that intends to show off real world
91
+ usage scenarios of the RTPConnect library.
92
+
93
+ ### IRB Tip
94
+
95
+ When working with the RTPConnect library in irb, you may be annoyed with all
96
+ the information that is printed to screen, regardless of your log level.
97
+ This is because in irb every variable loaded in the program is
98
+ automatically printed to the screen. A useful hack to avoid this effect is
99
+ to append ";0" after a command.
100
+
101
+ Example:
102
+
103
+ rtp = Plan.read('some_file.rtp') ;0
104
+
105
+
106
+ ## RESOURCES
107
+
108
+ * [Rubygems download](https://rubygems.org/gems/rtp-connect)
109
+ * [Documentation](http://rubydoc.info/gems/rtp-connect/frames)
110
+ * [Source code repository](https://github.com/dicom/rtp-connect)
111
+
112
+
113
+ ## RESTRICTIONS
114
+
115
+ ### Supported records
116
+
117
+ * Plan definition [PLAN_DEF]
118
+ * Extended plan definition [EXTENDED_PLAN_DEF]
119
+ * Prescription site [RX_DEF]
120
+ * Site setup [SITE_SETUP_DEF]
121
+ * Simulation field [SIM_DEF]
122
+ * Treatment field [FIELD_DEF]
123
+ * Extended treatment field [EXTENDED_FIELD_DEF]
124
+ * Control point record [CONTROL_PT_DEF]
125
+ * Dose tracking record [DOSE_DEF]
126
+
127
+ ### Unsupported records
128
+
129
+ * Document based treatment field [PDF_FIELD_DEF]
130
+ * Multileaf collimator [MLC_DEF]
131
+ * MLC shape [MLC_SHAPE_DEF]
132
+ * Dose action points [DOSE_ACTION]
133
+
134
+ If you encounter an RTP file with an unsupported record type, please contact me.
135
+
136
+
137
+ ## COPYRIGHT
138
+
139
+ Copyright 2011-2020 Christoffer Lervåg
140
+
141
+ This program is free software: you can redistribute it and/or modify
142
+ it under the terms of the GNU General Public License as published by
143
+ the Free Software Foundation, either version 3 of the License, or
144
+ (at your option) any later version.
145
+
146
+ This program is distributed in the hope that it will be useful,
147
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
148
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
149
+ GNU General Public License for more details.
150
+
151
+ You should have received a copy of the GNU General Public License
152
+ along with this program. If not, see http://www.gnu.org/licenses/ .
153
+
154
+
155
+ ## ABOUT THE AUTHOR
156
+
157
+ * Name: Christoffer Lervåg
158
+ * Location: Norway
159
+ * Email: chris.lervag [@nospam.com] @gmail.com
160
+
161
+ Please don't hesitate to email me if you have any feedback related to this project!
@@ -6,6 +6,7 @@ require_relative 'rtp-connect/logging'
6
6
  require_relative 'rtp-connect/record'
7
7
  # Core library:
8
8
  require_relative 'rtp-connect/plan'
9
+ require_relative 'rtp-connect/extended_plan'
9
10
  require_relative 'rtp-connect/plan_to_dcm'
10
11
  require_relative 'rtp-connect/prescription'
11
12
  require_relative 'rtp-connect/site_setup'
@@ -1,58 +1,59 @@
1
- module RTP
2
-
3
- # The seed value used in the RTPConnect implementation of the CCITT algorithm.
4
- CRC_SEED = 0x0521
5
-
6
- # The table & values used in the RTPConnect implementation of the CCITT algorithm.
7
- CRC_TABLE = [
8
- 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
9
- 0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
10
- 0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,
11
- 0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841,
12
- 0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40,
13
- 0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41,
14
- 0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641,
15
- 0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040,
16
- 0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240,
17
- 0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441,
18
- 0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41,
19
- 0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840,
20
- 0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41,
21
- 0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40,
22
- 0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640,
23
- 0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041,
24
- 0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240,
25
- 0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441,
26
- 0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41,
27
- 0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840,
28
- 0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41,
29
- 0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40,
30
- 0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640,
31
- 0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041,
32
- 0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241,
33
- 0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440,
34
- 0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40,
35
- 0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841,
36
- 0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40,
37
- 0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41,
38
- 0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641,
39
- 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040
40
- ]
41
-
42
- # Pairs of RTPConnect keywords and parse method names.
43
- PARSE_METHOD = {
44
- "PLAN_DEF" => :plan_definition,
45
- "RX_DEF" => :prescription_site,
46
- "SITE_SETUP_DEF" => :site_setup,
47
- "SIM_DEF" => :simulation_field,
48
- "FIELD_DEF" => :treatment_field,
49
- "EXTENDED_FIELD_DEF" => :extended_treatment_field,
50
- "PDF_FIELD_DEF" => :document_based_treatment_field,
51
- "MLC_DEF" => :multileaf_collimator,
52
- "CONTROL_PT_DEF" => :control_point,
53
- "MLC_SHAPE_DEF" => :mlc_shape,
54
- "DOSE_DEF" => :dose_tracking,
55
- "DOSE_ACTION" => :dose_action,
56
- }
57
-
1
+ module RTP
2
+
3
+ # The seed value used in the RTPConnect implementation of the CCITT algorithm.
4
+ CRC_SEED = 0x0521
5
+
6
+ # The table & values used in the RTPConnect implementation of the CCITT algorithm.
7
+ CRC_TABLE = [
8
+ 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
9
+ 0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
10
+ 0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,
11
+ 0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841,
12
+ 0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40,
13
+ 0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41,
14
+ 0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641,
15
+ 0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040,
16
+ 0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240,
17
+ 0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441,
18
+ 0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41,
19
+ 0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840,
20
+ 0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41,
21
+ 0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40,
22
+ 0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640,
23
+ 0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041,
24
+ 0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240,
25
+ 0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441,
26
+ 0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41,
27
+ 0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840,
28
+ 0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41,
29
+ 0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40,
30
+ 0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640,
31
+ 0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041,
32
+ 0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241,
33
+ 0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440,
34
+ 0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40,
35
+ 0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841,
36
+ 0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40,
37
+ 0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41,
38
+ 0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641,
39
+ 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040
40
+ ]
41
+
42
+ # Pairs of RTPConnect keywords and parse method names.
43
+ PARSE_METHOD = {
44
+ "PLAN_DEF" => :plan_definition,
45
+ "EXTENDED_PLAN_DEF" => :extended_plan_def,
46
+ "RX_DEF" => :prescription_site,
47
+ "SITE_SETUP_DEF" => :site_setup,
48
+ "SIM_DEF" => :simulation_field,
49
+ "FIELD_DEF" => :treatment_field,
50
+ "EXTENDED_FIELD_DEF" => :extended_treatment_field,
51
+ "PDF_FIELD_DEF" => :document_based_treatment_field,
52
+ "MLC_DEF" => :multileaf_collimator,
53
+ "CONTROL_PT_DEF" => :control_point,
54
+ "MLC_SHAPE_DEF" => :mlc_shape,
55
+ "DOSE_DEF" => :dose_tracking,
56
+ "DOSE_ACTION" => :dose_action,
57
+ }
58
+
58
59
  end
@@ -8,8 +8,11 @@ module RTP
8
8
  #
9
9
  class ControlPoint < Record
10
10
 
11
+ # The number of attributes not having their own variable for this record (200 - 2).
12
+ NR_SURPLUS_ATTRIBUTES = 198
13
+
11
14
  # The Record which this instance belongs to.
12
- attr_reader :parent
15
+ attr_accessor :parent
13
16
  # The MLC shape record (if any) that belongs to this ControlPoint.
14
17
  attr_reader :mlc_shape
15
18
  attr_reader :field_id
@@ -43,6 +46,9 @@ module RTP
43
46
  attr_reader :couch_dir
44
47
  attr_reader :couch_pedestal
45
48
  attr_reader :couch_ped_dir
49
+ attr_reader :iso_pos_x
50
+ attr_reader :iso_pos_y
51
+ attr_reader :iso_pos_z
46
52
  # Note: This attribute contains an array of all MLC LP A values (leaves 1..100).
47
53
  attr_reader :mlc_lp_a
48
54
  # Note: This attribute contains an array of all MLC LP B values (leaves 1..100).
@@ -56,50 +62,8 @@ module RTP
56
62
  # @raise [ArgumentError] if given a string containing an invalid number of elements
57
63
  #
58
64
  def self.load(string, parent)
59
- # Get the quote-less values:
60
- values = string.to_s.values
61
- low_limit = 233
62
- high_limit = 233
63
- raise ArgumentError, "Invalid argument 'string': Expected at least #{low_limit} elements, got #{values.length}." if values.length < low_limit
64
- RTP.logger.warn "The number of elements (#{values.length}) for this ControlPoint 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
65
65
  cp = self.new(parent)
66
- # Assign the values to attributes:
67
- cp.keyword = values[0]
68
- cp.field_id = values[1]
69
- cp.mlc_type = values[2]
70
- cp.mlc_leaves = values[3]
71
- cp.total_control_points = values[4]
72
- cp.control_pt_number = values[5]
73
- cp.mu_convention = values[6]
74
- cp.monitor_units = values[7]
75
- cp.wedge_position = values[8]
76
- cp.energy = values[9]
77
- cp.doserate = values[10]
78
- cp.ssd = values[11]
79
- cp.scale_convention = values[12]
80
- cp.gantry_angle = values[13]
81
- cp.gantry_dir = values[14]
82
- cp.collimator_angle = values[15]
83
- cp.collimator_dir = values[16]
84
- cp.field_x_mode = values[17]
85
- cp.field_x = values[18]
86
- cp.collimator_x1 = values[19]
87
- cp.collimator_x2 = values[20]
88
- cp.field_y_mode = values[21]
89
- cp.field_y = values[22]
90
- cp.collimator_y1 = values[23]
91
- cp.collimator_y2 = values[24]
92
- cp.couch_vertical = values[25]
93
- cp.couch_lateral = values[26]
94
- cp.couch_longitudinal = values[27]
95
- cp.couch_angle = values[28]
96
- cp.couch_dir = values[29]
97
- cp.couch_pedestal = values[30]
98
- cp.couch_ped_dir = values[31]
99
- cp.mlc_lp_a = [*values[32..131]]
100
- cp.mlc_lp_b = [*values[132..231]]
101
- cp.crc = values[-1]
102
- return cp
66
+ cp.load(string)
103
67
  end
104
68
 
105
69
  # Creates a new ControlPoint.
@@ -107,14 +71,54 @@ module RTP
107
71
  # @param [Record] parent a record which is used to determine the proper parent of this instance
108
72
  #
109
73
  def initialize(parent)
74
+ super('CONTROL_PT_DEF', 233, 236)
110
75
  # Child:
111
76
  @mlc_shape = nil
112
77
  # Parent relation (may get more than one type of record here):
113
78
  @parent = get_parent(parent.to_record, Field)
114
79
  @parent.add_control_point(self)
115
- @keyword = 'CONTROL_PT_DEF'
116
80
  @mlc_lp_a = Array.new(100)
117
81
  @mlc_lp_b = Array.new(100)
82
+ @attributes = [
83
+ # Required:
84
+ :keyword,
85
+ :field_id,
86
+ :mlc_type,
87
+ :mlc_leaves,
88
+ :total_control_points,
89
+ :control_pt_number,
90
+ :mu_convention,
91
+ :monitor_units,
92
+ :wedge_position,
93
+ :energy,
94
+ :doserate,
95
+ :ssd,
96
+ :scale_convention,
97
+ :gantry_angle,
98
+ :gantry_dir,
99
+ :collimator_angle,
100
+ :collimator_dir,
101
+ :field_x_mode,
102
+ :field_x,
103
+ :collimator_x1,
104
+ :collimator_x2,
105
+ :field_y_mode,
106
+ :field_y,
107
+ :collimator_y1,
108
+ :collimator_y2,
109
+ :couch_vertical,
110
+ :couch_lateral,
111
+ :couch_longitudinal,
112
+ :couch_angle,
113
+ :couch_dir,
114
+ :couch_pedestal,
115
+ :couch_ped_dir,
116
+ :iso_pos_x,
117
+ :iso_pos_y,
118
+ :iso_pos_z,
119
+ :mlc_lp_a,
120
+ :mlc_lp_b
121
+ ]
118
122
  end
119
123
 
120
124
  # Checks for equality.
@@ -145,51 +149,51 @@ module RTP
145
149
 
146
150
  # Converts the collimator_x1 attribute to proper DICOM format.
147
151
  #
152
+ # @param [Symbol] scale if set, relevant device parameters are converted from a native readout format to IEC1217 (supported values are :elekta & :varian)
148
153
  # @return [Float] the DICOM-formatted collimator_x1 attribute
149
154
  #
150
- def dcm_collimator_x1
151
- attribute = (scale_convertion? ? :collimator_y1 : :collimator_x1)
152
- target = (@field_x_mode && !@field_x_mode.empty? ? self : @parent)
153
- target.send(attribute).to_f * 10 * scale_factor
155
+ def dcm_collimator_x1(scale=nil)
156
+ dcm_collimator_1(scale, default_axis=:x)
154
157
  end
155
158
 
156
159
  # Converts the collimator_x2 attribute to proper DICOM format.
157
160
  #
161
+ # @param [Symbol] scale if set, relevant device parameters are converted from native readout format to IEC1217 (supported values are :elekta & :varian)
158
162
  # @return [Float] the DICOM-formatted collimator_x2 attribute
159
163
  #
160
- def dcm_collimator_x2
161
- attribute = (scale_convertion? ? :collimator_y2 : :collimator_x2)
162
- target = (@field_x_mode && !@field_x_mode.empty? ? self : @parent)
163
- target.send(attribute).to_f * 10
164
+ def dcm_collimator_x2(scale=nil)
165
+ axis = (scale == :elekta ? :y : :x)
166
+ dcm_collimator(axis, coeff=1, side=2)
164
167
  end
165
168
 
166
169
  # Converts the collimator_y1 attribute to proper DICOM format.
167
170
  #
171
+ # @param [Symbol] scale if set, relevant device parameters are converted from native readout format to IEC1217 (supported values are :elekta & :varian)
168
172
  # @return [Float] the DICOM-formatted collimator_y1 attribute
169
173
  #
170
- def dcm_collimator_y1
171
- attribute = (scale_convertion? ? :collimator_x1 : :collimator_y1)
172
- target = (@field_y_mode && !@field_y_mode.empty? ? self : @parent)
173
- target.send(attribute).to_f * 10 * scale_factor
174
+ def dcm_collimator_y1(scale=nil)
175
+ dcm_collimator_1(scale, default_axis=:y)
174
176
  end
175
177
 
176
178
  # Converts the collimator_y2 attribute to proper DICOM format.
177
179
  #
180
+ # @param [Symbol] scale if set, relevant device parameters are converted from native readout format to IEC1217 (supported values are :elekta & :varian)
178
181
  # @return [Float] the DICOM-formatted collimator_y2 attribute
179
182
  #
180
- def dcm_collimator_y2
181
- attribute = (scale_convertion? ? :collimator_x2 : :collimator_y2)
182
- target = (@field_y_mode && !@field_y_mode.empty? ? self : @parent)
183
- target.send(attribute).to_f * 10
183
+ def dcm_collimator_y2(scale=nil)
184
+ axis = (scale == :elekta ? :x : :y)
185
+ dcm_collimator(axis, coeff=1, side=2)
184
186
  end
185
187
 
186
188
  # Converts the mlc_lp_a & mlc_lp_b attributes to a proper DICOM formatted string.
187
189
  #
190
+ # @param [Symbol] scale if set, relevant device parameters are converted from native readout format to IEC1217 (supported values are :elekta & :varian)
188
191
  # @return [String] the DICOM-formatted leaf pair positions
189
192
  #
190
- def dcm_mlc_positions
193
+ def dcm_mlc_positions(scale=nil)
194
+ coeff = (scale == :elekta ? -1 : 1)
191
195
  # As with the collimators, the first side (1/a) may need scale invertion:
192
- pos_a = @mlc_lp_a.collect{|p| (p.to_f * 10 * scale_factor).round(1) unless p.empty?}.compact
196
+ pos_a = @mlc_lp_a.collect{|p| (p.to_f * 10 * coeff).round(1) unless p.empty?}.compact
193
197
  pos_b = @mlc_lp_b.collect{|p| (p.to_f * 10).round(1) unless p.empty?}.compact
194
198
  (pos_a + pos_b).join("\\")
195
199
  end
@@ -219,7 +223,7 @@ module RTP
219
223
  # @return [Array<String>] an array of attributes (in the same order as they appear in the RTP string)
220
224
  #
221
225
  def values
222
- return [
226
+ [
223
227
  @keyword,
224
228
  @field_id,
225
229
  @mlc_type,
@@ -252,6 +256,9 @@ module RTP
252
256
  @couch_dir,
253
257
  @couch_pedestal,
254
258
  @couch_ped_dir,
259
+ @iso_pos_x,
260
+ @iso_pos_y,
261
+ @iso_pos_z,
255
262
  *@mlc_lp_a,
256
263
  *@mlc_lp_b
257
264
  ]
@@ -265,23 +272,6 @@ module RTP
265
272
  self
266
273
  end
267
274
 
268
- # Encodes the ControlPoint object + any hiearchy of child objects,
269
- # to a properly formatted RTPConnect ascii string.
270
- #
271
- # @return [String] an RTP string with a single or multiple lines/records
272
- #
273
- def to_s
274
- str = encode
275
- if children
276
- children.each do |child|
277
- str += child.to_s
278
- end
279
- end
280
- return str
281
- end
282
-
283
- alias :to_str :to_s
284
-
285
275
  # Sets the mlc_lp_a attribute.
286
276
  #
287
277
  # @note As opposed to the ordinary (string) attributes, this attribute
@@ -289,9 +279,7 @@ module RTP
289
279
  # @param [Array<nil, #to_s>] array the new attribute values
290
280
  #
291
281
  def mlc_lp_a=(array)
292
- array = array.to_a
293
- raise ArgumentError, "Invalid argument 'array'. Expected length 100, got #{array.length}." unless array.length == 100
294
- @mlc_lp_a = array.collect! {|e| e && e.to_s.strip}
282
+ @mlc_lp_a = array.to_a.validate_and_process(100)
295
283
  end
296
284
 
297
285
  # Sets the mlc_lp_b attribute.
@@ -301,21 +289,7 @@ module RTP
301
289
  # @param [Array<nil, #to_s>] array the new attribute values
302
290
  #
303
291
  def mlc_lp_b=(array)
304
- array = array.to_a
305
- raise ArgumentError, "Invalid argument 'array'. Expected length 100, got #{array.length}." unless array.length == 100
306
- @mlc_lp_b = array.collect! {|e| e && e.to_s.strip}
307
- end
308
-
309
- # Sets the keyword attribute.
310
- #
311
- # @note Since only a specific string is accepted, this is more of an argument check than a traditional setter method
312
- # @param [#to_s] value the new attribute value
313
- # @raise [ArgumentError] if given an unexpected keyword
314
- #
315
- def keyword=(value)
316
- value = value.to_s.upcase
317
- raise ArgumentError, "Invalid keyword. Expected 'CONTROL_PT_DEF', got #{value}." unless value == "CONTROL_PT_DEF"
318
- @keyword = value
292
+ @mlc_lp_b = array.to_a.validate_and_process(100)
319
293
  end
320
294
 
321
295
  # Sets the field_id attribute.
@@ -566,6 +540,30 @@ module RTP
566
540
  @couch_ped_dir = value && value.to_s
567
541
  end
568
542
 
543
+ # Sets the iso_pos_x attribute.
544
+ #
545
+ # @param [nil, #to_s] value the new attribute value
546
+ #
547
+ def iso_pos_x=(value)
548
+ @iso_pos_x = value && value.to_s.strip
549
+ end
550
+
551
+ # Sets the iso_pos_y attribute.
552
+ #
553
+ # @param [nil, #to_s] value the new attribute value
554
+ #
555
+ def iso_pos_y=(value)
556
+ @iso_pos_y = value && value.to_s.strip
557
+ end
558
+
559
+ # Sets the iso_pos_z attribute.
560
+ #
561
+ # @param [nil, #to_s] value the new attribute value
562
+ #
563
+ def iso_pos_z=(value)
564
+ @iso_pos_z = value && value.to_s.strip
565
+ end
566
+
569
567
 
570
568
  private
571
569
 
@@ -577,31 +575,73 @@ module RTP
577
575
  #
578
576
  alias_method :state, :values
579
577
 
580
- # Checks whether the contents of the this record indicates that scale
581
- # convertion is to be applied. This convertion entails converting a value
582
- # from IEC1217 format to the target machine's native readout format.
583
- # Note that the scope of this scale conversion is not precisely known (the
584
- # current implementation is based on a few observations made from a single
585
- # RTP file).
578
+ # Converts the collimator attribute to proper DICOM format.
586
579
  #
587
- # @return [Boolean] true if the scale convention attribute indicates scale convertion
580
+ # @param [Symbol] axis a representation for the axis of interest (x or y)
581
+ # @param [Integer] coeff a coeffecient (of -1 or 1) which the attribute is multiplied with
582
+ # @param [Integer] nr collimator side/index (1 or 2)
583
+ # @return [Float] the DICOM-formatted collimator attribute
588
584
  #
589
- def scale_convertion?
590
- # A scale convention of 1 means that geometric parameters are represented
591
- # in the target machine's native readout format, as opposed to the IEC 1217
592
- # convention. The consequences of this is not totally clear, but at least for
593
- # an Elekta device, there are a number of convertions which seems to be indicated.
594
- @scale_convention.to_i == 1 ? true : false
585
+ def dcm_collimator(axis, coeff, nr)
586
+ mode = self.send("field_#{axis}_mode")
587
+ if mode && !mode.empty?
588
+ target = self
589
+ else
590
+ target = @parent
591
+ end
592
+ target.send("collimator_#{axis}#{nr}").to_f * 10 * coeff
595
593
  end
596
594
 
597
- # Gives a factor used for scale convertion, which depends on the
598
- # 'scale_convention' attribute.
595
+ # Converts the collimator1 attribute to proper DICOM format.
599
596
  #
600
- # @param [Numerical] value the value to process
601
- # @return [Numerical] the scale converted value
597
+ # @param [Symbol] scale if set, relevant device parameters are converted from a native readout format to IEC1217 (supported values are :elekta & :varian)
598
+ # @return [Float] the DICOM-formatted collimator_x1 attribute
602
599
  #
603
- def scale_factor
604
- scale_convertion? ? -1 : 1
600
+ def dcm_collimator_1(scale=nil, axis)
601
+ coeff = 1
602
+ if scale == :elekta
603
+ axis = (axis == :x ? :y : :x)
604
+ coeff = -1
605
+ elsif scale == :varian
606
+ coeff = -1
607
+ end
608
+ dcm_collimator(axis, coeff, side=1)
609
+ end
610
+
611
+ # Gives an array of indices indicating where the attributes of this record gets its
612
+ # values from in the comma separated string which the instance is created from.
613
+ #
614
+ # @param [Integer] length the number of elements to create in the indices array
615
+ #
616
+ def import_indices(length)
617
+ # Note that this method is defined in the parent Record class, where it is
618
+ # used for most record types. However, because this record has two attributes
619
+ # which contain an array of values, we use a custom import_indices method.
620
+ #
621
+ # Furthermore, as of Mosaiq version 2.64, the RTP ControlPoint record includes
622
+ # 3 new attributes: iso_pos_x/y/z. Since these (unfortunately) are not placed
623
+ # at the end of the record (which is the norm), but rather inserted before the
624
+ # MLC leaf positions, we have to take special care here to make sure that this
625
+ # gets right for records where these are included or excluded.
626
+ #
627
+ # Override length:
628
+ applied_length = 235
629
+ ind = Array.new(applied_length - NR_SURPLUS_ATTRIBUTES) { |i| [i] }
630
+ # Override indices for mlc_pl_a and mlc_lp_b:
631
+ # Allocation here is dependent on the RTP file version:
632
+ # For 2.62 and earlier, where length is 232, we dont have the 3 iso_pos_x/y/z values preceeding the mlc arrays leaf position arrays.
633
+ # For 2.64 (and later), where length is 235, we have the 3 iso_pos_x/y/z values preceeding the mlc leaf position arrays.
634
+ if length == 232
635
+ ind[32] = nil
636
+ ind[33] = nil
637
+ ind[34] = nil
638
+ ind[35] = (32..131).to_a
639
+ ind[36] = (132..231).to_a
640
+ else # (length = 235)
641
+ ind[35] = (35..134).to_a
642
+ ind[36] = (135..234).to_a
643
+ end
644
+ ind
605
645
  end
606
646
 
607
647
  end