Tutorial: Escritura y simulación de programas de nivel de cúbit en Q#

Le damos la bienvenida al tutorial del kit de desarrollo de Quantum (QDK) sobre cómo escribir y simular un programa cuántico básico que funciona en cúbits individuales.

Aunque Q# se creó como un lenguaje de programación de alto nivel para programas cuánticos a gran escala, puede utilizarse con la misma facilidad para explorar el nivel inferior de los programas cuánticos: dirigirse directamente a cúbits específicos. La flexibilidad de Q# permite a los usuarios abordar los sistemas cuánticos desde cualquier nivel de abstracción, y en este tutorial nos sumergimos en los propios cúbits.

En concreto, en este tutorial vamos a echar un vistazo a fondo a la transformación cuántica de Fourier, una subrutina que forma parte de muchos algoritmos cuánticos de mayor tamaño.

Observe que esta vista general del procesamiento cuántico de la información suele describirse en términos de “circuitos cuánticos”, que representan la aplicación secuencial de puertas a los cúbits específicos de un sistema.

Así, las operaciones de uno y varios cúbits que aplicamos secuencialmente pueden representarse fácilmente en un “diagrama de circuitos”. En este caso, definirá una operación de Q# para realizar la transformación cuántica de Fourier completa de tres cúbits, que tiene la siguiente representación como circuito:


Three qubit quantum Fourier transform circuit diagram

Requisitos previos

En este tutorial, aprenderá a:

  • Definir las operaciones cuánticas en Q#.
  • Llamar a las operaciones de Q# directamente desde la línea de comandos o utilizando un programa host clásico.
  • Simular una operación cuántica desde la asignación de cúbits hasta la salida de la medida.
  • Observar cómo evoluciona la función de onda simulada del sistema cuántico a lo largo de la operación.

La ejecución de un programa cuántico con el QDK suele constar de dos partes:

  1. El programa en sí, que se implementa utilizando el lenguaje de programación cuántico Q# y luego se invoca para que se ejecute en un ordenador cuántico o en un simulador cuántico. Se componen de:
    • Operaciones de Q#: subrutinas que actúan sobre registros cuánticos.
    • Funciones Q#: subrutinas clásicas utilizadas dentro del algoritmo cuántico.
  2. El punto de entrada utilizado para llamar al programa cuántico y especificar la máquina de destino en la que debe ejecutarse. Esto puede hacerse directamente desde el símbolo del sistema o mediante un programa host escrito en un lenguaje de programación clásico, como Python o C#. Este tutorial incluye instrucciones para cualquiera de los métodos que prefiera.

Asignación de cúbits y definición de operaciones cuánticas

La primera parte de este tutorial consiste en definir la operación de Q# Perform3qubitQFT, que realiza la transformación cuántica de Fourier sobre tres cúbits.

Además, se usará la función DumpMachine para observar cómo evoluciona la función de onda simulada de nuestro sistema de tres cúbits a lo largo de la operación.

El primer paso es crear su proyecto de Q# y su archivo. Los pasos dependen del entorno que vaya a utilizar para llamar al programa, y puede encontrar los detalles en las respectivas guías de instalación.

En este tutorial se le guiará paso a paso por los componentes del archivo, pero el código también está disponible como un bloque completo a continuación.

Espacios de nombres para acceder a otras operaciones de Q#

Dentro del archivo, primero se define mos el espacio de nombres NamespaceQFT al que accederá el compilador. Para que esta operación haga uso de las operaciones de Q# existentes, se abren los espacios de nombres de Microsoft.Quantum.<> correspondientes.

namespace NamespaceQFT {
    open Microsoft.Quantum.Intrinsic;
    open Microsoft.Quantum.Diagnostics;
    open Microsoft.Quantum.Math;
    open Microsoft.Quantum.Arrays;

    // operations go here
}

Definición de operaciones con argumentos y retornos

A continuación, se define la operación Perform3qubitQFT:

    operation Perform3qubitQFT() : Unit {
        // do stuff
    }

Por ahora, la operación no toma argumentos y no devuelve nada. En este caso, escribimos que devuelve un objeto Unit, que es parecido a void en C# o una tupla vacía, Tuple[()], en Python. Más adelante, se modificará para que devuelva una matriz de resultados de las medidas y, en ese momento, Unit se reemplazará por Result[].

Asignación de cúbits con use

Dentro de nuestra operación de Q#, se asigna un registro de tres cúbits con la palabra clave use:

        use qs = Qubit[3];

        Message("Initial state |000>:");
        DumpMachine();

Con use, los cúbits se asignan automáticamente en el estado $\ket{0}$. Para comprobarlo, use Message(<string>) y DumpMachine(), que imprimen una cadena y el estado actual del sistema en la consola.

Nota

Las funciones Message(<string>) y DumpMachine() (de Microsoft.Quantum.Intrinsic y Microsoft.Quantum.Diagnostics, respectivamente) imprimen directamente en la consola. Al igual que una computación cuántica real, Q# no nos permite acceder directamente a los estados de los cúbits. Sin embargo, como DumpMachine imprime el estado actual de la máquina de destino, puede proporcionar información valiosa para depuración y aprendizaje cuando se utiliza con el simulador de estado completo.

Aplicación de compuertas de un solo cúbit y controladas

A continuación, aplique las puertas que componen la operación propiamente dicha. Q# ya contiene muchas puertas cuánticas básicas como operaciones en el espacio de nombres Microsoft.Quantum.Intrinsic, y estas no son una excepción.

Dentro de una operación de Q#, las instrucciones que invocan las llamadas se ejecutarán, por supuesto, en orden secuencial. Por lo tanto, la primera puerta que se aplicará es H (Hadamard) al primer cúbit:


Circuit diagram for three qubit QFT through first Hadamard

Para aplicar una operación a un cúbit específico de un registro (por ejemplo, un solo Qubit de una matriz Qubit[]) utilizamos la notación de índice estándar. Así, la aplicación de H al primer cúbit de nuestro registro qs toma la siguiente forma:

            H(qs[0]);

Además de aplicar la puerta H (Hadamard) a los cúbits individuales, el circuito de QFT consiste principalmente en rotaciones R1 controladas. Una operación R1(θ, <qubit>) en general deja el componente $\ket{0}$ del cúbit sin cambios, mientras que aplica una rotación de $e^{i\theta}$ al componente $\ket{1}$.

Operaciones controladas

Q# hace que sea extremadamente fácil condicionar la ejecución de una operación a uno o varios cúbits de control. En general, simplemente se antepone Controlled a la llamada, y los argumentos de la operación cambian de la siguiente manera:

Op(<normal args>) $\to$ Controlled Op([<control qubits>], (<normal args>)).

Tenga en cuenta que los cúbits de control deben proporcionarse como una matriz, aunque se trate de un solo cúbit.

Después de H, las siguientes puertas son las puertas R1 que actúan sobre el primer cúbit (y están controladas por el segundo y tercero):


Circuit diagram for three qubit QFT through first qubit

Puede llamar a estos métodos con:

            Controlled R1([qs[1]], (PI()/2.0, qs[0]));
            Controlled R1([qs[2]], (PI()/4.0, qs[0]));

Observe que se usa la función PI() del espacio de nombres Microsoft.Quantum.Math para definir las rotaciones en términos de radianes pi. Además, los códigos se dividen entre un valor Double (por ejemplo, 2.0) porque dividir por un valor 2 entero daría un error de tipo.

Sugerencia

R1(π/2) y R1(π/4) son equivalentes a las operaciones S y T (también en Microsoft.Quantum.Intrinsic).

Después de aplicar las operaciones H pertinentes y las rotaciones controladas al segundo y tercer cúbit:

            //second qubit:
            H(qs[1]);
            Controlled R1([qs[2]], (PI()/2.0, qs[1]));

            //third qubit:
            H(qs[2]);

Solo hay que aplicar una puerta SWAP para completar el circuito:

            SWAP(qs[2], qs[0]);

Esto es necesario porque la naturaleza de la transformación cuántica de Fourier hace que los cúbits salgan en orden inverso, por lo que los intercambios permiten una integración perfecta de la subrutina en algoritmos de mayor tamaño.

Esto quiere decir que ha terminado de escribir las operaciones en el nivel de cúbit de la transformación cuántica de Fourier en nuestra operación de Q#:

Three qubit quantum Fourier transform circuit diagram

Sin embargo, los cúbits estaban en el estado $\ket{0}$ cuando los asignamos, por lo que debe desasignarlos para restablecer el estado inicial.

Desasignación de cúbits

Vuelva a llamar a DumpMachine() para ver el estado después de la operación y, por último, se aplica ResetAll al registro de cúbits para restablecer nuestros cúbits a $\ket{0}$ antes de completar la operación:

            Message("After:");
            DumpMachine();

            ResetAll(qs);

Requerir que todos los cúbits reasignados se establezcan explícitamente en $\ket{0}$ es una característica básica de Q#, ya que permite que otras operaciones conozcan su estado con precisión cuando comiencen a usar esos mismos cúbits (un recurso escaso). Además, esto asegura que no se entrelazarán con ningún otro cúbit del sistema. Si el restablecimiento no se realiza al final de un bloque de asignación use, puede producirse un error en tiempo de ejecución.

Su archivo de Q# completo tendrá ahora este aspecto:

namespace NamespaceQFT {
    open Microsoft.Quantum.Intrinsic;
    open Microsoft.Quantum.Diagnostics;
    open Microsoft.Quantum.Math;
    open Microsoft.Quantum.Arrays;

    operation Perform3qubitQFT() : Unit {

        use qs = Qubit[3];

        Message("Initial state |000>:");
        DumpMachine();

        //QFT:
        //first qubit:
        H(qs[0]);
        Controlled R1([qs[1]], (PI()/2.0, qs[0]));
        Controlled R1([qs[2]], (PI()/4.0, qs[0]));

        //second qubit:
        H(qs[1]);
        Controlled R1([qs[2]], (PI()/2.0, qs[1]));

        //third qubit:
        H(qs[2]);

        SWAP(qs[2], qs[0]);

        Message("After:");
        DumpMachine();

        ResetAll(qs);

    }
}

Una vez completados el archivo de Q# y la operación, el programa cuántico está listo para la llamada y la simulación.

Ejecución del programa

Como se ha definido la operación de Q# en un archivo .qs, ahora hay que llamar a esa operación y observar los datos clásicos que devuelve. Por ahora, no se devuelve nada (recuerde que nuestra operación definida anteriormente devuelve Unit), pero cuando más adelante modifique la operación de Q# para que devuelva una matriz de resultados de medida (Result[]), nos ocuparemos de esto.

Aunque el programa de Q# está omnipresente en los entornos que se utilizan para llamarlo, la forma de hacerlo variará. Por lo tanto, solo tiene que seguir las instrucciones de la pestaña correspondiente a su configuración: trabajar desde la aplicación de Q# o utilizar un programa host en Python o C#.

Ejecutar el programa de Q# desde la línea de comandos solo requiere un pequeño cambio en el archivo de Q#.

Basta con añadir @EntryPoint() a una línea que preceda a la definición de la operación:

    @EntryPoint()
    operation Perform3qubitQFT() : Unit {
        // ...

Para ejecutar el programa, abra el terminal en la carpeta de su proyecto e introduzca lo siguiente:

dotnet run

Una vez completado, verá las salidas siguientes Message y DumpMachine impresas en su consola.

Initial state |000>:
# wave function for qubits with ids (least to most significant): 0;1;2
|0>:     1.000000 +  0.000000 i  ==     ******************** [ 1.000000 ]     --- [  0.00000 rad ]
|1>:     0.000000 +  0.000000 i  ==                          [ 0.000000 ]                   
|2>:     0.000000 +  0.000000 i  ==                          [ 0.000000 ]                   
|3>:     0.000000 +  0.000000 i  ==                          [ 0.000000 ]                   
|4>:     0.000000 +  0.000000 i  ==                          [ 0.000000 ]                   
|5>:     0.000000 +  0.000000 i  ==                          [ 0.000000 ]                   
|6>:     0.000000 +  0.000000 i  ==                          [ 0.000000 ]                   
|7>:     0.000000 +  0.000000 i  ==                          [ 0.000000 ]                   
After:
# wave function for qubits with ids (least to most significant): 0;1;2
|0>:     0.353553 +  0.000000 i  ==     ***                  [ 0.125000 ]     --- [  0.00000 rad ]
|1>:     0.353553 +  0.000000 i  ==     ***                  [ 0.125000 ]     --- [  0.00000 rad ]
|2>:     0.353553 +  0.000000 i  ==     ***                  [ 0.125000 ]     --- [  0.00000 rad ]
|3>:     0.353553 +  0.000000 i  ==     ***                  [ 0.125000 ]     --- [  0.00000 rad ]
|4>:     0.353553 +  0.000000 i  ==     ***                  [ 0.125000 ]     --- [  0.00000 rad ]
|5>:     0.353553 +  0.000000 i  ==     ***                  [ 0.125000 ]     --- [  0.00000 rad ]
|6>:     0.353553 +  0.000000 i  ==     ***                  [ 0.125000 ]     --- [  0.00000 rad ]
|7>:     0.353553 +  0.000000 i  ==     ***                  [ 0.125000 ]     --- [  0.00000 rad ]

Cuando se llama en el simulador de estado completo, DumpMachine() proporciona estas representaciones múltiples de la función de onda del estado cuántico. Los posibles estados de un sistema de $n$ cúbits se pueden representar mediante estados de base computacional $2^n$, cada uno con un coeficiente complejo correspondiente (simplemente una amplitud y una fase). Los estados de base computacional se corresponden con todas las cadenas binarias posibles de longitud $n$; es decir, todas las combinaciones posibles de estados de cúbit $\ket{0}$ y $\ket{1}$, donde cada dígito binario corresponde a un cúbit individual.

La primera fila proporciona un comentario con los identificadores de los cúbits correspondientes en su orden significativo. Que el cúbit 2 sea el "más significativo" simplemente significa que, en la representación binaria del vector de estado base $\ket{i}$, el estado de cúbit 2 corresponde al dígito situado más a la izquierda. Por ejemplo, $\ket{6} = \ket{110}$ se compone de los cúbits 2 y 1, ambos en $\ket{1}$, y del cúbit 0 en $\ket{0}$.

El resto de las filas describen la amplitud de probabilidad de medir el vector de estado base $\ket{i}$ en formato cartesiano y polar. Detalle de la primera fila del estado de entrada $\ket{000}$:

  • |0>: esta fila corresponde al estado de base computacional 0 (como el estado inicial después de la asignación era $\ket{000}$, esperaríamos que este fuera el único estado con amplitud de probabilidad en este momento).
  • 1.000000 + 0.000000 i : amplitud de probabilidad en formato cartesiano.
  • == : el signo equal separa ambas representaciones equivalentes.
  • ******************** : representación gráfica de la magnitud; el número de * es proporcional a la probabilidad de medir este vector de estado.
  • [ 1.000000 ] : valor numérico de la magnitud.
  • --- : representación gráfica de la fase de la amplitud.
  • [ 0.0000 rad ] : valor numérico de la fase (en radianes).

Tanto la magnitud como la fase se muestran con una representación gráfica. La representación de la magnitud es sencilla: muestra una barra de * y, cuanto mayor sea la probabilidad, mayor será la barra. Para más información sobre la fase, consulte Prueba y depuración: funciones de volcado para las posibles representaciones de los símbolos en función de los intervalos angulares.

Por lo tanto, la salida impresa ilustra que nuestras puertas programadas transformaron nuestro estado de

$$ \ket{\psi}_{initial} = \ket{000} $$

to

$$ \begin{align} \ket{\psi}_{final} &= \frac{1}{\sqrt{8}} \left( \ket{000} + \ket{001} + \ket{010} + \ket{011} + \ket{100} + \ket{101} + \ket{110} + \ket{111} \right) \\ &= \frac{1}{\sqrt{2^n}}\sum_{j=0}^{2^n-1} \ket{j}, \end{align} $$

que es precisamente el comportamiento de la transformación de Fourier de tres cúbits.

Si tiene curiosidad sobre cómo se ven afectados otros estados de entrada, le recomendamos que pruebe a aplicar operaciones de cúbits antes de la transformación.

Adición de medidas

Lamentablemente, una piedra angular de la mecánica cuántica nos dice que un sistema cuántico real no puede tener esta función DumpMachine. En su lugar, la información se extrae mediante medidas que, por lo general, no solo no proporcionan el estado cuántico completo, sino que también pueden modificar drásticamente el propio sistema. Hay muchos tipos de medidas cuánticas, pero nos centraremos en la más básica: la medida de proyección en cúbits únicos. Tras medir en una base determinada (por ejemplo, la base de cálculo $ { \ket{0}, \ket{1} } $), el estado del cúbit se proyecta en el estado base que se midió, lo que destruye cualquier superposición entre los dos.

Para implementar la medida dentro de un programa de Q#, se usa la operación M (de Microsoft.Quantum.Intrinsic), que devuelve un tipo Result.

En primer lugar, modifique la operación Perform3QubitQFT para devolver una matriz de resultados de medida, Result[], en lugar de Unit.

    operation Perform3QubitQFT() : Result[] {

Definición e inicialización de la matriz Result[]

Antes incluso de asignar cúbits (por ejemplo, antes de la instrucción use), declare y enlace esta matriz de longitud 3 (un Result para cada cúbit):

        mutable resultArray = new Result[3];

La palabra clave mutable que precede a resultArray permite que la variable se vuelva a enlazar más adelante en el código; por ejemplo, al agregar los resultados de la medida.

Medida en un bucle for y adición de los resultados a la matriz

Después de las operaciones de transformación de Fourier, inserte el código siguiente:

            for i in IndexRange(qs) {
                set resultArray w/= i <- M(qs[i]);
            }

La función IndexRange a la que se llama en una matriz (por ejemplo, nuestra matriz de cúbits, qs) devuelve un intervalo de índices de la matriz. Aquí, se usa en el bucle for para medir secuencialmente cada cúbit con la instrucción M(qs[i]). A continuación, cada tipo de Result medido (Zero o One) se agrega a la posición del índice correspondiente en resultArray con una instrucción de actualización y reasignación.

Nota

La sintaxis de esta instrucción es única para Q#, pero corresponde a la reasignación de variables similar resultArray[i] <- M(qs[i]) que se ve en otros lenguajes, como F# y R.

La palabra clave set siempre se usa para reasignar variables enlazadas mediante mutable.

Devuelve resultArray.

Con los tres bits cúbits medidos y los resultados agregados a resultArray, es seguro restablecer y desasignar los cúbits como antes. Para devolver las medidas, inserte:

        return resultArray;

Descripción de los efectos de la medida

Vamos a cambiar la ubicación de nuestras funciones DumpMachine para generar el estado antes y después de las medidas. El código final de la operación tendrá el siguiente aspecto:

    operation Perform3QubitQFT() : Result[] {

        mutable resultArray = new Result[3];

        use qs = Qubit[3];

        //QFT:
        //first qubit:
        H(qs[0]);
        Controlled R1([qs[1]], (PI()/2.0, qs[0]));
        Controlled R1([qs[2]], (PI()/4.0, qs[0]));

        //second qubit:
        H(qs[1]);
        Controlled R1([qs[2]], (PI()/2.0, qs[1]));

        //third qubit:
        H(qs[2]);

        SWAP(qs[2], qs[0]);

        Message("Before measurement: ");
        DumpMachine();

        for i in IndexRange(qs) {
            set resultArray w/= i <- M(qs[i]);
        }

        Message("After measurement: ");
        DumpMachine();

        ResetAll(qs);

        return resultArray;

    }

Si trabaja desde el símbolo del sistema, la matriz devuelta se mostrará directamente en la consola al final de la ejecución. De lo contrario, actualice el programa host para procesar la matriz devuelta.

Para comprender mejor la matriz devuelta que se imprimirá en la consola, puede agregar otro Message en el archivo de Q# justo antes de la instrucción return:

        Message("Post-QFT measurement results [qubit0, qubit1, qubit2]: ");
        return resultArray;

Ejecute el proyecto; la salida será similar al siguiente ejemplo:

Before measurement: 
# wave function for qubits with ids (least to most significant): 0;1;2
|0>:     0.353553 +  0.000000 i  ==     ***                  [ 0.125000 ]     --- [  0.00000 rad ]
|1>:     0.353553 +  0.000000 i  ==     ***                  [ 0.125000 ]     --- [  0.00000 rad ]
|2>:     0.353553 +  0.000000 i  ==     ***                  [ 0.125000 ]     --- [  0.00000 rad ]
|3>:     0.353553 +  0.000000 i  ==     ***                  [ 0.125000 ]     --- [  0.00000 rad ]
|4>:     0.353553 +  0.000000 i  ==     ***                  [ 0.125000 ]     --- [  0.00000 rad ]
|5>:     0.353553 +  0.000000 i  ==     ***                  [ 0.125000 ]     --- [  0.00000 rad ]
|6>:     0.353553 +  0.000000 i  ==     ***                  [ 0.125000 ]     --- [  0.00000 rad ]
|7>:     0.353553 +  0.000000 i  ==     ***                  [ 0.125000 ]     --- [  0.00000 rad ]
After measurement:
# wave function for qubits with ids (least to most significant): 0;1;2
|0>:     0.000000 +  0.000000 i  ==                          [ 0.000000 ]
|1>:     0.000000 +  0.000000 i  ==                          [ 0.000000 ]
|2>:     0.000000 +  0.000000 i  ==                          [ 0.000000 ]
|3>:     1.000000 +  0.000000 i  ==     ******************** [ 1.000000 ]     --- [  0.00000 rad ]
|4>:     0.000000 +  0.000000 i  ==                          [ 0.000000 ]
|5>:     0.000000 +  0.000000 i  ==                          [ 0.000000 ]
|6>:     0.000000 +  0.000000 i  ==                          [ 0.000000 ]
|7>:     0.000000 +  0.000000 i  ==                          [ 0.000000 ]

Post-QFT measurement results [qubit0, qubit1, qubit2]: 
[One,One,Zero]

Esta salida muestra algunas cosas diferentes:

  1. Al comparar el resultado devuelto con la medida previa DumpMachine, no se ilustra claramente la superposición posterior de QFT sobre los estados base. Una medida solo devuelve un estado base único, con una probabilidad determinada por la amplitud de ese estado en la función de onda del sistema.
  2. A partir de la medida posterior DumpMachine, vemos que la medida cambia el estado en sí y lo proyecta desde la superposición inicial sobre los estados base al estado base único que corresponde al valor medido.

Si esta operación se repite muchas veces, las estadísticas de los resultados comienzan a ilustrar la superposición igualmente ponderada del estado posterior a QFT que da lugar a un resultado aleatorio en cada toma. Sin embargo, además de ser un método ineficaz e imperfecto, solo reproduciría las amplitudes relativas de los estados base, no las fases relativas entre ellos. Esto último no es un problema en este ejemplo, pero vería que aparecen fases relativas si se da una entrada más compleja a QFT que $\ket{000}$.

Medidas parciales

Para explorar cómo la medida de solo algunos cúbits del registro puede afectar al estado del sistema, intente agregar lo siguiente dentro del bucle for, después de la línea de medida:

                let iString = IntAsString(i);
                Message("After measurement of qubit " + iString + ":");
                DumpMachine();

Tenga en cuenta que, para acceder a la función IntAsString, tendrá que agregar

    open Microsoft.Quantum.Convert;

con el resto de las instrucciones open de espacio de nombres.

En la salida resultante, verá la proyección gradual en subespacios cuando se vaya midiendo cada cúbit.

Uso de las bibliotecas deQ#

Tal y como se mencionó en la introducción, gran parte de la potencia de Q# se debe al hecho de que permite olvidarse de las preocupaciones de tener que tratar con cúbits individuales. De hecho, si desea desarrollar programas cuánticos aplicables a escala completa, tener que preocuparse de si una operación H va antes o después de una rotación determinada es un lastre.

Las bibliotecas de Q# contienen la operación QFT, que puede tomar y aplicar a cualquier número de cúbits. Para probarlo, defina una nueva operación en su archivo de Q# que tenga el mismo contenido que Perform3QubitQFT, pero reemplazando todo desde la primera operación H hasta SWAP por dos sencillas líneas:

            let register = BigEndian(qs);    //from Microsoft.Quantum.Arithmetic
            QFT(register);                   //from Microsoft.Quantum.Canon

La primera línea simplemente crea una expresión BigEndian de la matriz de cúbits asignada, qs, que es lo que la operación QFT toma como argumento. Esto corresponde a la ordenación del índice de los cúbits en el registro.

Para tener acceso a estas operaciones, agregue instrucciones open para los respectivos espacios de nombres al principio del archivo de Q#:

    open Microsoft.Quantum.Canon;
    open Microsoft.Quantum.Arithmetic;

Ahora, ajuste el programa host para llamar al nombre de la nueva operación (por ejemplo, PerformIntrinsicQFT) y darle una vuelta.

Para ver la ventaja real de usar las operaciones de la biblioteca de Q#, cambie el número de cúbits a otro que no sea 3:

        mutable resultArray = new Result[4];

        use qs = Qubit[4];
        //...

Así puede aplicar la operación QFT adecuada para un número determinado de cúbits sin tener que preocuparse por el desorden de las nuevas operaciones y rotaciones de H en cada cúbit.

Tenga en cuenta que el simulador cuántico tarda exponencialmente más tiempo en ejecutarse a medida que aumenta el número de cúbits, y precisamente por eso esperamos con interés el hardware cuántico real.