Skip to content

Quantum Types

Once initialized, Qmod variables reference a quantum object in some state. Quantum types determine the overall number of qubits used to store the object, as well as the interpretation of its state. For example, a quantum object stored on 4 qubits can represent an array of 4 bits, an integer number in the domain 0 to 15, or an array of two fixed-point numbers in the domain [-1.0, -0.5, 0, -0.5]. The type determines which interpretation is the intended one, for example, when evaluating quantum operators.

Qmod has two categories of quantum scalar types - bits and numbers. Qmod also supports quantum array and struct types, which can be arbitrarily nested.

Quantum Scalar Types

In Qmod, there are two kinds of scalar quantum types:

  • qbit represents the states \(|0\rangle\), \(|1\rangle\), or a superposition of the two
  • qnum represents numbers in some discrete domain - integers or fixed-point reals

When declaring a qnum variable, you can optionally specify its numeric attributes - overall size in bits, whether it is signed, and the number of binary fraction digits.

Syntax

qbit

qnum [ < size-int-expr [ , sign-bool-expr , frac-digits-int-expr ] > ]

In Python the classes QBit and QNum are used as type hints in the declaration of arguments:

name : QBit

name : QNum [ [ size-int-expr [ , sign-bool-expr , frac-digits-int-expr ] ] ]

The same classes are used to declare local variables:

name = QBit ( " local_name " )

name = QBit ( " name " , [ [ size = ] size-int-expr , [ [ is_signed = ] sign-bool-expr , [ [ fraction_digits = ] frac-digits-int-expr ] )

It is recommended to use the SIGNED and UNSIGNED built-in constants instead of True and False respectively when specifying the sign-bool-expr qnum property.

Semantics

  • Computational-basis encoding of numeric types is big-endian (the most significant bit has the highest index).
  • size-int-expr determines the overall number of qubits used to store the number, including sign and fraction where applicable.
  • If sign-bool-expr is True (SIGNED), two's complement is used to represent signed numbers, utilizing the most-significant bit for sign.
  • frac-digits-int-expr determines the number of least-significant bits representing binary fraction digits.
  • When only size-int-expr is specified and sign-bool-expr and frac-digits-int-expr are left out, the later two are set to UNSIGNED and 0 (integer) respectively.

Examples

In the following example, two 4-qubit numeric variables, x and y, are prepared to store the bit string 1101. x is declared with no sign bit and no fraction digits, and therefore its state represents the number 13. y is declared to be signed and have one fraction-digit, and thus, the same bit-level state represents the number -1.5.

qfunc prepare_1101(output qba: qbit[]) {
  allocate(4, qba);
  X(qba[0]);
  X(qba[2]);
  X(qba[3]);
}

qfunc main(output x: qnum<4>, output y: qnum<4, SIGNED, 1>) {
  prepare_1101(x);
  prepare_1101(y);
}
from classiq import qfunc, Output, QNum, QArray, allocate, QBit, SIGNED, X


@qfunc
def prepare_1101(qba: Output[QArray[QBit]]):
    allocate(4, qba)
    X(qba[0])
    X(qba[2])
    X(qba[3])


@qfunc
def main(x: Output[QNum[4]], y: Output[QNum[4, SIGNED, 1]]):
    prepare_1101(x)
    prepare_1101(y)

Numeric Inference Rules

Numeric representation modifiers are optional in the declaration. When left out, the representation attributes of a qnum variable are determined upon its first initialization. Following are the inference rules for these cases:

  • When the variable is passed to a function as its output argument with declared type qbit[], the size is determined by the actual array size, while sign and fraction-digits default to False (UNSIGNED) and 0 respectively. Specifically, this is the rule when using allocate.
  • When the variable is passed to a function as its output argument with declared type qnum, the size is determined by the actual size, sign, and fraction-digits of the function's output.
  • When the variable is initialized on the left of an out-of-place assignment =, the domain of the expression determines its representation properties.
  • Variables retain their type, including the representation attributes, even after being un-initialized (for example, when occurring on the left side of a bind statement). Subsequent initializations must agree with the specific qnum type.
  • On the right side of a bind statement (->) the representation attributes of qnum variables must already be known either through declaration, or by previous initialization (and subsequent un-initialization).

Examples

The following example demonstrates the default and explicit numeric interpretation of quantum states. Two variables, a and b, are initialized to some quantum state. a is left with the default unsigned integer interpretation. b is initialized to a superposition of the bit strings 01 and 10 interpreted with a sign bit and one fraction digit. This implies that its domain is [-1.0, -0.5, 0, 0.5] and its value is in a superposition of -1.0 and 0.5. res is accordingly uniformly distributed on the 8 possible addition values.

qfunc main(output a: qnum, output b: qnum<2, SIGNED, 1>, output res: qnum) {
  allocate(2, a);
  hadamard_transform(a);
  prepare_state([0, 0.5, 0.5, 0], 0, b);
  res = a + b;
}
from classiq import *


@qfunc
def main(a: Output[QNum], b: Output[QNum[2, SIGNED, 1]], res: Output[QNum]) -> None:
    allocate(2, a)  # 'a' is a 3 qubit unsigned int in the domain [0, 1, 2, 3]
    hadamard_transform(a)  # 'a' is in a superposition of all values in its domain
    prepare_state([0, 0.5, 0.5, 0], 0, b)  # 'b' is in superposition of 01 and 10
    res |= a + b

Allocate-num

To allocate a qnum in the zero state with a specific numeric interpretation, use the function allocate_num().

qfunc allocate_num<num_qubits: int, is_signed: bool, fraction_digits: int>(output out: qnum<num_qubits, is_signed, fraction_digits>);
def allocate_num(
    num_qubits: CInt,
    is_signed: CBool,
    fraction_digits: CInt,
    out: Output[QNum["num_qubits", "is_signed", "fraction_digits"]],
) -> None:
    pass

Quantum arrays

A quantum array is an object that supports indexed access to parts of its state - its elements. Elements are interpreted as values of the specified array element type. A quantum array is one object with respect to its lifetime. Elements of an array cannot be initialized separately or bound separately to other variables. Also, an array's length (the number of elements it represents) is fixed at the time of its initialization and remains constant throughout its lifetime.

Syntax

element-type [ [ _length-expr- ]

In Python the class QArray is used as type hints in the declaration of arguments:

name : QArray [ [ element-type [ , length-expr ] ] ]

The same class is used to declare local variables:

name = QArray ( " name " [ , [ element_type =** ] element-type ][ **,** [ **length =** ] length-expr ] )**

Semantics

  • element_type optionally determines the type of the array elements. Arrays are homogenous, that is, all elements are of the same type. When left unspecified, the type defaults to qbit.
  • length-expr optionally determines the number of elements in the array. The overall size of the array is its length multiplied by the size of the element type. When the length is unspecified, it is determined upon initialization based on the element size. Similarly, when the size of the element type is not specified, it is inferred upon initialization based on the length. Either the length, or the size of the element type, must be specified in the declaration
  • The length cannot change throughout the lifetime of an array.

Expressions of quantum array type support the following operations:

  • Subscript: array-expression [ index-expression ]
  • Slice: array-expression [ from-index-expression : to-index-expression ]
  • Length: array-expression . len

Examples

In the following example, a Boolean expression of a 3-SAT formula is evaluated over the elements of a qubit array, which is prepared in the state of uniform superposition. Note that bitwise operators are used in this case, but equivalent logical operators and, or, and not (and their respective Python counterparts in package qmod.symbolic) are also supported.

qfunc main(output x: qbit[3], output res: qbit) {
  allocate(3, x);
  hadamard_transform(x);
  res = (x[0] | ~x[1] | ~x[2]) & (~x[0] | x[1] | ~x[2]);
}
from classiq import qfunc, Output, QArray, QBit, hadamard_transform, bind


@qfunc
def main(x: Output[QArray[QBit, 3]], res: Output[QBit]) -> None:
    allocate(3, x)
    hadamard_transform(x)
    res |= (x[0] | ~x[1] | ~x[2]) & (~x[0] | x[1] | ~x[2])

The next example demonstrates the initialization of a numeric array using the bind statement (->). Two numeric variables are declared and initialized separately and subsequently bound together to initialize the array. The declared type of these variables is an unsigned integer, but the declared element type of the array is signed. Hence,
the values 6 and 7 are interpreted as -2 and -1, respectively. When executing the resulting quantum program, res is sampled with the value -3 (with probability 1).

qfunc main(output res: qnum) {
  n0: qnum<3>;
  n0 = 6;
  n1: qnum<3>;
  n1 = 7;
  n_arr: qnum<3, SIGNED, 0>[];
  {n0, n1} -> n_arr;
  res = n_arr[0] + n_arr[1];
}
from classiq import qfunc, Output, QArray, QNum, SIGNED, bind


@qfunc
def main(res: Output[QNum]) -> None:
    n0 = QNum("n0", 3)
    n0 |= 6
    n1 = QNum("n1", 3)
    n1 |= 7

    n_arr = QArray("n_arr", QNum[3, SIGNED, 0])
    bind([n0, n1], n_arr)

    res |= n_arr[0] + n_arr[1]

Quantum structs

A quantum struct is an object that supports named access to parts of its state - its fields. Each field corresponds to a slice of the overall object, interpreted according to its declared type. A quantum struct is one object with respect to its lifetime. Fields of a struct cannot be initialized separately or bound separately to other variables.

Quantum structs are typically used to pack and unpack multiple variables, that is, to switch between contexts that treat the object in a generic way (as a qubit array) and in a problem-specific way (to capture expressions over fields).

Syntax

The following syntax is used to define a quantum struct type -

qstruct name { field_declarations }

field-declarations is a list of one or more field declarations in the form - name : quantum-type ;.

A quantum struct type in Python is defined using a Python class derived from the class QStruct. Fields are declared with type hints, similar to how member variables are declared in a Python dataclass.

Semantics

  • Only quantum types are allowed as field types in a quantum struct.
  • Quantum structs may be arbitrarily nested, that is, a field of a struct may itself be a struct or a struct array. However, recursive struct types are not allowed.
  • The overall size of a struct (the number of qubits used to store it) must be known upon declaration. This means that the size of all fields, except at most one, must be fully specified.

Expressions of quantum struct type support field-access operation in the form - struct-expression . field-name.

Examples

In the following example, quantum struct MyQStruct is defined and subsequently initialized and prepared in a specific state in function main.

qstruct MyQStruct {
  a: qbit;
  b: qnum;
}

qfunc main(output s: MyQStruct) {
  allocate(4, s);
  H(s.a);
  inplace_prepare_int(6, s.b);
}
from classiq import qfunc, Output, QBit, allocate, QStruct, QNum, inplace_prepare_int, H


class MyQStruct(QStruct):
    a: QBit
    b: QNum


@qfunc
def main(s: Output[MyQStruct]) -> None:
    allocate(4, s)
    H(s.a)
    inplace_prepare_int(6, s.b)

The example below demonstrates the common situation where an algorithm alternates between the two views of a quantum state - the structured view with partition into problem variables, and the unstructured view as an array of qubits. The example defines a constraint over two variables, a and b, of different numeric types. It uses Grover-search to find a solution. In the Grover-search algorithm, encapsulated by the function grover_search, the oracle application uses the structured view of the state to evaluate the constraint, while the diffuser is defined in a generic way and uses the qubit array view of the state.

qstruct MyProblem {
  a: qnum<2, UNSIGNED, 2>;
  b: qnum<3, UNSIGNED, 3>;
}

qfunc my_problem_constraint(p: MyProblem, res: qbit) {
  res ^= (p.a + p.b) == 0.625;
}

qfunc main(output p: MyProblem) {
  allocate(5, p);
  grover_search(2, lambda(p) {
    phase_oracle(my_problem_constraint, p);
  }, p);
}
from classiq import *


class MyProblem(QStruct):
    a: QNum[2, UNSIGNED, 2]
    b: QNum[3, UNSIGNED, 3]


@qfunc
def my_problem_constraint(p: MyProblem, res: QBit) -> None:
    res ^= p.a + p.b == 0.625


@qfunc
def main(p: Output[MyProblem]) -> None:
    allocate(5, p)
    grover_search(2, lambda p: phase_oracle(my_problem_constraint, p), p)

Executing this model will sample a state representing a solution to the problem in very high probability. This is an example of an output. Here is an output example:

state={'p': {'a': 0.0, 'b': 0.625}} shots=350
state={'p': {'a': 0.25, 'b': 0.375}} shots=344
state={'p': {'a': 0.5, 'b': 0.125}} shots=306