Квантовая монетка на IBM Quantum: как я переложил выбор свидания на 8 кубитов

от автора

Когда не можешь выбрать, куда пойти на свидание, можно мучиться между кофейней, баром и прогулкой. А можно поступить взросло: отправить задачу на квантовый компьютер IBM и переложить ответственность на физику. Внутри — Qiskit, 8 кубитов, реальный job_id и самый пафосный способ заменить подбрасывание обычной монетки.

GitHub репо по ссылке

Да, это максимально избыточный способ заменить random.choice(). В этом и смысл. Зато в конце будет настоящий job_id, запуск на IBM Quantum и моральное право сказать: «Это не я выбрал. Это квантовая механика так решила».

Что понадобится

  1. Python и библиотека qiskit

    pip install qiskit qiskit-aer qiskit-ibm-runtime
  2. Бесплатный токен IBM Quantum. Регистрируетесь на quantum.ibm.com, получаете токен, копируете в переменную окружения IBM_QUANTUM_TOKEN. Всё.

Код

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

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

Код под спойлером:
```pythonimport osimport sysimport secretsfrom dataclasses import dataclassfrom collections import Counter# pip install qiskit qiskit-aer qiskit-ibm-runtime## Для реального IBM Quantum:#   export IBM_QUANTUM_TOKEN="ваш_api_key"## Опционально, но желательно:#   export IBM_QUANTUM_INSTANCE="ваш_CRN_или_service_name"SHOTS = 256QUBITS = 8REGISTER_NAME = "coin"# ========================# ВАШ СПИСОК ВАРИАНТОВ# ========================OPTIONS = [    "Кофейня",    "Бар",    "Кино",    "Прогулка",    "Не идти никуда и наконец-то выспаться",]# ========================@dataclass(frozen=True)class CoinRun:    bitstrings: list[str]    counts: dict[str, int]    backend: str    job_id: str | None = Nonedef validate_options() -> None:    if not OPTIONS:        raise ValueError("Список OPTIONS пуст. Даже квантовая механика тут бессильна.")    max_values = 2 ** QUBITS    if len(OPTIONS) > max_values:        raise ValueError(            f"Слишком много вариантов: {len(OPTIONS)}. "            f"При {QUBITS} кубитах доступно максимум {max_values} базовых значений."        )def make_circuit():    from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister    q = QuantumRegister(QUBITS, "q")    c = ClassicalRegister(QUBITS, REGISTER_NAME)    qc = QuantumCircuit(q, c, name="quantum_coin")    # Каждый кубит отправляем в состояние, где при измерении    # 0 и 1 выпадают примерно с равной вероятностью.    for qubit in q:        qc.h(qubit)    qc.measure(q, c)    return qcdef get_job_id(job) -> str | None:    job_id = getattr(job, "job_id", None)    if callable(job_id):        return job_id()    if job_id:        return str(job_id)    return Nonedef get_bits_local() -> CoinRun:    """    Локальный режим:    1. Сначала пробуем AerSimulator.    2. Если qiskit-aer не установлен — используем secrets как честный fallback.    """    try:        from qiskit_aer import AerSimulator        qc = make_circuit()        simulator = AerSimulator()        job = simulator.run(qc, shots=SHOTS, memory=True)        result = job.result()        bitstrings = result.get_memory(qc)        counts = result.get_counts(qc)        return CoinRun(            bitstrings=bitstrings,            counts=dict(counts),            backend="AerSimulator локально",            job_id=get_job_id(job),        )    except ImportError:        bitstrings = [            "".join(secrets.choice("01") for _ in range(QUBITS))            for _ in range(SHOTS)        ]        return CoinRun(            bitstrings=bitstrings,            counts=dict(Counter(bitstrings)),            backend="Python secrets fallback",            job_id=None,        )def get_bits_ibm() -> CoinRun:    from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler    from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager    token = os.getenv("IBM_QUANTUM_TOKEN")    instance = os.getenv("IBM_QUANTUM_INSTANCE")    if not token:        raise RuntimeError(            "Токен не найден. Укажите IBM_QUANTUM_TOKEN в переменной окружения."        )    service_kwargs = {        "channel": "ibm_quantum_platform",        "token": token,    }    if instance:        service_kwargs["instance"] = instance    service = QiskitRuntimeService(**service_kwargs)    backend = service.least_busy(        operational=True,        simulator=False,        min_num_qubits=QUBITS,    )    qc = make_circuit()    pass_manager = generate_preset_pass_manager(        backend=backend,        optimization_level=1,    )    isa_circuit = pass_manager.run(qc)    sampler = Sampler(mode=backend)    job = sampler.run([isa_circuit], shots=SHOTS)    job_id = get_job_id(job)    print(f"\nОтправили задачу на IBM Quantum backend: {backend.name}")    if job_id:        print(f"Job ID: {job_id}")    print("Ждём ответ от Вселенной...")    result = job.result()    pub_result = result[0]    register_data = getattr(pub_result.data, REGISTER_NAME)    bitstrings = register_data.get_bitstrings()    counts = register_data.get_counts()    return CoinRun(        bitstrings=bitstrings,        counts=dict(counts),        backend=backend.name,        job_id=job_id,    )def choose_option(bitstrings: list[str]) -> tuple[str, int, str, int]:    """    Выбираем вариант по одному измеренному битстрингу.    Важно:    простой value % len(OPTIONS) может давать небольшой перекос,    если число вариантов не делит 2**QUBITS без остатка.    Поэтому используем rejection sampling:    берём только значения из диапазона, который ровно делится    на количество вариантов.    """    variants_count = len(OPTIONS)    quantum_space = 2 ** QUBITS    usable_space = (quantum_space // variants_count) * variants_count    for bitstring in bitstrings:        value = int(bitstring, 2)        if value < usable_space:            index = value % variants_count            return OPTIONS[index], index, bitstring, value    # Практически сюда попасть почти невозможно при SHOTS=256,    # но пусть fallback будет.    index = secrets.randbelow(variants_count)    return OPTIONS[index], index, "fallback", indexdef print_top_counts(counts: dict[str, int], selected_bitstring: str) -> None:    if not counts:        return    print("\nСтатистика измерений:")    print("-" * 40)    selected_count = counts.get(selected_bitstring, 0)    total = sum(counts.values())    if total > 0 and selected_count > 0:        selected_percent = selected_count / total * 100        print(            f"Выбранный битстринг встретился {selected_count} раз "            f"из {total} ({selected_percent:.2f}%)."        )    print("\nТоп-5 самых частых битстрингов:")    for bitstring, count in sorted(        counts.items(),        key=lambda item: item[1],        reverse=True,    )[:5]:        print(f"  {bitstring}: {count}")def main() -> None:    validate_options()    print("\n⚛️  КВАНТОВАЯ МОНЕТКА ⚛️")    print("=" * 40)    print(f"Кубитов: {QUBITS}")    print(f"Измерений: {SHOTS}")    print(f"Вариантов: {len(OPTIONS)}")    print("=" * 40)    use_ibm = input("Дёрнуть реальный IBM Quantum? (y/n): ").strip().lower() == "y"    if use_ibm:        try:            run = get_bits_ibm()            print(f"\n✅ Ответ получен с backend: {run.backend}")        except Exception as error:            print(f"\n❌ IBM Quantum не ответил: {error}")            print("Переключаемся на локальный режим...")            run = get_bits_local()    else:        run = get_bits_local()        print(f"\n💻 Считаем локально: {run.backend}")    choice, index, selected_bitstring, raw_value = choose_option(run.bitstrings)    print("\n🎲 РЕЗУЛЬТАТ 🎲")    print("=" * 40)    print(f"Источник: {run.backend}")    if run.job_id:        print(f"Job ID: {run.job_id}")    print(f"Битстринг: {selected_bitstring}")    print(f"Число: {raw_value}")    print(f"Вариант: {index + 1} из {len(OPTIONS)}")    print(f"ВЫБОР: {choice}")    print("=" * 40)    print_top_counts(run.counts, selected_bitstring)    print(        "\nЭто всё ещё максимально избыточная замена random.choice(). "        "Но теперь с кубитами, job_id и чувством научной важности."    )if __name__ == "__main__":    try:        main()    except KeyboardInterrupt:        print("\nВыбор отменён. Судьба подождёт.")        sys.exit(130)```

Не забудьте выставить ключ и запустить

$env:IBM_QUANTUM_TOKEN="ваштокен"python quantum.py

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

ну значит идем в кино)

ну значит идем в кино)

IBM в панели отчитался о выполненной задаче.

Как это работает

Честно — сначала сам не знал, но разобрался.

Схема такая:

  1. Создаём 8 кубитов.

  2. На каждый кубит применяем гейт Адамара. Если совсем грубо: после этого при измерении каждый кубит может дать 0 или 1 с примерно равной вероятностью.

  3. Измеряем схему 256 раз и получаем набор битстрингов вроде 01010110, 11100001, 00011010.

  4. Берём один реально измеренный битстринг.

  5. Превращаем его в число.

  6. Маппим число на индекс в списке вариантов.

Какие списки можно подставить

```python# Мягкий режим: написать или не написатьOPTIONS = [    "Написать Ане",    "Написать Кате",    "Написать Маше",    "Написать Лене",    "Не писать никому и героически лечь спать в 23:00",]# Свидание без выбора людей как пунктов менюOPTIONS = [    "Позвать в кофейню",    "Позвать в бар",    "Позвать в кино",    "Позвать просто погулять",    "Не устраивать социальный эксперимент и спокойно пережить вечер",]# Режим «я голодный, но решения принимать не способен»OPTIONS = [    "Пицца",    "Суши",    "Бургер",    "Шаурма",    "Гречка: скучно, зато без архитектурных рисков",]# Для тех, кто в субботу за ноутомOPTIONS = [    "Закрыть баг",    "Написать тесты",    "Дописать README",    "Рефакторить то, что никто не просил трогать",    "Пойти гулять, пока проект не превратился в свой фреймворк",]# Для разработчика перед релизомOPTIONS = [    "Задеплоить и сделать вид, что всё под контролем",    "Сначала всё-таки прогнать тесты",    "Посмотреть логи и пожалеть об этом",    "Откатиться, пока никто не заметил",    "Сказать: «у меня локально работало»",]# Для выбора пет-проектаOPTIONS = [    "Сделать маленькую полезную утилиту",    "Начать новый SaaS и страдать",    "Написать плагин для IDE",    "Сделать локальный AI-инструмент",    "Закрыть ноутбук и не плодить ещё один репозиторий",]```

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

В комментариях жду

  • Ваши самые безумные списки OPTIONS

  • Скриншоты job’ов из IBM Quantum

  • Священные холивары, куда без них 🙂

Любые претензии к выбору направлять не мне, а в Институт Нильса Бора. Я только скрипт написал 🙂

GitHub репо по ссылке

Удачи в экспериментах!

ссылка на оригинал статьи https://habr.com/ru/articles/1048848/