[ANN] StringMatrix

(This isn't a library I plan on maintaining, but ANN felt like the right prefix for offering up code for mass enjoyment.)

At work yesterday I needed to do some algebra on some matrix math to reverse-derive a value. (To be specific, I needed to figure out how to draw the three euler rotation values out of a ZXY rotation matrix.) After spending 2 minutes writing out equations and fearing I was making a mistake, I turned to Excel and some stupid text manipulation to produce my formulae. After 10 minutes of messing around, I got the answer wrong anyhow.

I decided to write a little Ruby library to do the task for me. I wrote a matrix class which knows how to do matrix math on strings, producing formulae from them. After the initial pass produced a lot of values like "0*sinX" and "0 + 0 + 1*(sinX)" I made it a bit smarter, so that 0s and 1s properly simplify the equations during calculation.

It doesn't produce perfectly-reduced equations by any means, but it did the trick. In the end, I got my result :slight_smile:

Sample output follows, followed by the code itself. Enjoy!

M1:

···

---------------------------------------------------------------------
0 | 1 | 2
3 | 4 | 5

M2:
---------------------------------------------------------------------
x | y | z
3q | 4r | 5s

M1 scaled by 2:
---------------------------------------------------------------------
0 | 2 | 4
6 | 8 | 10

M2 scaled by 2:
---------------------------------------------------------------------
x * 2 | y * 2 | z * 2
3q * 2 | 4r * 2 | 5s * 2

M1 + M2:
---------------------------------------------------------------------
    x | 1 + y | 2 + z
3 + 3q | 4 + 4r | 5 + 5s

X:
-----------------------------------------------------------------------------
1 | 0 | 0
0 | cosX | sinX
0 | -sinX | cosX

Y:
-----------------------------------------------------------------------------
cosY | 0 | -sinY
   0 | 1 | 0
sinY | 0 | cosY

Z:
-----------------------------------------------------------------------------
cosZ | sinZ | 0
-sinZ | cosZ | 0
   0 | 0 | 1

X - Y:
-----------------------------------------------------------------------------
1 - cosY | 0 | sinY
     0 | cosX - 1 | sinX
   -sinY | -sinX | cosX - cosY

X * Y * Z:
-----------------------------------------------------------------------------
                cosY * cosZ | cosY * sinZ | -sinY
((sinX * sinY) * cosZ) + (cosX * -sinZ) | ((sinX * sinY) * sinZ) + (cosX * cosZ) | sinX * cosY
((cosX * sinY) * cosZ) + (-sinX * -sinZ) | ((cosX * sinY) * sinZ) + (-sinX * cosZ) | cosX * cosY

Z * X * Y:
-----------------------------------------------------------------------------
(cosZ * cosY) + ((sinZ * sinX) * sinY) | sinZ * cosX | (cosZ * -sinY) + ((sinZ * sinX) * cosY)
(-sinZ * cosY) + ((cosZ * sinX) * sinY) | cosZ * cosX | (-sinZ * -sinY) + ((cosZ * sinX) * cosY)
               cosX * sinY | -sinX

               cosX * cosY

stringmatrix.rb
-----------------------------------------------------------------------------
# A two-dimensional matrix of arbitrary size, with common matrix
# math methods. When entries in the matrix are strings, the math
# methods produce strings representing the equation.
#
# Strings and numbers mix nicely so that multiplying by 0 wipes
# out the string properly, and multiplying by 1 or adding 0
# leaves the original unchanged.
#
# For example:
# x = StringMatrix.parse <<END
# 1 0 0
# 0 cosX sinX
# 0 -sinX cosX
# END
#
# y = StringMatrix.parse <<END
# cosY | 0 | -sinY
# 0 | 1 | 0
# sinY | 0 | cosY
# END
#
# puts x, ' ', y, ' ', x - y, ' ', x * y
#
# #=> 1 | 0 | 0
# #=> 0 | cosX | sinX
# #=> 0 | -sinX | cosX
# #=>
# #=> cosY | 0 | -sinY
# #=> 0 | 1 | 0
# #=> sinY | 0 | cosY
# #=>
# #=> 1 - cosY | 0 | sinY
# #=> 0 | cosX - 1 | sinX
# #=> -sinY | -sinX | cosX - cosY
# #=>
# #=> cosY | 0 | -sinY
# #=> sinX * sinY | cosX | sinX * cosY
# #=> cosX * sinY | -sinX | cosX * cosY
class StringMatrix
     # Add two values intelligently
     def self.add( v1, v2 )
         if v1==0
             v2
         elsif v2==0
             v1
         elsif Numeric===v1 && Numeric===v2
             v1+v2
         else
             self.operator_join( v1, v2, '+' )
         end
     end

     # Subtract two values intelligently
     def self.subtract( v1, v2 )
         if v2==0
             v1
         elsif v1==0
             if v2 =~ /^-\((.+)\)$/ || v2 =~ /^-(.+)$/
                 $1
             elsif v2 =~ /\s/
                 "-(#{v2})"
             else
                 "-#{v2}"
             end
         elsif Numeric===v1 && Numeric===v2
             v1-v2
         else
             self.operator_join( v1, v2, '-' )
         end
     end

     # Multiply two values intelligently
     def self.multiply( v1, v2 )
         if v1==1
             v2
         elsif v2==1
             v1
         elsif v1==0 || v2==0
             0
         elsif Numeric===v1 && Numeric===v2
             v1*v2
         else
             self.operator_join( v1, v2, '*' )
         end
     end

     # Join two values semi-intelligently
     def self.operator_join( v1, v2, op_str )
         out = ( Numeric === v1 || v1 =~ /^\S+$/ ) ? "#{v1}" : "(#{v1})"
         out << " #{op_str} "
         out << ( ( Numeric === v2 || v2 =~ /^\S+$/ ) ? "#{v2}" : "(#{v2})" )
     end

     attr_reader :width, :height

     # Creates a new matrix of the specified _width_ and _height_, optionally
     # specifying a _default_value_ to fill each cell.
     def initialize( width, height, default_value='' )
         @width = width
         @height = height
         @values = Array.new( width ){ Array.new(height){ default_value } }
     end

     # Reads the value from column _x_, row _y_.
     #
     # StringMatrices are 1-based, not zero-based.
     # (The first item in the matrix is 1,1 and the last is _width_,_height_)
     def ( x, y )
         if !y
             @values[ x-1 ].dup
         elsif !x
             a =
             1.upto(@width){ |x|
                 a << @values[ x-1 ][ y-1 ]
             }
             a
         else
             @values[ x-1 ][ y-1 ]
         end
     end

     # Sets the value in column _x_, row _y_ to _val_.
     #
     # StringMatrices are 1-based, not zero-based.
     # (The first item in the matrix is 1,1 and the last is _width_,_height_)
     def =( x, y, val )
         @values[ x-1 ][ y-1 ] = val
     end

     # Adds the supplied _right_matrix_ from the current matrix
     # and returns the result. (The original matrix is not modified.)
     def +( right_matrix )
         raise "Size mismatch" if width != right_matrix.width || height != right_matrix.height
         out = self.class.new( width, height )
         1.upto( @height ){ |y|
             1.upto( @width ){ |x|
                 out[ x, y ] = self.class.add( self[ x, y ], right_matrix[ x, y ] )
             }
         }
         out
     end

     # Subtracts the supplied _right_matrix_ from the current matrix
     # and returns the result. (The original matrix is not modified.)
     def -( right_matrix )
         raise "Size mismatch" if width != right_matrix.width || height != right_matrix.height
         out = self.class.new( width, height )
         1.upto( @height ){ |y|
             1.upto( @width ){ |x|
                 out[ x, y ] = self.class.subtract( self[ x, y ], right_matrix[ x, y ] )
             }
         }
         out
     end

     # Performs matrix multiplication between the two matrices.
     # (The original matrix is not modified.)
     def *( right_matrix )
         raise "Size mismatch" if width != right_matrix.height || height != right_matrix.width
         out = self.class.new( width, height )
         1.upto( @height ){ |y|
             1.upto( @width ){ |x|
                 row = self[ nil, y ]
                 col = right_matrix[ x, nil ]
                 val = row.zip( col ).inject( 0 ){ |sum, pair|
                     self.class.add( sum, self.class.multiply( *pair ) )
                 }
                 out[ x, y ] = val
             }
         }
         out
     end

     # Scales the matrix, multiplying each value by _scale_value_ and returning
     # the resulting matrix.
     # (The original matrix is not modified.)
     def scale( scale_value )
         out = self.class.new( width, height )
         1.upto( @height ){ |y|
             1.upto( @width ){ |x|
                 out[ x, y ] = self.class.multiply( self[ x, y ], scale_value )
             }
         }
         out
     end

     # Parses a multi-line string for use as a StringMatrix
     #
     # Lines in the string may be delimited by tabs, vertical bars, or commas.
     # The most common item is used as the separator; if none of the above are
     # present in the string, spaces are used.
     def self.parse( raw_str )
         best_count = 1
         split_char = [ "\t", '|', ',' ].inject( ' ' ){ |split_char, char>
             count = raw_str.scan( char ).length
             if count > best_count
                 best_count = count
                 char
             else
                 split_char
             end
         }

         values =
         y = 0
         raw_str.each_line{ |line|
             line.split( split_char ).each_with_index{ |val, x|
                 val.strip!
                 ( values[ x ] ||= )[ y ] = case val
                     when /^\d+$/ then val.to_i
                     when /^\d+\.\d+$/ then val.to_f
                     else val
                 end
             }
             y += 1
         }

         width = values.length
         height = y
         out = self.new( width, height, '' )
         1.upto( height ){ |y|
             1.upto( width ){ |x|
                 out[ x, y ] = values[ x-1 ][ y-1 ]
             }
         }
         out
     end

     def to_s( no_padding=false )
         out = ''
         column_widths = @values.collect{ |col|
             no_padding ? 0 : col.inject(0){ |max_len,val|
                 len = val.to_s.length
                 max_len > len ? max_len : len
             }
         }
         1.upto(@height){ |y|
             1.upto(@width){ |x|
                 out << self[ x, y ].to_s.centered_in( column_widths[ x-1 ] )
                 out << " | " unless x == @width
             }
             out << "\n" unless y == @height
         }
         out
     end

end

class String
     # Returns a copy of the string, centered (by padding both sides with spaces)
     # within the specified width.
     #
     # If width is smaller than the length of the string, the string itself is returned.
     def centered_in( width )
         out = self.dup
         remains = width - out.length
         if remains > 0
             back = remains / 2
             front = remains - back
             out = " "*front + out + " "*back
         end
         out
     end
end

if $0 == __FILE__
     m1 = StringMatrix.parse( "0,1,2\n3,4,5" )
     m2 = StringMatrix.parse( "x,y,z\n3q,4r,5s" )
     puts <<-ENDOUTPUT
M1:
---------------------------------------------------------------------
#{ m1 }

M2:
---------------------------------------------------------------------
#{ m2 }

M1 scaled by 2:
---------------------------------------------------------------------
#{ m1.scale( 2 ) }

M2 scaled by 2:
---------------------------------------------------------------------
#{ m2.scale( 2 ) }

M1 + M2:
---------------------------------------------------------------------
#{ m1 + m2 }

     ENDOUTPUT

x = StringMatrix.parse <<-ENDMATRIX
1 0 0
0 cosX sinX
0 -sinX cosX
     ENDMATRIX

y = StringMatrix.parse <<-ENDMATRIX
cosY | 0 | -sinY
0 | 1 | 0
sinY | 0 | cosY
     ENDMATRIX

     z = StringMatrix.parse <<-ENDMATRIX
cosZ sinZ 0
-sinZ cosZ 0
0 0 1
     ENDMATRIX

     puts <<-ENDOUT
X:
-----------------------------------------------------------------------------
#{x}

Y:
-----------------------------------------------------------------------------
#{y}

Z:
-----------------------------------------------------------------------------
#{z}

X - Y:
-----------------------------------------------------------------------------
#{x-y}

X * Y * Z:
-----------------------------------------------------------------------------
#{x*y*z}
     ENDOUT

end

Hard to enjoy it properly when email wraps the lines so badly.
http://phrogz.net/RubyLibs/stringmatrix.rb

···

On Jun 8, 2005, at 8:23 AM, Gavin Kistner wrote:

Sample output follows, followed by the code itself. Enjoy!

* Gavin Kistner <gavin@refinery.com> [2005-06-08 23:23:31 +0900]:

(This isn't a library I plan on maintaining, but ANN felt like the
right prefix for offering up code for mass enjoyment.)

At work yesterday I needed to do some algebra on some matrix math to
reverse-derive a value. (To be specific, I needed to figure out how
to draw the three euler rotation values out of a ZXY rotation
matrix.) After spending 2 minutes writing out equations and fearing I
was making a mistake, I turned to Excel and some stupid text
manipulation to produce my formulae. After 10 minutes of messing
around, I got the answer wrong anyhow.

I decided to write a little Ruby library to do the task for me. I
wrote a matrix class which knows how to do matrix math on strings,
producing formulae from them. After the initial pass produced a lot
of values like "0*sinX" and "0 + 0 + 1*(sinX)" I made it a bit
smarter, so that 0s and 1s properly simplify the equations during
calculation.

It doesn't produce perfectly-reduced equations by any means, but it
did the trick. In the end, I got my result :slight_smile:

Ahh, a symbolic math library. Gavin, you're a young Wolfram at heart. :wink:

I remember seeing a symbolic library in the RAA long ago. Don't know if
it did matrices, but it did reduction. Possibly you could send the
result of your code through its reduction engine as a final step.

Nice work.

···

--
Jim Freeze
Theory and practice are the same, in theory. -- Ryan Davis

on a related note

   http://blade.nagaokaut.ac.jp/~sinara/ruby/math
   http://blade.nagaokaut.ac.jp/~sinara/ruby/math/algebra/

cheers.

-a

···

On Wed, 8 Jun 2005, Gavin Kistner wrote:

(This isn't a library I plan on maintaining, but ANN felt like the right prefix for offering up code for mass enjoyment.)

At work yesterday I needed to do some algebra on some matrix math to reverse-derive a value. (To be specific, I needed to figure out how to draw the three euler rotation values out of a ZXY rotation matrix.) After spending 2 minutes writing out equations and fearing I was making a mistake, I turned to Excel and some stupid text manipulation to produce my formulae. After 10 minutes of messing around, I got the answer wrong anyhow.

I decided to write a little Ruby library to do the task for me. I wrote a matrix class which knows how to do matrix math on strings, producing formulae from them. After the initial pass produced a lot of values like "0*sinX" and "0 + 0 + 1*(sinX)" I made it a bit smarter, so that 0s and 1s properly simplify the equations during calculation.

It doesn't produce perfectly-reduced equations by any means, but it did the trick. In the end, I got my result :slight_smile:

--

email :: ara [dot] t [dot] howard [at] noaa [dot] gov
phone :: 303.497.6469
My religion is very simple. My religion is kindness.
--Tenzin Gyatso

===============================================================================

Do you (or anyone else) use this library? I _barely_ understand how to set up a single polynomial function using it (what is a 'ring' anyhow?), and I can't see how you would create a matrix of arbitrary formulae that would be properly multiplied and reduced.

···

On Jun 8, 2005, at 10:09 AM, Ara.T.Howard wrote:

On Wed, 8 Jun 2005, Gavin Kistner wrote:

I decided to write a little Ruby library to do the task for me. I wrote a matrix class which knows how to do matrix math on strings, producing formulae from them. After the initial pass produced a lot of values like "0*sinX" and "0 + 0 + 1*(sinX)" I made it a bit smarter, so that 0s and 1s properly simplify the equations during calculation.

It doesn't produce perfectly-reduced equations by any means, but it did the trick. In the end, I got my result :slight_smile:

on a related note

  http://blade.nagaokaut.ac.jp/~sinara/ruby/math
  http://blade.nagaokaut.ac.jp/~sinara/ruby/math/algebra/

As an example, the set of integers Z (positive and negative) is a Ring.
(but not the positive integers only). You have a the common addition and
multiplication, but not division. The set of polynomials on a field is
also a Ring.

That said, I can't help you with the math library as I've never used
it :(.

Guillaume.

···

On Thu, 2005-06-09 at 09:43 +0900, Gavin Kistner wrote:

On Jun 8, 2005, at 10:09 AM, Ara.T.Howard wrote:
> On Wed, 8 Jun 2005, Gavin Kistner wrote:
>> I decided to write a little Ruby library to do the task for me. I
>> wrote a matrix class which knows how to do matrix math on strings,
>> producing formulae from them. After the initial pass produced a
>> lot of values like "0*sinX" and "0 + 0 + 1*(sinX)" I made it a bit
>> smarter, so that 0s and 1s properly simplify the equations during
>> calculation.
>>
>> It doesn't produce perfectly-reduced equations by any means, but
>> it did the trick. In the end, I got my result :slight_smile:
> on a related note
>
> http://blade.nagaokaut.ac.jp/~sinara/ruby/math
> http://blade.nagaokaut.ac.jp/~sinara/ruby/math/algebra/

Do you (or anyone else) use this library? I _barely_ understand how
to set up a single polynomial function using it (what is a 'ring'
anyhow?),