Игра Яндекс Практикума
Игра Яндекс Практикума
Игра Яндекс Практикума

Учимся программировать квантовый компьютер на основе игры «Морской бой»

Отредактировано

Такие компании, как IBM и Google, занимаются созданием квантовых устройств. Это значит, что время игры с кубитами уже настало. В этой статье мы расскажем вам, как создать игру «Морской бой» на квантовом компьютере.

19К открытий20К показов
Учимся программировать квантовый компьютер на основе игры «Морской бой»

«Квантовые компьютеры — довольно интересный новый вид вычислительной техники. Они словно мчатся через немыслимое количество параллельных вселенных, чтобы быстрее запускать программы. В их работе используются принципы, вводившие в заблуждение даже Эйнштейна. Это волшебные коробки, из-за которых ваши блоки с транзисторами будут храниться в пыли», — вероятно, такой текст вы увидите в популярных научных статьях. Они, несомненно, внесли свой вклад в то, чтобы новая технология звучала здорово. Однако они же, авторы подобных статей, могут превратить квантовые вычисления в какое-то оккультное искусство, заниматься которым позволено только самым умным учёным. Но несмотря на то, что это не так, большая часть программистов не увлекается квантовыми вычислениями.

Такие компании, как IBM и Google, занимаются созданием квантовых устройств. Это значит, что время игры с кубитами уже настало.

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

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

Примечание Обратите внимание, что код на Python, приведённый в статье, далёк от идеала и служит лишь для объяснения принципов работы квантового компьютера.

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

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

Что такое квантовый компьютер?

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

В основе работы обычного компьютера лежат биты — переменные, имеющие только два значения: 0 и 1. Хотя, в контексте булевой алгебры их также можно называть «true» и «false». Неважно, как мы их называем. Важно лишь то, что существует два состояния.

С помощью битов можно выполнить простые битовые операции, такие как побитовое отрицание (NOT), побитовое «И» (AND) и побитовое «ИЛИ» (OR). Они являются основой вычислений. С битами можно сделать всё что угодно. Любая более сложная переменная, чем бит (int или float) — это просто набор из множества битов. Любая более сложная операция, чем побитовое «И» или побитовое отрицание (NOT) — это просто несколько таких операций, соединённых вместе. Именно так и работает обычный компьютер на самом базовом уровне.

В основе квантовых компьютеров лежат квантовые биты (кубиты). Они тоже имеют два возможных значения — 0 и 1. Однако законы квантовой механики допускают другие значения, которые называются состояниями суперпозиции.

В каком-то смысле состояния суперпозиции представляют собой значения, существующие между экстремумами 0 и 1. Кубит можно представить в виде сферы, на которой 0 и 1 расположены на противоположных полюсах, а состояния суперпозиции — все остальные возможные точки на её поверхности.

Учимся программировать квантовый компьютер на основе игры «Морской бой» 1

Кроме 0 и 1 на изображении выше обозначена пара важных состояний суперпозиции. Одно из них называется u3(0.5*pi, 0,0) 0. Не самое запоминающееся название, но вы узнаете, что оно означает, по ходу чтения статьи.

Такой ход мысли выставляет кубиты чем-то похожим на непрерывные переменные. Можно представить любую точку на поверхности сферы (например, точку ψ в изображении), используя полярные координаты с углами (θ и φ). В принципе простительно полагать, что кубиты — плавающие значения (изменяют своё значение). В некотором смысле они ими и являются. Но с точки зрения абсолютной точности это не так.

Важной отличительной чертой кубита является невозможность извлечения из него недвоичной информации. Законы физики по своей природе нам не позволяют узнать, что именно кубиты делают. Не получится спросить у кубита точные детали о положении его суперпозиции. Можно только заставить выбирать его между двумя противоположными точками на сфере (вроде 0 и 1). Если это что-то, что не является ни нулём, ни единицей, то кубиту просто нужно будет случайным образом решить, что выбрать.

Кубит имеет некоторые свойства непрерывной и дискретной переменных, но не является ни той, ни другой. Кубит — квантовая переменная.

Что предстоит делать с кубитами?

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

Можно использовать кубит для каждого корабля, чтобы симулировать эти действия на квантовом компьютере. Ноль может означать невредимый корабль, а единица — полностью уничтоженный корабль. Состояния суперпозиции могут служить для отображения степени повреждений корабля. Корабль, уничтожаемый с одного выстрела, довольно просто симулировать. Можно его инициализировать с нулём, а затем применить побитовое отрицание (NOT), когда он будет уничтожен.

Давайте посмотрим, как реализовать этот простой процесс на квантовом компьютере. Мы не будем обращать внимание на какие-либо другие корабли на этом этапе.

Основной язык сборки для квантовых компьютеров называется QASM. Ниже представлен скрипт, который инициализирует корабль, атакует его, а затем проверяет, на плаву ли он ещё.

			OPENQASM 2.0;
include "qelib1.inc";
qreg q[1]; \\ Инициализация регистра с помощью одного кубита
creg c[1]; \\ Инициализация регистра с помощью обычного бита
        
u3(pi,0,0) q[0]; \\ Применение побитового отрицания (NOT) к кубиту

measure q[0] -> c[0]; \\ Измерение кубита
		

Первые две линии всегда находятся в верхней части любого файла на QASM. После этого мы определяем все биты, которые нам понадобятся: кубиты и обычные биты. В этом примере мы ссылаемся на этот кубит в коде как q[0]. В примере мы определили один кубит в регистре под названием q. Так как на выходе должна быть нормальная удобная для чтения информация, мы также определяем один нормальный бит в регистре «c».

Кубит q[0] автоматически инициализируется в состоянии 0, так как это то состояние, с которого мы хотим начать, дальнейшая подготовка не требуется.

Затем нужно взять q[0] = 0 — полностью целый корабль – и выполнить побитовое отрицание (NOT). С обычным битом и с обычным языком программирования это выглядело бы так: q[0] = !q[0] (для C++) или q[0] = not q[0] (для Python). В случае с QASM это может быть выполнено несколькими разными способами. Самый простой заключается в использовании операций с x и y. Ниже приведены некоторые примеры использования этих операций с эквивалентами на C++ и Python для сравнения.

			q[0] = not q[0] # Реализация побитового отрицания на Python
		
			q[0] = !q[0]; \\ Реализация побитового отрицания на C++
		
			x q[0]; \\ Побитовое отрицание с помощью QASM
y q[0]; \\ Ещё один способ реализации побитового отрицания с помощью QASM
		

Есть некоторые различия между x и y, с которыми нам ещё придётся столкнуться, но об этом поговорим в другой раз.

Вы скорее всего заметите, что ни x, ни y не присутствует в предыдущем файле на QASM. Вместо них написано следующее: u3(pi, 0, 0) — ещё один способ применения побитового отрицания (NOT).

			u3(pi,0,0) q[0]; \\ Ещё один способ побитового отрицания на QASM
		

Это полностью равнозначно использованию x. Но u3 — более сложная операция. У неё есть три аргумента, изменяя которые, можно выполнять другие действия.

Первый аргумент — угол со значением, выраженным в радианах. Это тот угол, вокруг которого будет вращаться сфера нашего кубита. Угол pi равен 180°, это значит, что сфера будет перевёрнута полностью с ног на голову. 0 становится на место 1, а 1 — на место 0, поэтому эта операция и выглядит как побитовое отрицание (NOT). Чтобы выполнить побитовое отрицание наполовину, можно просто использовать половину угла: u3(0,5*pi,0,0)

Теперь появился ещё один способ представления побитового отрицания при работе с кубитом: можно выполнить половинное побитовое отрицание дважды.

			\\ Очередной способ реализации побитового отрицания на QASM
u3(0.5*pi,0,0) q[0];
u3(0.5*pi,0,0) q[0];
		

Аналогичные действия можно сделать с 1/3 побитового отрицания, выполнив его трижды и так далее.

Последняя строка в файле на QASM выглядит так:

			measure q[0] -> c[0];
		

Таким образом измеряется кубит. Мы как бы говорим q[0] решить, чем быть: единицей или нулём. Конечный результат сохраняется в c[0], и наш мозг или обычный компьютер уже смогут прочитать его. Значение c[0] заменяется результатом вычисления на выходе.

Если ничего не было сделано в период между инициализацией и измерением, то результат всегда должен быть таким: c[0] = 0

			qreg q[1];
creg c[1];

measure q[0] -> c[0];
		

Если было выполнено побитовое отрицание, то результат всегда должен быть таким: c[0]=1

			qreg q[1];
creg c[1];
        
u3(pi,0,0) q[0];

measure q[0] -> c[0];
		

Если выполнили только половинное побитовое отрицание, то q[0] будет между 0 и 1 в процессе измерения — а именно в состоянии суперпозиции, к которому мы уже обращались раннее: u3(0.5*pi,0,0) 0. Измерение принадлежности его к нулю или единице заставит его случайным образом выбрать одну из двух позиций с равной вероятностью.

			qreg q[1];
creg c[1];
        
u3(0.5*pi,0,0) q[0];

measure q[0] -> c[0];
		

Эта вероятностная природа придаёт квантовым вычислениям некую случайность. Дополнительная хаотичность также присутствует в современных устройствах из-за шумов. Хотя это и происходит на довольно низком уровне, не стоит об этом забывать. Именно из-за шума иногда получается 1 на выходе, вместо 0 и наоборот.

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

Создание игры

Итак, давайте же использовать способы выполнения побитового отрицания для создания игры на квантовом компьютере.

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

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

Это тот способ, который используется в текущей версии API IBM. И именно его мы используем в этой статье. Есть, конечно, другие подходы, например, Project Q.

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

Для запуска программы вам потребуется Jupyter, а также зависимости SDK.

Теперь давайте обратимся к коду.

Прежде всего нужно импортировать всё, что понадобится для запуска кода на IBM Quantum Experience.

			import sys
sys.path.append("../../")
from qiskit import QuantumProgram
import Qconfig
		

Этот код ссылается на файл Qconfig, который понадобится для создания учётной записи на сайте IBM. Не бойтесь, это бесплатно. Обратитесь к руководству по SDK IBM для получения более подробной информации.

Далее следует несколько стандартных импортов и некоторая информация об игре, выводящаяся на экран.

			d = input("Do you want to play on the real device? (y/n)\n")
if (d == "y"):
    device = 'ibmqx2'
else:
    device = 'local_qasm_simulator'
# заметьте, что значением для "device" должно быть 'ibmqx_qasm_simulator', 'ibmqx2' или 'local_qasm_simulator'
		

Игроку задаётся вопрос, в котором ему нужно ответить, какое устройство следует использовать для запуска квантовой части схемы. Существует три основных варианта. Один из них заключается в симулировании квантовых файлов на вашем собственном компьютере (local_qasm_simulator). Ещё один способ основывается на запросе к IBM на выполнение симуляции на их компьютерах (ibmqx_qasm_simulator). Также можете использовать реальное квантовое устройство с 5 кубитами: ibmqx2. Мы спрашиваем игрока, хочет ли он использовать квантовые устройства или симулировать выполнение кода локально. Этот процесс не настолько сложный, чтобы использовать мощности IBM для его выполнения.

На следующем этапе нужно добавить ещё одну важную переменную: количество запусков. Это нужно для сбора статистики.

			# давайте установим количество запусков (shots)
shots = 1024
		

Нет никаких определённых причин для использования числа 1024. Вы можете выбрать другое.

Теперь нужно перейти к настройке досок, на которых игрок будет выбирать позиции для трёх кораблей. Доступно всего 5 возможных позиций, соответствующих пяти кубитам на чипе IBM ibmqx2. Ниже представлена визуализация сетки.

			4       0
|\     /|
| \   / |
|  \ /  |
|   2   |
|  / \  |
| /   \ |
|/     \|
3       1
		

Числа — имена, используемые для каждой позиции. Они также являются именами, которые использует IBM. Например, 2 — позиция кубита q[2].

Выбор, сделанный игроками, хранится в shipPos, там же есть запись для каждого из двух игроков (player=0 — для первого игрока и player=1 — для второго) и для каждого из трёх кораблей, которые они размещают. Запись shipPos[player][ship] хранит позицию (0, 1, 2 , 3, 4) для корабля с номером, принадлежащим определённому игроку.

Получение выбора от игроков в основном осуществляется следующим образом:

			position = getpass.getpass(“Player “ + str(player+1) + “, choose a position for ship “ + str(ship+1) + “ (0, 1, 2, 3 or 4)\n” )
shipPos[player][ship] = position
		

Примечание Обратите внимание, что в статье приводятся блоки кода. Ссылка на полный код будет в конце статьи.

Этот код выводит следующее: «Player player, choose a position for ship ship+1 (0, 1, 2, 3 or 4)», а затем запрашивается ввод данных. Использование getpass не позволит другому игроку увидеть входные данные на своём экране. Заметьте, что первый корабль называется ship=0, но для обычных людей счёт начинается с 1, собственно, поэтому ship=0 — корабль 1 для людей. Эти две строки встроены в пару циклов над «игроками» и «кораблями» для извлечения всех необходимых значений. Они также фактически не находятся рядом друг с другом в реальном коде, так как между ними располагается другой код, который гарантирует, что игрок ввёл корректное значение. Возможно, ваш способ реализации будет лучше, поэкспериментируйте.

Теперь пришло время для основного цикла игры. На этом этапе задаётся вопрос о выборе места для атаки поля оппонента. Затем результат добавляется в запись bomb[player][position], которая подсчитывает, сколько раз игрок атаковал позицию в течение игры.

			position = input("Choose a position to bomb (0, 1, 2, 3 or 4)\n")
bomb[player][position] = bomb[player][position] + 1
		

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

Если мы пишем файл напрямую, то начать следует так:

			OPENQASM 2.0;
include "qelib1.inc";
qreg q[5];
creg c[5];
		

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

Вместо написания QASM файла напрямую, мы попросим Python сделать это за нас. Таким образом, вместо вышеупомянутого вступления нужно определить словарь, содержащий те же спецификации.

			Q_SPECS = {
        "circuits": [{
            "name": "gridScript",
            "quantum_registers": [{
                "name": "q",
                "size": 5
            }],
            "classical_registers": [{
                "name": "c",
                "size": 5
            }]}],
}
		

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

			# создайте программу с этими спецификациями
Q_program = QuantumProgram(specs=Q_SPECS) 
# обратитесь к схеме по имени
gridScript = Q_program.get_circuit("gridScript")
		

Здесь "gridScript" — то, что мы называем схемой в спецификациях. А gridScript — то, на что мы будем ссылаться при добавлении новых операций. Можно было бы использовать разные имена, но уже поздно.
Чтобы добавить операции над квантовыми и классическими битами, нужно определить, как мы будем ссылаться на них в коде.

			# обратитесь к квантовому регистру по имени
q = Q_program.get_quantum_registers("q")
# обратитесь к классическому регистру по имени
c = Q_program.get_classical_registers("c")
		

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

			u3( frac * pi,0,0) q[position];
		

Но при помощи замены frac и position на числа. Это можно сделать следующим образом на Python:

			gridScript.u3(frac * math.pi, 0.0, 0.0, q[position])
		

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

С точки зрения кубитов, это значит, что бомба, которая попадает по первому судну, применяет полное побитовое отрицание (NOT). Для бомбы, которая попадает по второму кораблю, применяется половинное побитовое отрицание, а при попадании по третьему кораблю применяется 1/3 побитового отрицания. Дробь frac определяется тем, какой корабль атакован. Реализуется таким образом: frac=1/(ship+1).

После завершения бомбардировки наступает время просмотра результатов. Для этого нужно добавить команды измерения. Каждый кубит в регистре измеряется и результаты копируются в соответствующий стандартный бит. Так это реализуется:

			for qubit in range(5):
    gridScript.measure(q[qubit], c[qubit])
		

Это создаёт строки кода на QASM:

			measure q[0] -> c[0];
measure q[1] -> c[1];
measure q[2] -> c[2];
measure q[3] -> c[3];
measure q[4] -> c[4];
		

В нашем распоряжении появился полноценный файл QASM, сохранённый как gridScript. Пришло время запустить его:

			# установите APIToken и API URL
Q_program.set_api(Qconfig.APItoken, Qconfig.config["url"])
# скомпилируйте и запустите QASM
Q_program.execute(["gridScript"], device, shots, wait=2, timeout=60)
		

Здесь device и shots были определены ранее, когда мы попросили игрока выбрать устройство. Прогресс будет обновляться каждую секунду простоя, а затем перестанет обновляться после таймаута.

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

После выполнения задания можно извлечь данные:

			grid[player] = Q_program.get_counts("gridScript")
		

Результаты копируются в grid  вместе с результатами бомбардировки кораблей игрока (player) в grid[player].

Результаты сохраняются в grid[player] в виде словаря. Ключами служат строки битов и поэтому выглядят примерно как 110. Самый правый бит — c[0], поэтому c[0]=0 в этой примерной строке битов. После обработки результатов мы можем определить, сколько запусков было у каждого результата (0 или 1) для каждого из обычных битов.

Мы интерпретируем дробь в виде процентного урона для корабля для тех моментов, когда кубит измерялся единицей. Нельзя ожидать 100%-го урона из-за влияния шумов (или погоды, если перенестись в нашу вселенную). Поэтому считаем, что урон превышает 95%, так как корабль утонул.
Давайте рассмотрим несколько примеров на выходе из квантового процесса. Предположим, что один игрок поставил свой третий корабль на позицию 2. Затем оппонент его бомбит. Результат будет примерно таким:

			{'00000': 734, '00100': 290}
		

Здесь во время 290 запусков из 1024 было найдено 1 в позиции 2, показывая, что корабль действительно был повреждён.

Другой пример: один игрок поместил свой корабль на позицию 1, а второй — на позицию 2. Оппонент бомбил позицию 1 в первом раунде и позицию 2 во втором раунде. Результаты были бы такими:

			{'00110': 532, '00010': 492}
		

Здесь все результаты имеют 1 во втором слоте справа (тот, который для позиции 1). Этот корабль был уничтожен одной бомбой. Второй на позиции 2 был повреждён только наполовину, поэтому половина результатов выдаётся с 1 для этого корабля.

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

Если произошла ошибка при извлечении файла QASM на квантовом чипе (или симуляторе), то на выходе вы увидите такое сообщение:

			{'status': 'Error'}
		

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

			if ( ( 'Error' in grid[0].values() ) or ( 'Error' in grid[1].values() ) ):
   print("\nThe process timed out. Try this round again.\n")
		

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

			?       ? 
 |\     /|
 | \   / |
 |  \ /  |
 |  50%  |
 |  / \  |
 | /   \ |
 |/     \|
 ?       ?
		

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

Вы могли заметить, что бомбардировка третьего корабля игрока даёт урон около 25%, поскольку этот корабль является третьим на пути к уничтожению. Вы, вероятно, ожидали, что урон будет составлять 33%. Но нет, тут дело в тригонометрии. Сейчас не будем на этом останавливаться.

Важно отметить, что сценарий повторяется каждый раз. Если время между очередями будет около одной минуты, но это почти вечность для кубитов. Даже при температуре в -273,15 градусов по Цельсию информация сгорела бы задолго до того, как пришла бы её очередь. Кроме того, наша потребность в извлечении информации также нарушит прекрасную квантовость в системе. По этим причинам любой процесс, который нуждается в человеческом взаимодействии, потребует, чтобы квантовые части повторялись с нуля для каждого нового ввода.

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

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

Следите за новыми постами
Следите за новыми постами по любимым темам
19К открытий20К показов