第2章 QRunes基本类型与变量

2.1 QRunes的基本对象类型

2.1.1 量子类型 Quantum Type

量子类型的对象描述的是量子芯片上的量子比特。

例如:qubit q;

它在运行期间会映射某一个量子芯片上的量子比特。这种对象只能被最终用于量子逻辑门操作中。它的赋值相当于创建这个映射的别名,并非描述其中的数据的复制。

2.1.2 量子线路类型 Quantum Circuit Type

量子线路,也称量子逻辑电路是最常用的通用量子计算模型,表示在抽象概念下,对于量子比特进行操作的线路,是各种逻辑门组成的集合。最后常需要量子测量将结果读取出来。不同于传统电路是用金属线所连接以传递电压讯号或电流讯号,在量子线路中,线路是由时间所连接,亦即量子比特的状态随着时间自然演化,过程中是按照哈密顿运算符的指示,一直到遇上逻辑门而被操作。由于组成量子线路的每一个量子逻辑门都是一个 酉矩阵 ,所以整个量子线路整体也是一个大的酉矩阵。 首先,初始化一个量子线路对象有以下两种风格:

C++风格

circuit cir;

C语言风格(主要有两种方式)

//方式1
circuit cir = Createqc(); //Createqc is the initialization method()

//方式2
circuit qc2 =  qc1;  //qc1 has already defined

dagger的作用是复制一份当前的量子线路,并更新复制的量子线路的dagger标记。举个例子:

circuit cir;
circuit cir_dagger = cir.dagger();

除了转置共轭操作,您也可以为量子线路添加控制比特。circuit类型为您内置了两个成员函数用于添加控制比特:control、setControl。

control的作用是复制当前的量子线路,并给复制的量子线路添加控制比特,例如:

circuit cir;
circuit cir_control = cir.control(qvec);

上述都需要接收一个参数,参数类型为QVec,QVec是qubit的vector容器类型。我们将在下一部分仔细介绍。

2.1.3 数组类型 Array Construct Type

2.1.3.1 类型声明

vector是同一种类型的对象的集合,每个对象都有一个对应的整数索引值(对象和变量的区分和C++类似)。在QRunes中,系统将负责管理与存储元素相关的内存。和C++一样,因为它可以包含其他对象,我们也可以将这一类型称为容器。必须明确的是,一个容器中的所有对象都必须是同一类型的。在QRunes中,容器的内置类型只支持量子类型、辅助类型和经典类型。使用容器可以写一个声明或函数定义,从而用于多个不同的数据类型。因此,我们可以定义保存经典类型对象的容器,或保存辅助类型对象的容器。

声明数组类型的对象,需要提供附加信息。详细的说,就是要提供具体的类型,即容器保存的是何种对象的类型,通过将类型放在名称后面的尖括号中来指定类型,如:

vector <int> ives;     //  ivec holds objects of type auxiliary primary type
vector <cbit> cvec;   //  cvec holds objects of classical primary type

和C++类似,定义数组类型对象需要指定类型和一个变量的列表。上面的第一个定义,类型是vector <int>,该类型即是含有若干int型对象的vector,变量名为ivec。第二个定义的变量名是cvec,它所保存的元素都是经典类型的对象。我们需要注意的是,数组类型为我们提供了一套类似于C++中的类模板机制,我们可以用它来定义多种数据类型。数组类型的内置类型的每一种都指定了其保存元素的类型。因此,我们可以简单地认为上述两个例子都是一种数据类型。

2.1.3.2 数组类型的操作

切片(slice)是对数组一个连续片段的引用,因此切片是一个引用类型。切片的内部结构包含开始位置地址(&)、大小(length)和容量(cap)。切片并不存储任何数据,它只是描述了底层数组中的一段。更改切片的元素会修改其底层数组中对应的元素。

切片的使用和Python类似,由数组生成新的切片主要通过两个下标来界定,即一个上界和一个下界,二者以冒号分隔,它的一般形式为:

a[low : high]

以下为切片的内部结构的详细含义

地址:即切片创建时指向一个底层数组元素的指针

长度:即切片内部元素数量,可以用length求得

容量:当长度大于容量时,成倍增长

在QRunes中,切片的上界和下界必须指定,不允许使用默认值操作。不难看出,从数组生成新的切片拥有以下特性:

(1)取出的元素数量为:结束位置-开始位置

(2)取出元素不包含结束位置对应的索引,切片最后一个元素使用 slice[length(slice)] 获取

(3)上界下界同时为0时,等效于空切片,一般用于切片复位

2.1.4 辅助类型 Auxiliary Type

辅助类型是为了更方便创建量子操作的辅助对象。它在编译后的程序中不存在。它可以用于描述一些用于决定量子程序构造的变量或者常量。它也可以是一个编译期间的if判断或者for循环。

对于一组qubit,例如vector<qubit> qs,我们要创建作用在它们上面的Hadamard门,我们可以利用如下语句:

for (i = 0: 1: qs.length()) {
    H(qs[i]);
}

这一组语句是一个典型的for循环,但是执行这个程序的时机是在编译期间,因此这个for循环并不是在量子计算机中运行的for循环。它的效果相当于全部展开。即:

H(q[0]);
H(q[1]);
H(q[2]);
...

2.1.5 经典类型 Classical Type

经典类型是在量子测控系统中存在的对象。他们的创建、计算、管理都是由量子芯片的测控系统完成的。这个系统具有实时性的特点,因此这些变量的生命周期和qubit的退相干时间共存。它是为了解决普通的宿主机和量子芯片之间无法进行实时数据交换的问题而存在的。简而言之,它们介于宿主机(辅助类型)和量子芯片(量子类型)之间。

经典类型的变量典型地可以被用于保存量子比特的测量结果。除此之外,由测量结果决定的IF和WHILE操作,即后面会提到的QIF,QWHILE操作也是在测控系统中完成的,所以也属于经典类型。要注意到QIF和QWHILE和宿主机(辅助类型)的if,for,while等操作具有完全不同的运行时机,其中辅助类型的变量、表达式、语句等是编译期间计算的,经典类型是运行期间计算的。

例如:

cbit c;
qubit q;
H(q);
measure(q,c);
qif(c){
    // do something...
}

这个程序就根据一个qubit在执行完Hadamard门之后进行的测量的结果来选择执行分支。注意到c是一个在测控系统中存在的变量,而qif的判断也是在这个系统中实时完成的,之间与宿主机不会发生数据传输。

经典变量之间还可以进行计算,比如:

qif(!c) {} // 对c求非
qif(c1 == c2) {} //比较c1与c2的值
qif(c1 == True) {} //等价于qif(c1)

但是经典辅助的if中是绝对不允许存在经典类型的变量的,原因是辅助类型的值是要求编译期间能够完全确定的,例如:

if(c) {} // Error:编译期间无法判断c的值

2.1.6 量子程序类型 Quantum Prog Type

量子程序类型一般用于量子程序的编写与构造,在这里,我们可以简单的理解为一个操作序列。由于量子算法中也会包含经典计算,因而业界设想,最近将来出现的量子计算机是基于混合结构的,它包含两大部分:经典计算机和量子设备。经典计算机负责执行经典计算与控制;量子设备负责执行量子计算。

量子程序类型是量子编程的一个容器类,是一个量子程序的最高单位,初始化一个空的QProg对象有以下两种:

C++风格

qprog prog;

C语言风格

qprog prog = CreateEmptyQProg();

2.1.7 函数回调类型 Callback Construct Type

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。通俗的理解就是,程序并没有调用自己定义的函数,但是在某个特定的条件下,函数却执行了(笔者的理解)。需要注意的是,如果函数回调需要传参,我们可以有两种方法避免发生错误。

方法1:(代码为伪码描述)

将回调函数的参数作为与回调函数同等级的参数进行传递,比如:

circuit<int value> fun{
 value++;
}
circuit<some function, value> exe{
 some function(value);
}
exe(fun,c);//c is the parameter required for fun method

方法2:(代码为伪码描述)

将回调函数的参数在调用回调函数内部创建,比如:

circuit<int value> fun{
 value++;
}
circuit<some function> exe{
value={.....}; // what in the {} is custom method
some function(value);
}
exe(fun);

不难看出,函数回调类型支持上述所有类型。 最后笔者给出一个上述所有类型使用的程序,如下所示。有兴趣的读者可在Qpanda中运行并查看结果。

int main(void)
{
  init();
  auto qvec = qAllocMany(4);
  auto cbits = cAllocMany(4);
  auto circuit = CreateEmptyCircuit();

  circuit << H(qvec[0]) << CNOT(qvec[0], qvec[1])
          << CNOT(qvec[1], qvec[2]) << CNOT(qvec[2], qvec[3]);
  circuit.setDagger(true);
  auto prog = CreateEmptyQProg();
  prog << H(qvec[3]) << circuit << Measure(qvec[0], cbits[0]);

  auto result = runWithConfiguration(prog, cbits, 1000);
  for (auto &val : result)
  {
      std::cout << val.first << ", " << val.second << std::endl;
  }

  finalize();
  return 0;
}

2.2 字面值常量

像42这样的值,在经典程序中被当作字面值常量(literal constant)。称之为字面值常量是因为只能用它的值称呼它,称之为常量是因为它的值不能修改。每个字面值都有相应的类型,在QRunes中,支持整型、浮点型和布尔型。只有内置类型存在字面值。

2.2.1整型字面值规则

在QRunes中定义字面值整数常量默认使用十进制,整型常量在底层都会以二进制形式表示。例如,我们将值25定义为整型常量:

25     //decimal

字面值整数常量的类型默认为int类型。它的表示范围是-32768~32767。

2.2.2浮点字面值规则

在QRunes中定义字面值浮点常量默认使用十进制。例如,我们将值 3.14159265358979定义为浮点常量:

3.14159265358979     //the default value of Pi

2.2.3 布尔字面值

单词true和false都是布尔型的字面值。

2.3 变量

2.3.1 什么是变量

QRunes是一门静态类型语言,在编译时会作类型检查。和大多数语言一样,对象的类型限制了对象可以执行的操作。如果某种类型不支持某种操作,那么这种类型的对象也就不能执行该操作。在QRunes中,操作是否合法是在编译时检查的。当编写表达式时,编译器检查表达式中的对象是否按该对象的类型定义的使用方式使用。如果不是的话,那么编译器会提示错误,而不产生可执行文件。随着程序和使用的类型变得越来越复杂,我们将看到静态类型检查能帮助我们更早地发现错误.静态类型检查使得编译器必须能识别程序中的每个实体的类型。因此,QRunes使用变量前必须先定义变量的类型. 首先我们看一下什么是变量,和传统编程语言一样,变量提供了程序可以操作的有名字的存储区。QRunes中的每一个变量都有特定的类型,该类型决定了变量的内存大小和布局、能够存储于该内存中的值的取值范围以及可应用在该变量上的操作集。我们常常把变量称为“变量”或“对象(object)”。 说到变量,难免要说到左值和右值,我们将在第3章详细探讨表达式,现在首先简单介绍一下QRunes中的两种表达式:


(1)左值(Ivalue):左值可以出现在赋值语句的左边或右边。

(2)右值(rvalue);右值只能出现在赋值的右边,不能出现在赋位语句的左边。

变量是左值,因此可以出现在赋值语句的左边。数字字面值是右值,因此不能被赋值。给定以下变量:

let a=25;
let b=3.2526;

下面两条语句会产生编译错误:

a*a=b; //error: arithmetic expression is not an lvalue
0=1;  //error: lieral comstant is not an lvalue

这一部分将会在表达式章节详细介绍,此处便不再赘述。

2.3.2 变量名

变量名,即变量的标识符(identifier),可以由字母、数字组成。变量名必须以字母开头,并且严格区分大小写字母:QRunes中的标识符都是大小写敏感的。下面例出了三个不同的标识符:

//three different variables
somename,someName,someName

在QRunes中并没有限制变量名的长度,但考虑到将会阅读(和|或)修改我们的代码的其他人,变量名不应太长。

2.3.3 关键字

QRunes中保留了一组词用作改语言的关键字。关键字不能用作改语言的标识符。下面列出了所有的关键字:

let qubit X1
include cbit Y1
int circuit Z1
bool qprog U4
if variationalCircuit RX
else hamiltonian RY
for VQG_NOT RZ
lib VQG_RZ CNOT
qrunes VQG_RX CZ
avar H CR
double X CU
default NOT isWAP
in T measure
vector S qif
Pi Y qwhile
return Z qelse
lambda while  

2.3.4 变量命名习惯

变量命名有很多被普遍接受的习惯,遵循这些习惯可以提高程序的可读性。

  1. 变量名一般用小写字母。
  2. 标识符应使用能帮助记忆的名字,也就是说,能够提示其在程序中的用法的名字,如salary.

2.3.5 变量的定义

变量的定义分为两个部分来说明:

1.形参变量

形参变量,只做变量声明,由传递函数的实参进行初始化,作用域为所在函数体内,当函数结束的时候,形参即被销毁。 形参变量的格式: 变量类型 变量名 当前QRunes支持的形参变量类型有:

int hamiltionian
double avar
bool circuit
map callback_type
qubit  
cbit  
vector_type  

hamiltionian类型是哈密顿量类型数据,它是一种复合类型。

avar是可变参数类型。

vector_type是数组类型的数据,具体的参数类型需要在泛型中确定。 例如:vector<qubit>表示qubit类型的数组。

callback_type是回调函数类型,由 返回类型<参数> 组成。 例如:

circuit unitary(vector<qubit> q) {
    RX(q[0], -Pi);
}

//qc为返回类型为circuit类型,参数类型为vector<qubit>的回调函数类型
circuit unitarypower(vector<qubit> q, int min, circuit<vector<qubit>> qc) {
    for (let i=0: 1: (1 << min)) {
        qc(q);
    }
}

unitarypower(q, min, unitary)  //函数的调用,callback参数类型只需传入所需调用的函数名

2.变量

在QRunes中变量的定义分为三部分来说明:

a.量子类型的变量。

格式:量子类型 变量名 比如:

qubit q; => q = allocMany(1);
vector<qubit> qvec;

b.经典辅助类型的变量。

格式:let 变量名 = 初始值 在辅助类型中的let关键字作用是定义并初始化辅助类型的变量。(占位符也是自动类型推断)。 其中变量的类型由量子编译器根据初始值来推断确定变量的类型。 这样做的好处:

1).简化量子编程的编程操作,并使代码简介。(凡是辅助类型的变量直接用let关键字来定义)

2).let关键字涉及的行为只在编译期间,而不是运行期间。

注意:

1).let 关键字定义的变量必须有初始值。

let a; //ERROR
let a = 3.14; //CORRECT

2).函数参数不可以被声明为 let。

ker(qubit q, let a){ //ERROR
    ...
}

3).let不能与其他类型组合连用。

let int a = 0.09; //ERROR

4).定义一个let关键字序列的对象的变量,其所有初始值必须为最终能推导为同一类型。

let a = 0.09, b = false, c =10; //ERROR
let a = 0.09, b = 3.14, c=100.901; //CORRECT

c.经典类型的变量。

格式:经典类型 变量名 比如:

cbit c;