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.
- data/CHANGELOG.rdoc +10 -0
- data/COPYING +674 -0
- data/README.rdoc +107 -0
- data/lib/rtkit.rb +68 -0
- data/lib/rtkit/beam.rb +346 -0
- data/lib/rtkit/bin_image.rb +578 -0
- data/lib/rtkit/bin_matcher.rb +241 -0
- data/lib/rtkit/bin_volume.rb +263 -0
- data/lib/rtkit/collimator.rb +157 -0
- data/lib/rtkit/collimator_setup.rb +143 -0
- data/lib/rtkit/constants.rb +215 -0
- data/lib/rtkit/contour.rb +213 -0
- data/lib/rtkit/control_point.rb +371 -0
- data/lib/rtkit/coordinate.rb +83 -0
- data/lib/rtkit/data_set.rb +264 -0
- data/lib/rtkit/dose.rb +70 -0
- data/lib/rtkit/dose_distribution.rb +206 -0
- data/lib/rtkit/dose_volume.rb +280 -0
- data/lib/rtkit/frame.rb +164 -0
- data/lib/rtkit/image.rb +372 -0
- data/lib/rtkit/image_series.rb +290 -0
- data/lib/rtkit/logging.rb +158 -0
- data/lib/rtkit/methods.rb +105 -0
- data/lib/rtkit/mixins/image_parent.rb +40 -0
- data/lib/rtkit/patient.rb +229 -0
- data/lib/rtkit/pixel_data.rb +237 -0
- data/lib/rtkit/plan.rb +259 -0
- data/lib/rtkit/plane.rb +165 -0
- data/lib/rtkit/roi.rb +388 -0
- data/lib/rtkit/rt_dose.rb +237 -0
- data/lib/rtkit/rt_image.rb +179 -0
- data/lib/rtkit/ruby_extensions.rb +165 -0
- data/lib/rtkit/selection.rb +189 -0
- data/lib/rtkit/series.rb +77 -0
- data/lib/rtkit/setup.rb +198 -0
- data/lib/rtkit/slice.rb +184 -0
- data/lib/rtkit/staple.rb +305 -0
- data/lib/rtkit/structure_set.rb +442 -0
- data/lib/rtkit/study.rb +214 -0
- data/lib/rtkit/variables.rb +23 -0
- data/lib/rtkit/version.rb +6 -0
- metadata +159 -0
@@ -0,0 +1,83 @@
|
|
1
|
+
module RTKIT
|
2
|
+
|
3
|
+
# Contains a X,Y,Z triplet, which along with other Coordinates, defines a Contour.
|
4
|
+
#
|
5
|
+
# === Relations
|
6
|
+
#
|
7
|
+
# * The Coordinate belongs to a Contour.
|
8
|
+
#
|
9
|
+
class Coordinate
|
10
|
+
|
11
|
+
# The Contour that the Coordinate belongs to.
|
12
|
+
attr_reader :contour
|
13
|
+
# The X location (in units of mm).
|
14
|
+
attr_reader :x
|
15
|
+
# The Y location (in units of mm).
|
16
|
+
attr_reader :y
|
17
|
+
# The Z location (in units of mm).
|
18
|
+
attr_reader :z
|
19
|
+
|
20
|
+
# Creates a new Coordinate instance.
|
21
|
+
#
|
22
|
+
# === Parameters
|
23
|
+
#
|
24
|
+
# * <tt>x</tt> -- Float. The location of the Contour point along the x-axis (in units of mm).
|
25
|
+
# * <tt>y</tt> -- Float. The location of the Contour point along the y-axis (in units of mm).
|
26
|
+
# * <tt>z</tt> -- Float. The location of the Contour point along the z-axis (in units of mm).
|
27
|
+
# * <tt>contour</tt> -- The Contour instance (if any) that this Coordinate belongs to.
|
28
|
+
#
|
29
|
+
def initialize(x, y, z, contour=nil)
|
30
|
+
raise ArgumentError, "Invalid argument 'x'. Expected Float, got #{x.class}." unless x.is_a?(Float)
|
31
|
+
raise ArgumentError, "Invalid argument 'y'. Expected Float, got #{y.class}." unless y.is_a?(Float)
|
32
|
+
raise ArgumentError, "Invalid argument 'z'. Expected Float, got #{z.class}." unless z.is_a?(Float)
|
33
|
+
raise ArgumentError, "Invalid argument 'contour'. Expected Contour (or nil), got #{contour.class}." if contour && !contour.is_a?(Contour)
|
34
|
+
@contour = contour
|
35
|
+
@x = x
|
36
|
+
@y = y
|
37
|
+
@z = z
|
38
|
+
# Register ourselves with the Contour:
|
39
|
+
@contour.add_coordinate(self) if contour
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns true if the argument is an instance with attributes equal to self.
|
43
|
+
#
|
44
|
+
def ==(other)
|
45
|
+
if other.respond_to?(:to_coordinate)
|
46
|
+
other.send(:state) == state
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
alias_method :eql?, :==
|
51
|
+
|
52
|
+
# Generates a Fixnum hash value for this instance.
|
53
|
+
#
|
54
|
+
def hash
|
55
|
+
state.hash
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns self.
|
59
|
+
#
|
60
|
+
def to_coordinate
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
# Returns a string where the x, y & z values are separated by a '\'.
|
66
|
+
#
|
67
|
+
def to_s
|
68
|
+
[@x, @y, @z].join("\\")
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
|
75
|
+
# Returns the attributes of this instance in an array (for comparison purposes).
|
76
|
+
#
|
77
|
+
def state
|
78
|
+
[@x, @y, @z]
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
@@ -0,0 +1,264 @@
|
|
1
|
+
# Copyright 2012 Christoffer Lervag
|
2
|
+
#
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
6
|
+
# (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License
|
14
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
15
|
+
#
|
16
|
+
module RTKIT
|
17
|
+
|
18
|
+
# Handles the DICOM data found at a particular location, typically all files contained in a specific directory.
|
19
|
+
# A DataSet contains all DICOM objects read from the specified source,
|
20
|
+
# organized in a patient - study - series - image structure.
|
21
|
+
#
|
22
|
+
class DataSet
|
23
|
+
|
24
|
+
# An array of Frame instances loaded for this DataSet.
|
25
|
+
attr_reader :frames
|
26
|
+
# An array of Patient instances loaded for this DataSet.
|
27
|
+
attr_reader :patients
|
28
|
+
|
29
|
+
# Creates (and returns) a new DataSet instance from an array of DICOM objects.
|
30
|
+
#
|
31
|
+
# === Parameters
|
32
|
+
#
|
33
|
+
# * <tt>objects</tt> -- An array of DICOM::DObject instances which will be loaded into the DataSet.
|
34
|
+
#
|
35
|
+
def self.load(objects)
|
36
|
+
raise ArgumentError, "Invalid argument 'objects'. Expected Array, got #{objects.class}." unless objects.is_a?(Array)
|
37
|
+
raise ArgumentError, "Invalid argument 'objects'. Expected Array to contain only DObjects, got #{objects.collect{|dcm| dcm.class}.uniq}." if objects.collect{|dcm| dcm.class}.uniq != [DICOM::DObject]
|
38
|
+
# We will put the objects in arrays sorted by modality, to control
|
39
|
+
# the sequence in which they are loaded in our data structure:
|
40
|
+
images = Array.new
|
41
|
+
structs = Array.new
|
42
|
+
plans = Array.new
|
43
|
+
doses = Array.new
|
44
|
+
rtimages = Array.new
|
45
|
+
# Read and sort:
|
46
|
+
objects.each do |dcm|
|
47
|
+
# Find out which modality our DICOM file is and handle it accordingly:
|
48
|
+
modality = dcm.value("0008,0060")
|
49
|
+
case modality
|
50
|
+
when *IMAGE_SERIES
|
51
|
+
images << dcm
|
52
|
+
when 'RTSTRUCT'
|
53
|
+
structs << dcm
|
54
|
+
when 'RTPLAN'
|
55
|
+
plans << dcm
|
56
|
+
when 'RTDOSE'
|
57
|
+
doses << dcm
|
58
|
+
when 'RTIMAGE'
|
59
|
+
rtimages << dcm
|
60
|
+
else
|
61
|
+
#puts "Notice: Unsupported modality (ignored): #{modality}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
# Create the DataSet:
|
65
|
+
ds = DataSet.new
|
66
|
+
# Add the objects to our data structure in a specific sequence:
|
67
|
+
[images, structs, plans, doses, rtimages].each do |modality|
|
68
|
+
modality.each do |dcm|
|
69
|
+
ds.add(dcm)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
return ds
|
73
|
+
end
|
74
|
+
|
75
|
+
# Creates (and returns) a new DataSet instance from a specified path,
|
76
|
+
# by reading and loading the DICOM files found in this directory (including its sub-directories).
|
77
|
+
#
|
78
|
+
# === Parameters
|
79
|
+
#
|
80
|
+
# * <tt>path</tt> -- A path to the directory containing the DICOM files you want to load.
|
81
|
+
#
|
82
|
+
def self.read(path)
|
83
|
+
raise ArgumentError, "Invalid argument 'path'. Expected String, got #{path.class}." unless path.is_a?(String)
|
84
|
+
# Get the files:
|
85
|
+
files = RTKIT.files(path)
|
86
|
+
raise ArgumentError, "No files found in the specified folder: #{path}" unless files.length > 0
|
87
|
+
# Load DICOM objects:
|
88
|
+
objects = Array.new
|
89
|
+
failed = Array.new
|
90
|
+
files.each do |file|
|
91
|
+
dcm = DICOM::DObject.read(file)
|
92
|
+
if dcm.read?
|
93
|
+
objects << dcm
|
94
|
+
else
|
95
|
+
failed << file
|
96
|
+
#puts "Warning: A file was not successfully read as a DICOM object. (#{file})"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
raise ArgumentError, "No DICOM files were successfully read from the specified folder: #{path}" unless objects.length > 0
|
100
|
+
# Create the DataSet:
|
101
|
+
return DataSet.load(objects)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Creates a new DataSet instance.
|
105
|
+
#
|
106
|
+
def initialize
|
107
|
+
# Create instance variables:
|
108
|
+
@frames = Array.new
|
109
|
+
@patients = Array.new
|
110
|
+
@associated_frames = Hash.new
|
111
|
+
@associated_patients = Hash.new
|
112
|
+
end
|
113
|
+
|
114
|
+
# Returns true if the argument is an instance with attributes equal to self.
|
115
|
+
#
|
116
|
+
def ==(other)
|
117
|
+
if other.respond_to?(:to_data_set)
|
118
|
+
other.send(:state) == state
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
alias_method :eql?, :==
|
123
|
+
|
124
|
+
# Adds a DICOM object to the dataset, by adding it
|
125
|
+
# to an existing Patient, or creating a new Patient.
|
126
|
+
#
|
127
|
+
# === Restrictions
|
128
|
+
#
|
129
|
+
# To ensure a correct relationship between objects of different modality, please add
|
130
|
+
# DICOM objects in the specific order: images, structs, plans, doses, rtimages
|
131
|
+
# Alternatively, use the class method DataSet.load(objects), which handles this automatically.
|
132
|
+
#
|
133
|
+
def add(dcm)
|
134
|
+
id = dcm.value(PATIENTS_ID)
|
135
|
+
p = patient(id)
|
136
|
+
if p
|
137
|
+
p.add(dcm)
|
138
|
+
else
|
139
|
+
add_patient(Patient.load(dcm, self))
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Adds a Frame to this DataSet.
|
144
|
+
#
|
145
|
+
def add_frame(frame)
|
146
|
+
raise ArgumentError, "Invalid argument 'frame'. Expected Frame, got #{frame.class}." unless frame.is_a?(Frame)
|
147
|
+
# Do not add it again if the frame already belongs to this instance:
|
148
|
+
@frames << frame unless @associated_frames[frame.uid]
|
149
|
+
@associated_frames[frame.uid] = frame
|
150
|
+
end
|
151
|
+
|
152
|
+
# Adds a Patient to this DataSet.
|
153
|
+
#
|
154
|
+
def add_patient(patient)
|
155
|
+
raise ArgumentError, "Invalid argument 'patient'. Expected Patient, got #{patient.class}." unless patient.is_a?(Patient)
|
156
|
+
# Do not add it again if the patient already belongs to this instance:
|
157
|
+
@patients << patient unless @associated_patients[patient.id]
|
158
|
+
@associated_patients[patient.id] = patient
|
159
|
+
end
|
160
|
+
|
161
|
+
# Returns the Frame instance mathcing the specified Frame's UID (if an UID argument is used).
|
162
|
+
# If a specified UID doesn't match, nil is returned.
|
163
|
+
# If no argument is passed, the first Frame instance associated with the DataSet is returned.
|
164
|
+
#
|
165
|
+
# === Parameters
|
166
|
+
#
|
167
|
+
# * <tt>uid</tt> -- String. The Frame of Reference UID.
|
168
|
+
#
|
169
|
+
def frame(*args)
|
170
|
+
raise ArgumentError, "Expected one or none arguments, got #{args.length}." unless [0, 1].include?(args.length)
|
171
|
+
if args.length == 1
|
172
|
+
raise ArgumentError, "Expected String (or nil), got #{args.first.class}." unless [String, NilClass].include?(args.first.class)
|
173
|
+
return @associated_frames[args.first]
|
174
|
+
else
|
175
|
+
# No argument used, therefore we return the first Frame instance:
|
176
|
+
return @frames.first
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Generates a Fixnum hash value for this instance.
|
181
|
+
#
|
182
|
+
def hash
|
183
|
+
state.hash
|
184
|
+
end
|
185
|
+
|
186
|
+
# Returns the Patient instance mathcing the specified Patient's ID (if an ID argument is used).
|
187
|
+
# If a specified ID doesn't match, nil is returned.
|
188
|
+
# If no argument is passed, the first Patient instance associated with the DataSet is returned.
|
189
|
+
#
|
190
|
+
# === Parameters
|
191
|
+
#
|
192
|
+
# * <tt>id</tt> -- String. The value of the Patient's ID element.
|
193
|
+
#
|
194
|
+
def patient(*args)
|
195
|
+
raise ArgumentError, "Expected one or none arguments, got #{args.length}." unless [0, 1].include?(args.length)
|
196
|
+
if args.length == 1
|
197
|
+
raise ArgumentError, "Expected String (or nil), got #{args.first.class}." unless [String, NilClass].include?(args.first.class)
|
198
|
+
return @associated_patients[args.first]
|
199
|
+
else
|
200
|
+
# No argument used, therefore we return the first Patient instance:
|
201
|
+
return @patients.first
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# Prints the nested structure of patient - study - series - images that have been loaded to the dataset instance.
|
206
|
+
#
|
207
|
+
def print
|
208
|
+
@patients.each do |p|
|
209
|
+
puts p.name
|
210
|
+
p.studies.each do |st|
|
211
|
+
puts " #{st.uid}"
|
212
|
+
st.series.each do |se|
|
213
|
+
puts " #{se.modality}"
|
214
|
+
if se.images
|
215
|
+
puts " (#{se.images.length} images)"
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
# Prints the nested structure of patient - study - modalities from
|
223
|
+
# a RT point of view, with image_series - ss - plan, etc.
|
224
|
+
#
|
225
|
+
def print_rt
|
226
|
+
@patients.each do |p|
|
227
|
+
puts p.name
|
228
|
+
p.studies.each do |st|
|
229
|
+
puts " #{st.uid}"
|
230
|
+
st.image_series.each do |is|
|
231
|
+
puts " #{is.modality} (#{is.images.length} images)"
|
232
|
+
is.structs.each do |struct|
|
233
|
+
puts " StructureSet"
|
234
|
+
struct.plans.each do |plan|
|
235
|
+
puts " RTPlan"
|
236
|
+
puts " RTDose" if plan.dose
|
237
|
+
puts " RTImage" if plan.rtimage
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# Returns self.
|
246
|
+
#
|
247
|
+
def to_data_set
|
248
|
+
self
|
249
|
+
end
|
250
|
+
|
251
|
+
|
252
|
+
# Following methods are private:
|
253
|
+
private
|
254
|
+
|
255
|
+
|
256
|
+
# Returns the attributes of this instance in an array (for comparison purposes).
|
257
|
+
#
|
258
|
+
def state
|
259
|
+
[@frames, @patients]
|
260
|
+
end
|
261
|
+
|
262
|
+
end
|
263
|
+
|
264
|
+
end
|
data/lib/rtkit/dose.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
module RTKIT
|
2
|
+
|
3
|
+
# NB! The Dose class is as of yet just a concept, and not in actual use
|
4
|
+
# by RTKIT. It will probably be put to use in a future version.
|
5
|
+
#
|
6
|
+
# Contains data and methods related to a single Dose value.
|
7
|
+
# The Dose value may be an average, median, max/min, etc derived
|
8
|
+
# from a collection of Dose values as given in a DoseDistribution.
|
9
|
+
#
|
10
|
+
# === Relations
|
11
|
+
#
|
12
|
+
# * A Dose (value) belongs to the DoseDistribution from which it was created.
|
13
|
+
# * The Dose class can be considered a subclass of Float (although strictly
|
14
|
+
# speaking it is rather a Float delegated class.
|
15
|
+
#
|
16
|
+
require 'delegate'
|
17
|
+
class Dose < DelegateClass(Float)
|
18
|
+
|
19
|
+
# The DoseDistribution that the single Dose value is derived from.
|
20
|
+
attr_reader :distribution
|
21
|
+
# The Dose value.
|
22
|
+
attr_reader :value
|
23
|
+
|
24
|
+
# Creates a new Dose instance.
|
25
|
+
#
|
26
|
+
# === Parameters
|
27
|
+
#
|
28
|
+
# * <tt>value</tt> -- Float. A dose value.
|
29
|
+
# * <tt>distribution</tt> -- The DoseDistribution which this single Dose value belongs to.
|
30
|
+
#
|
31
|
+
def initialize(value, distribution)
|
32
|
+
raise ArgumentError, "Invalid argument 'distribution'. Expected DoseDistribution, got #{distribution.class}." unless distribution.is_a?(DoseDistribution)
|
33
|
+
super(value.to_f)
|
34
|
+
@value = value
|
35
|
+
@distribution = distribution
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns true if the argument is an instance with attributes equal to self.
|
39
|
+
#
|
40
|
+
def ==(other)
|
41
|
+
if other.respond_to?(:to_dose)
|
42
|
+
other.send(:state) == state
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
alias_method :eql?, :==
|
47
|
+
|
48
|
+
# Generates a Fixnum hash value for this instance.
|
49
|
+
#
|
50
|
+
def hash
|
51
|
+
state.hash
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns self.
|
55
|
+
#
|
56
|
+
def to_dose
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# Returns the attributes of this instance in an array (for comparison purposes).
|
64
|
+
#
|
65
|
+
def state
|
66
|
+
[@value, @distribution]
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,206 @@
|
|
1
|
+
module RTKIT
|
2
|
+
|
3
|
+
# Contains DICOM data and methods related to a DoseDistribution,
|
4
|
+
# a collection of dose points extracted from a dose volume.
|
5
|
+
#
|
6
|
+
# === Relations
|
7
|
+
#
|
8
|
+
# * A DoseDistribution belongs to the DoseVolume from which it was created.
|
9
|
+
# * A DoseDistribution contains various methods to return a Dose (point) instance.
|
10
|
+
#
|
11
|
+
class DoseDistribution
|
12
|
+
|
13
|
+
# The doses values belonging to this distribution (array of floats).
|
14
|
+
attr_reader :doses
|
15
|
+
# The DoseVolume that the DoseDistribution is derived from.
|
16
|
+
attr_reader :volume
|
17
|
+
|
18
|
+
# Creates a new DoseDistribution instance from a BinVolume.
|
19
|
+
# The BinVolume is typically defined from a ROI delineation against a DoseVolume.
|
20
|
+
# Returns the DoseDistribution instance.
|
21
|
+
#
|
22
|
+
# === Parameters
|
23
|
+
#
|
24
|
+
# * <tt>bin_volume</tt> -- A BinVolume, referencing a DoseVolume, from which to extract a DoseDistribution.
|
25
|
+
#
|
26
|
+
def self.create(bin_volume)
|
27
|
+
raise ArgumentError, "Invalid argument 'bin_volume'. Expected BinVolume, got #{bin_volume.class}." unless bin_volume.is_a?(BinVolume)
|
28
|
+
raise ArgumentError, "Invalid argument 'bin_volume'. It must reference a DoseVolume, got #{bin_volume.bin_images.first.image.series.class}." unless bin_volume.bin_images.first.image.series.is_a?(DoseVolume)
|
29
|
+
dose_volume = bin_volume.bin_images.first.image.series
|
30
|
+
# Extract a selection of pixel values from the dose images based on the provided binary volume:
|
31
|
+
dose_values = NArray.sfloat(0)
|
32
|
+
bin_volume.bin_images.each do |bin_image|
|
33
|
+
slice_pixel_values = bin_image.image.pixel_values(bin_image.selection)
|
34
|
+
slice_dose_values = slice_pixel_values.to_type(4) * bin_image.image.series.scaling
|
35
|
+
dose_values = NArray[*dose_values, *slice_dose_values]
|
36
|
+
end
|
37
|
+
# Create the DoseDistribution instance:
|
38
|
+
dd = self.new(dose_values, dose_volume)
|
39
|
+
return dd
|
40
|
+
end
|
41
|
+
|
42
|
+
# Creates a new DoseDistribution instance.
|
43
|
+
#
|
44
|
+
# === Parameters
|
45
|
+
#
|
46
|
+
# * <tt>doses</tt> -- An array/NArray of doses (floats).
|
47
|
+
# * <tt>volume</tt> -- The DoseVolume which this DoseDistribution belongs to.
|
48
|
+
#
|
49
|
+
def initialize(doses, volume)
|
50
|
+
#raise ArgumentError, "Invalid argument 'doses'. Expected Array, got #{doses.class}." unless doses.is_a?(Array)
|
51
|
+
raise ArgumentError, "Invalid argument 'volume'. Expected DoseVolume, got #{volume.class}." unless volume.is_a?(DoseVolume)
|
52
|
+
# Store doses as a sorted (float) NArray:
|
53
|
+
@doses = NArray.to_na(doses).sort.to_type(4)
|
54
|
+
# Set references:
|
55
|
+
@volume = volume
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns true if the argument is an instance with attributes equal to self.
|
59
|
+
#
|
60
|
+
def ==(other)
|
61
|
+
if other.respond_to?(:to_dose_distribution)
|
62
|
+
other.send(:state) == state
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
alias_method :eql?, :==
|
67
|
+
|
68
|
+
# Calculates the dose that at least the specified
|
69
|
+
# percentage of the volume receives.
|
70
|
+
# Returns a dose (Float) in units of Gy.
|
71
|
+
#
|
72
|
+
# === Parameters
|
73
|
+
#
|
74
|
+
# * <tt>percent</tt> -- Integer/Float. The percent of the volume which receives a dose higher than the returned dose.
|
75
|
+
#
|
76
|
+
# === Examples
|
77
|
+
#
|
78
|
+
# # Calculate the near minimum dose (e.g. up to 2 % of the volume receives a dose less than this):
|
79
|
+
# near_min = ptv_distribution.d(98)
|
80
|
+
# # Calculate the near maximum dose (e.g. at most 2 % of the volume receives a dose higher than this):
|
81
|
+
# near_max = ptv_distribution.d(2)
|
82
|
+
#
|
83
|
+
def d(percent)
|
84
|
+
raise RangeError, "Argument 'percent' must be in the range [0-100]." if percent.to_f < 0 or percent.to_f > 100
|
85
|
+
d_index = ((@doses.length - 1) * (1 - percent.to_f * 0.01)).round
|
86
|
+
return @doses[d_index]
|
87
|
+
end
|
88
|
+
|
89
|
+
# Generates a Fixnum hash value for this instance.
|
90
|
+
#
|
91
|
+
def hash
|
92
|
+
state.hash
|
93
|
+
end
|
94
|
+
|
95
|
+
# Calculates the homogeneity index of the dose distribution.
|
96
|
+
# A low (near zero) value corresponds to high homogeneity (e.q. 0.1).
|
97
|
+
# Returns the index value as a float.
|
98
|
+
#
|
99
|
+
# === Notes
|
100
|
+
#
|
101
|
+
# * The homogeneity index is defined as:
|
102
|
+
# HI = ( d(2) - d(98) ) / d(50)
|
103
|
+
# For more details, refer to ICRU Report No. 83, Chapter 3.7.1.
|
104
|
+
#
|
105
|
+
# === Examples
|
106
|
+
#
|
107
|
+
# # Calculate the homogeneity index of the dose distribution of a PTV ROI for a given plan:
|
108
|
+
# homogeneity_index = ptv_distribution.hindex
|
109
|
+
#
|
110
|
+
def hindex
|
111
|
+
return (d(2) - d(98)) / d(50).to_f
|
112
|
+
end
|
113
|
+
|
114
|
+
# Returns the number of dose values in the DoseDistribution.
|
115
|
+
#
|
116
|
+
def length
|
117
|
+
@doses.length
|
118
|
+
end
|
119
|
+
|
120
|
+
alias_method :size, :length
|
121
|
+
|
122
|
+
# Calculates the maximum dose.
|
123
|
+
#
|
124
|
+
def max
|
125
|
+
@doses.max
|
126
|
+
end
|
127
|
+
|
128
|
+
# Calculates the arithmethic mean (average) dose.
|
129
|
+
#
|
130
|
+
def mean
|
131
|
+
@doses.mean
|
132
|
+
end
|
133
|
+
|
134
|
+
# Calculates the median dose.
|
135
|
+
#
|
136
|
+
def median
|
137
|
+
@doses.median
|
138
|
+
end
|
139
|
+
|
140
|
+
# Calculates the minimum dose.
|
141
|
+
#
|
142
|
+
def min
|
143
|
+
@doses.min
|
144
|
+
end
|
145
|
+
|
146
|
+
# Calculates the root mean square deviation (the population standard deviation).
|
147
|
+
#
|
148
|
+
# === Notes
|
149
|
+
#
|
150
|
+
# * Uses N in the denominator for calculating the standard deviation of the sample.
|
151
|
+
#
|
152
|
+
def rmsdev
|
153
|
+
@doses.rmsdev
|
154
|
+
end
|
155
|
+
|
156
|
+
# Calculates the sample standard deviation of the dose distribution.
|
157
|
+
#
|
158
|
+
# === Notes
|
159
|
+
#
|
160
|
+
# * Uses Bessel's correction (N-1 in the denominator).
|
161
|
+
#
|
162
|
+
def stddev
|
163
|
+
@doses.stddev
|
164
|
+
end
|
165
|
+
|
166
|
+
# Returns self.
|
167
|
+
#
|
168
|
+
def to_dose_distribution
|
169
|
+
self
|
170
|
+
end
|
171
|
+
|
172
|
+
# Calculates the percentage of the volume that receives
|
173
|
+
# a dose higher than or equal to the specified dose.
|
174
|
+
# Returns a percentage (Float).
|
175
|
+
#
|
176
|
+
# === Parameters
|
177
|
+
#
|
178
|
+
# * <tt>dose</tt> -- Integer/Float. The dose threshold value to apply to the dose distribution.
|
179
|
+
#
|
180
|
+
# === Examples
|
181
|
+
#
|
182
|
+
# # Calculate the low dose spread (e.g. the percentage of the lung that receives a dose higher than 5 Gy):
|
183
|
+
# coverage_low = lung_distribution.v(5)
|
184
|
+
# # Calculate the high dose spread (e.g. the percentage of the lung that receives a dose higher than 20 Gy):
|
185
|
+
# coverage_high = lung_distribution.v(20)
|
186
|
+
#
|
187
|
+
def v(dose)
|
188
|
+
raise RangeError, "Argument 'dose' cannot be negative." if dose.to_f < 0
|
189
|
+
# How many dose values are higher than the threshold?
|
190
|
+
num_above = (@doses.ge dose.to_f).where.length
|
191
|
+
# Divide by total number of elements and convert to percentage:
|
192
|
+
return num_above / @doses.length.to_f * 100
|
193
|
+
end
|
194
|
+
|
195
|
+
|
196
|
+
private
|
197
|
+
|
198
|
+
|
199
|
+
# Returns the attributes of this instance in an array (for comparison purposes).
|
200
|
+
#
|
201
|
+
def state
|
202
|
+
[@doses.to_a, @volume]
|
203
|
+
end
|
204
|
+
|
205
|
+
end
|
206
|
+
end
|