Transpilacja obwodów bozonowych
Ten przewodnik wyjaśnia, jak dokonać transpilacji BosonicCircuit do standardowego QuantumCircuit. Obwód bozonowy nie niesie obcięcia, kodowania ani liczby kubitów (zobacz Praca z obwodami bozonowymi), więc to transpiler jest miejscem, w którym podejmowane są te decyzje. Obcięcie i kodowanie opisuje LevelSpec (zobacz Opisywanie obcięcia za pomocą LevelSpec), a mapper zamieniający operatory na operatory kubitowe jest opisany w Odwzorowywanie reprezentacji operatorów. Wynikiem jest zwykły QuantumCircuit, więc może go uruchomić każdy symulator zgodny z Qiskit.
Przewodnik używa jednego małego przykładu: dwóch modów bozonowych, stanu z jednym bozonem w każdym oraz pojedynczej ewolucji czasowej pod hamiltonianem Bosego-Hubbarda, w którym człony wiązań i człony lokalne są w osobnych grupach (zobacz Grupowanie członów operatora: wykorzystanie struktury operatora):
>>> from qiskit_bosons.circuit import BosonicCircuit
>>> from qiskit_bosons.circuit.library import Evolution, InitializeModes
>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>>
>>> terms = {(cre(0), ann(1)): -1.0, (cre(1), ann(0)): -1.0}
>>> terms.update({(cre(mode), cre(mode), ann(mode), ann(mode)): 0.5 for mode in range(2)})
>>> hamiltonian = BosonOperator.from_dict(terms)
>>> # the bond is group 0 and the on-site interaction is group 1; derive the index from the term,
>>> # because the stored order of a dictionary is not specified
>>> hamiltonian.groups = [
... 0 if len({mode for _, mode in term}) == 2 else 1 for term, _ in hamiltonian.iter_terms()
... ]
>>>
>>> circuit = BosonicCircuit(2)
>>> circuit.append(InitializeModes([1, 1]), circuit.modes)
>>> circuit.append(Evolution(2, hamiltonian, time=0.1), circuit.modes)
>>> dict(circuit.count_ops())
{'InitializeModes': 1, 'Evolution': 1}
Etapy transpilacji
Jak wyjaśniono w qiskit_bosons.transpiler, predefiniowane menedżery przebiegów w tym pakiecie dzielą transpilację na następujące etapy:
Wejście
Konwertuje obwód na strukturę danych skierowanego grafu acyklicznego (DAG), czyli reprezentację, na której pracują wszystkie późniejsze przebiegi.
Optymalizacja
Pozostawia obwód w przestrzeni bozonowej. Te przebiegi wykorzystują strukturę operatorów bozonowych, w szczególności ich grupy, podczas gdy informacja o kubitach jeszcze nie istnieje. B2BSynthesis to przebieg, który tutaj rozkłada bramki, z wtyczką dla każdego typu bramki: formułą produktową lub qDRIFT dla Evolution oraz siatką Clementsa dla ModeRotation (zobacz Synteza bramki Evolution w przestrzeni bozonowej, aby dowiedzieć się, jak wybierana jest metoda). RelabelModes zmienia kolejność modów. Wersja predefiniowana dekomponuje w tym etapie rotacje modów, a wszystko inne zostawia bez zmian.
Układ
Odwzorowuje każdy rejestr modów bozonowych na rejestr kwantowy. Mod bozonowy zajmuje zwykle kilka kubitów ( przy kodowaniach unarnych, przy kompaktowych), więc rejestr kwantowy jest szerszy niż rejestr modów. OccupationB2QLayout wyprowadza szerokość rejestru z LevelSpec.
Synteza
Konwertuje bramki bozonowe na operacje kubitowe, jedna wtyczka na typ bramki. W tym kroku odbywa się kodowanie: wtyczka dla bramki Evolution odwzorowuje jej operator na operator kubitowy, a wtyczka dla bramki InitializeModes ustawia kubity kodujące każde obsadzenie. Bariera jest przenoszona na każdy kubit każdego rejestru, którego dotykała.
Kubit
Transpiluje otrzymany obwód kubitowy własnymi metodami Qiskit, na przykład do bazowego zbioru bramek lub urządzenia. Predefiniowany menedżer zawsze go zawiera; domyślnie jest to predefiniowany menedżer przebiegów Qiskit, który syntezuje każdą ewolucję do bramek (zobacz Kontynuacja na poziomie kubitów).
Wyjście
Konwertuje wewnętrzną strukturę danych DAG z powrotem na QuantumCircuit.
Obcięcie i kodowanie nie są przechowywane w obwodzie. Są przekazywane transpilerowi, a układ, przygotowanie stanu i mapper muszą się co do nich zgadzać. Predefiniowany menedżer to zapewnia, przekazując ten sam LevelSpec wszystkim trzem.
Praktyczny przykład
generate_preset_occupation_pass_manager() buduje potok na podstawie specyfikacji. Obniżenie przykładu przy dwóch kodowaniach zmienia tylko specyfikację. Predefiniowany menedżer kończy się własną transpilacją Qiskit na poziomie kubitów, która zsyntezowałaby ewolucję do bramek. Aby zobaczyć obwód dokładnie tak, jak emitują go etapy bozonowe, najpierw opróżnij ten etap:
>>> from qiskit.transpiler import PassManager
>>> from qiskit_bosons.mappers import LevelSpec
>>> from qiskit_bosons.transpiler.presets import generate_preset_occupation_pass_manager
>>>
>>> for encoding in ("unary_unprojected", "gray"):
... pass_manager = generate_preset_occupation_pass_manager(LevelSpec(2, encoding))
... pass_manager.qubit = PassManager()
... lowered = pass_manager.run(circuit)
... print(f"{encoding:18s} qubits={lowered.num_qubits} ops={dict(lowered.count_ops())}")
unary_unprojected qubits=6 ops={'x': 2, 'PauliEvolution': 1}
gray qubits=4 ops={'x': 2, 'PauliEvolution': 1}
Dwa kodowania używają odpowiednio sześciu i czterech kubitów (trzy poziomy na mod w kodowaniu unarnym, dwa kubity na mod w kodzie Graya), ale struktura obwodu jest taka sama: dwie operacje XGate przygotowują obsadzenia, a jedna bramka PauliEvolutionGate niesie ewolucję.
Porównanie z tradycyjnym przepływem pracy
Przewodnik obwody bozonowe opisuje tradycyjny przepływ pracy, który odwzorowuje hamiltonian na operator kubitowy przed zbudowaniem QuantumCircuit. Obie drogi dają równoważne obwody. Różnią się tym, co trzeba powtórzyć przy zmianie obcięcia. Obwód bozonowy jest budowany raz i obniżany przy każdym odcięciu, podczas gdy droga z odwzorowaniem na początku wymaga ponownego odwzorowania operatora i przebudowania obwodu:
>>> from qiskit import QuantumCircuit
>>> from qiskit.circuit.library import PauliEvolutionGate
>>> from qiskit_bosons.mappers.library import occupation_encoding
>>>
>>> basis = ["rz", "sx", "x", "cx"]
>>> cx_counts = []
>>> for max_occupation in (1, 2, 3):
... spec = LevelSpec(max_occupation, "gray")
... # the bosonic circuit is reused as it is
... lowered = generate_preset_occupation_pass_manager(
... spec, optimization_level=0, basis_gates=basis
... ).run(circuit)
... # the map-first route maps the operator and builds a new circuit for every cutoff
... qubit_hamiltonian = occupation_encoding(hamiltonian, spec)
... by_hand = QuantumCircuit(qubit_hamiltonian.num_qubits)
... _ = by_hand.append(PauliEvolutionGate(qubit_hamiltonian, time=0.1), by_hand.qubits)
... cx_counts.append(lowered.count_ops().get("cx", 0))
... print(
... f"d={max_occupation + 1}: {lowered.num_qubits} qubits; map-first: "
... f"{len(qubit_hamiltonian)} Pauli terms to place by hand"
... )
d=2: 2 qubits; map-first: 2 Pauli terms to place by hand
d=3: 4 qubits; map-first: 10 Pauli terms to place by hand
d=4: 4 qubits; map-first: 22 Pauli terms to place by hand
Liczba bramek splątujących zależy od tego, jak Qiskit syntezuje bramkę ewolucji, co zmienia się między wydaniami, więc narysuj ją na wykresie:
Liczba szybko rośnie wraz z odcięciem, i właśnie dlatego odcięcie warto czynić parametrem transpilacji, a nie obwodu. Obie drogi są tu równoważne, ale tylko bozonowa potrafi także przeplatać grupowanie operatora z formułą produktową (zobacz Synteza bramki Evolution w przestrzeni bozonowej) lub przekazać kodowanie innej specyfikacji bez zmiany jednej linii obwodu.
Specyfikacja z jednym odcięciem pozostawia liczbę modów otwartą, więc jeden menedżer przebiegów obsługuje obwody o dowolnej szerokości. Specyfikacja z jednym odcięciem na mod ustala szerokość:
>>> wide = BosonicCircuit(3)
>>> wide.append(InitializeModes([1, 0, 1]), wide.modes)
>>> shared = generate_preset_occupation_pass_manager(LevelSpec(2, "gray"))
>>> shared.run(wide).num_qubits
6
>>> per_mode = generate_preset_occupation_pass_manager(LevelSpec([2, 1], "gray"))
>>> per_mode.run(circuit).num_qubits
3
>>> try:
... per_mode.run(wide)
... except ValueError as error:
... print(error)
the spec covers 2 modes, but 3 were expected
Ręczne budowanie potoku
Preset to MultiStagePassManager z jednym wpisem na każdy etap. Ten sam potok możesz złożyć z jego części, co jest punktem wyjścia dla każdej z poniższych personalizacji:
>>> from qiskit.passmanager import MultiStagePassManager
>>> from qiskit_bosons.mappers.library import occupation_encoding
>>> from qiskit_bosons.transpiler import BosonicCircuitToDAG, QuantumDAGToCircuit
>>> from qiskit_bosons.transpiler.passes import (
... B2QSynthesis,
... MapperFnEvolutionSynthesis,
... OccupationB2QLayout,
... OccupationInitializeModesSynthesis,
... )
>>>
>>> spec = LevelSpec(2, "gray")
>>>
>>> def mapper_fn(operator, num_qubits):
... return occupation_encoding(operator, spec, num_qubits)
>>>
>>> synthesis = B2QSynthesis()
>>> synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(mapper_fn)
>>> synthesis.methods["InitializeModes"] = OccupationInitializeModesSynthesis(spec)
>>>
>>> by_hand = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>> preset = generate_preset_occupation_pass_manager(spec)
>>> preset.qubit = PassManager()
>>> by_hand.run(circuit) == preset.run(circuit)
True
Funkcja mappera otrzymuje operator oraz szerokość rejestru wyznaczoną przez układ (layout) i zwraca SparseObservable. Ponieważ funkcja dostaje szerokość, przekazanie num_qubits do occupation_encoding() sprawia, że mapper sprawdza, czy specyfikacja mieści się w rejestrze, więc niezgodność między układem a kodowaniem kończy się czytelnym komunikatem o błędzie.
Zaawansowane przepływy pracy
Podaj jawny układ
CustomB2QLayout zastępuje etap układu podanym przez Ciebie odwzorowaniem każdego rejestru bozonowego obwodu na rejestr kubitowy. Jest przydatny, gdy rejestr musi być szerszy, niż wymaga specyfikacja, na przykład aby zostawić nieużywane kubity do innych celów:
>>> from qiskit.circuit import QuantumRegister
>>> from qiskit_bosons.transpiler.passes import CustomB2QLayout
>>>
>>> (mode_register,) = BosonicCircuitToDAG().run(circuit).qregs.values()
>>> custom = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... layout=CustomB2QLayout({mode_register: QuantumRegister(6, "q")}),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>> custom.run(circuit).num_qubits
6
Za własny układ odpowiadasz Ty. Wtyczki syntezy nadal adresują kubity przez specyfikację, więc rejestr niezgodny z szerokościami ze specyfikacji daje błędny obwód lub błąd głęboko wewnątrz wtyczki. Własny układ pomija też kontrolę, którą OccupationB2QLayout wykonuje po RelabelModes (zobacz następną sekcję).
Zmień kolejność modów
RelabelModes to przebieg etapu optymalizacji, który zmienia kolejność modów. Permutacja jest interpretowana jako odwzorowanie z oryginalnego indeksu modu na nowy i jest zapisywana w metadanych obwodu. Zmiana kolejności zmienia to, które kubity zajmuje każdy mod, a przez to może zmniejszyć głębokość obwodu po obniżeniu poziomu.
Ponieważ wynik jest wyrażony w nowej kolejności modów, zmierzone ciągi bitów trzeba odwzorować z powrotem, zanim się je zinterpretuje. Przy kilku kubitach na mod odwzorowanie działa na blokach kubitów, a nie na pojedynczych bitach. Poniższy przykład zmienia etykiety trzech modów, z których każdy zajmuje dwa kubity w kodzie Graya, i odzyskuje oryginalne obsadzenie ze zmierzonych zliczeń:
>>> # BasicSimulator is a convenient pure-Python choice; any simulator works
>>> from qiskit.providers.basic_provider import BasicSimulator
>>> from qiskit_bosons.transpiler.passes import RelabelModes
>>>
>>> state = BosonicCircuit(3)
>>> state.append(InitializeModes([2, 0, 1]), state.modes)
>>> spec = LevelSpec(2, "gray") # three levels, two qubits per mode
>>> width = spec.resolve(3).widths[0]
>>> permutation = [2, 0, 1]
>>>
>>> synthesis = B2QSynthesis()
>>> synthesis.methods["InitializeModes"] = OccupationInitializeModesSynthesis(spec)
>>> relabeled = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... optimization=RelabelModes(permutation),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... ).run(state)
>>> relabeled.metadata
{'permutation': [2, 0, 1]}
>>>
>>> def restore(bitstring, permutation, width):
... """Map a bitstring in the new mode order back to the original mode order."""
... qubits = bitstring[::-1] # character q is now the value of qubit q
... blocks = [qubits[mode * width : (mode + 1) * width] for mode in range(len(permutation))]
... return "".join(blocks[new] for new in permutation)[::-1]
>>>
>>> relabeled.measure_all()
>>> (measured,) = BasicSimulator().run(relabeled, shots=1).result().get_counts()
>>> measured
'110100'
>>> restore(measured, permutation, width)
'010011'
Odtworzony ciąg bitów to wynik, jaki obwód daje bez zmiany etykiet, odczytany w kolejności little-endian używanej w Qiskit: mod 0 ma kod Graya poziomu drugiego (11), mod 1 ma poziom zerowy (00), a mod 2 ma poziom pierwszy (01).
>>> plain = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... ).run(state)
>>> plain.measure_all()
>>> (reference,) = BasicSimulator().run(plain, shots=1).result().get_counts()
>>> restore(measured, permutation, width) == reference
True
Specyfikacji, która nadaje modom różne odcięcia, nie można połączyć z RelabelModes. Odcięcia są przypisane do modów według ich pozycji, więc zmiana kolejności modów przypisałaby odcięcie niewłaściwemu modowi. OccupationB2QLayout w takim przypadku odrzuca obwód ze zmienionymi etykietami:
>>> mixed = LevelSpec([2, 1, 2], "gray")
>>> synthesis = B2QSynthesis()
>>> synthesis.methods["InitializeModes"] = OccupationInitializeModesSynthesis(mixed)
>>> refused = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... optimization=RelabelModes(permutation),
... layout=OccupationB2QLayout(mixed),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>> try:
... refused.run(state)
... except ValueError as error:
... print(str(error).split(",")[0])
the circuit has been relabeled by RelabelModes
Wybierz wtyczki syntezy
B2QSynthesis nie ma domyślnych wtyczek; preset rejestruje po jednej dla każdego typu bramki. Dostępnymi wtyczkami zarządza B2QSynthesisPluginManager, który wymienia je według nazwy bramki, na której działają:
>>> from qiskit_bosons.transpiler.passes import B2QSynthesisPluginManager
>>> manager = B2QSynthesisPluginManager()
>>> for gate in manager.op_names():
... print(gate, manager.method_names(gate))
Evolution ['MapperFn']
InitializeModes ['Occupation']
Dwie wbudowane metody to Evolution.MapperFn i InitializeModes.Occupation. Pierwsza syntezuje ewolucję z dowolnej funkcji mappera, i w ten sposób podłącza się własne kodowanie (zobacz Map operator representations). Biblioteka nie zawiera wtyczki dla bramki ModeRotation. Preset najpierw rozkłada ją w przestrzeni bozonowej na bramki Evolution, które obsługuje pierwsza metoda, ale wtyczka zarejestrowana dla ModeRotation może zamiast tego obniżyć tę bramkę bezpośrednio; zobacz Rotate bosonic modes i Lower a rotation without decomposing it. Metodę możesz zarejestrować po nazwie przez argument config w B2QSynthesis albo wpisać obiekt wtyczki bezpośrednio do B2QSynthesis.methods, tak jak w poprzednich przykładach.
Kontynuuj na poziomie kubitów
Preset zawsze kończy się etapem qubit, czyli własnym presetowym menedżerem przebiegów Qiskit. Argumenty nazwane generate_preset_occupation_pass_manager() są przekazywane do qiskit.transpiler.generate_preset_pass_manager(), więc możesz przetłumaczyć obwód na bazowy zestaw bramek lub bezpośrednio wskazać urządzenie. Użyj optimization_level=0, aby obwód pozostał bliski temu, co wytworzyły etapy bozonowe:
>>> from qiskit.transpiler import generate_preset_pass_manager
>>>
>>> spec = LevelSpec(2, "gray")
>>> translated = generate_preset_occupation_pass_manager(
... spec, optimization_level=0, basis_gates=["rz", "sx", "x", "cx"]
... ).run(circuit)
>>> sorted(translated.count_ops())
['cx', 'rz', 'sx', 'x']
Powszechne są dwa inne wybory. Przypisz pusty PassManager do etapu qubit, gdy chcesz pokazać obwód dokładnie takim, jaki zsyntezował potok, tak jak w powyższych przykładach. Albo złóż potok samodzielnie i dodaj dowolnego menedżera przebiegów Qiskit jako etap qubit w MultiStagePassManager. Etapy działają w podanej kolejności, więc etap qubit musi poprzedzać output, ponieważ przebiegi Qiskit oczekują DAG, a nie gotowego obwodu:
>>> synthesis = B2QSynthesis()
>>> synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(
... lambda operator, num_qubits: occupation_encoding(operator, spec, num_qubits)
... )
>>> synthesis.methods["InitializeModes"] = OccupationInitializeModesSynthesis(spec)
>>> staged = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... qubit=generate_preset_pass_manager(
... optimization_level=0, basis_gates=["rz", "sx", "x", "cx"]
... ),
... output=QuantumDAGToCircuit(),
... )
>>> sorted(staged.run(circuit).count_ops())
['cx', 'rz', 'sx', 'x']
Kolejne kroki
-
Map operator representations o kodowaniach i pisaniu własnego mappera.
-
Synthesize an Evolution gate in bosonic space o wyborze formuły iloczynowej dla bramek ewolucji.
-
Rotate bosonic modes o obniżaniu rotacji modów.
-
Simulate Bose-Hubbard dynamics with a choice of encoding o pełnej symulacji, która porównuje obniżony obwód z dokładną dynamiką.
-
qiskit_bosons.transpilero pełnym opisie etapów i dostępnych przebiegów.