SciRuby/nmatrix

View on GitHub
lib/nmatrix/io/harwell_boeing.rb

Summary

Maintainability
A
2 hrs
Test Coverage
#--
# = NMatrix
#
# A linear algebra library for scientific computation in Ruby.
# NMatrix is part of SciRuby.
#
# NMatrix was originally inspired by and derived from NArray, by
# Masahiro Tanaka: http://narray.rubyforge.org
#
# == Copyright Information
#
# SciRuby is Copyright (c) 2010 - 2016, Ruby Science Foundation
# NMatrix is Copyright (c) 2012 - 2016, John Woods and the Ruby Science Foundation
#
# Please see LICENSE.txt for additional copyright notices.
#
# == Contributing
#
# By contributing source code to SciRuby, you agree to be bound by
# our Contributor Agreement:
#
# * https://github.com/SciRuby/sciruby/wiki/Contributor-Agreement
#
# == io/matlab/harwell_boeing.rb
#
# Harwell Boeing file reader (and eventually writer too).
# => Supports only assembled, non-symmetric, real matrices
# => Data types supported are exponential, floating point and integer
# => Returned NMatrix is of type :float64
#++

require_relative './fortran_format.rb'

class NMatrix
  module IO
    module HarwellBoeing

      class << self
        # Loads the contents of a valid Harwell Boeing format file and 
        # returns an NMatrix object with the values of the file and optionally
        # only the header info.
        # 
        # Supports only assembled, non-symmetric, real matrices. File name must
        # have matrix type as extension.
        # 
        # Example - test_file.rua
        # 
        # == Arguments
        # 
        # * +file_path+ - Path of the Harwell Boeing file  to load.
        # * +opts+      - Options for specifying whether you want
        #                 the values and  header or only the header.
        # 
        # == Options
        # 
        # * +:header+ - If specified as *true*, will return only the header of
        #               the HB file.Will return the NMatrix object and
        #               header as an array if left blank.
        # 
        # == Usage
        # 
        #   mat, head = NMatrix::IO::HarwellBoeing.load("test_file.rua")
        # 
        #   head = NMatrix::IO::HarwellBoeing.load("test_file.rua", {header: true})
        # 
        # == Alternate Usage
        # 
        # You can specify the file using NMatrix::IO::Reader.new("path/to/file")
        # and then call *header* or *values* on the resulting object.
        def load file_path, opts={}
          hb_obj = NMatrix::IO::HarwellBoeing::Reader.new(file_path)

          return hb_obj.header if opts[:header]

          [hb_obj.values, hb_obj.header]
        end
      end

      class Reader
        def initialize file_name
          raise(IOError, "Unsupported file format. Specify file as \
            file_name.rua.") if !file_name.match(/.*\.[rR][uU][aA]/)

          @file_name   = file_name
          @header      = {}
          @body        = nil
        end

        def header
          return @header if !@header.empty?
          @file = File.open @file_name, "r"

          line = @file.gets

          @header[:title] = line[0...72].strip
          @header[:key]   = line[72...80].strip

          line = @file.gets

          @header[:totcrd] = line[0...14] .strip.to_i
          @header[:ptrcrd] = line[14...28].strip.to_i
          @header[:indcrd] = line[28...42].strip.to_i
          @header[:valcrd] = line[42...56].strip.to_i
          @header[:rhscrd] = line[56...70].strip.to_i

          raise(IOError, "Right hand sides not supported.") \
           if @header[:rhscrd] > 0

          line = @file.gets

          @header[:mxtype] = line[0...3]

          raise(IOError, "Currently supports only real, assembled, unsymmetric \
            matrices.") if !@header[:mxtype].match(/RUA/)

          @header[:nrow]   = line[13...28].strip.to_i
          @header[:ncol]   = line[28...42].strip.to_i
          @header[:nnzero] = line[42...56].strip.to_i
          @header[:neltvl] = line[56...70].strip.to_i

          line = @file.gets

          fortran_reader = NMatrix::IO::FortranFormat::Reader

          @header[:ptrfmt] = fortran_reader.new(line[0...16].strip) .parse
          @header[:indfmt] = fortran_reader.new(line[16...32].strip).parse
          @header[:valfmt] = fortran_reader.new(line[32...52].strip).parse
          @header[:rhsfmt] = fortran_reader.new(line[52...72].strip).parse

          @header
        end

        def values
          @header      = header if @header.empty?
          @file.lineno = 5      if @file.lineno != 5
          @matrix      = NMatrix.new([ @header[:nrow], @header[:ncol] ], 
                                      0, dtype: :float64)

          read_column_pointers
          read_row_indices
          read_values

          @file.close
          
          assemble_matrix

          @matrix
        end

       private

        def read_column_pointers
          @col_ptrs  = []
          pointer_lines     = @header[:ptrcrd]
          pointers_per_line = @header[:ptrfmt][:repeat]
          pointer_width     = @header[:ptrfmt][:field_width]

          @col_ptrs = read_numbers :to_i, pointer_lines, pointers_per_line, 
                                             pointer_width

          @col_ptrs.map! {|c| c -= 1}
        end

        def read_row_indices
          @row_indices     = []
          row_lines        = @header[:indcrd]
          indices_per_line = @header[:indfmt][:repeat]
          row_width        = @header[:indfmt][:field_width]

          @row_indices = read_numbers :to_i, row_lines, indices_per_line, 
                                      row_width

          @row_indices.map! {|r| r -= 1}
        end

        def read_values
          @vals = []
          value_lines = @header[:valcrd]
          values_per_line = @header[:valfmt][:repeat]
          value_width    = @header[:valfmt][:field_width]

          @vals = read_numbers :to_f, value_lines, values_per_line, 
                                  value_width
        end

        def read_numbers to_dtype, num_of_lines, numbers_per_line, number_width
          data = []

          num_of_lines.times do 
            line  = @file.gets
            index = 0

            numbers_per_line.times do
              delimiter = index + number_width

              data << line[index...delimiter].strip.send(to_dtype)

              break if line.length <= delimiter
              index += number_width
            end
          end

          data
        end

        def assemble_matrix
          col = 0
          @col_ptrs[0..-2].each_index do |index|
            @col_ptrs[index].upto(@col_ptrs[index+1] - 1) do |row_ptr|
              row               = @row_indices[row_ptr]
              @matrix[row, col] = @vals[row_ptr]
            end

            col += 1
          end
        end
      end

    end
  end
end