แมปการแทนตัวดำเนินการ
mapper แปลง BosonOperator ให้เป็นตัวดำเนินการระดับ Qubit ในที่นี้คือ SparseObservable ตัวดำเนินการเองไม่รู้อะไรเกี่ยวกับ Qubit เลย มันเก็บตัวดำเนินการแลดเดอร์บนโหมด และโหมดโบซอนหนึ่งโหมดรองรับอนุภาคได้มากเท่าใดก็ได้ ทุกอย่างที่ทำให้ตัวดำเนินการมีขนาดจำกัด (คุณเก็บระดับการครอบครองไว้กี่ระดับ และระดับเหล่านั้นถูกจัดวางบน Qubit อย่างไร) จะถูกกำหนดตอนที่คุณแมป
mapper ของไลบรารี occupation_encoding() ทำงานในฐานการครอบครอง (Fock): ระดับ ของโหมดคือสถานะที่มีโบซอน ตัว มันรับตัวดำเนินการและ LevelSpec ซึ่งเก็บการตัดทอนและการเข้ารหัสไว้ คู่มือนี้อธิบายว่าสองตัวเลือกนั้นหมายถึงอะไรและแต่ละอย่างมีต้นทุนเท่าใด สำหรับเส้นทางครบวงจรที่ใช้ทั้งสองอย่าง ดู จำลองพลวัตของ Bose-Hubbard โดยเลือกการเข้ารหัส ส่วนวิธีที่ Transpiler ใช้ตัวเลือกเดียวกันกับทั้ง Circuit ดู Transpile Circuit แบบโบซอน
ตัดทอนปริภูมิ Fock
โหมดที่เก็บไว้ ระดับ โดยที่ คือ 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)
เมื่อค่าตัดขาดเป็นหนึ่ง ความน่าจะเป็นที่จะอยู่ที่ เท่ากับหนึ่ง เพราะสองสถานะที่พลวัตจะทำให้เกิดขึ้นนั้นไม่มีอยู่ เมื่อค่าตัดขาดเป็นสอง ความน่าจะเป็นนั้นหายไป ซึ่งก็คือปรากฏการณ์ Hong-Ou-Mandel นี่คือเหตุผลที่ max_occupation เป็นอาร์กิวเมนต์ที่จำเป็นและไม่มีค่าเริ่มต้น เพราะมันเป็นการตัดสินใจด้านการสร้างแบบจำลองและเป็นพารามิเตอร์การลู่เข้าที่คุณควรกวาดค่า
การแมปไม่สลับที่กับการเรียงแบบปกติ ตัวดำเนินการ และ เท่ากันก่อนการตัดทอน แต่แมปไปเป็นตัวดำเนินการระดับ Qubit ที่ต่างกัน:
>>> 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]
ทั้งสองตรงกันทุกระดับยกเว้นระดับสูงสุด เมื่อกระทำกับระดับบนสุด ไม่มีที่ไป และแอมพลิจูดถูกตัดทิ้ง ดังนั้น เป็นศูนย์ที่นั่น ในขณะที่ ให้ ให้ตัดสินใจครั้งเดียวว่า Hamiltonian ของคุณนิยามในรูปแบบเรียงปกติหรือไม่ แล้วแมปรูปแบบนั้นอย่างสม่ำเสมอ
มีสามวิธีที่ช่วยคุณเลือกค่าตัดขาด และทั้งหมดเป็นขอบเขตล่าง ตัวดำเนินการรายงานจำนวนตัวดำเนินการสร้างสูงสุดที่มันกระทำกับแต่ละโหมดภายในเทอมเดียว และ spec จะเปรียบเทียบจำนวนนั้นกับค่าตัดขาดของมัน:
>>> 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 รวมค่าตัดขาดของทุกโหมดเข้ากับการเข้ารหัส และคำนวณจากสิ่งเหล่านั้นว่าแต่ละโหมดใช้ Qubit กี่ตัวและ Qubit ของมันเริ่มที่ใด ทุกส่วนที่ต้องตกลงกันเรื่องนี้ (mapper, เลย์เอาต์ของ Transpiler, การเตรียมสถานะ) จะได้รับ spec เดียวกัน การเข้ารหัสเป็นอาร์กิวเมนต์ที่จำเป็น ค่าตัดขาดก็เช่นกัน ทั้งสองอย่างไม่มีค่าเริ่มต้น
ค่าตัดขาดเป็นจำนวนเต็มตัวเดียวที่ใช้ร่วมกันทุกโหมด หรือเป็นจำนวนเต็มหนึ่งตัวต่อโหมด:
>>> shared = LevelSpec(2, "gray")
>>> per_mode = LevelSpec([4, 2, 2], "gray")
>>> shared.num_modes is None, per_mode.num_modes
(True, 3)
spec ที่สร้างจากจำนวนเต็มตัวเดียวไม่ได้กำหนดจำนวนโหมด คุณจึงต้องกำหนดมันก่อนขอความกว้าง ลำดับจะไม่ถูกกระจายซ้ำไม่ว่าจะยาวเท่าใด และมันกำหนดจำนวนโหมด:
>>> 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
โหมด ใช้ Qubit ตั้งแต่ offsets[m] ถึง offsets[m] + widths[m] - 1 ดังนั้นโหมดที่มีค่าตัดขาดต่างกันจึงใช้จำนวน Qubit ต่างกันได้ และ offsets คือผลรวมสะสมของความกว้าง คุณไม่ต้องส่งจำนวนโหมดให้ mapper มันดึงจาก spec หรือสำหรับ spec ที่มีค่าตัดขาดเดียว ดึงจากตัวดำเนินการผ่าน min_num_modes() รีจิสเตอร์กว้างกว่าที่โหมดต้องการได้ Qubit ส่วนเกินจะไม่ทำงานและอยู่ท้ายสุด:
>>> 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
ตัวดำเนินการที่กระทำกับโหมดที่อยู่เลยโหมดสุดท้ายของ spec แบบรายโหมดจะถูกปฏิเสธ:
>>> 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
ข้อจำกัดบางอย่างเกิดจากวิธีที่การเข้ารหัสนับ Qubit ภายใต้การเข้ารหัสแบบกะทัดรัด โหมดที่มีระดับเดียว (max_occupation = 0) ไม่ใช้ Qubit เลย เพราะระดับเดียวของมันไม่ต้องใช้อะไรเพื่อแยกจากระดับอื่น โหมดเช่นนั้นอนุญาตและไม่ทำให้ Qubit ของโหมดหลังมันขยับ แต่ spec ที่ทุกโหมดไม่ใช้ Qubit เลยจะถูกปฏิเสธ การเข้ารหัสแบบยูนารีรองรับได้มากสุด 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 ตัดสินว่าสถานะฐานใดของกลุ่ม Qubit แทนแต่ละระดับ มีสี่แบบ และไม่มีแบบใดดีที่สุดโดยทั่วไป [2] จำนวน Qubit ต่อโหมดและรูปแบบบิตของแต่ละระดับเป็นไปตามการเข้ารหัส:
>>> 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']
รูปแบบบิตคือจำนวนเต็มที่บิต คือค่าของ Qubit การเข้ารหัสแบบยูนารีใช้ Qubit หนึ่งตัวต่อระดับและตั้งค่าเพียงตัวเดียว การเข้ารหัสแบบกะทัดรัดใช้ Qubit ตัวและเก็บหมายเลขระดับ ไม่ว่าจะเป็นแบบไบนารีหรือแบบรหัสเกรย์ที่ระดับข้างเคียงต่างกันหนึ่งบิต ชื่อสตริงของการเข้ารหัสใช้ได้ทุกที่ที่ใช้การเข้ารหัสได้
ข้อแลกเปลี่ยนคือ Qubit เทียบกับน้ำหนักของตัวดำเนินการ การเข้ารหัสแบบกะทัดรัดใช้ Qubit น้อยลงแบบเอกซ์โพเนนเชียล แต่ตัวดำเนินการแลดเดอร์เชื่อมระดับที่รูปแบบอาจต่างกันหลายบิต เทอมของมันจึงกระทำกับ Qubit มากขึ้น ส่วนต่อไปนี้ดูว่าสิ่งนี้มีต้นทุนเท่าใด โดยเริ่มจากการเข้ารหัสยูนารีสองแบบ ซึ่งมีเลย์เอาต์เหมือนกันและต่างกันที่ตัวดำเนินการเท่านั้น
เปรียบเทียบการเข้ารหัสยูนารีสองแบบ
ในการเข้ารหัสยูนารี ตัวดำเนินการแลดเดอร์ย้ายการกระตุ้นหนึ่งครั้งระหว่าง Qubit ข้างเคียงสองตัว การเขียนสิ่งนั้นเป็นตัวดำเนินการบนทั้งโหมด คุณต้องเลือกว่าจะทำอย่างไรกับ Qubit ที่เหลือ สองแบบเลือกต่างกัน:
-
unary_projectedใส่ตัวฉาย บนทุก Qubit อื่นของโหมด นั่นบังคับข้อจำกัด one-hot ในตัวดำเนินการเอง: เทอมจะทำงานเฉพาะเมื่อส่วนที่เหลือของโหมดว่าง -
unary_unprojectedปล่อย Qubit เหล่านั้นไว้ ดังนั้นเทอมจึงทำหน้าที่เป็นเอกลักษณ์บนพวกมัน
ลองดู ตัวเดียวที่ ภายใต้ทั้งสองแบบ mapper คืน SparseObservable ซึ่งเทอมของมันเป็นสตริงของเทอมบิตแบบ Qubit เดี่ยว นี่คือสี่เทอมแรกของแต่ละแบบ พร้อม Qubit ที่มันกระทำ:
>>> 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
ทั้งสองมีสิบสองเทอมด้วยสัมประสิทธิ์เดียวกัน เทอมแบบ projected มีเทอมบิต 0 บน Qubit สองตัวที่เทอมแบบ unprojected ไม่กล่าวถึง ดังนั้นเทอม projected แต่ละเทอมแตะ Qubit ทั้งสี่ตัว และเทอม unprojected แต่ละเทอมแตะสองตัว โดยทั่วไปเทอม projected แตะ Qubit ทั้ง ตัวของโหมด เพราะมันมีสองตัวประกอบ แต่ละตัวเป็น หรือ และมีตัวฉายบนทุก Qubit อื่น
ตัวดำเนินการทั้งสองเท่ากันบนสถานะที่การเข้ารหัสมีไว้สำหรับ คือสถานะฐาน one-hot (codeword) และต่างกันนอกเหนือจากนั้น เพื่อตรวจสอบ ให้ใช้ตัวดำเนินการเล็กบนสองโหมดที่ แล้วเปรียบเทียบเมทริกซ์บนเก้า codeword:
>>> 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
ทั้งสองยังทำให้ปริภูมิย่อย codeword คงที่ ดังนั้นสถานะที่เตรียมบน codeword จะอยู่บน codeword ตัวเลือกมีผลเฉพาะกับสถานะที่ออกจากปริภูมิย่อยนั้น เช่นผ่านสัญญาณรบกวน ที่นั่นตัวดำเนินการ projected จะระงับวิวัฒนาการ เพราะตัวฉายของมันล้มเหลว ในขณะที่ตัวดำเนินการ unprojected ยังคงกระทำต่อไป พฤติกรรมทั้งสองไม่ใช่ข้อกำหนดทางฟิสิกส์ ตัวดำเนินการ unprojected นิยามเฉพาะบนปริภูมิย่อย one-hot ดังนั้นการเลือกมันเป็นการตัดสินใจด้านการสร้างแบบจำลอง ไม่ใช่การปรับให้เหมาะสมที่ฟรี
สิ่งที่ตัวฉายต้องแลกคือ Gate วิวัฒนาการ ที่เป็น Hermitian บนหนึ่งโหมดเป็นเวลาสั้นๆ แล้วนับ Gate สอง Qubit หลังการ transpile ของ 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 สังเคราะห์เกตวิวัฒนาการ ซึ่งเปลี่ยนไประหว่างรุ่น จึงควรพล็อตดู:
จำนวนแบบ unprojected เพิ่มเชิงเส้นตาม เพราะแต่ละการเปลี่ยนสถานะจาก ครั้งเป็นการหมุนสอง Qubit จำนวนแบบ projected เพิ่มเร็วกว่ามาก เพราะทุกเทอมกระทำกับ Qubit ทั้ง ตัว การเข้ารหัสแบบกะทัดรัดใช้ Qubit น้อยกว่า แต่น้ำหนักของเทอมขึ้นกับว่ารูปแบบบิตของระดับข้างเคียงต่างกันอย่างไร: รหัสเกรย์ที่ต่างกันหนึ่งบิตถูกกว่าในสองแบบกะทัดรัด และไบนารีมาตรฐานกระโดดเมื่อตัวทดพลิกหลายบิตพร้อมกัน () การเข้ารหัสแบบกะทัดรัดจะชนะยูนารีแบบ unprojected ด้วยหรือไม่ขึ้นกับ และวิธีที่ Qiskit สังเคราะห์วิวัฒนาการ ดังนั้นให้อ่านแนวโน้ม ไม่ใช่อันดับที่ขนาดเดียว
ตัวฉายยังมีผลเมื่อคุณขยายตัวดำเนินการที่แมปแล้วเป็น Pauli ซึ่ง from_sparse_observable() ทำโดยแทนตัวฉายแต่ละตัวด้วยผลรวมของตัวดำเนินการ Pauli สองตัว เทอมที่มีตัวฉาย ตัวกลายเป็นเทอม Pauli เทอม นี่คือ ตัวเดียวที่ เล็ก ซึ่งเป็นขนาดเดียวที่คุณควรลองทำแบบนี้:
>>> 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
ทั้งสองแบบเก็บจำนวนเทอมเท่ากัน แต่การขยายของแบบ projected โตแบบเอกซ์โพเนนเชียลใน สำหรับสิ่งที่ใหญ่กว่าตัวอย่างเล็กๆ ให้อ่านเทอมของ SparseObservable โดยตรง อย่างที่รายการข้างต้นทำ แทนที่จะแปลง
การเข้ารหัสแบบกะทัดรัดทิ้งตัวฉายไม่ได้ ที่นั่นบิตศูนย์เป็นส่วนหนึ่งของหมายเลขระดับ ดังนั้นการลบ ออกจะเปลี่ยนว่าเทอมเชื่อมระดับใด และตัวดำเนินการจะไม่เหมือนเดิมบน codeword นี่คือเหตุผลที่มีเพียงการเข้ารหัสยูนารีที่มีสองแบบ
ผลคูณของแลดเดอร์ถูกตัดทอนก่อน
ทางเลือกการออกแบบอีกข้อหนึ่งปรากฏในต้นทุนของเทอมแนวทแยง mapper ไม่ได้แมปตัวดำเนินการแลดเดอร์แต่ละตัวแล้วคูณผลลัพธ์ มันรวบรวมตัวดำเนินการแลดเดอร์ที่เทอมหนึ่งกระทำกับโหมดเดียว คูณพวกมันเป็นเมทริกซ์ ในฐาน Fock ตัดทอนผลคูณนั้นหนึ่งครั้ง และเข้ารหัสผลลัพธ์ [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
เทอมของมันคือตัวฉาย บน Qubit ของแต่ละระดับที่สูงกว่าศูนย์ ถ่วงน้ำหนักด้วยหมายเลขระดับ (ระดับศูนย์มีน้ำหนักศูนย์จึงไม่มีเทอม) วิวัฒนาการตามเวลาของมันเป็นชุดการหมุน Qubit เดี่ยวและไม่ต้องใช้ Gate สอง Qubit การคูณตัวดำเนินการแลดเดอร์ที่แมปแล้วสองตัวแทนจะให้ตัวดำเนินการเดียวกันบน codeword แต่ต่างกันนอกเหนือจากนั้น โดยมีสตริงนอกแนวทแยงที่ต้องเสีย Gate:
>>> 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
เทอมแนวทแยง เช่นอันตรกิริยาบนไซต์ของโมเดล Bose-Hubbard คือผลคูณเหล่านี้ ซึ่งเป็นที่ที่ทางเลือกนี้มีผล
เขียน mapper แบบกำหนดเอง
หากคุณต้องการการเข้ารหัสที่ไลบรารีไม่มีให้ ให้สร้างมันจากภาพของตัวดำเนินการแลดเดอร์ตัวเดียว map_boson_action_generators() รับฟังก์ชันที่แมป BosonAction หนึ่งตัว และฟังก์ชันที่คืนเอกลักษณ์ แล้วสร้าง mapper ของตัวดำเนินการทั้งตัวจากสองสิ่งนั้น มันคูณภาพของ action ของแต่ละเทอมตามลำดับ คูณด้วยสัมประสิทธิ์ และรวมเทอม ผลลัพธ์เป็นชนิดใดก็ได้ที่รองรับการคูณสเกลาร์และการประกอบ
เป็นตัวอย่าง ให้ใช้การเข้ารหัสสำหรับ ที่เก็บหนึ่งโหมดใน Qubit หนึ่งตัว ตัวดำเนินการสร้างคือ ซึ่งคือ ตามข้อตกลงของ 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
mapper ที่เขียนเองนี้คูณตัวดำเนินการแลดเดอร์ที่แมปแล้ว และตรงกับ mapper ของไลบรารีที่นี่ เพราะทุกโหมดมีสองระดับและ Qubit หนึ่งตัวเก็บทั้งโหมด ที่ ใหญ่ขึ้น ดังที่ส่วนก่อนหน้าแสดง การประกอบแลดเดอร์ที่แมปแล้วกับการตัดทอนผลคูณก่อนไม่ใช่ตัวดำเนินการเดียวกันนอก codeword
หากจะใช้ mapper แบบนี้ใน Circuit ให้ห่อมันด้วยฟังก์ชันที่มีลายเซ็น (operator, num_qubits) -> SparseObservable แล้วส่งให้ MapperFnEvolutionSynthesis ซึ่งแมปตัวดำเนินการของทุกเกต Evolution ด้วยมัน ไปป์ไลน์ของ Transpiler อธิบายไว้ใน Transpile Circuit แบบโบซอน
>>> 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)
มีรายละเอียดสองอย่างที่ควรรู้ อย่างแรก ตัวดำเนินการระดับ Qubit ต้องมีสัมประสิทธิ์เป็นจำนวนจริง เพราะ PauliEvolutionGate ปฏิเสธส่วนจินตภาพ ตัวดำเนินการ Hermitian แมปไปเป็นจำนวนจริง แต่ต่อเมื่อรวมเทอมที่หักล้างกันแล้ว mapper ที่เขียนเองนี้ไม่รวมพวกมัน ดังนั้นเทอมการกระโดดและตัวผูกพันของมันจึงทิ้งสัมประสิทธิ์จินตภาพไว้จนกว่าคุณจะทำให้ผลลัพธ์ง่ายขึ้น mapper ของไลบรารีรวมผลลัพธ์ของตัวเอง ส่วนแบบกำหนดเองต้องทำเอง เช่นห่อด้วย 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']
อย่างที่สอง สูตรผลคูณสังเคราะห์เทอมตามลำดับที่ได้รับ และลำดับที่ mapper สร้างไม่รับประกันว่าจะคงที่ ตัวช่วย simplify() และ group_wise() ห่อฟังก์ชัน mapper เพื่อควบคุมมัน: simplify รวมสตริงที่ซ้ำกันและเรียงเทอมเป็นลำดับมาตรฐาน ส่วน group_wise แมปตัวดำเนินการทีละกลุ่มเพื่อให้เทอมของกลุ่มอยู่ติดกัน อย่าใช้สองอย่างนี้ร่วมกัน เพราะการเรียงจะทิ้งการจัดกลุ่ม คู่มือการจัดกลุ่ม แสดงวิธีกำหนดกลุ่ม และ คู่มือการสังเคราะห์ แสดงว่าลำดับส่งผลต่อ Circuit อย่างไร
ขั้นตอนถัดไป
-
ทำตาม จำลองพลวัตของ Bose-Hubbard โดยเลือกการเข้ารหัส สำหรับการจำลองเต็มรูปแบบที่กวาดค่าตัดขาดและเปรียบเทียบการเข้ารหัสกับพลวัตที่แน่นอน
-
ดู Transpile Circuit แบบโบซอน เพื่อดูว่า
LevelSpecเดียวกันถูกนำไปใช้กับ Circuit แบบโบซอนอย่างไร -
อ่าน หลักการออกแบบการแทนตัวดำเนินการ เพื่อดูว่า
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.