Complex numbers (Ruby)

From LiteratePrograms

Jump to: navigation, search
Other implementations: C++ | Java | Ruby | Scala


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
Views