Setembro de 2019

Volume 34 – Número 9

[Computação Quântica]

Mensagens quânticas com Q# e Blazor

Por Daniel Vaughan

Neste artigo, exploro as mensagens quânticas, que tiram proveito do notável fenômeno de emaranhamento quântico para transferir de forma instantânea metade de uma mensagem por distâncias potencialmente grandes, eliminando o risco de interceptação. Analisarei a implementação de um algoritmo quântico para codificação superdensa em Q#, a nova linguagem de programação quântica da Microsoft; como emaranhar qubits usando portões quânticos e como codificar mensagens ASCII como qubits. Em seguida, criarei uma interface do usuário habilitada para o Blazor e baseada na Web, que aproveita o algoritmo quântico e simula o envio de partículas quânticas para diferentes partes. Vou mostrar a você como consumir uma biblioteca Q# em um aplicativo para servidores do Blazor e como iniciar e coordenar várias janelas do navegador. Você também aprenderá a empregar o MVVM (Model-View-ViewModel) em um aplicativo Blazor.

A teoria quântica é bastante empregada neste artigo. Se você é novo na computação quântica, aconselho a ler meu "Quantum Computation Primer". Você encontrará a primeira parte em tinyurl.com/quantumprimer1.

Vamos começar dando uma olhada na codificação superdensa.

Entender a codificação superdensa

A codificação superdensa aproveita o fenômeno do emaranhamento quântico, em que uma partícula de um par emaranhado pode afetar o estado compartilhado de ambas, apesar de estarem separadas por distâncias potencialmente vastas.

Para entender o protocolo de codificação superdensa, digamos que você tem três atores: Alice, Bob e Charlie (A, B e C). Charlie cria um par de qubits emaranhados e envia um para Alice e um para Bob. Quando Alice quer enviar uma mensagem de 2 bits para Bob, tudo o que ela tem que fazer é manipular o próprio qubit – que influencia o estado quântico do par – e, em seguida, enviar o único qubit dela para Bob. Então, Bob mede o qubit de Alice e o dele para receber a mensagem de 2 bits.

O ponto importante é que não há nenhuma maneira de codificar mais de um bit de informações em um único qubit – mas apenas um qubit muda de mãos para entregar uma mensagem de 2 bits. Apesar de qualquer distância entre os qubits, o estado compartilhado dos qubits emaranhados permite que toda a mensagem de 2 bits seja codificada usando apenas um dos qubits.

Além disso, quando Alice envia uma mensagem a Bob, um longo tempo pode ter passado desde que Charlie enviou a cada um deles um qubit emaranhado. Você pode pensar em qubits emaranhados desta forma como um recurso, esperando para ser consumido como parte do processo de comunicação.

O pré-compartilhamento de qubits também traz um importante benefício de segurança: Quem quiser decodificar a mensagem precisa estar na posse dos qubits de Alice e Bob. Interceptar apenas o qubit de Alice não é suficiente para decodificar a mensagem e, considerando que os qubits emaranhados são pré-compartilhados, o risco de interceptação é eliminado.

Se você está cético sobre a física que fundamenta a codificação superdensa – e não há problemas nisso – o fenômeno foi validado por experimentos (tinyurl.com/75entanglement), até mesmo com satélites e lasers (tinyurl.com/spookyrecord).

Introdução ao Q#

O suporte à linguagem Q# para Visual Studio acompanha o Microsoft Quantum Development Kit. A maneira mais fácil de instalar o kit para o Visual Studio é usar o Gerenciador de Extensões do Visual Studio.

No menu Extensões do Visual Studio 2019, selecione Gerenciar Extensões para exibir a caixa de diálogo Gerenciar Extensões. Digite "quantum" na caixa de pesquisa para localizar o kit. Depois de instalado, você poderá criar novos tipos de projeto do Q#, o realce da sintaxe do Q# estará habilitado e a depuração de projetos do Q# funcionará como esperado.

O Q# é uma linguagem específica de domínio de procedimento, influenciada sintaticamente por C# e F#. Os arquivos do Q# contêm operações, funções e definições de tipo personalizado, cujos nomes devem ser exclusivos em um namespace, como mostra a Figura 1. Os membros de outros namespaces podem ser disponibilizados usando a diretiva "open".

Figura 1 Funções, operações e definições de tipo personalizadas

namespace Quantum.SuperdenseCoding
{
  open Microsoft.Quantum.Diagnostics;
  open Microsoft.Quantum.Intrinsic;
  open Microsoft.Quantum.Canon;
  function SayHellow(name : String) : String
  {
    return “Hello “ + name;
  }
  operation EntangledPair(qubit1 : Qubit, qubit2 : Qubit) : Unit
    is Ctl + Adj
  {
    H(qubit1);
    CNOT(qubit1, qubit2);
  }
  newtype IntTuple = (Int, Int);
}

As funções são análogas a funções ou métodos em outras linguagens de procedimento. Uma função aceita zero ou mais argumentos e pode retornar um único objeto, valor ou tupla. Cada parâmetro para uma função consiste em um nome de variável seguido por dois-pontos e pelo tipo desse parâmetro. O valor retornado para uma função é especificado após dois-pontos, no final da lista de parâmetros.

Lembre-se que, de acordo com a parte 3 do meu "Quantum Computation Primer" tinyurl.com/controlledgates, portas U controladas permitem que você adicione uma entrada de controle para uma porta quântica arbitrária. O Q# tem uma palavra-chave Controlled para essa finalidade, que permite aplicar um controle a um operador, por exemplo:

Controlled X([controlQubit1, controlQubit2], targetQubit)

Aqui, a instrução Controlled coloca duas entradas de controle na porta Pauli-X.

As operações compartilham as mesmas características que as funções, mas as operações também podem representar operadores unitários. Você pode considerá-las como portas quânticas compostas. Elas permitem que você defina o que acontece quando elas são usadas como parte de uma operação controlada.

Além disso, as operações permitem que você defina explicitamente o inverso da operação (por meio da palavra-chave adjoint). Adjoint é a transposição conjugada complexa do operador. Para obter mais informações, confira tinyurl.com/brafromket.

A palavra-chave newtype permite que você defina um tipo personalizado, que pode ser usado em operações e funções.

Agora, examinaremos alguns dos tipos de expressão mais comuns.

Em Q#, as variáveis são imutáveis por padrão. Para declarar uma nova variável, use a palavra-chave let, da seguinte maneira:

let foo = 3;

O compilador infere o tipo da variável do valor atribuído a ela por você. Se você precisar alterar o valor de uma variável, use a palavra-chave mutable ao declará-la:

mutable foo = 0;
set foo += 1;

Para criar uma matriz imutável em Q#, use o seguinte:

let immutableArray = [11, 21, 3, 0];

Uma matriz em Q# é um tipo de valor e é, em grande parte, imutável, mesmo quando criada usando a palavra-chave mutable. Quando a palavra-chave mutable é usada, você pode atribuir outra matriz ao identificador, mas não pode substituir itens na matriz:

mutable foo = new Int[4];
set foo = foo w/ 0 <- 1;

O operador w/ é uma abreviação da palavra "with". Na segunda linha, todos os valores em foo são copiados para uma nova matriz, com o primeiro item (índice 0) na matriz definida como 1. Ele tem uma complexidade de runtime igual a O(n). A instrução de atribuição anterior pode ser expressa de forma mais sucinta, combinando-a com um sinal de igual, da seguinte maneira:

set foo w/= 0 <- 1;

Quando o operador w/= é usado, uma substituição in-loco é executada quando possível, reduzindo a complexidade do runtime para O(1).

Você pode criar um loop usando uma instrução for:

for (i in 1..10)
{
  Message($”i={i},”);
}

A função de mensagem interna grava mensagens no console. O Q# é compatível com a interpolação de cadeias de caracteres. Ao prefixar um literal de cadeia de caracteres com um "$", você pode colocar expressões entre chaves.

Para executar um loop por uma coleção, use uma instrução for, da seguinte maneira:

for (qubit in qubits) // Qubits is an array of Qubits
{
  H(qubit); // Apply a Hadamard gate to the Qubit
}

Este foi um tour bem rápido pelo Q#. Para obter uma cobertura mais aprofundada, visite tinyurl.com/qsharp. Também recomendo trabalhar com o Microsoft Quantum Katas (tinyurl.com/quantumkatas).

Como implementar a codificação superdensa com Q#

Agora, examinaremos a implementação do protocolo quântico de codificação superdensa em Q#. Estou supondo que você leu a primeira e a segunda partes da minha série ou que já tem uma compreensão da notação Dirac e dos conceitos básicos de computação quântica.

Na descrição de abertura da codificação superdensa, lembre-se de que Charlie emaranha um par de qubits e envia um para Alice e outro para Bob.

Para emaranhar um par de qubits, que começam em um estado |0, você envia o primeiro qubit por meio de uma porta Hadamard, que altera o estado dele para:

A porta Hadamard coloca o primeiro qubit em uma superposição de |0 e |1, enviando-o para o estado |+. Eis o código:

operation CreateEntangledPair(qubit1 : Qubit, qubit2 : Qubit) : Unit
{
  H(qubit1);
  CNOT(qubit1, qubit2);
}

O portão CNOT inverterá o qubit2 se o estado do qubit1 for |1, resultando no estado |Ф+:

O estado dos qubits, quando medido, terá uma chance de 50% de ser 00 ou 11.

Com essa superposição criada, se você fosse medir apenas um dos qubits, isso faria com que o estado do outro diminuísse para o mesmo valor, independentemente da distância entre os qubits. Os qubits são considerados emaranhados.

Depois de Alice e Bob terem enviado um qubit de um par emaranhado, Alice poderá afetar o estado quântico geral dos dois qubits apenas manipulando o qubit dela. Um único qubit pode codificar apenas 1 bit de informação. Mas, quando emaranhado com outro qubit, esse qubit pode ser usado de modo autônomo para codificar 2 bits de informações no estado quântico compartilhado.

Alice é capaz de transmitir 2 bits de informações passando o qubit dela por meio de uma ou mais portas. As portas e o estado quântico resultante do par de qubits são mostrados na Figura 2. O subscrito nos qubits identifica o proprietário: A para Alice, B para Bob.

Portas usadas para produzir estados de Bell
Figura 2 Portas usadas para produzir estados de Bell

O envio de uma mensagem de 2 bits de 00 não requer nenhuma alteração no qubit de Alice ("I" é a porta de identidade, que não faz nada). O envio de 01 ou 10 requer uma única porta: X ou Z, respectivamente, enquanto o envio de 11 requer a aplicação de uma porta X e de outra Z.

Na mecânica quântica, os únicos estados perfeitamente distinguíveis são aqueles que são ortogonais. Para qubits, esses são estados que são perpendiculares na esfera de Bloch. Assim, acontece que os estados de Bell são ortogonais, então, quando chega a hora de Bob medir os qubits, ele pode realizar uma medida ortogonal nos dois qubits para derivar a mensagem de 2 bits original de Alice.

Para codificar o qubit de Alice, ele é enviado para a operação de Q# EncodeMessageInQubit, como mostra a Figura 3. Junto com o qubit de Alice, o EncodeMessageInQubit aceita dois valores boolianos que representam os 2 bits de informações a serem enviados para Bob. A primeira instrução let concatena o par de valores de bit em um inteiro entre 002 e 112, com o bit1 ocupando o bit mais significativo. A sequência de bits resultante indica a porta ou portas corretas a serem aplicadas.

Figura 3 Operação EncodeMessageInQubit

operation EncodeMessageInQubit(
  aliceQubit : Qubit, bit1 : Bool, bit2 : Bool) : Unit
{
  let bits = (bit1 ? 1 | 0) * 2 + (bit2 ? 1 | 0);
  if (bits == 0b00)
  {
    I(aliceQubit);
  }
  elif (bits == 0b01)
  {
    X(aliceQubit);
  }
  elif (bits == 0b10)
  {
    Z(aliceQubit);
  }
  elif (bits == 0b11)
  {
    X(aliceQubit);
    Z(aliceQubit);
  }
}

O Q# é compatível com muitos dos recursos de linguagem presentes no C# e no F#. Convenientemente, isso inclui literais binários.

Depois que Alice codifica sua mensagem e envia o qubit dela para Bob, ele recebe a mensagem e aplica uma porta CNOT aos qubits e a porta Hadamard ao qubit de Alice:

operation DecodeMessageFromQubits (bobQubit : Qubit, aliceQubit : Qubit) : (Bool, Bool)
{
  CNOT(aliceQubit, bobQubit);
  H(aliceQubit);
  return (M(aliceQubit) == One, M(bobQubit) == One);
}

Em seguida, cada qubit é medido. O resultado é uma tupla que consiste nos dois bits de mensagem originais enviados por Alice. Os valores medidos de cada qubit correspondem à mensagem de bit clássica codificada por Alice.

Vamos examinar mais detalhadamente como esse protocolo de codificação superdensa funciona. Lembre-se que Alice e Bob recebem, cada um, um determinado qubit que pertence a um par emaranhado. A representação de matriz do estado quântico dos dois qubits é fornecida por:

Usarei esse resultado de matriz em alguns instantes para demonstrar o processo de codificação.

Se Alice quiser enviar a mensagem 012 para Bob, ela aplicará a porta X ao seu próprio qubit, o que alterará o estado quântico para:

Converter isso no respectivo formato de matriz produz:

Em seguida, Alice envia o qubit dela para Bob.

Para decodificar a mensagem, Bob começa aplicando uma porta CNOT aos qubits:

Agora você pode pegar o resultado da matriz da etapa anterior e multiplicá-lo pela matriz CNOT, lembrando que é possível comutar o multiplicador escalar:

Ao considerar a matriz resultante, você obtém:

Em seguida, Bob aplica uma porta Hadamard ao qubit de Alice:

Você pode substituir a representação de matriz dos qubits e da porta Hadamard:

Dividindo a parte superior e a inferior por √2, é possível eliminar o divisor comum, que deixa alguma aritmética de matriz a fazer:

Como você pode ver, o resultado final é a mensagem original enviada por Alice!

Quando Alice passou o qubit dela pela porta X, isso afetou a sobreposição geral dos dois qubits. Bob foi capaz de determinar a alteração feita por Alice ao desenrolar o estado sobreposto.

Como alocar qubits

Quando um objeto qubit é alocado em Q#, ele precisa ser retornado ao simulador. Isso é imposto sintaticamente. Uma instrução using é necessária para alocar qubits.

Para alocar um único qubit, use a expressão qubit, conforme mostrado aqui:

using (qubit = Qubit())
{
  Reset(qubit);
}

A função Reset retorna o qubit para o estado |0.

Ao sair do bloco using, um qubit deve estar no estado |0. Esse é um requisito de runtime, imposto pelo simulador quântico. É possível passar um qubit para outras operações, alterar o estado dele e assim por diante, mas, se a execução atingir o final do bloco using sem que o qubit seja retornado para o estado |0, uma exceção será gerada. É possível desabilitar exceções para qubits sem redefinição por meio do construtor QuantumSimulator.

Também é possível criar vários objetos qubit usando a expressão de matriz Qubit[]:

using (qubits = Qubit[2])
{
  // Do something with the qubits...
  ResetAll(qubits);
}

Os qubits criados com a expressão de matriz ainda precisarão ser redefinidos quando você terminar. Use a função interna ResetAll para redefinir vários qubits.

Lembre-se de que qubits simulados usam uma quantidade exponencial de memória. Cada qubit alocado duplica a quantidade de RAM necessária. Em meus testes em um computador com 16 GB de RAM, o uso chegou a cerca de 13 GB para 26 qubits. Excedendo isso, a memória virtual é acionada. Consegui atingir 31 qubits. No entanto, o uso de memória foi aumentado para 53 GB. Como esperado, 32 qubits produziram uma exceção de interoperabilidade.

Como usar o Q# para fazer o que você quiser

O projeto de exemplo passa qubits de modo assíncrono entre objetos que representam Alice, Bob e Charlie. Por esse motivo, eu precisava encontrar uma maneira de manter objetos qubit e evitar o requisito do Q# de que um bloco using seja usado.

Há uma forte afinidade entre as ferramentas do Q# e do C# no Visual Studio. Quando você cria um projeto em Q#, os arquivos .qs nesse projeto são convertidos em C#. As ferramentas de Q# geram como saída arquivos do C# com uma extensão de arquivo ".g.cs", colocando-os no diretório \obj\qsharp\src do projeto pai.

Acredito que essa etapa de conversão possa ser substituída pela compilação direta para IL ou alguma coisa diferente no futuro, dado o investimento que a Microsoft está fazendo no compilador Q#. No entanto, por enquanto, descobri que é útil explorar os arquivos .cs gerados, observando como as coisas funcionam nos bastidores. Logo percebi que é possível aproveitar os assemblies do SDK do C# no Q# para obter o que eu precisava.

A classe QOperations, no código de exemplo que pode ser baixado, chama diretamente as APIs Q#, o que permite que ele solicite um qubit do simulador sem precisar liberá-lo imediatamente. Em vez de restringir o Q# para todas as atividades quânticas, uso o C# para alocar, emaranhar e desalocar qubits.

A classe QOperations cria uma instância da classe QuantumSimulator, que está localizada no namespace Microsoft.Quantum.Simulation.Simulators. O QuantumSimulator fornece uma API para manipular o componente simulador nativo. O simulador é instalado com o Microsoft Quantum Development Kit.

O QuantumSimulator tem um contêiner de IoC interno que permite que você recupere vários objetos que são comumente usados pelo código C# gerado por ele. Por exemplo, em um arquivo C#, você pode usar o método Get<T> do objeto QuantumSimulator para recuperar um objeto para redefinir o estado de um qubit e liberar um qubit, executando a mesma ação que um bloco using em Q#:

resetOperation = simulator.Get<ICallable<Qubit, QVoid>>(typeof(Reset));
releaseOperation = simulator.Get<Release>(typeof(Release));

Para alocar um novo qubit, use o objeto Microsoft.Quantum.Intrinsic.Allocate. Você também precisa dos objetos de porta CNOT e H (Hadamard), que são recuperados do contêiner, desta forma:

allocator = simulator.Get<Allocate>(typeof(Allocate));
cnotGate = simulator.Get<IUnitary<(Qubit, Qubit)>>(typeof(CNOT));
hGate = simulator.Get<IUnitary<Qubit>>(typeof(H));

Com esses objetos, você pode gerar pares de qubits emaranhados. O método GetEntangledPairsAsync da classe QOperations cria uma lista de tuplas de qubit (confira a Figura 4).

Figura 4 Como criar qubits emaranhados em C#

public Task<IList<(Qubit, Qubit)>> GetEntangledPairsAsync(int count)
{
  IList<(Qubit, Qubit)> result = new List<(Qubit, Qubit)>(count);
  for (int i = 0; i < count; i++)
  {
    var qubits = allocator.Apply(2);
    hGate.Apply(qubits[0]);
    cnotGate.Apply((qubits[0], qubits[1]));
    result.Add((qubits[0], qubits[1]));
  }
  return Task.FromResult(result);
}

Uso o método Apply do objeto Allocate para recuperar dois qubits livres do simulador. Eu apliquei o portão Hadamard ao primeiro qubit e, em seguida, o CNOT a ambos, colocando-os em um estado emaranhado. Não é necessário redefini-los e liberá-los imediatamente. Posso retornar os pares de qubit do método. Os qubits ainda precisam ser liberados – caso contrário, o aplicativo ficaria rapidamente com memória insuficiente. Apenas adiei essa etapa.

Os qubits são liberados com o método ReleaseQubitsAsync, como mostrado na Figura 5.

Figura 5 Como liberar qubits em C#

public Task ReleaseQubitsAsync(IEnumerable<Qubit> qubits)
{
  foreach (Qubit qubit in qubits)
  {
    resetOperation.Apply(qubit);
    releaseOperation.Apply(qubit);
    /* Alternatively, we could do: */
    // simulator.QubitManager.Release(qubit);
  }
  return Task.CompletedTask;
}

Para cada qubit fornecido, redefini o qubit usando o método Apply do objeto Reset. Em seguida, informo ao simulador que o qubit é gratuito, usando o método Apply do objeto Release. Como alternativa, você pode usar a propriedade QubitManager do simulador para liberar o qubit.

Isso conclui o elemento quântico deste artigo. Agora, vamos dar uma olhada no Blazor e na implementação de um projeto do Blazor no lado do servidor com o MVVM.

Como usar o padrão MVVM com o Blazor

Sou fã do XAML há muito tempo. Gosto da maneira como posso escrever a lógica do modelo de exibição em C# e combiná-la com um arquivo de layout reutilizável e fácil de designer combinado com associações de dados. O Blazor permite a mesma abordagem, mas em vez de XAML, ele usa o Razor, que tem uma sintaxe concisa para marcar o conteúdo dinâmico no HTML.

Se você for novo no Blazor, confira as instruções de instalação em tinyurl.com/installblazor para o IDE de sua escolha.

Primeiro, implementei este projeto usando a UWP (Plataforma Universal do Windows). A UWP é compatível com o .NET Core e faz sentido criar rapidamente a interface do usuário em uma tecnologia baseada em XAML. Na verdade, a versão UWP do aplicativo está localizada no código de exemplo que pode ser baixado. Se você não quiser instalar o Blazor e quiser apenas ver o aplicativo em ação, fique à vontade para apenas executar a versão UWP. Ambos os aplicativos são idênticos em comportamento.

Portar o aplicativo para o Blazor foi fácil demais. Nenhuma alteração de código é necessária. As referências do NuGet à estrutura MVVM do Codon (codonfx.com) estavam corretas e tudo funcionou bem.

Escolhi o modelo do lado do servidor do Blazor para este projeto para que eu pudesse compartilhar um único objeto QuantumSimulator entre os três modelos de exibição que representam Alice, Bob e Charlie, bem como para passar objetos qubit. 

Análise detalhada do aplicativo Blazor O aplicativo Blazor contém três Razor Pages, mostradas na Figura 6, que representam os três atores: Alice.razor, Bob.razor e Charlie.razor.

Razor Pages de Alice, Bob e Charlie
Figura 6 Razor Pages de Alice, Bob e Charlie

Cada página tem um modelo de exibição associado. Todos os três modelos de exibição no projeto são subclasses da classe ViewModelBase do Codon, que fornece suporte interno para eventos INPC (INotifyPropertyChanged), uma referência de contêiner IoC e mensagens acopladas flexivelmente entre os componentes no aplicativo.

Cada Razor Page tem um bloco de funções, que inclui uma propriedade para o respectivo modelo de exibição. Aqui está o bloco de funções em Alice.razor:

@functions {
  AliceViewModel ViewModel { get; set; }
  protected override async Task OnInitAsync()
  {
    ViewModel = Dependency.Resolve<AliceViewModel>();
    ViewModel.PropertyChanged += (o, e) => Invoke(StateHasChanged);
  }
}

O modelo de exibição é recuperado do contêiner IoC do Codon durante o método OnInitAsync.

As alterações a propriedades do modelo de exibição não fazem com que o conteúdo da página seja atualizado automaticamente. Para isso, você assina o evento PropertyChanged das classes ViewModelBase. Quando o evento ocorre, o método StateHasChanged da Razor Page é chamado, o que dispara uma atualização dos elementos HTML cujos dados são associados ao modelo de exibição.

Você usa o método Invoke da página para garantir que as atualizações ocorram no thread da interface do usuário. Se o método StateHasChanged for chamado de um thread que não seja da interface do usuário, uma exceção será gerada.

Como solicitar qubits de Charlie A Razor Page de Charlie indica o número de qubits enviados para Alice e Bob. Como mostra a Figura 7, há um pouco mais acontecendo no bloco de funções dessa página.

Figura 7 Trecho do bloco de funções de Charlie.razor

@functions {
  bool scriptInvoked;
  ...
  protected override void OnAfterRender()
  {
    if (!scriptInvoked)
    {
      scriptInvoked = true;
      jsRuntime.InvokeAsync<object>(“eval”, evalScript);
    }
  }
  const string evalScript = @”var w1 = window.open(‘/alice’, ‘_blank’,
    ‘left=100,top=50,width=500,height=350,toolbar=0,resizable=0’);
var w2 = window.open(‘/bob’, ‘_blank’,
  ‘left=100,top=500,width=500,height=350,toolbar=0,resizable=0’);
window.addEventListener(‘beforeunload’, function (e) {
  try {
    w1.close();
  } catch (err) {}
  try {
    w2.close();
  } catch (err) {}
  (e || window.event).returnValue = null;
  return null;
});”;
}

Além da inicialização do modelo de exibição, a página também é encarregada da abertura de uma nova janela do navegador para Alice e Bob. Ela faz isso chamando JSRuntime.InvokeAsync e usando a função eval do JavaScript para executar o JavaScript no navegador. O script eval é aberto e retém as janelas como variáveis. Quando o evento beforeunload da página Charlie ocorre, as janelas são fechadas. Isso impede o acúmulo de janelas de navegador órfãs quando o depurador é interrompido.

Lembre-se de que a maioria dos navegadores hoje bloqueia pop-ups por padrão ou, pelo menos, solicita consentimento antes de abrir um. Se isso acontecer, você precisará aceitar os pop-ups e atualizar a página. Meu uso de pop-ups neste projeto é simplesmente um modo prático de abrir todas as três janelas de atores ao mesmo tempo.

A página Charlie.razor contém um único elemento dinâmico – um contador que exibe o número de qubits que foram expedidos:

<p>Qubit sent count: @Model.QubitSentCount</p>

O valor dentro do parágrafo é associado à propriedade QubitSentCount do modelo de exibição.

Quando classes derivam de Codon.UIModel.ViewModelBase, qualquer implementação de IMessageSubscriber<T> assina automaticamente as mensagens do tipo T. CharlieViewModel implementa IMessageSubscriber<RequestQubitsMessage> e IMessageSubscriber<ReleaseQubitsMessage>; portanto, quando um RequestQubitsMessage é publicado pelo AliceViewModel ou um ReleaseQubitsMessage é publicado por BobViewModel, ele é manipulado na classe Charlie­ViewModel. Isso é tudo de que Charlie é encarregado. Ele envia qubits emaranhados para Alice e Bob quando necessário.

Quando CharlieViewModel recebe um RequestQubitsMessage, ele usa a instância QOperations para recuperar uma lista de pares de qubits emaranhados, como mostra a Figura 8.

Figura 8 Método CharlieViewModel.ReceiveMessageAsync(RequestQubitsMessage)

async Task IMessageSubscriber<RequestQubitsMessage>.ReceiveMessageAsync(
  RequestQubitsMessage message)
{
  IList<(Qubit, Qubit)> qubits =
    await QOperations.GetEntangledPairsAsync(message.QubitCount);
  await Messenger.PublishAsync(new BobQubitMessage(qubits.Select(x => x.Item2)));
  await Messenger.PublishAsync(new AliceQubitMessage(qubits.Select(x => x.Item1)));
  QubitSentCount += message.QubitCount;
}

Em seguida, o CharlieViewModel envia o primeiro item de cada par para Alice e o segundo para Bob, por meio do IMessenger.

Por fim, o QubitSentCount é aumentado, o que atualiza a página de Charlie.

Como enviar mensagens como Alice Vamos dar uma olhada na página de Alice e no modelo de exibição associado. A classe AliceViewModel contém um AsyncActionCommand, que é definido desta forma:

ICommand sendCommand;
public ICommand SendCommand => sendCommand ??
         (sendCommand = new AsyncActionCommand(SendAsync));

Ainda estou satisfeito em ver as propriedades escritas de modo tão conciso, em que uma expressão lambda combinada com um operador de união nulo resulta em carregamento lento em apenas algumas linhas de código.

A infraestrutura de comando do Codon é compatível com manipuladores de comandos assíncronos. Usando o AsyncActionCommand do Codon, você pode especificar o método assíncrono SendAsync como o manipulador de comandos.

Alice.razor contém um campo de texto e um botão:

<input type=”text” bind=”@ViewModel.Message” />
<button class=”btn btn-primary”
      onclick=”@ViewModel.SendCommand.Execute”>Send to Bob</button>

A caixa de entrada está associada à propriedade Message do modelo de exibição. Quando o botão enviar para Bob é clicado, o SendCommand do modelo de exibição é executado e o respectivo método SendAsync é chamado (confira a Figura 9).

Figura 9 Método AliceViewModel.SendAsync

async Task SendAsync(object arg)
{
  var bytes = Encoding.ASCII.GetBytes(Message);
  foreach (byte b in bytes)
  {
    byteQueue.Enqueue(b);
    await Messenger.PublishAsync(
      new RequestQubitsMessage(qubitsRequiredForByte));
  }
  Message = string.Empty;
}

O modelo de exibição tem uma fila que contém os bytes de cada mensagem que é codificada e enviada para Bob. O IMessenger do Codon é usado para enviar uma mensagem que, por fim, chega a Charlie, solicitando que quatro qubits sejam emaranhados e enviados, dois para Alice e dois para Bob.

Por fim, a propriedade Message é redefinida como string.empty, o que limpa a caixa de entrada e a deixa pronta para uma nova mensagem.

AliceViewModel implementa IMessageSubscriber<AliceQubitMessage> e, assim, recebe todas as mensagens desse tipo:

gasync Task IMessageSubscriber<AliceQubitMessage>.ReceiveMessageAsync(
  AliceQubitMessage message)
{
  foreach (var qubit in message.Qubits)
  {
    qubitQueue.Enqueue(qubit);
  }
  await DispatchItemsInQueue();
}

Quando ReceiveMessageAsync é chamado, o conteúdo conta com uma coleção de instâncias de qubit de Charlie. O AliceViewModel também mantém uma fila de objetos Qubit, aos quais os qubits recém-recebidos são adicionados.

O AliceViewModel agora está pronto para enviar pelo menos parte de uma mensagem para Bob. DispatchItemsInQueue é chamado, conforme mostrado na Figura 10.

Figura 10 Método AliceViewModel.DispatchItemsInQueue

async Task DispatchItemsInQueue()
{
  var qOps = Dependency.Resolve<QOperations, QOperations>(true);
  while (byteQueue.Any())
  {
    if (qubitQueue.Count < qubitsRequiredForByte)
    {
      return;
    }
    IList<Qubit> qubits =
      qubitQueue.DequeueMany(qubitsRequiredForByte).ToList();
    byte b = byteQueue.Dequeue();
    BitArray bitArray = new BitArray(new[] { b });
    /* Convert classical bit pairs to single qubits. */
    for (int i = 0, j = 0; i < bitArray.Length; i += 2, j++)
    {
      await qOps.EncodeMessageInQubitAsync(
        qubits[j],
        bitArray[i],
        bitArray[i + 1]);
    }
    await Messenger.PublishAsync(new DecodeQubitsMessage(qubits));
  }
}

DispatchItemsInQueue primeiro recupera a instância QOperations do contêiner de IoC. Dependency.Resolve<T,T>(singleton booliano) fará com que uma instância seja criada se nenhuma ainda tiver sido. Essa instância é, então, retida como um singleton para que as solicitações futuras resolvam o mesmo objeto.

Um método de extensão é usado para remover quatro qubits da fila de qubits. O byte de mensagem também é removido da fila e colocado em um BitArray. Em seguida, o objeto QOperations é encarregado de posicionar cada qubit em uma sobreposição que representa a mensagem de 2 bits. Observe novamente como um qubit é capaz de representar dois bits clássicos porque seu estado afeta o estado quântico do par.

O IMessenger é, então, usado para distribuir um DecodeQubits­Message que é, por fim, recebido por Bob.

Como receber mensagens como Bob Assim como Alice, Bob tem uma fila de qubits que chegam de Charlie. BobViewModel implementa IMessageSubscriber<BobQubitMessage>. Quando Charlie envia qubits encapsulados em um BobQubitMessage, eles são colocados na fila.

BobViewModel também implementa IMessageSubscriber<Decode­QubitsMessage>. Quando Alice envia uma mensagem para Bob, ela vem encapsulada em um objeto DecodeQubitsMessage e é manipulada no método ReceiveMessageAsync(DecodeQubitsMessage) (confira a Figura 11). O método remove um número igual de qubits da respectiva fila. O método QOperations DecodeQubits é usado para converter cada par de qubits de Alice e de Bob em 2 bits de dados. Cada byte na mensagem consiste em quatro pares de 2 bits (8 bits), portanto, o loop é incrementado para i e j.

Figura 11 Método ReceiveMessageAsync(DecodeQubitsMessage)

async Task IMessageSubscriber<DecodeQubitsMessage>.ReceiveMessageAsync(
  DecodeQubitsMessage message)
{
  IList<Qubit> aliceQubits = message.Qubits;
  List<Qubit> bobQubits = qubits.DequeueMany(aliceQubits.Count).ToList();
  var qOps = Dependency.Resolve<QOperations, QOperations>(true);
  var bytes = new List<byte>();
  for (int i = 0; i < bobQubits.Count; i += 4)
  {
    byte b = 0;
    for (int j = 0; j < 4; j++)
    {
      (bool aliceBit, bool bobBit) = await qOps.DecodeQubits(
        bobQubits[i + j], aliceQubits[i + j]);
      if (bobBit)
      {
        b |= (byte)(1 << (j * 2 + 1));
      }
      if (aliceBit)
      {
        b |= (byte)(1 << (j * 2));
      }
    }
    bytes.Add(b);
  }
  Message += Encoding.ASCII.GetString(bytes.ToArray());
  await Messenger.PublishAsync(new ReleaseQubitsMessage(aliceQubits));
  await Messenger.PublishAsync(new ReleaseQubitsMessage(bobQubits));
}

Cada byte é construído por bits com deslocamento à esquerda de acordo com os valores dos bits decodificados. Os bytes decodificados são revertidos para uma cadeia de caracteres usando o método Encoding.ASCII.GetString e anexados à propriedade Message exibida na página.

Depois que os qubits são decodificados, eles são liberados por meio da publicação de um ReleaseQubitsMessage, que é recebido pelo CharlieViewModel. Em seguida, QOperations é usado para liberar os qubits:

async Task IMessageSubscriber<ReleaseQubitsMessage>.ReceiveMessageAsync(
  ReleaseQubitsMessage message)
{
  await QOperations.ReleaseQubitsAsync(message.Qubits);
}

Conclusão

Neste artigo, implementei um algoritmo quântico para codificação superdensa em Q#, a nova linguagem de programação quântica da Microsoft. Expliquei como emaranhar qubits usando portas quânticas e como codificar mensagens ASCII como qubits. Vimos uma interface do usuário habilitada para o Blazor e baseada na Web, que aproveitou o algoritmo quântico e simulou o envio de partículas quânticas para diferentes partes. Também mostrei como consumir uma biblioteca Q# em um aplicativo para servidores do Blazor e como iniciar e coordenar várias janelas do navegador. Por fim, expliquei como empregar o MVVM em um aplicativo Blazor.

Referências

  • "Statements and Other Constructs", recuperado em 5 de julho de 2019, de bit.ly/2Ofx1Ld
  • Yanofsky, N., & Mannucci, M. (2008) “Quantum Computing for Computer Scientists”, Cambridge University Press
  • Nielsen, M. & Chuang, I., (2010) "Quantum Computation and Quantum Information", 10ª edição, Cambridge University Press, Cambridge, RU
  • Watrous, J., (2006) "Lecture 3: Superdense coding, quantum circuits, and partial measurements", recuperado em 5 de julho de 2019, de bit.ly/2XTPEDN

Daniel Vaughané um autor e engenheiro de software trabalhando para a Microsoft em Redmond, Wash. Ele foi nove vezes Microsoft MVP e é o desenvolvedor por trás de vários aplicativos móveis de consumidor e empresariais aclamados, como Surfy Browser para Android e Windows Phone e Airlock Browser para Android. Ele também é o criador de vários projetos de software livre populares, incluindo as estruturas Codon e Calcium. Vaughan bloga em danielvaughan.org e tweeta como @dbvaughan.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: Bettina Heim (equipe Quantum da Microsoft)
Bettina Heim é membro da equipe Quantum da Microsoft.


Discuta esse artigo no fórum do MSDN Magazine