Pular para o conteúdo principal
Este projeto está em fase beta. Encontrou um bug? Abra uma issue no GitHub. Tem uma pergunta ou ideia? Pergunte no Discussions — adoraríamos receber seu feedback!
Como está a qualidade da tradução nesta página?

Transpile circuitos bosônicos

Este guia explica como transpilar um BosonicCircuit para um QuantumCircuit padrão. Um circuito bosônico não carrega truncagem, codificação nem contagem de qubits (veja Trabalhe com circuitos bosônicos), então o transpilador é onde essas escolhas são feitas. A truncagem e a codificação são descritas por um LevelSpec (veja Descreva a truncagem com um LevelSpec), e o mapper que transforma operadores em operadores de qubits é descrito em Mapeie representações de operadores. O resultado é um QuantumCircuit comum, então qualquer simulador compatível com o Qiskit pode executá-lo.

O guia usa um pequeno exemplo do começo ao fim: dois modos bosônicos, um estado com um bóson em cada um e uma única evolução temporal sob um Hamiltoniano de Bose-Hubbard cujos termos de ligação e locais estão em grupos separados (veja Agrupe termos de operadores: use a estrutura do operador):

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

Etapas da transpilação​

Como explicado em qiskit_bosons.transpiler, os pass managers predefinidos deste pacote dividem a transpilação nas seguintes etapas:

Entrada​

Converte o circuito em uma estrutura de dados de grafo acíclico dirigido (DAG), a representação sobre a qual todas as passagens posteriores trabalham.

Otimização​

Mantém o circuito no espaço bosônico. Essas passagens usam a estrutura dos operadores bosônicos, em particular seus grupos, enquanto ainda não existe informação de qubits. B2BSynthesis é a passagem que decompõe as portas aqui, com um plugin para cada tipo de porta: uma fórmula de produto ou qDRIFT para uma Evolution e a malha de Clements para uma ModeRotation (veja Sintetize uma porta Evolution no espaço bosônico para saber como um método é escolhido). RelabelModes reordena os modos. O preset decompõe rotações de modo nesta etapa e deixa todo o resto como está.

Disposição​

Mapeia cada registrador de modos bosônicos para um registrador quântico. Um modo bosônico geralmente ocupa vários qubits (dd deles nas codificações unárias, ⌈log⁡2d⌉\lceil \log_2 d \rceil nas compactas), então o registrador quântico é mais largo que o registrador de modos. OccupationB2QLayout deriva a largura do registrador a partir de um LevelSpec.

Síntese​

Converte portas bosônicas em operações de qubits, um plugin por tipo de porta. A codificação acontece nesta etapa: o plugin para uma porta Evolution mapeia seu operador para um operador de qubits, e o plugin para uma porta InitializeModes define os qubits que codificam cada ocupação. Uma barreira é levada para todo qubit de cada registrador que ela tocava.

Qubit​

Transpila o circuito de qubits resultante com os métodos próprios do Qiskit, por exemplo para um conjunto de portas base ou um dispositivo. O preset sempre o inclui; por padrão é o pass manager predefinido do Qiskit, que sintetiza toda evolução em portas (veja Continue no nível de qubits).

Saída​

Converte a estrutura de dados DAG interna de volta em um QuantumCircuit.

A truncagem e a codificação não são armazenadas no circuito. Elas são dadas ao transpilador, e o layout, a preparação de estado e o mapper devem concordar sobre elas. O preset garante isso ao entregar o mesmo LevelSpec aos três.

Um exemplo prático​

generate_preset_occupation_pass_manager() constrói o pipeline a partir de uma especificação. Reduzir o exemplo sob duas codificações só muda a especificação. O preset termina na transpilação em nível de qubits do próprio Qiskit, que sintetizaria a evolução em portas. Para ver o circuito exatamente como as etapas bosônicas o emitem, esvazie essa etapa primeiro:

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

As duas codificações usam seis e quatro qubits, respectivamente (três níveis por modo na codificação unária, dois qubits por modo no código de Gray), mas a estrutura do circuito é a mesma: duas operações XGate preparam as ocupações, e um PauliEvolutionGate carrega a evolução.

Compare com o fluxo de trabalho tradicional​

O guia de circuitos bosônicos descreve o fluxo de trabalho tradicional, que mapeia o Hamiltoniano para um operador de qubits antes de construir o QuantumCircuit. As duas rotas produzem circuitos equivalentes. Elas diferem no que precisa ser refeito quando a truncagem muda. O circuito bosônico é construído uma vez e reduzido em cada corte, enquanto a rota que mapeia primeiro precisa que o operador seja mapeado de novo e o circuito reconstruído:

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

A contagem de portas emaranhadoras depende de como o Qiskit sintetiza a porta de evolução, o que muda entre versões, então plote-a:

CX count of the lowered circuit against the number of levels d, growing quickly with d.

A contagem cresce rapidamente com o corte, e essa é a razão para manter o corte como parâmetro da transpilação e não do circuito. As duas rotas são equivalentes aqui, mas somente a bosônica também pode intercalar o agrupamento do operador com a fórmula de produto (veja Sintetize uma porta Evolution no espaço bosônico) ou entregar a codificação a outra especificação sem alterar uma linha do circuito.

Uma especificação com um único corte deixa o número de modos em aberto, então um pass manager atende circuitos de qualquer largura. Uma especificação com um corte por modo fixa a largura:

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

Construa o pipeline manualmente​

O preset é um MultiStagePassManager com uma entrada por etapa. Você pode montar o mesmo pipeline a partir de suas partes, que é o ponto de partida para cada personalização abaixo:

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

A função mapeadora recebe um operador e a largura do registrador que o layout derivou, e retorna um SparseObservable. Como ela recebe a largura, passar num_qubits para occupation_encoding() faz o mapeador verificar se a especificação cabe no registrador, de modo que uma incompatibilidade entre o layout e a codificação falha com uma mensagem clara.

Fluxos de trabalho avançados​

Forneça um layout explícito​

CustomB2QLayout substitui a etapa de layout por um mapeamento que você fornece, de cada registrador bosônico do circuito para um registrador quântico. É útil quando o registrador precisa ser mais largo do que a especificação exige, por exemplo para deixar qubits ociosos para outros fins:

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

Um layout personalizado é de sua responsabilidade. Os plugins de síntese ainda endereçam os qubits por meio da especificação, então um registrador que não corresponde às larguras da especificação gera um circuito errado ou um erro no fundo de um plugin. Um layout personalizado também ignora a verificação que OccupationB2QLayout realiza depois de RelabelModes (veja a próxima seção).

Reordene os modos​

RelabelModes é uma passagem da etapa de otimização que reordena os modos. A permutação é lida como um mapeamento do índice original para o novo índice de modo, e é registrada nos metadados do circuito. A reordenação muda quais qubits cada modo ocupa e, assim, pode reduzir a profundidade do circuito reduzido.

Como o resultado é expresso na nova ordem dos modos, as bitstrings medidas precisam ser mapeadas de volta antes de serem interpretadas. Com vários qubits por modo, o mapeamento age sobre blocos de qubits e não sobre bits individuais. O exemplo a seguir reetiqueta três modos, cada um ocupando dois qubits sob o código Gray, e recupera a ocupação original a partir das contagens medidas:

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

A bitstring restaurada é o que o circuito produz sem o reetiquetamento, lida na ordem little-endian do Qiskit: o modo 0 contém o código Gray do nível dois (11), o modo 1 contém o nível zero (00) e o modo 2 contém o nível um (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
Aviso

Uma especificação que dá aos modos cutoffs diferentes não pode ser combinada com RelabelModes. Os cutoffs são associados aos modos pela posição, então reordenar os modos colocaria um cutoff no modo errado. OccupationB2QLayout recusa um circuito reetiquetado nesse caso:

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

Escolha os plugins de síntese​

B2QSynthesis não tem plugins padrão; o preset registra um por tipo de porta. Os plugins disponíveis são gerenciados por B2QSynthesisPluginManager, que os lista pelo nome da porta sobre a qual atuam:

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

Os dois métodos embutidos são Evolution.MapperFn e InitializeModes.Occupation. O primeiro sintetiza uma evolução a partir de qualquer função mapeadora, e é assim que uma codificação personalizada é conectada (veja Map operator representations). A biblioteca não traz nenhum plugin para uma porta ModeRotation. O preset a decompõe no espaço bosônico em portas Evolution primeiro, que o primeiro método então trata, mas um plugin registrado para ModeRotation pode reduzir a porta diretamente; veja Rotate bosonic modes e Lower a rotation without decomposing it. Você pode registrar um método pelo nome por meio do argumento config de B2QSynthesis, ou escrever um objeto de plugin diretamente em B2QSynthesis.methods, como fazem os exemplos anteriores.

Continue no nível de qubit​

O preset sempre termina em uma etapa qubit, que é o próprio gerenciador de passagens preset do Qiskit. Os argumentos nomeados de generate_preset_occupation_pass_manager() são repassados para qiskit.transpiler.generate_preset_pass_manager(), então você pode traduzir para um conjunto de portas base ou ter como alvo um dispositivo diretamente. Prefira optimization_level=0 para manter o circuito próximo ao que as etapas bosônicas produziram:

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

Duas outras escolhas são comuns. Atribua um PassManager vazio à etapa qubit quando quiser mostrar o circuito exatamente como o pipeline o sintetizou, como fazem os exemplos acima. Ou monte o pipeline você mesmo e adicione qualquer gerenciador de passagens do Qiskit como a etapa qubit do MultiStagePassManager. As etapas são executadas na ordem em que são dadas, então a etapa qubit deve vir antes de output, porque as passagens do Qiskit esperam um DAG e não um circuito finalizado:

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

Próximos passos​