Большинство языков программирования общего назначения либо не поддерживают, либо ограниченно поддерживают комплексные числа. Типичными вариантами являются изучение какого-нибудь специализированного инструмента, такого как MATLAB, или поиск сторонней библиотеки. Python — редкое исключение, потому что он имеет встроенные комплексные числа.

Несмотря на пугающее название, комплексные числа не сложны! Они удобны для решения практических задач, с которыми вы познакомитесь в этом руководстве. Вы изучите векторную графику и анализ частот звука, но комплексные числа также могут помочь, например, в рисовании фракталов.

В этом уроке вы узнаете:

  • Определение комплексных чисел с помощью литералов в Python.
  • Представление комплексных чисел в прямоугольных и полярных координатах.
  • Как используются комплексные числа в арифметических выражениях.
  • Какие преимущества у встроенного модуля cmath.
  • Как записывать математические формулы прямо в код Python.

План

Если вам нужно быстро освежить свои знания или сделать легкое введение в теорию комплексных чисел, вы можете посмотреть:

Создание комплексных чисел в Python   

Создание комплексных чисел и управление ими в Python не сильно отличается от других встроенных типов данных, особенно числовых. Это возможно, потому что язык рассматривает их как первоклассных граждан. Это означает, что вы можете выражать математические формулы, содержащие комплексные числа, с небольшими накладными расходами. Python позволяет использовать комплексные числа в арифметических выражениях и вызывать для них функции так же, как и для других чисел в Python. Это приводит к элегантному синтаксису, который читается почти как учебник математики.

Литерал комплексного числа   

Самый быстрый способ определить комплексное число в Python — ввести его литерал непосредственно в исходный код:

>>> z = 3 + 2j

Хотя это выглядит как алгебраическая формула, выражение справа от знака равенства уже является фиксированным значением, которое не нуждается в дальнейшей оценке. Проверив его тип, вы убедитесь, что это действительно комплексное число:

>>> type(z)

Чем это отличается от сложения двух чисел с помощью оператора плюс? Наглядным признаком является приклеенная ко второму числу буква j, что полностью меняет смысл выражения. Если вы удалите букву, вместо этого вы получите знакомый целочисленный результат:

>>> z = 3 + 2

>>> type(z)

Кстати, вы можете использовать числа с плавающей запятой и для создания комплексных чисел:

>>> z = 3.14 + 2.71j
>>> type(z)

Литералы комплексных чисел в Python имитируют математическую нотацию, которая также известна как стандартная форма, алгебраическая форма или иногда каноническая форма комплексного числа. В Python вы можете использовать в этих литералах либо нижний регистр j, либо верхний регистр J.

В традиционной записи комплексных чисел вместо j используется буква i, поскольку она обозначает мнимую единицу. Если у вас есть математическое образование, вы можете почувствовать легкий дискомфорт, используя условные обозначения Python. Однако есть несколько причин, которые могут оправдать неоднозначный выбор Python:

  • Это соглашение, уже принятое инженерами, чтобы избежать столкновения названий с электрическим током, который обозначается буквой i.
  • В вычислениях буква i часто используется для обозначения переменной индексации в циклах.
  • Букву i можно легко спутать с l или 1 в исходном коде.

Это было поднято на трекере ошибок Python более десяти лет назад, а создатель Python, сам Гвидо ван Россум, закрыл проблему следующим комментарием:

This will not be fixed. For one thing, the letter ‘i’ or upper case ‘I’ look too much like digits. The way numbers are parsed either by the language parser (in source code) or by the built-in functions (int, float, complex) should not be localizable or configurable in any way; that’s asking for huge disappointments down the road. If you want to parse complex numbers using ‘i’ instead of ‘j’, you have plenty of solutions available already. (Source)

Это не будет исправлено. Во-первых, буква «i» или заглавная буква «I» слишком похожи на цифры. Способ анализа чисел либо языковым синтаксическим анализатором (в исходном коде), либо встроенными функциями (int, float, complex) не должен быть локализуемым или настраиваемым каким-либо образом; это приводит к огромным разочарованиям в будущем. Если вы хотите анализировать комплексные числа, используя «i» вместо «j», у вас уже есть множество доступных решений.

Вот и все. Если вы не хотите начать использовать MATLAB, вам придется жить с использованием j для обозначения ваших комплексных чисел.

Алгебраическая форма комплексного числа соответствует стандартным правилам алгебры, что удобно при выполнении арифметики. Например, сложение имеет коммутативное свойство, которое позволяет менять порядок двух частей литерала комплексного числа без изменения его значения:

>>> 3 + 2j == 2j + 3
True

Точно так же вы можете заменить вычитание сложением в литерале комплексного числа, потому что знак минус — это просто сокращенное обозначение эквивалентной формы:

>>> 3 - 2j == 3 + (-2j)
True

Всегда ли литерал комплексного числа в Python должен состоять из двух чисел? Может быть больше? Они заказаны? Чтобы ответить на эти вопросы, давайте проведем несколько экспериментов. Неудивительно, что если вы укажете только одно число без буквы j, то вы получите обычное целое число или число с плавающей запятой:

>>> z = 3.14
>>> type(z)

С другой стороны, добавление буквы j к числовому литералу немедленно превратит его в комплексное число:

>>> z = 3.14j
>>> type(z)

Строго говоря, с математической точки зрения, вы только что создали чисто мнимое число, но Python не может представить его как отдельный тип данных. Следовательно, без другой части это просто комплексное число.

А как насчет противоположного? Чтобы создать комплексное число без мнимой части, вы можете воспользоваться преимуществом нуля и добавить или вычесть его следующим образом:

>>> z = 3.14 + 0j
>>> type(z)

На самом деле обе части комплексного числа присутствуют всегда. Если вы не видите единицы, это означает, что она имеет нулевое значение. Давайте посмотрим, что произойдет, если вы попытаетесь ввести в сумму больше терминов, чем раньше:

>>> 2 + 3j + 4 + 5j
(6+8j)

На этот раз ваше выражение больше не является буквальным, потому что Python вычислил его в комплексное число, состоящее всего из двух частей. Помните, что основные правила алгебры переносятся на комплексные числа, поэтому, если вы сгруппируете похожие термины и примените покомпонентное сложение, вы получите 6 + 8j.

Обратите внимание, как Python по умолчанию отображает комплексные числа. Их текстовое представление содержит пару круглых скобок, строчную букву j и без пробелов. Кроме того, на втором месте стоит мнимая часть.

Комплексные числа, которые также являются чисто мнимыми числами, отображаются без скобок и показывают только их мнимую часть:

>>> 3 + 0j
(3+0j)
>>> 0 + 3j
3j

Фабрика комплексных чисел функция complex()   

В Python есть встроенная функция complex(), которую можно использовать в качестве альтернативы литералу комплексного числа:

>>> z = complex(3, 2)

В этом виде он напоминает кортеж или упорядоченную пару обычных чисел. Аналогия не так уж надумана. Комплексные числа имеют геометрическую интерпретацию в декартовой системе координат, которую вы немного изучите. Вы можете думать о комплексных числах как о двумерных.

Интересный факт: в математике комплексные числа традиционно обозначаются буквой z, поскольку это следующая буква в алфавите после x и y, которые обычно обозначают координаты.

Фабрика комплексных чисел принимает два числовых параметра. Первая представляет собой действительную часть, а вторая представляет собой мнимую часть, обозначенную буквой j в литерале, который вы видели ранее:

>>> complex(3, 2) == 3 + 2j
True

Оба параметра являются необязательными, со значениями по умолчанию, равными нулю, что упрощает определение комплексных чисел без мнимой части или как действительной, так и мнимой частей:

>>> complex(3) == 3 + 0j
True
>>> complex() == 0 + 0j
True

Версия с одним аргументом может быть полезна при приведении типов. Например, вы можете передать нечисловое значение, такое как строковый литерал, для получения соответствующего сложного объекта. Обратите внимание, что строка не может содержать пробелов:

>>> complex("3+2j")
(3+2j)

>>> complex("3 + 2j")
Traceback (most recent call last):
  File "", line 1, in 
ValueError: complex() arg is a malformed string

Позже вы узнаете, как сделать ваши классы совместимыми с этим механизмом приведения типов. Интересно, что когда вы передаете комплексное число в complex(), то получите тот же экземпляр:

>>> z = complex(3, 2)
>>> z is complex(z)
True

Это согласуется с тем, как работают другие типы чисел в Python, потому что все они неизменяемы. Чтобы создать отдельную копию комплексного числа, вы должны снова вызвать функцию с обоими аргументами или объявить другую переменную с литералом комплексного числа:

>>> z = complex(3, 2)
>>> z is complex(3, 2)
False

Когда функция имеет два аргумента, то они всегда должны быть числами, такими как int, float или complex. В противном случае вы получите ошибку во время выполнения. Технически говоря, bool является подклассом int, поэтому он тоже будет работать:

>>> complex(False, True)  # Логические значения, такие же, как и complex (0, 1)
1j

>>> complex(3, 2)  # Целые числа
(3+2j)

>>> complex(3.14, 2.71)  # Числа с плавающей запятой
(3.14+2.71j)

>>> complex("3", "2")  # Строки
Traceback (most recent call last):
  File "", line 1, in 
TypeError: complex() can't take second arg if first is a string

Все становится еще более причудливым, когда вы предоставляете фабричную функцию complex() с комплексными числами в качестве аргументов. Однако, если вы предоставите только первый аргумент, он будет вести себя как прокси, как и раньше:

>>> complex(complex(3, 2))
(3+2j)

Однако, когда присутствуют два аргумента и хотя бы один из них является комплексным, то получите результаты, которые с первого взгляда трудно объяснить:

>>> complex(1, complex(3, 2))
(-1+3j)

>>> complex(complex(3, 2), 1)
(3+3j)

>>> complex(complex(3, 2), complex(3, 2))
(1+5j)

Чтобы получить ответы, давайте взглянем на строку документации фабричной функции или онлайн-документацию, в которой объясняется, что происходит под капотом, когда вы вызываете complex(real, imag):

Return a complex number with the value real + imag*1j or convert a string or number to a complex number. (Source)

Верните комплексное число со значением real + imag * 1j или преобразуйте строку или число в комплексное число. (Источник)

В этом объяснении real и imag — это имена аргументов функции. Второй аргумент умножается на мнимую единицу j, а результат добавляется к первому аргументу. Не волнуйтесь, если это все еще не имеет смысла. Вы можете вернуться к этой части, когда прочитали об арифметике комплексных чисел. Правила, о которых вы узнаете, упростят это.

Когда вы хотите использовать фабричную функцию complex() вместо литерала? Это зависит от обстоятельств, но вызов функции может быть более удобным, например, при работе с динамически генерируемыми данными.

Знакомство с комплексными числами Python   

В математике комплексные числа — это надмножество действительных чисел, что означает, что каждое действительное число также является комплексным числом, мнимая часть которого равна нулю. Python моделирует эти отношения с помощью концепции, называемой числовой башней, описанной в PEP 3141:

>>> import numbers
>>> issubclass(numbers.Real, numbers.Complex)
True

Встроенный модуль numbers определяет иерархию числовых типов через абстрактные классы, которые можно использовать для проверки типов и классификации чисел. Например, чтобы определить, принадлежит ли значение определенному набору чисел, вы можете вызвать для него isinstance():

>>> isinstance(3.14, numbers.Complex)
True
>>> isinstance(3.14, numbers.Integral)
False

Значение с плавающей запятой 3.14 — это действительное число, которое также является комплексным числом, но не целым. Обратите внимание, что вы не можете использовать встроенные типы непосредственно в таком тесте:

>>> isinstance(3.14, complex)
False

Разница между комплексными и числовыми. Сложными состоит в том, что они принадлежат отдельным ветвям в дереве иерархии числовых типов, а последний является абстрактным базовым классом без какой-либо реализации:

Абстрактные базовые классы, которые обозначены красным на диаграмме выше, могут обходить обычный механизм проверки наследования, регистрируя несвязанные классы в качестве своих виртуальных подклассов. Вот почему значение с плавающей запятой в этом примере кажется экземпляром чисел. Сложным, но не сложным.

Доступ к реальным и мнимым частям   

Чтобы получить действительную и мнимую части комплексного числа в Python, вы можете использовать соответствующие атрибуты .real и .imag:

>>> z = 3 + 2j
>>> z.real
3.0
>>> z.imag
2.0

Оба свойства доступны только для чтения, потому что комплексные числа неизменяемы, поэтому попытка присвоить новое значение любому из них не удастся:

>>> z.real = 3.14
Traceback (most recent call last):
  File "", line 1, in 
AttributeError: readonly attribute

Поскольку каждое число в Python является более конкретным типом комплексного числа, атрибуты и методы, определенные в числах, также доступны во всех числовых типах, включая int и float:

>>> x = 42
>>> x.real
42
>>> x.imag
0

Мнимая часть таких чисел всегда равна нулю.

Вычисление конъюгата комплексного числа   

Комплексные числа Python имеют только три открытых члена. Помимо свойств .real и .imag, они предоставляют метод .conjugate(), который меняет знак мнимой части:

>>> z = 3 + 2j
>>> z.conjugate()
(3-2j)

Для чисел, мнимая часть которых равна нулю, это не повлияет:

>>> x = 3.14
>>> x.conjugate()
3.14

Эта операция сама по себе является обратимой, поэтому ее повторный вызов даст вам исходный номер, с которого вы начали:

>>> z.conjugate().conjugate() == z
True

Хотя это может показаться малопригодным, комплексное сопряжение имеет несколько полезных арифметических свойств, которые, помимо прочего, могут помочь вычислить деление двух комплексных чисел ручкой и бумагой.

Арифметика комплексных чисел   

Поскольку комплексный тип данных в Python, вы можете вставлять комплексные числа в арифметические выражения и вызывать для них многие встроенные функции. Более сложные функции для комплексных чисел определены в модуле cmath, который является частью стандартной библиотеки. Вы познакомитесь с ним в более поздней части этого руководства.

Пока что, запоминание единственного правила позволит вам применить свои знания арифметики в начальной школе для вычисления основных операций с комплексными числами. Правило, которое следует запомнить, — это определение мнимой единицы, которая удовлетворяет следующему уравнению:

j^2 = -1

Когда вы думаете о j как о действительном числе, это выглядит неправильно, но не паникуйте. Если вы на мгновение проигнорируете его и замените каждое вхождение j^2 на -1, как если бы оно было константой, то все будет установлено. Посмотрим, как это работает.

Сложение   

Сумма двух или более комплексных чисел эквивалентна покомпонентному сложению их действительной и мнимой частей:

>>> z1 = 2 + 3j
>>> z2 = 4 + 5j
>>> z1 + z2
(6+8j)

Ранее вы узнали, что алгебраические выражения, состоящие из действительных и мнимых чисел, подчиняются стандартным правилам алгебры. Когда вы запишете это алгебраически, вы сможете применить свойство распределения и упростить формулу, выделив и сгруппировав общие термины:

z_1 + z_2 = (x_1 + y_1j) (x_2 + y_2j)\\ = x_1 + x_2 + y_1j + y_2j\\ = (x_1 + x_2) + (y_1 + y_2)j

При сложении Python автоматически приводит все операнды к комплексному типу:

>>> z = 2 + 3j
>>> z + 7  # Добавить комплекс к целому числу
(9+3j)

Это похоже на неявное преобразование int во float, с которым вы, возможно, более знакомы.

Вычитание   

Вычитание комплексных чисел аналогично их сложению, что означает, что вы также можете применять его поэлементно:

>>> z1 = 2 + 3j
>>> z2 = 4 + 5j
>>> z1 - z2
(-2-2j)

Однако, в отличие от суммы, порядок операндов имеет значение и дает разные результаты, как и с действительными числами:

>>> z1 + z2 == z2 + z1
True
>>> z1 - z2 == z2 - z1
False

Вы также можете использовать унарный оператор минус (-), чтобы сделать отрицательное комплексное число:

>>> z = 3 + 2j
>>> -z
(-3-2j)

Происходит инвертирование действительной и мнимой частей комплексного числа.

Умножение   

Интереснее становится произведение двух или более комплексных чисел:

>>> z1 = 2 + 3j
>>> z2 = 4 + 5j
>>> z1 * z2
(-7+22j)

Как, черт возьми, получилось отрицательное число из только положительных? Чтобы ответить на этот вопрос, надо вспомнить определение мнимой единицы и переписать выражение в терминах действительной и мнимой частей:

z_1 \times z_2 = (x_1 + y_1j) \times (x_2 + y_2j)\\ = (x_1 + y_1j) \times x_2 + (x_1 + y_1j) \times y_2j\\ = x_1x_2 + y_1jx_2 + x_1y_2j + y_1jy_2j\\ = x_1x_2 + (y_1x_2 + x_1y_2)j + y_1y_2j^2\\ = x_1x_2 + (y_1x_2 + x_1y_2)j - y_1y_2\\ = x_1x_2 - y_1y_2 + (x_1y_2 + x_2y_1)j

Ключевое наблюдение: j умножить на j дает j^2, которое можно заменить на -1. Это меняет знак одного из слагаемых, в то время как остальные правила остаются такими же, как и раньше.

Деление   

На первый взгляд деление комплексных чисел может показаться устрашающим:

>>> z1 = 2 + 3j
>>> z2 = 4 + 5j
>>> z1 / z2
(0.5609756097560976+0.0487804878048781j)

Вы не поверите, но вы можете получить тот же результат, используя всего лишь ручку и бумагу! (Хорошо, калькулятор может избавить вас от головной боли.) Когда оба числа выражены в их стандартных формах, хитрость состоит в том, чтобы умножить числитель и знаменатель на комплексно сопряженное последнего:

\dfrac{z_1}{z_2} = \dfrac{z_1\overline{z_2}}{z_2\overline{z_2}} = \dfrac{z_1\overline{z_2}}{|z_2|^2}

Знаменатель становится квадратом модуля делителя. Позже вы узнаете больше о модуле комплексных чисел. Когда вы продолжите вывод формулы, вы получите следующее:

\dfrac{z_1}{z_2} = \dfrac{z_1\overline{z_2}}{z_2\overline{z_2}}\\ = \dfrac{(x_1 + y_1j)(x_2-y_2j)}{(x_2 + y_2j)(x_2-y_2j)}\\ = \dfrac{x_1x_2 - x_1y_2j + y_1jx_2 -y_1y_2j^2}{x_2^2 - y_2^2j^2}\\ = \dfrac{(x_1x_2 + y_1y_2j) + (y_1x_2 - x_1y_2)j}{x_2^2 + y_2^2}\\ = \dfrac{x_1x_2 + y_1y_2j}{x_2^2 + y_2^2} + \dfrac{(y_1x_2 - x_1y_2)}{x_2^2 + y_2^2}j

Обратите внимание, что комплексные числа не поддерживают деление с округлением до ближайшего целого числа, также известное как целочисленное деление:

>>> z1 // z2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't take floor of complex number.

>>> z1 // 3.14
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't take floor of complex number.

Раньше, в Python 2.x это работало, но позже было удалено, чтобы избежать двусмысленности.

Возведение в степень   

Вы можете возвести комплексные числа в степень, используя оператор двоичного возведения в степень (**) или встроенный pow(), но не тот, который определен в математическом модуле, который поддерживает только значения с плавающей запятой:

>>> z = 3 + 2j

>>> z**2
(5+12j)

>>> pow(z, 2)
(5+12j)

>>> import math
>>> math.pow(z, 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't convert complex to float

И основание, и показатель степени могут быть любых числовых типов, включая целые числа, числа с плавающей запятой, мнимые или комплексные:

>>> 2**z
(1.4676557979464138+7.86422192328995j)

>>> z**2
(5+12j)

>>> z**0.5
(1.8173540210239707+0.5502505227003375j)

>>> z**3j
(-0.13041489185767086-0.11115341486478239j)

>>> z**z
(-5.409738793917679-13.410442370412747j)

Возведение в степень комплексных чисел вручную становится очень трудным, если они выражены в стандартной форме. Намного удобнее переписать число в тригонометрической форме и вычислить степень, используя базовую тригонометрию. Если вы немного знаете математику, то формула Муавра поможет это сделать.

Использование комплексных чисел Python в качестве 2D-векторов   

Вы можете визуализировать комплексные числа как точки или векторы на евклидовой плоскости в декартовой или прямоугольной системе координат:

Комплексная плоскость, также известная как плоскость Гаусса или диаграмма Аргана, имеет две оси. Ось X представляет действительную часть комплексного числа, а ось Y представляет его мнимую часть.

Этот факт приводит к одной из самых крутых функций данных комплексного типа в Python, которая бесплатно воплощает элементарную реализацию двумерного вектора.
Хотя не все операции в них работают одинаково, векторы и комплексные числа имеют много общего.

Получение координат   

Бермудский треугольник — легендарный регион, известный своими паранормальными явлениями, который простирается через южную оконечность Флориды, Пуэрто-Рико и крошечный остров Бермуды. Его вершины приблизительно обозначены тремя крупными городами, географические координаты которых следующие:

  1. Майами: 25 ° 45 ’42.054” N, 80 ° 11 ’30.438” W
  2. Сан-Хуан: 18 ° 27 ’58,8” северной широты, 66 ° 6 ’20,598” западной долготы.
  3. Гамильтон: 32 ° 17 ‘41,64 дюйма северной широты, 64 ° 46’ 58,908 дюйма западной долготы.

После преобразования этих координат в десятичные градусы вы получите два числа с плавающей запятой для каждого города. Можно использовать комплексные числа для хранения упорядоченных пар значений. Поскольку широта — это вертикальная координата, а долгота — горизонтальная, было бы удобнее поменть их местами, чтобы следовать традиционному порядку декартовых координат:

miami_fl = complex(-80.191788, 25.761681)
san_juan = complex(-66.105721, 18.466333)
hamilton = complex(-64.78303, 32.2949)

Отрицательные значения долготы представляют западное полушарие, а положительные значения широты — северное полушарие.

Учтите, что это сферические координаты. Чтобы правильно спроецировать их на плоскую плоскость, вам нужно учитывать кривизну Земли. Одной из первых картографических проекций, использованных в картографии, была проекция Меркатора, которая помогала морякам ориентироваться на своих кораблях. Но давайте проигнорируем все это и предположим, что значения уже выражены в прямоугольной системе координат.

Когда вы нанесете числа на комплексную плоскость, то получите приблизительное изображение Бермудского треугольника:

Если вам не нравится вызывать функцию complex(), можно создать псевдоним типа с более подходящим именем или использовать буквальную форму комплексного числа, чтобы сэкономить несколько нажатий клавиш:

CityCoordinates = complex
miami_fl = CityCoordinates(-80.191788, 25.761681)
miami_fl = -80.191788 + 25.761681j

Если вам нужно упаковать больше атрибутов в городе, вы можете использовать именованный кортеж или класс данных или создать собственный класс.

Расчет магнитуды   

Величина, также известная как модуль или радиус комплексного числа, — это длина вектора, изображающего его на комплексной плоскости:

Вы можете вычислить его по теореме Пифагора, взяв квадратный корень из суммы квадрата действительной части и возведенной в квадрат мнимой части:

|z| = \sqrt{Re(z)^2 + Im(z)^2}

Вы могли подумать, что Python позволит вам вычислить длину такого вектора с помощью встроенной функции len(), но это не так. Чтобы получить величину комплексного числа, вы должны вызвать другую глобальную функцию с именем abs(), которая обычно используется для вычисления абсолютного значения числа:

>>> len(3 + 2j)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object of type 'complex' has no len()

>>> abs(3 + 2j)
3.605551275463989

Эта функция удаляет знак из целых чисел, которые вы передаете, но для комплексных чисел она возвращает величину или длину вектора:

>>> abs(-42)
42

>>> z = 3 + 2j

>>> abs(z)
3.605551275463989

>>> from math import sqrt
>>> sqrt(z.real**2 + z.imag**2)
3.605551275463989

Возможно, вы помните из предыдущего раздела, что комплексное число, умноженное на его сопряжение, дает его величину в квадрате.

Определение расстояния между двумя точками   

Давайте найдем геометрический центр Бермудского треугольника и расстояния до него от трех городов, образующих его границы. Во-первых, вам нужно просуммировать все координаты и разделить результат на их количество, чтобы получить среднее значение:

geometric_center = sum(miami_fl, san_juan, hamilton) / 3

Мы получим точку, расположенную в Атлантическом океане, где-то внутри треугольника:

Теперь вы можете создавать векторы, привязанные к городам и направленные к геометрическому центру треугольника. Векторы создаются путем вычитания источника из целевой точки:

v1 = geometric_center - miami_fl
v2 = geometric_center - san_juan
v3 = geometric_center - hamilton

Поскольку вы вычитаете комплексные числа, каждый вектор также является комплексным числом, состоящим из двух частей. Чтобы получить расстояние, вычислите величину каждого вектора:

>>> abs(v1)
9.83488994681275

>>> abs(v2)
8.226809506084367

>>> abs(v3)
8.784732429678444

Эти длины векторов не отражают значимых расстояний, но являются хорошим приближением для такого игрушечного примера, как этот. Чтобы представить точные результаты в материальных единицах, вам нужно сначала преобразовать координаты из сферических в прямоугольные или вместо этого рассчитать расстояние, используя метод большого круга или ортодромия.

Перенос, поворот, масштабирование и вращение   

Вас может беспокоить появление треугольника во втором квадранте декартовой системы координат. Переместим его так, чтобы его геометрический центр совпал с началом координат. Все три вершины будут переведены на длину вектора, указанного геометрическим центром, но в противоположном направлении:

triangle = miami_fl, san_juan, hamilton
offset = -geometric_center
centered_triangle = [vertex + offset for vertex in triangle]

Обратите внимание, что вы складываете два комплексных числа вместе, при этом выполняется их поэлементное сложение. Это аффинное преобразование, поскольку оно не меняет форму треугольника или относительное расположение его вершин:

Зеркальное отражение треугольника вокруг действительной или мнимой оси требует инвертирования соответствующего компонента в его вершинах. Например, чтобы перевернуть его по горизонтали, вам нужно использовать негатив реальной части, который соответствует горизонтальному направлению. Чтобы перевернуть его по вертикали, возьмем отрицательную часть воображаемой:

flipped_horizontally = [complex(-v.real, v.imag) for v in centered_triangle]
flipped_vertically = [complex(v.real, -v.imag) for v in centered_triangle]

Последнее, по сути, аналогично вычислению сопряженного комплексного числа, поэтому вы можете вызвать .conjugate() непосредственно для каждой вершины, чтобы выполнить тяжелую работу за вас:

flipped_vertically = [v.conjugate() for v in centered_triangle]

Продолжайте экспериментировать с различными комбинациями переворотов, используя интерактивную записную книжку Jupyter, доступную в загружаемых материалах. Вот как будет выглядеть треугольник, если перевернуть его по обеим осям:

Масштабирование похоже на перевод, но вместо добавления смещения вы собираетесь умножить каждую вершину на постоянный коэффициент, который должен быть действительным числом:

scaled_triangle = [1.5*vertex for vertex in centered_triangle]

В результате оба компонента каждого комплексного числа умножаются на одинаковую величину. Он должен растянуть ваш Бермудский треугольник, чтобы он выглядел больше на участке:

С другой стороны, умножение вершин треугольника на другое комплексное число приводит к его вращению вокруг начала координат системы координат. Это сильно отличается от того, как вы обычно умножаете векторы друг на друга. Например, скалярное произведение двух векторов приведет к скаляру, в то время как их перекрестное произведение возвращает новый вектор в трехмерном пространстве, перпендикулярном поверхности, которую они определяют.

Примечание. Произведение двух комплексных чисел не является векторным умножением. Вместо этого он определяется как умножение матриц в двумерном векторном пространстве, где 1 и j являются стандартными базисами. Умножение (x1 + y1j) на (x2 + y2j) соответствует следующему умножению матриц:

|z| = \begin{bmatrix}x_1 & -y_1\\y_1 & x_1 \end{bmatrix}\begin{bmatrix}x_2\\y_2\end{bmatrix} = \begin{bmatrix}x_1x_2 - y_1y_2\\y_1x_2 + x_1e_2\end{bmatrix}

Это матрица вращения слева, благодаря которой математика работает нормально.

Когда вы умножаете вершины на мнимую единицу, треугольник повернется на 90° против часовой стрелки. Если вы будете продолжать это повторять, то в конце концов придете к тому, с чего начали:

Как найти конкретное комплексное число, которое будет вращать другое комплексное число на любой желаемый угол при умножении обоих? Во-первых, взгляните на следующую таблицу, в которой суммированы последовательные повороты на 90°:

Вращение на 90° Общий угол Формула Степень Значение
0 z j0 1
1 90° z × j j1 j
2 180° z × j × j j2 -1
3 270° z × j × j × j j3 j
4 360° z × j × j × j × j j4 1
5 450° z × j × j × j × j × j j5 j
6 540° z × j × j × j × j × j × j j6 -1
7 630° z × j × j × j × j × j × j × j j7 j
8 720° z × j × j × j × j × j × j × j × j j8 1

Когда вы выражаете многократное умножение на j через положительные целые показатели, тогда возникает закономерность. Обратите внимание, как увеличение мнимой единицы до последующих степеней заставляет ее циклически перебирать одни и те же значения. Вы можете экстраполировать это на дробные показатели и ожидать, что они будут соответствовать промежуточным углам.

Например, показатель степени на полпути первого поворота равен 0,5 и представляет собой угол 45°:

Итак, если вы знаете, что степень единицы представляет собой прямой угол, а все, что находится между ними, пропорционально масштабируется, то можете вывести эту общую формулу для произвольных вращений:

def rotate(z: complex, degrees: float) -> complex:
    return z * 1j**(degrees/90)

Обратите внимание, что вращение становится более естественным, когда вы выражаете комплексные числа в полярных координатах, которые уже описывают угол. Затем вы можете воспользоваться экспоненциальной формой, чтобы упростить вычисления:

Есть два способа повернуть число в полярных координатах:

import math, cmath

def rotate1(z: complex, degrees: float) -> complex:
    radius, angle = cmath.polar(z)
    return cmath.rect(radius, angle + math.radians(degrees))

def rotate2(z: complex, degrees: float) -> complex:
    return z * cmath.rect(1, math.radians(degrees))

Вы можете суммировать углы или умножить комплексное число на единичный вектор.

Вы узнаете о них больше в следующем разделе.

Математический модуль для комплексных чисел: cmath   

Вы уже видели, что некоторые встроенные функции, такие как abs() и pow(), принимают комплексные числа, а другие — нет. Например, вы не можете round() комплексное число, потому что такая операция не имеет смысла:

>>> round(3 + 2j)
Traceback (most recent call last):
  File "", line 1, in 
TypeError: type complex doesn't define __round__ method

Многие сложные математические функции, такие как тригонометрические, гиперболические или логарифмические функции, доступны в стандартной библиотеке. К сожалению, даже если вы знаете все о математическом модуле Python, это не поможет, потому что ни одна из его функций не поддерживает комплексные числа. Вам нужно будет объединить его с модулем cmath, который определяет соответствующие функции для комплексных чисел.

Модуль cmath переопределяет все константы с плавающей запятой из математики, чтобы они были у вас под рукой без необходимости импортировать оба модуля:

>>> import math, cmath
>>> for name in "e", "pi", "tau", "nan", "inf":
...     print(name, getattr(math, name) == getattr(cmath, name))
...
e True
pi True
tau True
nan False
inf True

Обратите внимание, что nan — это особое значение, которое никогда не равно ничему другому, в том числе самому себе! Вот почему в приведенном выше выводе вы видите только одно значение False. В дополнение к этому cmath предоставляет два сложных аналога для NaN (не числа) и infinity (бесконечность), причем оба имеют нулевые действительные части:

>>> from cmath import nanj, infj
>>> nanj.real, nanj.imag
(0.0, nan)
>>> infj.real, infj.imag
(0.0, inf)

В cmath примерно вдвое меньше функций, чем в стандартном математическом модуле. Большинство из них имитируют исходное поведение, но некоторые уникальны для комплексных чисел. Они позволят вам выполнить преобразование между двумя системами координат, которые вы изучите в этом разделе.

Извлечение корня комплексного числа   

Основная теорема алгебры утверждает, что многочлен степени n с комплексными коэффициентами имеет ровно n комплексных корней. Это очень важно, если вы подумаете об этом, так что позвольте этому на мгновение погрузиться в размышления.

Теперь возьмем для примера квадратичную функцию x^2 + 1. Визуально эта парабола не пересекает ось X, потому что она находится на одну единицу выше начала координат. Дискриминант функции отрицательный, что арифметически подтверждает это наблюдение. В то же время это многочлен второй степени, поэтому у него должно быть два комплексных корня, даже если у него нет реальных!

Чтобы найти эти корни, вы можете переписать функцию в виде квадратного уравнения,затем переместите константу вправо и извлеките квадратный корень из обеих частей:

x^2 + 1 = 0\\x^2 = -1\\x = \sqrt{-1}

В области действительных чисел квадратный корень определяется только для неотрицательных входных значений. Следовательно, вызов этой функции в Python вызовет исключение и соответствующее сообщение об ошибке:

>>> import math
>>> math.sqrt(-1)
Traceback (most recent call last):
  File "", line 1, in 
ValueError: math domain error

Однако, если вместо этого вы рассматриваете \sqrt{-1} как комплексное число и вызываете соответствующую функцию из модуля cmath, вы получите более значимый результат:

>>> import cmath
>>> cmath.sqrt(-1)
1j

В этом есть смысл. В конце концов, промежуточная форма квадратного уравнения x^2 = -1 — это само определение мнимой единицы. Но подождите минутку. Куда делся другой комплексный корень? А как насчет комплексных корней многочленов высшей степени?

Например, полином четвертой степени x^4 + 1, который можно записать как уравнение x^4 = -1, имеет четыре сложных корня:

  • z_0 = -\sqrt{2}/2 + \sqrt{2}/2j
  • z_1 = -\sqrt{2}/2 - \sqrt{2}/2j
  • z_2 = \sqrt{2}/2 + \sqrt{2}/2j
  • z_3 = \sqrt{2}/2 - \sqrt{2}/2j

Возведение каждого корня в четвертую степень дает комплексное число, равное -1 + 0j или действительное число -1:

>>> import cmath
>>> z0 = -cmath.sqrt(2)/2 + cmath.sqrt(2)/2*1j

>>> z0**4
(-1.0000000000000004-0j)

>>> (z0**4).real
-1.0000000000000004

Вы заметите, что результирующее значение не равно -1 из-за ошибки округления в арифметике с плавающей запятой. Чтобы учесть это, вы можете вызывать cmath.isclose() всякий раз, когда вам нужно определить, близки ли два комплексных числа по значению:

>>> cmath.isclose(z0**4, -1)
True

К сожалению, вы не можете вычислить другие сложные корни с помощью чистого Python, потому что регулярное возведение в степень всегда дает одно решение:

>>> pow(-1, 1/4)
(0.7071067811865476+0.7071067811865475j)

Это только один из перечисленных выше корней. Математическая формула для нахождения всех комплексных корней использует преимущества тригонометрической формы комплексных чисел:

z_k = r(\cos{(\varphi + \dfrac{2\pi k}{n})}) + j\sin{(\varphi + \dfrac{2\pi k}{n})}

r и \varphi — полярные координаты комплексного числа, n — степень многочлена, а k — индекс корня, начиная с нуля. Хорошая новость в том, что вам не нужно тщательно вычислять эти корни самостоятельно. Самый быстрый способ найти их — установить стороннюю библиотеку, такую ​​как NumPy, и импортировать ее в свой проект:

>>> import numpy as np
>>> np.roots([1, 0, 0, 0, 1])  # Coefficients of the polynomial x**4 + 1
array([-0.70710678+0.70710678j, -0.70710678-0.70710678j,
        0.70710678+0.70710678j,  0.70710678-0.70710678j])

Полезно знать о различных формах комплексных чисел и их системах координат. Как видите, это помогает в решении практических задач вроде поиска сложных корней. Итак, в следующем разделе вы углубитесь в подробности.

Переходы между прямоугольными и полярными координатами   

Геометрически комплексное число можно рассматривать двояко. С одной стороны, это точка, горизонтальное и вертикальное расстояния которой от начала координат однозначно определяют ее местоположение. Они известны как прямоугольные координаты, состоящие из действительной и мнимой частей.

С другой стороны, можно описать ту же точку в полярных координатах, что также позволит вам однозначно найти ее с двумя расстояниями:

  1. Радиальное расстояние — это длина радиуса, измеренная от начала координат.
  2. Угловое расстояние — это угол, измеряемый между горизонтальной осью и радиусом.

Радиус, также известный как модуль, соответствует величине комплексного числа или длине вектора. Угол обычно называют фазой или аргументом комплексного числа. При работе с тригонометрическими функциями полезно выражать угол в радианах, а не в градусах.

Вот изображение комплексного числа в обеих системах координат:

Следовательно, точка (3, 2) в декартовой системе координат имеет радиус примерно 3,6 и угол примерно 33,7°, или примерно \pi более 5,4 радиана.

Преобразование между двумя системами координат стало возможным благодаря нескольким функциям, скрытым в модуле cmath. В частности, чтобы получить полярные координаты комплексного числа,вы должны передать его в cmath.polar():

>>> import cmath
>>> cmath.polar(3 + 2j)
(3.605551275463989, 0.5880026035475675)

Она вернет кортеж, где первый элемент — это радиус, а второй — угол в радианах. Обратите внимание, что радиус имеет то же значение, что и величина, которую вы можете вычислить, вызвав abs() для вашего комплексного числа. И наоборот, если вас интересует только угол комплексного числа, вы можете вызвать cmath.phase():

>>> z = 3 + 2j

>>> abs(z)  # Magnitude is also the radial distance
3.605551275463989

>>> import cmath
>>> cmath.phase(3 + 2j)
0.5880026035475675

>>> cmath.polar(z) == (abs(z), cmath.phase(z))
True

Угол может быть получен с помощью базовой тригонометрии, поскольку действительная часть, мнимая часть и величина вместе образуют прямоугольный треугольник:

Вы можете использовать обратные тригонометрические функции, такие как арксинус, либо из математики, либо из cmath, но последняя будет давать комплексные значения с мнимой частью, равной нулю:

>>> z = 3 + 2j

>>> import math
>>> math.acos(z.real / abs(z))
0.5880026035475675
>>> math.asin(z.imag / abs(z))
0.5880026035475676
>>> math.atan(z.imag / z.real)  # Prefer math.atan2(z.imag, z.real)
0.5880026035475675

>>> import cmath
>>> cmath.acos(z.real / abs(z))
(0.5880026035475675-0j)

Однако при использовании функции арктангенса следует соблюдать одну небольшую деталь, которая побудила многие языки программирования разработать альтернативную реализацию под названием atan2(). Вычисление отношения между мнимой и действительной частями может иногда вызывает сингулярность, например, из-за деления на ноль. Более того, отдельные знаки двух значений теряются в процессе, что делает невозможным точное определение угла:

>>> import math

>>> math.atan(1 / 0)
Traceback (most recent call last):
  File "", line 1, in 
ZeroDivisionError: division by zero

>>> math.atan2(1, 0)
1.5707963267948966

>>> math.atan(1 / 1) == math.atan(-1 / -1)
True

>>> math.atan2(1, 1) == math.atan2(-1, -1)
False

Обратите внимание, как atan() не может распознать две разные точки, расположенные в противоположных квадрантах системы координат. С другой стороны, atan2() ожидает, что два аргумента вместо одного сохранят отдельные знаки перед разделением один на другой, а также избегают других проблем.

Чтобы получить градусы вместо радианов, можно снова выполнить необходимое преобразование, используя математический модуль:

>>> import math
>>> math.degrees(0.5880026035475675)  # Радианы в градусы
33.690067525979785
>>> math.radians(180)  # Градусы в радианы
3.141592653589793

Обратный процесс, то есть преобразование полярных координат в прямоугольные, зависит от другой функции. Однако вы не можете просто передать тот же кортеж, который вы получили от cmath.polar(), поскольку cmath.rect() ожидает два отдельных аргумента:

>>> cmath.rect(cmath.polar(3 + 2j))
Traceback (most recent call last):
  File "", line 1, in 
TypeError: rect expected 2 arguments, got 1

Хорошая идея сначала распаковать кортеж и дать этим элементам более описательные имена. Теперь вы можете правильно вызвать cmath.rect():

>>> radius, angle = cmath.polar(3 + 2j)
>>> cmath.rect(radius, angle)
(3+1.9999999999999996j)

Вы можете столкнуться с ошибками округления по пути, пока Python производит вычисления. За кулисами он вызывает тригонометрические функции для получения реальной и мнимой частей:

>>> import math
>>> radius*(math.cos(angle) + math.sin(angle)*1j)
(3+1.9999999999999996j)

>>> import cmath
>>> radius*(cmath.cos(angle) + cmath.sin(angle)*1j)
(3+1.9999999999999996j)

Опять же, не имеет значения, используете ли вы math или cmath в этом случае, поскольку результаты будут идентичными.

Различное представление комплексных чисел   

Независимо от системы координат, одно и то же комплексное число можно представить в нескольких математически эквивалентных формах:

  • Алгебраической (стандартный);
  • Геометрической;
  • Тригонометрической;
  • Экспоненциальной.

Этот список не полный, так как представлений гораздо больше, например, матричное представление комплексных чисел.

Для решения каждой конкретной проблемы, имея столько альтернатив, можно выбрать наиболее удобную форму. Например, вам понадобится экспоненциальная форма для вычисления дискретного преобразования Фурье в следующем разделе. Эта форма также подходит для умножения и деления комплексных чисел.

Вот краткое изложение отдельных форм комплексных чисел и их координат:

Форма Прямоугольные Полярные
Алгебраическая z = x + yj
Геометрическая z = (x, y) z = (r, \varphi)
Триганометрическая z = |z|(\cos{(x/|z|)}) + j\sin{(y/|z|)} z = r(\cos{(\varphi)} + j\sin{(\varphi)}
Экспоненциальная z = |z|e^{atan2(y/x)j} z = r(e^{j\varphi})

Алгебраическая форма является родной для Python, когда вы указываете комплексные числа с помощью их литералов. Вы также можете рассматривать их как точки на евклидовой плоскости в декартовой или полярной системе координат. Хотя в Python нет отдельных представлений для тригонометрической или экспоненциальной формы, вы можете проверить, верны ли математические принципы. Например, формула Эйлера, вставленная в тригонометрическую форму превратит ее в экспоненциальную. Вы можете вызвать exp() модуля cmath или возвести константу e в степень, чтобы получить тот же результат:

>>> import cmath

>>> algebraic = 3 + 2j
>>> geometric = complex(3, 2)
>>> radius, angle = cmath.polar(algebraic)
>>> trigonometric = radius * (cmath.cos(angle) + 1j*cmath.sin(angle))
>>> exponential = radius * cmath.exp(1j*angle)

>>> for number in algebraic, geometric, trigonometric, exponential:
...     print(format(number, "g"))
...
3+2j
3+2j
3+2j
3+2j

Все формы действительно представляют собой разные способы кодирования одного и того же числа. Однако вы не можете сравнивать их напрямую из-за ошибок округления, которые могут возникнуть в это время. Используйте cmath.isclose() для безопасного сравнения или соответствующим образом форматируйте числа как строки. Вы узнаете, как отформатировать такие строки в следующем разделе.

Объяснение того, почему разные формы комплексного числа эквивалентны, требует исчисления и выходит далеко за рамки этого урока. Однако, если вас интересует математика, вы обнаружите, что связи между различными областями математики, которые проявляются с помощью комплексных чисел, будут увлекательными.

Анализ комплексного числа в Python   

Вы уже много узнали о комплексных числах Python и видели предварительные примеры. Однако прежде чем двигаться дальше, стоит затронуть несколько последних тем. В этом разделе вы изучите сравнение комплексных чисел, форматирование строк, которые их содержат, и многое другое.

Проверка равенства комплексных чисел   

С точки зрения математики два комплексных числа равны, если они имеют одинаковые значения, независимо от принятой системы координат. Однако преобразование между полярными и прямоугольными координатами обычно приводит к ошибкам округления в Python, поэтому вам нужно следить за мельчайшими различиями при их сравнении.

Например, единичная окружность, радиус которой равен единице, имеет точку с наклоном под углом 60°, тогда тригонометрия прекрасно работает, упрощая преобразование с помощью ручки и бумаги:

>>> import math, cmath

>>> z1 = cmath.rect(1, math.radians(60))
>>> z2 = complex(0.5, math.sqrt(3)/2)

>>> z1 == z2
False

>>> z1.real, z2.real
(0.5000000000000001, 0.5)
>>> z1.imag, z2.imag
(0.8660254037844386, 0.8660254037844386)

Даже если вы знаете, что z1 и z2 — это одна и та же точка, Python не может определить это из-за ошибок округления. К счастью, в документе PEP 485 определены функции для приблизительного равенства, которые доступны в модулях math и cmath:

>>> math.isclose(z1.real, z2.real)
True

>>> cmath.isclose(z1, z2)
True

Не забывайте всегда использовать их при сравнении комплексных чисел! Если допуск по умолчанию недостаточно хорош для ваших расчетов, вы можете изменить его, указав дополнительные аргументы.

Порядок комплексных чисел   

Если вы знакомы с кортежами, то знаете, что Python может их сортировать:
>>> planets = [
… (6, «saturn»),
… (4, «mars»),
… (1, «mercury»),
… (5, «jupiter»),
… (8, «neptune»),
… (3, «earth»),
… (7, «uranus»),
… (2, «venus»),
… ]
>>> from pprint import pprint
>>> pprint(sorted(planets))
[(1, ‘mercury’),
(2, ‘venus’),
(3, ‘earth’),
(4, ‘mars’),
(5, ‘jupiter’),
(6, ‘saturn’),
(7, ‘uranus’),
(8, ‘neptune’)]


По умолчанию отдельные кортежи сравниваются слева направо:

>>> (6, "saturn") < (4, "mars")
False
>>> (3, "earth") < (3, "moon")
True

В первом случае число 6 больше 4, поэтому названия планет вообще не учитываются. Однако они могут помочь разрешить ничью. Однако это не относится к комплексным числам, поскольку они не определяют отношения естественного порядка. Например, вы получите сообщение об ошибке, если попытаетесь сравнить два комплексных числа:

>>> (3 + 2j) < (2 + 3j)
Traceback (most recent call last):
  File "", line 1, in 
TypeError: '<' not supported between instances of 'complex' and 'complex'

Должно ли воображаемое измерение иметь больший вес, чем реальное? Стоит ли сравнивать их величины? Выбор за вами, и ответы будут разными. Поскольку вы не можете напрямую сравнивать комплексные числа, вам нужно указать Python, как их сортировать, указав специальную ключевую функцию, такую ​​как abs():

>>> cities = {
...     complex(-64.78303, 32.2949): "Гамильтон",
...     complex(-66.105721, 18.466333): "Сан Хуан",
...     complex(-80.191788, 25.761681): "Майами"
... }

>>> for city in sorted(cities, key=abs, reverse=True):
...     print(abs(city), cities[city])
...
84.22818453809096 Майами
72.38647347392259 Гамильтон
68.63651945864338 Сан Хуан

Здесь комплексные числа отсортированы по их величине в порядке убывания.

Форматирование комплексных чисел как строк   

Не существует специальных кодов формата для комплексных чисел, но вы можете форматировать их действительную и мнимую части отдельно, используя стандартные коды для чисел с плавающей запятой. Ниже вы найдете несколько техник, которые это продемонстрируют. Некоторые из них фактически применит ваш спецификатор формата как к реальной, так и к мнимой части за один раз.

Примечание. Форматирование строки позволяет игнорировать ошибку представления с плавающей запятой и делать вид, что ее не существует:

>>> import cmath
>>> z = abs(3 + 2j) * cmath.exp(1j*cmath.phase(3 + 2j))

>>> str(z)
'(3+1.9999999999999996j)'

>>> format(z, "g")
'3+2j'

Буква «g» в спецификаторе формата обозначает общий формат, который округляет ваше число до требуемой точности. По умолчанию точность составляет шесть значащих цифр.

Возьмем в качестве примера следующее комплексное число и отформатируем его с двумя десятичными знаками в обеих частях:

>>> z = pow(3 + 2j, 0.5)
>>> print(z)
(1.8173540210239707+0.5502505227003375j)

Быстрый способ сделать это - либо вызвать format() со спецификатором числового формата, либо создать f-строку соответствующего формата:

>>> format(z, ".2f")
'1.82+0.55j'

>>> f"{z:.2f}"
'1.82+0.55j'

Если вам нужен больший контроль, например, для добавления дополнительных отступов вокруг оператора плюса, то лучшим выбором будет f-строка:

>>> f"{z.real:.2f} + {z.imag:.2f}j"
'1.82 + 0.55j'

Вы также можете вызвать .format() для строкового объекта и передать ему позиционные или ключевые аргументы:

>>> "{0:.2f} + {0:.2f}j".format(z.real, z.imag)
'1.82 + 1.82j'

>>> "{re:.2f} + {im:.2f}j".format(re=z.real, im=z.imag)
'1.82 + 0.55j'

Позиционные аргументы обеспечивают последовательность значений, а аргументы ключевого слова позволяют ссылаться на них по имени. Точно так же вы можете использовать строковый оператор по модулю (%) с кортежем или словарем:

>>> "%.2f + %.2fj" % (z.real, z.imag)
'1.82 + 0.55j'

>>> "%(re).2f + %(im).2fj" % {"re": z.real, "im": z.imag}
'1.82 + 0.55j'

Однако здесь используется другой синтаксис заполнителя и он немного старомоден.

Создание собственного сложного типа данных   

Модель данных Python определяет набор специальных методов, которые вы можете реализовать, чтобы сделать ваши классы совместимыми с определенными встроенными типами. Допустим, вы работали с точками и векторами и хотели получить угол между двумя связанными векторами. Вы можете вычислить их скалярное произведение и провести некоторую тригонометрию. В качестве альтернативы, вы можете использовать комплексные числа.

Давайте сначала определим ваши классы:

from typing import NamedTuple

class Point(NamedTuple):
    x: float
    y: float

class Vector(NamedTuple):
    start: Point
    end: Point

Точка имеет координаты x и y, а вектор соединяет две точки. Возможно, вы помните cmath.phase(), которая вычисляет угловое расстояние комплексного числа. Теперь, если вы относились к своим векторам как к комплексным числам и знали их фазы, то вы могли бы вычесть их, чтобы получить желаемый угол.Чтобы Python распознавал экземпляры векторов как комплексные числа, вы должны указать .__complex__() в теле класса:

class Vector(NamedTuple):
    start: Point
    end: Point

    def __complex__(self):
        real = self.end.x - self.start.x
        imag = self.end.y - self.start.y
        return complex(real, imag)

Код внутри должен всегда возвращать экземпляр комплексного типа данных, поэтому он обычно создает новое комплексное число из вашего объекта. Здесь вы вычитаете начальную и конечную точки, чтобы получить горизонтальные и вертикальные смещения, которые служат реальной и мнимой частями. Метод будет выполняться через делегирование, когда вы вызываете глобальный complex() для экземпляра вектора:

>>> vector = Vector(Point(-2, -1), Point(1, 1))
>>> complex(vector)
(3+2j)

В некоторых случаях вам не нужно выполнять такое приведение типов самостоятельно. Давайте посмотрим на пример на практике:

>>> v1 = Vector(Point(-2, -1), Point(1, 1))
>>> v2 = Vector(Point(10, -4), Point(8, -1))

>>> import math, cmath
>>> math.degrees(cmath.phase(v2) - cmath.phase(v1))
90.0

У вас есть два вектора, обозначенных четырьмя разными точками. Затем вы передаете их непосредственно в cmath.phase(), который выполняет преобразование в комплексное число за вас и возвращает фазу. Разность фаз - это угол между двумя векторами.

Разве это было не красиво? Вы избавили себя от множества ошибок - склонный код, совмещая комплексные числа и немного магии Python.

Вычисление дискретного преобразования Фурье с комплексными числами   

Хотя вы можете использовать действительные числа для вычисления коэффициентов синуса и косинуса частот периодической функции с помощью преобразования Фурье, обычно удобнее иметь дело только с одним комплексным коэффициентом на частоту. Дискретное преобразование Фурье в комплексной области задается следующей формулой:

F_k = \sum\limits_{n=0}^{N-1}{x_n e^{-jwn}}

Для каждой частоты k он измеряет корреляцию сигнала и конкретной синусоидальной волны, выраженной как комплексное число в экспоненциальной форме. (Спасибо, Леонард Эйлер!) Угловую частоту волны можно вычислить, умножив круглый угол, равный 2\pi радиан, на k на количество дискретных отсчетов:

w = \dfrac{2\pi k}{N}

Кодирование этого на Python выглядит довольно аккуратно, если вы воспользуетесь преимуществами комплексного типа данных:

from cmath import pi, exp

def discrete_fourier_transform(x, k):
    omega = 2 * pi * k / (N := len(x))
    return sum(x[n] * exp(-1j * omega * n) for n in range(N))

Эта функция является буквальной транскрипцией приведенных выше формул. Теперь вы можете выполнить частотный анализ звука, который вы загружаете из аудиофайла с помощью модуля Python wave или синтезируете с нуля. Чтобы построить частотный спектр с помощью Matplotlib, вы должны знать частоту дискретизации, которая определяет разрешение вашего частотного бина, а также частота Найквиста:

import matplotlib.pyplot as plt

def plot_frequency_spectrum(
    samples,
    samples_per_second,
    min_frequency=0,
    max_frequency=None,
):
    num_bins = len(samples) // 2
    nyquist_frequency = samples_per_second // 2

    magnitudes = []
    for k in range(num_bins):
        magnitudes.append(abs(discrete_fourier_transform(samples, k)))

    # Нормализовать величины
    magnitudes = [m / max(magnitudes) for m in magnitudes]

    # Вычислить интервалы частоты
    bin_resolution = samples_per_second / len(samples)
    frequency_bins = [k * bin_resolution for k in range(num_bins)]

    plt.xlim(min_frequency, max_frequency or nyquist_frequency)
    plt.bar(frequency_bins, magnitudes, width=bin_resolution)

Количество частотных элементов в спектре равно половине отсчетов, в то время как частота Найквиста ограничивает максимальную частоту, которую вы можете измерить. Преобразование возвращает комплексное число, величина которого соответствует амплитуде синусоидальной волны на данной частоте, а ее угол - фаза.

Примечание. Чтобы получить правильные значения амплитуды, необходимо удвоить число и разделить полученную величину на количество отсчетов. С другой стороны, если вас интересует только частотная гистограмма, вы можете нормализовать величины по их сумме или максимальной частоте.

Вот примерный график частоты звуковой волны, состоящей из трех тонов - 440 Гц, 1,5 кГц и 5 кГц - с равными амплитудами:

Обратите внимание, что это был чисто академический пример, поскольку вычисление дискретного преобразования Фурье с вложенными итерациями имеет временную сложность O(n^2), что делает его непригодным для использования на практике. Для реальных приложений вы хотите использовать алгоритм быстрого преобразования Фурье (БПФ), лучше всего реализованный в библиотеке C, такой как FFT в SciPy.

Заключение   

Простота использования комплексных чисел в Python делает их на удивление забавным и практичным инструментом. Вы увидели двумерные векторы, реализованные практически бесплатно, и благодаря им вы смогли анализировать звуковые частоты. Комплексные числа позволяют элегантно выражать математические формулы в коде без использования шаблонного синтаксиса.

В этом уроке вы узнали, как:

  • Определить комплексные числа с помощью литералов в Python.
  • Представлять комплексные числа в прямоугольных и полярных координатах.
  • Использовать комплексные числа в арифметическихвыражениях.
  • Использовать преимуществами встроенного модуля cmath.
  • Переводить математические формулы прямо в код Python.

Какой у вас был опыт работы с комплексными числами Python? Вас когда-нибудь пугали они? Как вы думаете, какие еще интересные проблемы они позволят вам решить?

Вы можете щелкнуть ссылку ниже, чтобы получить полный исходный код этого руководства:

На основе Simplify Complex Numbers With Python

Опубликовано Вадим В. Костерин

ст. преп. кафедры ЦЭиИТ. Автор более 130 научных и учебно-методических работ. Лауреат ВДНХ (серебряная медаль).

Оставьте комментарий

Ваш адрес email не будет опубликован.