rtp-connect 1.6 → 1.11
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/{CHANGELOG.rdoc → CHANGELOG.md} +137 -90
- data/COPYING +674 -674
- data/Gemfile +2 -2
- data/Gemfile.lock +31 -21
- data/README.md +161 -0
- data/lib/rtp-connect.rb +1 -0
- data/lib/rtp-connect/constants.rb +58 -57
- data/lib/rtp-connect/control_point.rb +158 -118
- data/lib/rtp-connect/dose_tracking.rb +37 -54
- data/lib/rtp-connect/extended_field.rb +36 -69
- data/lib/rtp-connect/extended_plan.rb +127 -0
- data/lib/rtp-connect/field.rb +158 -143
- data/lib/rtp-connect/methods.rb +85 -62
- data/lib/rtp-connect/plan.rb +645 -636
- data/lib/rtp-connect/plan_to_dcm.rb +668 -694
- data/lib/rtp-connect/prescription.rb +57 -74
- data/lib/rtp-connect/record.rb +225 -57
- data/lib/rtp-connect/ruby_extensions.rb +34 -3
- data/lib/rtp-connect/simulation_field.rb +606 -701
- data/lib/rtp-connect/site_setup.rb +112 -80
- data/lib/rtp-connect/version.rb +5 -5
- data/rakefile.rb +0 -1
- data/rtp-connect.gemspec +27 -27
- metadata +67 -58
- data/README.rdoc +0 -136
data/Gemfile
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
source "http://www.rubygems.org"
|
2
|
-
|
1
|
+
source "http://www.rubygems.org"
|
2
|
+
|
3
3
|
gemspec
|
data/Gemfile.lock
CHANGED
@@ -1,35 +1,45 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
rtp-connect (1.
|
4
|
+
rtp-connect (1.11)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: http://www.rubygems.org/
|
8
8
|
specs:
|
9
|
-
dicom (0.9.
|
10
|
-
diff-lcs (1.2.
|
11
|
-
metaclass (0.0.
|
12
|
-
mocha (
|
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 (
|
15
|
-
|
16
|
-
|
17
|
-
rspec-
|
18
|
-
rspec-
|
19
|
-
|
20
|
-
rspec-
|
21
|
-
|
22
|
-
rspec-
|
23
|
-
|
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.
|
30
|
-
dicom (~> 0.9.
|
31
|
-
mocha (~>
|
32
|
-
rake (~>
|
33
|
-
|
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.
|
42
|
+
yard (~> 0.9, >= 0.9.12)
|
43
|
+
|
44
|
+
BUNDLED WITH
|
45
|
+
1.16.5
|
data/README.md
ADDED
@@ -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!
|
data/lib/rtp-connect.rb
CHANGED
@@ -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
|
-
"
|
46
|
-
"
|
47
|
-
"
|
48
|
-
"
|
49
|
-
"
|
50
|
-
"
|
51
|
-
"
|
52
|
-
"
|
53
|
-
"
|
54
|
-
"
|
55
|
-
"
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
162
|
-
|
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
|
-
|
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
|
-
|
182
|
-
|
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 *
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
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
|
-
# @
|
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
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
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
|
-
#
|
598
|
-
# 'scale_convention' attribute.
|
595
|
+
# Converts the collimator1 attribute to proper DICOM format.
|
599
596
|
#
|
600
|
-
# @param [
|
601
|
-
# @return [
|
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
|
604
|
-
|
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
|