Septiembre de 2019

Volumen 34, número 9

[Computación cuántica]

Mensajería cuántica con Q# y Blazor

Por Daniel Vaughan

En este artículo hablaré de la mensajería cuántica, que aprovecha el extraordinario fenómeno del entrelazamiento cuántico para transferir al instante medio mensaje entre distancias que pueden ser enormes y elimina así el riesgo de que lo intercepten. Comentaré la implementación de un algoritmo cuántico para la codificación superdensa en Q#, el nuevo lenguaje de programación cuántica de Microsoft, cómo entrelazar cúbits usando puertas cuánticas y cómo codificar mensajes ASCII en forma de cúbits. A continuación, crearé una interfaz de usuario basada en web con Blazor que aprovecha el algoritmo cuántico y simula el envío de partículas cuánticas a diferentes destinatarios. Mostraré cómo se consume una biblioteca de Q# en una aplicación de servidor de Blazor y cómo lanzar y coordinar varias ventanas de explorador. También explicaré cómo utilizar el patrón de arquitectura Modelo-Vista-Modelo de vista (MVVM) en una aplicación de Blazor.

Este artículo contiene bastante teoría cuántica. Si aún no conoce la computación cuántica, le recomiendo que lea mi manual de computación cuántica “Quantum Computation Primer”. La primera parte está disponible en tinyurl.com/quantumprimer1.

Comencemos por ver qué es la codificación superdensa.

Concepto de codificación superdensa

La codificación superdensa aprovecha el fenómeno del entrelazamiento cuántico, por el que una partícula de un par entrelazado puede afectar al estado compartido de ambas, independientemente de que estén separadas por distancias que pueden ser enormes.

Para comprender el protocolo de la codificación superdensa, imaginemos que tenemos tres actores: Alice, Bob y Charlie (A, B y C). Charlie crea un par entrelazado de cúbits y envía uno a Alice y otro a Bob. Cuando Alice quiere enviar un mensaje de 2 bits a Bob, todo lo que tiene que hacer es manipular su propio cúbit, que influye en el estado cuántico del par, y enviarle su único cúbit a Bob. Después, Bob mide el cúbit de Alice y su propio cúbit para recibir el mensaje de 2 bits.

Lo importante es que no hay forma de codificar más de un bit de información en un solo cúbit. Sin embargo, solo un cúbit cambia de manos para entregar un mensaje de 2 bits. A pesar de la distancia que pueda haber entre los cúbits, el estado compartido de los cúbits entrelazados permite codificar todo el mensaje de 2 bits usando solo uno de los cúbits.

Además, cuando Alice le envía un mensaje a Bob, puede haber pasado una gran cantidad de tiempo desde que Charlie les envió un cúbit entrelazado a cada uno. En este sentido, podemos decir que los cúbits entrelazados son un recurso que está a la espera de ser consumido en el proceso de la comunicación.

La separación previa de los cúbits supone también una ventaja de seguridad importante: todo aquel que quiera descodificar el mensaje necesitará tener los dos cúbits, el de Alice y el de Bob. Si se intercepta el cúbit de Alice, no es suficiente para descodificar el mensaje y, como los cúbits entrelazados se han separado previamente, se elimina el riesgo de que los intercepten.

Si es escéptico en cuanto a la física que subyace en la codificación superdensa (que no es nada malo), sepa que el fenómeno se ha validado experimentalmente (tinyurl.com/75entanglement), incluso con satélites y láseres (tinyurl.com/spookyrecord).

Introducción a Q#

La compatibilidad de Visual Studio con el lenguaje Q# se consigue con Microsoft Quantum Development Kit. La forma más sencilla de instalar el kit para Visual Studio es usar el Administrador de extensiones de Visual Studio.

En el menú Extensiones de Visual Studio 2019, seleccione Administrar extensiones para mostrar el cuadro de diálogo correspondiente. Escriba “quantum” en el cuadro de búsqueda para localizar el kit. Una vez instalado, puede crear nuevos tipos de proyectos de Q#. El resaltado de sintaxis de Q# está habilitado y la depuración de proyectos de Q# funciona como sería de esperar.

Q# es un lenguaje de procedimientos específico del dominio que, desde el punto de vista de la sintaxis, está influenciado por C# y F#. Los archivos de Q# contienen operaciones, funciones y definiciones de tipos personalizados, cuyos nombres deben ser únicos en un espacio de nombres, como se muestra en la figura 1. Para que los miembros de otros espacios de nombres estén disponibles, se puede usar la directiva “open”.

Figura 1. Funciones, operaciones y definiciones de tipos personalizados

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

Las funciones son análogas a las funciones o los métodos de otros lenguajes de procedimientos. Una función acepta cero o más argumentos y puede devolver un solo objeto, valor o tupla. Cada parámetro de una función consta de un nombre de variable seguido de dos puntos y el tipo. El valor devuelto de una función se especifica después de dos puntos, al final de la lista de parámetros.

Recordemos lo que se explica en la tercera parte de mi manual de computación cuántica (tinyurl.com/controlledgates), que las puertas U controladas permiten agregar la entrada de un control para una puerta cuántica arbitraria. Q# tiene la palabra clave Controlled para esto, que permite aplicar un control a un operador, por ejemplo:

Controlled X([controlQubit1, controlQubit2], targetQubit)

Aquí, la instrucción Controlled pone dos entradas de control en la puerta Pauli-X.

Las operaciones tienen los mismos rasgos que las funciones, pero las operaciones pueden representar también operadores unitarios. Podemos decir que son puertas cuánticas compuestas. Permiten definir lo que ocurre cuando se usan como parte de una operación controlada.

Además, las operaciones permiten definir explícitamente la operación inversa (con la palabra clave adjoint). Adjoint es la transposición conjugada compleja del operador. Para obtener más información, consulte tinyurl.com/brafromket.

La palabra clave newtype permite definir un tipo personalizado, que se puede usar en operaciones y funciones.

Veamos ahora algunos tipos de expresiones comunes.

Las variables son inmutables en Q# de forma predeterminada. Para declarar una variable nueva, use la palabra clave let, de este modo:

let foo = 3;

El compilador infiere el tipo de la variable del valor que le asigne. Si tiene que cambiar el valor de una variable, use la palabra clave mutable cuando la declare:

mutable foo = 0;
set foo += 1;

Para crear una matriz inmutable en Q#, utilice lo siguiente:

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

Una matriz en Q# es un tipo de valor y, en su mayor parte, es inmutable, incluso si se crea con la palabra clave mutable. Cuando se utiliza la palabra clave mutable, se puede asignar otra matriz al identificador, pero no se pueden reemplazar elementos de la matriz:

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

El operador w/ es una abreviatura de la palabra inglesa “with” (con). En la segunda línea, todos los valores de foo se copian en una nueva matriz, con el primer elemento (índice 0) de la matriz establecido en 1. Tiene una complejidad en tiempo de ejecución de O(n). La instrucción de asignación anterior se puede expresar de una forma más corta combinándola con un signo igual, de este modo:

set foo w/= 0 <- 1;

Cuando se usa el operador w/=, se lleva a cabo un reemplazo en contexto donde es posible, lo que reduce la complejidad en tiempo de ejecución a O(1).

Puede crear un bucle usando una instrucción for:

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

La función Message integrada escribe mensajes en la consola. Q# admite la interpolación de cadenas. Al anteponer “$” a un literal de cadena, se pueden poner las expresiones entre llaves.

Para crear un bucle en una colección, use una instrucción for, así:

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

Esta ha sido una explicación rápida de Q#. Si desea conocerlo más a fondo, visite tinyurl.com/qsharp. También recomiendo trabajar con Microsoft Quantum Katas (tinyurl.com/quantumkatas).

Implementación de la codificación superdensa con Q#

Veamos ahora la implementación del protocolo cuántico de codificación superdensa en Q#. Doy por hecho que ha leído la primera y la segunda parte de mi serie o que ya conoce los aspectos básicos de la notación Dirac y de la computación cuántica.

En la descripción introductoria de la codificación superdensa, recuerde que Charlie entrelaza un par de cúbits y envía uno a Alice y otro a Bob.

Para entrelazar un par de cúbits, que comienza con el estado |0, envía el primer cúbit a través de una puerta Hadamard, que cambia su estado a:

La puerta Hadamard pone el primer cúbit en una superposición de |0 y |1, con lo que lo envía al estado |+. Este es el código:

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

La puerta CNOT invierte qubit2 si el estado de qubit1 es |1 y da como resultado el estado |Ф+:

El estado de los cúbits cuando se miden tendrá un 50 % de posibilidades de ser 00 o 11.

Con la creación de esta superposición, si midiera solo uno de los cúbits, haría que el estado del otro tomase el mismo valor, independientemente de la distancia que hubiera entre ellos. Se dice que los cúbits están entrelazados.

Una vez que se les ha enviado a Alice y a Bob un cúbit a cada uno de un par entrelazado, Alice puede modificar el estado cuántico global de los dos cúbits con solo manipular su cúbit. Un único cúbit puede codificar solo 1 bit de información. Pero, cuando se entrelaza con otro cúbit, se puede usar solo ese cúbit para codificar 2 bits de información en el estado cuántico compartido.

Alice puede transmitir 2 bits de información pasando su cúbit por una o varias puertas. Las puertas y el estado cuántico resultante del par de cúbits se muestran en la figura 2. El subíndice de los cúbits identifica al propietario: A para Alice, B para Bob.

Uso de puertas para producir estados de Bell
Figura 2. Uso de puertas para producir estados de Bell

Para enviar un mensaje de 2 bits de 00 no es necesario cambiar el cúbit de Alice (“I” es la puerta de identidad, que no hace nada). Para enviar 01 o 10, solo se necesita una puerta: X o Z, respectivamente; y, para enviar 11, es necesario aplicar las dos puertas, X y Z.

En la mecánica cuántica, los únicos estados que se pueden distinguir perfectamente son los ortogonales, que, en el caso de los cúbits, son estados perpendiculares en la esfera de Bloch. Resulta que los estados de Bell son ortogonales, por tanto, cuando le toca a Bob medir los cúbits, puede hacer una medición ortogonal de los dos cúbits para derivar el mensaje original de 2 bits de Alice.

Para codificar el cúbit de Alice, se envía a la operación EncodeMessageInQubit de Q#, como se muestra en la figura 3. Junto con el cúbit de Alice, EncodeMessageInQubit acepta dos valores booleanos que representan los 2 bits de información que se envían a Bob. La primera instrucción let concatena el par de valores de bit en un entero entre 002 y 112, donde bit1 es el bit más significativo. La secuencia de bits resultante indica las puertas que deben aplicarse.

Figura 3. Operación 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);
  }
}

Q# incluye muchas de las características de lenguaje de C# y F#, entre las que se encuentran los literales binarios, que resultan muy prácticos.

Después de que Alice codifica el mensaje y le envía su cúbit a Bob, este recibe el mensaje y aplica una puerta CNOT a los cúbits y la puerta Hadamard al cúbit de Alice:

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

Después, se mide cada uno de los cúbits. El resultado es una tupla que consta de los dos bits del mensaje original que envió Alice. Los valores medidos de cada cúbit corresponden al mensaje de bits clásicos codificado por Alice.

Veamos con más detalle cómo funciona este protocolo de codificación superdensa. Recordemos que Alice y Bob reciben cada uno un cúbit que pertenece a un par entrelazado. La representación de la matriz del estado cuántico de los dos cúbits es la siguiente:

Usaré el resultado de esta matriz dentro de un momento para mostrar el proceso de codificación.

Si Alice quiere enviar el mensaje 012 a Bob, aplica la puerta X a su cúbit, que cambia el estado cuántico a:

Al convertir esto en una matriz, obtenemos:

Después, Alice envía su cúbit a Bob.

Para descodificar el mensaje, Bob comienza por aplicar una puerta CNOT a los cúbits:

Ahora puede tomar el resultado de matriz del paso anterior y multiplicarlo por la matriz CNOT, teniendo en cuenta que el multiplicador escalar es conmutable:

Cuando factoriza la matriz resultante, obtiene:

Después, Bob aplica una puerta Hadamard al cúbit de Alice:

Puede sustituirlo en la representación de matriz de la puerta Hadamard y los cúbits:

Si divide el numerador y el denominador por √2, elimina el denominador común, lo que deja algunas operaciones aritméticas matriciales para hacer:

Como puede ver, el resultado final es el mensaje original que envió Alice.

Cuando Alice pasó su cúbit por la puerta X, afectó a la superposición global de los dos cúbits. Bob pudo determinar el cambio que Alice había realizado desentrañando el estado superpuesto.

Asignación de objetos Qubit

Cuando se asigna un objeto Qubit en Q#, debe devolverse al simulador. Esto se hace mediante sintaxis. Para asignar objetos Qubit, se necesita una instrucción using.

Para asignar un solo Qubit, use la expresión Qubit, como se muestra aquí:

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

La función Reset devuelve el Qubit al estado |0.

Al salir del bloque using, un objeto Qubit debe tener el estado |0. Este es un requisito en tiempo de ejecución impuesto por el simulador cuántico. Puede pasar un objeto Qubit a otras operaciones, cambiar su estado y así sucesivamente, pero si la ejecución llega al final del bloque using sin haber devuelto el Qubit al estado |0, se produce una excepción. Si desea deshabilitar las excepciones para los objetos Qubit no restablecidos, puede hacerlo con el constructor QuantumSimulator.

También puede crear varios objetos Qubit usando la expresión de matriz Qubit[]:

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

Los objetos Qubit creados con la expresión de matriz también deben restablecerse al terminar. Use la función ResetAll integrada para restablecer varios objetos Qubit.

Tenga en cuenta que los objetos Qubit simulados utilizan una cantidad exponencial de memoria. Cada Qubit asignado duplica la cantidad de memoria RAM necesaria. En las pruebas que he realizado en una máquina con 16 GB de RAM, el uso llegó a ser de unos 13 GB para 26 objetos Qubit. Si se supera este valor, se utiliza la memoria virtual. Pude llegar a los 31 objetos Qubit, pero el uso de la memoria se disparó hasta los 53 GB. Lógicamente, 32 objetos Qubit produjeron una excepción de interoperabilidad.

Uso de Q# a voluntad

El proyecto de ejemplo pasa objetos Qubit de forma asincrónica entre los objetos que representan a Alice, Bob y Charlie. Por ese motivo, tenía que buscar la forma de retener los objetos Qubit y evitar el requisito de Q# de utilizar un bloque using.

Hay una gran afinidad entre las herramientas de Q# y de C# en Visual Studio. Cuando se crea un proyecto en Q#, los archivos .qs de ese proyecto se traducen a C#. Las herramientas de Q# emiten los archivos de C# generados con la extensión de archivo “.g.cs” y los ponen en el directorio \obj\qsharp\src del proyecto principal.

Sospecho que este paso de traducción se podría reemplazar por la compilación directa como IL o algo así en el futuro, teniendo en cuenta la inversión que está haciendo Microsoft en el compilador de Q#. Pero, por el momento, me pareció útil explorar los archivos .cs generados echando un vistazo al funcionamiento subyacente. En seguida me di cuenta de que se pueden aprovechar los ensamblados de C# del SDK de Q# para conseguir lo que necesitaba.

La clase QOperations, en el código de ejemplo que se puede descargar, llama directamente a las API de Q#, lo que le permite solicitar un objeto Qubit del simulador sin tener que liberarlo de inmediato. En lugar de limitarme a Q# para todas las actividades cuánticas, utilizo C# para asignar, entrelazar y desasignar cúbits.

La clase QOperations crea una instancia de la clase QuantumSimulator, que está en el espacio de nombres Microsoft.Quantum.Simulation.Simulators. QuantumSimulator proporciona una API para manipular el componente del simulador nativo. El simulador se instala con Microsoft Quantum Development Kit.

QuantumSimulator tiene un contenedor IoC interno que permite recuperar varios objetos que suele utilizar el código de C# generado. Por ejemplo, en un archivo de C# puede usar el método Get<T> del objeto QuantumSimulator para recuperar un objeto con el fin de restablecer el estado de un objeto Qubit y liberarlo, que es lo mismo que hace un bloque using en Q#:

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

Para asignar un nuevo objeto Qubit, utilice el objeto Microsoft.Quantum.Intrinsic.Allocate. También necesita los objetos de puerta CNOT y H (Hadamard), que se recuperan del contenedor, de este modo:

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

Con estos objetos puede generar pares de cúbits entrelazados. El método GetEntangledPairsAsync de la clase QOperations crea una lista de tuplas de objetos Qubit (vea la figura 4).

Figura 4. Creación de objetos Qubit entrelazados en 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);
}

Yo utilizo el método Apply del objeto Allocate para recuperar dos objetos Qubit libres del simulador. Aplico la puerta Hadamard al primer objeto Qubit y, después, la puerta CNOT a los dos, con lo que los pongo en el estado entrelazado. No es necesario restablecerlos y liberarlos de inmediato; puedo devolver los pares de objetos Qubit desde el método. Aún sigue siendo necesario liberar los objetos Qubit. De lo contrario, la aplicación se quedaría rápidamente sin memoria. Simplemente, pospongo ese paso.

Los objetos Qubit se liberan con el método ReleaseQubitsAsync, como se muestra en la figura 5.

Figura 5. Liberación de objetos Qubit en 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;
}

Cada Qubit suministrado lo restablezco con el método Apply del objeto Reset. Después, informo al simulador de que el objeto Qubit está libre con el método Apply del objeto Release. También podría usar la propiedad QubitManager del simulador para liberar el objeto Qubit.

Con esto termina la parte cuántica de este artículo. Ahora veamos Blazor y la implementación de un proyecto de Blazor del lado servidor con MVVM.

Uso del patrón MVVM con Blazor

Soy fan de XAML desde hace mucho tiempo. Me gusta la forma en la que puedo escribir lógica de tipo Modelo de vista en C# y combinarla con un archivo de diseño reutilizable fácil de crear mediante enlaces de datos. Blazor permite hacer lo mismo, pero, en lugar de XAML, utiliza Razor, que tiene una sintaxis concisa para marcar el contenido dinámico en HTML.

Si no conoce Blazor, consulte las instrucciones de instalación que se proporcionan en tinyurl.com/installblazor para el IDE que desee.

La primera vez que implementé este proyecto, utilicé la Plataforma universal de Windows (UWP). UWP es compatible con .NET Core y tenía sentido crear rápidamente la interfaz de usuario con una tecnología basada en XAML. De hecho, la versión de UWP de la aplicación está en el código de ejemplo que se puede descargar. Si no le apetece instalar Blazor y solo quiere ver cómo funciona la aplicación, puede ejecutar la versión de UWP simplemente. Las dos aplicaciones se comportan de idéntica forma.

Migrar la aplicación a Blazor fue muy sencillo. No tuve que realizar cambios en el código. Las referencias de NuGet al marco MVVM Codon (codonfx.com) valían y todo funcionó sin más.

Elegí el modelo del lado servidor de Blazor para este proyecto con el fin de poder compartir un solo objeto QuantumSimulator entre los tres patrones Modelo de vista que representaban a Alice, Bob y Charlie, y también para pasar objetos Qubit. 

Disección de la aplicación de Blazor La aplicación de Blazor contiene tres páginas de Razor, como se muestra en la figura 6, que representan a tres actores: Alice.razor, Bob.razor y Charlie.razor.

Páginas de Razor para Alice, Bob y Charlie
Figura 6. Páginas de Razor para Alice, Bob y Charlie

Cada página tiene asociado un modelo de vista. Los tres patrones Modelo de vista del proyecto son subclases de la clase ViewModelBase de Codon, que proporciona compatibilidad integrada con eventos INotifyPropertyChanged (INPC), una referencia a un contenedor IoC y mensajería ligeramente acoplada entre los componentes de la aplicación.

Cada página de Razor tiene un bloque functions que incluye una propiedad para su modelo de vista correspondiente. Este es el bloque functions en Alice.razor:

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

El modelo de vista se recupera del contenedor IoC de Codon durante el método OnInitAsync.

Los cambios de propiedades del modelo de vista no hacen que el contenido de la página se actualice automáticamente. Por eso hay que suscribirse al evento PropertyChanged de las clases ViewModelBase. Cuando tiene lugar el evento, se llama al método StateHasChanged de la página de Razor, que desencadena una actualización de los elementos HTML que están enlazados a datos en el modelo de vista.

Para asegurar que se llevan a cabo las actualizaciones en el subproceso de la interfaz de usuario, se utiliza el método Invoke de la página. Si se llama al método StateHasChanged desde un subproceso que no sea de la interfaz de usuario, se produce una excepción.

Solicitud de los cúbits de Charlie. La página de Razor de Charlie indica el número de cúbits enviados a Alice y a Bob. Como muestra la figura 7, en el bloque functions de esta página ocurre algo más.

Figura 7. Fragmento del bloque functions de la página 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;
});”;
}

Además de la inicialización del modelo de vista, la página tiene también la tarea de abrir una ventana de explorador nueva para Alice y para Bob. Para esto, llama a JSRuntime.InvokeAsync y utiliza la función eval de JavaScript para ejecutar JavaScript en el explorador. El script de eval abre y retiene las ventanas como variables. Cuando tiene lugar el evento beforeunload de la página de Charlie, las ventanas se cierran. Esto impide la acumulación de ventanas de explorador huérfanas cuando se detiene el depurador.

Tenga en cuenta que la mayoría de los exploradores actuales bloquean las ventanas emergentes de forma predeterminada o, al menos, solicitan permiso para abrirlas. Si eso ocurre, deberá aceptar las ventanas emergentes y actualizar la página. Yo utilizo las ventanas emergentes en este proyecto solamente por la comodidad de abrir las ventanas de los tres actores a la vez.

La página Charlie.razor contiene un único elemento dinámico, un contador que muestra el número de cúbits que se han enviado:

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

El valor del párrafo está enlazado a la propiedad QubitSentCount del modelo de vista.

Cuando se derivan clases de Codon.UIModel.ViewModelBase, cualquier implementación de IMessageSubscriber<T> se suscribe automáticamente a los mensajes de tipo T. CharlieViewModel implementa IMessageSubscriber<RequestQubitsMessage> e IMessageSubscriber<ReleaseQubitsMessage>; por tanto, cuando AliceViewModel publica un RequestQubitsMessage o BobViewModel publica un ReleaseQubitsMessage, se controla en la clase Charlie­ViewModel. Esta es la única tarea que tiene Charlie, enviar cúbits entrelazados a Alice y Bob cuando es necesario.

Cuando CharlieViewModel recibe un RequestQubitsMessage, utiliza la instancia QOperations para recuperar una lista de pares de cúbits entrelazados, como se muestra en la figura 8.

Figura 8. Método ReceiveMessageAsync(RequestQubitsMessage) de CharlieViewModel

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

Después, CharlieViewModel envía el primer elemento de cada par a Alice, y el segundo, a Bob, a través de IMessenger.

Finalmente, aumenta el valor de QubitSentCount, que actualiza la página de Charlie.

Envío de mensajes a Alice. Echemos un vistazo a la página de Alice y a su modelo de vista asociado. La clase AliceViewModel contiene un comando AsyncActionCommand, que se define de este modo:

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

Todavía me entusiasma ver la brevedad con la que se escriben las propiedades. Una expresión lambda combinada con un operador de fusión nulo incorpora carga diferida con solo un par de líneas de código.

La infraestructura de comandos de Codon admite controladores de comandos asincrónicos. El comando AsyncActionCommand de Codon permite especificar el método asincrónico SendAsync como controlador del comando.

Alice.razor contiene un campo de texto y un botón:

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

El cuadro de entrada está enlazado a la propiedad Message del modelo de vista. Cuando se hace clic en el botón Send to Bob (Enviar a Bob), se ejecuta el comando SendCommand del modelo de vista y se llama a su método SendAsync (vea la 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;
}

El modelo de vista tiene una cola que contiene los bytes de cada mensaje que se codifica y se le envía a Bob. Para enviar el mensaje, se usa IMessenger de Codon. Finalmente, el mensaje le llega a Charlie y solicita que se entrelacen cuatro cúbits y se envíen dos a Alice y dos a Bob.

Por último, la propiedad Message se restablece en string.empty, que borra el cuadro de entrada y lo deja preparado para un mensaje nuevo.

AliceViewModel implementa IMessageSubscriber<AliceQubitMessage> y, por tanto, recibe todos los mensajes de ese tipo:

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

Cuando se llama a ReceiveMessageAsync, la carga útil contiene una colección de instancias Qubit de Charlie. AliceViewModel retiene también una cola de objetos Qubit, a la que se agregan los cúbits que acaban de llegar.

Ahora AliceViewModel está preparado para enviar al menos parte de un mensaje a Bob. Se llama a DispatchItemsInQueue, como se muestra en la 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 primero recibe la instancia QOperations del contenedor IoC. Dependency.Resolve<T,T>(bool singleton) da lugar a que se cree una instancia nueva si no se había creado ya. Después, esa instancia se retiene como un singleton para que las solicitudes futuras resuelvan el mismo objeto.

Para quitar los cuatro cúbits de la cola de objetos Qubit, se utiliza un método de extensión. También se quita el byte del mensaje y se pone en un objeto BitArray. A continuación, el objeto QOperations recibe la tarea de poner cada cúbit en una superposición que represente el mensaje de 2 bits. De nuevo, vea que un cúbit puede representar dos bits clásicos, porque su estado afecta al estado cuántico del par.

Después, se usa IMessenger para enviar un objeto DecodeQubits­Message que recibirá Bob.

Recepción de mensajes como Bob. Al igual que Alice, Bob tiene una cola de objetos Qubit que le llegan de Charlie. BobViewModel implementa IMessageSubscriber<BobQubitMessage>. Cuando Charlie envía cúbits encapsulados en un BobQubitMessage, se ponen en la cola.

BobViewModel implementa también IMessageSubscriber<Decode­QubitsMessage>. Cuando Alice envía un mensaje a Bob, va encapsulado en un objeto DecodeQubitsMessage y lo controla el método ReceiveMessageAsync(DecodeQubitsMessage) (vea la figura 11). El método quita de su cola un número igual de cúbits. El método DecodeQubits de QOperations se utiliza para convertir cada par de cúbits de Alice y Bob en 2 bits de datos. Cada byte del mensaje consta de cuatro pares de 2 bits (8 bits), por lo que el bucle aumenta para i y 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 se construye desplazando los bits hacia la izquierda conforme a los valores de los bits descodificados. Los bytes descodificados se vuelven a convertir en una cadena con el método Encoding.ASCII.GetString y se anexan a la propiedad Message que se muestra en la página.

Una vez descodificados los cúbits, se liberan publicando un ReleaseQubitsMessage que recibe CharlieViewModel. Después, se utiliza QOperations para liberar los cúbits:

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

Resumen

En este artículo, he implementado un algoritmo cuántico para la codificación superdensa en Q#, el nuevo lenguaje de programación cuántica de Microsoft. He explicado cómo entrelazar cúbits usando puertas cuánticas y cómo codificar mensajes ASCII como cúbits. He hablado de una interfaz de usuario basada en web con Blazor que aprovecha el algoritmo cuántico y simula el envío de partículas cuánticas a diferentes destinatarios. También he mostrado cómo se consume una biblioteca de Q# en una aplicación de servidor de Blazor y cómo lanzar y coordinar varias ventanas de explorador. Finalmente, he explicado cómo utilizar el patrón MVVM en una aplicación de Blazor.

Referencias

  • “Statements and Other Constructs”, obtenido el 5 de julio de 2019 de bit.ly/2Ofx1Ld.
  • Yanofsky, N. y Mannucci, M., (2008) “Quantum Computing for Computer Scientists”, Cambridge University Press.
  • Nielsen, M. y Chuang, I., (2010) “Quantum Computation and Quantum Information”, décima edición, Cambridge University Press, Cambridge (Reino Unido).
  • Watrous, J., (2006) “Lecture 3: Superdense coding, quantum circuits, and partial measurements”, obtenido el 5 de julio de 2019 de bit.ly/2XTPEDN.

Daniel Vaughan es escritor e ingeniero de software y trabaja para Microsoft en Redmond (Washington). Fue Microsoft MVP nueve veces y es el desarrollador de varias aplicaciones empresariales y de consumidor para móviles muy aclamadas, como Surfy Browser para Android y Windows Phone y Airlock Browser para Android. También es el creador de un gran número de proyectos de código abierto muy populares, como los marcos Codon y Calcium. Vaughan publica entradas de blog en danielvaughan.org y tweets como @dbvaughan.

Gracias al siguiente experto técnico de Microsoft por revisar este artículo: Bettina Heim (equipo de Microsoft Quantum)
Bettina Heim forma parte del equipo de Microsoft Quantum.


Comente este artículo en el foro de MSDN Magazine