class RTP::Plan
The Plan
class is the highest level Record
in the RTPConnect records hierarchy, and the one the user will interact with to read, modify and write files.
@note Relations:
* Parent: nil * Children: Prescription, DoseTracking
Attributes
An array of DoseTracking
records (if any) that belongs to this Plan
.
The ExtendedPlan
record (if any) that belongs to this Plan
.
The Record
which this instance belongs to (nil by definition).
An array of Prescription
records (if any) that belongs to this Plan
.
Public Class Methods
Creates a new Plan
by loading a plan definition string (i.e. a single line).
@note This method does not perform crc verification on the given string.
If such verification is desired, use methods ::parse or ::read instead.
@param [#to_s] string the plan definition record string line @param [Hash] options the options to use for loading the plan definition string @option options [Boolean] :repair if true, a record containing invalid CSV will be attempted fixed and loaded @return [Plan] the created Plan
instance @raise [ArgumentError] if given a string containing an invalid number of elements
# File lib/rtp-connect/plan.rb, line 73 def self.load(string, options={}) rtp = self.new rtp.load(string, options) end
Creates a new Plan
.
# File lib/rtp-connect/plan.rb, line 156 def initialize super('PLAN_DEF', 10, 28) @current_parent = self # Child records: @extended_plan = nil @prescriptions = Array.new @dose_trackings = Array.new # No parent (by definition) for the Plan record: @parent = nil @attributes = [ # Required: :keyword, :patient_id, :patient_last_name, :patient_first_name, :patient_middle_initial, :plan_id, :plan_date, :plan_time, :course_id, # Optional: :diagnosis, :md_last_name, :md_first_name, :md_middle_initial, :md_approve_last_name, :md_approve_first_name, :md_approve_middle_initial, :phy_approve_last_name, :phy_approve_first_name, :phy_approve_middle_initial, :author_last_name, :author_first_name, :author_middle_initial, :rtp_mfg, :rtp_model, :rtp_version, :rtp_if_protocol, :rtp_if_version ] end
Creates a Plan
instance by parsing an RTPConnect string.
@param [#to_s] string an RTPConnect ascii string (with single or multiple lines/records) @param [Hash] options the options to use for parsing the RTP
string @option options [Boolean] :ignore_crc if true, the RTP
records will be successfully loaded even if their checksums are invalid @option options [Boolean] :repair if true, any RTP
records containing invalid CSV will be attempted fixed and loaded @option options [Boolean] :skip_unknown if true, unknown records will be skipped, and record instances will be built from the remaining recognized string records @return [Plan] the created Plan
instance @raise [ArgumentError] if given an invalid string record
# File lib/rtp-connect/plan.rb, line 88 def self.parse(string, options={}) lines = string.to_s.split("\r\n") # Create the Plan object: line = lines.first RTP.verify(line, options) rtp = self.load(line, options) lines[1..-1].each do |line| # Validate, determine type, and process the line accordingly to # build the hierarchy of records: RTP.verify(line, options) values = line.values(options[:repair]) keyword = values.first method = RTP::PARSE_METHOD[keyword] if method rtp.send(method, line) else if options[:skip_unknown] logger.warn("Skipped unknown record definition: #{keyword}") else raise ArgumentError, "Unknown keyword #{keyword} extracted from string." end end end return rtp end
Creates an Plan
instance by reading and parsing an RTPConnect file.
@param [String] file a string which specifies the path of the RTPConnect file to be loaded @param [Hash] options the options to use for reading the RTP
file @option options [Boolean] :ignore_crc if true, the RTP
records will be successfully loaded even if their checksums are invalid @option options [Boolean] :repair if true, any RTP
records containing invalid CSV will be attempted fixed and loaded @option options [Boolean] :skip_unknown if true, unknown records will be skipped, and record instances will be built from the remaining recognized string records @return [Plan] the created Plan
instance @raise [ArgumentError] if given an invalid file or the file given contains an invalid record
# File lib/rtp-connect/plan.rb, line 124 def self.read(file, options={}) raise ArgumentError, "Invalid argument 'file'. Expected String, got #{file.class}." unless file.is_a?(String) # Read the file content: str = nil unless File.exist?(file) logger.error("Invalid (non-existing) file: #{file}") else unless File.readable?(file) logger.error("File exists but I don't have permission to read it: #{file}") else if File.directory?(file) logger.error("Expected a file, got a directory: #{file}") else if File.size(file) < 10 logger.error("This file is too small to contain valid RTP information: #{file}.") else str = File.open(file, 'rb:ISO8859-1') { |f| f.read } end end end end # Parse the file contents and create the RTP instance: if str rtp = self.parse(str, options) else raise "An RTP::Plan object could not be created from the specified file. Check the log for more details." end return rtp end
Public Instance Methods
Checks for equality.
Other and self are considered equivalent if they are of compatible types and their attributes are equivalent.
@param other an object to be compared with self. @return [Boolean] true if self and other are considered equivalent
# File lib/rtp-connect/plan.rb, line 206 def ==(other) if other.respond_to?(:to_plan) other.send(:state) == state end end
Adds a dose tracking record to this instance.
@param [DoseTracking] child a DoseTracking
instance which is to be associated with self
# File lib/rtp-connect/plan.rb, line 218 def add_dose_tracking(child) @dose_trackings << child.to_dose_tracking child.parent = self end
Adds an extended plan record to this instance.
@param [ExtendedPlan] child an ExtendedPlan
instance which is to be associated with self
# File lib/rtp-connect/plan.rb, line 227 def add_extended_plan(child) @extended_plan = child.to_extended_plan child.parent = self end
Adds a prescription site record to this instance.
@param [Prescription] child a Prescription
instance which is to be associated with self
# File lib/rtp-connect/plan.rb, line 236 def add_prescription(child) @prescriptions << child.to_prescription child.parent = self end
Collects the child records of this instance in a properly sorted array.
@return [Array<Prescription, DoseTracking>] a sorted array of self's child records
# File lib/rtp-connect/plan.rb, line 245 def children return [@extended_plan, @prescriptions, @dose_trackings].flatten.compact end
Sets the course_id
attribute.
@param [nil, to_s] value the new attribute value
# File lib/rtp-connect/plan.rb, line 381 def course_id=(value) @course_id = value && value.to_s end
Removes the reference of the given instance from this instance.
@param [ExtendedPlan, Prescription
, DoseTracking] record a child record to be removed from this instance
# File lib/rtp-connect/plan.rb, line 253 def delete(record) case record when Prescription delete_child(:prescriptions, record) when DoseTracking delete_child(:dose_trackings, record) when ExtendedPlan delete_extended_plan else logger.warn("Unknown class (record) given to Plan#delete: #{record.class}") end end
Removes all dose_tracking
references from this instance.
# File lib/rtp-connect/plan.rb, line 268 def delete_dose_trackings delete_children(:dose_trackings) end
Removes the extended plan reference from this instance.
# File lib/rtp-connect/plan.rb, line 274 def delete_extended_plan delete_child(:extended_plan) end
Removes all prescription references from this instance.
# File lib/rtp-connect/plan.rb, line 280 def delete_prescriptions delete_children(:prescriptions) end
Sets the diagnosis attribute.
@param [nil, to_s] value the new attribute value
# File lib/rtp-connect/plan.rb, line 389 def diagnosis=(value) @diagnosis = value && value.to_s end
Computes a hash code for this object.
@note Two objects with the same attributes will have the same hash code.
@return [Fixnum] the object's hash code
# File lib/rtp-connect/plan.rb, line 290 def hash state.hash end
Sets the md_approve_first_name
attribute.
@param [nil, to_s] value the new attribute value
# File lib/rtp-connect/plan.rb, line 429 def md_approve_first_name=(value) @md_approve_first_name = value && value.to_s end
Sets the md_approve_last_name
attribute.
@param [nil, to_s] value the new attribute value
# File lib/rtp-connect/plan.rb, line 421 def md_approve_last_name=(value) @md_approve_last_name = value && value.to_s end
Sets the md_approve_middle_initial
attribute.
@param [nil, to_s] value the new attribute value
# File lib/rtp-connect/plan.rb, line 437 def md_approve_middle_initial=(value) @md_approve_middle_initial = value && value.to_s end
Sets the md_first_name
attribute.
@param [nil, to_s] value the new attribute value
# File lib/rtp-connect/plan.rb, line 405 def md_first_name=(value) @md_first_name = value && value.to_s end
Sets the md_last_name
attribute.
@param [nil, to_s] value the new attribute value
# File lib/rtp-connect/plan.rb, line 397 def md_last_name=(value) @md_last_name = value && value.to_s end
Sets the md_middle_initial
attribute.
@param [nil, to_s] value the new attribute value
# File lib/rtp-connect/plan.rb, line 413 def md_middle_initial=(value) @md_middle_initial = value && value.to_s end
Sets the patient_first_name
attribute.
@param [nil, to_s] value the new attribute value
# File lib/rtp-connect/plan.rb, line 341 def patient_first_name=(value) @patient_first_name = value && value.to_s end
Sets the patient_id
attribute.
@param [nil, to_s] value the new attribute value
# File lib/rtp-connect/plan.rb, line 327 def patient_id=(value) @patient_id = value && value.to_s end
Sets the patient_last_name
attribute.
# File lib/rtp-connect/plan.rb, line 333 def patient_last_name=(value) @patient_last_name = value && value.to_s end
Sets the patient_middle_initial
attribute.
@param [nil, to_s] value the new attribute value
# File lib/rtp-connect/plan.rb, line 349 def patient_middle_initial=(value) @patient_middle_initial = value && value.to_s end
Sets the phy_approve_first_name
attribute.
@param [nil, to_s] value the new attribute value
# File lib/rtp-connect/plan.rb, line 453 def phy_approve_first_name=(value) @phy_approve_first_name = value && value.to_s end
Sets the phy_approve_last_name
attribute.
@param [nil, to_s] value the new attribute value
# File lib/rtp-connect/plan.rb, line 445 def phy_approve_last_name=(value) @phy_approve_last_name = value && value.to_s end
Sets the phy_approve_middle_initial
attribute.
@param [nil, to_s] value the new attribute value
# File lib/rtp-connect/plan.rb, line 461 def phy_approve_middle_initial=(value) @phy_approve_middle_initial = value && value.to_s end
Sets the plan_date
attribute.
@param [nil, to_s] value the new attribute value
# File lib/rtp-connect/plan.rb, line 365 def plan_date=(value) @plan_date = value && value.to_s end
Sets the plan_id
attribute.
@param [nil, to_s] value the new attribute value
# File lib/rtp-connect/plan.rb, line 357 def plan_id=(value) @plan_id = value && value.to_s end
Sets the plan_time
attribute.
@param [nil, to_s] value the new attribute value
# File lib/rtp-connect/plan.rb, line 373 def plan_time=(value) @plan_time = value && value.to_s end
Sets the rtp_if_protocol
attribute.
@param [nil, to_s] value the new attribute value
# File lib/rtp-connect/plan.rb, line 517 def rtp_if_protocol=(value) @rtp_if_protocol = value && value.to_s end
Sets the rtp_if_version
attribute.
@param [nil, to_s] value the new attribute value
# File lib/rtp-connect/plan.rb, line 525 def rtp_if_version=(value) @rtp_if_version = value && value.to_s end
Sets the rtp_mfg
attribute.
@param [nil, to_s] value the new attribute value
# File lib/rtp-connect/plan.rb, line 493 def rtp_mfg=(value) @rtp_mfg = value && value.to_s end
Sets the rtp_model
attribute.
@param [nil, to_s] value the new attribute value
# File lib/rtp-connect/plan.rb, line 501 def rtp_model=(value) @rtp_model = value && value.to_s end
Sets the rtp_version
attribute.
@param [nil, to_s] value the new attribute value
# File lib/rtp-connect/plan.rb, line 509 def rtp_version=(value) @rtp_version = value && value.to_s end
Converts the Plan
(and child) records to a DICOM::DObject of modality RTPLAN.
@note Only photon plans have been tested.
Electron beams beams may give an invalid DICOM file. Also note that, due to limitations in the RTP file format, some original values can not be recreated, like e.g. Study UID or Series UID.
@param [Hash] options the options to use for creating the DICOM object @option options [Boolean] :dose_ref if set, Dose Reference & Referenced Dose Reference sequences will be included in the generated DICOM file @option options [String] :manufacturer the value used for the manufacturer tag (0008,0070) in the beam sequence @option options [String] :model the value used for the manufacturer's model name tag (0008,1090) in the beam sequence @option options [Symbol] :scale if set, relevant device parameters are converted from native readout format to IEC1217 (supported values are :elekta & :varian) @option options [String] :serial_number the value used for the device serial number tag (0018,1000) in the beam sequence @return [DICOM::DObject] the converted DICOM object
# File lib/rtp-connect/plan_to_dcm.rb, line 28 def to_dcm(options={}) # # FIXME: This method is rather big, with a few sections of somewhat similar, repeating code. # Refactoring and simplifying it at some stage might be a good idea. # require 'dicom' original_level = DICOM.logger.level DICOM.logger.level = Logger::FATAL p = @prescriptions.first # If no prescription is present, we are not going to be able to make a valid DICOM object: logger.error("No Prescription Record present. Unable to build a valid RTPLAN DICOM object.") unless p dcm = DICOM::DObject.new # # TOP LEVEL TAGS: # # Specific Character Set: DICOM::Element.new('0008,0005', 'ISO_IR 100', :parent => dcm) # Instance Creation Date DICOM::Element.new('0008,0012', Time.now.strftime("%Y%m%d"), :parent => dcm) # Instance Creation Time: DICOM::Element.new('0008,0013', Time.now.strftime("%H%M%S"), :parent => dcm) # SOP Class UID: DICOM::Element.new('0008,0016', '1.2.840.10008.5.1.4.1.1.481.5', :parent => dcm) # SOP Instance UID (if an original UID is not present, we make up a UID): begin sop_uid = p.fields.first.extended_field.original_plan_uid.empty? ? DICOM.generate_uid : p.fields.first.extended_field.original_plan_uid rescue sop_uid = DICOM.generate_uid end DICOM::Element.new('0008,0018', sop_uid, :parent => dcm) # Study Date DICOM::Element.new('0008,0020', Time.now.strftime("%Y%m%d"), :parent => dcm) # Study Time: DICOM::Element.new('0008,0030', Time.now.strftime("%H%M%S"), :parent => dcm) # Accession Number: DICOM::Element.new('0008,0050', '', :parent => dcm) # Modality: DICOM::Element.new('0008,0060', 'RTPLAN', :parent => dcm) # Manufacturer: DICOM::Element.new('0008,0070', 'rtp-connect', :parent => dcm) # Referring Physician's Name: DICOM::Element.new('0008,0090', "#{@md_last_name}^#{@md_first_name}^#{@md_middle_name}^^", :parent => dcm) # Operator's Name: DICOM::Element.new('0008,1070', "#{@author_last_name}^#{@author_first_name}^#{@author_middle_name}^^", :parent => dcm) # Patient's Name: DICOM::Element.new('0010,0010', "#{@patient_last_name}^#{@patient_first_name}^#{@patient_middle_name}^^", :parent => dcm) # Patient ID: DICOM::Element.new('0010,0020', @patient_id, :parent => dcm) # Patient's Birth Date: DICOM::Element.new('0010,0030', '', :parent => dcm) # Patient's Sex: DICOM::Element.new('0010,0040', '', :parent => dcm) # Manufacturer's Model Name: DICOM::Element.new('0008,1090', 'RTP-to-DICOM', :parent => dcm) # Software Version(s): DICOM::Element.new('0018,1020', "RubyRTP#{VERSION}", :parent => dcm) # Study Instance UID: DICOM::Element.new('0020,000D', DICOM.generate_uid, :parent => dcm) # Series Instance UID: DICOM::Element.new('0020,000E', DICOM.generate_uid, :parent => dcm) # Study ID: DICOM::Element.new('0020,0010', '1', :parent => dcm) # Series Number: DICOM::Element.new('0020,0011', '1', :parent => dcm) # Frame of Reference UID (if an original UID is not present, we make up a UID): begin for_uid = p.site_setup.frame_of_ref_uid.empty? ? DICOM.generate_uid : p.site_setup.frame_of_ref_uid rescue for_uid = DICOM.generate_uid end DICOM::Element.new('0020,0052', for_uid, :parent => dcm) # Position Reference Indicator: DICOM::Element.new('0020,1040', '', :parent => dcm) # RT Plan Label (max 16 characters): plan_label = p ? p.rx_site_name[0..15] : @course_id DICOM::Element.new('300A,0002', plan_label, :parent => dcm) # RT Plan Name: plan_name = p ? p.rx_site_name : @course_id DICOM::Element.new('300A,0003', plan_name, :parent => dcm) # RT Plan Description: plan_desc = p ? p.technique : @diagnosis DICOM::Element.new('300A,0004', plan_desc, :parent => dcm) # RT Plan Date: plan_date = @plan_date.empty? ? Time.now.strftime("%Y%m%d") : @plan_date DICOM::Element.new('300A,0006', plan_date, :parent => dcm) # RT Plan Time: plan_time = @plan_time.empty? ? Time.now.strftime("%H%M%S") : @plan_time DICOM::Element.new('300A,0007', plan_time, :parent => dcm) # Approval Status: DICOM::Element.new('300E,0002', 'UNAPPROVED', :parent => dcm) # # SEQUENCES: # # Tolerance Table Sequence: if p && p.fields.first && !p.fields.first.tolerance_table.empty? tt_seq = DICOM::Sequence.new('300A,0040', :parent => dcm) tt_item = DICOM::Item.new(:parent => tt_seq) # Tolerance Table Number: DICOM::Element.new('300A,0042', p.fields.first.tolerance_table, :parent => tt_item) end # Structure set information: if p && p.site_setup && !p.site_setup.structure_set_uid.empty? # # Referenced Structure Set Sequence: # ss_seq = DICOM::Sequence.new('300C,0060', :parent => dcm) ss_item = DICOM::Item.new(:parent => ss_seq) # Referenced SOP Class UID: DICOM::Element.new('0008,1150', '1.2.840.10008.5.1.4.1.1.481.3', :parent => ss_item) DICOM::Element.new('0008,1155', p.site_setup.structure_set_uid, :parent => ss_item) # RT Plan Geometry: DICOM::Element.new('300A,000C', 'PATIENT', :parent => dcm) else # RT Plan Geometry: DICOM::Element.new('300A,000C', 'TREATMENT_DEVICE', :parent => dcm) end # # Patient Setup Sequence: # ps_seq = DICOM::Sequence.new('300A,0180', :parent => dcm) ps_item = DICOM::Item.new(:parent => ps_seq) # Patient Position: begin pat_pos = p.site_setup.patient_orientation.empty? ? 'HFS' : p.site_setup.patient_orientation rescue pat_pos = 'HFS' end DICOM::Element.new('0018,5100', pat_pos, :parent => ps_item) # Patient Setup Number: DICOM::Element.new('300A,0182', '1', :parent => ps_item) # Setup Technique (assume Isocentric): DICOM::Element.new('300A,01B0', 'ISOCENTRIC', :parent => ps_item) # # Dose Reference Sequence: # create_dose_reference(dcm, plan_name) if options[:dose_ref] # # Fraction Group Sequence: # fg_seq = DICOM::Sequence.new('300A,0070', :parent => dcm) fg_item = DICOM::Item.new(:parent => fg_seq) # Fraction Group Number: DICOM::Element.new('300A,0071', '1', :parent => fg_item) # Number of Fractions Planned (try to derive from total dose/fraction dose, or use 1 as default): begin num_frac = p.dose_ttl.empty? || p.dose_tx.empty? ? '1' : (p.dose_ttl.to_i / p.dose_tx.to_f).round.to_s rescue num_frac = '0' end DICOM::Element.new('300A,0078', num_frac, :parent => fg_item) # Number of Brachy Application Setups: DICOM::Element.new('300A,00A0', '0', :parent => fg_item) # Referenced Beam Sequence (items created for each beam below): rb_seq = DICOM::Sequence.new('300C,0004', :parent => fg_item) # # Beam Sequence: # b_seq = DICOM::Sequence.new('300A,00B0', :parent => dcm) if p # If no fields are present, we are not going to be able to make a valid DICOM object: logger.error("No Field Record present. Unable to build a valid RTPLAN DICOM object.") unless p.fields.length > 0 p.fields.each_with_index do |field, i| # Fields with modality 'Unspecified' (e.g. CT or 2dkV) must be skipped: unless field.modality == 'Unspecified' # If this is an electron beam, a warning should be printed, as these are less reliably converted: logger.warn("This is not a photon beam (#{field.modality}). Beware that DICOM conversion of Electron beams are experimental, and other modalities are unsupported.") if field.modality != 'Xrays' # Reset control point 'current value' attributes: reset_cp_current_attributes # Beam number and name: beam_number = field.extended_field ? field.extended_field.original_beam_number : (i + 1).to_s beam_name = field.extended_field ? field.extended_field.original_beam_name : field.field_name # Ref Beam Item: rb_item = DICOM::Item.new(:parent => rb_seq) # Beam Dose (convert from cGy to Gy): field_dose = field.field_dose.empty? ? '' : (field.field_dose.to_f * 0.01).round(4).to_s DICOM::Element.new('300A,0084', field_dose, :parent => rb_item) # Beam Meterset: DICOM::Element.new('300A,0086', field.field_monitor_units, :parent => rb_item) # Referenced Beam Number: DICOM::Element.new('300C,0006', beam_number, :parent => rb_item) # Beam Item: b_item = DICOM::Item.new(:parent => b_seq) # Optional method values: # Manufacturer: DICOM::Element.new('0008,0070', options[:manufacturer], :parent => b_item) if options[:manufacturer] # Manufacturer's Model Name: DICOM::Element.new('0008,1090', options[:model], :parent => b_item) if options[:model] # Device Serial Number: DICOM::Element.new('0018,1000', options[:serial_number], :parent => b_item) if options[:serial_number] # Treatment Machine Name (max 16 characters): DICOM::Element.new('300A,00B2', field.treatment_machine[0..15], :parent => b_item) # Primary Dosimeter Unit: DICOM::Element.new('300A,00B3', 'MU', :parent => b_item) # Source-Axis Distance (convert to mm): DICOM::Element.new('300A,00B4', "#{field.sad.to_f * 10}", :parent => b_item) # Beam Number: DICOM::Element.new('300A,00C0', beam_number, :parent => b_item) # Beam Name: DICOM::Element.new('300A,00C2', beam_name, :parent => b_item) # Beam Description: DICOM::Element.new('300A,00C3', field.field_note, :parent => b_item) # Beam Type: beam_type = case field.treatment_type when 'Static' then 'STATIC' when 'StepNShoot' then 'STATIC' when 'VMAT' then 'DYNAMIC' else logger.error("The beam type (treatment type) #{field.treatment_type} is not yet supported.") end DICOM::Element.new('300A,00C4', beam_type, :parent => b_item) # Radiation Type: rad_type = case field.modality when 'Elect' then 'ELECTRON' when 'Xrays' then 'PHOTON' else logger.error("The radiation type (modality) #{field.modality} is not yet supported.") end DICOM::Element.new('300A,00C6', rad_type, :parent => b_item) # Treatment Delivery Type: DICOM::Element.new('300A,00CE', 'TREATMENT', :parent => b_item) # Number of Wedges: DICOM::Element.new('300A,00D0', (field.wedge.empty? ? '0' : '1'), :parent => b_item) # Number of Compensators: DICOM::Element.new('300A,00E0', (field.compensator.empty? ? '0' : '1'), :parent => b_item) # Number of Boli: DICOM::Element.new('300A,00ED', (field.bolus.empty? ? '0' : '1'), :parent => b_item) # Number of Blocks: DICOM::Element.new('300A,00F0', (field.block.empty? ? '0' : '1'), :parent => b_item) # Final Cumulative Meterset Weight: DICOM::Element.new('300A,010E', 1, :parent => b_item) # Referenced Patient Setup Number: DICOM::Element.new('300C,006A', '1', :parent => b_item) # # Beam Limiting Device Sequence: # create_beam_limiting_devices(b_item, field) # # Block Sequence (if any): # FIXME: It seems that the Block Sequence (300A,00F4) may be # difficult (impossible?) to reconstruct based on the RTP file's # information, and thus it is skipped altogether. # # # Applicator Sequence (if any): # unless field.e_applicator.empty? app_seq = DICOM::Sequence.new('300A,0107', :parent => b_item) app_item = DICOM::Item.new(:parent => app_seq) # Applicator ID: DICOM::Element.new('300A,0108', field.e_field_def_aperture, :parent => app_item) # Applicator Type: DICOM::Element.new('300A,0109', "ELECTRON_#{field.e_applicator.upcase}", :parent => app_item) # Applicator Description: DICOM::Element.new('300A,010A', "Appl. #{field.e_field_def_aperture}", :parent => app_item) end # # Control Point Sequence: # # A field may have 0 (no MLC), 1 (conventional beam with MLC) or 2n (IMRT) control points. # The DICOM file shall always contain 2n control points (minimum 2). # cp_seq = DICOM::Sequence.new('300A,0111', :parent => b_item) if field.control_points.length < 2 # When we have 0 or 1 control point, use settings from field, and insert MLC settings if present: # First CP: cp_item = DICOM::Item.new(:parent => cp_seq) # Control Point Index: DICOM::Element.new('300A,0112', "0", :parent => cp_item) # Nominal Beam Energy: DICOM::Element.new('300A,0114', "#{field.energy.to_f}", :parent => cp_item) # Dose Rate Set: DICOM::Element.new('300A,0115', field.doserate, :parent => cp_item) # Gantry Angle: DICOM::Element.new('300A,011E', field.gantry_angle, :parent => cp_item) # Gantry Rotation Direction: DICOM::Element.new('300A,011F', (field.arc_direction.empty? ? 'NONE' : field.arc_direction), :parent => cp_item) # Beam Limiting Device Angle: DICOM::Element.new('300A,0120', field.collimator_angle, :parent => cp_item) # Beam Limiting Device Rotation Direction: DICOM::Element.new('300A,0121', 'NONE', :parent => cp_item) # Patient Support Angle: DICOM::Element.new('300A,0122', field.couch_pedestal, :parent => cp_item) # Patient Support Rotation Direction: DICOM::Element.new('300A,0123', 'NONE', :parent => cp_item) # Table Top Eccentric Angle: DICOM::Element.new('300A,0125', field.couch_angle, :parent => cp_item) # Table Top Eccentric Rotation Direction: DICOM::Element.new('300A,0126', 'NONE', :parent => cp_item) # Table Top Vertical Position: couch_vert = field.couch_vertical.empty? ? '' : (field.couch_vertical.to_f * 10).to_s DICOM::Element.new('300A,0128', couch_vert, :parent => cp_item) # Table Top Longitudinal Position: couch_long = field.couch_longitudinal.empty? ? '' : (field.couch_longitudinal.to_f * 10).to_s DICOM::Element.new('300A,0129', couch_long, :parent => cp_item) # Table Top Lateral Position: couch_lat = field.couch_lateral.empty? ? '' : (field.couch_lateral.to_f * 10).to_s DICOM::Element.new('300A,012A', couch_lat, :parent => cp_item) # Isocenter Position (x\y\z): if p.site_setup DICOM::Element.new('300A,012C', "#{(p.site_setup.iso_pos_x.to_f * 10).round(2)}\\#{(p.site_setup.iso_pos_y.to_f * 10).round(2)}\\#{(p.site_setup.iso_pos_z.to_f * 10).round(2)}", :parent => cp_item) else logger.warn("No Site Setup record exists for this plan. Unable to provide an isosenter position.") DICOM::Element.new('300A,012C', '', :parent => cp_item) end # Source to Surface Distance: add_ssd(field.ssd, cp_item) # Cumulative Meterset Weight: DICOM::Element.new('300A,0134', '0', :parent => cp_item) # Beam Limiting Device Position Sequence: if field.control_points.length > 0 create_beam_limiting_device_positions(cp_item, field.control_points.first, options) else create_beam_limiting_device_positions_from_field(cp_item, field, options) end # Referenced Dose Reference Sequence: create_referenced_dose_reference(cp_item) if options[:dose_ref] # Second CP: cp_item = DICOM::Item.new(:parent => cp_seq) # Control Point Index: DICOM::Element.new('300A,0112', "1", :parent => cp_item) # Cumulative Meterset Weight: DICOM::Element.new('300A,0134', '1', :parent => cp_item) else # When we have multiple (2 or more) control points, iterate each control point: field.control_points.each { |cp| create_control_point(cp, cp_seq, options) } # Make sure that hte cumulative meterset weight of the last control # point is '1' (exactly equal to final cumulative meterset weight): cp_seq.items.last['300A,0134'].value = '1' end # Number of Control Points: DICOM::Element.new('300A,0110', b_item['300A,0111'].items.length, :parent => b_item) end end # Number of Beams: DICOM::Element.new('300A,0080', fg_item['300C,0004'].items.length, :parent => fg_item) end # Restore the DICOM logger: DICOM.logger.level = original_level return dcm end
Returns self.
@return [Plan] self
# File lib/rtp-connect/plan.rb, line 298 def to_plan self end
Returns self.
@return [Plan] self
# File lib/rtp-connect/plan.rb, line 306 def to_rtp self end
Writes the Plan
object, along with its hiearchy of child objects, to a properly formatted RTPConnect ascii file.
@param [String] file a path/file string @param [Hash] options an optional hash parameter @option options [Float] :version the Mosaiq compatibility version number (e.g. 2.4) used for the output
# File lib/rtp-connect/plan.rb, line 317 def write(file, options={}) f = open_file(file) f.write(to_s(options)) f.close end
Private Instance Methods
Adds an angular type value to a Control Point Item, by creating the necessary DICOM elements. Note that the element is only added if there is no 'current' attribute defined, or the given value is different form the current attribute.
@param [DICOM::Item] item the DICOM control point item in which to create the elements @param [String] angle_tag the DICOM tag of the angle element @param [String] direction_tag the DICOM tag of the direction element @param [String, NilClass] angle the collimator angle attribute @param [String, NilClass] direction the collimator rotation direction attribute @param [Symbol] current_angle the instance variable that keeps track of the current value of this attribute
# File lib/rtp-connect/plan_to_dcm.rb, line 383 def add_angle(item, angle_tag, direction_tag, angle, direction, current_angle) if !self.send(current_angle) || angle != self.send(current_angle) self.send("#{current_angle}=", angle) DICOM::Element.new(angle_tag, angle, :parent => item) DICOM::Element.new(direction_tag, (direction.empty? ? 'NONE' : direction), :parent => item) end end
Adds a Table Top Position element to a Control Point Item. Note that the element is only added if there is no 'current' attribute defined, or the given value is different form the current attribute.
@param [DICOM::Item] item the DICOM control point item in which to create the element @param [String] tag the DICOM tag of the couch position element @param [String, NilClass] value the couch position @param [Symbol] current the instance variable that keeps track of the current value of this attribute
# File lib/rtp-connect/plan_to_dcm.rb, line 400 def add_couch_position(item, tag, value, current) if !self.send(current) || value != self.send(current) self.send("#{current}=", value) DICOM::Element.new(tag, (value.empty? ? '' : value.to_f * 10), :parent => item) end end
Adds a Dose Rate Set element to a Control Point Item. Note that the element is only added if there is no 'current' attribute defined, or the given value is different form the current attribute.
@param [String, NilClass] value the doserate attribute @param [DICOM::Item] item the DICOM control point item in which to create an element
# File lib/rtp-connect/plan_to_dcm.rb, line 414 def add_doserate(value, item) if !@current_doserate || value != @current_doserate @current_doserate = value DICOM::Element.new('300A,0115', value, :parent => item) end end
Adds a Nominal Beam Energy element to a Control Point Item. Note that the element is only added if there is no 'current' attribute defined, or the given value is different form the current attribute.
@param [String, NilClass] value the energy attribute @param [DICOM::Item] item the DICOM control point item in which to create an element
# File lib/rtp-connect/plan_to_dcm.rb, line 428 def add_energy(value, item) if !@current_energy || value != @current_energy @current_energy = value DICOM::Element.new('300A,0114', "#{value.to_f}", :parent => item) end end
Adds an Isosenter element to a Control Point Item. Note that the element is only added if there is a Site Setup record present, and it contains a real (non-empty) value. Also, the element is only added if there is no 'current' attribute defined, or the given value is different form the current attribute.
@param [SiteSetup, NilClass] site_setup
the associated site setup record @param [DICOM::Item] item the DICOM control point item in which to create an element
# File lib/rtp-connect/plan_to_dcm.rb, line 443 def add_isosenter(site_setup, item) if site_setup # Create an element if the value is new or unique: if !@current_isosenter iso = "#{(site_setup.iso_pos_x.to_f * 10).round(2)}\\#{(site_setup.iso_pos_y.to_f * 10).round(2)}\\#{(site_setup.iso_pos_z.to_f * 10).round(2)}" if iso != @current_isosenter @current_isosenter = iso DICOM::Element.new('300A,012C', iso, :parent => item) end end else # Log a warning if this is the first control point: unless @current_isosenter logger.warn("No Site Setup record exists for this plan. Unable to provide an isosenter position.") end end end
Adds a Source to Surface Distance element to a Control Point Item. Note that the element is only added if the SSD attribute contains real (non-empty) value.
@param [String, NilClass] value the SSD attribute @param [DICOM::Item] item the DICOM control point item in which to create an element
# File lib/rtp-connect/plan_to_dcm.rb, line 468 def add_ssd(value, item) DICOM::Element.new('300A,0130', "#{value.to_f * 10}", :parent => item) if value && !value.empty? end
Creates a control point record from the given string.
@param [String] string a string line containing a control point definition
# File lib/rtp-connect/plan.rb, line 537 def control_point(string) cp = ControlPoint.load(string, @current_parent) @current_parent = cp end
Creates an ASYMX or ASYMY item.
@param [ControlPoint] cp the RTP
control point to fetch device parameters from @param [DICOM::Sequence] dcm_parent the DICOM sequence in which to insert the item @param [Symbol] axis the axis for the item (:x or :y) @return [DICOM::Item] the constructed ASYMX or ASYMY item
# File lib/rtp-connect/plan_to_dcm.rb, line 583 def create_asym_item(cp, dcm_parent, axis, options={}) val1 = cp.send("dcm_collimator_#{axis.to_s}1", options[:scale]) val2 = cp.send("dcm_collimator_#{axis.to_s}2", options[:scale]) item = DICOM::Item.new(:parent => dcm_parent) # RT Beam Limiting Device Type: DICOM::Element.new('300A,00B8', "ASYM#{axis.to_s.upcase}", :parent => item) # Leaf/Jaw Positions: DICOM::Element.new('300A,011C', "#{val1}\\#{val2}", :parent => item) item end
Creates a beam limiting device positions sequence in the given DICOM object.
@param [DICOM::Item] cp_item the DICOM control point item in which to insert the sequence @param [ControlPoint] cp the RTP
control point to fetch device parameters from @return [DICOM::Sequence] the constructed beam limiting device positions sequence
# File lib/rtp-connect/plan_to_dcm.rb, line 559 def create_beam_limiting_device_positions(cp_item, cp, options={}) dp_seq = DICOM::Sequence.new('300A,011A', :parent => cp_item) # The ASYMX item ('backup jaws') doesn't exist on all models: if ['SYM', 'ASY'].include?(cp.parent.field_x_mode.upcase) dp_item_x = create_asym_item(cp, dp_seq, axis=:x, options) end # Always create one ASYMY item: dp_item_y = create_asym_item(cp, dp_seq, axis=:y, options) # MLCX: dp_item_mlcx = DICOM::Item.new(:parent => dp_seq) # RT Beam Limiting Device Type: DICOM::Element.new('300A,00B8', "MLCX", :parent => dp_item_mlcx) # Leaf/Jaw Positions: DICOM::Element.new('300A,011C', cp.dcm_mlc_positions(options[:scale]), :parent => dp_item_mlcx) dp_seq end
Creates a beam limiting device positions sequence in the given DICOM object.
@param [DICOM::Item] cp_item the DICOM control point item in which to insert the sequence @param [Field] field the RTP
treatment field to fetch device parameters from @return [DICOM::Sequence] the constructed beam limiting device positions sequence
# File lib/rtp-connect/plan_to_dcm.rb, line 600 def create_beam_limiting_device_positions_from_field(cp_item, field, options={}) dp_seq = DICOM::Sequence.new('300A,011A', :parent => cp_item) # ASYMX: dp_item_x = DICOM::Item.new(:parent => dp_seq) DICOM::Element.new('300A,00B8', "ASYMX", :parent => dp_item_x) DICOM::Element.new('300A,011C', "#{field.dcm_collimator_x1}\\#{field.dcm_collimator_x2}", :parent => dp_item_x) # ASYMY: dp_item_y = DICOM::Item.new(:parent => dp_seq) DICOM::Element.new('300A,00B8', "ASYMY", :parent => dp_item_y) DICOM::Element.new('300A,011C', "#{field.dcm_collimator_y1}\\#{field.dcm_collimator_y2}", :parent => dp_item_y) dp_seq end
Creates a beam limiting device sequence in the given DICOM object.
@param [DICOM::Item] beam_item the DICOM beam item in which to insert the sequence @param [Field] field the RTP
field to fetch device parameters from @return [DICOM::Sequence] the constructed beam limiting device sequence
# File lib/rtp-connect/plan_to_dcm.rb, line 525 def create_beam_limiting_devices(beam_item, field) bl_seq = DICOM::Sequence.new('300A,00B6', :parent => beam_item) # The ASYMX item ('backup jaws') doesn't exist on all models: if ['SYM', 'ASY'].include?(field.field_x_mode.upcase) bl_item_x = DICOM::Item.new(:parent => bl_seq) DICOM::Element.new('300A,00B8', "ASYMX", :parent => bl_item_x) DICOM::Element.new('300A,00BC', "1", :parent => bl_item_x) end # The ASYMY item is always created: bl_item_y = DICOM::Item.new(:parent => bl_seq) # RT Beam Limiting Device Type: DICOM::Element.new('300A,00B8', "ASYMY", :parent => bl_item_y) # Number of Leaf/Jaw Pairs: DICOM::Element.new('300A,00BC', "1", :parent => bl_item_y) # MLCX item is only created if leaves are defined: # (NB: The RTP file doesn't specify leaf position boundaries, so we # have to set these based on a set of known MLC types, their number # of leaves, and their leaf boundary positions.) if field.control_points.length > 0 bl_item_mlcx = DICOM::Item.new(:parent => bl_seq) DICOM::Element.new('300A,00B8', "MLCX", :parent => bl_item_mlcx) num_leaves = field.control_points.first.mlc_leaves.to_i DICOM::Element.new('300A,00BC', num_leaves.to_s, :parent => bl_item_mlcx) DICOM::Element.new('300A,00BE', "#{RTP.leaf_boundaries(num_leaves).join("\\")}", :parent => bl_item_mlcx) end bl_seq end
Creates a control point item in the given control point sequence, based on an RTP
control point record.
@param [ControlPoint] cp the RTP
ControlPoint
record to convert @param [DICOM::Sequence] sequence the DICOM parent sequence of the item to be created @param [Hash] options the options to use for creating the control point @option options [Boolean] :dose_ref if set, a Referenced Dose Reference sequence will be included in the generated control point item @return [DICOM::Item] the constructed control point DICOM item
# File lib/rtp-connect/plan_to_dcm.rb, line 481 def create_control_point(cp, sequence, options={}) cp_item = DICOM::Item.new(:parent => sequence) # Some CP attributes will always be written (CP index, BLD positions & Cumulative meterset weight). # The other attributes are only written if they are different from the previous control point. # Control Point Index: DICOM::Element.new('300A,0112', "#{cp.index}", :parent => cp_item) # Beam Limiting Device Position Sequence: create_beam_limiting_device_positions(cp_item, cp, options) # Source to Surface Distance: add_ssd(cp.ssd, cp_item) # Cumulative Meterset Weight: DICOM::Element.new('300A,0134', cp.monitor_units.to_f, :parent => cp_item) # Referenced Dose Reference Sequence: create_referenced_dose_reference(cp_item) if options[:dose_ref] # Attributes that are only added if they carry an updated value: # Nominal Beam Energy: add_energy(cp.energy, cp_item) # Dose Rate Set: add_doserate(cp.doserate, cp_item) # Gantry Angle & Rotation Direction: add_angle(cp_item, '300A,011E', '300A,011F', cp.gantry_angle, cp.gantry_dir, :current_gantry) # Beam Limiting Device Angle & Rotation Direction: add_angle(cp_item, '300A,0120', '300A,0121', cp.collimator_angle, cp.collimator_dir, :current_collimator) # Patient Support Angle & Rotation Direction: add_angle(cp_item, '300A,0122', '300A,0123', cp.couch_pedestal, cp.couch_ped_dir, :current_couch_pedestal) # Table Top Eccentric Angle & Rotation Direction: add_angle(cp_item, '300A,0125', '300A,0126', cp.couch_angle, cp.couch_dir, :current_couch_angle) # Table Top Vertical Position: add_couch_position(cp_item, '300A,0128', cp.couch_vertical, :current_couch_vertical) # Table Top Longitudinal Position: add_couch_position(cp_item, '300A,0129', cp.couch_longitudinal, :current_couch_longitudinal) # Table Top Lateral Position: add_couch_position(cp_item, '300A,012A', cp.couch_lateral, :current_couch_lateral) # Isocenter Position (x\y\z): add_isosenter(cp.parent.parent.site_setup, cp_item) cp_item end
Creates a dose reference sequence in the given DICOM object.
@param [DICOM::DObject] dcm the DICOM object in which to insert the sequence @param [String] description the value to use for Dose Reference Description @return [DICOM::Sequence] the constructed dose reference sequence
# File lib/rtp-connect/plan_to_dcm.rb, line 619 def create_dose_reference(dcm, description) dr_seq = DICOM::Sequence.new('300A,0010', :parent => dcm) dr_item = DICOM::Item.new(:parent => dr_seq) # Dose Reference Number: DICOM::Element.new('300A,0012', '1', :parent => dr_item) # Dose Reference Structure Type: DICOM::Element.new('300A,0014', 'SITE', :parent => dr_item) # Dose Reference Description: DICOM::Element.new('300A,0016', description, :parent => dr_item) # Dose Reference Type: DICOM::Element.new('300A,0020', 'TARGET', :parent => dr_item) dr_seq end
Creates a referenced dose reference sequence in the given DICOM object.
@param [DICOM::Item] cp_item the DICOM item in which to insert the sequence @return [DICOM::Sequence] the constructed referenced dose reference sequence
# File lib/rtp-connect/plan_to_dcm.rb, line 638 def create_referenced_dose_reference(cp_item) # Referenced Dose Reference Sequence: rd_seq = DICOM::Sequence.new('300C,0050', :parent => cp_item) rd_item = DICOM::Item.new(:parent => rd_seq) # Cumulative Dose Reference Coeffecient: DICOM::Element.new('300A,010C', '', :parent => rd_item) # Referenced Dose Reference Number: DICOM::Element.new('300C,0051', '1', :parent => rd_item) rd_seq end
Creates a dose tracking record from the given string.
@param [String] string a string line containing a dose tracking definition
# File lib/rtp-connect/plan.rb, line 546 def dose_tracking(string) dt = DoseTracking.load(string, @current_parent) @current_parent = dt end
Creates an extended plan record from the given string.
@param [String] string a string line containing an extended plan definition
# File lib/rtp-connect/plan.rb, line 555 def extended_plan_def(string) ep = ExtendedPlan.load(string, @current_parent) @current_parent = ep end
Creates an extended treatment field record from the given string.
@param [String] string a string line containing an extended treatment field definition
# File lib/rtp-connect/plan.rb, line 564 def extended_treatment_field(string) ef = ExtendedField.load(string, @current_parent) @current_parent = ef end
Tests if the path/file is writable, creates any folders if necessary, and opens the file for writing.
@param [String] file a path/file string @raise if the given file cannot be created
# File lib/rtp-connect/plan.rb, line 574 def open_file(file) # Check if file already exists: if File.exist?(file) # Is (the existing file) writable? unless File.writable?(file) raise "The program does not have permission or resources to create this file: #{file}" end else # File does not exist. # Check if this file's path contains a folder that does not exist, and therefore needs to be created: folders = file.split(File::SEPARATOR) if folders.length > 1 # Remove last element (which should be the file string): folders.pop path = folders.join(File::SEPARATOR) # Check if this path exists: unless File.directory?(path) # We need to create (parts of) this path: require 'fileutils' FileUtils.mkdir_p(path) end end end # It has been verified that the file can be created: return File.new(file, 'wb:ISO8859-1') end
Creates a prescription site record from the given string.
@param [String] string a string line containing a prescription site definition
# File lib/rtp-connect/plan.rb, line 605 def prescription_site(string) p = Prescription.load(string, @current_parent) @current_parent = p end
Resets the types of control point attributes that are only written to the first control point item, and for following control point items only when they are different from the 'current' value. When a new field is reached, it is essential to reset these attributes, or else we could risk to start the field with a control point with missing attributes, if one of its first attributes is equal to the last attribute of the previous field.
# File lib/rtp-connect/plan_to_dcm.rb, line 656 def reset_cp_current_attributes @current_gantry = nil @current_collimator = nil @current_couch_pedestal = nil @current_couch_angle = nil @current_couch_vertical = nil @current_couch_longitudinal = nil @current_couch_lateral = nil @current_isosenter = nil end
Creates a simulation field record from the given string.
@param [String] string a string line containing a simulation field definition
# File lib/rtp-connect/plan.rb, line 639 def simulation_field(string) sf = SimulationField.load(string, @current_parent) @current_parent = sf end
Creates a site setup record from the given string.
@param [String] string a string line containing a site setup definition
# File lib/rtp-connect/plan.rb, line 614 def site_setup(string) s = SiteSetup.load(string, @current_parent) @current_parent = s end
Creates a treatment field record from the given string.
@param [String] string a string line containing a treatment field definition
# File lib/rtp-connect/plan.rb, line 630 def treatment_field(string) f = Field.load(string, @current_parent) @current_parent = f end