Back

Python

"""
################################################################################
#                                   LICENSE                                    #
################################################################################
#   This file is part of mitx_mathematics_programming_examples.                #
#                                                                              #
#   mitx_mathematics_programming_examples is free software: you can            #
#   redistribute it and/or modify it under the terms of the GNU General        #
#   Public License as published by the Free Software Foundation, either        #
#   version 3 of the License, or (at your option) any later version.           #
#                                                                              #
#   mitx_mathematics_programming_examples is distributed in the hope that      #
#   it will be useful but WITHOUT ANY WARRANTY; without even the implied       #
#   warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.           #
#   See the GNU General Public License for more details.                       #
#                                                                              #
#   You should have received a copy of the GNU General Public License          #
#   along with mitx_mathematics_programming_examples. If not, see              #
#   <https://www.gnu.org/licenses/>.                                           #
################################################################################
#   Purpose:                                                                   #
#       Implements complex numbers using a Python class.                       #
################################################################################
#   Author: Ryan Maguire                                                       #
#   Date:   2024/11/05                                                         #
################################################################################
"""

# The modulus and principal argument of a complex number are computed using
# the square root function and the arctan function. These are found here.
import math

class Complex:
    """
        Class:
            Complex
        Purpose:
            Python class for working with complex numbers.
            This contains methods (functions) for working with
            common routines found in complex analysis, and
            implements the basics of complex arithmetic.
        Attributes:
            real (float):
                The real part of the complex number.
            imag (float):
                The imaginary part of the complex number.
        Operators:
            + (complex addition)
            += (in-place complex addition)
            - (complex subtraction)
            -= (in-place complex subtraction)
            * (complex multiplication)
            *= (in-place complex multiplication)
            / (complex division)
            /= (in-place complex division)
            - (unary operator, complex negation)
    """
    def __init__(self, real, imag):
        """
            Function:
                __init__
            Purpose:
                Creates an instance of the Complex class.
            Arguments:
                real (float):
                    The real part of the complex number.
                imag (float):
                    The imaginary part of the complex number.
            Output:
                z (Complex):
                    The complex number real + i*imag
        """

        # Both inputs should be "floats", which are real numbers in a computer.
        try:
            self.real = float(real)
            self.imag = float(imag)

        # If we can't convert the inputs into floats, the user gave invalid
        # inputs. Treat this as an error.
        except (TypeError, ValueError) as err:
            raise TypeError(
                "A complex number is created from two real numbers."
            ) from err

    # + operator for complex addition.
    def __add__(self, other):
        """
            Operator:
                Addition (+).
            Purpose:
                Performs complex addition.
            Arguments:
                other (Complex):
                    The complex number being added to self.
            Outputs:
                sum (Complex):
                    The complex sum of self and other.
            Method:
                Add the complex numbers component-wise. If self and other
                are represented as z = a + ib and w = c + id,
                respectively, the sum is then:

                    z + w = (a + ib) + (c + id)
                          = (a + c) + i(b + d)

                That is, complex addition is done component-wise.
        """

        # We can only add complex numbers and real numbers together. Check if
        # the input is either of these.
        if not isinstance(other, Complex):

            # The input is not a complex number. See if it is a real number.
            try:
                other_real = float(other)
            except (TypeError, ValueError) as err:
                raise TypeError(
                    "Input is not real or complex. Cannot add."
                ) from err

            # If we get here, the input was a real number. We can add.
            real_sum = self.real + other_real
            imag_sum = self.imag
            return Complex(real_sum, imag_sum)

        # If we get here, the input is a complex number. Add the components.
        real_sum = self.real + other.real
        imag_sum = self.imag + other.imag
        return Complex(real_sum, imag_sum)

    # += operator for complex addition.
    def __iadd__(self, other):
        """
            Operator:
                Addition (+=).
            Purpose:
                Performs complex addition in-place.
            Arguments:
                other (Complex):
                    The complex number being added to self.
            Outputs:
                self
            Method:
                Perform complex addition component-wise and store
                the result in self.
        """

        # Similar error check to the one found in __add__.
        if not isinstance(other, Complex):

            # The input is not a complex number. See if it is a real number.
            try:
                self.real += float(other)
            except (TypeError, ValueError) as err:
                raise TypeError(
                    "Input is not real or complex. Cannot add."
                ) from err

        # If we get here, the input is a complex number. Add the components.
        else:
            self.real += other.real
            self.imag += other.imag

        return self

    # - operator for complex subtraction.
    def __sub__(self, other):
        """
            Operator:
                Subtraction (-).
            Purpose:
                Performs complex subtraction.
            Arguments:
                other (Complex):
                    The complex number being subtracted from self.
            Outputs:
                diff (Complex):
                    The complex difference of self and other.
            Method:
                Subtract the complex numbers component-wise. If self
                and other are represented as z = a + ib and w = c + id,
                respectively, the difference is then:

                    z - w = (a + ib) - (c + id)
                          = (a - c) + i(b - d)

                That is, complex subtraction is done component-wise.
        """

        # We can only subtract complex numbers and real numbers. Check if
        # the input is either of these.
        if not isinstance(other, Complex):

            # The input is not a complex number. See if it is a real number.
            try:
                other_real = float(other)
            except (TypeError, ValueError) as err:
                raise TypeError(
                    "Input is not real or complex. Cannot subtract."
                ) from err

            # If we get here, the input was a real number. We can subtract.
            real_diff = self.real - other_real
            imag_diff = self.imag
            return Complex(real_diff, imag_diff)

        # If we get here, the input is a complex number. Subtract components.
        real_diff = self.real - other.real
        imag_diff = self.imag - other.imag
        return Complex(real_diff, imag_diff)

    # -= operator for complex subtraction.
    def __isub__(self, other):
        """
            Operator:
                Subtraction (-=).
            Purpose:
                Performs complex subtraction in-place.
            Arguments:
                other (Complex):
                    The complex number being subtracted from self.
            Outputs:
                self
            Method:
                Perform complex subtraction component-wise and store
                the result in self.
        """

        # Subtraction can be done with real or complex numbers.
        if not isinstance(other, Complex):

            # The input is not a complex number. See if it is a real number.
            try:
                self.real -= float(other)
            except (TypeError, ValueError) as err:
                raise TypeError(
                    "Input is not real or complex. Cannot subtract."
                ) from err

        # If we get here, the input is a complex number. Subtract components.
        else:
            self.real -= other.real
            self.imag -= other.imag

        return self

    # * operator for complex multiplication.
    def __mul__(self, other):
        """
            Operator:
                Multiplication (*).
            Purpose:
                Performs complex multiplication.
            Arguments:
                other (Complex):
                    A complex number.
            Outputs:
                prod (Complex):
                    The product of self and other.
            Method:
                Complex multiplication is computed using i^2 = -1.
                That is, if z = a + ib, and w = c + id, then:

                    z*w = (a + ib) * (c + id)
                        = ac + ibc + iad + i^2bd
                        = (ac - bd) + i(ad + bc)

                If other is purely real, this is just scalar multiplication.
        """

        # The input must be a real or complex number.
        if not isinstance(other, Complex):
            try:
                other_real = float(other)
            except (TypeError, ValueError) as err:
                raise TypeError(
                    "Input is not real or complex. Cannot multiply."
                ) from err

            # If we get here the input was a real number. Multiply components.
            prod_real = self.real * other_real
            prod_imag = self.imag * other_real
            return Complex(prod_real, prod_imag)

        # If we get here, the input was complex.
        prod_real = self.real * other.real - self.imag * other.imag
        prod_imag = self.real * other.imag + self.imag * other.real
        return Complex(prod_real, prod_imag)

    # * operator for complex multiplication on the right.
    def __rmul__(self, other):
        """
            Operator:
                Multiplication (*).
            Purpose:
                Performs complex multiplication (on the right).
            Arguments:
                other (Complex):
                    A complex number.
            Outputs:
                prod (Complex):
                    The product of self and other.
            Method:
                Complex multiplication is commutative, so this operator
                does the same thing as multiplication on the left.
        """

        # The input must be a real or complex number.
        if not isinstance(other, Complex):
            try:
                other_real = float(other)
            except (TypeError, ValueError) as err:
                raise TypeError(
                    "Input is not real or complex. Cannot multiply."
                ) from err

            # If we get here the input was a real number. Multiply components.
            prod_real = other_real * self.real
            prod_imag = other_real * self.imag
            return Complex(prod_real, prod_imag)

        # If we get here, the input was complex.
        prod_real = other.real * self.real - other.imag * self.imag
        prod_imag = other.real * self.imag + other.imag * self.real
        return Complex(prod_real, prod_imag)

    # *= operator for complex multiplication.
    def __imul__(self, other):
        """
            Operator:
                Multiplication (*=).
            Purpose:
                Performs complex multiplication in-place.
            Arguments:
                other (Complex):
                    A complex number.
            Outputs:
                self.
            Method:
                Performing complex multiplication naively in-place will
                overwrite the real part first, meaning the imaginary part
                will get the wrong input. We correct this by saving the
                real part as a new variable, and performing complex
                multiplication with this.
        """

        # The input must be a real or complex number.
        if not isinstance(other, Complex):
            try:
                other_real = float(other)
            except (TypeError, ValueError) as err:
                raise TypeError(
                    "Input is not real or complex. Cannot multiply."
                ) from err

            # If we get here the input was a real number. Multiply components.
            self.real *= other_real
            self.imag *= other_real
            return self

        # If we get here, the input was complex. Avoid overwriting data,
        # save the real part of self before computing.
        self_real = self.real

        # We can now perform the product.
        self.real = self.real * other.real - self.imag * other.imag
        self.imag = self_real * other.imag + self.imag * other.real
        return self

    # Negation operator.
    def __neg__(self):
        """
            Operator:
                Negation (-):
            Purpose:
                Negates a complex number.
            Arguments:
                None.
            Outputs:
                neg_self (Complex):
                    The negation of self.
            Method:
                Negate each component and return.
        """

        # Negate all components and return.
        real_val = -self.real
        imag_val = -self.imag
        return Complex(real_val, imag_val)

    # Complex conjugation.
    def conj(self):
        """
            Function:
                conj
            Purpose:
                Computes the complex conjugate of a complex number.
            Arguments:
                None.
            Output:
                conj_self (Complex):
                    The complex conjugate of self.
            Method:
                Negate the imaginary part and return.
        """
        return Complex(self.real, -self.imag)

    # Computes the complex modulus, or magnitude, of a complex number.
    def modulus(self):
        """
            Function:
                modulus
            Purpose:
                Computes the complex modulus of a complex number.
            Arguments:
                None.
            Output:
                abs_z (Complex):
                    The magnitude of self.
            Method:
                Use the Pythagorean formula.
        """

        # Pythagorean formula: Square root of the sum of the squares.
        real_squared = self.real * self.real
        imag_squared = self.imag * self.imag
        return math.sqrt(real_squared + imag_squared)

    # Computes the principal argument of a complex number.
    def arg(self):
        """
            Function:
                arg
            Purpose:
                Computes the principal argument of a complex number.
            Arguments:
                None.
            Output:
                arg_z (Complex):
                    The principal argument of self.
            Method:
                Use the atan2 function from the math module.
        """
        return math.atan2(self.imag, self.real)

    # Used for copying an instance of a complex number into a new variable.
    def copy(self):
        """
            Function:
                copy
            Purpose:
                Copies the data of self to a new variable.
            Arguments:
                None.
            Output:
                self_copy (Complex):
                    A copy of self.
        """
        return Complex(self.real, self.imag)

    # Converts a complex number into a string.
    def __str__(self):
        """
            Function:
                __str__
            Purpose:
                Converts a complex number into a string. Used for printing.
            Arguments:
                None.
            Output:
                self_string (str):
                    String representation of self.
        """

        # If the imaginary part is negative, write real - |imag| i, instead
        # of writing real + -imag i.
        if self.imag < 0.0:
            return f'{self.real} - {abs(self.imag)} i'

        return f'{self.real} + {self.imag} i'

# Let's test our routines.
if __name__ == "__main__":

    # Two test complex numbers to play with.
    z = Complex(1.0, 2.0)
    w = Complex(1.0, -1.0)

    # Test out negation.
    neg_z = -z

    # Test out complex arithmetic.
    summ = z + w
    diff = z - w
    prod = z * w

    # And try scalar multiplication.
    right_scale = z * 4.0
    left_scale = 4.0 * z

    # Test basic complex functions.
    magnitude = z.modulus()
    argument = z.arg()

    # Lastly, try out in-place arithmetic.
    new_sum = z.copy()
    new_diff = z.copy()
    new_prod = z.copy()

    new_sum += w
    new_diff -= w
    new_prod *= w

    # Print all of the results to the screen.
    print(f'z = {z}')
    print(f'w = {w}')
    print(f'-z = {neg_z}')
    print(f'z + w = {summ}')
    print(f'z - w = {diff}')
    print(f'z * w = {prod}')
    print(f'4 * z = {left_scale}')
    print(f'z * 4 = {right_scale}')
    print(f'|z| = {magnitude}')
    print(f'Arg(z) = {argument}')
    print(f'z += w -> {new_sum}')
    print(f'z -= w -> {new_diff}')
    print(f'z *= w -> {new_prod}')

C

/******************************************************************************
 *                                  LICENSE                                   *
 ******************************************************************************
 *  This file is part of mitx_mathematics_programming_examples.               *
 *                                                                            *
 *  mitx_mathematics_programming_examples is free software: you can           *
 *  redistribute it and/or modify it under the terms of the GNU General       *
 *  Public License as published by the Free Software Foundation, either       *
 *  version 3 of the License, or (at your option) any later version.          *
 *                                                                            *
 *  mitx_mathematics_programming_examples is distributed in the hope that     *
 *  it will be useful but WITHOUT ANY WARRANTY; without even the implied      *
 *  warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.          *
 *  See the GNU General Public License for more details.                      *
 *                                                                            *
 *  You should have received a copy of the GNU General Public License         *
 *  along with mitx_mathematics_programming_examples. If not, see             *
 *  <https://www.gnu.org/licenses/>.                                          *
 ******************************************************************************
 *  Purpose:                                                                  *
 *      Provides basic syntax for complex numbers using the C99 standard.     *
 ******************************************************************************
 *  Author: Ryan Maguire                                                      *
 *  Date:   2024/11/04                                                        *
 ******************************************************************************/

/*  Complex numbers and complex functions are found here.                     */
#include <complex.h>

/*  The printf function, used for printing messages, provided here.           */
#include <stdio.h>

int main(void)
{
    /*  C provides the variable "_Complex_I" which is the imaginary unit.     *
     *  Let us label this as "i" to make this program easier to read. The     *
     *  expression (complex double) that appears before the variable just     *
     *  means we want our variable to be complex and "double" precision,      *
     *  which is computer-talk for numbers that have decimal points (as       *
     *  opposed to integers which don't have decimal points).                 */
    complex double i = (complex double)_Complex_I;

    /*  We can now create the complex number z = 1 + i.                       */
    complex double z = 1 + i;

    /*  complex.h provides creal and cimag, which compute the real and        *
     *  imaginary parts of a complex number.                                  */
    double x = creal(z);
    double y = cimag(z);

    /*  We can also compute the modulus and the principal argument using the  *
     *  standard library. complex.h provides "cabs" and "carg".               */
    double r = cabs(z);
    double theta = carg(z);

    /*  Let's print our work to the screen. printf works as follows. It takes *
     *  a "formatted string", which is a bunch of characters that form the    *
     *  message we want to print. By formatted, we mean that every time we    *
     *  want to print a (double) variable, we write %f. After our message is  *
     *  done, we pass the variables to the function in the order they appear  *
     *  in the message. So:                                                   *
     *      "z = %f + %f i", x, y                                             *
     *  will print z = 1.0 + 1.0 i, instead of printing z = x + i y. That is, *
     *  by using this formatting we can print the value of the variable, and  *
     *  not the variable itself.                                              *
     *                                                                        *
     *  Note that our message ends with "\n" which means means "new-line."    *
     *  This is equivalent to hitting the enter key to move on to the next    *
     *  line in a text.                                                       */
    printf("z = %f + %f i\n", x, y);

    /*  We can print the polar form as well.                                  */
    printf("|z| = |%f + %f i| = %f\n", x, y, r);
    printf("Arg(z) = Arg(%f + %f i) = %f\n", x, y, theta);

    /*  We are done with the function. "return 0" tells the function to exit. */
    return 0;
}

C

/******************************************************************************
 *                                  LICENSE                                   *
 ******************************************************************************
 *  This file is part of mitx_mathematics_programming_examples.               *
 *                                                                            *
 *  mitx_mathematics_programming_examples is free software: you can           *
 *  redistribute it and/or modify it under the terms of the GNU General       *
 *  Public License as published by the Free Software Foundation, either       *
 *  version 3 of the License, or (at your option) any later version.          *
 *                                                                            *
 *  mitx_mathematics_programming_examples is distributed in the hope that     *
 *  it will be useful but WITHOUT ANY WARRANTY; without even the implied      *
 *  warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.          *
 *  See the GNU General Public License for more details.                      *
 *                                                                            *
 *  You should have received a copy of the GNU General Public License         *
 *  along with mitx_mathematics_programming_examples. If not, see             *
 *  <https://www.gnu.org/licenses/>.                                          *
 ******************************************************************************
 *  Purpose:                                                                  *
 *      Provides basic syntax for complex numbers using the C99 standard.     *
 ******************************************************************************
 *  Author: Ryan Maguire                                                      *
 *  Date:   2024/11/04                                                        *
 ******************************************************************************/

/*  Complex numbers and complex functions are found here.                     */
#include <complex.h>

/*  The printf function, used for printing messages, provided here.           */
#include <stdio.h>

int main(void)
{
    /*  C provides the variable "_Complex_I" which is the imaginary unit.     *
     *  Let us label this as "i" to make this program easier to read. The     *
     *  expression (complex double) that appears before the variable just     *
     *  means we want our variable to be complex and "double" precision,      *
     *  which is computer-talk for numbers that have decimal points (as       *
     *  opposed to integers which don't have decimal points).                 */
    complex double i = (complex double)_Complex_I;

    /*  We can now create the complex number z = 1 + i.                       */
    complex double z = 1 + i;

    /*  complex.h provides creal and cimag, which compute the real and        *
     *  imaginary parts of a complex number.                                  */
    double x = creal(z);
    double y = cimag(z);

    /*  We can also compute the modulus and the principal argument using the  *
     *  standard library. complex.h provides "cabs" and "carg".               */
    double r = cabs(z);
    double theta = carg(z);

    /*  Let's print our work to the screen. printf works as follows. It takes *
     *  a "formatted string", which is a bunch of characters that form the    *
     *  message we want to print. By formatted, we mean that every time we    *
     *  want to print a (double) variable, we write %f. After our message is  *
     *  done, we pass the variables to the function in the order they appear  *
     *  in the message. So:                                                   *
     *      "z = %f + %f i", x, y                                             *
     *  will print z = 1.0 + 1.0 i, instead of printing z = x + i y. That is, *
     *  by using this formatting we can print the value of the variable, and  *
     *  not the variable itself.                                              *
     *                                                                        *
     *  Note that our message ends with "\n" which means means "new-line."    *
     *  This is equivalent to hitting the enter key to move on to the next    *
     *  line in a text.                                                       */
    printf("z = %f + %f i\n", x, y);

    /*  We can print the polar form as well.                                  */
    printf("|z| = |%f + %f i| = %f\n", x, y, r);
    printf("Arg(z) = Arg(%f + %f i) = %f\n", x, y, theta);

    /*  We are done with the function. "return 0" tells the function to exit. */
    return 0;
}

Python

"""
################################################################################
#                                   LICENSE                                    #
################################################################################
#   This file is part of mitx_mathematics_programming_examples.                #
#                                                                              #
#   mitx_mathematics_programming_examples is free software: you can            #
#   redistribute it and/or modify it under the terms of the GNU General        #
#   Public License as published by the Free Software Foundation, either        #
#   version 3 of the License, or (at your option) any later version.           #
#                                                                              #
#   mitx_mathematics_programming_examples is distributed in the hope that      #
#   it will be useful but WITHOUT ANY WARRANTY; without even the implied       #
#   warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.           #
#   See the GNU General Public License for more details.                       #
#                                                                              #
#   You should have received a copy of the GNU General Public License          #
#   along with mitx_mathematics_programming_examples. If not, see              #
#   <https://www.gnu.org/licenses/>.                                           #
################################################################################
#   Purpose:                                                                   #
#       Implements complex numbers using a Python class.                       #
################################################################################
#   Author: Ryan Maguire                                                       #
#   Date:   2024/11/05                                                         #
################################################################################
"""

# The modulus and principal argument of a complex number are computed using
# the square root function and the arctan function. These are found here.
import math

class Complex:
    """
        Class:
            Complex
        Purpose:
            Python class for working with complex numbers.
            This contains methods (functions) for working with
            common routines found in complex analysis, and
            implements the basics of complex arithmetic.
        Attributes:
            real (float):
                The real part of the complex number.
            imag (float):
                The imaginary part of the complex number.
        Operators:
            + (complex addition)
            += (in-place complex addition)
            - (complex subtraction)
            -= (in-place complex subtraction)
            * (complex multiplication)
            *= (in-place complex multiplication)
            / (complex division)
            /= (in-place complex division)
            - (unary operator, complex negation)
    """
    def __init__(self, real, imag):
        """
            Function:
                __init__
            Purpose:
                Creates an instance of the Complex class.
            Arguments:
                real (float):
                    The real part of the complex number.
                imag (float):
                    The imaginary part of the complex number.
            Output:
                z (Complex):
                    The complex number real + i*imag
        """

        # Both inputs should be "floats", which are real numbers in a computer.
        try:
            self.real = float(real)
            self.imag = float(imag)

        # If we can't convert the inputs into floats, the user gave invalid
        # inputs. Treat this as an error.
        except (TypeError, ValueError) as err:
            raise TypeError(
                "A complex number is created from two real numbers."
            ) from err

    # + operator for complex addition.
    def __add__(self, other):
        """
            Operator:
                Addition (+).
            Purpose:
                Performs complex addition.
            Arguments:
                other (Complex):
                    The complex number being added to self.
            Outputs:
                sum (Complex):
                    The complex sum of self and other.
            Method:
                Add the complex numbers component-wise. If self and other
                are represented as z = a + ib and w = c + id,
                respectively, the sum is then:

                    z + w = (a + ib) + (c + id)
                          = (a + c) + i(b + d)

                That is, complex addition is done component-wise.
        """

        # We can only add complex numbers and real numbers together. Check if
        # the input is either of these.
        if not isinstance(other, Complex):

            # The input is not a complex number. See if it is a real number.
            try:
                other_real = float(other)
            except (TypeError, ValueError) as err:
                raise TypeError(
                    "Input is not real or complex. Cannot add."
                ) from err

            # If we get here, the input was a real number. We can add.
            real_sum = self.real + other_real
            imag_sum = self.imag
            return Complex(real_sum, imag_sum)

        # If we get here, the input is a complex number. Add the components.
        real_sum = self.real + other.real
        imag_sum = self.imag + other.imag
        return Complex(real_sum, imag_sum)

    # += operator for complex addition.
    def __iadd__(self, other):
        """
            Operator:
                Addition (+=).
            Purpose:
                Performs complex addition in-place.
            Arguments:
                other (Complex):
                    The complex number being added to self.
            Outputs:
                self
            Method:
                Perform complex addition component-wise and store
                the result in self.
        """

        # Similar error check to the one found in __add__.
        if not isinstance(other, Complex):

            # The input is not a complex number. See if it is a real number.
            try:
                self.real += float(other)
            except (TypeError, ValueError) as err:
                raise TypeError(
                    "Input is not real or complex. Cannot add."
                ) from err

        # If we get here, the input is a complex number. Add the components.
        else:
            self.real += other.real
            self.imag += other.imag

        return self

    # - operator for complex subtraction.
    def __sub__(self, other):
        """
            Operator:
                Subtraction (-).
            Purpose:
                Performs complex subtraction.
            Arguments:
                other (Complex):
                    The complex number being subtracted from self.
            Outputs:
                diff (Complex):
                    The complex difference of self and other.
            Method:
                Subtract the complex numbers component-wise. If self
                and other are represented as z = a + ib and w = c + id,
                respectively, the difference is then:

                    z - w = (a + ib) - (c + id)
                          = (a - c) + i(b - d)

                That is, complex subtraction is done component-wise.
        """

        # We can only subtract complex numbers and real numbers. Check if
        # the input is either of these.
        if not isinstance(other, Complex):

            # The input is not a complex number. See if it is a real number.
            try:
                other_real = float(other)
            except (TypeError, ValueError) as err:
                raise TypeError(
                    "Input is not real or complex. Cannot subtract."
                ) from err

            # If we get here, the input was a real number. We can subtract.
            real_diff = self.real - other_real
            imag_diff = self.imag
            return Complex(real_diff, imag_diff)

        # If we get here, the input is a complex number. Subtract components.
        real_diff = self.real - other.real
        imag_diff = self.imag - other.imag
        return Complex(real_diff, imag_diff)

    # -= operator for complex subtraction.
    def __isub__(self, other):
        """
            Operator:
                Subtraction (-=).
            Purpose:
                Performs complex subtraction in-place.
            Arguments:
                other (Complex):
                    The complex number being subtracted from self.
            Outputs:
                self
            Method:
                Perform complex subtraction component-wise and store
                the result in self.
        """

        # Subtraction can be done with real or complex numbers.
        if not isinstance(other, Complex):

            # The input is not a complex number. See if it is a real number.
            try:
                self.real -= float(other)
            except (TypeError, ValueError) as err:
                raise TypeError(
                    "Input is not real or complex. Cannot subtract."
                ) from err

        # If we get here, the input is a complex number. Subtract components.
        else:
            self.real -= other.real
            self.imag -= other.imag

        return self

    # * operator for complex multiplication.
    def __mul__(self, other):
        """
            Operator:
                Multiplication (*).
            Purpose:
                Performs complex multiplication.
            Arguments:
                other (Complex):
                    A complex number.
            Outputs:
                prod (Complex):
                    The product of self and other.
            Method:
                Complex multiplication is computed using i^2 = -1.
                That is, if z = a + ib, and w = c + id, then:

                    z*w = (a + ib) * (c + id)
                        = ac + ibc + iad + i^2bd
                        = (ac - bd) + i(ad + bc)

                If other is purely real, this is just scalar multiplication.
        """

        # The input must be a real or complex number.
        if not isinstance(other, Complex):
            try:
                other_real = float(other)
            except (TypeError, ValueError) as err:
                raise TypeError(
                    "Input is not real or complex. Cannot multiply."
                ) from err

            # If we get here the input was a real number. Multiply components.
            prod_real = self.real * other_real
            prod_imag = self.imag * other_real
            return Complex(prod_real, prod_imag)

        # If we get here, the input was complex.
        prod_real = self.real * other.real - self.imag * other.imag
        prod_imag = self.real * other.imag + self.imag * other.real
        return Complex(prod_real, prod_imag)

    # * operator for complex multiplication on the right.
    def __rmul__(self, other):
        """
            Operator:
                Multiplication (*).
            Purpose:
                Performs complex multiplication (on the right).
            Arguments:
                other (Complex):
                    A complex number.
            Outputs:
                prod (Complex):
                    The product of self and other.
            Method:
                Complex multiplication is commutative, so this operator
                does the same thing as multiplication on the left.
        """

        # The input must be a real or complex number.
        if not isinstance(other, Complex):
            try:
                other_real = float(other)
            except (TypeError, ValueError) as err:
                raise TypeError(
                    "Input is not real or complex. Cannot multiply."
                ) from err

            # If we get here the input was a real number. Multiply components.
            prod_real = other_real * self.real
            prod_imag = other_real * self.imag
            return Complex(prod_real, prod_imag)

        # If we get here, the input was complex.
        prod_real = other.real * self.real - other.imag * self.imag
        prod_imag = other.real * self.imag + other.imag * self.real
        return Complex(prod_real, prod_imag)

    # *= operator for complex multiplication.
    def __imul__(self, other):
        """
            Operator:
                Multiplication (*=).
            Purpose:
                Performs complex multiplication in-place.
            Arguments:
                other (Complex):
                    A complex number.
            Outputs:
                self.
            Method:
                Performing complex multiplication naively in-place will
                overwrite the real part first, meaning the imaginary part
                will get the wrong input. We correct this by saving the
                real part as a new variable, and performing complex
                multiplication with this.
        """

        # The input must be a real or complex number.
        if not isinstance(other, Complex):
            try:
                other_real = float(other)
            except (TypeError, ValueError) as err:
                raise TypeError(
                    "Input is not real or complex. Cannot multiply."
                ) from err

            # If we get here the input was a real number. Multiply components.
            self.real *= other_real
            self.imag *= other_real
            return self

        # If we get here, the input was complex. Avoid overwriting data,
        # save the real part of self before computing.
        self_real = self.real

        # We can now perform the product.
        self.real = self.real * other.real - self.imag * other.imag
        self.imag = self_real * other.imag + self.imag * other.real
        return self

    # Negation operator.
    def __neg__(self):
        """
            Operator:
                Negation (-):
            Purpose:
                Negates a complex number.
            Arguments:
                None.
            Outputs:
                neg_self (Complex):
                    The negation of self.
            Method:
                Negate each component and return.
        """

        # Negate all components and return.
        real_val = -self.real
        imag_val = -self.imag
        return Complex(real_val, imag_val)

    # Complex conjugation.
    def conj(self):
        """
            Function:
                conj
            Purpose:
                Computes the complex conjugate of a complex number.
            Arguments:
                None.
            Output:
                conj_self (Complex):
                    The complex conjugate of self.
            Method:
                Negate the imaginary part and return.
        """
        return Complex(self.real, -self.imag)

    # Computes the complex modulus, or magnitude, of a complex number.
    def modulus(self):
        """
            Function:
                modulus
            Purpose:
                Computes the complex modulus of a complex number.
            Arguments:
                None.
            Output:
                abs_z (Complex):
                    The magnitude of self.
            Method:
                Use the Pythagorean formula.
        """

        # Pythagorean formula: Square root of the sum of the squares.
        real_squared = self.real * self.real
        imag_squared = self.imag * self.imag
        return math.sqrt(real_squared + imag_squared)

    # Computes the principal argument of a complex number.
    def arg(self):
        """
            Function:
                arg
            Purpose:
                Computes the principal argument of a complex number.
            Arguments:
                None.
            Output:
                arg_z (Complex):
                    The principal argument of self.
            Method:
                Use the atan2 function from the math module.
        """
        return math.atan2(self.imag, self.real)

    # Used for copying an instance of a complex number into a new variable.
    def copy(self):
        """
            Function:
                copy
            Purpose:
                Copies the data of self to a new variable.
            Arguments:
                None.
            Output:
                self_copy (Complex):
                    A copy of self.
        """
        return Complex(self.real, self.imag)

    # Converts a complex number into a string.
    def __str__(self):
        """
            Function:
                __str__
            Purpose:
                Converts a complex number into a string. Used for printing.
            Arguments:
                None.
            Output:
                self_string (str):
                    String representation of self.
        """

        # If the imaginary part is negative, write real - |imag| i, instead
        # of writing real + -imag i.
        if self.imag < 0.0:
            return f'{self.real} - {abs(self.imag)} i'

        return f'{self.real} + {self.imag} i'

# Let's test our routines.
if __name__ == "__main__":

    # Two test complex numbers to play with.
    z = Complex(1.0, 2.0)
    w = Complex(1.0, -1.0)

    # Test out negation.
    neg_z = -z

    # Test out complex arithmetic.
    summ = z + w
    diff = z - w
    prod = z * w

    # And try scalar multiplication.
    right_scale = z * 4.0
    left_scale = 4.0 * z

    # Test basic complex functions.
    magnitude = z.modulus()
    argument = z.arg()

    # Lastly, try out in-place arithmetic.
    new_sum = z.copy()
    new_diff = z.copy()
    new_prod = z.copy()

    new_sum += w
    new_diff -= w
    new_prod *= w

    # Print all of the results to the screen.
    print(f'z = {z}')
    print(f'w = {w}')
    print(f'-z = {neg_z}')
    print(f'z + w = {summ}')
    print(f'z - w = {diff}')
    print(f'z * w = {prod}')
    print(f'4 * z = {left_scale}')
    print(f'z * 4 = {right_scale}')
    print(f'|z| = {magnitude}')
    print(f'Arg(z) = {argument}')
    print(f'z += w -> {new_sum}')
    print(f'z -= w -> {new_diff}')
    print(f'z *= w -> {new_prod}')