Mapping representasi operator
Mapper adalah rutin yang mengubah operator dari satu representasi ke representasi lain sambil menjaga kesetaraan matematis. Pakai untuk bekerja secara fleksibel di berbagai aljabar operator dan untuk menyiapkan operator bagi eksekusi circuit kuantum.
Mengimplementasikan mapper kustom
Modul mappers memudahkan implementasi mapping kustom. Mapper bekerja dengan mentransformasi aksi-aksi fundamental (creation, annihilation, Majorana, dan seterusnya) yang menyusun sebuah operator, lalu menggabungkan aksi-aksi hasil transformasi itu sesuai aturan representasi target.
Sebagai contoh konkret, coba lihat transformasi Jordan-Wigner yang memetakan MajoranaOperator ke operator qubit dengan map_majorana_action_generators():
- PYTHON
- C
>>> from qiskit.quantum_info import SparseObservable
>>> from qiskit_fermions.mappers import map_majorana_action_generators
>>> from qiskit_fermions.operators import MajoranaOperator
>>>
>>> # Create a Majorana operator
>>> maj_op = MajoranaOperator.from_dict({(0, 1): 1.0, (2,): 0.5})
>>>
>>> # Define how each Majorana action maps to Pauli strings
>>> num_qubits = 2
>>> def map_action(mode: int) -> SparseObservable:
... idx = mode // 2
... qubits = list(range(idx + 1))
... pauli = "Y" if mode % 2 else "X"
... return SparseObservable.from_sparse_list(
... [("Z" * idx + pauli, qubits, 1.0)],
... num_qubits=num_qubits,
... )
>>>
>>> # Apply the mapping to transform the operator
>>> custom_observable = map_majorana_action_generators(
... maj_op,
... map_action,
... identity=lambda: SparseObservable.identity(num_qubits),
... compose=SparseObservable.compose,
... )
>>> print(custom_observable.simplify())
<SparseObservable with 2 terms on 2 qubits: (0+1j)(Z_0) + (0.5+0j)(X_1 Z_0)>
// The C API provides direct mapping functions for common transformations.
// Custom mapper prototyping as shown in Python is primarily a Python feature.
// For custom mappings in C, you will need to perform the iteration and
// conversion logic entirely by yourself.
Langkah-langkah utama untuk membuat mapper kustom apa pun:
-
Tentukan transformasi aksi: Tentukan bagaimana setiap aksi (diindeks berdasarkan mode) dipetakan ke representasi target. Di contoh di atas, aksi Majorana dipetakan ke string Pauli sesuai aturan Jordan-Wigner.
-
Pakai salah satu fungsi pemetaan bawaan: Modul
mappersmenyediakan fungsi utilitas untuk mengiterasi suku-suku operator dan menerapkan peta aksi kustom secara berurutan untuk representasi operator yang disediakan moduloperators. Misalnya,map_majorana_action_generators()bekerja dengan operator Majorana. -
Ambil hasilnya: Fungsi pemetaan mengembalikan operator dalam representasi target, dengan transformasi yang diterapkan secara konsisten ke semua suku.
Desain ini bikin kamu gampang membuat prototipe pemetaan kustom tanpa perlu paham struktur internal operator. Transformasinya diterapkan otomatis ke setiap suku di operator, sambil menjaga ekuivalensi matematisnya.
Kamu juga bisa mengiterasi suku-suku operator secara manual, tanpa memakai fungsi iterator bawaan. Lihat bagian iterasi suku di panduan operator untuk detailnya.
Implementasi pustaka
Modul qiskit_fermions.mappers.library menyediakan implementasi pemetaan umum yang efisien dan sudah diuji menyeluruh. Pola dasarnya sama dengan mapper kustom, tapi sudah dioptimalkan untuk penggunaan produksi dan tersedia di API Python maupun C.
Contoh di bawah menerapkan mapper pustaka ke MajoranaOperator yang sama dari bagian mapper kustom, dan menunjukkan hasil yang setara:
- PYTHON
- C
>>> from qiskit_fermions.mappers.library import majorana_to_fermion, jordan_wigner
>>>
>>> # Convert MajoranaOperator to FermionOperator using library mapper
>>> ferm_op = majorana_to_fermion(maj_op)
>>>
>>> # Apply Jordan-Wigner mapper to get qubit operator
>>> sparse_observable = jordan_wigner(ferm_op, num_qubits=2)
>>> difference = sparse_observable - custom_observable
>>> print(difference.simplify() == SparseObservable.zero(num_qubits))
True
#include <qiskit_fermions.h>
// Create a MajoranaOperator with the same terms as maj_op above
uint64_t num_terms = 2;
uint64_t num_modes = 3;
uint32_t modes[3] = {0, 1, 2};
QkComplex64 coeffs[2] = {{1.0, 0.0}, {0.5, 0.0}};
uint32_t boundaries[3] = {0, 2, 3};
QfMajoranaOperator *maj_op = qf_maj_op_new(num_terms, num_modes, coeffs, modes, boundaries);
// Convert MajoranaOperator to FermionOperator using library mapper
QfFermionOperator *ferm_op = qf_majorana_to_fermion(maj_op);
// Apply Jordan-Wigner mapper to get qubit operator
QkObs *qubit_op;
QfExitCode exit = qf_ferm_op_jordan_wigner(ferm_op, 2, &qubit_op);
assert(exit == QfExitCode_Success);
// Clean up
qf_ferm_op_free(ferm_op);
qf_maj_op_free(maj_op);
qk_obs_free(qubit_op);
Dua langkah di atas sengaja ditulis lengkap untuk menunjukkan dua mapper pustaka yang dikomposisikan. Dalam praktiknya, kamu akan memetakan MajoranaOperator dalam satu langkah saja, dengan majorana_jordan_wigner() (qf_maj_op_jordan_wigner() di API C), dan begitu juga untuk tipe operator lainnya – edge_vertex_jordan_wigner() dan transfer_vertex_jordan_wigner() (qf_edge_op_jordan_wigner(), qf_transfer_op_jordan_wigner()). Lewat FermionOperator juga membengkakkan hasil antara: setiap generator aljabar ini dipetakan ke satu string Pauli, sedangkan setiap aksi fermionik dipetakan ke jumlah dua suku, jadi suku yang dibangun dari beberapa generator mengembang dulu hanya untuk digabung lagi. Penghematan itu makin besar seiring panjang sukunya; untuk suku generator tunggal dari Hamiltonian hopping, kedua rute biayanya hampir sama.
Di API Python, sebagian mapper pustaka ini juga bisa diakses lewat mapper praktis yang tidak bergantung pada tipe. fermion_operator() dan majorana_operator() berada di depan majorana_to_fermion() dan fermion_to_majorana() (dan lainnya), dan secara internal mendelegasikan ke implementasi pustaka yang sesuai berdasarkan tipe objek yang diberikan, jadi kamu tidak perlu memilih fungsi yang tepat sendiri. Hal yang sama berlaku untuk jordan_wigner() dan ternary_tree() (diperkenalkan di bawah), yang masing-masing meneruskan ke implementasi langsung yang cocok dengan operator yang diberikan. Lihat qiskit_fermions.protocols untuk cara dispatch ini diimplementasikan secara internal.
Pilih encoding dengan pohon terner
Jordan-Wigner bukan satu-satunya encoding fermion-ke-qubit, dan bukan yang paling murah. String -nya bertambah panjang seiring indeks mode, jadi operator Majorana pada mode terakhir dari mode dipetakan ke string Pauli berbobot . Encoding yang menyebarkan informasi paritas lewat pohon, bukan rantai, secara asimtotik lebih baik.
Sebuah pohon terner dengan simpul mencakup seluruh keluarga ini. Setiap simpul adalah sebuah qubit dengan tiga tautan ke bawah berlabel X, Y, dan Z; sebuah tautan bisa turun ke simpul anak (sebuah edge) atau berhenti (sebuah leg). Jalur setiap leg kembali ke akar mengeja sebuah string Pauli: melewati tautan berlabel yang turun dari simpul menyumbang pada qubit . Setiap pohon seperti itu menghasilkan encoding yang valid, dan yang sudah familiar hanyalah bentuk tertentu – rantai sepanjang Z adalah Jordan-Wigner, sedangkan pohon yang setiap simpulnya punya tiga anak, sejauh jumlah simpul memungkinkan, adalah pohon seimbang.
- PYTHON
- C
>>> from qiskit_fermions.mappers import TernaryTree, TernaryTreeEncoding
>>>
>>> # Jordan-Wigner is a chain descending along Z, with X and Y as legs at every node.
>>> TernaryTree.chain(13, "Z").max_weight
13
>>> # The balanced tree spreads the same information over depth log_3(2n+1).
>>> TernaryTree.breadth_first(13, 3).max_weight
3
#include <qiskit_fermions.h>
QfTernaryTree *chain;
qf_ternary_tree_chain(13, QfPauliLabel_Z, &chain);
QfPauliLabel order[3] = {QfPauliLabel_X, QfPauliLabel_Y, QfPauliLabel_Z};
QfTernaryTree *balanced;
qf_ternary_tree_breadth_first(13, 3, order, &balanced);
assert(qf_ternary_tree_max_weight(chain) == 13);
assert(qf_ternary_tree_max_weight(balanced) == 3);
qf_ternary_tree_free(chain);
qf_ternary_tree_free(balanced);
Bobot maksimum itu optimal. Jiang et al. membuktikan bahwa operator Majorana dari sembarang pemetaan fermion-ke-qubit untuk mode punya bobot Pauli rata-rata paling sedikit , jadi tidak ada pemetaan yang bisa menurunkan yang terbesar di bawah . Kedua pohon memakai tepat qubit, jadi penghematannya ada di bobot Pauli, bukan jumlah qubit.
Untuk memetakan operator, kompilasi pohon menjadi encoding lalu berikan ke mapper bersama jumlah qubit untuk hasilnya. Kompilasi memasangkan leg pohon menjadi operator Majorana dan membangun string Pauli-nya sekali saja, jadi encoding yang sama sebaiknya dipakai ulang untuk banyak operator. Jumlah qubit tetap ada di pemanggilan pemetaan, bukan di encoding, jadi satu encoding bisa melayani register dengan lebar berbeda; jumlah di atas jumlah simpul pohon diisi dengan identitas.
- PYTHON
- C
>>> from qiskit_fermions.mappers.library import fermion_ternary_tree
>>> from qiskit_fermions.operators import FermionOperator
>>>
>>> hamiltonian = FermionOperator.from_dict(
... {((True, 0), (False, 3)): 1.0, ((True, 3), (False, 0)): 1.0}
... )
>>> encoding = TernaryTreeEncoding(TernaryTree.breadth_first(4, 3))
>>> fermion_ternary_tree(hamiltonian, encoding, 4).simplify()
<SparseObservable with 2 terms on 4 qubits: (0.5+0j)(Y_3 Z_1 Y_0) + (0.5+0j)(X_3 Z_2 X_0)>
// Create the hopping Hamiltonian a^dagger_0 a_3 + a^dagger_3 a_0
QfFermionOperator *hamiltonian = qf_ferm_op_zero();
QkComplex64 coeffs[2] = {{1.0, 0.0}, {1.0, 0.0}};
bool actions[4] = {true, false, true, false};
uint32_t indices[4] = {0, 3, 3, 0};
for (int i = 0; i < 2; i++) {
qf_ferm_op_add_term(hamiltonian, 2, actions + 2 * i, indices + 2 * i, &coeffs[i]);
}
QfPauliLabel order[3] = {QfPauliLabel_X, QfPauliLabel_Y, QfPauliLabel_Z};
QfTernaryTree *tree;
qf_ternary_tree_breadth_first(4, 3, order, &tree);
// Compile the tree once, then reuse the encoding for every operator mapped through it.
QfTernaryTreeEncoding *encoding;
qf_ternary_tree_encoding_new(tree, NULL, &encoding);
QkObs *qubit_op;
QfExitCode exit = qf_ferm_op_ternary_tree(hamiltonian, encoding, 4, &qubit_op);
assert(exit == QfExitCode_Success);
// Clean up
qf_ferm_op_free(hamiltonian);
qf_ternary_tree_encoding_free(encoding);
qf_ternary_tree_free(tree);
qk_obs_free(qubit_op);
Memetakan lewat rantai sepanjang Z menghasilkan persis sama dengan fermion_jordan_wigner() (qf_ferm_op_jordan_wigner() di API C), dan inilah maksud dari "menggeneralisasi" Jordan-Wigner:
>>> from qiskit_fermions.mappers.library import fermion_jordan_wigner
>>> chain = TernaryTreeEncoding(TernaryTree.chain(4, "Z"))
>>> direct = fermion_jordan_wigner(hamiltonian, 4)
>>> (fermion_ternary_tree(hamiltonian, chain, 4) - direct).simplify().num_terms
0
Setiap representasi operator punya mapper pohon terner, mengikuti set Jordan-Wigner; lihat qiskit_fermions.mappers.library untuk daftar lengkapnya, dan Mapper Library di API C. Di API Python, semuanya juga bisa diakses lewat ternary_tree(), pembungkus yang tidak bergantung pada tipe dan memilih mapper yang tepat untuk operator yang diberikan, sama seperti jordan_wigner():
>>> from qiskit_fermions.mappers.library import ternary_tree
>>> ternary_tree(hamiltonian, encoding, 4).simplify()
<SparseObservable with 2 terms on 4 qubits: (0.5+0j)(Y_3 Z_1 Y_0) + (0.5+0j)(X_3 Z_2 X_0)>
Beberapa identitas yang berlaku di Jordan-Wigner adalah sifat dari rantai, bukan dari pohon terner. Operator bilangan hanya berlaku di rantai; di pohon lain, menjadi string atas sekumpulan qubit. Demikian juga operator vertex adalah Pauli berbobot 1 hanya di rantai, tempat rantai dari dua string Majorana saling meniadakan.
Karena setiap operator bilangan tetap dipetakan ke satu string , state Fock tetap berupa state basis komputasi di pohon terner mana pun, tapi bit tidak lagi menunjukkan okupasi . Okupasi justru adalah paritas dari bit terukur pada support citra . Baca support itu dari mapper, jangan menurunkannya dari pohon, supaya dekoding tetap benar untuk bentuk atau permutasi mode apa pun:
>>> for mode in range(4):
... number = FermionOperator.from_dict({((True, mode), (False, mode)): 1.0})
... print(mode, ternary_tree(number, encoding, 4).simplify())
0 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_2 Z_1 Z_0)>
1 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_1)>
2 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_2)>
3 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_3)>
Di pohon seimbang ini, mode 0 terisi ketika qubit 0, 1, dan 2 memuat jumlah ganjil angka satu, jadi menyiapkannya berarti membalik qubit 0 saja, sedangkan mendekode bitstring hasil ukur butuh paritas dari ketiganya. Satu error bit-flip pada qubit 1 lalu merusak okupasi mode 0 dan mode 1 sekaligus, yang penting untuk workflow apa pun yang membaca okupasi dari sampel yang berderau.
Ketika sebuah Evolution diturunkan lewat mapper seperti itu, bungkus mapper tersebut dengan group_wise() (lihat Sintesis Evolution gate di ruang fermionik). Suku hopping dipetakan ke dua string Pauli yang saling komut, dan tak satu pun menjaga jumlah partikel sendirian; hanya hasil kali keduanya dalam evolusi yang menjaganya. Di Jordan-Wigner kedua string bekerja pada qubit yang sama, jadi urutan suku yang disederhanakan menjaga keduanya tetap berdekatan. Di pohon lain support keduanya berbeda (seperti Y_3 Z_1 Y_0 dan X_3 Z_2 X_0 tadi), urutan mentah maupun yang disederhanakan sama-sama menyelipkan citra suku lain di antara keduanya, dan rumus produk di sisi qubit lalu membocorkan jumlah partikel.
Bangun pohon sembarang
Konstruktor rantai dan breadth-first mencakup keluarga yang seragam, tapi antarmuka umumnya menerima satu pasangan (parent, label) per simpul, dengan None untuk akar (API C menerima indeks akar sebagai gantinya). Ini penting karena pohon yang layak dicari biasanya bukan rantai dan tidak seragam – algoritma Bonsai, misalnya, menumbuhkan pohon rentang dari graf kopling perangkat, jadi percabangannya mengikuti konektivitas itu.
- PYTHON
- C
>>> # Node 0 branches three ways, node 1 twice, and node 4 carries a single child.
>>> tree = TernaryTree([None, (0, "X"), (0, "Y"), (0, "Z"), (1, "X"), (1, "Z"), (4, "Y")])
>>> tree.num_nodes, tree.num_legs, tree.max_weight
(7, 15, 4)
>>> tree.children(0)
{'X': 1, 'Y': 2, 'Z': 3}
// Node 0, the root, branches three ways, node 1 twice, and node 4 carries a single child.
// The root has no parent and no incoming link, so its entries are never read: any value works.
#define IGNORED 0
uint32_t parents[7] = {IGNORED, 0, 0, 0, 1, 1, 4};
QfPauliLabel labels[7] = {
IGNORED, QfPauliLabel_X, QfPauliLabel_Y, QfPauliLabel_Z,
QfPauliLabel_X, QfPauliLabel_Z, QfPauliLabel_Y,
};
QfTernaryTree *tree;
QfExitCode exit = qf_ternary_tree_new(7, 0, parents, labels, &tree);
assert(exit == QfExitCode_Success);
assert(qf_ternary_tree_num_nodes(tree) == 7);
assert(qf_ternary_tree_num_legs(tree) == 15);
assert(qf_ternary_tree_max_weight(tree) == 4);
qf_ternary_tree_free(tree);
Perhatikan jumlah leg: selalu , apa pun bentuknya. Simpul dengan lebih sedikit anak tidak menghasilkan lebih sedikit string Pauli – tautan yang tidak terpakai adalah leg, dan sebuah leg mengeja string sama seperti keturunan sebuah edge. Dari string itu, dipasangkan menjadi operator Majorana untuk mode, dan tepat satu tersisa.
Paritas total
Leg yang tersisa itu adalah paritas fermionik total . Ini sifat dari encoding, bukan pemetaan operator mana pun, jadi ini sebuah metode pada encoding itu sendiri:
- PYTHON
- C
>>> TernaryTreeEncoding(TernaryTree.chain(4, "Z")).total_parity(4)
<SparseObservable with 1 term on 4 qubits: (1+0j)(Z_3 Z_2 Z_1 Z_0)>
>>> TernaryTreeEncoding(TernaryTree.breadth_first(4, 3)).total_parity(4)
<SparseObservable with 1 term on 4 qubits: (1+0j)(Z_3 Z_0)>
QfTernaryTree *tree;
qf_ternary_tree_chain(4, QfPauliLabel_Z, &tree);
QfTernaryTreeEncoding *encoding;
qf_ternary_tree_encoding_new(tree, NULL, &encoding);
// On the chain this is the full Z string; on the balanced tree it is not.
QkObs *parity;
qf_ternary_tree_encoding_total_parity(encoding, 4, &parity);
qf_ternary_tree_encoding_free(encoding);
qf_ternary_tree_free(tree);
qk_obs_free(parity);
Seperti ditunjukkan kasus seimbang, ini bukan string serba- secara umum: ini adalah leg yang dicapai oleh jalur serba-, yang bekerja sebagai identitas pada setiap simpul yang tidak ada di jalur itu.
Paritas total di sini adalah observabel untuk diukur, bukan batasan untuk dipaksakan. Kedua nilai eigennya fisis, jadi jangan memproyeksikan state ke sektor : encoding pohon terner memakai satu qubit per mode dan mencakup seluruh ruang berdimensi , sehingga tidak ada subruang tak fisis yang perlu diproyeksikan keluar.
Contoh kerja: pohon Bravyi-Kitaev
Spesifikasi lebih sering dihitung daripada diketik, karena pohon untuk encoding nyata jarang cukup kecil untuk ditulis tangan. Bravyi-Kitaev contoh yang bagus: ia termasuk keluarga ini, tapi pohonnya tidak seragam, jadi tidak ada konstruktor praktis yang mencakupnya.
Pohonnya dibangun dengan pemisahan rekursif atas simpul-simpul, satu per mode. Dalam rentang simpul , misalkan adalah pangkat dua terbesar yang muat; simpul yang ditempatkan untuk rentang itu adalah , yang terakhir di blok tersebut. Tautan X-nya turun ke pohon yang dibangun dari blok sebelumnya dan tautan Z-nya ke pohon yang dibangun dari sisa setelahnya, sedangkan tautan Y-nya dibiarkan menjadi leg:
>>> def bravyi_kitaev_tree(num_modes: int) -> TernaryTree:
... """Builds the ternary tree of the Bravyi-Kitaev encoding on ``num_modes`` modes."""
... spec: list[tuple[int, str] | None] = [None] * num_modes
... def build(left: int, right: int) -> int | None:
... if left > right:
... return None # an empty range: the parent's link stays a leg
... block = 1
... while 2 * block <= right - left + 1:
... block *= 2
... node = left + block - 1
... for child, label in (
... (build(left, node - 1), "X"), # the block before this node
... (build(node + 1, right), "Z"), # the remainder after it
... ):
... if child is not None:
... spec[child] = (node, label)
... return node
... build(0, num_modes - 1)
... return TernaryTree(spec)
Pada empat mode seluruh rentang adalah satu blok, jadi simpul yang pertama ditempatkan adalah 3, dengan sebelumnya dan tidak ada setelahnya; sub-rentang itu menghasilkan simpul 1 di atas simpul tunggal 0 dan 2. Mengompilasi pohon mengonfirmasi bahwa encoding-nya memang Bravyi-Kitaev dan bukan sekadar pohon dengan kelas bentuk yang sama – citra Majorana pertamanya adalah :
>>> tree = bravyi_kitaev_tree(4)
>>> tree.children(1)
{'X': 0, 'Z': 2}
>>> encoding = TernaryTreeEncoding(tree)
>>> encoding.majorana_image(0)
[(0, 'X'), (1, 'X'), (3, 'X')]
Paritas totalnya membuat poin bagian sebelumnya jadi konkret: leg yang tersisa adalah yang dicapai oleh jalur serba-, dan di sini jalur itu berhenti di akar, jadi observabelnya adalah satu qubit, bukan string atas keempat qubit:
>>> encoding.total_parity(4)
<SparseObservable with 1 term on 4 qubits: (1+0j)(Z_3)>
Menskalakan tidak butuh apa-apa selain argumen yang lebih besar. Karena tidak ada simpul yang memakai tautan Y-nya untuk edge, masing-masing bercabang paling banyak dua arah, jadi bobot Pauli tumbuh secara logaritmik, bukan linear:
>>> [bravyi_kitaev_tree(n).max_weight for n in (4, 8, 16, 32, 64)]
[3, 4, 5, 6, 7]