The exponential and trigonometric functions in Python
# The exponential and trigonometric functions
# in Python also accept complex arguments when
# we use the 'cmath' library.
#
# One of the most important relationships
# involving complex numbers is Euler's formula:
#
#     exp(beta*j) = cos(beta) + j*sin(beta),
#
# where beta is a real number measured in
# radians.

import cmath
import math


beta = math.pi/3

print( "\nexp(beta*j):" )
print( cmath.exp(beta*1j) )

print( "\ncos(beta) + j*sin(beta):" )
print( math.cos(beta) + 1j*math.sin(beta) )


# Apart from possible floating-point rounding,
# these two values are the same.
#
# Notice that the magnitude of
#
#     exp(beta*j)
#
# is always 1. Changing beta simply changes its
# argument.
#
# Thus, as beta varies,
#
#     exp(beta*j)
#
# moves around the unit circle.


# Euler's formula also allows us to express the
# trigonometric functions using exponential
# functions.
#
# Starting with
#
#     exp(beta*j) = cos(beta) + j*sin(beta),
#
# take the complex conjugate of both sides:
#
#     conj(exp(beta*j))
#
#         = cos(beta) - j*sin(beta).
#
# Since
#
#     conj(exp(beta*j)) = exp(-beta*j),
#
# we have
#
#     exp(-beta*j) = cos(beta) - j*sin(beta).


# Adding these two equations gives
#
#     exp(beta*j) + exp(-beta*j)
#
#         = 2*cos(beta).
#
# Therefore,
#
#                 exp(beta*j) + exp(-beta*j)
#     cos(beta) = -----------------------------.
#                              2


print( "\ncos(beta):" )
print( math.cos(beta) )

print( "\nCalculated using exponentials:" )
print(
    (cmath.exp(beta*1j)
     + cmath.exp(-beta*1j))/2
)


# Subtracting the two equations instead gives
#
#     exp(beta*j) - exp(-beta*j)
#
#         = 2*j*sin(beta).
#
# Therefore,
#
#                 exp(beta*j) - exp(-beta*j)
#     sin(beta) = -----------------------------.
#                             2*j


print( "\nsin(beta):" )
print( math.sin(beta) )

print( "\nCalculated using exponentials:" )
print(
    (cmath.exp(beta*1j)
     - cmath.exp(-beta*1j))/(2j)
)


# These identities are often written as
#
#                 exp(j*beta) + exp(-j*beta)
#     cos(beta) = -----------------------------
#                              2
#
# and
#
#                 exp(j*beta) - exp(-j*beta)
#     sin(beta) = -----------------------------.
#                             2*j
#
# These are not merely computational tricks.
# They show that the exponential and
# trigonometric functions are closely connected
# through complex numbers.


# Python also allows the arguments of the
# trigonometric functions themselves to be
# complex when we use the 'cmath' library.
#
# For example,

z = 0.4 + 0.7j

print( "\nA complex value z:" )
print( z )

print( "\nexp(z):" )
print( cmath.exp(z) )

print( "\ncos(z):" )
print( cmath.cos(z) )

print( "\nsin(z):" )
print( cmath.sin(z) )


# The exponential definitions of sine and
# cosine continue to work even when z itself is
# complex:
#
#              exp(j*z) + exp(-j*z)
#     cos(z) = ---------------------
#                        2
#
# and
#
#              exp(j*z) - exp(-j*z)
#     sin(z) = ---------------------.
#                       2*j


print( "\ncos(z) using exponentials:" )
print(
    (cmath.exp(1j*z)
     + cmath.exp(-1j*z))/2
)

print( "\nsin(z) using exponentials:" )
print(
    (cmath.exp(1j*z)
     - cmath.exp(-1j*z))/(2j)
)


# The exponential function is also closely
# related to powers and logarithms.
#
# Recall that, for positive real numbers,
#
#      w
#     z  = exp(w*ln(z)).
#
# In Python, as in most programming languages,
# the function
#
#     log(...)
#
# means the NATURAL LOGARITHM.
#
# That is,
#
#     log(z)
#
# means
#
#     ln(z),
#
# the logarithm with base e.
#
# It does NOT mean the base-10 logarithm.
#
# For real numbers, the 'math' library provides
#
#     math.log(...)
#
# for the natural logarithm and
#
#     math.log10(...)
#
# for the base-10 logarithm.


print( "\nNatural logarithm of 10:" )
print( math.log(10) )

print( "\nBase-10 logarithm of 10:" )
print( math.log10(10) )


# The 'cmath' library extends the natural
# logarithm to complex numbers.
#
# Thus,
#
#     cmath.log(z)
#
# is the natural logarithm of a complex number.
#
# Again, 'log' means NATURAL logarithm.


# If
#
#     z = r*exp(j*theta),
#
# then a complex logarithm of z is
#
#     log(z) = log(r) + j*theta.
#
# Since an argument is not unique, however,
#
#     theta + 2*k*pi
#
# represents the same direction for every
# integer k.
#
# Consequently, the complex logarithm is
# naturally multivalued:
#
#     log(r) + j*(theta + 2*k*pi).
#
# Python's cmath.log(z) returns the PRINCIPAL
# value, using the principal argument of z.


z = 2 + 3j

print( "\nA complex value z:" )
print( z )

print( "\nThe principal value of log(z):" )
print( cmath.log(z) )


# We can see the connection with magnitude and
# argument directly.
#
# The real component of log(z) is
#
#     log(|z|),
#
# while its imaginary component is the
# principal argument of z.

print( "\nlog(|z|):" )
print( math.log(abs(z)) )

print( "\nThe principal argument of z:" )
print( cmath.phase(z) )


# We can reconstruct z by exponentiating its
# logarithm.

print( "\nexp(log(z)):" )
print( cmath.exp(cmath.log(z)) )


# This gives us a very powerful way to define
# complex powers.
#
# For complex numbers z and w, with z nonzero,
# we can calculate a principal value of
#
#      w
#     z
#
# using
#
#      w
#     z  = exp(log(z)*w).
#
# Here, log(z) is the principal complex
# logarithm returned by Python.


z = 2 + 3j
w = 0.4 - 0.7j

print( "\nz:" )
print( z )

print( "\nw:" )
print( w )

print( "\nz**w calculated directly:" )
print( z**w )

print( "\nz**w calculated using exp and log:" )
print( cmath.exp(cmath.log(z)*w) )


# Apart from possible floating-point rounding,
# these calculations give the same principal
# value.
#
# Notice that exponentiation in Python uses
#
#     **
#
# and NOT
#
#     ^.
#
# The operator '^' is the bitwise exclusive-OR
# operator in Python.


# Thus, the exponential and logarithm allow us
# to make sense of powers in which both the
# base and the exponent may be complex:
#
#      w
#     z  = exp(w*log(z)).
#
# This is an important extension of the familiar
# rules for real exponentials and logarithms.


# There is one important mathematical detail.
#
# Because a nonzero complex number has
# infinitely many possible arguments,
#
#     theta + 2*k*pi,
#
# it also has infinitely many possible
# logarithms:
#
#     log(|z|) + j*(theta + 2*k*pi).
#
# If these different logarithms are used in
#
#     exp(w*log(z)),
#
# they may produce different values when w is
# not an integer.
#
# Programming languages must choose one value
# when evaluating functions such as
#
#     log(z)
#
# and
#
#     z**w.
#
# Python uses the principal complex logarithm
# to select the principal value.
#
# Thus, when working with complex numbers, it
# is important to distinguish between the
# mathematical multivalued logarithm or power
# and the single principal value returned by a
# programming language.