Lumaktaw sa pangunahing nilalaman
Ang proyektong ito ay nasa beta. Nakakita ng bug? Mag-bukas ng GitHub issue. May tanong o ideya? Magtanong sa Discussions — gusto naming marinig ang iyong feedback!
Gaano kaganda ang kalidad ng pagsasalin sa pahinang ito?

I-rotate ang mga bosonic mode

Hinahalo ng isang ModeRotation ang mga bosonic mode gamit ang unitary matrix na UU, tulad ng ginagawa ng network ng mga beamsplitter at phase shifter. Tinatawag itong passive dahil pinapanatili nito ang kabuuang bilang ng boson: naglilipat ito ng mga boson sa pagitan ng mga mode at hindi kailanman lumilikha o sumisira ng isa. (Ang squeezing at displacement ay ang mga active na operasyon, at hindi ito kayang ipahayag ng mode rotation.) Ina-map nito ang mga creation operator bilang

bi†↦∑jUji bj† ,b^\dagger_i \mapsto \sum_j U_{ji} \, b^\dagger_j \, ,

na sa buong Fock space ay ang many-body unitary na

exp⁡(∑ijlog⁡(U)ij bi†bj) .\exp\left(\sum_{ij} \log(U)_{ij} \, b^\dagger_i b_j\right) \, .

Ito ang gate sa likod ng isang linear-optical interferometer: magpadala ng mga Fock state, i-rotate ang mga mode, at basahin kung paano muling naipamahagi ang mga boson. Tulad ng bawat gate ng isang BosonicCircuit (tingnan ang Gumamit ng mga bosonic circuit), wala itong dalang truncation at walang encoding. Pareho silang pumapasok sa oras ng transpile, at ipinapakita ng gabay na ito kung bakit nangangailangan ng espesyal na pag-iingat ang truncation para sa isang rotation.

Bumuo ng rotation​

Kinukuha ng constructor ang M×MM \times M na unitary matrix. Kumikilos ang gate sa MM na mga mode. Hindi sinusuri ng class kung unitary ang matrix, kaya iyon ay responsibilidad mo.

>>> 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

May mga nakalaang constructor ang mga karaniwang rotation. Ang ModeRotation.beamsplitter() ay kumukuha ng mixing angle na θ\theta at opsyonal na phase na ϕ\phi, at ang θ=π/4\theta = \pi/4 ay nagbibigay ng balanseng 50:50 na beamsplitter. Ang ModeRotation.phase_shift() ay kumukuha ng isang phase kada mode.

>>> 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])

Puwede ka ring magsimula sa anti-Hermitian na generator na KK at hayaan ang gate na i-exponentiate ito, na nagbibigay ng rotation na exp⁡(K)\exp(K). Nakukuha muli ang generator gamit ang 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

Ang tulay papunta sa operator algebra ay ang to_operator(), na nagbabalik ng Hermitian na generator na H=∑ijilog⁡(U)ij bi†bjH = \sum_{ij} \mathrm{i} \log(U)_{ij} \, b^\dagger_i b_j bilang isang BosonOperator, kaya ang gate ay e−iHe^{-\mathrm{i} H}. Para sa 50:50 na beamsplitter, isa itong pares ng mga hopping term na may magkabilang imaginary na coefficient. Hermitian ang operator, na siyang kailangan ng isang Evolution gate, at pinapanatili nito ang bilang ng particle.

>>> 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

Binabasa ang bawat term bilang (is_creation, mode) mula kaliwa pakanan, kaya ang pangalawa ay iπ4 b1†b0\mathrm{i} \tfrac{\pi}{4} \, b^\dagger_1 b_0.

Kumilos sa mga Fock state​

Naglilipat ng mga boson sa pagitan ng mga mode ang isang rotation, kaya pinakamadaling makita ang epekto nito sa mga Fock state. Ang Hong-Ou-Mandel effect ang karaniwang halimbawa. Magpadala ng isang boson sa bawat input mode ng balanseng beamsplitter. Ang dalawang landas na nag-iiwan sa mga boson sa magkaibang output mode ay nag-i-interfere nang destructive, kaya nawawala ang estadong ∣1,1⟩\lvert 1, 1 \rangle at laging magkasamang umaalis ang mga boson, sa ∣2,0⟩\lvert 2, 0 \rangle o ∣0,2⟩\lvert 0, 2 \rangle, bawat isa ay may probability na kalahati.

Buuin ang circuit sa mga bosonic mode at ibaba ito gamit ang preset pass manager. Itinatakda ng LevelSpec ang truncation at ang encoding sa puntong ito. Ang cutoff na max_occupation=2 ay nagtatago ng mga level 0, 1, at 2 ng bawat mode.

>>> 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)

Nagbabalik ang pipeline ng ordinaryong qubit circuit. Basahin ang mga outcome probability mula sa huling estado, dito gamit ang Statevector ng Qiskit, na eksakto sa laking ito. Gumagana ang anumang simulator na nagbabalik ng mga amplitude o count. Ang isang Fock state na ∣n0,n1⟩\lvert n_0, n_1 \rangle ay iisang computational basis state, na ang index ay ibinibigay ng mga bit pattern ng LevelEncoding.level_to_bits(); sumusunod ang mga qubit ng pangalawang mode sa mga qubit ng una.

>>> 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}

Zero ang probability ng outcome na ∣1,1⟩\lvert 1, 1 \rangle, at pantay na naghahati ang dalawang bunched na outcome sa probability.

Piliin ang cutoff​

Kailangan ng naunang resulta ang max_occupation=2. Patakbuhin ang parehong circuit na may cutoff na 1. Sapat iyon para hawakan ang input state na ∣1,1⟩\lvert 1, 1 \rangle, pero hindi ang estadong nililikha ng rotation mula rito, na may bigat sa ∣2,0⟩\lvert 2, 0 \rangle at ∣0,2⟩\lvert 0, 2 \rangle:

>>> fock_probabilities(circuit, max_occupation=1)
{(1, 1): 1.0}

Tumatakbo ang circuit nang walang anumang error, at mali ang sagot: nananatili ang estado sa ∣1,1⟩\lvert 1, 1 \rangle na may probability na isa. Hinahalo ng rotation ang mga occupation sector sa loob ng isang mode. Puwedeng mapunta ang dalawang boson sa iisang output mode, kaya ang eksaktong estado ay may bigat sa occupation 2 sa mode na nagsimula sa isang boson. Sa cutoff na 1, hindi umiiral ang level na iyon, at hindi maililipat ng na-truncate na generator ang estado roon.

Kaya hindi sapat ang bound sa occupation ng paunang estado. Dapat sakop ng cutoff ang kabuuang bilang ng boson na puwedeng makarating sa anumang iisang mode ng rotation. Para sa NN na boson sa kabuuan, ang ligtas na pagpili ay cutoff na NN.

Nalalapat ang parehong pangangatwiran sa bawat mode nang hiwalay, kaya ang halo ng magkakaibang cutoff kada mode (tingnan ang Ilarawan ang truncation gamit ang LevelSpec) ay nagpapadali ng maling pag-map ng isang rotation. Ang mode na puwedeng tumanggap ng mga boson ay nangangailangan ng cutoff na sumasakop sa kaya nitong tanggapin, hindi lang sa hawak nito sa simula. Ikinukumpara ng check na LevelSpec.truncation() ang isang spec sa lower bound ng occupation ng isang operator. Hindi nito matutukoy ang kaso sa itaas. Ang bawat term ng beamsplitter generator ay may hawak na iisang creation operator, kaya ang bound ay isa at walang iniuulat na kakulangan ang check sa cutoff na 1, kahit mali ang resulta roon:

>>> LevelSpec(1, "unary_unprojected").truncation(beamsplitter.to_operator())
[0, 0]

May isa pang babala na nagmumula sa paraan ng pagbuo ng transpiler ng rotation (susunod na seksyon). Inilalapat nito ang rotation bilang sunud-sunod na two-mode na mga factor, at binabalaan ng dokumentasyon ng plugin na ang isang factor ay puwedeng magpuno ng level na hindi kailanman naaabot ng kumpletong rotation. Magkaibang approximation ng parehong unitary ang na-truncate na sequence at ang na-truncate na iisang evolution. Ang paghahanap sa 1,080 na random na three-mode rotation, na may isang boson sa bawat isa sa dalawang input mode at walang double occupation sa output, ay hindi nakahanap ng ganoong kaso, kaya ituring ito bilang pag-iingat at hindi karaniwang pagkabigo. I-converge ang cutoff gaya ng gagawin mo sa anumang ibang circuit, sa pamamagitan ng pagtaas nito hanggang tumigil sa pagbabago ang resulta.

Paano sini-synthesize ng transpiler ang isang rotation​

Ang yugto ng optimization ng preset pipeline ay naghahati muna sa isang ModeRotation sa bosonic space, gamit ang pass na B2BSynthesis at ang method na BosonicClementsDecomposition (nakarehistro bilang ModeRotation.Clements). Pagpili iyon ng preset at hindi requirement ng framework: ibinababa ng isang susunod na seksyon ang parehong gate nang walang anumang decomposition. Hindi ine-exponentiate ng decomposition ang buong generator sa isang hakbang. Una nitong fina-factorize ang unitary sa hanggang M(M−1)/2M(M-1)/2 na mga rotation sa pagitan ng magkatabing mode kasama ang MM na single-mode phase, ayon sa rectangular mesh ni Clements et al. [1]. Ang bawat factor ay nagiging atomic na Evolution gate, na ima-map ng boson-to-qubit stage tulad ng iba pa.

Ang factorization mismo ay givens_decomposition(), na tinatawag ng decomposition. Ang mapa mula sa isang unitary papunta sa many-body rotation ay isang group homomorphism, kaya ang product ng mga factor rotation ay eksaktong katumbas ng buong rotation sa untruncated na espasyo. Kaya walang dagdag na error mula sa mesh mismo. Ang benepisyo nito ay locality. Bawat inilalabas na evolution ay may kinalaman lang sa mga Pauli term ng dalawang mode na hinahalo nito (o ng isang mode para sa phase), sa halip na mga term sa lahat ng mode.

I-transpile ang isang random na three-mode rotation at tingnan kung ano ang inilabas ng pipeline. Ang isang generic na 3×33 \times 3 unitary ay nangangailangan ng maximum na tatlong rotation, dagdag ang tatlong phase, at bawat factor ay sarili nitong PauliEvolutionGate. Lahat sila ay idineklara sa buong register, pero ang mga operator sa loob ay kumikilos sa kakaunting qubit lang:

>>> 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]

Sa tatlong level bawat mode, bawat mode ay gumagamit ng tatlong qubit. Ang beamsplitter sa pagitan ng dalawang mode ay humahawak sa anim na qubit ng mga mode na iyon, at ang phase ay humahawak sa mga qubit ng isang mode.

Eksakto lang ang mga factor kapag maayos ang synthesis ng bawat evolution. Hindi nagko-commute ang mga term ng generator ng beamsplitter, kaya ang default na single-step na product formula ay nag-iiwan ng error sa bawat factor. Magpasa ng product formula na may mas maraming repetition sa plugin na Evolution na nagma-map ng mga factor para mabawasan ito:

>>> 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(),
... )

Ang isang boson na pumapasok sa mode 0 ay lalabas sa mode jj na may probability na ∣Uj0∣2|U_{j0}|^2. Ihambing ang na-synthesize na circuit sa eksaktong resultang iyon para sa dumaraming bilang ng repetition. I-decompose muna ang circuit para maipatupad ang product formula; kung hindi, ang isang exact simulator gaya ng Statevector ay ipapatupad ang gate bilang buong exponential nito at itatago ang error.

>>> 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

Bumababa ang error nang humigit-kumulang apat na beses sa bawat apat na beses na pagtaas ng repetition, gaya ng inaasahan sa isang first-order na formula. Proporsyonal na lumalaki ang gastos, dahil inuulit ang bawat factor.

Ibaba ang isang rotation nang hindi ito dine-decompose​

Ang decomposition sa bosonic space ay isang paraan para ibaba ang isang rotation. (Walang ibang paraan sa library, pero hindi umaasa rito ang boson-to-qubit stage.) Hinahanap ng B2QSynthesis ang isang plugin ayon sa pangalan ng gate, at ang isang ModeRotation na iniwang buo ay umaabot dito tulad ng ibang gate. Ang plugin para dito ay nagma-map ng generator ng ModeRotation.to_operator() nang sabay-sabay at naglalabas ng iisang PauliEvolutionGate sa unit time. Iyon na ang lahat:

>>> 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))

I-register ito para sa pangalan ng gate sa isang pass manager na hindi nagde-decompose nito:

>>> 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])

Ang rotation ay isa nang evolution sa lahat ng siyam na qubit, samantalang ang mesh ay may anim, na wala ni isang sumasaklaw sa higit sa anim. Hindi magkapareho ang dalawang approximation. Ang truncation ay inilalapat nang isang beses sa generator ng buong rotation dito, at sa bawat factor nang hiwalay sa mesh (kaya ang babala tungkol sa cutoff sa itaas). Ang direktang landas ay katumbas ng occupation_encoding() ng operator sa anumang cutoff, kaya ito ang reference na pagkukumparahan ng isang mesh. Ang kapalit nito ay locality: ang mga term nito ay sumasaklaw sa lahat ng mode, at mas maraming term ang kailangang hatiin ng isang product formula.

Walang mas mabuti sa pangkalahatan, kaya nga bukas ang pagpili. Sumulat ng plugin na tulad nito para ihambing ang mga lowering, gumamit ng encoding-specific na rotation, o ipasa ang gate sa isang bagay na nakakaintindi nito nang native.

Susunod na hakbang​

[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.