Відображення представлень операторів
Відображувач перетворює BosonOperator на кубітний оператор, тут — SparseObservable. Сам оператор нічого не знає про кубіти: він зберігає драбинні оператори на модах, а бозонна мода вміщує довільну кількість частинок. Усе, що робить оператор скінченним (скільки рівнів заповнення ти залишаєш і як ці рівні розташовуються на кубітах), визначається під час відображення.
Бібліотечний відображувач occupation_encoding() працює в базисі заповнення (Фока): рівень моди — це стан з бозонами. Він приймає оператор і LevelSpec, що містить обрізання та кодування. Цей посібник пояснює, що означають ці два вибори і чого коштує кожен із них. Наскрізний маршрут з їх використанням дивись у Симуляція динаміки Бозе-Габбарда з вибором кодування; про те, як транспайлер застосовує ті самі вибори до всієї схеми, дивись у Транспіляція бозонних схем.
Обрізання простору Фока
Мода, що зберігається з рівнями, де — це її max_occupation, більше не підпорядковується канонічному комутаційному співвідношенню. Позначивши оператор знищення, обрізаний до цих рівнів,
співвідношення виконується на кожному рівні, крім найвищого [1]. Це можна побачити, відобразивши комутатор двох драбинних операторів і прочитавши його діагональ на рівнях однієї моди. Наведена нижче допоміжна функція будує щільну матрицю відображеного оператора для однієї моди, що доречно лише для дуже малих регістрів:
>>> import numpy as np
>>> from qiskit.quantum_info import SparsePauliOp
>>> from qiskit_bosons.mappers import LevelEncoding, LevelSpec
>>> from qiskit_bosons.mappers.library import occupation_encoding
>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>>
>>> def level_diagonal(operator, max_occupation, encoding):
... """The diagonal of a mapped one-mode operator on the codewords of the levels."""
... spec = LevelSpec(max_occupation, encoding)
... matrix = SparsePauliOp.from_sparse_observable(occupation_encoding(operator, spec)).to_matrix()
... codewords = [spec.encoding.level_to_bits(level) for level in range(max_occupation + 1)]
... return [round(float(matrix[c, c].real), 6) for c in codewords]
>>>
>>> b = BosonOperator.from_dict({(ann(0),): 1.0})
>>> b_dagger = BosonOperator.from_dict({(cre(0),): 1.0})
>>> commutator = (b @ b_dagger) - (b_dagger @ b)
>>> level_diagonal(commutator, 2, "unary_unprojected") # d = 3
[1.0, 1.0, -2.0]
Останній елемент дорівнює . Оскільки це означає, що лінійна оболонка не замкнена відносно комутатора, жодне кодування не є ізоморфізмом алгебр. Кодування відображає стани разом з операторами, що діють у обрізаному підпросторі. Наступні два наслідки важливі на практиці.
Обрізання має обмежувати досяжне заповнення. Недостатньо обмежити заповнення, з якого ти починаєш. Світлоподільник, що діє на , заповнює і , тому обрізання на одиниці мовчки дає хибний стан:
>>> from scipy.linalg import expm
>>>
>>> beamsplitter = BosonOperator.from_dict({(cre(0), ann(1)): 1.0, (cre(1), ann(0)): 1.0})
>>>
>>> def stays_in_11(max_occupation):
... """Probability to remain in |1, 1> after a 50:50 beamsplitter."""
... spec = LevelSpec(max_occupation, "gray").resolve(2)
... observable = occupation_encoding(beamsplitter, spec, spec.total_width)
... matrix = SparsePauliOp.from_sparse_observable(observable).to_matrix()
... unitary = expm(-1j * (np.pi / 4) * matrix)
... index = sum(spec.encoding.level_to_bits(1) << offset for offset in spec.offsets)
... return round(float(abs(unitary[index, index]) ** 2), 6)
>>>
>>> stays_in_11(1), stays_in_11(2)
(1.0, 0.0)
З обрізанням на одиниці ймовірність залишитися в дорівнює одиниці, бо двох станів, які заповнила б динаміка, не існує. З обрізанням на двох вона зникає, і це ефект Хонга-Оу-Менделя. Саме тому max_occupation є обов'язковим аргументом без значення за замовчуванням: це рішення моделювання та параметр збіжності, який ти маєш перебирати.
Відображення не комутує з нормальним впорядкуванням. Оператори і рівні до обрізання, але відображаються на різні кубітні оператори:
>>> product = BosonOperator.from_dict({(ann(0), cre(0)): 1.0})
>>> ordered = BosonOperator.from_dict({(cre(0), ann(0)): 1.0}) + BosonOperator.one()
>>> bool(product.equiv(ordered))
True
>>> level_diagonal(product, 3, "unary_unprojected") # d = 4
[1.0, 2.0, 3.0, 0.0]
>>> level_diagonal(ordered, 3, "unary_unprojected")
[1.0, 2.0, 3.0, 4.0]
Вони збігаються на кожному рівні, крім найвищого. Застосований до верхнього рівня, не має куди діватися, і амплітуду обрізано, тому там дорівнює нулю, тоді як дає . Визнач раз і назавжди, чи твій гамільтоніан задано в нормально впорядкованій формі, і відображай цю форму послідовно.
Три методи допомагають обрати обрізання, і всі вони дають нижні межі. Оператор повідомляє найбільшу кількість операторів народження, яку він застосовує до кожної моди в межах одного доданка, а специфікація порівнює її зі своїми обрізаннями:
>>> quartic = BosonOperator.from_dict({(cre(0), cre(0), ann(0), ann(0)): 1.0})
>>> quartic.mode_occupations(), quartic.max_occupation()
([2], 2)
>>> LevelSpec(1, "gray").truncation(quartic) # one level short
[1]
>>> LevelSpec(2, "gray").truncation(quartic)
[0]
Ненульовий елемент означає, що обрізання нижче за межу, а значення показує, наскільки. Ці допоміжні засоби не знають про динаміку, тож успішна перевірка не робить обрізання достатнім; світлоподільник вище має межу одиницю, але потребує обрізання на двох. Вони також можуть повідомити про нестачу для доданка, який дорівнює нулю на збережених рівнях, як-от для обрізання на одиниці.
Опис обрізання за допомогою LevelSpec
LevelSpec об'єднує обрізання кожної моди з кодуванням і виводить з них, скільки кубітів займає кожна мода та де починаються її кубіти. Усе, що має бути узгоджене щодо цього (відображувач, розкладка транспайлера, підготовка стану), отримує ту саму специфікацію. Кодування є обов'язковим аргументом, так само як і обрізання: жодне з них не має значення за замовчуванням.
Обрізання — це або одне ціле число, спільне для всіх мод, або одне ціле число на моду:
>>> shared = LevelSpec(2, "gray")
>>> per_mode = LevelSpec([4, 2, 2], "gray")
>>> shared.num_modes is None, per_mode.num_modes
(True, 3)
Специфікація, побудована з одного цілого числа, залишає кількість мод відкритою, тому її потрібно визначити, перш ніж запитувати ширини. Послідовність ніколи не поширюється, незалежно від довжини, і вона фіксує кількість мод:
>>> per_mode.widths, per_mode.offsets, per_mode.total_width
([3, 2, 2], [0, 3, 5], 7)
>>> shared.resolve(3).widths
[2, 2, 2]
>>> try:
... shared.widths
... except ValueError as error:
... print(error)
this spec was built from a single cutoff, so its number of modes is open; call `resolve(num_modes)` first
Мода займає кубіти від offsets[m] до offsets[m] + widths[m] - 1, тож моди з різними обрізаннями можуть займати різну кількість кубітів, а зсуви є поточною сумою ширин. Передавати відображувачу кількість мод не потрібно. Він бере її зі специфікації або, для специфікації з одним обрізанням, з оператора через min_num_modes(). Регістр може бути ширшим, ніж потрібно модам; зайві кубіти простоюють і розташовані наприкінці:
>>> number = BosonOperator.from_dict({(cre(0), ann(0)): 1.0})
>>> number.min_num_modes()
1
>>> occupation_encoding(number, LevelSpec(1, "gray")).num_qubits
1
>>> occupation_encoding(number, LevelSpec(1, "gray"), 4).num_qubits
4
Оператор, що діє на моду за останньою з покремої специфікації для кожної моди, відхиляється:
>>> far = BosonOperator.from_dict({(cre(5), ann(5)): 1.0})
>>> try:
... occupation_encoding(far, LevelSpec([1, 1], "gray"))
... except ValueError as error:
... print(error)
the operator acts on 6 modes, but the spec covers only 2
Кілька обмежень випливають із того, як кодування рахують кубіти. За компактного кодування мода з єдиним рівнем (max_occupation = 0) не займає жодного кубіта, адже її єдиному рівню не потрібно нічого, щоб відрізнятися від іншого. Така мода дозволена і не зсуває кубіти наступних мод, але специфікація, у якій жодна мода не займала б кубіта, відхиляється. Унарні кодування вміщують щонайбільше 128 рівнів на моду, а більше обрізання спричиняє помилку з пропозицією використати компактне кодування:
>>> LevelSpec([0, 1], "gray").resolve(2).widths
[0, 1]
>>> try:
... LevelSpec(0, "gray")
... except ValueError as error:
... print(str(error).split(":")[0])
the spec would span no qubits
>>> try:
... LevelSpec(128, "unary_projected")
... except ValueError as error:
... print(str(error).split(" (")[0])
max_occupation = 128 is too large for the unary encodings, which hold at most 128 levels per mode
Вибір кодування
LevelEncoding визначає, який базисний стан блоку кубітів представляє кожен рівень. Їх чотири, і жодне не є найкращим узагалі [2]. Кількість кубітів на моду та бітовий шаблон кожного рівня випливають із кодування:
>>> encodings = (
... LevelEncoding.UNARY_PROJECTED,
... LevelEncoding.UNARY_UNPROJECTED,
... LevelEncoding.STANDARD_BINARY,
... LevelEncoding.GRAY,
... )
>>> rows = ["encoding qubits for d = 1, 2, 3, 4, 5, 8 level 0 to 4 as bit patterns"]
>>> for encoding in encodings:
... widths = [encoding.num_qubits(levels) for levels in (1, 2, 3, 4, 5, 8)]
... patterns = [f"{encoding.level_to_bits(level):05b}" for level in range(5)]
... rows.append(f"{encoding!s:18s} {widths!s:34s} {patterns}")
>>> print("\n".join(rows))
encoding qubits for d = 1, 2, 3, 4, 5, 8 level 0 to 4 as bit patterns
unary_projected [1, 2, 3, 4, 5, 8] ['00001', '00010', '00100', '01000', '10000']
unary_unprojected [1, 2, 3, 4, 5, 8] ['00001', '00010', '00100', '01000', '10000']
standard_binary [0, 1, 2, 2, 3, 3] ['00000', '00001', '00010', '00011', '00100']
gray [0, 1, 2, 2, 3, 3] ['00000', '00001', '00011', '00010', '00110']
Бітові шаблони — це цілі числа, у яких біт є значенням кубіта . Унарні кодування витрачають один кубіт на рівень і встановлюють один із них. Компактні кодування витрачають кубітів і зберігають номер рівня в двійковому записі або як код Грея, у якому сусідні рівні відрізняються одним бітом. Рядкова назва кодування приймається всюди, де приймається саме кодування.
Компроміс полягає між кубітами та вагою оператора. Компактні кодування потребують експоненційно менше кубітів, але драбинний оператор з'єднує рівні, чиї шаблони можуть відрізнятися кількома бітами, тож його доданки діють на більше кубітів. Наступні розділи розглядають, чого це коштує, починаючи з двох унарних кодувань, які мають однакові розкладки й відрізняються лише оператором.
Порівняння двох унарних кодувань
В унарному кодуванні драбинний оператор переміщує одне збудження між двома сусідніми кубітами. Щоб записати це як оператор на всій моді, потрібно вирішити, що робити з іншими кубітами. Два варіанти вирішують по-різному:
-
unary_projectedставить проєктор на кожен інший кубіт моди. Це забезпечує обмеження one-hot в самому операторі: доданок діє, лише якщо решта моди порожня. -
unary_unprojectedзалишає ці кубіти без змін, тож доданок діє на них як тотожність.
Розглянь один при за обох варіантів. Відображувач повертає SparseObservable, чиї доданки — це рядки однокубітних бітових доданків. Ось перші чотири доданки кожного з кубітами, на які вони діють:
>>> for encoding in ("unary_projected", "unary_unprojected"):
... observable = occupation_encoding(b_dagger, LevelSpec(3, encoding))
... terms = observable.to_sparse_list()
... print(encoding, "->", len(terms), "terms on", observable.num_qubits, "qubits")
... for label, qubits, coefficient in terms[:4]:
... print(f" {label:5s} on qubits {qubits} coefficient {complex(coefficient):.4f}")
unary_projected -> 12 terms on 4 qubits
XX00 on qubits [0, 1, 2, 3] coefficient 0.2500+0.0000j
XY00 on qubits [0, 1, 2, 3] coefficient 0.0000-0.2500j
YX00 on qubits [0, 1, 2, 3] coefficient 0.0000+0.2500j
YY00 on qubits [0, 1, 2, 3] coefficient 0.2500+0.0000j
unary_unprojected -> 12 terms on 4 qubits
XX on qubits [0, 1] coefficient 0.2500+0.0000j
XY on qubits [0, 1] coefficient 0.0000-0.2500j
YX on qubits [0, 1] coefficient 0.0000+0.2500j
YY on qubits [0, 1] coefficient 0.2500+0.0000j
Обидва мають дванадцять доданків з однаковими коефіцієнтами. Проєктовані доданки містять бітовий доданок 0 на двох кубітах, яких непроєктовані доданки не згадують, тому кожен проєктований доданок торкається всіх чотирьох кубітів, а кожен непроєктований — двох. Загалом проєктований доданок торкається всіх кубітів моди, бо він містить два множники, кожен або , і проєктор на кожному іншому кубіті.
Ці два оператори рівні на станах, для яких призначене кодування, — базисних станах one-hot (кодових словах), — і відрізняються поза ними. Щоб перевірити, візьми малий оператор на двох модах при і порівняй матриці на дев'яти кодових словах:
>>> hamiltonian = BosonOperator.from_dict({
... (cre(0), ann(1)): 1.0, (cre(1), ann(0)): 1.0,
... (cre(0), ann(0)): 0.5, (cre(1), cre(1), ann(1), ann(1)): 0.25,
... })
>>> matrices = {}
>>> for encoding in ("unary_projected", "unary_unprojected"):
... observable = occupation_encoding(hamiltonian, LevelSpec(2, encoding))
... matrices[encoding] = SparsePauliOp.from_sparse_observable(observable).to_matrix()
>>> unary = LevelEncoding.UNARY_PROJECTED
>>> codewords = sorted(
... unary.level_to_bits(first) | (unary.level_to_bits(second) << 3)
... for first in range(3) for second in range(3)
... )
>>> projected, unprojected = matrices["unary_projected"], matrices["unary_unprojected"]
>>> bool(np.allclose(projected[np.ix_(codewords, codewords)], unprojected[np.ix_(codewords, codewords)]))
True
>>> bool(np.allclose(projected, unprojected))
False
Обидва також залишають підпростір кодових слів інваріантним, тож стан, підготовлений на кодовому слові, залишається на кодових словах. Вибір має значення лише для станів, що залишають цей підпростір, наприклад через шум. Там проєктований оператор пригнічує еволюцію, бо його проєктори не спрацьовують, тоді як непроєктований продовжує діяти. Жодна з поведінок не є фізичною вимогою. Непроєктований оператор визначений лише на підпросторі one-hot, тож його вибір — це рішення моделювання, а не безкоштовна оптимізація.
Проєктори коштують вентилів. Еволюціонуй ермітів на одній моді короткий час і порахуй двокубітні вентилі після транспіляції Qiskit:
>>> from qiskit import QuantumCircuit, transpile
>>> from qiskit.circuit.library import PauliEvolutionGate
>>>
>>> def cx_count(observable):
... circuit = QuantumCircuit(observable.num_qubits)
... circuit.append(PauliEvolutionGate(observable, time=0.1), range(observable.num_qubits))
... basis = ["cx", "rz", "sx", "x"]
... return transpile(circuit, basis_gates=basis, optimization_level=1).count_ops().get("cx", 0)
>>>
>>> hermitian = b + b_dagger
>>> levels = (3, 4, 5, 6)
>>> counts = {
... str(encoding): [
... cx_count(occupation_encoding(hermitian, LevelSpec(d - 1, encoding))) for d in levels
... ]
... for encoding in encodings
... }
Точні кількості залежать від того, як Qiskit синтезує вентиль еволюції, що змінюється між випусками, тож побудуй графік:
Кількість для непроєктованого варіанта зростає лінійно з , бо кожен із переходів є двокубітним обертанням. Кількість для проєктованого зростає значно швидше, бо кожен доданок діє на всі кубітів. Компактні кодування потребують менше кубітів, але вага їхніх доданків залежить від того, як відрізняються бітові шаблони сусідніх рівнів: код Грея, у якому вони відрізняються одним бітом, дешевший із двох компактних кодувань, а стандартне двійкове стрибає, коли перенос перемикає кілька бітів одночасно (). Чи компактне кодування також перевершує непроєктоване унарне, залежить від і від того, як Qiskit синтезує еволюцію, тож зважай на тенденцію, а не на рейтинг за одного розміру.
Проєктори також мають значення, коли ти розкладаєш відображений оператор на Паулі, що робить from_sparse_observable(), замінюючи кожен проєктор сумою двох операторів Паулі. Доданок із проєкторами стає доданками Паулі. Ось один при малому , який є єдиним розміром, на якому варто це пробувати:
>>> rows = [f"{'d':>2s} {'stored (both)':>14s} {'expanded, projected':>20s} {'expanded, unprojected':>22s}"]
>>> for levels in (3, 4, 5):
... stored = len(occupation_encoding(b_dagger, LevelSpec(levels - 1, "unary_projected")))
... expanded = [
... len(SparsePauliOp.from_sparse_observable(
... occupation_encoding(b_dagger, LevelSpec(levels - 1, encoding))).simplify())
... for encoding in ("unary_projected", "unary_unprojected")
... ]
... rows.append(f"{levels:2d} {stored:14d} {expanded[0]:20d} {expanded[1]:22d}")
>>> print("\n".join(rows))
d stored (both) expanded, projected expanded, unprojected
3 8 16 8
4 12 48 12
5 16 128 16
Обидва варіанти зберігають однакову кількість доданків, але розклад проєктованого зростає експоненційно за . Для чогось більшого за іграшковий приклад читай доданки SparseObservable напряму, як у переліку вище, замість перетворення.
Компактні кодування не можуть відкинути свої проєктори. Там нульові біти є частиною номера рівня, тож видалення змінює, які рівні з'єднує доданок, і оператор більше не є тим самим на кодових словах. Саме тому лише унарне кодування має два варіанти.
Добуток драбин спочатку обрізається
Ще один вибір проєктування проявляється у вартості діагональних доданків. Відображувач не відображає кожен драбинний оператор окремо, щоб потім перемножити результати. Він збирає драбинні оператори, які доданок застосовує до однієї моди, перемножує їх як матрицю у базисі Фока, обрізає цей добуток один раз і кодує результат [2]. Тому добуток на кшталт оператора числа залишається діагональним:
>>> number_observable = occupation_encoding(number, LevelSpec(3, "unary_unprojected"))
>>> [(label, qubits) for label, qubits, _ in number_observable.to_sparse_list()]
[('1', [1]), ('1', [2]), ('1', [3])]
>>> cx_count(number_observable)
0
Його доданки — це проєктори на кубіті кожного рівня вище нуля, зважені номером рівня (рівень нуль має нульову вагу, тож не має доданка). Його еволюція — це набір однокубітних обертань і не потребує двокубітного вентиля. Натомість множення двох відображених драбинних операторів дає той самий оператор на кодових словах, але інший поза ними, з недіагональними рядками, що коштують вентилів:
>>> unprojected = LevelSpec(3, "unary_unprojected")
>>> as_pauli = lambda operator: SparsePauliOp.from_sparse_observable(
... occupation_encoding(operator, unprojected, 4)).simplify()
>>> composed = as_pauli(b_dagger).dot(as_pauli(b)).simplify()
>>> direct = as_pauli(number)
>>> len(direct), len(composed)
(4, 16)
>>> cx_count(direct)
0
>>> bool(cx_count(composed) > 0)
True
>>> codewords = [LevelEncoding.UNARY_UNPROJECTED.level_to_bits(level) for level in range(4)]
>>> bool(np.allclose(direct.to_matrix()[np.ix_(codewords, codewords)],
... composed.to_matrix()[np.ix_(codewords, codewords)]))
True
>>> bool(np.allclose(direct.to_matrix(), composed.to_matrix()))
False
Діагональні доданки, як-от локальна взаємодія в моделі Бозе-Габбарда, є саме такими добутками, і тут цей вибір має значення.
Написання власного відображувача
Якщо потрібне кодування, якого бібліотека не надає, побудуй його з образу одного драбинного оператора. map_boson_action_generators() приймає функцію, що відображає одну BosonAction, функцію, що повертає тотожність, і будує з них відображувач цілих операторів. Вона множить образи дій кожного доданка по порядку, масштабує на коефіцієнт і сумує доданки. Результатом може бути будь-який тип, що підтримує множення на скаляр і композицію.
Як приклад візьмемо кодування для , що зберігає моду в одному кубіті. Оператор народження — це , тобто за конвенцією Qiskit, а оператор знищення — його спряжений:
>>> from qiskit_bosons.mappers import map_boson_action_generators
>>> from qiskit_bosons.operators import BosonAction
>>>
>>> num_qubits = 2
>>>
>>> def map_action(action: BosonAction) -> SparsePauliOp:
... is_creation, mode = action
... sign = -1j if is_creation else 1j
... return SparsePauliOp.from_sparse_list(
... [("X", [mode], 0.5), ("Y", [mode], 0.5 * sign)], num_qubits=num_qubits
... )
>>>
>>> def identity() -> SparsePauliOp:
... return SparsePauliOp.from_sparse_list([("", [], 1.0)], num_qubits)
>>>
>>> model = BosonOperator.from_dict({
... (cre(0), ann(1)): -1.0, (cre(1), ann(0)): -1.0, (ann(0), cre(0)): 0.7, (cre(1), ann(1)): 0.3,
... })
>>> custom = map_boson_action_generators(model, map_action, identity)
>>> library = SparsePauliOp.from_sparse_observable(occupation_encoding(model, LevelSpec(1, "gray")))
>>> bool(np.allclose(custom.to_matrix(), library.to_matrix()))
True
Цей написаний вручну відображувач перемножує відображені драбинні оператори й тут збігається з бібліотечним, бо кожна мода має два рівні, а кубіт вміщує всю моду. При більших , як показав попередній розділ, композиція відображених драбин і попереднє обрізання добутку — це не той самий оператор поза кодовими словами.
Щоб використати такий відображувач у схемі, обгорни його у функцію з сигнатурою (operator, num_qubits) -> SparseObservable і передай у MapperFnEvolutionSynthesis, який відображає за її допомогою оператор кожного вентиля Evolution. Конвеєр транспайлера описано в Транспіляція бозонних схем.
>>> from qiskit.quantum_info import SparseObservable
>>> from qiskit_bosons.transpiler.passes import MapperFnEvolutionSynthesis
>>>
>>> def mapper_fn(operator, num_qubits):
... return SparseObservable.from_sparse_pauli_op(
... map_boson_action_generators(operator, map_action, identity)
... )
>>>
>>> synthesis = MapperFnEvolutionSynthesis(mapper_fn)
Варто знати дві деталі. По-перше, кубітний оператор повинен мати дійсні коефіцієнти, бо PauliEvolutionGate відхиляє уявну частину. Ермітів оператор відображається на дійсний, але лише після об'єднання доданків, що скорочуються. Цей написаний вручну відображувач їх не об'єднує, тож доданок стрибка та його спряжений залишають уявні коефіцієнти, доки ти не спростиш результат. Бібліотечний відображувач об'єднує свій результат сам; власний мусить робити це сам, наприклад обгорнувши його у simplify():
>>> hopping = BosonOperator.from_dict({(cre(0), ann(1)): 1.0})
>>> hermitian_hopping = hopping + hopping.adjoint()
>>> raw = mapper_fn(hermitian_hopping, 2)
>>> bool(np.allclose(np.asarray(raw.coeffs).imag, 0))
False
>>> merged = raw.simplify()
>>> bool(np.allclose(np.asarray(merged.coeffs).imag, 0))
True
>>> sorted(label for label, _, _ in merged.to_sparse_list())
['XX', 'YY']
По-друге, формула добутку синтезує доданки в порядку їх отримання, а порядок, який видає відображувач, не гарантовано стабільний. Допоміжні функції simplify() і group_wise() обгортають функцію відображувача, щоб керувати цим: simplify об'єднує однакові рядки й сортує доданки в канонічний порядок, тоді як group_wise відображає по одній групі оператора, щоб доданки групи залишалися поруч. Не поєднуй їх, бо сортування знищує групування. Посібник із групування показує, як призначати групи, а посібник із синтезу — як порядок впливає на схему.
Наступні кроки
-
Виконай Симуляція динаміки Бозе-Габбарда з вибором кодування для повної симуляції, що перебирає обрізання та порівнює кодування з точною динамікою.
-
Дивись Транспіляція бозонних схем, щоб дізнатися, як той самий
LevelSpecзастосовується до бозонної схеми. -
Прочитай Принципи проєктування представлень операторів, щоб дізнатися, як
BosonOperatorзберігає й упорядковує свої доданки, зокрема про правило згортання, що стоїть за прикладом нормального впорядкування вище. -
Прочитай Обертання бозонних мод про вентиль, що переміщує бозони між модами, де попередження про досяжне заповнення важить найбільше.
-
Зверніться до довідника API
qiskit_bosons.mappersіqiskit_bosons.mappers.library.
[1]
R. Somma, G. Ortiz, E. Knill, and J. Gubernatis, Quantum Simulations of Physics Problems (2003), arXiv:quant-ph/0304063, Sec. 4, Eq. 22.
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.