ข้ามไปยังเนื้อหาหลัก
โครงการนี้อยู่ในช่วงเบต้า พบข้อผิดพลาด? สร้าง GitHub Issue มีคำถามหรือไอเดีย? ถามใน Discussions — ยินดีรับฟังความคิดเห็น!
คุณภาพการแปลในหน้านี้เป็นอย่างไร?

จำลองพลวัต Fermi-Hubbard 2 มิติด้วย flow set

สำคัญ

แนวคิดในคู่มือนี้ใช้ได้เฉพาะใน Python API ในปัจจุบัน ฟังก์ชันที่เทียบเท่ากันจะมีใน C API ในรุ่นอนาคต

ดูเพิ่มเติม

อ่านคู่มือ flow set 1 มิติก่อน คู่มือนี้เป็นภาคต่อสองมิติของมัน และไม่ทวนข้อมูลที่พัฒนาไว้ที่นั่น: ตัวดำเนินการถ่ายโอนและจุดยอด flow set คืออะไร groups อยู่รอดเข้าไปในวงจรอย่างไร และการเข้ารหัสแบบกำหนดเองถูกเชื่อมเข้ากับ Transpiler ผ่าน MapperFnEvolutionSynthesis อย่างไร

ดูคู่มือ transpilation สำหรับขั้นตอนของ Transpiler ที่อ้างถึงตลอด และคู่มือ mappers สำหรับสูตรทั่วไปในการเขียน mapper แบบกำหนดเอง

คู่มือ 1 มิติปิดท้ายด้วยการกล่าวว่าต้องใช้การเข้ารหัสแบบท้องถิ่นเมื่อทำงานกับสองมิติ คู่มือนี้ก้าวต่อไปในขั้นนั้น สำหรับแบบจำลอง Fermi-Hubbard สองมิติบนแลตทิซสี่เหลี่ยมจัตุรัส โดยใช้การเข้ารหัส Verstraete-Cirac (VC) [2]

มีความแตกต่างสามข้อจากปัญหา 1 มิติ ซึ่งเป็นพื้นฐานของงานนี้:

  • flow set เพิ่มขึ้นหลายเท่า โซ่มีทิศทางลูกศรสองแบบ แลตทิซสี่เหลี่ยมจัตุรัสมีสี่แบบ (ตะวันออก ตะวันตก เหนือ ใต้) และแต่ละแบบเป็นการรวมกันของเส้นทางมีทิศทางที่ไม่มีจุดยอดร่วม (หนึ่งเส้นต่อแถวหรือคอลัมน์ของแลตทิซ) ไม่ใช่เส้นทางเดียว

  • การเข้ารหัสได้ ancilla เพิ่มต่อไซต์ ไม่ใช่หนึ่งตัวสำหรับทั้งระบบ ทำให้ได้​

    อัตราส่วน Qubit ต่อโหมดเท่ากับ 22 ตัวดำเนินการถ่ายโอนกลายเป็นเพาลีน้ำหนัก 3 (แนวนอน) และน้ำหนัก 4 (แนวตั้ง) และแต่ละใน flow set ทั้งสี่แมปไปยังรูปร่างเพาลีของตัวเอง ดังนั้นแต่ละชุดต้องมี Clifford ที่ทำให้เป็นแนวทแยงของตัวเอง

  • แฮมิลโทเนียนที่เข้ารหัสซื่อตรงเฉพาะบนปริภูมิย่อย: ปริภูมิไอเกน +1+1 ร่วม​

    ของข้อจำกัด plaquette ที่ทุกการเข้ารหัสท้องถิ่นแบบ 2 มิติมี [2] [3] ใน 1 มิติ การเข้ารหัสเป็นไอโซเมทรีธรรมดาและการตรวจสอบคือการเช็กสเปกตรัม ที่นี่ การเปรียบเทียบต้องถูกฉาย ดังที่ขั้นตอนที่ 9 แสดง

ผลตอบแทนมีรูปแบบเดียวกับใน 1 มิติ: ความลึกสองคิวบิตของหนึ่งขั้น Trotter คงที่ไม่ขึ้นกับขนาดแลตทิซ ที่ 2424 (1616 สำหรับ hopping ตรงกับเอกสารอ้างอิง [1] บวก 88 สำหรับอันตรกิริยา) ขณะที่การทำ Trotterization ทีละพจน์ของแฮมิลโทเนียนที่เข้ารหัสเดียวกันโตเชิงเส้น แน่นอนว่าจำนวนเกตยังโตตามจำนวนพันธะ สิ่งที่ไม่ขึ้นกับขนาดคือจำนวนเลเยอร์ที่เกตเหล่านั้นครอบครอง ความลึกทั้งหมดนับบนรีจิสเตอร์นามธรรมที่เชื่อมต่อกันเต็มที่ การแมปการเข้ารหัสลงบน coupling map ที่จำกัดของอุปกรณ์เป็นปัญหาในตัวเอง ซึ่งจะกล่าวถึงตอนท้าย

แลตทิซและทิศทางทั้งสี่​

การเปลี่ยนแปลงแรกมองเห็นได้บนกราฟอันตรกิริยามีทิศทาง ไซต์ถูกกำหนดหมายเลขตามลำดับแถวก่อน site(r, c) = r * cols + c และทุกพันธะมีทั้งสองทิศทาง เหมือนที่โซ่เป็นใน 1 มิติ แต่ตอนนี้พันธะวิ่งไปตามสองแกน:

A 3x3 square lattice with both orientations of every horizontal and vertical bond drawn as arrows, the center site carrying all four.

กฎการสลับที่เป็นของ 1 มิติ ไม่เปลี่ยน: ตัวดำเนินการถ่ายโอนสองตัวที่ใช้ไซต์ร่วมกัน สลับที่ได้ เมื่อลูกศรไหลผ่านมัน และ สลับที่แบบตรงข้าม เมื่อชนกัน อย่างไรก็ตาม ตอนนี้ไซต์มีพันธะที่ต่อเข้ามาได้สูงสุดสี่เส้นแทนที่จะเป็นสอง ลูกศรที่ผ่านมันจึงชนกันได้หลายแบบขึ้น และชุดลูกศรที่ไม่เคยชนกันก็ไม่ใช่เส้นทางเดียวอีกต่อไป การอ่านลูกศรแนวนอนจากซ้ายไปขวาให้ชุดหนึ่ง ลูกศรแนวตั้งจากบนลงล่างให้อีกชุด และการกลับทิศแต่ละชุดให้อีกสองชุด นี่คือ flow set ตะวันออก ตะวันตก เหนือ และใต้ ซึ่งจัดระเบียบทุกอย่างที่ตามมา

การเข้ารหัส Verstraete-Cirac​

Verstraete-Cirac (VC) จับคู่ทุกโหมดเฟอร์มิออน jj กับ Qubit ช่วยของมันเอง เมื่อมีไซต์แลตทิซ NN ไซต์ โหมด jj อยู่บน Qubit jj และ ancilla ของมันอยู่บน Qubit N+jN + j ดังนั้นรีจิสเตอร์มี 2N2N Qubit ตัวดำเนินการจุดยอดเรียบง่ายที่สุดเท่าที่จะเป็นได้

Vj=Zj ,V_j = Z_j \, ,

ซึ่งทำให้อ่านค่าการครอบครองได้โดยตรงในภายหลัง ตัวดำเนินการถ่ายโอนขึ้นกับทิศทางของเส้นเชื่อม สำหรับเส้นเชื่อมแนวนอน j→kj \to k โดย jj เป็นไซต์ตะวันตก และเส้นเชื่อมแนวตั้งที่ jj เป็นไซต์เหนือ

Tjkhorizontal=12XjXkZN+j ,Tjkvertical=12XjYkXN+jYN+k ,T_{jk}^{\text{horizontal}} = \tfrac{1}{2} X_j X_k Z_{N+j} \, , \qquad T_{jk}^{\text{vertical}} = \tfrac{1}{2} X_j Y_k X_{N+j} Y_{N+k} \, ,

“สามเหลี่ยม” น้ำหนัก 3 และ “สี่เหลี่ยม” น้ำหนัก 4 การระบุลูกศรแต่ละตัวมีความสำคัญ: เมื่อ jj เป็นไซต์ตะวันตก j→kj \to k ชี้ไปทางตะวันออก แต่เมื่อ jj เป็นไซต์เหนือ มันชี้ไปทางใต้ ทิศทางตรงข้ามได้จากเอกลักษณ์ Tkj=−VjVkTjkT_{kj} = -V_j V_k T_{jk} ซึ่งเปลี่ยนสามเหลี่ยม XXZ เป็น YYZ และสี่เหลี่ยม XYXY เป็น YXXY ป้ายที่ต่างกันสี่ป้ายนั้นคือ flow set ทั้งสี่ และส่วนที่เหลือของคู่มือนี้ติดตามพวกมัน

หมายเหตุ

สมการเหล่านี้ไม่ตรงกับสมการ (A1) ของ Ref. [1] เสียทีเดียว ซึ่งระบุว่า “จนถึงการหมุนของ qubit เดี่ยว” และต่างกันสามจุดที่ควรบอกไว้ เพราะคนที่ตรวจเทียบกับเปเปอร์จะสังเกตเห็น:

  • ตัวคูณนำหน้า: สมการ (A1) ไม่มีตัวคูณนี้ ส่วน 12\tfrac12 ในที่นี้คือสิ่งที่ทำให้ Tjk2=14T_{jk}^2 = \tfrac14 ตรงกับการทำนอร์มัลไลซ์ Tjk=−12(aj†−aj)(ak†+ak)T_{jk} = -\tfrac12 (a^\dagger_j - a_j)(a^\dagger_k + a_k) ที่ TransferVertexOperator ใช้ เปเปอร์ต้องใช้ transfer operator เฉพาะภายใน exp⁡(−i dt H)\exp(-i\,dt\,H) ซึ่งตัวคูณสเกลถูกดูดซับเข้าไปในมุมหมุนได้ แต่ในที่นี้มันต้องเป็น operator ที่เป็นไปตามเอกลักษณ์หนึ่ง

  • ทิศทาง: สมการ (A1) เลือกตัวอักษรตามความเป็นคู่คี่ของแถวของ jj ซึ่งสำหรับ edge แนวนอนจะเหมือนกันทั้งสองปลาย จึงแยก TjkT_{jk} ออกจาก TkjT_{kj} ไม่ได้ เปเปอร์กู้ทิศทางกลับมาแยกต่างหากโดยการคอนจูเกตด้วยชั้นของ RZ(π/2)R^Z(\pi/2) ส่วนคู่มือนี้ฝังมันไว้ในการเข้ารหัสแทน โดยหาทิศตรงข้ามจาก Tkj=−VjVkTjkT_{kj} = -V_j V_k T_{jk} ซึ่งเป็นสิ่งที่ทำให้สองทิศทางตกลงบน Pauli ที่ต่างกันในขั้นตอนที่ 3

  • รูปร่างแนวตั้ง: สมการ (A1) สลับ XXXX กับ YYYY ตามความเป็นคู่คี่ของแถว ส่วนคู่มือนี้ใช้ XYXY แบบเดียว สองวิธีนี้ให้ผลเดียวกัน คือความไม่สมมาตรของตัวอักษรที่ทำให้ edge แนวตั้งขัดกับ edge แนวนอนที่มาพบกันได้อย่างถูกต้อง อย่าตีความความต่างนี้ว่าเป็นการเลือก basis ได้อย่างอิสระ การเลิกสลับตามความเป็นคู่คี่แล้วใช้ XXXX แบบเดียวจะทำให้พีชคณิตพังทันที จึงต้องตรวจสอบว่าตัวอักษรต่างกันจริง แทนที่จะสมมติเอา

หมายเหตุ

ancilla ถูกผูกกับ site ทำให้ได้อัตราส่วน 22 ส่วนการเข้ารหัส Derby-Klassen จะวาง qubit เสริมหนึ่งตัวต่อ plaquette (ที่เป็นเลขคี่) เพื่อให้ได้อัตราส่วนที่เล็กกว่าคือ 3/23/2 ที่ใช้ VC ในที่นี้เพราะ transfer operator ของมันคือตัวที่ Ref. [1] ให้ Clifford สำหรับทำให้เป็นแนวทแยงในรูปแบบปิด

1. Hamiltonian บนแลตทิซสี่เหลี่ยม​

การสร้างเหมือนกับกรณี 1D แต่ใช้รายการ edge แบบสองมิติ site ใช้การนับเลขแบบ row-major เหมือนในรูปด้านบน site(r, c) = r * cols + c ดังนั้นพันธะแนวนอนเชื่อมดัชนีที่ต่อเนื่องกัน ส่วนพันธะแนวตั้งเชื่อมดัชนีที่ต่างกัน cols:

>>> from collections import defaultdict
>>> from qiskit_fermions.operators import TransferVertexOperator
>>>
>>> def fermi_hubbard_2d(rows, cols, tunneling, interaction):
... site = lambda r, c: r * cols + c
... edges = [(site(r, c), site(r, c + 1)) for r in range(rows) for c in range(cols - 1)]
... edges += [(site(r, c), site(r + 1, c)) for r in range(rows - 1) for c in range(cols)]
... data: defaultdict[tuple[tuple[int, int], ...], complex] = defaultdict(complex)
... for j, k in edges:
... # hopping: -t (a^dag_j a_k + a^dag_k a_j) = t (T_jk + T_kj)
... data[((j, k),)] += tunneling
... data[((k, j),)] += tunneling
... # interaction: U n_j n_k with n_j = (1 - V_j) / 2
... data[()] += interaction / 4
... data[((j, j),)] -= interaction / 4
... data[((k, k),)] -= interaction / 4
... data[((j, j), (k, k))] += interaction / 4
... return TransferVertexOperator.from_dict(data)
>>>
>>> rows, cols = 3, 3
>>> num_sites = rows * cols
>>> hamiltonian = fermi_hubbard_2d(rows, cols, tunneling=1.0, interaction=2.0)
>>> print(f"Number of terms: {len(hamiltonian)}")
Number of terms: 46

2. สี่ flow set แทนที่จะเป็นสอง​

แต่ละ hopping term ถูกจัดประเภทตามทิศที่ลูกศรของมันชี้บนแลตทิซ การเปรียบเทียบพิกัดแถว/คอลัมน์จาก divmod ของสองปลายจะแยก edge แนวนอนออกจากแนวตั้ง และเครื่องหมายของผลต่างจะบอกทิศทาง ทุกอย่างที่เป็นแนวทแยง (เอกลักษณ์, เทอม VjV_j และ VjVkV_j V_k) จะเข้ากลุ่มที่ห้า:

หมายเหตุ

ข้อตกลงเรื่องทิศทาง ชื่อเหล่านี้อ่านจากรูปด้านบนตรง ๆ: ลูกศรเป็น east หรือ west ตามที่ชี้ไปทางขวาหรือซ้ายตามแถว และเป็น north หรือ south ตามที่ชี้ขึ้นหรือลงตามคอลัมน์ เนื่องจากการนับเลขเป็นแบบ row-major ทิศ east และ south จึงเป็นทิศที่ดัชนีเพิ่มขึ้น ขั้นตอนถัด ๆ ไปไม่ได้พึ่งชื่อเหล่านี้ (มันเป็นแค่ป้ายของกลุ่ม) แต่สับสนกันง่าย จึงควรยึดให้ตรงกับรูป Ref. [1] ตั้งชื่อ flow set ของแลตทิซสี่เหลี่ยมแบบเดียวกัน

>>> EAST, WEST, NORTH, SOUTH, DIAGONAL = range(5)
>>>
>>> def flow_set_groups(operator, cols):
... groups = []
... for terms, _ in operator.iter_terms():
... if len(terms) == 1 and terms[0][0] != terms[0][1]:
... j, k = terms[0]
... row_j, col_j = divmod(j, cols)
... row_k, col_k = divmod(k, cols)
... if row_j == row_k:
... groups.append(EAST if col_k > col_j else WEST)
... else:
... groups.append(SOUTH if row_k > row_j else NORTH)
... else:
... groups.append(DIAGONAL)
... return groups
>>>
>>> hamiltonian.groups = flow_set_groups(hamiltonian, cols)
>>>
>>> names = ["east", "west", "north", "south", "diagonal"]
>>> for index, name in enumerate(names):
... count = sum(1 for group in hamiltonian.groups if group == index)
... print(f"{name:9s} {count:2d}")
east 6
west 6
north 6
south 6
diagonal 22

ภายใน flow set เดียว ลูกศรทุกตัวจะต่อกันแบบหัวชนหาง หรือไม่ก็ไม่ได้พบกันเลย เหมือนใน 1D ดังนั้น flow set ยังคอมมิวต์กันทีละ term และการวิวัฒน์ของมันไม่มีความคลาดเคลื่อนแบบ Trotter สิ่งที่ใหม่คือตอนนี้ flow set ไม่เชื่อมต่อกันแล้ว: ชุด east ของแลตทิซ rows x cols ประกอบด้วยเส้นทางซ้ายไปขวา rows เส้นที่เป็นอิสระต่อกัน การไม่เชื่อมต่อนี้เป็นของขวัญมากกว่าความยุ่งยาก ดังที่ขั้นตอนที่ 6 แสดง

3. เข้ารหัส operator​

การเข้ารหัสคือฟังก์ชันเดียวที่แมป generalized transfer operator หนึ่งตัวไปเป็น Pauli string ซึ่งส่งให้ map_transfer_vertex_generators() เมื่อเทียบกับเวอร์ชัน 1D ตัวฟังก์ชันมีกรณีมากขึ้น: แนวทแยง แนวนอน แนวตั้ง และทิศตรงข้ามผ่าน Tkj=−VjVkTjkT_{kj} = -V_j V_k T_{jk}

มีปัญหาอยู่อย่างหนึ่ง map_transfer_vertex_generators() เรียกกลับมาพร้อมกับ generator เท่านั้น แต่การตัดสินใจว่า Pauli string ของพันธะหนึ่งคืออะไรยังต้องใช้ cols เพื่อกู้พิกัดแถวและคอลัมน์จากดัชนีของ mode ด้วย แทนที่จะซ้อนฟังก์ชันเพื่อให้จับค่านั้นไว้ เราใช้ functools.partial() ผูกค่าไว้ ซึ่งทำให้ทั้งสองฟังก์ชันอยู่ระดับบนสุดและเรียกใช้แยกกันได้

>>> from functools import partial
>>>
>>> from qiskit.quantum_info import SparseObservable
>>> from qiskit_fermions.mappers import map_transfer_vertex_generators
>>>
>>> def ancilla_qubit(mode, num_sites):
... """The auxiliary qubit paired with fermionic mode `mode`."""
... return num_sites + mode
>>>
>>> def vc_action(generator, width, cols):
... """Map one generalized transfer operator to its Verstraete-Cirac Pauli."""
... num_sites = width // 2
... ancilla = partial(ancilla_qubit, num_sites=num_sites)
... j, k = generator
... if j == k: # vertex operator V_j = Z_j
... return SparseObservable.from_sparse_list([("Z", [j], 1.0)], num_qubits=width)
... row_j, col_j = divmod(j, cols)
... row_k, col_k = divmod(k, cols)
... if row_j == row_k: # horizontal: weight-3 triangle
... left, right = (j, k) if col_j < col_k else (k, j)
... operator = SparseObservable.from_sparse_list(
... [("XXZ", [left, right, ancilla(left)], 0.5)], num_qubits=width
... )
... forward = (j, k) == (left, right)
... else: # vertical: weight-4 square
... up, down = (j, k) if row_j < row_k else (k, j)
... operator = SparseObservable.from_sparse_list(
... [("XYXY", [up, down, ancilla(up), ancilla(down)], 0.5)], num_qubits=width
... )
... forward = (j, k) == (up, down)
... if forward:
... return operator
... # reverse orientation: T_kj = -V_j V_k T_jk
... v_j = SparseObservable.from_sparse_list([("Z", [j], 1.0)], num_qubits=width)
... v_k = SparseObservable.from_sparse_list([("Z", [k], 1.0)], num_qubits=width)
... return (-1.0 * v_j.compose(v_k).compose(operator)).simplify()
>>>
>>> def vc_encoding(operator, width, cols):
... """Map a TransferVertexOperator to a qubit observable, Verstraete-Cirac style."""
... return map_transfer_vertex_generators(
... operator,
... partial(vc_action, width=width, cols=cols),
... identity=lambda: SparseObservable.identity(width),
... compose=SparseObservable.compose,
... ).simplify()
>>>
>>> # bind the lattice width once; the transpiler only ever passes (operator, width)
>>> encoding = partial(vc_encoding, cols=cols)
>>> num_qubits = 2 * num_sites
>>> num_qubits
18

ประโยชน์ของการจัดกลุ่มก่อนแมปเห็นได้ทันที: แต่ละ flow set เข้ารหัสเป็นรูปร่าง Pauli เดียว สม่ำเสมอทั้งชุด

>>> for index, name in enumerate(names):
... observable = encoding(hamiltonian.split_out_groups()[index], num_qubits)
... shapes = sorted({label for label, _, _ in observable.to_sparse_list()})
... print(f"{name:9s} {shapes}")
east ['XXZ']
west ['YYZ']
north ['YXXY']
south ['XYXY']
diagonal ['', 'Z', 'ZZ']

รูปร่าง hopping ทั้งสี่ต่างกัน ดังนั้นแต่ละ flow set จึงมี Clifford ของตัวเองในขั้นตอนที่ 5 ทิศตรงข้ามคือ Pauli คนละตัว ไม่ใช่ตัวเดิมที่เปลี่ยนป้าย นี่คือเหตุผลที่การสังเคราะห์ flow set แบบ 2D ต้องใช้สี่วงจร ในขณะที่การสังเคราะห์แบบ 1D ใช้วงจรเดียว

4. Transpile ด้วยการเข้ารหัสแบบกำหนดเอง​

ขั้นตอนนี้คือขั้นตอนของ 1D ที่ใช้รีจิสเตอร์ qubit กว้างขึ้น: CustomF2QLayout แมปรีจิสเตอร์ NN mode ไปยัง 2N2N qubit, MapperFnEvolutionSynthesis ถือการเข้ารหัสไว้ และอาร์กิวเมนต์ product_formula ของมันเลือกวิธีที่ PauliEvolutionGate ที่ได้ถูกแยกส่วน เปรียบเทียบกับขั้นตอนที่ 6 ของคู่มือ 1D ซึ่งสร้างไปป์ไลน์เดียวกันสำหรับ ancilla ตัวเดียว

>>> from qiskit.circuit import QuantumRegister
>>> from qiskit.passmanager import MultiStagePassManager
>>> from qiskit.synthesis import LieTrotter
>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import Evolution
>>> from qiskit_fermions.circuit.library.synthesis import FermionicLieTrotter
>>> from qiskit_fermions.transpiler import FermionicCircuitToDAG, QuantumDAGToCircuit
>>> from qiskit_fermions.transpiler.passes import (
... CustomF2QLayout,
... F2QSynthesis,
... MapperFnEvolutionSynthesis,
... )
>>>
>>> total_time = 1.0
>>>
>>> def flow_set_pass_manager(mode_register, encoding, product_formula=None):
... """Build the pipeline mapping `mode_register` with the VC `encoding`."""
... synthesis = F2QSynthesis()
... synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(
... encoding, product_formula=product_formula
... )
... # this encoding spends one ancilla per fermionic mode
... qubit_register = QuantumRegister(2 * mode_register.size, "q")
... return MultiStagePassManager(
... input=FermionicCircuitToDAG(),
... layout=CustomF2QLayout({mode_register: qubit_register}),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>>
>>> circuit = FermionicCircuit(num_sites)
>>> circuit.append(
... Evolution(
... num_sites, hamiltonian, time=total_time, synthesis=FermionicLieTrotter()
... ),
... circuit.modes,
... )
>>>
>>> # as in 1D, one decompose() splits the Evolution group by group (five gates here,
>>> # the four hopping flow sets in group order, then the diagonal interaction)
>>> flow_set_circuit = circuit.decompose()
>>> flow_set_circuit.count_ops()["Evolution"]
5

ห้า Gate ซึ่งมี 6, 6, 6, 6 และ 22 term ตามที่นับในขั้นตอนที่ 2 แต่ละตัวถูกแมปและสังเคราะห์อย่างอิสระ ดังนั้นการแบ่งส่วนที่ตัดสินใจไว้ในระดับเฟอร์มิออนจึงคงอยู่จนถึงวงจร:

>>> pass_manager = flow_set_pass_manager(flow_set_circuit.register, encoding, LieTrotter())
>>> qubit_circuit = pass_manager.run(flow_set_circuit)
>>> qubit_circuit.num_qubits
18
>>>
>>> def two_qubit_stats(circuit):
... decomposed = circuit.decompose(reps=6)
... two_qubit = lambda instruction: len(instruction.qubits) == 2
... return decomposed.count_ops().get("cx", 0), decomposed.depth(two_qubit)
>>>
>>> two_qubit_stats(qubit_circuit)
(144, 47)

นี่คือ baseline: ทั้งการเข้ารหัสและการจัดกลุ่มอยู่ครบแล้ว แต่ LieTrotter สังเคราะห์แต่ละ flow set ทีละ term และความลึกเพิ่มขึ้นตามแลตทิซ

5. Clifford ความลึก 2 ต่อ flow set​

กลไกเหมือนกับใน 1D: คอนจูเกต flow set ที่คอมมิวต์กันทั้งชุดด้วย Clifford ที่แมปแต่ละ term ไปเป็น Pauli น้ำหนัก 1 ที่ต่างกัน เพื่อให้การวิวัฒน์ของทั้งชุดใช้เพียงการหมุน qubit เดี่ยว ใน 1D ชั้นอิฐของ Gate CZ ชั้นเดียวทำสิ่งนี้ได้

ใน 2D แต่ละ flow set ต้องมีวงจรของตัวเอง ตามหัวข้อ IV C และรูปที่ 7 ของ Ref. [1] ทั้งสี่ทำงานกับเส้นหนึ่งของแลตทิซทีละเส้น (แถวสำหรับ east/west คอลัมน์สำหรับ north/south) พร้อมกับ ancilla ของเส้นนั้น และทั้งสี่มีรูปแบบสามส่วนเหมือนกัน:

  • ชั้นของการหมุน qubit เดี่ยว ที่ใส่แต่ละ qubit ไว้ใน basis ที่ flow set ของมันต้องการ

  • ชั้น CX ชั้นแรก ซึ่งเป็น site-ไป-ancilla บน site เดียวกันเสมอ

  • ชั้น CX ชั้นที่สอง ซึ่งเชื่อมเพื่อนบ้านตามแนวเส้น

ขั้นตอนที่ 2 และ 3 เป็นสองชั้นสอง qubit เพียงชั้นเดียวที่มี ดังนั้นความลึกสอง qubit คือ 22 ไม่ว่าเส้นจะยาวเท่าใด ด้านล่างเขียนออกมาเป็นหนึ่งฟังก์ชันต่อ flow set การหมุนเป็นแบบนั้นเพราะมันถูกค้นพบโดยการคอนจูเกต term ของแต่ละ flow set และกำหนดให้ภาพที่ได้เป็นน้ำหนัก 1 ที่ต่างกัน ดังนั้นให้ถือว่าเป็นค่าที่กำหนดให้ และพึ่งการตรวจสอบที่ตามมา

สองชุดแนวนอนง่ายที่สุด term ของมันมีน้ำหนัก 3 คือ XXZ และ YYZ และต่างกันเพียง SS เพิ่มเติมที่เปลี่ยน XX เป็น YY บน site:

>>> from qiskit.circuit import QuantumCircuit
>>>
>>> def east_clifford(circuit, line, ancilla):
... """Diagonalize the east flow set (XXZ) on one lattice row."""
... for j in line:
... circuit.h(ancilla(j))
... for j in line[:-1]: # layer 1: each site with its own ancilla
... circuit.cx(ancilla(j), j)
... for j, k in zip(line, line[1:]): # layer 2: ancilla with the next site along
... circuit.cx(k, ancilla(j))
>>>
>>> def west_clifford(circuit, line, ancilla):
... """Diagonalize the west flow set (YYZ) on one lattice row."""
... for j in line:
... circuit.s(j) # the only difference from east: X -> Y on the sites
... circuit.h(ancilla(j))
... for j in line[:-1]:
... circuit.cx(ancilla(j), j)
... for j, k in zip(line, line[1:]):
... circuit.cx(k, ancilla(j))

สองชุดแนวตั้งมีน้ำหนัก 4 คือ XYXY และ YXXY และวุ่นวายกว่าตามไปด้วย: การหมุนสลับไปตามตำแหน่งบนเส้น และชั้น CX ชั้นที่สองก็สลับเช่นกัน โดยเชื่อม site ที่ตำแหน่งคู่และ ancilla ที่ตำแหน่งคี่ (หรือกลับกัน):

>>> def south_clifford(circuit, line, ancilla):
... """Diagonalize the south flow set (XYXY) on one lattice column."""
... for position, j in enumerate(line):
... if position % 2 == 0:
... circuit.sdg(ancilla(j))
... circuit.h(ancilla(j))
... else:
... circuit.h(j)
... circuit.s(j)
... circuit.h(ancilla(j))
... circuit.s(ancilla(j))
... for j in line: # layer 1
... circuit.cx(ancilla(j), j)
... for position, (j, k) in enumerate(zip(line, line[1:])): # layer 2
... if position % 2 == 0:
... circuit.cx(ancilla(j), ancilla(k)) # ancilla to ancilla
... else:
... circuit.cx(j, k) # site to site
>>>
>>> def north_clifford(circuit, line, ancilla):
... """Diagonalize the north flow set (YXXY) on one lattice column."""
... for position, j in enumerate(line):
... if position % 2 == 0:
... circuit.h(ancilla(j))
... circuit.s(ancilla(j))
... else:
... circuit.h(j)
... circuit.s(j)
... circuit.sdg(ancilla(j))
... circuit.h(ancilla(j))
... for j in line: # layer 1
... circuit.cx(ancilla(j), j)
... for position, (j, k) in enumerate(zip(line, line[1:])): # layer 2
... if position % 2 == 0:
... circuit.cx(k, j) # site to site, and the other way round from south
... else:
... circuit.cx(ancilla(j), ancilla(k))

เมื่อวาดบนเส้นสาม site เส้นเดียว (site ก่อน แล้วตามด้วย ancilla ของมัน) กลไกทั้งหมดก็ลงในรูปเดียวได้ รูปสามเหลี่ยมชัดเจนกว่าในสองแบบ: การเปลี่ยน basis บน ancilla จากนั้นสองชั้น CX โดยชั้นแรกจับคู่แต่ละ site กับ ancilla ของมันเอง และชั้นที่สองเอื้อมไปถึง site ถัดไปตามเส้น

>>> line = [0, 1, 2] # three sites, with ancillas on qubits 3, 4, 5
>>> triangle = QuantumCircuit(6)
>>> east_clifford(triangle, line, lambda mode: 3 + mode)
>>> triangle.draw("mpl")
<Figure size ... with 1 Axes>

The east Clifford on a three-site line: three H gates on the ancillas followed by two layers of two CX gates each.

รูปสี่เหลี่ยมต้องการงาน qubit เดี่ยวมากกว่า (การหมุนสลับไปตามเส้น) และชั้น CX ชั้นที่สองของมันสลับระหว่างการเชื่อม site กับการเชื่อม ancilla:

>>> square = QuantumCircuit(6)
>>> south_clifford(square, line, lambda mode: 3 + mode)
>>> square.draw("mpl")
<Figure size ... with 1 Axes>

The south Clifford on a three-site line: alternating single-qubit rotations followed by two CX layers, the second mixing site-site and ancilla-ancilla pairs.

ทั้งสองลึกสองชั้น CX และการเพิ่ม site ให้เส้นทำให้มันกว้างขึ้นโดยไม่ลึกขึ้น นั่นคือข้ออ้างเรื่องความลึกคงที่ทั้งหมด ซึ่งเห็นได้จากการตรวจดู Clifford ของ west และ north ต่างกันเพียงชั้น qubit เดี่ยว จึงไม่ได้วาดแยกไว้

สิ่งที่การสังเคราะห์ยังต้องการคือการหาว่าแต่ละ term ไปลงที่ใด เส้นที่มี LL site มี L−1L - 1 พันธะ แต่ละพันธะให้หนึ่ง term ดังนั้นตัวช่วยด้านล่างจึงคืนค่าหนึ่งรายการต่อพันธะ: qubit ที่ term นั้นไปลง และ Pauli qubit เดี่ยวที่มันกลายเป็น ประเด็นสำคัญคือค่าเหล่านี้ถูกอ่านจากการคอนจูเกต ไม่ใช่ทำนายเอา ต่างจาก 1D ที่ชั้นอิฐ CZ ชั้นเดียวแมป Xj+1↦ZjXj+1Zj+2X_{j+1} \mapsto Z_j X_{j+1} Z_{j+2} ในรูปแบบปิด ในที่นี้ไม่มีสูตรเช่นนั้น และ ancilla ของเส้นคอลัมน์ก็ไม่ต่อเนื่องกัน สิ่งเดียวที่ต่างกันระหว่าง flow set คือ Pauli string ที่ป้อนเข้าไป ซึ่งขั้นตอนที่ 3 พิมพ์ออกมาแล้ว:

>>> from qiskit.quantum_info import Clifford, SparsePauliOp
>>>
>>> def clifford_supports(clifford, pauli, line, ancilla, num_qubits):
... """Conjugate a flow set's terms to find which qubit each one lands on.
...
... `pauli` is the flow set's Pauli string, as printed in step 3. Its letters apply
... to the i-th term's qubits in the order below: the two sites of the bond, then
... their ancillas.
...
... This is the one check that needs SparsePauliOp: Clifford conjugation wants a
... `Pauli` and its symplectic bits, and SparseObservable exposes neither -- its own
... `evolve` conjugates by a Pauli, not by a Clifford.
... """
... supports = []
... for i in range(len(line) - 1):
... qubits = [line[i], line[i + 1], ancilla(line[i]), ancilla(line[i + 1])]
... term = SparsePauliOp.from_sparse_list(
... [(pauli, qubits[: len(pauli)], 1.0)], num_qubits=num_qubits
... )
... image = term.paulis[0].evolve(clifford, frame="s")
... # a weight-1 image has exactly one qubit with an X and/or Z bit set
... (support,) = np.nonzero(image.x | image.z)
... assert len(support) == 1, f"term {i} is not weight-1"
... supports.append((int(support[0]), image[support].to_label()))
... return supports

ภาพที่ได้ต้องออกมาต่างกันและมีน้ำหนัก 1 เพื่อให้แต่ละ term หมุนได้อย่างอิสระบน qubit ของตัวเอง ความลึกสอง qubit 22 เห็นได้อยู่แล้วในสี่โครงสร้างข้างต้น ซึ่งแต่ละอันให้สองชั้น CX โดยไม่ขึ้นกับความยาวเส้น ด้านล่างตรวจสอบทั้งสองคุณสมบัติกับทุก flow set ทุกเส้น และแลตทิซหลายขนาด จากนั้นขั้นตอนที่ 6 เรียก clifford_supports ดังนั้นมันจึงเป็นส่วนที่ใช้งานจริงของการสังเคราะห์ ไม่ใช่แค่การสาธิต

6. การสังเคราะห์ที่รับรู้ flow set​

การสังเคราะห์ได้ตัวช่วยเริ่มต้นที่ดี เพราะ Evolution แยกตัวเองทีละกลุ่ม synthesize จึงถูกเรียกหนึ่งครั้งต่อ flow set (ห้าครั้งในที่นี้ คือสี่ชุด hopping บวกกลุ่มแนวทแยง) ดังนั้นทุก hopping term ที่มันเห็นมีรูปร่าง Pauli เดียวกัน และ Clifford ตัวเดียวทำให้ทั้งหมดเป็นแนวทแยง มันเพียงอ่านรูปร่างนั้นจาก term แรกและเรียงที่เหลือตามเส้นที่มันอยู่ แล้วก็มาถึงข้อสังเกตที่ทำให้สองมิติถูกกว่าที่คิดไว้:

สำคัญ

ส่วนประกอบของ flow set เดียวไม่มีจุดยอดร่วมกัน: แถวของชุด west ไม่ใช้ qubit ร่วมกัน ดังนั้น Clifford ทั้งหมดของ flow set จึงปล่อยออกมาพร้อมกันได้ และใช้สองชั้นสอง qubit ชุดเดียวกัน แทนที่จะต่อคิวทีละแถว flow set หนึ่งจึงมีต้นทุนเป็นจำนวนชั้นคงที่ (สองสำหรับ Clifford และสองสำหรับอินเวอร์ส) ไม่ว่าแลตทิซจะมีกี่แถวหรือกี่คอลัมน์ ซึ่งเป็นเหตุผลที่ความลึกรวมคงที่

>>> from qiskit.synthesis.evolution import EvolutionSynthesis
>>>
>>> # The interaction couples nearest neighbors only, so every interior site carries four
>>> # Rzz gates -- one per bond -- and can only do one at a time: four layers is the best
>>> # any schedule can achieve. Horizontal bonds split by column parity, vertical ones by
>>> # row parity, which achieves it.
>>> def rzz_layers(circuit, angles, cols):
... """Emit the diagonal Rzz gates in four layers of disjoint qubit pairs."""
... horizontal = [(j, k) for j, k in angles if k - j == 1]
... vertical = [(j, k) for j, k in angles if k - j == cols]
... schedule = [
... [(j, k) for j, k in horizontal if (j % cols) % 2 == 0],
... [(j, k) for j, k in horizontal if (j % cols) % 2 == 1],
... [(j, k) for j, k in vertical if (j // cols) % 2 == 0],
... [(j, k) for j, k in vertical if (j // cols) % 2 == 1],
... ]
... # emit layer by layer, since Qiskit's depth follows the order gates were added
... for layer in schedule:
... for j, k in layer:
... circuit.rzz(angles[j, k], j, k)
>>>
>>> class FlowSetSynthesis(EvolutionSynthesis):
... """Synthesize one 2D line flow set at a time, via its depth-2 Clifford."""
...
... preserve_order = True
...
... def __init__(self, rows, cols):
... self.rows, self.cols = rows, cols
...
... def rows_of_lattice(self):
... return [[r * self.cols + c for c in range(self.cols)] for r in range(self.rows)]
...
... def columns_of_lattice(self):
... return [[r * self.cols + c for r in range(self.rows)] for c in range(self.cols)]
...
... def synthesize(self, evolution):
... num_qubits = 2 * self.rows * self.cols
... ancilla = partial(ancilla_qubit, num_sites=self.rows * self.cols)
... circuit = QuantumCircuit(evolution.operator.num_qubits)
... terms = [
... (label, indices, 2 * evolution.time * coeff.real)
... for label, indices, coeff in evolution.operator.to_sparse_list()
... ]
...
... # `Evolution.decompose()` hands us one gate per flow set, so every hopping term
... # here shares a single Pauli shape and one Clifford diagonalizes all of them
... shapes = tuple({label for label, _, _ in terms} - {"", "Z", "ZZ"})
... match shapes:
... case ("XXZ",):
... clifford_function, lines = east_clifford, self.rows_of_lattice()
... along_rows = True
... case ("YYZ",):
... clifford_function, lines = west_clifford, self.rows_of_lattice()
... along_rows = True
... case ("XYXY",):
... clifford_function, lines = south_clifford, self.columns_of_lattice()
... along_rows = False
... case ("YXXY",):
... clifford_function, lines = north_clifford, self.columns_of_lattice()
... along_rows = False
... case (): # the diagonal group: no conjugation needed
... self.diagonal(circuit, terms)
... return circuit
... case _:
... raise ValueError(f"expected one known flow set per gate, got {shapes}")
...
... # a term's line is the row it sits in and its slot the column, or vice versa
... rotations: dict[int, list] = defaultdict(list)
... for _, indices, angle in terms:
... row, col = divmod(indices[0], self.cols)
... line, slot = (row, col) if along_rows else (col, row)
... rotations[line].append((slot, angle))
...
... active = sorted(rotations)
... # the lines of one flow set are vertex-disjoint, so all of their Cliffords
... # are emitted together and share the same two two-qubit layers
... conjugation = QuantumCircuit(num_qubits)
... for line in active:
... clifford_function(conjugation, lines[line], ancilla)
... circuit.compose(conjugation, inplace=True)
... for line in active:
... supports = clifford_supports(
... Clifford(conjugation), shapes[0], lines[line], ancilla, num_qubits
... )
... for slot, angle in rotations[line]:
... match supports[slot]:
... case qubit, "X":
... circuit.rx(angle, qubit)
... case qubit, "Y":
... circuit.ry(angle, qubit)
... case qubit, "Z":
... circuit.rz(angle, qubit)
... circuit.compose(conjugation.inverse(), inplace=True)
... return circuit
...
... def diagonal(self, circuit, terms):
... """Synthesize the diagonal group: a phase, single-qubit Z and paired Rzz."""
... rzz = {}
... for label, indices, angle in terms:
... match label, indices:
... case "", _: # the identity contributes only a global phase
... circuit.global_phase -= angle / 2
... case "Z", [qubit]: # a lone vertex operator
... circuit.rz(angle, qubit)
... case "ZZ", [j, k]: # interaction, see rzz_layers above
... rzz[min(j, k), max(j, k)] = angle
... case _:
... raise ValueError(f"unexpected Pauli term: {label} on {indices}")
... rzz_layers(circuit, rzz, self.cols)

เช่นเดียวกับ 1D ฟังก์ชันนี้เขียนมาสำหรับการเข้ารหัสนี้โดยเฉพาะ: มันรู้จักรูปร่าง Pauli ทั้งสี่ และยกข้อผิดพลาดกับอย่างอื่นแทนที่จะถอยกลับไปเงียบ ๆ มันยังสมมติว่าแบ่งแบบหนึ่ง flow set ต่อหนึ่ง Gate ข้างต้น และบอกไว้ตรง ๆ ด้วย: รูปร่าง hopping มากกว่าหนึ่งแบบใน Gate เดียวหมายความว่าไม่ได้จัดกลุ่ม ซึ่งเป็นข้อผิดพลาด ไม่ใช่สิ่งที่ต้องหาทางหลีก

หมายเหตุ

เช่นเดียวกับคู่เทียบใน 1D การสังเคราะห์นี้ไม่รับจำนวนขั้นของ Trotter ขั้น Trotter หนึ่งต้องวนเวียน ระหว่าง flow set แต่ transpile_trotter_step ด้านล่างแยกวงจรเฟอร์มิออนออกเป็น Gate Evolution หนึ่งตัวต่อชุด ดังนั้นเมธอดนี้เห็นทีละชุดเท่านั้น และทำซ้ำได้เพียง ภายใน ชุด ซึ่งไม่เปลี่ยนอะไร: การทำซ้ำ operator ตัวเดียวก็แค่แบ่งการวิวัฒน์ของมันเป็นชิ้นเท่า ๆ กัน

ดังนั้น Trotterization หลายขั้นต้องใช้ product formula ที่เคารพ groups ของ operator ภายใน Gate เดียว ก่อนการแยกส่วน FermionicLieTrotter ทำสิ่งนั้น และขั้นตอนที่ 10 ใช้มันเพื่อวัดการลู่เข้า

สังเกตสองคลาสที่ชื่อคล้ายกันซึ่งเกี่ยวข้องอยู่ LieTrotter ของ Qiskit ที่ใช้เป็น baseline ข้างต้น ทำงานกับ Hamiltonian ที่ แมปแล้ว ซึ่งการจัดกลุ่มหายไปแล้ว ส่วน FermionicLieTrotter คือคู่เทียบแบบเฟอร์มิออน และเป็นตัวที่ยังเห็น flow set ได้

7. ผลลัพธ์: ความลึกคงที่​

ทุกอย่างจนถึงตอนนี้รันบนแลตทิซ 3×33 \times 3 เดียวที่สร้างในขั้นตอนที่ 1 การเปรียบเทียบสองเส้นทางการสังเคราะห์ข้ามแลตทิซที่โตขึ้นต้องทำลำดับทั้งหมดนั้น (operator, การจัดกลุ่ม, การเข้ารหัส, วงจร, pass manager) ซ้ำต่อขนาด จึงควรห่อไว้ครั้งเดียว:

>>> def transpile_trotter_step(rows, cols, product_formula, steps=1):
... """Transpile `steps` Trotter steps on a `rows` x `cols` lattice."""
... num_sites = rows * cols
... operator = fermi_hubbard_2d(rows, cols, 1.0, 2.0)
... operator.groups = flow_set_groups(operator, cols)
... encoding = partial(vc_encoding, cols=cols)
...
... circuit = FermionicCircuit(num_sites)
... circuit.append(
... Evolution(
... num_sites,
... operator,
... time=total_time,
... synthesis=FermionicLieTrotter(reps=steps),
... ),
... circuit.modes,
... )
... circuit = circuit.decompose() # split into one Evolution per flow set
...
... pass_manager = flow_set_pass_manager(circuit.register, encoding, product_formula)
... return pass_manager.run(circuit)

การเก็บทั้งสองเมตริกข้ามช่วงขนาดแลตทิซ และพล็อตเทียบกับขอบของแลตทิซ ทำให้ความต่างชัดเจน:

>>> sizes = [3, 4, 5, 6, 7, 8]
>>> lie_trotter = [
... two_qubit_stats(transpile_trotter_step(n, n, LieTrotter())) for n in sizes
... ]
>>> flow = [
... two_qubit_stats(transpile_trotter_step(n, n, FlowSetSynthesis(n, n))) for n in sizes
... ]
>>>
>>> figure, axes = plt.subplots(1, 2, figsize=(9, 3.5), layout="constrained")
>>> for panel, index, title in zip(axes, (1, 0), ("two-qubit depth", "CX count")):
... _ = panel.plot(sizes, [stat[index] for stat in lie_trotter], "o-", label="qubit-level")
... _ = panel.plot(sizes, [stat[index] for stat in flow], "s-", label="flow set")
... _ = panel.set(xlabel="lattice edge $L$ (an $L \\times L$ lattice)", title=title)
... _ = panel.legend()
>>> figure
<Figure size ... with 2 Axes>

Two-qubit depth and CX count against lattice edge; the qubit-level LieTrotter grows linearly while the flow-set synthesis stays flat at 24.

ความลึกสอง qubit คือ 24 และคงที่ตั้งแต่ 3×33 \times 3 ถึง 8×88 \times 8 (คือ 18 qubit ไปจนถึง 128) ในขณะที่ LieTrotter ที่ใช้กับ Hamiltonian ที่เข้ารหัสเหมือนกันเพิ่มขึ้นเชิงเส้นตามขอบแลตทิซ จาก 47 เป็น 92 จำนวน Gate เพิ่มขึ้นทั้งสองแบบ เพราะมี term O(N)O(N) ที่ต้องใช้ไม่ว่าแบบไหน สิ่งที่ flow set ตัดออกคือการเพิ่มขึ้นของจำนวนชั้นที่ Gate เหล่านั้นครอบครอง

งบประมาณแบ่งเป็น 16 สำหรับ hopping และแปดสำหรับอันตรกิริยา:

  • แต่ละ flow set ทั้งสี่มีต้นทุน 2+22 + 2 ชั้นสอง qubit: Clifford ขาเข้า และอินเวอร์สขาออก ซึ่งลึกสองทั้งคู่โดยการสร้าง โดยมีเพียงการหมุน qubit เดี่ยวคั่นกลาง นั่นคือ 4×4=164 \times 4 = 16 ซึ่งเป็นตัวเลขที่ Ref. [1] อ้างถึง เพราะมันนับเฉพาะ hopping term การตั้งค่า U=0U = 0 ในโค้ดข้างต้นจะได้ผลเดียวกัน

  • กลุ่มแนวทแยงเพิ่มอีกแปดที่เหลือ Gate Rzz ของมันอัดเป็นสี่ชั้น และแต่ละ Rzz แยกเป็นสอง Gate CX คร่อมการหมุน qubit เดี่ยว ดังนั้น 4×2=84 \times 2 = 8 สี่ชั้นคือค่าที่เหมาะสมที่สุด: บนแลตทิซสี่เหลี่ยม อันตรกิริยาไปถึงแค่เพื่อนบ้านใกล้สุด ดังนั้นทุก site ภายในมี Gate Rzz สี่ตัว (หนึ่งต่อพันธะ) และเข้าร่วมได้ครั้งละหนึ่งตัวเท่านั้น

ไม่มีส่วนใดในผลรวมทั้งสองที่อ้างถึงขนาดแลตทิซ ซึ่งคือประเด็นทั้งหมด

8. ตรวจสอบวงจร​

การสังเคราะห์แบบ flow set และ LieTrotter อธิบายขั้น Trotter เดียวกันของ Hamiltonian ที่เข้ารหัสเดียวกัน ดังนั้นยูนิทารีของมันต้องตรงกันจนถึงเฟสรวม statevector สุ่มตรวจพบความไม่ตรงกันได้ถูกกว่าการสร้างยูนิทารีเต็มมาก:

หมายเหตุ

ทั้งสองวงจรถูกแยกส่วนลงเหลือ Gate หนึ่งและสอง qubit ก่อน นำไปใช้กับสถานะ หากไม่แยกส่วน คำสั่งระดับสูง Evolution แต่ละคำสั่งจะบังคับให้ Statevector สร้าง operator แบบหนาแน่นเต็มขนาด 22N×22N2^{2N} \times 2^{2N} ของมัน การแยกส่วนก่อนทำให้มันใช้ Gate ทีละตัวแทน ซึ่งเร็วกว่าหลายร้อยเท่าในกรณีนี้

>>> import numpy as np
>>> from qiskit.quantum_info import Statevector
>>>
>>> rng = np.random.default_rng(1234)
>>>
>>> def agrees_up_to_one_phase(rows, cols, samples=3):
... """Compare both syntheses on random states.
...
... Returns whether they agree up to a phase, and whether it is the *same* phase
... for every input state.
... """
... lie_trotter = transpile_trotter_step(
... rows, cols, LieTrotter()
... ).decompose(reps=6)
... flow = transpile_trotter_step(
... rows, cols, FlowSetSynthesis(rows, cols)
... ).decompose(reps=6)
... dimension = 2**lie_trotter.num_qubits
... residuals, phases = [], []
... for _ in range(samples):
... vector = rng.normal(size=dimension) + 1j * rng.normal(size=dimension)
... state = Statevector(vector / np.linalg.norm(vector))
... a = state.evolve(lie_trotter).data
... b = state.evolve(flow).data
... overlap = np.vdot(a, b)
... phases.append(overlap / abs(overlap))
... residuals.append(np.linalg.norm(b - a * overlap / abs(overlap)))
... # the SAME global phase must work for every input state
... spread = max(abs(phase - phases[0]) for phase in phases)
... return bool(max(residuals) < 1e-9), bool(spread < 1e-9)
>>>
>>> agrees_up_to_one_phase(rows, cols)
(True, True)
หมายเหตุ

ทั้งสองครึ่งของการตรวจสอบนั้นสำคัญ การทับซ้อนที่มีโมดูลัสหนึ่งบนสถานะเดียวจำเป็นแต่ไม่เพียงพอ: การสังเคราะห์ที่ได้เฟสที่ขึ้นกับสถานะผิดก็ยังแสดงให้เห็น การกำหนดให้เฟสเดียวกันข้ามหลายสถานะสุ่มคือสิ่งที่ปิดช่องว่างนี้

9. ตรวจสอบฟิสิกส์บน stabilizer subspace​

นี่คือจุดยุ่งยากของสองมิติ ใน 1D การเข้ารหัสเป็น isometry และการเปรียบเทียบสเปกตรัมกับ Jordan-Wigner อ้างอิงก็เพียงพอ VC ไม่ใช่ isometry: มันใช้ 2N2N qubit สำหรับ NN mode ดังนั้นปริภูมิมิติ 22N2^{2N} บรรจุปริภูมิฟิสิกส์มิติ 2N2^N นี่เป็นลักษณะทั่วไปของการเข้ารหัสเฉพาะที่ในสองมิติ (qubit ส่วนเกินถูกจำกัดด้วยสมมาตรที่คอมมิวต์กันเฉพาะที่ หนึ่งต่อ plaquette [2] [3]) และหมายความว่า Hamiltonian ที่เข้ารหัสแทนค่าเฟอร์มิออนได้อย่างซื่อตรงเฉพาะบนปริภูมิลักษณะเฉพาะ +1+1 ร่วมของข้อจำกัดเหล่านั้นเท่านั้น

คำเตือน

คำว่า stabilizer ทำสองหน้าที่ในหัวข้อนี้ และสองความหมายไม่เกี่ยวข้องกัน Ref. [1] ใช้มันกับ transfer operator ที่คอมมิวต์กันของ flow set ซึ่งสร้างกรุป stabilizer ที่ Clifford ของขั้นตอนที่ 5 ทำให้เป็นแนวทแยง (เป็นอุปกรณ์ช่วยคอมไพล์ และเป็นความหมายที่ใช้ทุกที่ข้างต้น) ส่วนนี้ใช้ในความหมายของการแก้ไขข้อผิดพลาด: operator ที่ปริภูมิลักษณะเฉพาะ +1+1 ของมันคือปริภูมิฟิสิกส์ Ref. [1] ระบุอย่างชัดเจนถึงการชนกันนี้และวางความหมายที่สองไว้ข้าง ๆ ข้อจำกัด plaquette ด้านล่างเป็นสมบัติของการเข้ารหัส VC [2] ไม่ใช่ผลของกรอบ flow set

generator ถูกสร้างที่นี่แทนที่จะยกมา: หนึ่งต่อหนึ่งหน้าสี่เหลี่ยม และแต่ละตัวก็คือลูป operator ของ edge ที่เข้ารหัสแล้วรอบหน้านั้น

Edge operator เป็นวัตถุที่เหมาะกับงานนี้ ลูปปิดของมันเป็นสมมาตรของ Hamiltonian ที่รักษาจำนวนอนุภาคใด ๆ ในขณะที่ลูปของ transfer operator ที่สอดคล้องกันไม่เป็น: สองอย่างต่างกันด้วย vertex operator บน site ต้นทางของแต่ละพันธะ

Tjk=i2 VjEjk⟺Ejk=−2i VjTjk ,T_{jk} = \tfrac{i}{2} \, V_j E_{jk} \qquad \Longleftrightarrow \qquad E_{jk} = -2i \, V_j T_{jk} \, ,

รอบลูปปิด ทุก site เป็นต้นทางของพันธะพอดีหนึ่งพันธะ ลูปจึงรวบรวม VjV_j หนึ่งตัวต่อ site ซึ่งคือ parity เฉพาะที่ ของสี่ site ของ plaquette parity เฉพาะที่นี้คือสิ่งที่ทำให้เกิดความต่าง: Hamiltonian ย้ายอนุภาคข้ามขอบ plaquette ดังนั้นมันรักษาเพียง parity ทั่วทั้งระบบ ลูป transfer เปล่า ๆ จึงไม่คอมมิวต์กับมัน ในขณะที่ลูป edge (ซึ่งมีตัวประกอบ parity) คอมมิวต์

เนื่องจากการเข้ารหัสนิยามบน transfer operator การแมป edge operator จึงต้องใช้ความสัมพันธ์ข้างต้น นั่นคือ callback ห้าบรรทัด และมันเสียบเข้า map_edge_vertex_generators() ได้เหมือนกับที่ vc_action เสียบเข้า map_transfer_vertex_generators():

>>> from qiskit_fermions.mappers import map_edge_vertex_generators
>>> from qiskit_fermions.operators import EdgeVertexOperator
>>>
>>> def vc_edge_action(generator, width, cols):
... """Map one generalized *edge* operator to its Verstraete-Cirac Pauli."""
... j, k = generator
... if j == k: # a vertex operator is the same object in both algebras
... return SparseObservable.from_sparse_list([("Z", [j], 1.0)], num_qubits=width)
... # The library decomposes T_jk = (i/2) V_j E_jk, so E_jk = -2i V_j T_jk. Reuse the
... # transfer encoding for T_jk rather than repeating its four cases here. Note that
... # `compose` left-multiplies its argument, so this composes to V_j T_jk; the other
... # order carries the opposite sign, since a vertex and a transfer operator sharing a
... # mode anticommute.
... vertex = SparseObservable.from_sparse_list([("Z", [j], 1.0)], num_qubits=width)
... return (-2.0j * vc_action(generator, width, cols).compose(vertex)).simplify()
>>>
>>> def vc_edge_encoding(operator, width, cols):
... """Map an EdgeVertexOperator to a qubit observable, Verstraete-Cirac style."""
... return map_edge_vertex_generators(
... operator,
... partial(vc_edge_action, width=width, cols=cols),
... identity=lambda: SparseObservable.identity(width),
... compose=SparseObservable.compose,
... ).simplify()
>>>
>>> def plaquette_loops(rows, cols):
... """The directed edge loop around each square face."""
... at = lambda r, c: r * cols + c
... return [
... [(at(r, c), at(r, c + 1)), (at(r, c + 1), at(r + 1, c + 1)),
... (at(r + 1, c + 1), at(r + 1, c)), (at(r + 1, c), at(r, c))]
... for r in range(rows - 1)
... for c in range(cols - 1)
... ]
>>>
>>> def stabilizers(rows, cols):
... """One plaquette stabilizer per square face of the lattice."""
... num_qubits = 2 * rows * cols
... out = []
... for loop in plaquette_loops(rows, cols):
... loop_operator = EdgeVertexOperator.from_dict({tuple(loop): 1.0})
... encoded_loop = vc_edge_encoding(loop_operator, num_qubits, cols)
... out.append(encoded_loop / complex(encoded_loop.coeffs[0]))
... return out
>>>
>>> plaquette_stabilizers = stabilizers(rows, cols)
>>> for stabilizer in plaquette_stabilizers:
... print(stabilizer.to_sparse_list())
[('ZZYXXY', [1, 4, 9, 10, 12, 13], (1+0j))]
[('ZZYXXY', [2, 5, 10, 11, 13, 14], (1+0j))]
[('ZZYXXY', [4, 7, 12, 13, 15, 16], (1+0j))]
[('ZZYXXY', [5, 8, 13, 14, 16, 17], (1+0j))]

generator แต่ละตัวมีน้ำหนัก 66 กระจายบนสอง qubit ฟิสิกส์ของ plaquette และสี่ ancilla มันเป็น involution และคอมมิวต์กับ Hamiltonian ที่เข้ารหัส ดังนั้นปริภูมิฟิสิกส์ถูกรักษาไว้โดยพลวัต ทั้งสองสมบัติเป็นข้อความเกี่ยวกับ operator จึงตรวจสอบเชิงสัญลักษณ์บนพีชคณิต SparseObservable โดยไม่ต้องใช้เมทริกซ์ 218×2182^{18} \times 2^{18}:

>>> encoded = encoding(hamiltonian, num_qubits)
>>> identity = SparseObservable.identity(num_qubits)
>>> for stabilizer in plaquette_stabilizers:
... square = stabilizer.compose(stabilizer).simplify()
... print(square == identity, stabilizer.commutes(encoded))
True True
True True
True True
True True

เมื่อมี stabilizer ในมือแล้ว การเปรียบเทียบไม่ต้องการการจับคู่ basis เพราะ Vj=ZjV_j = Z_j หมายความว่าจำนวนการครอบครองอยู่บน qubit ฟิสิกส์ตัวเดียวกันในทั้งสองภาพ ดังนั้น: เขียนสถานะผลคูณตรง ๆ บน qubit ฟิสิกส์โดยให้ ancilla อยู่ใน ∣0⟩|0\rangle โปรเจกต์ลงบนปริภูมิ stabilizer วิวัฒน์ด้วย Hamiltonian ที่เข้ารหัส และอ่านความหนาแน่นของ site ⟨nj⟩=(1−⟨Zj⟩)/2\langle n_j \rangle = (1 - \langle Z_j \rangle)/2 จากทั้งสองข้าง

ทุกขั้นตอนที่นี่อยู่ที่ระดับเวกเตอร์ ตัวโปรเจกเตอร์ถูกใช้ทีละตัวประกอบ (1+S)/2(1 + S)/2 แทนที่จะคูณออกมา และการวิวัฒน์ตามเวลาใช้ expm_multiply() ซึ่งคำนวณ e−iHt∣ψ⟩e^{-iHt} |\psi\rangle โดยไม่ต้องสร้าง e−iHte^{-iHt} เลย เนื่องจาก Hamiltonian ที่เข้ารหัสเป็น operator แบบเบาบางมิติ 22N2^{2N} ความต่างนี้คือสิ่งที่ทำให้ส่วนนี้ถูก

>>> from scipy.sparse.linalg import expm_multiply
>>> from qiskit_fermions.mappers.library import transfer_vertex_jordan_wigner
>>>
>>> # an exact evolution time, unrelated to the Trotter step above
>>> evolution_time = 0.6
>>> reference = transfer_vertex_jordan_wigner(hamiltonian, num_sites).simplify()
>>>
>>> def to_sparse_matrix(observable):
... # SparseObservable carries no dense- or sparse-matrix method, so route through
... # SparsePauliOp. This is only ever needed for the numerical checks below --
... # the encoding itself never leaves SparseObservable.
... return SparsePauliOp.from_sparse_observable(observable).to_matrix(sparse=True)
>>>
>>> def basis_state(occupation, num_qubits):
... """The computational basis state with `occupation` on the first qubits.
...
... Qubit `q` carries bit `2**q`, and the ancillas are the *high* qubits `N..2N-1`,
... so the same integer indexes the same site occupations on either register and
... leaves every ancilla in |0>.
... """
... index = int("".join(str(bit) for bit in reversed(occupation)), 2)
... state = np.zeros(2**num_qubits, dtype=complex)
... state[index] = 1.0
... return state
>>>
>>> def project_onto_codespace(state, stabilizers):
... """Apply the projector one (1 + S) factor at a time, then renormalize.
...
... The factors commute, so their product is itself a projector and the order does
... not matter. Multiplying it out would build a dense 2^2N x 2^2N operator; applied
... factor by factor this stays a handful of sparse matrix-vector products.
...
... Each factor is really (1 + S)/2, but every one contributes the same scalar, so the
... halvings only rescale the result and the final normalization absorbs them.
... """
... for stabilizer in stabilizers:
... state += to_sparse_matrix(stabilizer) @ state
... return state / np.linalg.norm(state)
>>>
>>> occupation = [1, 0, 1, 0, 1, 0, 0, 1, 0] # site occupations of the initial state
>>> initial_reference = basis_state(occupation, num_sites)
>>> initial_encoded = project_onto_codespace(
... basis_state(occupation, num_qubits), plaquette_stabilizers
... )
>>>
>>> evolved_encoded = expm_multiply(
... -1j * evolution_time * to_sparse_matrix(encoded), initial_encoded
... )
>>> evolved_reference = expm_multiply(
... -1j * evolution_time * to_sparse_matrix(reference), initial_reference
... )
>>>
>>> def density(state, qubit, width):
... """Site density <n_j> = (1 - <Z_j>) / 2 of a statevector."""
... z = to_sparse_matrix(
... SparseObservable.from_sparse_list([("Z", [qubit], 1.0)], num_qubits=width)
... )
... return (1 - (state.conj() @ (z @ state)).real) / 2
>>>
>>> encoded_densities = [density(evolved_encoded, j, num_qubits) for j in range(num_sites)]
>>> reference_densities = [density(evolved_reference, j, num_sites) for j in range(num_sites)]
>>>
>>> for j, density_value in enumerate(encoded_densities):
... print(f"site {j}: {density_value:.10f}")
site 0: 0.6750993356
site 1: 0.3689791258
site 2: 0.6750993356
site 3: 0.3818130396
site 4: 0.4868585996
site 5: 0.3818130396
site 6: 0.2693544696
site 7: 0.4916285849
site 8: 0.2693544696
>>> np.allclose(encoded_densities, reference_densities, atol=1e-12)
True

นั่นคือความหนาแน่นที่เข้ารหัส มันตรงกับ Jordan-Wigner อ้างอิงถึงความแม่นยำของเครื่อง บนแลตทิซที่การเข้ารหัสซื่อตรงหลังโปรเจกชันเท่านั้น

10. วัดการลู่เข้าของ Trotter​

ขั้นตอนที่ 8 ยืนยันว่าสองการสังเคราะห์ตรงกัน ซึ่งเป็นข้อความเรื่องความเทียบเท่า ไม่ใช่ความแม่นยำ: ทั้งสองอธิบายขั้น Trotter เดียวกัน จึงมีความคลาดเคลื่อนแบบ Trotter เท่ากัน การวัดความคลาดเคลื่อนนั้นต้องมีค่าอ้างอิงที่แน่นอน และขั้นตอนที่ 9 เพิ่งสร้างไว้

จำนวนขั้นมาจาก FermionicLieTrotter ซึ่งเป็นคู่เทียบแบบเฟอร์มิออนของสูตรระดับ qubit ที่ใช้เป็น baseline ก่อนหน้า เมื่อใช้กับ operator ที่จัดกลุ่มทั้งหมดก่อนการแยกส่วน การทำซ้ำของมันวนเวียนระหว่างสี่ flow set และกลุ่มแนวทแยง แทนที่จะวนภายในชุดใดชุดหนึ่ง:

>>> initial_state = project_onto_codespace(
... basis_state(occupation, num_qubits), plaquette_stabilizers
... )
>>> exact = expm_multiply(
... -1j * total_time * to_sparse_matrix(encoded), initial_state
... )
>>>
>>> def trotter_fidelity(steps):
... # `steps` goes to the fermionic formula inside transpile_trotter_step, so the
... # repetition cycles between the flow sets rather than within any one of them
... circuit = transpile_trotter_step(
... rows, cols, FlowSetSynthesis(rows, cols), steps=steps
... )
... evolved = Statevector(initial_state).evolve(circuit.decompose(reps=6))
... return abs(np.vdot(evolved.data, exact)) ** 2
>>>
>>> for steps in (1, 2, 4, 8):
... print(f"{steps} step(s): fidelity {trotter_fidelity(steps):.6f}")
1 step(s): fidelity 0.053820
2 step(s): fidelity 0.704014
4 step(s): fidelity 0.932937
8 step(s): fidelity 0.982687

ความเที่ยงตรงเพิ่มขึ้นแบบทางเดียวเข้าหาหนึ่งเมื่อขั้นสั้นลง ซึ่งเป็นพฤติกรรมที่ product formula ต้องแสดง การทำให้ขั้นสั้นลงเป็นเพียงหนึ่งในสองปุ่ม; FermionicSuzukiTrotter แลกเปลี่ยนทั้งสองกันที่อันดับสูงขึ้น ดังที่ Synthesize an Evolution gate in fermionic space วัดไว้

ขั้นเดียวของ Hamiltonian นี้ที่ t=1t = 1 เป็นการประมาณที่แย่ (สี่ชุด hopping และอันตรกิริยาไม่คอมมิวต์กัน) ดังนั้นแถวแรกจึงห่างจากหนึ่งมาก และนั่นคือประเด็น: การลู่เข้าคือสิ่งที่ยืนยันการสร้าง ไม่ใช่แถวใดแถวหนึ่ง

หมายเหตุ

แต่ละตัวประกอบของตารางเวลาครอบคลุมทุก mode ของแลตทิซนี้ ดังนั้นการแคบตัวประกอบลงเหลือ support ของมันจึงไม่มีผลที่นี่ และดัชนี qubit สัมบูรณ์ที่ FlowSetSynthesis จับคู่ไม่เปลี่ยน การสังเคราะห์ที่เขียนสำหรับ operator ที่กลุ่มของมันแตะเพียงบางส่วนของรีจิสเตอร์จะต้องคำนึงถึงการเปลี่ยนป้ายนั้น

สิ่งที่ควรจำ​

  • สองมิติเปลี่ยนการทำบัญชี ไม่ใช่สูตร ส่วนผสมทุกอย่างจากคู่มือ 1D ถูกนำมาใช้โดยไม่แก้ไข: groups สำหรับการจัดกลุ่ม, map_transfer_vertex_generators() สำหรับการเข้ารหัส, CustomF2QLayout สำหรับรีจิสเตอร์ที่ขนาดไม่ตรงกัน และ MapperFnEvolutionSynthesis เพื่อเชื่อมมันเข้ากับ Transpiler สิ่งที่โตขึ้นคือจำนวนกรณีภายในแต่ละตัว

  • ความไม่มีจุดร่วมคือสิ่งที่ทำให้ 2D มีความลึกคงที่ flow set แยกออกเป็นเส้นที่ไม่มีจุดยอดร่วมกัน ดังนั้น Clifford ของมันถูกปล่อยออกมาพร้อมกันและใช้สองชั้นสอง qubit ร่วมกันไม่ว่าแลตทิซจะใหญ่เพียงใด สี่ flow set และกลุ่มแนวทแยงจึงให้งบประมาณคงที่: ในที่นี้ 1616 สำหรับสี่ flow set บวก 88 สำหรับกลุ่มอันตรกิริยาแนวทแยง

  • แต่ละทิศทางเป็นรูปร่าง Pauli ของตัวเอง Tkj=−VjVkTjkT_{kj} = -V_j V_k T_{jk} เปลี่ยน XXZ เป็น YYZ และ XYXY เป็น YXXY ดังนั้นสี่ flow set ต้องใช้สี่ Clifford การสมมติว่า Clifford ตัวเดียวครอบคลุมทั้งสองทิศของแกนเป็นวิธีที่ง่ายที่สุดที่จะทำให้การสังเคราะห์ flow set แบบ 2D ผิด

  • อ่าน support จากการคอนจูเกต ใน 1D ชั้นอิฐ CZ ชั้นเดียวให้ภาพในรูปแบบปิด ใน 2D ไม่มีสูตรเช่นนั้น และเส้นคอลัมน์มี ancilla ที่ไม่ต่อเนื่อง ดังนั้นการเดาที่หามาด้วยมือว่าแต่ละ term ไปลงที่ใดจึงเปราะบาง การคอนจูเกตและตรวจผลลัพธ์ใช้เพียงการสร้าง Clifford หนึ่งครั้งและผิดไม่ได้ มันยังหมายความว่า Clifford ที่ถูกต้องเท่ากันที่วาง term ไว้ที่อื่นก็ยังใช้ได้

  • ให้การจัดกลุ่มทำหน้าที่เรียง เพราะ Evolution แยกตัวเองตามกลุ่ม การสังเคราะห์จึงได้รับทีละ flow set และไม่ต้องหาการแบ่งส่วนใหม่เลย: มันอ่านรูปร่าง Pauli หนึ่งแบบจาก term แรกและเลือก Clifford ที่ตรงกัน การสมมติเช่นนั้นและยกข้อผิดพลาดเมื่อไม่เป็นจริง คือสิ่งที่ทำให้ synthesize สั้น

  • จำนวนขั้นอยู่เหนือการแยก การสังเคราะห์ในที่นี้เห็นหนึ่ง flow set ต่อ Gate ดังนั้นมันจึงแบ่งการวิวัฒน์ข้ามชุดไม่ได้ FermionicLieTrotter ทำสิ่งนั้นในปริภูมิเฟอร์มิออน ก่อนการแยกส่วน ซึ่งทำให้การทดสอบการลู่เข้าเป็นไปได้เลย

  • ตรวจสอบบนปริภูมิย่อย การเข้ารหัส NN mode ด้วย 2N2N qubit ซื่อตรงเฉพาะที่ที่ stabilizer บอกว่าเป็น เพราะ VC คง Vj=ZjV_j = Z_j ไว้ การตรวจสอบที่เชื่อถือได้และถูกที่สุดคือโปรเจกต์สถานะผลคูณแล้วเปรียบเทียบความหนาแน่น: ไม่ต้องใช้ intertwiner และไม่ต้องจับคู่สเปกตรัม ซึ่งเชื่อถือไม่ได้เมื่อ Hamiltonian ที่ถูกจำกัดเป็น degenerate

หมายเหตุ

Hamiltonian ที่เข้ารหัสคอมมิวต์กับ stabilizer ดังนั้นโปรเจกชันต้องใช้กับสถานะเริ่มต้นเท่านั้น การวิวัฒน์รักษาปริภูมิย่อยไว้เอง บนฮาร์ดแวร์จริง นี่ยังเป็นสิ่งที่ทำให้การเข้ารหัสมีประโยชน์ในฐานะทรัพยากรตรวจจับข้อผิดพลาด: stabilizer วัดได้

ถัดไป​

สองทิศทางขยายสิ่งที่อยู่ข้างต้น และทั้งสองเป็นการเปลี่ยนส่วนผสม ไม่ใช่วิธี อย่างแรกคือการเข้ารหัส: เลือก Verstraete-Cirac ที่นี่เพราะ Ref. [1] ให้ Clifford สำหรับทำให้เป็นแนวทแยงในรูปแบบปิด แต่ Derby-Klassen ใช้ ancilla หนึ่งตัวต่อ plaquette คี่แทนหนึ่งตัวต่อ site ทำให้อัตราส่วน qubit ต่อ mode เป็น 3/23/2 แทนที่จะเป็น 22 transfer operator ของมันมีรูปร่าง Pauli ต่างกัน ดังนั้นการพอร์ตจึงเท่ากับเขียนฟังก์ชันการเข้ารหัสของขั้นตอนที่ 3 ใหม่ และหา Clifford สำหรับแต่ละรูปร่างใหม่ ส่วนการจัดกลุ่ม layout และโครงสังเคราะห์ใช้ต่อได้โดยไม่ต้องแตะ

อย่างที่สองคือตัวแลตทิซเอง ทุกอย่างที่นี่สมมติขอบเปิดและเฟอร์มิออนชนิดเดียว ขอบเป็นคาบปิดแต่ละเส้นให้เป็นวงรอบ: ลูกศรทั้งหมดยังคอมมิวต์กัน เพราะแต่ละตัวพบตัวถัดไปแบบหัวชนหาง แต่สองรูปร่าง Clifford ของขั้นตอนที่ 5 สร้างมาสำหรับเส้นที่มีจุดปลาย วงรอบที่มี LL site มี LL พันธะต่อทิศ ในขณะที่เส้นมี L−1L - 1 ดังนั้นพันธะที่วนกลับจึงไม่มีที่ในการสร้างและต้องจัดการเอง โมเดลที่มีสปินง่ายกว่า: มันเพิ่มรีจิสเตอร์เป็นสองเท่าและเพิ่ม term อันตรกิริยาบน site ให้กลุ่มแนวทแยง โดยไม่แตะ flow set ไม่ว่าแบบไหนก็ไม่รบกวนข้อโต้แย้งเรื่องความลึกคงที่ แต่ทั้งสองเปลี่ยนการทำบัญชี ซึ่งตามที่ข้อสรุปแรกบอก คือส่วนที่สองมิติทำให้ใหญ่ขึ้น

สำคัญ

ที่นี่ไม่ได้กล่าวถึงการเชื่อมต่อของฮาร์ดแวร์ ทุกวงจรข้างต้นสร้างบนรีจิสเตอร์นามธรรมที่ qubit คู่ใดก็ทำงานร่วมกันได้ และความลึกที่รายงานนับในบริบทนั้น อุปกรณ์จริงมี coupling map ที่จำกัด และการแมปการเข้ารหัสนี้ลงบนอุปกรณ์เป็นปัญหาใหญ่ในตัวมันเอง: รีจิสเตอร์ VC เป็นกราฟ 2N2N qubit ที่ Gate ทำงานระหว่าง site กับ ancilla ของมันเอง ระหว่าง site ข้างเคียง และระหว่าง ancilla ข้างเคียง ดังนั้น layout ต้องทำให้ทั้งสามชนิดของคู่อยู่ติดกันพร้อมกัน หากทำไม่ได้ Transpiler จะแทรก Gate SWAP และชั้นที่มันเพิ่มคือสิ่งที่การสร้างแบบความลึกคงที่ซื้อคืนมา Ref. [1] ก็อ้างตัวเลขความลึกภายใต้กราฟการเชื่อมต่อสี่เหลี่ยมที่สมมติไว้ โดยมี CX เป็นตัวพันกันดั้งเดิมเพียงตัวเดียวเช่นกัน การหา layout และกลยุทธ์การจัดเส้นทางที่ดีสำหรับการเข้ารหัสเฉพาะที่บนโทโพโลยีอุปกรณ์หนึ่ง ๆ อยู่นอกขอบเขตของคู่มือนี้

[1] (1,2,3,4,5,6,7,8,9,10)

A. Gandon, S. Piccinelli, M. Rossmannek, F. Tacchino, A. Baiardi, J. Nys, and I. Tavernelli, Stabilizer-based quantum simulation of fermion dynamics with local qubit encodings (2026), arXiv:2512.11418v2.

[2] (1,2,3,4)

F. Verstraete and J. I. Cirac, Mapping local Hamiltonians of fermions to local Hamiltonians of spins, J. Stat. Mech. 2005, P09012 (2005).

[3] (1,2)

Y.-A. Chen and Y. Xu, Equivalence between fermion-to-qubit mappings in two spatial dimensions, PRX Quantum 4, 010326 (2023).