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

สังเคราะห์ Gate Evolution ในปริภูมิโบซอน

วิวัฒนาการตามเวลา e−itHe^{-itH} ของแฮมิลโทเนียนที่เป็นผลรวมของส่วนที่สลับที่กันไม่ได้ ไม่สามารถทำได้แบบแม่นยำทีละ Gate สูตรผลคูณ (product formula) ประมาณมันด้วยลำดับของวิวัฒนาการสั้น ๆ ภายใต้ส่วนย่อยแต่ละส่วน และค่าความคลาดเคลื่อนของการประมาณนี้เรียกว่าค่าความคลาดเคลื่อน Trotter

คุณใช้สูตรผลคูณได้สองที่ และสองที่นี้ใช้แทนกันไม่ได้

  1. ในปริภูมิโบซอน ก่อนที่จะเลือกการตัดทอนหรือการเข้ารหัสใด ๆ สูตรมองเห็น กลุ่ม ของตัวดำเนินการ (ดู จัดกลุ่มเทอมของตัวดำเนินการ: ใช้โครงสร้างของตัวดำเนินการ) และสลับสลับกลุ่มเหล่านั้นได้

  2. ในปริภูมิ Qubit ภายใน PauliEvolutionGate ของ Qiskit หลังจากแมปตัวดำเนินการแล้ว สูตรมองเห็นตัวดำเนินการเพาลีหนึ่งตัวต่อหนึ่งกลุ่ม และแบ่งย่อยได้เฉพาะเทอม ภายใน กลุ่มนั้น

มีเพียงแบบแรกที่ลดค่าความคลาดเคลื่อนระหว่างกลุ่มที่สลับที่กันไม่ได้ ซึ่งมักเป็นค่าที่ใหญ่ที่สุด คู่มือนี้แสดงวิธีใช้แบบนั้น

สำคัญ

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

ตั้งค่า​

โซ่ Bose-Hubbard สองไซต์ ซึ่งเป็นตัวอย่างที่เล็กที่สุดที่กลุ่มสลับที่กันไม่ได้ (ดู จำลองพลวัต Bose-Hubbard ด้วยการเลือกการเข้ารหัส สำหรับโมเดลฉบับเต็ม) มันถูกจัดเป็นกลุ่มเฮอร์มิเชียนหนึ่งกลุ่มสำหรับพันธะ และกลุ่มแนวทแยงหนึ่งกลุ่มสำหรับอันตรกิริยาบนไซต์:

>>> import numpy as np
>>> from qiskit.quantum_info import SparsePauliOp, Statevector
>>> from qiskit.transpiler import PassManager
>>> from scipy.sparse import csc_matrix
>>> from scipy.sparse.linalg import expm_multiply
>>> from qiskit_bosons.circuit import BosonicCircuit
>>> from qiskit_bosons.circuit.library import Evolution, InitializeModes
>>> from qiskit_bosons.mappers import LevelSpec
>>> from qiskit_bosons.mappers.library import occupation_encoding
>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>> from qiskit_bosons.transpiler.presets import generate_preset_occupation_pass_manager
>>>
>>> num_sites, max_occupation, total_time = 2, 2, 0.8
>>> bonds = [(0, 1)]
>>> terms = {}
>>> for left, right in bonds:
... terms[(cre(left), ann(right))] = -1.0
... terms[(cre(right), ann(left))] = -1.0
>>> for site in range(num_sites):
... terms[(cre(site), cre(site), ann(site), ann(site))] = 1.0
>>> hamiltonian = BosonOperator.from_dict(terms)
>>> hamiltonian.groups = [
... len(bonds)
... if len({mode for _, mode in term}) == 1
... else bonds.index(tuple(sorted({mode for _, mode in term})))
... for term, _ in hamiltonian.iter_terms()
... ]
>>> hamiltonian.num_groups()
2

ในการวัดค่าความคลาดเคลื่อน คุณต้องมีค่าอ้างอิง วิวัฒนาการที่แม่นยำของแฮมิลโทเนียน ตามที่แมปที่การตัดทอนเดียวกัน คือค่าที่ถูกต้อง เพราะการตัดทอนเป็นการประมาณแยกต่างหาก:

>>> spec = LevelSpec(max_occupation, "unary_projected")
>>> mapped = occupation_encoding(hamiltonian, spec).simplify()
>>> matrix = csc_matrix(SparsePauliOp.from_sparse_observable(mapped).to_matrix(sparse=True))
>>> pm = generate_preset_occupation_pass_manager(spec)
>>> pm.qubit = PassManager() # keep each group as one exact PauliEvolutionGate
>>>
>>> def build(**evolution_kwargs):
... circuit = BosonicCircuit(num_sites)
... circuit.append(InitializeModes([1, 1]), circuit.modes)
... circuit.append(
... Evolution(num_sites, hamiltonian, time=total_time, **evolution_kwargs), circuit.modes
... )
... return circuit
>>>
>>> # Under the unary encodings, occupation ``n`` of mode ``m`` sets qubit ``m * d + n``, with
>>> # ``d = max_occupation + 1`` levels per mode. That places |1, 1> on the basis state below.
>>> levels = max_occupation + 1
>>> index = sum(1 << (mode * levels + n) for mode, n in enumerate([1, 1]))
>>> initial = Statevector.from_int(index, 2**(num_sites * levels)).data
>>> exact = expm_multiply(-1j * total_time * matrix, initial)
>>> def error(circuit):
... """Distance to the exact evolution, of the circuit's product of group exponentials."""
... lowered = pm.run(circuit)
... state = Statevector.from_int(0, 2**lowered.num_qubits).evolve(lowered).data
... return float(np.max(np.abs(state - exact)))
หมายเหตุ

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

ค่าเริ่มต้น​

เมื่อไม่มีอาร์กิวเมนต์ synthesis Evolution Gate จะแยกส่วนด้วย BosonicLieTrotter ซึ่งเป็นสูตรอันดับหนึ่ง: หนึ่งตัวประกอบต่อกลุ่ม แต่ละตัวสำหรับเวลาทั้งหมด

>>> dict(build().decompose().count_ops())
{'Evolution': 2, 'InitializeModes': 1}

ถ้าไม่แยกส่วน Gate ทั้งก้อนจะถูกส่งไปยังขั้นตอน Qubit และจะไม่มีความคลาดเคลื่อน Trotter แบบโบซอนเลย:

>>> error(build()) < 1e-6
True

เมื่อแยกส่วนแล้ว จะเกิดความคลาดเคลื่อนจริง เพราะสองกลุ่มสลับที่กันไม่ได้:

>>> error(build().decompose()) > 0.1
True

ลดความคลาดเคลื่อน​

ส่งวิธีสังเคราะห์อื่นให้ Gate อันดับ (order) ที่สูงขึ้นประกอบตัวประกอบแบบสมมาตรเพื่อให้เทอมความคลาดเคลื่อนนำหน้าหักล้างกัน ส่วน การทำซ้ำ (repetitions) ที่มากขึ้นแบ่งเวลาเป็นขั้นที่สั้นลง

>>> from qiskit_bosons.circuit.library.synthesis import BosonicLieTrotter, BosonicSuzukiTrotter
>>>
>>> for label, synthesis in [
... ("Lie-Trotter", BosonicLieTrotter()),
... ("Suzuki, order 2", BosonicSuzukiTrotter(order=2)),
... ("Suzuki, order 4", BosonicSuzukiTrotter(order=4)),
... ("Suzuki, order 4*", BosonicSuzukiTrotter(order=4, merge_adjacent=False)),
... ]:
... circuit = build(synthesis=synthesis).decompose()
... print(f"{label:16s} {circuit.count_ops()['Evolution']:3d} factors")
Lie-Trotter 2 factors
Suzuki, order 2 3 factors
Suzuki, order 4 11 factors
Suzuki, order 4* 15 factors

ทุกครั้งที่เพิ่มอันดับขึ้นสอง จะประกอบห้า sweep ของอันดับก่อนหน้า (การเติบโตแบบ 5(k−2)/25^{(k-2)/2} ของการเวียนเกิดของ Suzuki ที่ใช้กับตัวประกอบ 2n−12n - 1 ตัวของอันดับ 2) ซึ่งเป็นจำนวนในบรรทัดสุดท้ายที่มีเครื่องหมายดาว sweep เหล่านั้นมาบรรจบที่ตัวประกอบเดียวกัน ดังนั้น Gate ที่อยู่ติดกันจึงวิวัฒน์ภายใต้ตัวดำเนินการเดียวกัน และถูกรวมเป็น Gate เดียวที่มีเวลารวมกัน ซึ่งประหยัดหนึ่ง Gate ต่อรอยต่อและแม่นยำ ส่ง merge_adjacent=False เพื่อคงลำดับตามตำรา ไม่ว่าแบบไหน อันดับที่สูงขึ้นแลกกับความคลาดเคลื่อนที่เล็กลง:

>>> lie = error(build(synthesis=BosonicLieTrotter()).decompose())
>>> second = error(build(synthesis=BosonicSuzukiTrotter(order=2)).decompose())
>>> fourth = error(build(synthesis=BosonicSuzukiTrotter(order=4)).decompose())
>>> lie > second > fourth
True

การทำซ้ำแลกความลึกกับความแม่นยำในอัตราที่ต่างออกไป ความคลาดเคลื่อนอันดับหนึ่งลดลงเป็นเส้นตรงตามขนาดขั้น ส่วนอันดับสองลดลงเป็นกำลังสอง:

>>> lie_errors = [error(build(synthesis=BosonicLieTrotter(reps=r)).decompose()) for r in (1, 2, 4, 8)]
>>> bool(all(a > b for a, b in zip(lie_errors, lie_errors[1:])))
True
>>> bool(lie_errors[0] / lie_errors[-1] > 6) # about 8, i.e. linear in 1 / reps
True
>>> suzuki_errors = [
... error(build(synthesis=BosonicSuzukiTrotter(order=2, reps=r)).decompose()) for r in (1, 2, 4)
... ]
>>> bool(suzuki_errors[0] / suzuki_errors[-1] > 10) # about 16, i.e. quadratic in 1 / reps
True

แบบไหนคุ้มกว่าขึ้นอยู่กับตัวดำเนินการ โปรดทราบว่าตัวดำเนินการที่มีกลุ่มเดียวแม่นยำที่ทุกอันดับ เพราะตัวประกอบเดี่ยวสลับที่กับตัวมันเองได้ และสูตรจะยุบเหลือ Gate เดียวแทนที่จะแบ่งใหม่

วัดต้นทุน​

ความคลาดเคลื่อนที่เล็กลงต้องใช้ตัวประกอบมากขึ้น และแต่ละตัวประกอบมีต้นทุนเป็น Gate พันกันเมื่อขั้นตอน Qubit สังเคราะห์มันแล้ว จำนวนเท่าไรขึ้นอยู่กับการเข้ารหัส ดังนั้นให้นับผ่านไปป์ไลน์ที่ตั้งไว้ล่วงหน้า โดยตรึงเบซิสและไม่มีการปรับให้เหมาะสมระดับ Qubit (optimization_level=0) เพื่อดูราคาของแต่ละวิธี:

>>> basis = ["rz", "sx", "x", "cx"]
>>>
>>> def cx_count(circuit, encoding):
... spec = LevelSpec(max_occupation, encoding)
... lowered = generate_preset_occupation_pass_manager(
... spec, optimization_level=0, basis_gates=basis
... ).run(circuit)
... return lowered.count_ops().get("cx", 0)
>>>
>>> methods = [
... ("Lie-Trotter", BosonicLieTrotter()),
... ("Suzuki, order 2", BosonicSuzukiTrotter(order=2)),
... ("Suzuki, order 4", BosonicSuzukiTrotter(order=4)),
... ]
>>> encodings = ("gray", "unary_unprojected")
>>> counts = {
... encoding: [
... cx_count(build(synthesis=method).decompose(), encoding) for _, method in methods
... ]
... for encoding in encodings
... }
>>> # the order buys accuracy at a price that grows with it
>>> bool(counts["gray"][0] < counts["gray"][1] < counts["gray"][2])
True

จำนวนที่แน่นอนขึ้นอยู่กับวิธีที่ Qiskit สังเคราะห์ Gate วิวัฒนาการ ซึ่งเปลี่ยนไปตามรุ่น ดังนั้นกราฟจึงแสดงจำนวนเหล่านั้นแทนตาราง:

Grouped bar chart of the CX count for Lie-Trotter and Suzuki of order 2 and 4, for the Gray and the unary encoding. The counts grow with the order, and the unary bars are higher.

แต่ละกลุ่มของแท่งคือหนึ่งวิธี: Lie-Trotter, Suzuki อันดับ 2 และ Suzuki อันดับ 4 อันดับแลกความแม่นยำด้วยราคาที่เพิ่มตามตัวประกอบของการเวียนเกิด และการเข้ารหัสแบบยูนารีจ่ายมากกว่ารหัส Gray ในทุกวิธี เพราะแต่ละตัวประกอบของมันแมปไปเป็น Gate พันกันมากกว่า ให้เลือกวิธีตามความแม่นยำที่ต้องการและเลือกการเข้ารหัสตามฮาร์ดแวร์ และแยกสองการเลือกนี้ออกจากกัน: Circuit แบบโบซอนข้างต้นไม่เปลี่ยนเลย

ใช้วิธีเดียวกับทั้ง Circuit​

ในการตั้งค่า synthesis ให้แต่ละ Gate คุณต้องส่งต่อตัวเลือกนั้นผ่านทุกสิ่งที่สร้าง Gate B2BSynthesis pass เลือกเพียงครั้งเดียวสำหรับทั้งไปป์ไลน์แทน config ของมันระบุวิธีสำหรับ Gate แต่ละชนิด และมันทั้งเลือกวิธีและขยาย Gate:

>>> from qiskit_bosons.transpiler import BosonicPassManager
>>> from qiskit_bosons.transpiler.passes import B2BSynthesis
>>>
>>> trotterize = BosonicPassManager([B2BSynthesis({"Evolution": BosonicSuzukiTrotter(order=2)})])
>>> dict(trotterize.run(build()).count_ops())
{'Evolution': 3, 'InitializeModes': 1}

วิธีหนึ่งยังระบุเป็นชื่อได้ พร้อมอาร์กิวเมนต์สำหรับ __init__ ของมัน แทนที่จะสร้างขึ้น: B2BSynthesis({"Evolution": ("SuzukiTrotter", (), {"order": 2})}) ชื่อที่ใช้ได้คือชื่อใน B2BSynthesisPluginManager และวิธีที่ไม่ได้ลงทะเบียนที่ไหนเลยสามารถส่งเป็นอินสแตนซ์ได้ ดังข้างต้น

เมื่อใช้ apply=False pass จะเลือกวิธีเท่านั้นและปล่อยการขยายไว้จนถึงจุดที่ต้องการ เช่นเพื่อตรวจสอบ Circuit ก่อน Gate แบบอะตอมิกจะไม่ถูกแตะต้อง ไม่แม้แต่ติดแท็ก:

>>> selected = BosonicPassManager(
... [B2BSynthesis({"Evolution": ("SuzukiTrotter", (), {"order": 2})}, apply=False)]
... ).run(build())
>>> selected.count_ops()["Evolution"] # selected, not yet expanded
1
>>> selected.decompose().count_ops()["Evolution"] # expanded on demand
3

Gate ที่ยังไม่ขยายจะไปถึงขั้นตอนโบซอนเป็น Qubit ทั้งก้อน ซึ่งถูกแมปโดยไม่ดู synthesis ของมัน ดังนั้นทุกวิธีจึงให้ผลลัพธ์เหมือนกัน

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

จำกัด pass ให้เฉพาะ Gate ที่ได้ประโยชน์​

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

>>> # one group per term, each Hermitian; the order is fixed because order two evolves the first
>>> # group twice, so which group comes first changes the cost
>>> diagonal = BosonOperator.from_terms_with_groups(
... [([cre(0), ann(0), cre(1), ann(1)], 1.0, 0), ([cre(0), ann(0)], 0.5, 1)]
... )
>>>
>>> def diagonal_cost(synthesis):
... circuit = BosonicCircuit(num_sites)
... circuit.append(Evolution(num_sites, diagonal, time=total_time, synthesis=synthesis), circuit.modes)
... return cx_count(circuit.decompose(), "gray")
>>>
>>> bool(diagonal_cost(BosonicSuzukiTrotter(order=2)) > diagonal_cost(BosonicLieTrotter()))
True

ทั้งสอง Circuit แม่นยำ ดังนั้นอันที่สองใช้ Gate พันกันมากกว่าโดยไม่ได้อะไร (สูตรอันดับสองแบบสมมาตรใช้กลุ่มแรกสองครั้งครึ่งเวลาละครั้ง จึงทำให้ต้นทุนขึ้นกับว่ากลุ่มไหนมาก่อน) filter รับเพรดิเคตเหนือ DAGOpNode และปล่อย Gate ที่ถูกปฏิเสธไว้ตามเดิม ดังนั้น pass เดียวจึงเพิ่มอันดับให้ Gate ที่ได้ประโยชน์และคงที่เหลือไว้ที่อันดับหนึ่ง เพรดิเคตด้านล่างยอมรับ Gate ก็ต่อเมื่อตัวดำเนินการของมันย้ายโบซอนระหว่างโหมด ซึ่งเป็นตอนที่กลุ่มสลับที่กันไม่ได้:

>>> def is_diagonal(operator):
... """Whether every term only counts bosons, rather than moving them between modes."""
... return all(
... sorted(mode for created, mode in actions if created)
... == sorted(mode for created, mode in actions if not created)
... for actions, _ in operator.iter_terms()
... )
>>>
>>> def mixed_circuit():
... circuit = BosonicCircuit(num_sites)
... circuit.append(Evolution(num_sites, hamiltonian, time=total_time), circuit.modes)
... circuit.append(Evolution(num_sites, diagonal, time=total_time), circuit.modes)
... return circuit
>>>
>>> order_2 = BosonicSuzukiTrotter(order=2)
>>> everywhere = cx_count(
... BosonicPassManager([B2BSynthesis({"Evolution": order_2})]).run(mixed_circuit()), "gray"
... )
>>> restricted = cx_count(
... BosonicPassManager(
... [
... B2BSynthesis(
... {"Evolution": order_2},
... filter=lambda node: not is_diagonal(node.op.operator),
... )
... ]
... ).run(mixed_circuit()),
... "gray",
... )
>>> bool(everywhere > restricted) # the filter saves entangling gates
True

ถ้าคุณรัน pass อีกครั้ง จะไม่มีอะไรเปลี่ยน: ตัวประกอบที่มันปล่อยออกมาเป็น อะตอมิก

ตัวประกอบอะตอมิก​

Evolution ที่ทำเครื่องหมาย atomic ไม่มีนิยาม ดังนั้น decompose() จะปล่อยมันไว้ที่เดิมไม่ว่าจะใช้กี่ครั้ง การสังเคราะห์ทำเครื่องหมายตัวประกอบที่มันปล่อยออกมาแบบนี้ และ BosonicQDrift ก็ทำเช่นกันกับเทอมที่มันสุ่มเลือก

นี่มากกว่าความเรียบร้อย การแบ่งกลุ่มเฮอร์มิเชียนต่อไปทีละเทอมให้ตัวประกอบที่แต่ละตัว ไม่ เป็นเฮอร์มิเชียน และเอกซ์โปเนนเชียลของตัวดำเนินการที่ไม่เป็นเฮอร์มิเชียนไม่เป็นยูนิตารี:

>>> circuit = build()
>>> once = circuit.decompose()
>>> dict(circuit.decompose(reps=5).count_ops()) == dict(once.count_ops())
True
ข้อควรระวัง

แต่ละตัวประกอบต้องเป็นเฮอร์มิเชียนด้วยตัวเอง ตัวดำเนินการเฮอร์มิเชียนที่แบ่งเป็นกลุ่มอาจให้กลุ่มที่ไม่เป็นเฮอร์มิเชียน เช่นเทอมการกระโดดที่แยกจากคู่ผันของมัน ตรวจสอบด้วย groups_are_hermitian() ตัวดำเนินการที่ ไม่มี กลุ่มจะถูกแบ่งทีละเทอม และขั้นตอน Qubit จะปฏิเสธตัวประกอบด้วย Operator contains complex coefficients การทำให้สมมาตรของอันดับคู่หักล้างความคลาดเคลื่อนของตัวประกอบที่ไม่เป็นเฮอร์มิเชียนได้บางส่วน ดังนั้นตัวดำเนินการที่จัดกลุ่มผิดจึงอาจดูดีกว่าที่อันดับ 2 เทียบกับอันดับ 1 ทั้งที่ยังผิดอยู่

ทำซ้ำขั้นสั้น​

สำหรับทางเลือกระดับ Circuit ให้สร้างขั้น Trotter สั้น ๆ หนึ่งขั้นแล้วทำซ้ำ repeat() แผ่การทำซ้ำเป็น Circuit เดียวและปล่อยให้ขอบเขตอยู่ติดกัน เพื่อให้ pass ทำงานข้ามขอบเขตเหล่านั้นได้:

>>> step = BosonicCircuit(num_sites)
>>> step.append(Evolution(num_sites, hamiltonian, time=total_time / 4), step.modes)
>>> dict(step.repeat(4).count_ops())
{'Evolution': 4}

ส่ง insert_barriers=True เพื่อแยกการทำซ้ำออกจากกัน barrier ถูกแมปไปยังทุก Qubit ของรีจิสเตอร์ที่มันแตะ เพราะการเข้ารหัสต้องใช้หลาย Qubit ต่อโหมด และโหมดไม่มีภาพต่อ Qubit ของตัวเอง:

>>> [inst.operation.name for inst in step.repeat(3, insert_barriers=True)._inner.data]
['Evolution', 'barrier', 'Evolution', 'barrier', 'Evolution']
หมายเหตุ

repeat ไม่เกี่ยวข้องกับ reps ของวิธีสังเคราะห์ การสังเคราะห์แบ่งย่อยวิวัฒนาการ เดียว เป็นตัวประกอบที่สั้นลงและมากขึ้น ส่วนการทำซ้ำ Circuit ที่มีวิวัฒนาการหนึ่งอันเพียงแค่คูณเวลารวมของมัน

รวมกับสูตรระดับ Qubit​

สองระดับนี้ประกอบกันได้ และเมื่อทั้งสองเป็นการประมาณ ความแม่นยำของผลลัพธ์จะถูกกำหนดโดยตัวที่อ่อนกว่า การเพิ่มอันดับแบบโบซอนในขณะที่สูตรฝั่ง Qubit ยังเป็นอันดับหนึ่งได้ประโยชน์น้อย ดู MapperFnEvolutionSynthesis สำหรับสูตรฝั่ง Qubit มันคงลำดับเทอมเพาลีที่ mapper เลือกไว้ ดังนั้น group_wise() จึงปล่อยเทอมของแต่ละกลุ่มให้อยู่ติดกันได้ และ simplify() กำหนดลำดับมาตรฐานแทนได้

ขั้นตอนถัดไป​