Rotește moduri bosonice
Un ModeRotation amestecă moduri bosonice cu o matrice unitară , așa cum o face o rețea de divizoare de fascicul și defazoare. Se numește pasiv deoarece conservă numărul total de bosoni: mută bosoni între moduri și nu creează și nu distruge niciunul. (Comprimarea și deplasarea sunt operațiile active, iar o rotație de moduri nu le poate exprima.) Ea mapează operatorii de creare astfel
care pe întregul spațiu Fock este unitarul many-body
Aceasta este poarta din spatele unui interferometru optic liniar: trimiți stări Fock la intrare, rotești modurile și citești cum se redistribuie bosonii. Ca orice poartă a unui BosonicCircuit (vezi Lucrează cu circuite bosonice), ea nu poartă nicio trunchiere și nicio codificare. Ambele intervin la momentul transpilării, iar acest ghid arată de ce trunchierea necesită o atenție deosebită pentru o rotație.
Construiește o rotație
Constructorul primește matricea unitară . Poarta acționează asupra a moduri. Clasa nu verifică dacă matricea este unitară, deci aceasta este responsabilitatea ta.
>>> import numpy as np
>>> from qiskit_bosons.circuit.library import ModeRotation
>>>
>>> unitary = np.array([[1, -1], [1, 1]]) / np.sqrt(2)
>>> rotation = ModeRotation(unitary)
>>> rotation.num_modes # the gate acts on two modes
2
Rotațiile uzuale au constructori dedicați. ModeRotation.beamsplitter() primește un unghi de amestec și o fază opțională , iar dă divizorul de fascicul echilibrat 50:50. ModeRotation.phase_shift() primește o fază pentru fiecare mod.
>>> beamsplitter = ModeRotation.beamsplitter(np.pi / 4)
>>> np.round(beamsplitter.rotation_unitary.real, 4)
array([[ 0.7071, -0.7071],
[ 0.7071, 0.7071]])
>>> bool(np.allclose(beamsplitter.rotation_unitary, unitary))
True
>>> shift = ModeRotation.phase_shift([0.0, np.pi / 2])
>>> np.round(np.diag(shift.rotation_unitary), 4)
array([1.+0.j, 0.+1.j])
Poți porni și de la generatorul anti-hermitian și lăsa poarta să îl exponențieze, ceea ce dă rotația . Generatorul este recuperat cu generator().
>>> generator = np.array([[0, -0.3], [0.3, 0]], dtype=complex)
>>> small = ModeRotation.from_generator(generator)
>>> np.round(small.rotation_unitary.real, 4)
array([[ 0.9553, -0.2955],
[ 0.2955, 0.9553]])
>>> bool(np.allclose(small.generator(), generator))
True
Puntea către algebra operatorilor este to_operator(), care returnează generatorul hermitian ca BosonOperator, astfel încât poarta este . Pentru divizorul de fascicul 50:50, aceasta este o pereche de termeni de salt cu coeficienți imaginari opuși. Operatorul este hermitian, ceea ce are nevoie o poartă Evolution, și conservă numărul de particule.
>>> operator = beamsplitter.to_operator()
>>> # The stored order of the terms is not specified, so sort them before printing.
>>> sorted((term[0], round(term[1].imag, 4)) for term in operator.iter_terms())
[([(True, 0), (False, 1)], -0.7854), ([(True, 1), (False, 0)], 0.7854)]
>>> operator.is_hermitian()
True
>>> operator.conserves_particle_number()
True
Fiecare termen citește (is_creation, mode) de la stânga la dreapta, deci al doilea este .
Acționează asupra stărilor Fock
O rotație mută bosoni între moduri, deci efectul ei se vede cel mai ușor pe stări Fock. Efectul Hong-Ou-Mandel este exemplul standard. Trimite câte un boson în fiecare mod de intrare al unui divizor de fascicul echilibrat. Cele două căi care lasă bosonii în moduri de ieșire diferite interferează distructiv, deci starea dispare, iar bosonii pleacă mereu împreună, în sau , fiecare cu probabilitatea o doime.
Construiește circuitul pe moduri bosonice și coboară-l cu pass manager-ul predefinit. LevelSpec fixează în acest punct trunchierea și codificarea. O limită de max_occupation=2 păstrează nivelurile 0, 1 și 2 ale fiecărui mod.
>>> from qiskit_bosons.circuit import BosonicCircuit
>>> from qiskit_bosons.circuit.library import InitializeModes
>>> from qiskit_bosons.mappers import LevelEncoding, LevelSpec
>>> from qiskit_bosons.transpiler.presets import generate_preset_occupation_pass_manager
>>>
>>> circuit = BosonicCircuit(2)
>>> circuit.append(InitializeModes([1, 1]), circuit.modes)
>>> circuit.append(ModeRotation.beamsplitter(np.pi / 4), circuit.modes)
Pipeline-ul returnează un circuit obișnuit pe qubiți. Citește probabilitățile rezultatelor din starea finală, aici cu Statevector din Qiskit, care este exact la această dimensiune. Funcționează orice simulator care returnează amplitudini sau numărători. O stare Fock este o singură stare de bază computațională, al cărei index este dat de tiparele de biți din LevelEncoding.level_to_bits(); qubiții celui de-al doilea mod urmează qubiților primului.
>>> from qiskit.quantum_info import Statevector
>>> from qiskit.transpiler import PassManager
>>>
>>> def fock_probabilities(circuit, max_occupation):
... """Maps each pair of occupations to the probability of that outcome."""
... encoding = LevelEncoding.UNARY_UNPROJECTED
... spec = LevelSpec(max_occupation, encoding)
... pass_manager = generate_preset_occupation_pass_manager(spec)
... pass_manager.qubit = PassManager() # keep the evolutions as the pipeline emitted them
... qubit_circuit = pass_manager.run(circuit)
... state = Statevector(qubit_circuit)
... width = encoding.num_qubits(max_occupation + 1)
... probabilities = {}
... for n0 in range(max_occupation + 1):
... for n1 in range(max_occupation + 1):
... index = encoding.level_to_bits(n0) | (encoding.level_to_bits(n1) << width)
... probability = round(float(abs(state.data[index]) ** 2), 4)
... if probability > 0:
... probabilities[(n0, n1)] = probability
... return probabilities
>>>
>>> fock_probabilities(circuit, max_occupation=2)
{(0, 2): 0.5, (2, 0): 0.5}
Rezultatul are probabilitate zero, iar cele două rezultate grupate împart probabilitatea în mod egal.
Alege limita
Rezultatul anterior necesită max_occupation=2. Rulează același circuit cu o limită de 1. Aceasta este suficientă pentru a conține starea de intrare , dar nu și starea pe care o produce rotația din ea, care are pondere pe și :
>>> fock_probabilities(circuit, max_occupation=1)
{(1, 1): 1.0}
Circuitul rulează fără nicio eroare, iar răspunsul este greșit: starea rămâne în cu probabilitatea unu. O rotație amestecă sectoarele de ocupare în interiorul unui mod. Doi bosoni pot ajunge în același mod de ieșire, deci starea exactă are pondere pe ocuparea 2 într-un mod care a început cu un boson. Cu o limită de 1, acel nivel nu există, iar generatorul trunchiat nu poate muta starea acolo.
O margine pentru ocuparea stării inițiale nu este deci suficientă. Limita trebuie să acopere numărul total de bosoni care pot ajunge în orice mod al rotației. Pentru bosoni în total, alegerea sigură este o limită de .
Același raționament se aplică fiecărui mod separat, deci un amestec de limite diferite pe mod (vezi Descrie trunchierea cu un LevelSpec) face ca o rotație să fie ușor de mapat incorect. Un mod care poate primi bosoni are nevoie de o limită care acoperă ceea ce poate primi, nu doar ceea ce conține inițial. Verificarea LevelSpec.truncation() compară un spec cu marginea inferioară de ocupare a unui operator. Ea nu poate semnala cazul de mai sus. Fiecare termen al generatorului divizorului de fascicul conține un singur operator de creare, deci marginea este unu, iar verificarea nu raportează niciun deficit la o limită de 1, deși rezultatul de acolo este greșit:
>>> LevelSpec(1, "unary_unprojected").truncation(beamsplitter.to_operator())
[0, 0]
O avertizare suplimentară decurge din felul în care transpiler-ul construiește o rotație (secțiunea următoare). El aplică rotația ca o secvență de factori pe două moduri, iar documentația plugin-ului avertizează că un singur factor poate popula un nivel pe care rotația completă nu îl atinge niciodată. Secvența trunchiată și evoluția unică trunchiată sunt atunci aproximări diferite ale aceluiași unitar. O căutare printre 1.080 de rotații aleatorii pe trei moduri, cu un boson în fiecare din două moduri de intrare și fără dublă ocupare la ieșire, nu a găsit un astfel de caz, deci tratează aceasta ca pe o precauție și nu ca pe o defecțiune frecventă. Fă limita să conveargă ca la orice alt circuit, mărind-o până când rezultatul nu se mai schimbă.
Cum sintetizează transpiler-ul o rotație
Etapa optimization a pipeline-ului predefinit descompune mai întâi un ModeRotation în spațiul bosonic, cu pass-ul B2BSynthesis și metoda BosonicClementsDecomposition (înregistrată ca ModeRotation.Clements). Aceasta este o alegere a presetului și nu o cerință a framework-ului: o secțiune ulterioară coboară aceeași poartă fără nicio descompunere. Descompunerea nu exponențiază generatorul complet dintr-un singur pas. Mai întâi factorizează unitarul în cel mult rotații între moduri adiacente plus faze pe un singur mod, urmând rețeaua dreptunghiulară a lui Clements et al. [1]. Fiecare factor devine o poartă Evolution atomică, pe care etapa de la boson la qubit o mapează ca pe oricare alta.
Factorizarea propriu-zisă este givens_decomposition(), pe care o apelează descompunerea. Aplicația care duce o matrice unitară în rotația many-body este un omomorfism de grupuri, deci produsul rotațiilor factor este exact egal cu rotația completă pe spațiul netrunchiat. Prin urmare, rețeaua nu adaugă nicio eroare proprie. Avantajul ei este localitatea. Fiecare evoluție emisă implică doar termenii Pauli ai celor două moduri pe care le amestecă (sau ai unui singur mod, pentru o fază), în loc de termeni din toate modurile.
Transpilează o rotație aleatoare pe trei moduri și inspectează ce a emis pipeline-ul. O matrice unitară generică necesită numărul maxim de trei rotații, plus trei faze, iar fiecare factor este propriul său PauliEvolutionGate. Toți sunt declarați pe întregul registru, dar operatorii din interior acționează doar pe câțiva qubiți:
>>> from qiskit.quantum_info import random_unitary
>>>
>>> random = np.asarray(random_unitary(3, seed=7).data)
>>> spec = LevelSpec(2, "unary_unprojected")
>>> rotation_circuit = BosonicCircuit(3)
>>> rotation_circuit.append(InitializeModes([1, 0, 0]), rotation_circuit.modes)
>>> rotation_circuit.append(ModeRotation(random), rotation_circuit.modes)
>>> pass_manager = generate_preset_occupation_pass_manager(spec)
>>> pass_manager.qubit = PassManager()
>>> qubit_circuit = pass_manager.run(rotation_circuit)
>>> dict(qubit_circuit.count_ops())
{'PauliEvolution': 6, 'x': 3}
>>> for instruction in qubit_circuit.data:
... if instruction.operation.name == "PauliEvolution":
... terms = instruction.operation.operator.to_sparse_list()
... print(len(terms), sorted({qubit for _, indices, _ in terms for qubit in indices}))
64 [0, 1, 2, 3, 4, 5]
64 [3, 4, 5, 6, 7, 8]
64 [0, 1, 2, 3, 4, 5]
2 [1, 2]
2 [4, 5]
2 [7, 8]
Cu trei niveluri per mod, fiecare mod ocupă trei qubiți. Un beamsplitter între două moduri atinge cei șase qubiți ai acelor moduri, iar o fază atinge qubiții unui singur mod.
Factorii sunt exacți doar dacă fiecare evoluție este sintetizată bine. Termenii unui generator de beamsplitter nu comută, deci formula de produs implicită, cu un singur pas, lasă o eroare în fiecare factor. Transmite o formulă de produs cu mai multe repetări pluginului Evolution care mapează factorii, pentru a o reduce:
>>> from qiskit.passmanager import MultiStagePassManager
>>> from qiskit.synthesis import LieTrotter
>>> from qiskit_bosons.circuit.library.synthesis import BosonicClementsDecomposition
>>> from qiskit_bosons.mappers.library import occupation_encoding
>>> from qiskit_bosons.transpiler import (
... BosonicCircuitToDAG,
... BosonicPassManager,
... QuantumDAGToCircuit,
... )
>>> from qiskit_bosons.transpiler.passes import (
... B2BSynthesis,
... B2QSynthesis,
... MapperFnEvolutionSynthesis,
... OccupationB2QLayout,
... OccupationInitializeModesSynthesis,
... )
>>>
>>> def with_product_formula(product_formula):
... synthesis = B2QSynthesis()
... synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(
... lambda operator, num_qubits: occupation_encoding(operator, spec, num_qubits),
... product_formula,
... )
... synthesis.methods["InitializeModes"] = OccupationInitializeModesSynthesis(spec)
... return MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... optimization=BosonicPassManager(
... B2BSynthesis({"ModeRotation": BosonicClementsDecomposition()})
... ),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
Un singur boson care intră în modul 0 iese în modul cu probabilitatea . Compară circuitul sintetizat cu acest rezultat exact pentru numere crescânde de repetări. Descompune mai întâi circuitul, astfel încât formula de produs să fie aplicată; fără aceasta, un simulator exact precum Statevector aplică poarta ca exponențială completă și ascunde eroarea.
>>> encoding = LevelEncoding.UNARY_UNPROJECTED
>>> width = encoding.num_qubits(3)
>>> exact = np.abs(random[:, 0]) ** 2
>>>
>>> def one_boson_probabilities(qubit_circuit):
... """Probability that the single boson sits in mode j, for each j."""
... state = Statevector(qubit_circuit)
... probabilities = []
... for j in range(3):
... occupations = [1 if mode == j else 0 for mode in range(3)]
... index = sum(
... encoding.level_to_bits(n) << (width * mode) for mode, n in enumerate(occupations)
... )
... probabilities.append(abs(state.data[index]) ** 2)
... return np.array(probabilities)
>>>
>>> for repetitions in (1, 4, 16):
... lowered = with_product_formula(LieTrotter(reps=repetitions)).run(rotation_circuit)
... error = np.max(np.abs(one_boson_probabilities(lowered.decompose(reps=2)) - exact))
... print(f"{repetitions:2d} repetitions: error {error:.1e}")
1 repetitions: error 1.2e-03
4 repetitions: error 7.6e-05
16 repetitions: error 4.7e-06
Eroarea scade de aproximativ patru ori la fiecare creștere de patru ori a numărului de repetări, așa cum se așteaptă pentru o formulă de ordinul întâi. Costul crește proporțional, deoarece fiecare factor este repetat.
Reduce o rotație fără a o descompune
O descompunere în spațiul bosonic este o modalitate de a reduce o rotație. (Biblioteca nu include altă modalitate, dar etapa de la boson la qubit nu depinde de ea.) B2QSynthesis caută un plugin după numele porții, iar un ModeRotation rămas întreg ajunge la el ca orice altă poartă. Un plugin pentru acesta mapează dintr-o dată generatorul lui ModeRotation.to_operator() și emite un singur PauliEvolutionGate la timp unitar. Asta este tot:
>>> from qiskit.circuit.library import PauliEvolutionGate
>>> from qiskit_bosons.transpiler.passes.utils import map_node_single_register
>>>
>>> class DirectModeRotationSynthesis:
... """Maps the generator of the whole rotation, with no bosonic decomposition."""
...
... def __init__(self, mapper_fn):
... self.mapper_fn = mapper_fn
...
... def run(self, in_node, out_dag, *, b2q_layout):
... modes, qreg = map_node_single_register(in_node, b2q_layout)
... generator = in_node.op.to_operator().relabel_modes(list(modes))
... observable = self.mapper_fn(generator, len(qreg))
... out_dag.apply_operation_back(PauliEvolutionGate(observable, time=1.0), tuple(qreg))
Înregistrează-l pentru numele porții într-un pass manager care nu o descompune:
>>> 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)
>>> synthesis.methods["ModeRotation"] = DirectModeRotationSynthesis(mapper_fn)
>>> direct = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>> direct_circuit = direct.run(rotation_circuit)
>>> dict(direct_circuit.count_ops())
{'x': 3, 'PauliEvolution': 1}
>>> terms = direct_circuit.data[-1].operation.operator.to_sparse_list()
>>> len(terms), sorted({qubit for _, indices, _ in terms for qubit in indices})
(198, [0, 1, 2, 3, 4, 5, 6, 7, 8])
Rotația este acum o singură evoluție pe toți cei nouă qubiți, în timp ce rețeaua are șase, dintre care niciuna nu cuprinde mai mult de șase. Cele două nu sunt aceeași aproximare. Trunchierea se aplică aici o singură dată generatorului întregii rotații, iar în rețea fiecărui factor separat (de unde avertismentul despre limita de mai sus). Calea directă este egală cu occupation_encoding() al operatorului la orice limită, deci este referința cu care se compară o rețea. Prețul ei este localitatea: termenii ei cuprind toate modurile, iar o formulă de produs are mai mulți de subdivizat.
Niciuna nu este mai bună în general, tocmai de aceea merită ca alegerea să rămână deschisă. Scrie un plugin ca acesta pentru a compara reducerile, pentru a folosi o rotație specifică unei codificări sau pentru a preda poarta unui component care o înțelege nativ.
Pașii următori
-
Transpilează circuite bosonice explică etapele pe care le rulează pass manager-ul presetat și cum să înlocuiești pluginul care gestionează o poartă.
-
Mapează reprezentările operatorilor acoperă trunchierea, codificările și
LevelSpec, care fixează aranjamentul fiecărui mod. -
Lucrează cu circuite bosonice arată cum să asamblezi și să combini circuite bosonice.
-
Documentația API listează
qiskit_bosons.circuit.librarypentru porți,qiskit_bosons.linalgpentru descompunere șiqiskit_bosons.transpiler.passespentru pluginurile de sinteză.
[1]
W. R. Clements, P. C. Humphreys, B. J. Metcalf, W. S. Kolthammer, and I. A. Walmsley, Optimal design for universal multiport interferometers, Optica 3, 1460 (2016), arXiv:1603.08788.