Rotace bosonových módů
ModeRotation míchá bosonové módy unitární maticí , podobně jako síť beamsplitterů a fázových posuvů. Nazývá se pasivní, protože zachovává celkový počet bosonů: přesouvá bosony mezi módy a žádný nevytváří ani neničí. (Squeezing a posunutí jsou aktivní operace a rotace módů je vyjádřit nedokáže.) Tvořicí operátory mapuje jako
což je na celém Fockově prostoru mnohočásticový unitární operátor
Toto je gate za lineárně-optickým interferometrem: pošli dovnitř Fockovy stavy, rotuj módy a přečti, jak se bosony přerozdělí. Jako každý gate BosonicCircuit (viz Práce s bosonovými obvody) nenese žádné ořezání ani kódování. Obojí vstupuje v době transpilace a tento průvodce ukazuje, proč ořezání u rotace vyžaduje zvláštní péči.
Sestavení rotace
Konstruktor přijímá unitární matici . Gate působí na módů. Třída nekontroluje, zda je matice unitární, takže to je tvá odpovědnost.
>>> 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
Běžné rotace mají vyhrazené konstruktory. ModeRotation.beamsplitter() přijímá směšovací úhel a volitelnou fázi a dává vyvážený beamsplitter 50:50. ModeRotation.phase_shift() přijímá jednu fázi na mód.
>>> 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])
Můžeš také vyjít z antihermitovského generátoru a nechat gate, aby ho exponencioval, což dává rotaci . Generátor se získá zpět pomocí 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
Mostem k algebře operátorů je to_operator(), která vrací hermitovský generátor jako BosonOperator, takže gate je . Pro beamsplitter 50:50 je to dvojice hoppingových členů s opačnými imaginárními koeficienty. Operátor je hermitovský, což gate Evolution potřebuje, a zachovává počet částic.
>>> 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
Každý člen se čte jako (is_creation, mode) zleva doprava, takže ten druhý je .
Působení na Fockovy stavy
Rotace přesouvá bosony mezi módy, takže její účinek je nejlépe vidět na Fockových stavech. Standardním příkladem je Hongův-Ouův-Mandelův jev. Pošli po jednom bosonu do každého vstupního módu vyváženého beamsplitteru. Dvě cesty, které nechají bosony v různých výstupních módech, spolu destruktivně interferují, takže stav zmizí a bosony vždy odcházejí spolu, v nebo , každý s pravděpodobností jedna polovina.
Sestav obvod na bosonových módech a převeď ho nižší úroveň pomocí přednastaveného pass manageru. LevelSpec v tomto bodě pevně určuje ořezání a kódování. Ořezání max_occupation=2 ponechává úrovně 0, 1 a 2 každého módu.
>>> 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 vrací běžný qubitový obvod. Pravděpodobnosti výsledků přečti z koncového stavu, zde pomocí Statevector z Qiskitu, který je při této velikosti přesný. Funguje jakýkoli simulátor, který vrací amplitudy nebo počty. Fockův stav je jediný výpočetní bázový stav, jehož index je dán bitovými vzory z LevelEncoding.level_to_bits(); qubity druhého módu následují za qubity prvního módu.
>>> 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}
Výsledek má pravděpodobnost nula a dva svázané výsledky si pravděpodobnost dělí rovným dílem.
Volba ořezání
Předchozí výsledek potřebuje max_occupation=2. Spusť stejný obvod s ořezáním na 1. To stačí k uložení vstupního stavu , ale ne stavu, který z něj rotace vytvoří a který má váhu na a :
>>> fock_probabilities(circuit, max_occupation=1)
{(1, 1): 1.0}
Obvod proběhne bez jediné chyby a odpověď je chybná: stav zůstává v s pravděpodobností jedna. Rotace míchá obsazovací sektory uvnitř módu. Dva bosony mohou skončit ve stejném výstupním módu, takže přesný stav má váhu na obsazení 2 v módu, který začínal s jedním bosonem. S ořezáním na 1 tato úroveň neexistuje a ořezaný generátor tam stav přesunout nedokáže.
Mez obsazení počátečního stavu proto nestačí. Ořezání musí pokrýt celkový počet bosonů, které mohou dosáhnout kteréhokoli jednoho módu rotace. Pro celkem bosonů je bezpečnou volbou ořezání .
Stejná úvaha platí pro každý mód zvlášť, takže směs různých ořezání po módech (viz Popis ořezání pomocí LevelSpec) usnadňuje chybné mapování rotace. Mód, který může přijímat bosony, potřebuje ořezání, které pokryje to, co může přijmout, a ne jen to, co obsahuje na začátku. Kontrola LevelSpec.truncation() porovnává spec s dolní mezí obsazení operátoru. Výše uvedený případ označit nedokáže. Každý člen generátoru beamsplitteru obsahuje jediný tvořicí operátor, takže mez je jedna a kontrola při ořezání na 1 nehlásí žádný nedostatek, přestože je výsledek tam chybný:
>>> LevelSpec(1, "unary_unprojected").truncation(beamsplitter.to_operator())
[0, 0]
Další upozornění vyplývá ze způsobu, jakým transpiler rotaci sestavuje (další část). Aplikuje rotaci jako posloupnost dvoumódových faktorů a dokumentace pluginu varuje, že jediný faktor může zaplnit úroveň, kterou úplná rotace nikdy nedosáhne. Ořezaná posloupnost a ořezaná jediná evoluce jsou pak různé aproximace téhož unitárního operátoru. Prohledání 1 080 náhodných třímódových rotací s jedním bosonem ve dvou vstupních módech a bez dvojího obsazení na výstupu takový případ neodhalilo, takže to ber jako opatření, a ne jako běžnou chybu. Ořezání konverguj stejně jako u jakéhokoli jiného obvodu, tedy zvyšuj ho, dokud se výsledek přestane měnit.
Jak transpiler syntetizuje rotaci
Fáze optimization přednastavené pipeline nejprve rozloží ModeRotation v bosonovém prostoru pomocí passu B2BSynthesis a metody BosonicClementsDecomposition (registrované jako ModeRotation.Clements). To je volba přednastavení, a ne požadavek frameworku: pozdější část převádí tentýž gate bez jakékoli dekompozice. Dekompozice neexponencuje celý generátor najednou. Nejprve rozloží unitární operátor na nejvýše rotací mezi sousedními módy plus jednomódových fází podle obdélníkové sítě od Clementse a kol. [1]. Každý faktor se stane atomickým gatem Evolution, který fáze mapování bosonů na qubity mapuje jako jakýkoli jiný.
Samotná faktorizace je givens_decomposition(), kterou dekompozice volá. Zobrazení z unitární matice na mnohočásticovou rotaci je grupový homomorfismus, takže součin rotací jednotlivých faktorů se přesně rovná celé rotaci na neoříznutém prostoru. Síť proto nepřidává žádnou vlastní chybu. Její výhodou je lokalita. Každá vygenerovaná evoluce zahrnuje jen Pauliho členy dvou módů, které míchá (nebo jednoho módu u fáze), místo členů přes všechny módy.
Transpiluj náhodnou rotaci tří módů a prohlédni si, co pipeline vygenerovala. Obecná unitární matice potřebuje maximálně tři rotace plus tři fáze a každý faktor je samostatný PauliEvolutionGate. Všechny jsou deklarovány na celém registru, ale operátory uvnitř působí jen na několik qubitů:
>>> 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]
Při třech úrovních na mód zabírá každý mód tři qubity. Beamsplitter mezi dvěma módy se dotýká šesti qubitů těchto módů a fáze se dotýká qubitů jednoho módu.
Faktory jsou přesné, jen když je každá evoluce dobře syntetizována. Členy generátoru beamsplitteru nekomutují, takže výchozí jednokroková produktová formule zanechává chybu v každém faktoru. Pro její snížení předej produktovou formuli s více opakováními pluginu Evolution, který faktory mapuje:
>>> 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(),
... )
Jediný boson vstupující do módu 0 odchází v módu s pravděpodobností . Porovnej syntetizovaný obvod s tímto přesným výsledkem pro rostoucí počty opakování. Obvod nejprve dekomponuj, aby se produktová formule použila; bez toho přesný simulátor jako Statevector aplikuje hradlo jako úplnou exponenciálu a chybu skryje.
>>> 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
Chyba klesá zhruba čtyřikrát při každém čtyřnásobném zvýšení počtu opakování, jak se u formule prvního řádu očekává. Náklady rostou úměrně, protože se každý faktor opakuje.
Sniž rotaci bez dekompozice
Dekompozice v bosonickém prostoru je jeden ze způsobů, jak rotaci snížit. (Knihovna jiný způsob neobsahuje, ale fáze boson-na-qubit na něm nezávisí.) B2QSynthesis hledá plugin podle názvu hradla a ModeRotation ponechaná vcelku k němu dorazí jako každé jiné hradlo. Plugin pro ni zmapuje generátor z ModeRotation.to_operator() najednou a vygeneruje jediný PauliEvolutionGate v jednotkovém čase. To je všechno:
>>> 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))
Zaregistruj ho pod názvem hradla v pass manageru, který ho nedekomponuje:
>>> 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])
Rotace je nyní jedna evoluce přes všech devět qubitů, zatímco síť má šest, z nichž žádná nepokrývá více než šest. Obě aproximace nejsou stejné. Oříznutí se zde aplikuje jednou na generátor celé rotace, v síti na každý faktor zvlášť (odtud varování o cutoffu výše). Přímá cesta se při libovolném cutoffu rovná occupation_encoding() daného operátoru, takže je referencí, se kterou se síť porovnává. Cenou je lokalita: její členy zahrnují všechny módy a produktová formule jich má více k rozdělení.
Ani jedno není obecně lepší, a proto je dobré nechat volbu otevřenou. Napiš plugin jako tento, abys porovnal snížení, použil rotaci specifickou pro kódování nebo předal hradlo něčemu, co mu rozumí nativně.
Další kroky
-
Transpilace bosonických obvodů vysvětluje fáze, které předvolený pass manager spouští, a jak nahradit plugin zpracovávající hradlo.
-
Mapování reprezentací operátorů pokrývá oříznutí, kódování a
LevelSpec, který určuje rozložení každého módu. -
Práce s bosonickými obvody ukazuje, jak bosonické obvody sestavovat a kombinovat.
-
Dokumentace API uvádí
qiskit_bosons.circuit.librarypro hradla,qiskit_bosons.linalgpro dekompozici aqiskit_bosons.transpiler.passespro syntézní pluginy.
[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.