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

Симуляція динаміки Бозе-Хаббарда з вибором кодування

Цей посібник будує схему для часової динаміки одновимірної моделі Бозе-Хаббарда: від запису гамільтоніана до перевірки еволюціонованих густин на вузлах за точним еталоном. Цей шлях цікавий однією ідеєю:

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

Для бозонів кодування майже завжди потрібно відкладати. Бозонна мода може містити довільну кількість частинок, тому її простір Фока нескінченновимірний, і жодного кодування не існує, доки ти спершу не оберешь обрізання dd (кількість рівнів заповнення, які ти зберігаєш). У коді обрізання задається як найбільше заповнення, яке може містити мода, max_occupation =d−1= d - 1, тож мода, що зберігає рівні 0,1,20, 1, 2, має max_occupation = 2 і d=3d = 3.

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

Дивись також

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

Обрізання та його ціна​

Коли ти обрізаєш моду до dd рівнів, ти порушуєш канонічний комутатор на найвищому збереженому рівні [1], тож жодне кодування не є ізоморфізмом алгебр: бозонне кодування відображає стани та оператори, що діють усередині обрізаного підпростору. Два наслідки важливі для всього подальшого:

  1. dd має обмежувати заповнення, досяжне в ході динаміки, а не лише заповнення початкового стану.

  2. Відображення не комутує з нормальним упорядкуванням, тож один раз визнач, чи заданий твій гамільтоніан у нормально впорядкованій формі, і дотримуйся цього послідовно.

Посібник з мапперів виводить дефект і показує обидва наслідки на працюючому коді.

1. Гамільтоніан​

Модель Бозе-Хаббарда на ґратці зі зв'язками ⟨ij⟩\langle i j \rangle має вигляд

H=−t∑⟨ij⟩(bi†bj+bj†bi)+U2∑ini(ni−1) ,H = -t \sum_{\langle i j \rangle} \left(b^\dagger_i b_j + b^\dagger_j b_i\right) + \frac{U}{2} \sum_i n_i (n_i - 1) \, ,

де ni=bi†bin_i = b^\dagger_i b_i. Взаємодія враховує пари бозонів на вузлі, тож вузол має містити дві частинки одночасно. У нормально впорядкованому вигляді ni(ni−1)=bi†bi†bibin_i(n_i-1) = b^\dagger_i b^\dagger_i b_i b_i, і саме ця форма використовується далі.

Геометрія входить лише через список ребер, тож нічого не прив'язано до ланцюжка:

>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>>
>>> def bose_hubbard(edges, num_sites, tunneling, interaction):
... """Build the normal-ordered Bose-Hubbard Hamiltonian on an arbitrary lattice."""
... terms = {}
... for i, j in edges:
... terms[(cre(i), ann(j))] = -tunneling
... terms[(cre(j), ann(i))] = -tunneling
... for i in range(num_sites):
... # U/2 * n_i (n_i - 1), already normal-ordered
... terms[(cre(i), cre(i), ann(i), ann(i))] = 0.5 * interaction
... return BosonOperator.from_dict(terms)
>>>
>>> num_sites = 4
>>> edges = [(i, i + 1) for i in range(num_sites - 1)]
>>> hamiltonian = bose_hubbard(edges, num_sites, tunneling=1.0, interaction=2.0)
>>> print(f"Number of terms: {len(hamiltonian)}")
Number of terms: 10

Варто спершу перевірити дві властивості. Гамільтоніан має бути ермітовим, щоб e−itHe^{-itH} було унітарним, і він зберігає повну кількість бозонів:

>>> hamiltonian.is_hermitian()
True
>>> hamiltonian.conserves_particle_number()
True

Тепер бозонне питання: скільки рівнів потребує цей оператор?

>>> hamiltonian.max_occupation()
2

Член взаємодії застосовує два оператори народження до однієї моди, тож мода має містити два бозони, щоб цей член узагалі діяв. Це максимальне заповнення 2, і саме це число приймає LevelSpec як max_occupation. Мода, що зберігає заповнення 0,1,20, 1, 2, має d=max_occupation+1=3d = \texttt{max\_occupation} + 1 = 3 рівні, тож max_occupation >= 2 – та сама вимога, що й d≥3d \geq 3.

Це лише нижня межа: вона випливає з одного застосування оператора, а повторне застосування під час еволюції в часі може сягати далі. Вона відсікає обрізання, які напевно замалі, а крок 6 визначає, наскільки великим має бути обрізання для цієї моделі та стану. mode_occupations() повідомляє межу для кожної моди окремо, з нею й порівнюється обрізання для кожної моди.

2. Групування перед відображенням​

Еволюція тротеризується, і те, як розбито гамільтоніан, визначає, що отримує синтез. Два міркування вказують в один бік.

По-перше, фізичне: окремий член переходу bi†bjb^\dagger_i b_j не є ермітовим. Ермітовою є лише пара bi†bj+bj†bib^\dagger_i b_j + b^\dagger_j b_i. Якщо еволюціонувати під одним неермітовим членом, операція не є унітарною, а відображений оператор мав би комплексні коефіцієнти, які PauliEvolutionGate відхиляє. Тому два члени переходу кожного зв'язку мають залишатися разом.

По-друге, щодо ефективності: усі внутрішньовузлові члени взаємодії діагональні в базисі заповнення та взаємно комутують, тож вони можуть ділити одну групу й синтезуватися разом. Те саме стосується зв'язків, що не торкаються спільного вузла. На ланцюжку кожен другий зв'язок утворює таку множину: зв'язки 0 і 2 діють на неперетинні пари мод, тож їхні члени переходу комутують і можуть ділити групу, а зв'язок 1 утворює власну групу. Якщо згрупувати парні та непарні зв'язки, потрібні три групи, тоді як одна група на зв'язок потребує чотирьох. Менше груп означає менше множників, а також меншу глибину схеми: з кодом Грея за max_occupation = 2 двокубітна глибина становить 95 проти 139. Ґратка визначає, які зв'язки можуть ділити групу, тому розбиття обирається тут.

Атрибут groups переносить це рішення в схему: вентиль Evolution над згрупованим оператором розкладається на одну підеволюцію на групу. Розбиття обирається на бозонному рівні, поки структура ґратки ще видима, і воно залишається незмінним крізь відображення. Посібник з групування описує перевірки, які має проходити групування.

>>> def group_of(actions, edges):
... """Assign each term to the even bonds, the odd bonds, or the on-site group."""
... creations = sorted(mode for action, mode in actions if action)
... annihilations = sorted(mode for action, mode in actions if not action)
... if creations == annihilations:
... return 2 # diagonal: the on-site interaction group
... bond = tuple(sorted(set(creations) | set(annihilations)))
... return edges.index(bond) % 2 # bonds 0 and 2 share a group, bond 1 has its own
>>>
>>> hamiltonian.groups = [group_of(actions, edges) for actions, _ in hamiltonian.iter_terms()]
>>> hamiltonian.num_groups()
3

Кожна група сама має бути ермітовою, що й забезпечує попереднє парування. groups_are_hermitian() перевіряє всі групи одним викликом:

>>> from qiskit_bosons.operators.terms import groups_are_hermitian
>>> groups_are_hermitian(hamiltonian)
[True, True, True]

Щоб побачити, що містить кожна група, розділи групи:

>>> for index, group in enumerate(hamiltonian.split_out_groups()):
... print(f"group {index}: {len(group)} terms, hermitian={group.is_hermitian()}")
group 0: 4 terms, hermitian=True
group 1: 2 terms, hermitian=True
group 2: 4 terms, hermitian=True

Групування на парні й непарні зв'язки – це вибір, і альтернативу легко виміряти. Якщо дати кожному зв'язку власну групу, потрібні чотири групи, і та сама схема тоді коштує більше двокубітної глибини. Глибина рахується після зниження схеми з кодом Грея за max_occupation = 2, що пояснює крок 4:

>>> from qiskit.transpiler import PassManager
>>> from qiskit_bosons.circuit import BosonicCircuit
>>> from qiskit_bosons.circuit.library import Evolution
>>> from qiskit_bosons.mappers import LevelSpec
>>> from qiskit_bosons.transpiler.presets import generate_preset_occupation_pass_manager
>>>
>>> def two_qubit_depth(operator):
... """The two-qubit depth of one evolution under ``operator``, lowered with the Gray code."""
... probe = BosonicCircuit(num_sites)
... probe.append(Evolution(num_sites, operator, time=0.4), probe.modes)
... pass_manager = generate_preset_occupation_pass_manager(LevelSpec(2, "gray"))
... pass_manager.qubit = PassManager()
... lowered = pass_manager.run(probe.decompose())
... return lowered.decompose(reps=6).depth(lambda instruction: len(instruction.qubits) == 2)
>>>
>>> per_bond = bose_hubbard(edges, num_sites, tunneling=1.0, interaction=2.0)
>>> per_bond.groups = [
... len(edges) if sorted(m for a, m in actions if a) == sorted(m for a, m in actions if not a)
... else edges.index(tuple(sorted({m for _, m in actions})))
... for actions, _ in per_bond.iter_terms()
... ]
>>> per_bond.num_groups(), two_qubit_depth(per_bond)
(4, 139)
>>> hamiltonian.num_groups(), two_qubit_depth(hamiltonian)
(3, 95)

3. Бозонна схема​

Зверни увагу, чого в схемі немає. У ній немає ні обрізання, ні кодування, ні кількості кубітів. Вона діє на чотири бозонні моди:

>>> from qiskit_bosons.circuit import BosonicCircuit
>>> from qiskit_bosons.circuit.library import Evolution, InitializeModes
>>>
>>> total_time = 0.4
>>> occupation = [1, 1, 0, 0] # one boson on each of the first two sites
>>>
>>> circuit = BosonicCircuit(num_sites)
>>> circuit.append(InitializeModes(occupation), circuit.modes)
>>> circuit.append(Evolution(num_sites, hamiltonian, time=total_time), circuit.modes)
>>> dict(circuit.count_ops())
{'InitializeModes': 1, 'Evolution': 1}

A BosonicCircuit on four modes with an InitializeModes gate followed by one Evolution gate.

Лінії – це бозонні моди, а не кубіти. Зауваж, що InitializeModes приймає цілі числа, а не прапорці: мода може містити будь-яку кількість бозонів, тож стан Фока – це список чисел заповнення.

Один виклик decompose() перетворює групування з кроку 2 на один вентиль на групу:

>>> decomposed = circuit.decompose()
>>> dict(decomposed.count_ops())
{'Evolution': 3, 'InitializeModes': 1}

The decomposed BosonicCircuit with one Evolution gate per group.

Перша група містить зв'язки на модах 0 і 1 та на модах 2 і 3, тож її вентиль охоплює всі чотири моди, і так само внутрішньовузлова група. Середній зв'язок має власний вентиль на модах 1 і 2.

Пропусти це розкладання, і групування просто ігнорується. Те, який метод виконує розбиття, визначає synthesis вентиля, за замовчуванням це BosonicLieTrotter; альтернативи показує Синтез вентиля Evolution у бозонному просторі.

4. Обери кодування під час транспіляції​

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

>>> from qiskit.transpiler import PassManager
>>> from qiskit_bosons.mappers import LevelEncoding, LevelSpec
>>> from qiskit_bosons.transpiler.presets import generate_preset_occupation_pass_manager
>>>
>>> max_occupation = 2 # the largest occupation per mode, so d = max_occupation + 1 = 3 levels
>>>
>>> encodings = (
... LevelEncoding.UNARY_PROJECTED,
... LevelEncoding.UNARY_UNPROJECTED,
... LevelEncoding.STANDARD_BINARY,
... LevelEncoding.GRAY,
... )
>>> for encoding in encodings:
... pass_manager = generate_preset_occupation_pass_manager(
... LevelSpec(max_occupation, encoding)
... )
... pass_manager.qubit = PassManager() # keep the circuit as the pipeline synthesized it
... qubit_circuit = pass_manager.run(decomposed)
... basis = qubit_circuit.decompose(reps=6)
... two_qubit = basis.depth(lambda instruction: len(instruction.qubits) == 2)
... print(f"{encoding!s:18s} qubits={qubit_circuit.num_qubits:2d} 2q-depth={two_qubit:4d}")
unary_projected qubits=12 2q-depth= 645
unary_unprojected qubits=12 2q-depth= 383
standard_binary qubits= 8 2q-depth= 218
gray qubits= 8 2q-depth= 95

Ці числа суперечать природному припущенню. Унарні кодування використовують dd кубітів на моду проти ⌈log⁡2d⌉\lceil \log_2 d \rceil для компактних кодів, і за d=3d = 3 компактні коди виграють одразу за обома показниками: менше кубітів і набагато менше заплутувальних вентилів. Два унарні варіанти відрізняються оператором, який вони видають, а не розкладкою: LevelEncoding.UNARY_PROJECTED несе проєктор на кожному другому кубіті моди, що робить його найдорожчим з чотирьох для еволюції, а LevelEncoding.UNARY_UNPROJECTED пропускає проєктори й збігається з ним на one-hot підпросторі. Посібник з мапперів показує обидва оператори поруч і пояснює, чому код Грея перевершує стандартний бінарний (SB) на членах переходу.

Sawaya et al. повідомляють про той самий порядок [2]:

хоча унарний код зазвичай потребує більше кубітів, але менше операцій, часто SB або код Грея ефективніший і за кількістю кубітів, і за кількістю операцій

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

Примітка

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

5. Перевірка за точною динамікою​

Жоден із попередніх кроків не встановлює, що схема правильна. Для цього порівняй густини на вузлах ⟨nm⟩\langle n_m \rangle після еволюції з точним еталоном, обчисленим для закодованого гамільтоніана.

Еталон потрібно обчислювати в тому самому обрізаному просторі, у якому працює схема, бо порівняння з необрізаним обчисленням змішало б похибку Троттера з похибкою обрізання, а це різні речі, які варто розділяти (дивись Обрізання простору Фока).

>>> import numpy as np
>>> from qiskit.quantum_info import SparsePauliOp, Statevector
>>> from scipy.sparse.linalg import expm_multiply
>>> from qiskit_bosons.mappers.library import occupation_encoding
>>>
>>> def sparse_matrix(operator, encoding):
... """Map a bosonic operator and hand back a sparse qubit matrix."""
... spec = LevelSpec(max_occupation, encoding).resolve(num_sites)
... observable = occupation_encoding(operator, spec)
... return SparsePauliOp.from_sparse_observable(observable).to_matrix(sparse=True)
>>>
>>> def fock_index(occupation, encoding):
... """The computational basis index encoding a product Fock state."""
... spec = LevelSpec(max_occupation, encoding).resolve(num_sites)
... return sum(
... encoding.level_to_bits(n) << offset for n, offset in zip(occupation, spec.offsets)
... )
>>>
>>> def densities(state, encoding):
... """The per-site boson number <n_m> of an encoded state vector."""
... out = []
... for mode in range(num_sites):
... number = BosonOperator.from_dict({(cre(mode), ann(mode)): 1.0})
... matrix = sparse_matrix(number, encoding)
... out.append(float((state.conj() @ (matrix @ state)).real))
... return out

Маючи це, точна еволюція в обрізаному просторі:

>>> encoding = LevelEncoding.GRAY
>>> num_qubits = LevelSpec(max_occupation, encoding).resolve(num_sites).total_width
>>>
>>> initial = np.zeros(2**num_qubits, dtype=complex)
>>> initial[fock_index(occupation, encoding)] = 1.0
>>>
>>> evolved_exact = expm_multiply(
... -1j * total_time * sparse_matrix(hamiltonian, encoding), initial
... )
>>> reference = densities(evolved_exact, encoding)
>>> for site, value in enumerate(reference):
... print(f"site {site}: {value:.6f}")
site 0: 0.993834
site 1: 0.858853
site 2: 0.141309
site 3: 0.006004

Бозони розтікаються із заповненого кінця ланцюжка до порожнього, і два бозони зберігаються:

>>> bool(np.isclose(sum(reference), 2.0))
True

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

>>> from qiskit_bosons.circuit.library.synthesis import BosonicLieTrotter
>>>
>>> def trotter_densities(steps, encoding):
... """Site densities after `steps` first-order Trotter steps of the grouped evolution."""
... circuit = BosonicCircuit(num_sites)
... circuit.append(
... Evolution(
... num_sites, hamiltonian, time=total_time, synthesis=BosonicLieTrotter(reps=steps)
... ),
... circuit.modes,
... )
... pass_manager = generate_preset_occupation_pass_manager(
... LevelSpec(max_occupation, encoding)
... )
... qubit_circuit = pass_manager.run(circuit.decompose())
... # decompose BEFORE evolving: see the warning that follows
... basis = qubit_circuit.decompose(reps=6)
... state = np.asarray(Statevector(initial).evolve(basis).data)
... return densities(state, encoding)
>>>
>>> for steps in (1, 2, 4, 8, 16):
... error = max(
... abs(a - b) for a, b in zip(trotter_densities(steps, encoding), reference)
... )
... print(f"steps={steps:2d} max density error = {error:.4f}")
steps= 1 max density error = 0.0843
steps= 2 max density error = 0.0389
steps= 4 max density error = 0.0183
steps= 8 max density error = 0.0088
steps=16 max density error = 0.0043

Похибка зменшується вдвічі, коли кількість кроків подвоюється, що є ознакою O(t2/steps)O(t^2/\text{steps}) формули добутку першого порядку. Це свідчить, що розбіжність – це похибка Троттера, а кодування вірне.

Важливо

Повторення походять із synthesis вентиля, який тротеризує в бозонному просторі, де групування ще існує: кожен крок застосовує кожну групу один раз, і кроки йдуть один за одним. Це відрізняється від передавання LieTrotter з reps > 1 плагіну синтезу на кубітному рівні. Там кожна група вже є власним PauliEvolutionGate, тож формула лише ділить члени всередині групи та ніколи не перемежовує групи. Групи не комутують взаємно, тому залишається єдиний зовнішній крок Троттера, похибка якого не зменшується, якими б великими не були reps. Синтез вентиля Evolution у бозонному просторі порівнює обидва підходи та розглядає вищі порядки.

Попередження

Попередній виклик decompose() є суттєвим. evolve() використовує точне означення PauliEvolutionGate і повністю оминає формулу добутку. Якщо перевіряти за нерозкладеною схемою, ти побачиш майже ідеальне узгодження навіть для свідомо хибного синтезу. Нерозкладена схема також змушує будувати щільний оператор 2n×2n2^{n} \times 2^{n}, тоді як розкладена схема застосовує вентилі по одному. Інші симулятори можуть обробляти вентиль інакше, тож ця перевірка залежить від симулятора, тоді як апаратне забезпечення завжди потребує розкладеної форми.

Нарешті, головна перевірка: фізика не повинна залежати від того, яке кодування обрано.

>>> def exact_densities(encoding):
... """Exact truncated-space densities, computed under a given encoding."""
... width = LevelSpec(max_occupation, encoding).resolve(num_sites).total_width
... start = np.zeros(2**width, dtype=complex)
... start[fock_index(occupation, encoding)] = 1.0
... evolved = expm_multiply(
... -1j * total_time * sparse_matrix(hamiltonian, encoding), start
... )
... return densities(evolved, encoding)
>>>
>>> all(
... bool(np.allclose(exact_densities(name), reference, atol=1e-10))
... for name in encodings
... )
True

Кількості кубітів, структури Паулі та глибини схем – усе відрізняється, а фізика збігається до десяти знаків після коми. Кодування – це вибір реалізації, тому воно належить транспілятору. Два унарні варіанти тут збігаються, бо змодельований стан лишається на one-hot підпросторі; поза ним вони відрізнялися б.

6. Обрізання – це параметр збіжності​

Останній крок специфічний для бозонів. Досі скрізь було зафіксовано max_occupation = 2, тобто d=3d = 3. Однак обрізання не є вільним вибором: це параметр збіжності, і відповіді можна довіряти, лише коли вона перестає змінюватися (дивись Обрізання простору Фока).

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

>>> def densities_at_cutoff(max_occ, encoding=LevelEncoding.GRAY):
... """Exact truncated-space densities at a given cutoff."""
... spec = LevelSpec(max_occ, encoding).resolve(num_sites)
...
... start = np.zeros(2**spec.total_width, dtype=complex)
... start[
... sum(encoding.level_to_bits(n) << offset for n, offset in zip(occupation, spec.offsets))
... ] = 1.0
...
... def matrix(operator):
... observable = occupation_encoding(operator, spec)
... return SparsePauliOp.from_sparse_observable(observable).to_matrix(sparse=True)
...
... evolved = expm_multiply(-1j * total_time * matrix(hamiltonian), start)
... out = []
... for mode in range(num_sites):
... number = BosonOperator.from_dict({(cre(mode), ann(mode)): 1.0})
... out.append(float((evolved.conj() @ (matrix(number) @ evolved)).real))
... return out
>>>
>>> previous = None
>>> for max_occ in (1, 2, 3, 4):
... current = densities_at_cutoff(max_occ)
... # Report the drift bucketed rather than to full precision: after it reaches machine
... # epsilon, the exact digits are arithmetic noise and vary between BLAS builds.
... if previous is None:
... drift = "--"
... else:
... gap = max(abs(a - b) for a, b in zip(current, previous))
... drift = f"{gap:.0e}" if gap > 1e-12 else "< 1e-12"
... print(f"d={max_occ + 1} "
... + " ".join(f"{x:.6f}" for x in current)
... + f" drift={drift}")
... previous = current
d=2 0.993986 0.858173 0.141827 0.006014 drift=--
d=3 0.993834 0.858853 0.141309 0.006004 drift=7e-04
d=4 0.993834 0.858853 0.141309 0.006004 drift=< 1e-12
d=5 0.993834 0.858853 0.141309 0.006004 drift=< 1e-12

За max_occupation = 1 (d=2d = 2) кожен вузол містить щонайбільше один бозон, тож член взаємодії, якому потрібні два на вузлі, взагалі не може діяти, а густини відхиляються приблизно на 7×10−47 \times 10^{-4}. Коли додати третій рівень, результат враховує взаємодію, а від max_occupation = 3 результат стаціонарний до машинної точності: збіжність досягнуто.

Причина, чому це зупиняється на max_occupation = 2 – кількість частинок. Гамільтоніан зберігає кількість бозонів, а цей стан містить два, тож жоден вузол ніколи не може містити більше двох, незалежно від часу еволюції та того, відштовхувальна взаємодія чи притягальна. Якщо еволюціонувати до t=6t = 6 замість t=0.4t = 0.4 або змінити знак UU, густини за max_occupation = 2 і вище залишаються незмінними з точністю 10−1310^{-13}.

Вимогу підвищує кількість бозонів. Три бозони на тому самому ланцюжку змінюються приблизно на 2×10−32 \times 10^{-3} між max_occupation = 2 і 3 після тієї самої короткої еволюції та приблизно на 0.30.3 після t=6t = 6. Це твердження про цю модель і збережену величину, і воно є межею лише для операторів, що зберігають кількість частинок; це не загальне правило, тож межу потрібно перебирати, а не вгадувати.

Перебір має ціну й з боку схеми, чого точний еталон не показує. Та сама бозонна схема, знижена з кодом Грея за кожної межі:

>>> for max_occ in (1, 2, 3, 4):
... pass_manager = generate_preset_occupation_pass_manager(
... LevelSpec(max_occ, LevelEncoding.GRAY)
... )
... pass_manager.qubit = PassManager()
... lowered = pass_manager.run(decomposed)
... depth = lowered.decompose(reps=6).depth(lambda instruction: len(instruction.qubits) == 2)
... print(f"d={max_occ + 1} qubits={lowered.num_qubits:2d} 2q-depth={depth:4d}")
d=2 qubits= 4 2q-depth= 8
d=3 qubits= 8 2q-depth= 95
d=4 qubits= 8 2q-depth= 217
d=5 qubits=12 2q-depth=1176

Кожен додатковий рівень коштує глибини, а кількість кубітів стрибає щоразу, коли dd перетинає степінь двійки. Тож, збігаючись за dd, ти обмінюєш точність на розмір схеми, і перебір – це більше, ніж числова перевірка.

Зауваж також, що майже правильне d=2d = 2 – це збіг через спостережувану: густини на вузлах майже не змінюються, тоді як сам стан якісно хибний (він не має компоненти з подвійним заповненням). Якщо збігається одна спостережувана, це не доводить, що збіглася інша.

Що варто запам'ятати​

  • Кодування — це справа транспайлера. Бозонне коло на кроці 3 не згадує ні обрізання, ні кодування, ні кількості кубітів. Чотири кодування потім звели його до чотирьох різних кіл, і всі вони відтворюють ту саму фізику. Нічого вище за транспайлер змінювати не довелося.

  • Для бозонів це не просто зручність. Обрізання — це параметр збіжності, який потрібно перебирати, і він живе всередині кодування. Коло, побудоване під фіксоване кодування, довелося б перебудовувати для кожного значення dd.

  • Групуй до відображення і групуй з огляду на ермітовість. Окремий член стрибка не є ермітовим і не може бути експоненційований; пара зв'язку на руці є ермітовою, а отже може бути експоненційована. Визнач розбиття на бозонному рівні, де ґратку ще видно. Це фізично необхідно, і саме для того, щоб передати це рішення далі, існує атрибут groups.

  • Не припускай, що кодування з малою вагою — дешеве. За d=3d = 3 компактні коди перевершують обидва унарні варіанти одночасно за кубітами та гейтами, а проєктований варіант є найдорожчим з усіх. Вимірюй; перетин залежить від dd.

  • Обрізання порушує алгебру, і це можна виміряти. Лише на верхньому збереженому рівні, на dd. Саме тому відображення не комутує з нормальним впорядкуванням.

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

Цей приклад можна розвинути у двох напрямках. Якщо замінити список ребер ланцюжка зв'язками квадратної ґратки, жодних інших змін не потрібно, адже раніше визначена функція bose_hubbard уже не залежить від геометрії. Власне кодування (наприклад, блочно-унарний код, що інтерполює між унарним і компактним) також підключається через той самий шов MapperFnEvolutionSynthesis, який використовує пресет.

Важливо

Кожна наведена глибина стосується абстрактного повнозв'язного регістра. Відображення на реальну зв'язність апаратури — окремий етап, а блоки кубітів на моду в кодуванні заповнень дають маршрутизатору помітно іншу структуру, ніж один кубіт на моду.

[1]

R. Somma, G. Ortiz, E. Knill, and J. Gubernatis, Quantum Simulations of Physics Problems (2003), arXiv:quant-ph/0304063, Sec. 4, Eq. 22.

[2]

N. P. D. Sawaya, T. Menke, T. H. Kyaw, S. Johri, A. Aspuru-Guzik, and G. G. Guerreschi, Resource-efficient digital quantum simulation of d-level systems for photonic, vibrational, and spin-s Hamiltonians, npj Quantum Information 6, 49 (2020), arXiv:1909.12847.