:tocdepth: 2 .. _complex: ************************************** Complex type, I ************************************** .. contents:: :local: .. highlight:: python Up to this point, we have mostly ignored the more mathematical type for **complex** numbers. We have waited until now, because they are simpler to use when we have a grasp of :ref:`applying methods `. Complex type =========================================== In mathematics, the set of complex numbers :math:`\mathbb{C}` includes real :math:`\mathbb{R}` and imaginary :math:`\mathbb{I}~(= j\mathbb{R})` numbers, as well as their sum, where the "imaginary number" is defined as :math:`j^2\equiv-1`. (There is a divide in the mathematical sciences -- many people use :math:`i\equiv\sqrt{-1}` to denote the imaginary number, but we use :math:`j` here because that is what Python syntax uses. Congratulations, engineers!) In general, a complex number number can be written as the sum of a real component :math:`a` and an imaginary component :math:`b` as :math:`c = a + bj`, where :math:`a, b \in \mathbb{R}` (and we stress that the "imaginary component" :math:`b` is itself a real number). Each component can be found using the following functions: :math:`a\equiv\mathbf{Re}(c)` and :math:`b\equiv\mathbf{Im}(c)`. So, a real number is just a special case of a complex number with :math:`b=0`, and an imaginary number is one where :math:`a=0`. We can picture such numbers on a 2D complex plane (or *Argand diagram*). The horiontal axis is real :math:`\mathbb{R}` and the vertical axis is imaginary :math:`i\mathbb{R}`. Thus, a complex point :math:`c` is given by the coordinate pair :math:`(a, b)`, such that the real component provides the "x" coordinate and the imaginary component, the "y" coordinate. We can translate these to Python using the type which is evocatively named **complex**. Note that not every programming language has such a type; in others, you have consider the real and imaginary components as forming a short array (mimicking a vector with 2 components, say). This will be a useful picture to keep in mind at times, but we can also avail ourselves of Python's type/class and related functionality. In Python, we denote a complex type by including an imaginary-looking term, which is a number followed by *immediately* by the letter ``j``, with no space in between. Thus, the following are all recognized as complex by Python (you can use ``type()`` on each to verify it):: c1 = -2 + 3j # nonzero real and imag components c2 = 3j - 2 # order of components does not matter c3 = 34.1 - 1j # more nonzero real and imag components c4 = 5 + 0j # a complex with zero imag component c5 = 0 + 9j # a complex with zero real component c6 = 9j # same as above: complex with zero real component c7 = complex(4) # arg by position; imag=0 by default c8 = complex(5,6) # args by position, both real and imag set c9 = complex() # no args; real=0 and imag=0 by default .. hidden-code-block:: python :linenos: :label: + show/hide prints print(c1) print(c2) print(c3) print(c4) print(c5) print(c6) print(c7) print(c8) print(c9) \.\.\. which would print, respectively, to (comments added): .. code-block:: python (-2+3j) # c1 (-2+3j) # c2 (34.1-1j) # c3 (5+0j) # c4 9j # c5 9j # c6 (4+0j) # c7 (5+6j) # c8 0j # c9 Some comments: * For Python to recognize "j" as the imaginary number, it must **always** be preprended by a number. If we write ``1+j``, Python will treat ``j`` as a variable that must have been defined above; we would have to write ``1+1j`` to denote a complex number (assuming we hadn't defined a complex-valued variable ``j`` previously\.\.\.). * Note that ``c5`` and ``c6`` are both of type complex, as there is no separate "imaginary" type in Python; by including a ``j`` on one number, the result will be a complex, whether or not there is real term added in. * When there *is* a nonzero real component, the result is written in parentheses. This allows us to have expected behavior when multiplying by a constant: mathematically, :math:`3c = 3a + 3bj`. The following evaluate very differently: ``(34.1 - 1j)*5`` vs ``34.1 - 1j*5``. * To have a complex type, there *must* be an imaginary component, even if it is zero. ``c4`` is complex, while ``c4b = 5`` would be int. * In defining ``c7``, ``c8``, and ``c9``, we used the built-in ``complex()`` function with different numbers of args. We can read the help docstring via ``complex?`` to understand the default input values depending on how any args we enter: .. code-block:: none :linenos: Init signature: complex(real=0, imag=0) Docstring: Create a complex number from a real part and an optional imaginary part. This is equivalent to (real + imag*1j) where imag defaults to 0. Type: type Subclasses: complex128 Using basic mathematical operators with complex numbers just follows the mathematical rules. For example, ``c2 + c3`` evaluates to ``(32.1+2j)``, which is the expected sum of real and imaginary components. The magnitude :math:`|c|` and phase :math:`\theta` of a complex number are important concepts, forming a complementary pair of real values to describe it (the *polar* form, as opposed to the *rectangular* form above): :math:`c=|c|\,e^{i\theta}`. These can also be viewed on the complex plane, where :math:`|c|` is essentially the hypotenuse of the triangle formed by the sides :math:`a` and :math:`b` from the origin, and :math:`\theta` is the angle of this hypotenuse line the positive real (:math:`\mathbb{R}^+`) part of the horizontal axis. One can relate the rectangular and polar coordinate pairs mathematically: .. math:: a &= |c|\, \cos(\theta) \\ b &= |c|\, \sin(\theta) \\ |c| &= \sqrt{a^2 + b^2} \\[0.1cm] \theta &= \tan^{-1}\left(\dfrac{b}{a}\right) \.\.\. and we could translate these definitions to computational forms. Or, since they are pretty fundamental quantities, we could see if things exist in Python to generate the values from the complex directly. And there are, noting that "magnitude" is the same as the absolute value and "phase" is an angle:: print(np.real(c1)) print(np.imag(c1)) print(np.abs(c1)) print(np.angle(c1)) \.\.\. which output:: -2.0 3.0 3.605551275463989 2.158798930342464 And of course, we shouldn't just guess that the outputs are correct and/or what we want-- we should verify them. The real and imaginary parts are straightforward to verify by inspection, and we translate the mathematical expressions above to verify the polar values. In particular, we will also want to know if the angle value is in radians or degrees; looking at part of the help string for it (``np.angle?``): .. hidden-code-block:: none :linenos: :label: + show/hide help Signature: np.angle(z, deg=False) Docstring: Return the angle of the complex argument. Parameters ---------- z : array_like A complex number or sequence of complex numbers. deg : bool, optional Return angle in degrees if True, radians if False (default). Returns ------- angle : ndarray or scalar The counterclockwise angle from the positive real axis on the complex plane in the range ``(-pi, pi]``, with dtype as numpy.float64. ... \.\.\. we see that: 1) the output value is in radians by default, and 2) we can control this with a kwarg. If we want to initialize an array of complex numbers, we can use all we have learned so far about :ref:`arrays `, and just extend it to having a complex datatype:: arr_comp = np.zeros(5, dtype=complex) print(arr_comp) \.\.\. produces:: [0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j] One can have arrays and sequence of complex numbers; consider for example the following mathematical sequence (recognizing :math:`j` as the imaginary number here, not an index, based on the syntax!): .. math:: C_k = (2k+1) j^k,~~ k \in [0,10)\,. .. container:: qpractice **Q:** Use a for-loop to create the above array of values in Python. .. hidden-code-block:: python :linenos: :label: + show/hide code N = 10 C = np.zeros(N, dtype=complex) # initialize array with proper type+len for k in range(N): C[k] = (2*k + 1) * (1j)**k # note the "1j" here! print(C) **Q:** Make an array ``c_real`` of :math:`\mathbf{Re}(C_k)` and ``c_imag`` of :math:`\mathbf{Im}(C_k)` for all :math:`k`, and plot these. You have just made an Argand diagram, plotting on the complex plane. .. hidden-code-block:: python :linenos: :label: + show/hide code # ... continuing on from above ... import matplotlib.pyplot as plt # need plotting module # make new arrays c_real = np.zeros(N, dtype=float) # note the type c_imag = np.zeros(N, dtype=float) # note the type # loop over all elements of C, and store part of each [k]th element for k in range(N): c_real[k] = np.real(C[k]) c_imag[k] = np.imag(C[k]) print(C) # start to plot; add some axis lines and labels (why not?) plt.figure("Argand") plt.axhline(c = '0.75', lw = 0.5) plt.axvline(c = '0.75', lw = 0.5) plt.plot(c_real, c_imag, lw = 2, marker = 'o', ms = 4) plt.axis('scaled') plt.xlabel('real axis') plt.ylabel('imaginary axis') plt.title("Argand diagram") plt.ion() plt.show() Complex type methods =========================================== With our new found appreciation for methods that go with each type/class, let's look at some for the complex type with ``help(complex)``: .. code-block:: none :linenos: Help on class complex in module builtins: class complex(object) | complex(real=0, imag=0) | | Create a complex number from a real part and an optional imaginary part. | | This is equivalent to (real + imag*1j) where imag defaults to 0. | | Methods defined here: | | __abs__(self, /) | abs(self) | | __add__(self, value, /) | Return self+value. | | __bool__(self, /) | self != 0 ... | conjugate(...) | complex.conjugate() -> complex | | Return the complex conjugate of its argument. (3-4j).conjugate() == 3+4j. | ... | Data descriptors defined here: | | imag | the imaginary part of a complex number | | real | the real part of a complex number Some of these look familiar from our early adventures into methods, such as ``__abs__()`` and ``__add()``. Though, as we noted before, these methods are specific to *this* class/type, so they just happen to have the same name. The absolute value (AKA magnitude) is pretty interesting. For a complex number, we have a mathematical definition in terms of real and imaginary components as :math:`|c|\equiv\sqrt{a^2+b^2}`. If we try this method out using one of the above variables:: print(c1.__abs__()) \.\.\. we see that the result ``3.605551275463989`` is indeed what we would expect from the component values, running the check by evaluating ``((-2)**2 + 3**2)**0.5``. Looking further down the methods list, we also see ``conjugate``. This shares the name of the important mathematical operation for complex numbers, where the complex conjugate of :math:`c` is defined as :math:`c^* \equiv a - bj`. Testing out this complex class method, we see that:: print(c1.conjugate()) \.\.\. evaluates according to our expectation: ``(-2-3j)``. Phew. Finally, we don't see a list of attributes in the docstring, but we do see a list of **data descriptors** that looks kind of similar (i.e., like variables inside the type/class). Descriptors are different than attributes, and that differentiation is deeper, internal Python topic than we will cover here. But from a practical "usage" point of view, we can typically think of them as synonymous to attributes and use them similarly when we see them in docstring descriptions. So, let's do so here:: print(c1.real) print(c1.imag) \.\.\. produces:: -2.0 3.0 That looks correct, based on the value assigned to the complex. (It also matches with the functional values above from ``np.real()`` and ``np.imag()``. Practice ============= #. Evaluate ``(17 - 4j).real + 1``. #. Evaluate the square of the complex number ``5+3j``. #. What type are each of the real and imag components of a complex in Python? Is it always the same, e.g., whether one has ``1+2j``, ``1.0+2j`` or ``1+2.0j``? What does this imply about testing equality with either a full complex or any of its components?