Geometric series in Python
# A geometric series is a sum in which each
# term is obtained from the previous term by
# multiplying by the same value.
#
# For example,
#
#              2    3    4
#     1 + z + z  + z  + z  + ...
#
# is a geometric series with common ratio z.
#
# There is no requirement that the common ratio
# be real. The value z may be any complex
# number.

import cmath
import math


# You are welcome to change this value of 'z'.

z = 0.2 + 0.5j

print( "\nThe common ratio z:" )
print( z )

print( "\nThe magnitude of z:" )
print( abs(z) )


# First, consider the finite geometric sum
#
#          N
#        -----
#         \     n
#          )   z
#         /
#        -----
#        n = 0
#
# which means
#
#                    2              N
#     1 + z + z  + ... + z .
#
# Let this sum be S:
#
#                         2              N
#     S = 1 + z + z  + ... + z .
#
# Multiplying both sides by z gives
#
#                              2              N+1
#     z*S = z + z  + ... + z  + z.
#
# Subtracting the second equation from the first
# causes almost every term to cancel:
#
#                         N+1
#     S - z*S = 1 - z.
#
# Factoring the left side gives
#
#                         N+1
#     S*(1 - z) = 1 - z.
#
# Therefore, provided z is not equal to 1,
#
#             N+1
#        1 - z
#     S = --------.
#         1 - z
#
# Thus,
#
#          N
#        -----            N+1
#         \     n    1 - z
#          )   z  = --------.
#         /          1 - z
#        -----
#        n = 0
#
# This formula works for complex values of z
# just as it does for real values of z.


# Let us calculate the finite sum for N = 10.

N = 10

total = 0

for n in range(N + 1):
    total = total + z**n

print( "\nFinite sum up to", N, ":" )
print( total )


# We can calculate the same finite sum directly
# from the geometric-sum formula.

finite_formula = (1 - z**(N + 1))/(1 - z)

print( "\nUsing the finite-sum formula:" )
print( finite_formula )


# These two calculations should give essentially
# the same result. Any very small difference is
# the result of floating-point rounding.


# We are especially interested in what happens
# as N becomes larger and larger.
#
# The finite sum is
#
#             N+1
#        1 - z
#     S = --------.
#         1 - z
#
# If
#
#     |z| < 1,
#
# then
#
#      N+1
#     z     -> 0
#
# as N becomes arbitrarily large.
#
# Why?
#
# Recall that
#
#      N+1       N+1
#     |z    | = |z|   .
#
# If |z| < 1, repeatedly multiplying |z| by
# itself produces values that become smaller
# and smaller, approaching zero.
#
# Consequently,
#
#             N+1
#        1 - z            1
#     -----------  ->  -------.
#          1 - z        1 - z
#
# Therefore,
#
#        infinity
#        --------
#           \       n       1
#            )     z  = --------
#           /           1 - z
#        --------
#          n = 0
#
# provided
#
#     |z| < 1.


# Check whether the infinite geometric series
# converges for our chosen value of z.

if abs(z) >= 1:

    print(
        "\nFor the infinite geometric series "
        "to converge, |z| < 1."
    )

else:

    infinite_sum = 1/(1 - z)

    print( "\nThe infinite series converges to:" )
    print( infinite_sum )


# Let us now see numerically how the finite sums
# approach the infinite sum.
#
# Start by calculating
#
#          10
#        -----
#         \     n
#          )   z .
#         /
#        -----
#        n = 0

total = 0

for n in range(11):
    total = total + z**n

print( "\nFinite sum up to 10:" )
print( total )


# Add the next ten terms so that we now have
#
#          20
#        -----
#         \     n
#          )   z .
#         /
#        -----
#        n = 0
#
# Recall that the second argument of
# 'range(11, 21)' is not included. Thus, this
# produces
#
#     11, 12, ..., 20.

for n in range(11, 21):
    total = total + z**n

print( "\nFinite sum up to 20:" )
print( total )


# Add the next eighty terms so that we now have
#
#         100
#        -----
#         \     n
#          )   z .
#         /
#        -----
#        n = 0

for n in range(21, 101):
    total = total + z**n

print( "\nFinite sum up to 100:" )
print( total )


# Compare this with the exact value of the
# infinite geometric series.

if abs(z) < 1:

    infinite_sum = 1/(1 - z)

    print( "\nInfinite sum:" )
    print( infinite_sum )


# We can do more than simply observe that the
# finite sums appear to be approaching the
# infinite sum.
#
# We know exactly what the difference must be.
#
# The infinite sum is
#
#             1
#     S = --------
#          1 - z
#
# while the finite sum through z**N is
#
#                  N+1
#             1 - z
#     S_N = -----------
#               1 - z.
#
# Subtracting gives
#
#                       N+1
#                      z
#     S - S_N = -------------.
#                     1 - z
#
# Thus, the error remaining after adding the
# terms through z**N is exactly
#
#          N+1
#         z
#     ---------.
#       1 - z
#
# Taking magnitudes gives
#
#                         N+1
#                      |z|
#     |S - S_N| = -------------.
#                       |1 - z|
#
# Since |z| < 1, the numerator becomes smaller
# and smaller as N increases.


# Let us compare the calculated error with this
# formula.

if abs(z) < 1:

    infinite_sum = 1/(1 - z)

    print( "\nErrors in the finite sums:" )

    for N in [10, 20, 100]:

        finite_sum = (
            (1 - z**(N + 1))/(1 - z)
        )

        error = abs(infinite_sum - finite_sum)

        predicted_error = (
            abs(z)**(N + 1)/abs(1 - z)
        )

        print( "\nN =", N )
        print( "Calculated error:", error )
        print( "Predicted error: ", predicted_error )


# There is also a useful geometric
# interpretation of what is happening.
#
# Write the common ratio in polar form:
#
#     z = r exp(j*theta).
#
# Then
#
#      n    n
#     z  = r exp(j*n*theta).
#
# Each successive term therefore does two
# things:
#
#   1. Its magnitude is multiplied by r = |z|.
#
#   2. Its argument is increased by theta.
#
# If |z| < 1, the magnitudes of the terms
# become smaller and smaller.
#
# If the argument of z is nonzero, the
# direction of each successive term also
# rotates.
#
# Thus, the terms can be thought of as vectors
# that rotate while becoming progressively
# shorter.


# For our example,

print( "\nMagnitude of the common ratio:" )
print( abs(z) )

print( "\nArgument of the common ratio in radians:" )
print( cmath.phase(z) )

print( "\nArgument of the common ratio in degrees:" )
print( math.degrees(cmath.phase(z)) )


# Each successive term has its magnitude
# multiplied by |z| and its argument increased
# by phase(z).
#
# Consequently, the partial sums generally do
# not approach the infinite sum along a straight
# line. Instead, they may spiral toward it.


# Finally, consider what happens if |z| >= 1.
#
# If |z| > 1, then the magnitudes
#
#          2     3
#     1, |z|, |z|, |z|, ...
#
# grow rather than approach zero.
#
# If |z| = 1, then
#
#      n
#     |z| = 1
#
# for every n, so the terms again do not
# approach zero.
#
# A necessary condition for any infinite series
# to converge is that its individual terms
# approach zero.
#
# Therefore, the infinite geometric series
#
#        infinity
#        --------
#           \       n
#            )     z
#           /
#        --------
#          n = 0
#
# converges exactly when
#
#     |z| < 1.
#
# When it converges, its value is
#
#          1
#     ---------.
#       1 - z