Complex numbers (Ruby)
From LiteratePrograms
Contents |
Representation
We will represent a complex number with the Complex
class. This class includes initialization and accessors for to query and build the complex number as well as basic arithmetic functions:
<<complex class>>= class Complex initialization accessors arithmetic end
We will provide access to both the coordinate and polar/phasor representations of the complex number.
Initialization
The initialization method will store only the starting representation:
<<initialization>>= def initialize( a, b, type=:coordinate ) case type when :coordinate @real = a.to_f @imag = b.to_f when :polar @magnitude = a.to_f @theta = b.to_f else raise ArgumentError, "unknown type '#{type}'" end end
Accessors
Access to a representation other than the original is provided on demand, with caching:
<<accessors>>= def real @real ||= @magnitude * Math.cos(@theta) end def imag @imag ||= @magnitude * Math.sin(@theta) end def magnitude @magnitude ||= Math.hypot(@real, @imag) end def theta @theta ||= Math.atan2(@imag, @real) end
Arithmetic
Core arithmetic operations provided are addition, subtraction, multiplication, division and exponentiation. The arithmetic interface is unified to accept either complex or real arguments, with any necessary distinction made in the method itself.
<<arithmetic>>= addition subtraction multiplication division exponentiation
Addition and subtraction
Addition and subtraction are implemented using the coordinate representation:
<<addition>>= def +( other ) if Complex === other Complex.new( self.real + other.real, self.imag + other.imag ) else Complex.new( self.real + other, self.imag ) end end <<subtraction>>= def -( other ) if Complex === other Complex.new( self.real - other.real, self.imag - other.imag ) else Complex.new( self.real - other, self.imag ) end end
Multiplication and division
Multiplication and division are implemented using the polar/phasor representation:
<<multiplication>>= def *( other ) if Complex === other Complex.new( self.magnitude * other.magnitude, self.theta + other.theta, :polar ) else Complex.new( self.magnitude * other, self.theta, :polar ) end end <<division>>= def /( other ) if Complex === other Complex.new( self.magnitude / other.magnitude, self.theta - other.theta, :polar ) else Complex.new( self.magnitude / other, self.theta, :polar ) end end
Exponentiation
Exponentiation with a real exponent is easy using the polar/phasor representation. To implement exponentiation with a complex exponent, we'll make use of a change in base:
- qz = ezlnq
Letting and , we can derive:
- qz = ea + ib
- qz = eaeib
- | qz | = ea
- θ(qz) = b
<<exponentiation>>= def **( other ) if Complex === other log = log z_prime = other * log Complex.new( Math.exp(z_prime.real), z_prime.imag, :polar ) else Complex.new( self.magnitude ** other, self.theta * other, :polar ) end end
To calculate the logarithm of q when q is complex, we utilize the polar/phasor representation:
- lnq = lnreiθ = lnr + iθ
<<log>>= Complex.new( Math.ln(self.magnitude), self.theta, :coordinate )
Test Suite
To build a test suite for this class we'll use the Test::Unit
library. First we place the class in a file, then include that class and build a TestCase
.
<<complex.rb>>= complex class <<complex_test.rb>>= require 'test/unit' require './complex' class TestComplex < Test::Unit::TestCase define tolerance test initialization from coordinates test initialization from polar test polar access from coordinates test coordinate access from polar test addition test subtraction test multiplication test division test real exponentiation end
Initialization
First we'll test that initialization works as expected:
<<test initialization from coordinates>>= def test_initialization_from_coordinates z = Complex.new( 1, 2 ) assert_equal 1, z.real assert_equal 2, z.imag end <<test initialization from polar>>= def test_initialization_from_polar z = Complex.new( 1, 2, :polar ) assert_equal 1, z.magnitude assert_equal 2, z.theta end
Accessors
Next we verify that our on-demand accessors work correctly. These and the arithmetic checks all require that we define a tolerance first:
<<define tolerance>>= TOLERANCE = 0.001 <<test polar access from coordinates>>= def test_polar_access_from_coordinates z = Complex.new( 1, 1 ) assert_in_delta Math.sqrt(2), z.magnitude, TOLERANCE assert_in_delta Math::PI/4, z.theta, TOLERANCE end <<test coordinate access from polar>>= def test_coordinates_access_from_polar z = Complex.new( 1, Math::PI / 4, :polar ) assert_in_delta Math.sqrt(0.5), z.real, TOLERANCE assert_in_delta Math.sqrt(0.5), z.imag, TOLERANCE end
Arithmetic
Finally we'll check all of our arithmetic methods:
<<test addition>>= def test_addition z1 = Complex.new( 1, 2 ) z2 = Complex.new( 3, 4 ) z3 = z1 + z2 assert_equal 4, z3.real assert_equal 6, z3.imag z4 = z1 + 5 assert_equal 6, z4.real assert_equal 2, z4.imag end <<test subtraction>>= def test_subtraction z1 = Complex.new( 3, 4 ) z2 = Complex.new( 2, 1 ) z3 = z1 - z2 assert_equal 1, z3.real assert_equal 3, z3.imag z4 = z1 - 2 assert_equal 1, z4.real assert_equal 4, z4.imag end <<test multiplication>>= def test_multiplication z1 = Complex.new( 1, Math::PI / 3, :polar ) z2 = Complex.new( 2, Math::PI / 6, :polar ) z3 = z1 * z2 assert_equal 2, z3.magnitude assert_in_delta Math::PI / 2, z3.theta, TOLERANCE z4 = z1 * 5 assert_equal 5, z4.magnitude assert_equal Math::PI / 3, z4.theta end <<test division>>= def test_division z1 = Complex.new( 1, Math::PI / 3, :polar ) z2 = Complex.new( 2, Math::PI / 4, :polar ) z3 = z1 / z2 assert_in_delta 0.5, z3.magnitude, TOLERANCE assert_in_delta Math::PI / 12, z3.theta, TOLERANCE z4 = z1 / 3 assert_in_delta 1.0/3.0, z4.magnitude, TOLERANCE assert_equal Math::PI / 3, z4.theta end <<test real exponentiation>>= def test_exponentiation z1 = Complex.new( 2, Math::PI / 3, :polar ) z2 = z1 ** 3 assert_equal 8, z2.magnitude assert_in_delta Math::PI, z2.theta, TOLERANCE z3 = z1 ** 0.5 assert_in_delta Math.sqrt(2), z3.magnitude, TOLERANCE assert_in_delta Math::PI / 6, z3.theta, TOLERANCE end
Download code |