Tutorial: Exploración del entrelazamiento con Q#

En este tutorial, se muestra cómo escribir un programa de Q# que manipula y mide cúbits, y muestra los efectos de la superposición y el entrelazamiento.

Escribirá una aplicación llamada Bell para demostrar el entrelazamiento cuántico. El nombre Bell hace referencia a los estados de Bell, que son estados cuánticos específicos de dos cúbits que se usan para representar los ejemplos más sencillos de la superposición y el entrelazamiento cuántico.

Requisitos previos

También puede seguir sin instalar el QDK y revisar la introducción al lenguaje de programación Q# y los conceptos básicos de la computación cuántica.

En este tutorial, aprenderá a:

  • Crear y combinar operaciones en Q#.
  • Crear operaciones para colocar los cúbits en superposición, entrelazarlos y medirlos.
  • Demostrar el entrelazamiento cuántico con un programa Q# ejecutado en un simulador.

Demostración del comportamiento de los cúbits con el QDK

Mientras que los bits clásicos contienen un único valor binario, como 0 o 1, el estado de un cúbit puede ser una superposición de 0 y 1. Conceptualmente, el estado de un cúbit se podría considerar como una dirección en un espacio abstracto (también conocida como vector). El estado de un cúbit puede estar en cualquiera de las direcciones posibles. Los dos estados clásicos son las dos direcciones, que representan el 100 % de probabilidad de medir 0 y el 100 % de probabilidad de medir 1.

La acción de medir genera un resultado binario y cambia el estado del cúbit. La medición produce un valor binario, ya sea 0 o 1. El cúbit pasa de estar en superposición (cualquier dirección) a estar en uno de los estados clásicos. Después, si se repite la medida sin que intervenga ninguna operación, se produce el mismo resultado binario.

Varios cúbits pueden estar entrelazados. Cuando se toma una medida de un cúbit entrelazado, se actualizan también los conocimientos del estado de los otros.

Escritura de la aplicación Q#

El objetivo es preparar dos cúbits en un estado cuántico específico, demostrar cómo operar en los cúbits con Q# para cambiar su estado, y demostrar los efectos de la superposición y el entrelazamiento. Creará esto paso a paso para explicar los estados del cúbit, las operaciones y la medición.

Inicialización de cúbits mediante medición

El fragmento de código siguiente muestra cómo trabajar con cúbits en Q#. El código presenta dos operaciones, M y X, que transforman el estado de un cúbit.

Se define una operación SetQubitState que toma como parámetro un cúbit y otro parámetro, desired, que representa el estado en el que queremos que esté el cúbit. La operación SetQubitState toma una medida en el cúbit con la operación M. En Q#, la medición de un cúbit siempre devuelve Zero o One. Si la medición devuelve un valor distinto al deseado, SetQubitState "invierte" el cúbit; es decir, ejecuta una operación X que cambia el estado del cúbit a un nuevo estado en el que las probabilidades de que una medición devuelva Zero y One se invierten. De este modo, SetQubitState siempre coloca el qubit de destino en el estado deseado.

Reemplace el contenido de Program.qs por el código siguiente:

   namespace Bell {
       open Microsoft.Quantum.Intrinsic;
       open Microsoft.Quantum.Canon;

       operation SetQubitState(desired : Result, q1 : Qubit) : Unit {
           if desired != M(q1) {
               X(q1);
           }
       }
   }

Ahora se puede llamar a esta operación para establecer un cúbit en un estado clásico, ya sea devolviendo Zero el 100 % del tiempo o devolviendo One el 100 % del tiempo. Zero y One son constantes que representan los únicos dos resultados posibles de la medida de un cúbit.

La operación SetQubitState mide el cúbit. Si el cúbit está en el estado deseado, SetQubitState lo deja así; de lo contrario, al ejecutar la operación X, el estado del cúbit cambia al estado deseado.

Acerca de las operaciones Q#

Una operación Q# es una subrutina cuántica. Es decir, es una rutina invocable que contiene llamadas a otras operaciones cuánticas.

Los argumentos de una operación se especifican como una tupla, entre paréntesis.

El tipo de valor devuelto de la operación se especifica después de un signo de dos puntos. En este caso, la operación de SetQubitState no tiene ningún tipo de valor devuelto, por lo que se marca como Unit. Este es el equivalente en Q# de unit en F#, que es más o menos análogo a void en C#, y a una tupla vacía en Python ((), representada por la sugerencia de tipo Tuple[()]).

Ha usado dos operaciones cuánticas en la primera operación de Q#:

  • La operación M, que mide el estado del cúbit.
  • La operación X, que invierte el estado de un cúbit.

Una operación cuántica transforma el estado de un cúbit. Algunos usuarios hablan de puertas cuánticas en lugar de operaciones, por analogía con las puertas lógicas clásicas. Esto tiene su origen en los primeros tiempos de la computación cuántica, cuando los algoritmos eran una simple construcción teórica y se visualizaban como diagramas, de forma similar a los diagramas de circuitos de la computación clásica.

Recuento de resultados de medición

Para mostrar el efecto de la operación SetQubitState, se agrega una operación TestBellState. Esta operación toma como entrada un Zero o One, llama a la operación SetQubitState un número determinado de veces con esa entrada y cuenta el número de veces que la medida del cúbit devolvió Zero y el número de veces que se devolvió One. Por supuesto, en esta primera simulación de la operación TestBellState, se espera que la salida muestre que todas las medidas de un cúbit establecidas con Zero como entrada del parámetro devolverán Zero, y todas las medidas de un cúbit establecidas con One como entrada del parámetro devolverán One. Más adelante, se agregará código a TestBellState para demostrar la superposición y el entrelazamiento.

Agregue la siguiente operación al archivo Program.qs, dentro del espacio de nombres, después del final de la operación de SetQubitState:

   operation TestBellState(count : Int, initial : Result) : (Int, Int) {

       mutable numOnes = 0;
       use qubit = Qubit();
       for test in 1..count {
           SetQubitState(initial, qubit);
           let res = M(qubit);

           // Count the number of ones we saw:
           if res == One {       
               set numOnes += 1;
           }
       }

       SetQubitState(Zero, qubit);

       

       // Return number of times we saw a |0> and number of times we saw a |1>
       Message("Test results (# of 0s, # of 1s): ");
       return (count - numOnes, numOnes);
   }

Tenga en cuenta que hay una línea antes de return para imprimir un mensaje explicativo en la consola con la función (Message)[microsoft.quantum.intrinsic.message]

Esta operación (TestBellState) creará un bucle para count iteraciones, establecerá un valor de initial especificado en cúbits y, a continuación, medirá (M) el resultado. Recopilará estadísticas sobre cuántos ceros y unos se han medido y los devolverá al autor de la llamada. Realiza otra operación necesaria. Restablece el cúbit a un estado conocido (Zero) antes de devolverlo, lo que permite que otros usuarios asignen este cúbit en un estado conocido. Esto es necesario para la instrucción use.

Acerca de las variables en Q#

De forma predeterminada, las variables en Q# son inmutables; su valor no se puede cambiar una vez que se enlazaron. La palabra clave let se utiliza para indicar el enlace de una variable inmutable. Los argumentos de la operación son siempre inmutables.

Si necesita una variable cuyo valor puede cambiar, como numOnes en el ejemplo, puede declarar la variable con la palabra clave mutable. Se puede cambiar el valor de una variable mutable mediante una instrucción set.

En ambos casos, el compilador deduce el tipo de una variable. Para las variables, Q# no requiere ninguna anotación de tipo.

Acerca de las instrucciones use en Q#

La instrucción use también es especial para Q#. Se usa para asignar cúbits para su uso en un bloque de código. En Q#, todos los cúbits se asignan y liberan dinámicamente, en lugar de ser recursos fijos que están disponibles para toda la duración de un algoritmo complejo. Una instrucción use asigna un conjunto de cúbits al principio y libera esos cúbits al final del bloque.

Ejecución del código desde la línea de comandos

Para ejecutar el código, es necesario decir al compilador qué rutina invocable se puede ejecutar cuando se proporciona el comando dotnet run. Esto se hace con un cambio simple en el archivo Q#, agregando una línea con @EntryPoint() directamente antes de la operación invocable, TestBellState en este caso. El código completo debe ser:

namespace Bell {
    open Microsoft.Quantum.Canon;
    open Microsoft.Quantum.Intrinsic;

    operation SetQubitState(desired : Result, target : Qubit) : Unit {
        if desired != M(target) {
            X(target);
        }
    }

    @EntryPoint()
    operation TestBellState(count : Int, initial : Result) : (Int, Int) {

        mutable numOnes = 0;
        use qubit = Qubit();
        for test in 1..count {
            SetQubitState(initial, qubit);
            let res = M(qubit);
            

            // Count the number of ones we saw:
            if res == One {
                  set numOnes += 1;
            }
        }

        SetQubitState(Zero, qubit);
        

    // Return number of times we saw a |0> and number of times we saw a |1>
    Message("Test results (# of 0s, # of 1s): ");
    return (count - numOnes, numOnes);
    }
}

Para ejecutar el programa, es necesario especificar los argumentos count y initial desde el símbolo del sistema. Vamos a elegir, por ejemplo, count = 1000 y initial = One. Escriba el comando siguiente:

dotnet run --count 1000 --initial One

Debe observar la siguiente salida:

Test results (# of 0s, # of 1s):
(0, 1000)

Si lo intenta con initial = Zero, debe observar lo siguiente:

dotnet run --count 1000 --initial Zero
Test results (# of 0s, # of 1s):
(1000, 0)

Preparar la superposición

Ahora veremos cómo Q# expresa las maneras de colocar los cúbits en superposición. Recuerde que el estado de un cúbit puede ser una superposición de 0 y 1. Esto se puede lograr con la operación Hadamard. Si el cúbit está en cualquiera de los estados clásicos (al medir devuelve Zero siempre o One siempre), las operaciones Hadamard o H pondrán el cúbit en un estado tal que al medirlo, devolverá Zero el 50 % del tiempo y devolverá One el otro 50 % del tiempo. Conceptualmente, el cúbit se puede considerar a medio camino entre Zero y One. Ahora, en la simulación de la operación TestBellState, el resultado devolverá aproximadamente un número igual de Zero y One después de la medición.

X invierte el estado del cúbit

En primer lugar, intentaremos invertir el cúbit (si el cúbit está en el estado Zero se invierte a One, y viceversa). Esto se logra con una operación X antes de medirlo en TestBellState:

X(qubit);
let res = M(qubit);

Ahora los resultados están invertidos:

dotnet run --count 1000 --initial One
Test results (# of 0s, # of 1s):
(1000, 0)
dotnet run --count 1000 --initial Zero
Test results (# of 0s, # of 1s):
(0, 1000)

Ahora vamos a explorar las propiedades cuánticas de los cúbits.

H prepara la superposición

Lo único que hay que hacer es reemplazar la operación X de la ejecución anterior por una operación H o Hadamard. En lugar de invertir totalmente el cúbit de 0 a 1, solo lo invertimos a la mitad. Las líneas reemplazadas en TestBellState ahora tienen el siguiente aspecto:

H(qubit);
let res = M(qubit);

Ahora tenemos resultados más interesantes:

dotnet run --count 1000 --initial One
Test results (# of 0s, # of 1s):
(496, 504)
dotnet run --count 1000 --initial Zero
Test results (# of 0s, # of 1s):
(506, 494)

En cada medición, se pide un valor clásico, pero el cúbit se encuentra a medio camino entre 0 y 1, por lo que obtenemos (estadísticamente) 0 la mitad del tiempo y 1 la otra mitad del tiempo. Esto se conoce como superposición y nos proporciona nuestra primera vista real en un estado cuántico.

Preparación del entrelazamiento

Ahora veamos cómo Q# expresa las maneras de entrelazar los cúbits. En primer lugar, se establece el primer cúbit en el estado inicial y, después, se usa la operación H para ponerlo en superposición. Luego, antes de medir el primer cúbit, se usa nueva operación (CNOT), que significa Controlled-NOT. El resultado de ejecutar esta operación en dos cúbits es la inversión del segundo cúbit si el primero es One. Ahora, los dos cúbits están entrelazados. Las estadísticas del primer cúbit no han cambiado (50-50 de probabilidades de Zero o One después de medir) pero ahora, cuando se mide el segundo cúbit, es siempre igual que lo que se midió para el primer cúbit. Nuestro CNOT ha hecho el entrelazamiento de los dos cúbits, de modo que lo que le suceda a uno, le sucede al otro. Si invirtió las medidas (hizo la del segundo cúbit antes del primero), sucedería lo mismo. La primera medida sería aleatoria y la segunda sería en el paso de bloqueo con lo que se haya descubierto en primer lugar.

Para preparar el entrelazamiento, asigne primero dos cúbits en lugar de uno en TestBellState:

use (q0, q1) = (Qubit(), Qubit());

Esto permitirá agregar una nueva operación (CNOT) antes de medir (M) en TestBellState:

SetQubitState(initial, q0);
SetQubitState(Zero, q1);

H(q0);
CNOT(q0, q1);
let res = M(q0);

Agregue otra operación SetQubitState para inicializar el primer cúbit y asegurarse de que siempre esté en el estado Zero.

También hay que restablecer el segundo cúbit antes de liberarlo.

SetQubitState(Zero, q0);
SetQubitState(Zero, q1);

La rutina completa se ve ahora así:

    operation TestBellState(count : Int, initial : Result) : (Int, Int) {

        mutable numOnes = 0;
        use (q0, q1) = (Qubit(), Qubit());
        for test in 1..count {
            SetQubitState(initial, q0);
            SetQubitState(Zero, q1);

            H(q0);
            CNOT(q0,q1);
            let res = M(q0);

            // Count the number of ones we saw:
            if res == One {
                set numOnes += 1;
            }
        }

        SetQubitState(Zero, q0);
        SetQubitState(Zero, q1);
        

        // Return number of times we saw a |0> and number of times we saw a |1>
        return (count-numOnes, numOnes);
    }

Si se ejecuta, se obtiene exactamente el mismo resultado 50-50 que antes. Sin embargo, ahora queremos ver cómo reacciona el segundo cúbit al primero que se mide. Esta estadística se agrega con una nueva versión de la operación TestBellState:

    operation TestBellState(count : Int, initial : Result) : (Int, Int, Int) {
        mutable numOnes = 0;
        mutable agree = 0;
        use (q0, q1) = (Qubit(), Qubit());
        for test in 1..count {
            SetQubitState(initial, q0);
            SetQubitState(Zero, q1);

            H(q0);
            CNOT(q0, q1);
            let res = M(q0);

            if M(q1) == res {
                set agree += 1;
            }

            // Count the number of ones we saw:
            if res == One {
                set numOnes += 1;
            }
        }

        SetQubitState(Zero, q0);
        SetQubitState(Zero, q1);
        

        // Return times we saw |0>, times we saw |1>, and times measurements agreed
        Message("Test results (# of 0s, # of 1s, # of agreements)");
        return (count-numOnes, numOnes, agree);
    }

El nuevo valor devuelto (agree) realiza un seguimiento de cada vez que la medida del primer cúbit coincide con la medida del segundo.

Al ejecutar el código, se obtiene:

dotnet run --count 1000 --initial One
(505, 495, 1000)
dotnet run --count 1000 --initial Zero
Test results (# of 0s, # of 1s, # of agreements)
(507, 493, 1000)

Tal y como se indicó en la introducción, las estadísticas del primer cúbit no han cambiado (50-50 de probabilidades de 0 o 1 después de medir) pero ahora, al medir el segundo cúbit, es siempre igual que lo que se midió para el primer cúbit, porque están entrelazados.

Pasos siguientes

El tutorial sobre la búsqueda de Grover muestra cómo compilar y ejecutar una búsqueda de Grover, uno de los algoritmos de computación cuántica más populares, que ofrece un buen ejemplo de un programa de Q# que se puede usar para resolver problemas reales con la computación cuántica.

En Configuración de Azure Quantum se recomiendan más formas de aprender Q# y programación cuántica.