Mapear representações de operadores
Um mapeador transforma um BosonOperator em um operador de qubits, aqui um SparseObservable. O operador em si não sabe nada sobre qubits: ele armazena operadores de escada em modos, e um modo bosônico comporta um número arbitrário de partículas. Tudo o que torna o operador finito (quantos níveis de ocupação você mantém e como esses níveis são dispostos nos qubits) é decidido quando você o mapeia.
O mapeador da biblioteca occupation_encoding() trabalha na base de ocupação (Fock): o nível de um modo é o estado com bósons. Ele recebe o operador e um LevelSpec, que contém o truncamento e a codificação. Este guia explica o que essas duas escolhas significam e quanto cada uma custa. Para um caminho completo que as utiliza, veja Simular a dinâmica de Bose-Hubbard com a escolha de uma codificação; para saber como o transpilador aplica as mesmas escolhas a um circuito inteiro, veja Transpilar circuitos bosônicos.
Truncar o espaço de Fock
Um modo mantido em níveis, onde é seu max_occupation, deixa de obedecer à relação de comutação canônica. Escrevendo para o operador de aniquilação truncado nesses níveis,
a relação vale em todos os níveis, exceto no mais alto [1]. Você pode ver isso mapeando o comutador dos dois operadores de escada e lendo sua diagonal nos níveis de um modo. O auxiliar a seguir constrói a matriz densa de um operador mapeado para um único modo, o que só é razoável para registradores muito pequenos:
>>> import numpy as np
>>> from qiskit.quantum_info import SparsePauliOp
>>> from qiskit_bosons.mappers import LevelEncoding, LevelSpec
>>> from qiskit_bosons.mappers.library import occupation_encoding
>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>>
>>> def level_diagonal(operator, max_occupation, encoding):
... """The diagonal of a mapped one-mode operator on the codewords of the levels."""
... spec = LevelSpec(max_occupation, encoding)
... matrix = SparsePauliOp.from_sparse_observable(occupation_encoding(operator, spec)).to_matrix()
... codewords = [spec.encoding.level_to_bits(level) for level in range(max_occupation + 1)]
... return [round(float(matrix[c, c].real), 6) for c in codewords]
>>>
>>> b = BosonOperator.from_dict({(ann(0),): 1.0})
>>> b_dagger = BosonOperator.from_dict({(cre(0),): 1.0})
>>> commutator = (b @ b_dagger) - (b_dagger @ b)
>>> level_diagonal(commutator, 2, "unary_unprojected") # d = 3
[1.0, 1.0, -2.0]
A última entrada é . Como isso significa que o espaço gerado por não é fechado sob o comutador, nenhuma codificação é um isomorfismo de álgebras. Uma codificação mapeia estados, juntamente com os operadores que atuam dentro do subespaço truncado. As duas consequências a seguir importam na prática.
O corte deve limitar a ocupação alcançável. Não basta limitar a ocupação com a qual você começa. Um divisor de feixe (beamsplitter) atuando em popula e , então um corte de um produz silenciosamente um estado errado:
>>> from scipy.linalg import expm
>>>
>>> beamsplitter = BosonOperator.from_dict({(cre(0), ann(1)): 1.0, (cre(1), ann(0)): 1.0})
>>>
>>> def stays_in_11(max_occupation):
... """Probability to remain in |1, 1> after a 50:50 beamsplitter."""
... spec = LevelSpec(max_occupation, "gray").resolve(2)
... observable = occupation_encoding(beamsplitter, spec, spec.total_width)
... matrix = SparsePauliOp.from_sparse_observable(observable).to_matrix()
... unitary = expm(-1j * (np.pi / 4) * matrix)
... index = sum(spec.encoding.level_to_bits(1) << offset for offset in spec.offsets)
... return round(float(abs(unitary[index, index]) ** 2), 6)
>>>
>>> stays_in_11(1), stays_in_11(2)
(1.0, 0.0)
Com um corte de um, a probabilidade de permanecer em é um, porque os dois estados que a dinâmica popularia não existem. Com um corte de dois, ela se anula, o que é o efeito Hong-Ou-Mandel. É por isso que max_occupation é um argumento obrigatório que nunca tem um valor padrão: é uma decisão de modelagem e um parâmetro de convergência que você deve varrer.
O mapeamento não comuta com a ordenação normal. Os operadores e são iguais antes do truncamento, mas são mapeados para operadores de qubits diferentes:
>>> product = BosonOperator.from_dict({(ann(0), cre(0)): 1.0})
>>> ordered = BosonOperator.from_dict({(cre(0), ann(0)): 1.0}) + BosonOperator.one()
>>> bool(product.equiv(ordered))
True
>>> level_diagonal(product, 3, "unary_unprojected") # d = 4
[1.0, 2.0, 3.0, 0.0]
>>> level_diagonal(ordered, 3, "unary_unprojected")
[1.0, 2.0, 3.0, 4.0]
Os dois concordam em todos os níveis, exceto no mais alto. Aplicado ao nível mais alto, não tem para onde ir e a amplitude é cortada, então é zero ali, enquanto dá . Decida de uma vez se o seu hamiltoniano é definido na forma ordenada normal e mapeie essa forma de maneira consistente.
Três métodos ajudam você a escolher um corte, e todos eles são limites inferiores. O operador informa o maior número de operadores de criação que ele aplica a cada modo dentro de um único termo, e uma spec compara isso com seus cortes:
>>> quartic = BosonOperator.from_dict({(cre(0), cre(0), ann(0), ann(0)): 1.0})
>>> quartic.mode_occupations(), quartic.max_occupation()
([2], 2)
>>> LevelSpec(1, "gray").truncation(quartic) # one level short
[1]
>>> LevelSpec(2, "gray").truncation(quartic)
[0]
Uma entrada diferente de zero significa que o corte está abaixo do limite, e o valor indica por quanto. Esses auxiliares não conhecem a dinâmica, então uma verificação aprovada não torna um corte suficiente; o divisor de feixe acima tem um limite de um, mas precisa de um corte de dois. Eles também podem informar uma deficiência para um termo que é zero nos níveis retidos, como é para um corte de um.
Descreva o truncamento com um LevelSpec
Um LevelSpec reúne o corte de cada modo com a codificação, e deriva deles quantos qubits cada modo ocupa e onde seus qubits começam. Tudo o que precisa concordar sobre isso (o mapeador, o layout do transpilador, a preparação do estado) recebe a mesma spec. A codificação é um argumento obrigatório, assim como o corte: nenhum dos dois tem valor padrão.
O corte é ou um inteiro, compartilhado por todos os modos, ou um inteiro por modo:
>>> shared = LevelSpec(2, "gray")
>>> per_mode = LevelSpec([4, 2, 2], "gray")
>>> shared.num_modes is None, per_mode.num_modes
(True, 3)
Uma spec construída a partir de um único inteiro deixa o número de modos em aberto, então você precisa resolvê-lo antes de pedir as larguras. Uma sequência nunca é propagada (broadcast), qualquer que seja seu comprimento, e ela fixa o número de modos:
>>> per_mode.widths, per_mode.offsets, per_mode.total_width
([3, 2, 2], [0, 3, 5], 7)
>>> shared.resolve(3).widths
[2, 2, 2]
>>> try:
... shared.widths
... except ValueError as error:
... print(error)
this spec was built from a single cutoff, so its number of modes is open; call `resolve(num_modes)` first
O modo ocupa os qubits de offsets[m] a offsets[m] + widths[m] - 1, então modos com cortes diferentes podem ocupar números diferentes de qubits, e os deslocamentos são a soma acumulada das larguras. Você não precisa passar o número de modos ao mapeador. Ele o obtém da spec ou, no caso de uma spec com um único corte, do operador por meio de min_num_modes(). O registrador pode ser mais largo do que os modos precisam; os qubits excedentes ficam ociosos e ficam no final:
>>> number = BosonOperator.from_dict({(cre(0), ann(0)): 1.0})
>>> number.min_num_modes()
1
>>> occupation_encoding(number, LevelSpec(1, "gray")).num_qubits
1
>>> occupation_encoding(number, LevelSpec(1, "gray"), 4).num_qubits
4
Um operador que atua em um modo além do último de uma spec por modo é recusado:
>>> far = BosonOperator.from_dict({(cre(5), ann(5)): 1.0})
>>> try:
... occupation_encoding(far, LevelSpec([1, 1], "gray"))
... except ValueError as error:
... print(error)
the operator acts on 6 modes, but the spec covers only 2
Alguns limites decorrem da forma como as codificações contam qubits. Em uma codificação compacta, um modo com um único nível (max_occupation = 0) não ocupa nenhum qubit, porque seu único nível não precisa de nada para ser distinguido de outro. Esse modo é permitido e não desloca os qubits dos modos seguintes, mas uma spec em que todos os modos não ocupariam nenhum qubit é rejeitada. As codificações unárias comportam no máximo 128 níveis por modo, e um corte maior gera um erro que sugere uma codificação compacta:
>>> LevelSpec([0, 1], "gray").resolve(2).widths
[0, 1]
>>> try:
... LevelSpec(0, "gray")
... except ValueError as error:
... print(str(error).split(":")[0])
the spec would span no qubits
>>> try:
... LevelSpec(128, "unary_projected")
... except ValueError as error:
... print(str(error).split(" (")[0])
max_occupation = 128 is too large for the unary encodings, which hold at most 128 levels per mode
Escolha uma codificação
Um LevelEncoding decide qual estado da base de um bloco de qubits representa cada nível. Há quatro, e nenhuma delas é a melhor em geral [2]. O número de qubits por modo e o padrão de bits de cada nível decorrem da codificação:
>>> encodings = (
... LevelEncoding.UNARY_PROJECTED,
... LevelEncoding.UNARY_UNPROJECTED,
... LevelEncoding.STANDARD_BINARY,
... LevelEncoding.GRAY,
... )
>>> rows = ["encoding qubits for d = 1, 2, 3, 4, 5, 8 level 0 to 4 as bit patterns"]
>>> for encoding in encodings:
... widths = [encoding.num_qubits(levels) for levels in (1, 2, 3, 4, 5, 8)]
... patterns = [f"{encoding.level_to_bits(level):05b}" for level in range(5)]
... rows.append(f"{encoding!s:18s} {widths!s:34s} {patterns}")
>>> print("\n".join(rows))
encoding qubits for d = 1, 2, 3, 4, 5, 8 level 0 to 4 as bit patterns
unary_projected [1, 2, 3, 4, 5, 8] ['00001', '00010', '00100', '01000', '10000']
unary_unprojected [1, 2, 3, 4, 5, 8] ['00001', '00010', '00100', '01000', '10000']
standard_binary [0, 1, 2, 2, 3, 3] ['00000', '00001', '00010', '00011', '00100']
gray [0, 1, 2, 2, 3, 3] ['00000', '00001', '00011', '00010', '00110']
Os padrões de bits são inteiros em que o bit é o valor do qubit . As codificações unárias gastam um qubit por nível e ativam um deles. As codificações compactas gastam qubits e armazenam o número do nível, em binário ou como um código Gray em que níveis vizinhos diferem em um bit. O nome em string de uma codificação é aceito em qualquer lugar em que a codificação é aceita.
O compromisso é entre qubits e peso do operador. As codificações compactas precisam de exponencialmente menos qubits, mas um operador de escada conecta níveis cujos padrões podem diferir em vários bits, então seus termos atuam em mais qubits. As seções a seguir examinam quanto isso custa, começando pelas duas codificações unárias, que têm layouts idênticos e diferem apenas no operador.
Compare as duas codificações unárias
Na codificação unária, um operador de escada move uma excitação entre dois qubits vizinhos. Para escrever isso como um operador sobre o modo inteiro, você precisa escolher o que fazer com os outros qubits. As duas variantes escolhem de forma diferente:
-
unary_projectedcoloca um projetor em todos os outros qubits do modo. Isso impõe a restrição one-hot no próprio operador: o termo só atua se o resto do modo estiver vazio. -
unary_unprojecteddeixa esses qubits em paz, então o termo atua como a identidade sobre eles.
Observe um único com sob ambas. O mapeador retorna um SparseObservable, cujos termos são strings de bit terms de um único qubit. Aqui estão os quatro primeiros termos de cada um, com os qubits em que atuam:
>>> for encoding in ("unary_projected", "unary_unprojected"):
... observable = occupation_encoding(b_dagger, LevelSpec(3, encoding))
... terms = observable.to_sparse_list()
... print(encoding, "->", len(terms), "terms on", observable.num_qubits, "qubits")
... for label, qubits, coefficient in terms[:4]:
... print(f" {label:5s} on qubits {qubits} coefficient {complex(coefficient):.4f}")
unary_projected -> 12 terms on 4 qubits
XX00 on qubits [0, 1, 2, 3] coefficient 0.2500+0.0000j
XY00 on qubits [0, 1, 2, 3] coefficient 0.0000-0.2500j
YX00 on qubits [0, 1, 2, 3] coefficient 0.0000+0.2500j
YY00 on qubits [0, 1, 2, 3] coefficient 0.2500+0.0000j
unary_unprojected -> 12 terms on 4 qubits
XX on qubits [0, 1] coefficient 0.2500+0.0000j
XY on qubits [0, 1] coefficient 0.0000-0.2500j
YX on qubits [0, 1] coefficient 0.0000+0.2500j
YY on qubits [0, 1] coefficient 0.2500+0.0000j
Ambos têm doze termos com os mesmos coeficientes. Os termos projetados carregam um bit term 0 nos dois qubits que os termos não projetados não mencionam, então cada termo projetado toca todos os quatro qubits e cada termo não projetado toca dois. Em geral, um termo projetado toca todos os qubits do modo, porque carrega dois fatores, cada um ou , e um projetor em cada um dos outros qubits.
Os dois operadores são iguais nos estados para os quais a codificação foi pensada, os estados da base one-hot (as palavras-código), e diferem fora deles. Para verificar, tome um operador pequeno em dois modos com e compare as matrizes nas nove palavras-código:
>>> hamiltonian = BosonOperator.from_dict({
... (cre(0), ann(1)): 1.0, (cre(1), ann(0)): 1.0,
... (cre(0), ann(0)): 0.5, (cre(1), cre(1), ann(1), ann(1)): 0.25,
... })
>>> matrices = {}
>>> for encoding in ("unary_projected", "unary_unprojected"):
... observable = occupation_encoding(hamiltonian, LevelSpec(2, encoding))
... matrices[encoding] = SparsePauliOp.from_sparse_observable(observable).to_matrix()
>>> unary = LevelEncoding.UNARY_PROJECTED
>>> codewords = sorted(
... unary.level_to_bits(first) | (unary.level_to_bits(second) << 3)
... for first in range(3) for second in range(3)
... )
>>> projected, unprojected = matrices["unary_projected"], matrices["unary_unprojected"]
>>> bool(np.allclose(projected[np.ix_(codewords, codewords)], unprojected[np.ix_(codewords, codewords)]))
True
>>> bool(np.allclose(projected, unprojected))
False
Ambos também deixam o subespaço das palavras-código invariante, então um estado preparado em uma palavra-código permanece em palavras-código. A escolha só importa para estados que saem desse subespaço, por exemplo por causa de ruído. Nesse caso, o operador projetado suprime a evolução, porque seus projetores falham, enquanto o não projetado continua atuando. Nenhum dos comportamentos é um requisito físico. O operador não projetado só é definido no subespaço one-hot, então escolhê-lo é uma decisão de modelagem e não uma otimização gratuita.
O que os projetores custam são portas. Evolua um hermitiano em um modo por um tempo curto e conte as portas de dois qubits após a transpilação do Qiskit:
>>> from qiskit import QuantumCircuit, transpile
>>> from qiskit.circuit.library import PauliEvolutionGate
>>>
>>> def cx_count(observable):
... circuit = QuantumCircuit(observable.num_qubits)
... circuit.append(PauliEvolutionGate(observable, time=0.1), range(observable.num_qubits))
... basis = ["cx", "rz", "sx", "x"]
... return transpile(circuit, basis_gates=basis, optimization_level=1).count_ops().get("cx", 0)
>>>
>>> hermitian = b + b_dagger
>>> levels = (3, 4, 5, 6)
>>> counts = {
... str(encoding): [
... cx_count(occupation_encoding(hermitian, LevelSpec(d - 1, encoding))) for d in levels
... ]
... for encoding in encodings
... }
As contagens exatas dependem de como o Qiskit sintetiza a porta de evolução, o que muda entre versões, então plote-as:
A contagem não projetada cresce linearmente com , porque cada uma das transições é uma rotação de dois qubits. A contagem projetada cresce muito mais rápido, porque todo termo atua em todos os qubits. As codificações compactas precisam de menos qubits, mas o peso de seus termos depende de como os padrões de bits de níveis vizinhos diferem: o código Gray, em que eles diferem em um bit, é a mais barata das duas codificações compactas, e o binário padrão salta quando um vai-um inverte vários bits de uma vez (). Se uma codificação compacta também supera a unária não projetada depende de e de como o Qiskit sintetiza a evolução, então leia a tendência e não a classificação em um único tamanho.
Os projetores também importam quando você expande um operador mapeado em Paulis, o que from_sparse_observable() faz substituindo cada projetor por uma soma de dois operadores de Pauli. Um termo com projetores se torna termos de Pauli. Aqui está um único com pequeno, que é o único tamanho em que você deve tentar isso:
>>> rows = [f"{'d':>2s} {'stored (both)':>14s} {'expanded, projected':>20s} {'expanded, unprojected':>22s}"]
>>> for levels in (3, 4, 5):
... stored = len(occupation_encoding(b_dagger, LevelSpec(levels - 1, "unary_projected")))
... expanded = [
... len(SparsePauliOp.from_sparse_observable(
... occupation_encoding(b_dagger, LevelSpec(levels - 1, encoding))).simplify())
... for encoding in ("unary_projected", "unary_unprojected")
... ]
... rows.append(f"{levels:2d} {stored:14d} {expanded[0]:20d} {expanded[1]:22d}")
>>> print("\n".join(rows))
d stored (both) expanded, projected expanded, unprojected
3 8 16 8
4 12 48 12
5 16 128 16
As duas variantes armazenam o mesmo número de termos, mas a expansão da projetada cresce exponencialmente em . Para qualquer coisa maior que um exemplo simples, leia diretamente os termos do SparseObservable, como faz a listagem acima, em vez de convertê-lo.
As codificações compactas não podem descartar seus projetores. Nelas, os bits zero fazem parte do número do nível, então remover um muda quais níveis um termo conecta, e o operador deixa de ser o mesmo nas palavras-código. É por isso que só a codificação unária vem em duas variantes.
Um produto de operadores de escada é truncado primeiro
Mais uma escolha de design aparece no custo dos termos diagonais. O mapeador não mapeia cada operador de escada e multiplica os resultados. Ele reúne os operadores de escada que um termo aplica a um modo, multiplica-os como uma matriz na base de Fock, trunca esse produto uma única vez e codifica o resultado [2]. Um produto como o operador número permanece, portanto, diagonal:
>>> number_observable = occupation_encoding(number, LevelSpec(3, "unary_unprojected"))
>>> [(label, qubits) for label, qubits, _ in number_observable.to_sparse_list()]
[('1', [1]), ('1', [2]), ('1', [3])]
>>> cx_count(number_observable)
0
Seus termos são os projetores no qubit de cada nível acima de zero, ponderados pelo número do nível (o nível zero tem peso zero e, portanto, não tem termo). Uma evolução temporal dele é um conjunto de rotações de um qubit e não precisa de nenhuma porta de dois qubits. Multiplicar os dois operadores de escada mapeados, em vez disso, dá o mesmo operador nas palavras-código, mas um diferente fora delas, com strings fora da diagonal que custam portas:
>>> unprojected = LevelSpec(3, "unary_unprojected")
>>> as_pauli = lambda operator: SparsePauliOp.from_sparse_observable(
... occupation_encoding(operator, unprojected, 4)).simplify()
>>> composed = as_pauli(b_dagger).dot(as_pauli(b)).simplify()
>>> direct = as_pauli(number)
>>> len(direct), len(composed)
(4, 16)
>>> cx_count(direct)
0
>>> bool(cx_count(composed) > 0)
True
>>> codewords = [LevelEncoding.UNARY_UNPROJECTED.level_to_bits(level) for level in range(4)]
>>> bool(np.allclose(direct.to_matrix()[np.ix_(codewords, codewords)],
... composed.to_matrix()[np.ix_(codewords, codewords)]))
True
>>> bool(np.allclose(direct.to_matrix(), composed.to_matrix()))
False
Termos diagonais, como a interação local (on-site) do modelo de Bose-Hubbard, são esses produtos, e é aí que essa escolha importa.
Escreva um mapeador personalizado
Se você quer uma codificação que a biblioteca não oferece, construa-a a partir da imagem de um único operador de escada. map_boson_action_generators() recebe uma função que mapeia uma BosonAction, uma função que retorna a identidade, e constrói a partir delas o mapeador de operadores completos. Ela multiplica as imagens das ações de cada termo em ordem, multiplica pelo coeficiente e soma os termos. A saída pode ser de qualquer tipo que suporte multiplicação por escalar e composição.
Como exemplo, tome a codificação para que armazena um modo em um qubit. Um operador de criação é , que é na convenção do Qiskit, e um operador de aniquilação é seu adjunto:
>>> from qiskit_bosons.mappers import map_boson_action_generators
>>> from qiskit_bosons.operators import BosonAction
>>>
>>> num_qubits = 2
>>>
>>> def map_action(action: BosonAction) -> SparsePauliOp:
... is_creation, mode = action
... sign = -1j if is_creation else 1j
... return SparsePauliOp.from_sparse_list(
... [("X", [mode], 0.5), ("Y", [mode], 0.5 * sign)], num_qubits=num_qubits
... )
>>>
>>> def identity() -> SparsePauliOp:
... return SparsePauliOp.from_sparse_list([("", [], 1.0)], num_qubits)
>>>
>>> model = BosonOperator.from_dict({
... (cre(0), ann(1)): -1.0, (cre(1), ann(0)): -1.0, (ann(0), cre(0)): 0.7, (cre(1), ann(1)): 0.3,
... })
>>> custom = map_boson_action_generators(model, map_action, identity)
>>> library = SparsePauliOp.from_sparse_observable(occupation_encoding(model, LevelSpec(1, "gray")))
>>> bool(np.allclose(custom.to_matrix(), library.to_matrix()))
True
Esse mapeador escrito à mão multiplica os operadores de escada mapeados e concorda com o mapeador da biblioteca aqui porque cada modo tem dois níveis e um qubit comporta o modo inteiro. Com maior, como a seção anterior mostrou, compor as escadas mapeadas e truncar o produto primeiro não são o mesmo operador fora das palavras-código.
Para usar esse mapeador em um circuito, envolva-o em uma função com a assinatura (operator, num_qubits) -> SparseObservable e passe-a para MapperFnEvolutionSynthesis, que mapeia com ela o operador de cada porta Evolution. O pipeline do transpilador é descrito em Transpilar circuitos bosônicos.
>>> from qiskit.quantum_info import SparseObservable
>>> from qiskit_bosons.transpiler.passes import MapperFnEvolutionSynthesis
>>>
>>> def mapper_fn(operator, num_qubits):
... return SparseObservable.from_sparse_pauli_op(
... map_boson_action_generators(operator, map_action, identity)
... )
>>>
>>> synthesis = MapperFnEvolutionSynthesis(mapper_fn)
Dois detalhes merecem ser conhecidos. Primeiro, o operador de qubits precisa ter coeficientes reais, porque PauliEvolutionGate rejeita uma parte imaginária. Um operador hermitiano é mapeado para um real, mas só depois que os termos que se cancelam forem mesclados. Esse mapeador escrito à mão não os mescla, então um termo de salto e seu adjunto deixam coeficientes imaginários até que você simplifique o resultado. O mapeador da biblioteca mescla seu próprio resultado; um personalizado precisa fazer isso, por exemplo envolvendo-o em simplify():
>>> hopping = BosonOperator.from_dict({(cre(0), ann(1)): 1.0})
>>> hermitian_hopping = hopping + hopping.adjoint()
>>> raw = mapper_fn(hermitian_hopping, 2)
>>> bool(np.allclose(np.asarray(raw.coeffs).imag, 0))
False
>>> merged = raw.simplify()
>>> bool(np.allclose(np.asarray(merged.coeffs).imag, 0))
True
>>> sorted(label for label, _, _ in merged.to_sparse_list())
['XX', 'YY']
Segundo, uma fórmula de produto sintetiza os termos na ordem em que os recebe, e a ordem que um mapeador produz não tem garantia de ser estável. Os auxiliares simplify() e group_wise() envolvem uma função de mapeador para controlá-la: simplify mescla strings duplicadas e ordena os termos em uma ordem canônica, enquanto group_wise mapeia um grupo do operador por vez, de modo que os termos de um grupo permaneçam adjacentes. Não combine os dois, porque a ordenação descarta o agrupamento. O guia de agrupamento mostra como atribuir grupos, e o guia de síntese mostra como a ordem afeta o circuito.
Próximos passos
-
Siga Simular a dinâmica de Bose-Hubbard com a escolha de uma codificação para uma simulação completa que varre o corte e compara as codificações com a dinâmica exata.
-
Veja Transpilar circuitos bosônicos para saber como o mesmo
LevelSpecé aplicado a um circuito bosônico. -
Leia Princípios de design das representações de operadores para saber como um
BosonOperatorarmazena e ordena seus termos, incluindo a regra de contração por trás do exemplo de ordenação normal acima. -
Leia Rotacionar modos bosônicos para conhecer a porta que move bósons entre modos, que é onde o aviso sobre a ocupação alcançável mais importa.
-
Consulte a referência da API de
qiskit_bosons.mapperseqiskit_bosons.mappers.library.
[1]
R. Somma, G. Ortiz, E. Knill, and J. Gubernatis, Quantum Simulations of Physics Problems (2003), arXiv:quant-ph/0304063, Sec. 4, Eq. 22.
N. P. D. Sawaya, T. Menke, T. H. Kyaw, S. Johri, A. Aspuru-Guzik, and G. G. Guerreschi, Resource-efficient digital quantum simulation of d-level systems for photonic, vibrational, and spin-s Hamiltonians, npj Quantum Information 6, 49 (2020), arXiv:1909.12847.