Перейти до основного вмісту
Цей проєкт у бета-версії. Знайшов(-ла) помилку? Відкрити issue на GitHub. Є запитання чи ідея? Запитати в Discussions — будемо раді твоєму відгуку!
Яка якість перекладу на цій сторінці?

Транспіляція бозонних схем

Цей посібник пояснює, як транспілювати BosonicCircuit у стандартну QuantumCircuit. Бозонна схема не містить ні усічення, ні кодування, ні кількості кубітів (див. Робота з бозонними схемами), тому саме транспілятор є місцем, де ці вибори робляться. Усічення та кодування описуються LevelSpec (див. Опис усічення за допомогою LevelSpec), а маппер, що перетворює оператори на кубітні оператори, описаний у Відображення представлень операторів. Результат — звичайна QuantumCircuit, тож її може виконати будь-який сумісний з Qiskit симулятор.

У посібнику скрізь використовується один невеликий приклад: дві бозонні моди, стан з одним бозоном у кожній і єдина часова еволюція під гамільтоніаном Бозе-Хаббарда, члени зв'язку та вузла якого лежать в окремих групах (див. Групування членів оператора: використання структури оператора):

>>> from qiskit_bosons.circuit import BosonicCircuit
>>> from qiskit_bosons.circuit.library import Evolution, InitializeModes
>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>>
>>> terms = {(cre(0), ann(1)): -1.0, (cre(1), ann(0)): -1.0}
>>> terms.update({(cre(mode), cre(mode), ann(mode), ann(mode)): 0.5 for mode in range(2)})
>>> hamiltonian = BosonOperator.from_dict(terms)
>>> # the bond is group 0 and the on-site interaction is group 1; derive the index from the term,
>>> # because the stored order of a dictionary is not specified
>>> hamiltonian.groups = [
... 0 if len({mode for _, mode in term}) == 2 else 1 for term, _ in hamiltonian.iter_terms()
... ]
>>>
>>> circuit = BosonicCircuit(2)
>>> circuit.append(InitializeModes([1, 1]), circuit.modes)
>>> circuit.append(Evolution(2, hamiltonian, time=0.1), circuit.modes)
>>> dict(circuit.count_ops())
{'InitializeModes': 1, 'Evolution': 1}

Етапи транспіляції​

Як пояснено в qiskit_bosons.transpiler, попередньо налаштовані менеджери проходів у цьому пакеті розбивають транспіляцію на такі етапи:

Вхід​

Перетворює схему на структуру даних орієнтованого ациклічного графа (DAG) — представлення, з яким працює кожен наступний прохід.

Оптимізація​

Залишає схему в бозонному просторі. Ці проходи використовують структуру бозонних операторів, зокрема їхні групи, тоді як інформації про кубіти ще немає. B2BSynthesis — це прохід, що тут розкладає вентилі, з плагіном для кожного типу вентиля: формула добутку або qDRIFT для Evolution і сітка Клементса для ModeRotation (про вибір методу див. Синтез вентиля Evolution у бозонному просторі). RelabelModes змінює порядок мод. Попередньо налаштований менеджер на цьому етапі декомпонує обертання мод, а все інше залишає як є.

Розкладка​

Відображає кожен бозонний регістр мод на квантовий регістр. Бозонна мода загалом займає кілька кубітів (dd з них за унарних кодувань, ⌈log⁡2d⌉\lceil \log_2 d \rceil за компактних), тож квантовий регістр ширший за регістр мод. OccupationB2QLayout виводить ширину регістра з LevelSpec.

Синтез​

Перетворює бозонні вентилі на кубітні операції, по одному плагіну на тип вентиля. Кодування відбувається на цьому кроці: плагін для вентиля Evolution відображає його оператор на кубітний оператор, а плагін для вентиля InitializeModes встановлює кубіти, що кодують кожне заповнення. Бар'єр переноситься на кожен кубіт кожного регістра, якого він торкався.

Кубіт​

Транспілює отриману кубітну схему власними методами Qiskit, наприклад до базисного набору вентилів або пристрою. Попередньо налаштований менеджер завжди його містить; типово це попередньо налаштований менеджер проходів Qiskit, який синтезує кожну еволюцію у вентилі (див. Продовження на кубітному рівні).

Вихід​

Перетворює внутрішню структуру даних DAG назад на QuantumCircuit.

Усічення й кодування не зберігаються в схемі. Їх передають транспілятору, і розкладка, підготовка стану та маппер мають бути узгоджені щодо них. Попередньо налаштований менеджер забезпечує це, передаючи той самий LevelSpec усім трьом.

Практичний приклад​

generate_preset_occupation_pass_manager() будує конвеєр зі специфікації. Зниження рівня прикладу під двома кодуваннями змінює лише специфікацію. Попередньо налаштований менеджер завершується власною кубітною транспіляцією Qiskit, яка синтезувала б еволюцію у вентилі. Щоб побачити схему саме такою, як її видають бозонні етапи, спочатку спорожни цей етап:

>>> from qiskit.transpiler import PassManager
>>> from qiskit_bosons.mappers import LevelSpec
>>> from qiskit_bosons.transpiler.presets import generate_preset_occupation_pass_manager
>>>
>>> for encoding in ("unary_unprojected", "gray"):
... pass_manager = generate_preset_occupation_pass_manager(LevelSpec(2, encoding))
... pass_manager.qubit = PassManager()
... lowered = pass_manager.run(circuit)
... print(f"{encoding:18s} qubits={lowered.num_qubits} ops={dict(lowered.count_ops())}")
unary_unprojected qubits=6 ops={'x': 2, 'PauliEvolution': 1}
gray qubits=4 ops={'x': 2, 'PauliEvolution': 1}

Ці два кодування використовують відповідно шість і чотири кубіти (три рівні на моду за унарного кодування, два кубіти на моду за коду Грея), але структура схеми однакова: дві операції XGate готують заповнення, а один PauliEvolutionGate несе еволюцію.

Порівняння з традиційним робочим процесом​

Посібник про бозонні схеми описує традиційний робочий процес, який відображає гамільтоніан на кубітний оператор перед тим, як будувати QuantumCircuit. Обидва шляхи дають еквівалентні схеми. Вони відрізняються тим, що потрібно переробляти, коли змінюється усічення. Бозонну схему будують один раз і знижують на кожному обрізанні, тоді як шлях «спершу відображення» потребує повторного відображення оператора й перебудови схеми:

>>> from qiskit import QuantumCircuit
>>> from qiskit.circuit.library import PauliEvolutionGate
>>> from qiskit_bosons.mappers.library import occupation_encoding
>>>
>>> basis = ["rz", "sx", "x", "cx"]
>>> cx_counts = []
>>> for max_occupation in (1, 2, 3):
... spec = LevelSpec(max_occupation, "gray")
... # the bosonic circuit is reused as it is
... lowered = generate_preset_occupation_pass_manager(
... spec, optimization_level=0, basis_gates=basis
... ).run(circuit)
... # the map-first route maps the operator and builds a new circuit for every cutoff
... qubit_hamiltonian = occupation_encoding(hamiltonian, spec)
... by_hand = QuantumCircuit(qubit_hamiltonian.num_qubits)
... _ = by_hand.append(PauliEvolutionGate(qubit_hamiltonian, time=0.1), by_hand.qubits)
... cx_counts.append(lowered.count_ops().get("cx", 0))
... print(
... f"d={max_occupation + 1}: {lowered.num_qubits} qubits; map-first: "
... f"{len(qubit_hamiltonian)} Pauli terms to place by hand"
... )
d=2: 2 qubits; map-first: 2 Pauli terms to place by hand
d=3: 4 qubits; map-first: 10 Pauli terms to place by hand
d=4: 4 qubits; map-first: 22 Pauli terms to place by hand

Кількість заплутувальних вентилів залежить від того, як Qiskit синтезує вентиль еволюції, що змінюється між випусками, тому побудуй графік:

CX count of the lowered circuit against the number of levels d, growing quickly with d.

Кількість швидко зростає з обрізанням, і саме тому варто зробити обрізання параметром транспіляції, а не схеми. Обидва шляхи тут еквівалентні, але лише бозонний може також чергувати групування оператора з формулою добутку (див. Синтез вентиля Evolution у бозонному просторі) або передати кодування іншій специфікації, не змінюючи жодного рядка схеми.

Специфікація з єдиним обрізанням залишає кількість мод відкритою, тож один менеджер проходів обслуговує схеми будь-якої ширини. Специфікація з одним обрізанням на моду фіксує ширину:

>>> wide = BosonicCircuit(3)
>>> wide.append(InitializeModes([1, 0, 1]), wide.modes)
>>> shared = generate_preset_occupation_pass_manager(LevelSpec(2, "gray"))
>>> shared.run(wide).num_qubits
6
>>> per_mode = generate_preset_occupation_pass_manager(LevelSpec([2, 1], "gray"))
>>> per_mode.run(circuit).num_qubits
3
>>> try:
... per_mode.run(wide)
... except ValueError as error:
... print(error)
the spec covers 2 modes, but 3 were expected

Побудова конвеєра вручну​

Пресет — це MultiStagePassManager з одним записом на кожну стадію. Той самий конвеєр можна зібрати з його частин, і це відправна точка для кожного налаштування нижче:

>>> from qiskit.passmanager import MultiStagePassManager
>>> from qiskit_bosons.mappers.library import occupation_encoding
>>> from qiskit_bosons.transpiler import BosonicCircuitToDAG, QuantumDAGToCircuit
>>> from qiskit_bosons.transpiler.passes import (
... B2QSynthesis,
... MapperFnEvolutionSynthesis,
... OccupationB2QLayout,
... OccupationInitializeModesSynthesis,
... )
>>>
>>> spec = LevelSpec(2, "gray")
>>>
>>> def mapper_fn(operator, num_qubits):
... return occupation_encoding(operator, spec, num_qubits)
>>>
>>> synthesis = B2QSynthesis()
>>> synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(mapper_fn)
>>> synthesis.methods["InitializeModes"] = OccupationInitializeModesSynthesis(spec)
>>>
>>> by_hand = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>> preset = generate_preset_occupation_pass_manager(spec)
>>> preset.qubit = PassManager()
>>> by_hand.run(circuit) == preset.run(circuit)
True

Функція-мапер отримує оператор і ширину регістра, яку визначив layout, та повертає SparseObservable. Оскільки їй передається ширина, передавання num_qubits у occupation_encoding() змушує мапер перевіряти, що специфікація вміщується в регістр, тому невідповідність між layout і кодуванням завершується зрозумілим повідомленням.

Розширені робочі процеси​

Надайте явний layout​

CustomB2QLayout замінює стадію layout відображенням, яке ти надаєш, з кожного бозонного регістра схеми на квантовий регістр. Це корисно, коли регістр має бути ширшим, ніж потрібно специфікації, наприклад щоб залишити вільні кубіти для інших цілей:

>>> from qiskit.circuit import QuantumRegister
>>> from qiskit_bosons.transpiler.passes import CustomB2QLayout
>>>
>>> (mode_register,) = BosonicCircuitToDAG().run(circuit).qregs.values()
>>> custom = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... layout=CustomB2QLayout({mode_register: QuantumRegister(6, "q")}),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>> custom.run(circuit).num_qubits
6
Попередження

Власний layout — це твоя відповідальність. Плагіни синтезу все одно адресують кубіти через специфікацію, тож регістр, що не відповідає ширинам специфікації, дає хибну схему або помилку глибоко всередині плагіна. Власний layout також пропускає перевірку, яку OccupationB2QLayout виконує після RelabelModes (див. наступний розділ).

Змініть порядок мод​

RelabelModes — це прохід стадії оптимізації, що змінює порядок мод. Перестановка читається як відображення від початкового індексу моди до нового і записується в метадані схеми. Зміна порядку змінює, які кубіти займає кожна мода, тож вона може зменшити глибину знижуваної схеми.

Оскільки результат виражено в новому порядку мод, виміряні бітові рядки потрібно відобразити назад, перш ніж їх інтерпретувати. Якщо на моду припадає кілька кубітів, відображення діє на блоки кубітів, а не на окремі біти. Наступний приклад перейменовує три моди, кожна з яких займає два кубіти за кодом Грея, і відновлює початкове заповнення з виміряних підрахунків:

>>> # BasicSimulator is a convenient pure-Python choice; any simulator works
>>> from qiskit.providers.basic_provider import BasicSimulator
>>> from qiskit_bosons.transpiler.passes import RelabelModes
>>>
>>> state = BosonicCircuit(3)
>>> state.append(InitializeModes([2, 0, 1]), state.modes)
>>> spec = LevelSpec(2, "gray") # three levels, two qubits per mode
>>> width = spec.resolve(3).widths[0]
>>> permutation = [2, 0, 1]
>>>
>>> synthesis = B2QSynthesis()
>>> synthesis.methods["InitializeModes"] = OccupationInitializeModesSynthesis(spec)
>>> relabeled = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... optimization=RelabelModes(permutation),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... ).run(state)
>>> relabeled.metadata
{'permutation': [2, 0, 1]}
>>>
>>> def restore(bitstring, permutation, width):
... """Map a bitstring in the new mode order back to the original mode order."""
... qubits = bitstring[::-1] # character q is now the value of qubit q
... blocks = [qubits[mode * width : (mode + 1) * width] for mode in range(len(permutation))]
... return "".join(blocks[new] for new in permutation)[::-1]
>>>
>>> relabeled.measure_all()
>>> (measured,) = BasicSimulator().run(relabeled, shots=1).result().get_counts()
>>> measured
'110100'
>>> restore(measured, permutation, width)
'010011'

Відновлений бітовий рядок — це те, що схема дає без перейменування, прочитане в little-endian порядку Qiskit: мода 0 містить код Грея рівня два (11), мода 1 — рівня нуль (00), а мода 2 — рівня один (01).

>>> plain = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... ).run(state)
>>> plain.measure_all()
>>> (reference,) = BasicSimulator().run(plain, shots=1).result().get_counts()
>>> restore(measured, permutation, width) == reference
True
Попередження

Специфікацію, що задає модам різні обрізання, не можна поєднувати з RelabelModes. Обрізання прив'язані до мод за їхньою позицією, тому зміна порядку мод поставила б обрізання на неправильну моду. OccupationB2QLayout відхиляє перейменовану схему в такому випадку:

>>> mixed = LevelSpec([2, 1, 2], "gray")
>>> synthesis = B2QSynthesis()
>>> synthesis.methods["InitializeModes"] = OccupationInitializeModesSynthesis(mixed)
>>> refused = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... optimization=RelabelModes(permutation),
... layout=OccupationB2QLayout(mixed),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>> try:
... refused.run(state)
... except ValueError as error:
... print(str(error).split(",")[0])
the circuit has been relabeled by RelabelModes

Оберіть плагіни синтезу​

B2QSynthesis не має плагінів за замовчуванням; пресет реєструє по одному на кожен тип гейта. Доступними плагінами керує B2QSynthesisPluginManager, який перелічує їх за назвою гейта, на який вони діють:

>>> from qiskit_bosons.transpiler.passes import B2QSynthesisPluginManager
>>> manager = B2QSynthesisPluginManager()
>>> for gate in manager.op_names():
... print(gate, manager.method_names(gate))
Evolution ['MapperFn']
InitializeModes ['Occupation']

Два вбудовані методи — Evolution.MapperFn та InitializeModes.Occupation. Перший синтезує еволюцію з будь-якої функції-мапера, і саме так підключається власне кодування (див. Відображення подань операторів). Бібліотека не постачає плагін для гейта ModeRotation. Пресет спершу розкладає його в бозонному просторі на гейти Evolution, які потім обробляє перший метод, але плагін, зареєстрований для ModeRotation, може натомість знизити гейт безпосередньо; див. Обертання бозонних мод та Зниження обертання без розкладання. Метод можна зареєструвати за назвою через аргумент config у B2QSynthesis або записати об'єкт плагіна безпосередньо в B2QSynthesis.methods, як у попередніх прикладах.

Продовжте на кубітному рівні​

Пресет завжди закінчується стадією qubit, яка є власним пресетним pass manager Qiskit. Іменовані аргументи generate_preset_occupation_pass_manager() передаються в qiskit.transpiler.generate_preset_pass_manager(), тож можна транслювати в базисний набір гейтів або орієнтуватися безпосередньо на пристрій. Віддавай перевагу optimization_level=0, щоб схема лишалася близькою до того, що створили бозонні стадії:

>>> from qiskit.transpiler import generate_preset_pass_manager
>>>
>>> spec = LevelSpec(2, "gray")
>>> translated = generate_preset_occupation_pass_manager(
... spec, optimization_level=0, basis_gates=["rz", "sx", "x", "cx"]
... ).run(circuit)
>>> sorted(translated.count_ops())
['cx', 'rz', 'sx', 'x']

Поширені ще два варіанти. Призначте порожній PassManager стадії qubit, коли хочете показати схему точно такою, якою її синтезував конвеєр, як у прикладах вище. Або зберіть конвеєр самостійно й додайте будь-який pass manager Qiskit як стадію qubit у MultiStagePassManager. Стадії виконуються в заданому порядку, тому стадія qubit має передувати output, адже проходи Qiskit очікують DAG, а не готову схему:

>>> synthesis = B2QSynthesis()
>>> synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(
... lambda operator, num_qubits: occupation_encoding(operator, spec, num_qubits)
... )
>>> synthesis.methods["InitializeModes"] = OccupationInitializeModesSynthesis(spec)
>>> staged = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... qubit=generate_preset_pass_manager(
... optimization_level=0, basis_gates=["rz", "sx", "x", "cx"]
... ),
... output=QuantumDAGToCircuit(),
... )
>>> sorted(staged.run(circuit).count_ops())
['cx', 'rz', 'sx', 'x']

Наступні кроки​