当前位置 : 知识库 > 全部 > Quingo指令集用户手册
Quingo指令集用户手册
发布时间:2022-01-06 发布者: 量妹

浏览量:9681

青果用户手册上线啦


![quingo.png](https://quantumcomputer.oss-cn-hangzhou.aliyuncs.com/wiki/2022-01-10/92d2980f3afc471eb5ddea021c3b3ce1.png) # 青果语言官方文档 - [1. 快速上手青果(Quingo)](#1-快速上手青果quingo) - [1.1 如何快速创建一个青果工程?](#11-如何快速创建一个青果工程) - [1.2 经典主程序](#12-经典主程序) - [1.2.1. 调用量子内核程序](#121-调用量子内核程序) - [1.2.2. 返回内核程序计算结果](#122-返回内核程序计算结果) - [1.3. 量子内核程序](#13-量子内核程序) - [1.3.1. 如何快速编写一个量子内核程序?](#131-如何快速编写一个量子内核程序) - [1.3.2. 定义原子操作](#132-定义原子操作) - [1.3.3. 自定义复合操作](#133-自定义复合操作) - [1.3.3.1. 使用Operation定义复合操作](#1331-使用operation定义复合操作) - [1.3.3.2. 量子比特的分配](#1332-量子比特的分配) - [2. 青果编程语言](#2-青果编程语言) - [2.1. 类型系统](#21-类型系统) - [2.1.1. 基本数据类型](#211-基本数据类型) - [2.1.2. 元组](#212-元组) - [2.1.3. 数组](#213-数组) - [2.1.4. 函数类型](#214-函数类型) - [2.2. 操作](#22-操作) - [2.2.1. 原子操作](#221-原子操作) - [2.2.2. 用户自定义操作](#222-用户自定义操作) - [2.2.2.1. 变量声明语句](#2221-变量声明语句) - [2.2.2.2. 赋值语句](#2222-赋值语句) - [2.2.2.3. 程序流控制语句](#2223-程序流控制语句) - [2.2.2.4. 函数调用语句](#2224-函数调用语句) # 1. 快速上手青果(Quingo) 快速上手部分将着重介绍**青果(Quingo)采用的编程模型**以及**如何使用青果实现一个简单的量子算法**。下面将通过一个简单的青果工程示例,来帮助您快速地了解青果的一些基本特性。 青果设计实现了全面的量子-经典异构编程框架,使用青果描述的应用程序应当包含以下两个部分: * **经典主程序**: 经典主程序采用经典的编程语言编写,如Python或者C++语言等(目前仅支持使用Python语言编写主程序)。经典主程序通过编译或者解释最终将会经典处理器上运行。 * **量子内核程序**: 量子内核程序使用青果编程语言进行编写,其描述了应当在量子协处理器上执行的任务。并且量子内核程序通过编译能够生成量子指令集架构(`QISA`)中的量子汇编指令,最终这些指令将会被量子协处理器所执行。 总而言之,青果语言只用于描述在量子协处理器上执行的任务,而复杂的经典任务应该用经典语言进行描述。 ## 1.1 如何快速创建一个青果工程? 用户可以通过创建一个包含以下两个部分内容的新目录来创建一个新的青果工程: 1. **经典主程序**部分。该部分可以由一系列Python文件组成。 - 经典主程序部分负责描述经典逻辑任务,并且通过调用量子内核程序来获取计算结果。 2. **量子内核程序**部分。该部分由一系列后缀名为`.qu`的青果文件组成。 - 量子内核程序部分描述了量子逻辑任务,并且能够通过支持经典逻辑来对量子逻辑进行更加高效的描述。 ## 1.2 经典主程序 下面通过一个简洁的示例以开始对经典主程序的介绍。这里有两个接口函数值得关注,它们是 `call_quingo`函数(用于调用量子内核程序)以及 `read_result`函数(用于读取内核程序中的计算结果)。通过使用这两个函数,量子内核文件 `qu_file`中的量子操作函数 `bell_state`将被调用,其运行结果也随之会被读取出来。 ```python {.line-numbers} from pathlib import Path from quingo import quingo_interface as qi # qi.set_compiler("mlir") # qi.connect_backend("pyqcisim_quantumsim") qu_file = Path(__file__).parent / "kernel.qu" if qi.call_quingo(qu_file, "bell_state"): print(qi.read_result()) else: print("Failed to call the Quingo kernel") ``` 上述Python代码在第7行调用了文件`qu_file`中定义的量子操作`bell_state`,待该操作执行完毕之后,其结果将被读取出来并最终进行打印(见代码第8行)。 如上述代码所示,经典主程序与青果内核程序之间的交互主要包括以下四个步骤: 1. 导入青果接口库(代码第2行)。 2. 配置青果编译器以及量子模拟器(代码第4行和第5行)。该配置为可选配置,如果未配置将使用默认的编译器以及模拟器。 3. 使用 `call_quingo`接口函数(代码第7行)调用量子内核程序。 4. 使用 `read_result`接口函数(代码第8行)读回量子内核程序的计算结果。 在青果运行时系统的接口库`Quingo_interface`中定义了一系列的API,这些API旨在完成运行时系统和量子内核程序之间的交互。有关API的更多细节可在[青果运行时系统的README文件](https://gitee.com/hpcl_quanta/quingo-runtime#apis-of-the-quingo-runtime-system)中进行查看。 ### 1.2.1. 调用量子内核程序 调用量子内核程序需要使用接口函数`call_quingo`,该函数形式如下: ```python call_quingo(<kernel_filename>, <kernel_main_func>, <param1>, <param2>, ...) ``` 函数`call_quingo`中的几个参数含义如下: 1. 第一个参数(**必选**)表示青果文件的名称。在上述示例代码中,该参数为`"kernel.qu"`。 2. 第二个参数(**必选**)为量子内核程序中入口操作函数的名称。在上述示例代码中,该入口操作函数名为`"bell_state"`。 3. 剩下的所有参数均是**可选**参数。这些参数将被转换为相应的数据结构,随即用作入口操作函数中的参数。 接口函数`call_quingo`读取相应的青果文件后,通过调用青果编译器对该文件进行编译,并将生成的二进制代码或汇编代码加载至量子硬件或者量子模拟器中执行。如果执行成功,`call_quingo`函数将会返回`True`,反之返回`False`。 ### 1.2.2. 返回内核程序计算结果 如果量子内核程序执行成功,可以使用`read_result`方法读回量子内核程序的执行结果(见上述代码**第8行**)。 这里有一点需要注意,在调用量子内核程序之前或者读取量子内核程序执行结果之后,可以进行任意的经典计算,例如准备参数,对量子内核执行结果进行后处理,或者根据后处理结果调用另一个量子内核程序等动作。这对于需要量子-经典混合计算的NISQ量子程序而言特别重要,例如变分量子特征求解器 (VQE) 算法便需要上述的经典计算步骤。 ## 1.3. 量子内核程序 量子内核程序描述了在量子处理器上执行的任务。下面通过一个示例来展示如何编写量子内核程序: ``` C++ {.line-numbers} // kernel.qu opaque H(q: qubit) : unit; opaque Y2P(q: qubit) : unit; opaque Y2M(q: qubit) : unit; opaque CZ(q1: qubit, q2: qubit) : unit; opaque measure(q: qubit): bool; operation CNOT(q1: qubit, q2: qubit) : unit { Y2M(q2); CZ(q1, q2); Y2P(q2); } operation bell_state() : unit { using(q0: qubit, q1: qubit) { H(q0); CNOT(q0, q1); measure(q0); measure(q1); } } ``` ### 1.3.1. 如何快速编写一个量子内核程序? 事实上,从上述代码中可以看出青果量子内核程序最主要的两个部分: * 第一部分声明了一个**不透明**(`opaque`)的原子操作列表。例如,在上述代码中使用关键字`opaque`声明了 `H`门操作。 * 第二部分使用已经定义的原子操作来自定义更加复杂的操作,例如,在上述代码中定义了一个能够作用在两个量子比特上面的 `CNOT`门操作。 目前,青果量子内核程序中的量子操作必须使用以下两种方式进行定义: * 使用 `opaque`关键字定义原子操作。 * 使用 `operation`关键字自定义复合操作。 ### 1.3.2. 定义原子操作 所有原子操作必须使用 `opaque`关键字进行定义。事实上,编程语言本身无需关心底层硬件如何处理这些原子操作。使用`opaque`关键字进行定义原子操作一般采用如下形式: ```C++ opaque <operation_name>(...) : <returned_value_type>; ``` 在上面的示例代码中,量子操作 `CZ`便使用 `opaque`关键字进行了定义: ```C++ opaque CZ(q1: qubit, q2: qubit) : unit; ``` 对于上面定义的`CZ`操作,它有两个参数,分别为 `q1`和 `q2`,这两个参数的类型均为 `qubit`。`CZ`操作返回值的类型是 `unit`,即表示没有返回值。 ### 1.3.3. 自定义复合操作 #### 1.3.3.1. 使用Operation定义复合操作 用户可以使用以下的形式对复合操作进行自定义: ```C++ operation <operation_name>(...) : <returned_value_type> { ... } ``` 在上面的示例代码中,便使用了 `operation`关键字对量子操作 `CNOT`进行定义: ```C++ operation CNOT(q1: qubit, q2: qubit) : unit { ... } ``` `CNOT`操作同样有 `q1`和 `q2`两个参数,它们的类型均为 `qubit`。并且该操作无任何返回值。 #### 1.3.3.2. 量子比特的分配 除了操作输入参数中声明的量子比特之外,量子内核程序中使用的任何量子比特都必须使用类似于 `using(q0: qubit)`的语句进行分配,并且分配后的量子比特只能在一对花括号 `{...}`包围的区域内进行使用。 例如,下面代码对自定义操作 `bell_operation`中所使用的量子比特进行了分配: ``` using (q0: qubit, q1: qubit) { ... } ``` `bell_operation`使用了 `q0`和 `q1`两个量子比特,而 `qubit`则表示它们均是单量子比特。 # 2. 青果编程语言 青果语言有两个重要的组成部分,它们分别是: * 类型系统 * 操作 ## 2.1. 类型系统 青果编程语言是一种强类型的编程语言,它提供了**基本数据类型**, **元组**, **数组**, 以及 **函数类型**四种类型系统 ### 2.1.1 基本数据类型 青果编程语言提供以下基本数据类型: * `int`: 32位的整型。例如以下变量为整型:`1`,`100`,`-3`; * `double`: 双浮点型。例如以下变量为双浮点型:`1.1`,`9.99`; * `bool`: 布尔型。布尔型的变量可以是 `true`或者是 `false`; * `unit`: 空值类型。该类型表示函数不返回任何内容; * `qubit`: 表示变量为量子比特。量子比特对于用户来说是不透明的,分配之后直接使用即可。 ### 2.1.2. 元组 元组能够将不同类型的变量收集起来,方便对这些变量进行传递。一个元组的类型可以是 `(type 1, type 2, type 3, ...)`这种复合类型。例如,一个元组的类型可以是 `(int, bool, int)`或者 `(int,(int, bool))`等形式。下面给出了一个例子: ```C++ // 变量tuple1的类型为`(int, bool, double)` (int, bool, double) tuple1 = (1, false, 1.0); ``` ### 2.1.3. 数组 对于任意的基本数据类型 `T`,均可以构造出一个所含元素类型为`T`的数组,则相应数组的类型为 `T[]`。例如,`int[]`表示一维整型数组的类型,`bool[][]`代表着二维布尔型数组的类型。下面定义了一些不同类型的数组变量: ```C++ {.line-numbers} int a = 1; // x为一维的整型数组,其长度为3 int[3] x; // y为二维布尔型数组 bool[][] y; // 将变量"a"赋值给数组"x"的第一个元素 x[0] = a; // 将{1, 2, a}赋值给数组"x" x = {1, 2, a} ``` 目前,基于MLIR的青果编译器(版本为0.1)暂不支持定义锯齿状数组,即数组中每个维度的大小必须保持一致。例如: ```C++ {.line-numbers} int [][] x; // 定义了一个二维的整型数组 x = {{1, 2}, {1, 2, 3}}; // 错误: 暂不支持锯齿状数组 ``` ### 2.1.4. 函数类型 函数输入和输出变量的类型由其函数类型指定。例如,当函数类型的形式为 `(<input type> -> <return type>)`时,则 `<input type>`表示函数的输入变量类型,`<return type>`表示函数的返回值类型。所有函数都可以有一个或者多个输入值,或者没有输入值。并且函数可以返回单个值,而有些函数是没有返回值的,在这种情况下,返回值的类型为 `unit`。 下面是一些有关函数类型的具体示例: ```C++ {.line-numbers} // 函数"H"的类型为`qubit -> unit` opaque H(q0 : qubit) : unit; // 函数"func1"的类型为`(int,int) -> bool` operation func1(a: int, b: int): bool{ a = 1; b = 2; bool b1 = false; return b1; } // 输入参数"c"的类型为`(int, int) -> bool` // 函数"func2"的类型为`((int, int) -> bool) -> bool` operation func2(c: (int, int) -> bool): bool{ bool b2 = false; return b2; } // 函数"func3"的类型为`unit -> bool` operation func3() : bool{ bool b3 = false; return b3; } // 函数"main"的类型为`unit -> unit` operation main() : unit{ int x; } ``` ## 2.2. 操作 使用青果编程语言编写的量子内核程序是由诸多**原子操作**和**用户自定义操作**组成的。 ### 2.2.1. 原子操作 不同的量子技术可以定义不同的量子门操作集合。青果编程语言没有定义任何内置的量子操作原语。相反,青果编程语言提供了一种机制——可以定义依赖于平台特性的量子门操作,即所有的原子门操作都可以使用 `opaque`关键字进行定义。这些定义的原子操作需要青果编译器绑定具体的物理硬件才能产生相应的作用。原子操作采用如下形式进行定义: ```C++ opaque <operation_name>(...) : <returned_value_type>; ``` 一个原子操作只包含一个函数头,以下是上述形式中各个部分的含义: - `opaque`: 定义原子操作的关键字。 - `operation_name`: 表示原子操作的名称。 - `parameter_list`: 表示操作所接受的零个或者多个参数。 - `return_type`: 表示操作返回值的类型。 下面定义了名为`H()`的单量子门操作。该操作仅有一个参数`q`,且函数返回值的类型是`unit`。 ```C++ opaque H(q: qubit) : unit; ``` ### 2.2.2. 用户自定义操作 用户自定义的操作一般由函数头和函数体组成,用户自定义操作的格式如下: ```C++ operation <operation_name>(parameter_list) : <return_type> { // body of the user-defined operation } ``` 用户自定义操作的函数头以关键字 `operation`开头,后面依次是**操作名称**,**参数列表**以及**返回值类型**。这些字段与原子操作中的具有相同的属性和限制。函数体中的基本元素是一条条语句,通过语句,可以将量子操作(可以使用 `opaque`关键字或者 `operation`关键字定义)与经典操作自由混合,以实现经典程序流控制量子操作的执行。在青果编程语言中,用户可以通过**变量声明语句**,**赋值语句**,**程序流控制语句**以及**函数调用语句**来构造函数体中的内容。在接下来的内容中,将会对这些语句进行更加详细的介绍。 下面给出了一个名为`test()`的用户自定义操作示例。该操作没有任何参数,函数返回值的类型为`unit`。 ```C++ operation test() : unit { // 应将表示量子逻辑以及经典逻辑的语句列表放在此处 } ``` #### 2.2.2.1. 变量声明语句 变量声明语句用于声明具有指定类型的变量,其格式为`<type> <var_name>`。下面给出了一些变量声明语句的示例代码: ```C++ {.line-numbers} int a; double b; (int, double, bool) c; ``` 变量声明之后可以进行初始化,示例代码如下: ```C++ {.line-numbers} int a = 5; (bool, int) c = func(b); ``` 更多有关变量赋值的内容请见[赋值语句](#2222-赋值语句)。 在青果编程语言中,量子比特变量的声明有别于其他变量的声明。若想分配和使用量子比特必须使用`using语句`。`using语句`由关键字`using`、左括号`(`、量子比特的绑定、右括号`)`以及一个块语句组成,`using语句`的形式如下所示: ```C++ using(<qubit binding list>){ // 块语句 } ``` `<qubit binding list>`是一个混合着量子比特分配与量子比特数组分配的列表。其中,量子比特分配的格式为`<name>: qubit`,量子比特数组分配的格式为`<name>: qubit[ _n ]`。这里有一点需要注意的是,当用户想分配数个量子比特时,量子比特数组的大小应该是编译期间已知的值。除此之外,用户还可以同时分配单个量子比特和量子比特数组。例如下面所示: ```C++ using(q0: qubit, qs: qubit[], q1: qubit) { // 语句 } ``` 假设在对量子比特进行分配前,有着一个存放量子比特的池子。`using`语句中被声明的量子比特变量在该语句执行之后,将立即从池中选择量子比特与之分配。并在退出`using`语句作用域时,量子比特资源便会被收集。此外,从这个量子比特池中分配的所有量子比特均处于未知的状态,程序员将负责对它们进行初始化。 此外,青果编程语言不允许使用普通变量的声明格式(如`qubit q;`)来对量子比特进行分配。但是,用户可以通过类似`qubit qs1 = qs[1];`和 `qubit q0_alias = q0;`的方式来为量子比特变量定义别名,而其中的变量`q0`和`qs`必须是已经使用`using`语句定义过的量子比特变量。下面展示了一个简单的示例: ```C++ {.line-numbers} using(qs: qubit[2]){ qubit q1 = qs[1]; // 变量"q1"是量子比特"qs[1]"的别名。 } ``` #### 2.2.2.2. 赋值语句 赋值语句能够将变量与特定类型的值绑定在一起。基本类型变量,元组,数组或者数组中任意元素均可以进行赋值操作。而赋值语句中的表达式可以是一个基本类型常数,另一个变量、某个数组元素或者函数调用等形式。赋值语句的定义如下: ```C++ <variable> = <right expression>; ``` 在赋值语句中,等式左边的变量与等式右边的表达式必须保持相同的数据类型。因此,程序员在编写赋值语句时,必须确保等式两边类型一致,否则编译器将会报错。下面给出了一些简单的示例: ```C++ int b = 3; // 正确的变量赋值 int a = 1.0; // 错误:双浮点型数值不允许赋值给整型变量 int a = toInt(1.0); // 通过函数调用将返回值赋值给变量 ``` 此外,由于当前基于`MLIR`的编译器不支持锯齿状数组,因此对数组型变量赋值时,必须使用规则数组进行赋值。并且,青果语言中的内置函数`.length`可以对数组的长度进行检索。例如: ```C++ {.line-numbers} int len; int[2] array1; int[2][2] array2; array1 = {1, 2}; array2 = {{1, 2}, {3, 4}}; len = array1.length; ``` 下面为有关赋值语句的其他内容: - 使用青果语言编写量子内核程序时,用户可以编写一些复合的赋值语句。例如,赋值语句```a += b```等价于```a = a + b```。操作符`+`、`-`、`*`、`/`和`%`允许与`=`结合以实现更加简洁的赋值操作,但是该语法糖仅适用于对`float`型以及`int`型数值进行操作。 - 在条件表达式中,操作符`<`、`>`、`<=`、`==`、`=>`和`!=`可以作用于`float`型或`int`型数值。此外,操作符`==`和`!=`仅适用于`bool`型数值。 - 在逻辑表达式中,操作符`&&`、`||` 和 `!`只能作用于布尔值。 另外,青果语言中的一元运算符规定进行右结合,二元运算符则规定进行左结合。下面列举出了有关运算符的一些详细信息,其中操作符的顺序按照优先级从高到低进行排列: | Operator | Arity | Parameter type | Description | | :------------------: | :----: | :---------------------: | :--------------------------------------------------------------------------------: | | `!` | Unary | `Bool` | Logical Not | | `-`, `+` | Unary | `Int`, `Double ` | Hold or reverse value | | `*`, `/` | Binary | `Int`, `Double` | Multiplication, division | | `% ` | Binary | `Int` | integer modulus | | `-`, `+` | Binary | `Int`, `Double ` | Addition, subtraction | | `<`, `<=`, `>`, `>=` | Binary | `Int`, `Double` | Less-than, less-than-or-equal, greater-than, and greater-than-or-equal comparisons | | `==`, `!=` | Binary | `Int`, `Double`, `Bool` | equal and not-equal comparisons | | `&&` | Binary | `Bool` | Logical AND | | `||` | Binary | `Bool` | Logical OR | 在青果语言中,表达式中不允许混合使用`float`与`int`型的数值,例如`3.0 + 5`。但是,青果语言提供了一些内置函数,这些函数能够对`int`型和`double`型数值进行类型转换。其中: - 函数`toInt()`:将表达式从`double`类型转换成`int`类型。 - 函数`toDouble()`:将表达式从`int`类型转换成`double`类型。 #### 2.2.2.3. 程序流控制语句 青果语言支持使用关键字`if-else`/`for`/`while`进行程序流控制,这一点与C语言十分类似。此外,含有关键字`for`和`while`的循环语句中允许使用关键字`continue`和`break`。 以下有关程序流控制语句的几点特性值得注意: 1. 青果语言不支持使用关键字`switch`。 2. 与C语言不同的是,在使用`if-else`/`for`/`while`构建的控制结构中,花括号`{...}`不能省略,即使条件后面仅仅只有单个语句。 3. 在青果语言中,允许将获取到的测量结果用于条件判断(这种条件被称为**动态条件**)。目前不同版本的青果编译器对于动态条件的支持略有不同。 * 由于`QCIS`量子指令集不支持实时反馈控制,因此,目前基于`MLIR`的青果编译器暂时只支持**静态已知条件**的控制语句。未来版本的青果编译器将会全面支持动态条件的控制语句。 * 通过借助`eQASM`量子指令集中的经典指令,基于`Xtext`的青果编译器(后端能够生成`eQASM`指令)能够支持动态条件的控制语句。 下面给出了一些有关程序流控制语句的简单示例: ```C++ {.line-numbers} operation main(): unit { bool a = true; int b; if(a) { b = 1; } else { b = 2; } if (false) { b = 3; } } ``` ```C++ {.line-numbers} operation main(): unit { int a = 1; for(int i = 0; i < 10; i += 1) { a += 1; // 该for循环体将会重复执行10次 } } ``` ```C++ {.line-numbers} operation main(): unit { int a = 0; while(a < 10) { a += 1; } // 该while循环体将会重复执行10次 while(false){ a = 100; } // 该while循环体不会执行 } ``` #### 2.2.2.4. 函数调用语句 青果语言中的函数调用与C语言中的函数调用类似,其通过函数调用表达式和终止分号`;`进行定义。需要注意的是: * 在同一个青果文件中定义的操作可以相互调用,且无需前向声明。 * 为了管理一个大型的青果工程,可以将多个操作集中在同一个青果文件中(该文件可以作为**库**被其他文件调用)。某个文件若想导入其他文件中的操作,则可以使用`import`语句达成目的。 * 基于`MLIR`的青果编译器将会根据预定义的规则,然后通过依次搜索以下路径来获取模块: - 文件所在的路劲。 - 默认路径(例如`~/quingo/`)。 - 由系统变量指定的路径(例如 `QUINGO_MODULE_PATH`)。 - 系统模块路径(例如`/user/lib/quingo/`)。 * 一个青果工程应当被组织在一个文件夹中。 - 文件夹根目录应当包含一个后缀名为`.project`的文件,用来声明该文件夹是一个工程。 - 模块的名称应当根据从库的根目录到文件的路径来进行定义。例如,文件`<mypkg_dir>/a/b/c.qu`对应的模块名称应为`mypkg.a.b.c`。 下面展示了一些简单的函数调用示例: ```C++ {.line-numbers} opaque CNOT(q1: qubit, q2: qubit): unit; opaque H(q: qubit): unit; opaque measure(q: qubit): bool; operation bell_state(q1: qubit, q2: qubit) : unit { H(q1); CNOT(q1, q2); } operation main() : bool[] { bool[2] a; using(q1: qubit, q2: qubit) { bell_state(q1, q2); a[0] = measure(q1); a[1] = measure(q2); } return a; } ``` 青果语言侧面支持高阶函数以及函数数组。编译器实现方面,目前基于`Xtext`的青果编译器可以支持高阶函数调用,但是暂时不支持函数数组功能。 ### [完整Quingo语言手册](https://quingo.gitee.io/docs/)

quingo.png

青果语言官方文档

1. 快速上手青果(Quingo)

快速上手部分将着重介绍青果(Quingo)采用的编程模型以及如何使用青果实现一个简单的量子算法。下面将通过一个简单的青果工程示例,来帮助您快速地了解青果的一些基本特性。

青果设计实现了全面的量子-经典异构编程框架,使用青果描述的应用程序应当包含以下两个部分:

  • 经典主程序:
    经典主程序采用经典的编程语言编写,如Python或者C++语言等(目前仅支持使用Python语言编写主程序)。经典主程序通过编译或者解释最终将会经典处理器上运行。
  • 量子内核程序:
    量子内核程序使用青果编程语言进行编写,其描述了应当在量子协处理器上执行的任务。并且量子内核程序通过编译能够生成量子指令集架构(QISA)中的量子汇编指令,最终这些指令将会被量子协处理器所执行。

总而言之,青果语言只用于描述在量子协处理器上执行的任务,而复杂的经典任务应该用经典语言进行描述。

1.1 如何快速创建一个青果工程?

用户可以通过创建一个包含以下两个部分内容的新目录来创建一个新的青果工程:

  1. 经典主程序部分。该部分可以由一系列Python文件组成。
    • 经典主程序部分负责描述经典逻辑任务,并且通过调用量子内核程序来获取计算结果。
  2. 量子内核程序部分。该部分由一系列后缀名为.qu的青果文件组成。
    • 量子内核程序部分描述了量子逻辑任务,并且能够通过支持经典逻辑来对量子逻辑进行更加高效的描述。

1.2 经典主程序

下面通过一个简洁的示例以开始对经典主程序的介绍。这里有两个接口函数值得关注,它们是 call_quingo函数(用于调用量子内核程序)以及 read_result函数(用于读取内核程序中的计算结果)。通过使用这两个函数,量子内核文件 qu_file中的量子操作函数 bell_state将被调用,其运行结果也随之会被读取出来。

from pathlib import Path from quingo import quingo_interface as qi # qi.set_compiler("mlir") # qi.connect_backend("pyqcisim_quantumsim") qu_file = Path(__file__).parent / "kernel.qu" if qi.call_quingo(qu_file, "bell_state"): print(qi.read_result()) else: print("Failed to call the Quingo kernel")

上述Python代码在第7行调用了文件qu_file中定义的量子操作bell_state,待该操作执行完毕之后,其结果将被读取出来并最终进行打印(见代码第8行)。

如上述代码所示,经典主程序与青果内核程序之间的交互主要包括以下四个步骤:

  1. 导入青果接口库(代码第2行)。
  2. 配置青果编译器以及量子模拟器(代码第4行和第5行)。该配置为可选配置,如果未配置将使用默认的编译器以及模拟器。
  3. 使用 call_quingo接口函数(代码第7行)调用量子内核程序。
  4. 使用 read_result接口函数(代码第8行)读回量子内核程序的计算结果。

在青果运行时系统的接口库Quingo_interface中定义了一系列的API,这些API旨在完成运行时系统和量子内核程序之间的交互。有关API的更多细节可在青果运行时系统的README文件中进行查看。

1.2.1. 调用量子内核程序

调用量子内核程序需要使用接口函数call_quingo,该函数形式如下:

call_quingo(<kernel_filename>, <kernel_main_func>, <param1>, <param2>, ...)

函数call_quingo中的几个参数含义如下:

  1. 第一个参数(必选)表示青果文件的名称。在上述示例代码中,该参数为"kernel.qu"
  2. 第二个参数(必选)为量子内核程序中入口操作函数的名称。在上述示例代码中,该入口操作函数名为"bell_state"
  3. 剩下的所有参数均是可选参数。这些参数将被转换为相应的数据结构,随即用作入口操作函数中的参数。

接口函数call_quingo读取相应的青果文件后,通过调用青果编译器对该文件进行编译,并将生成的二进制代码或汇编代码加载至量子硬件或者量子模拟器中执行。如果执行成功,call_quingo函数将会返回True,反之返回False

1.2.2. 返回内核程序计算结果

如果量子内核程序执行成功,可以使用read_result方法读回量子内核程序的执行结果(见上述代码第8行)。

这里有一点需要注意,在调用量子内核程序之前或者读取量子内核程序执行结果之后,可以进行任意的经典计算,例如准备参数,对量子内核执行结果进行后处理,或者根据后处理结果调用另一个量子内核程序等动作。这对于需要量子-经典混合计算的NISQ量子程序而言特别重要,例如变分量子特征求解器 (VQE) 算法便需要上述的经典计算步骤。

1.3. 量子内核程序

量子内核程序描述了在量子处理器上执行的任务。下面通过一个示例来展示如何编写量子内核程序:

// kernel.qu opaque H(q: qubit) : unit; opaque Y2P(q: qubit) : unit; opaque Y2M(q: qubit) : unit; opaque CZ(q1: qubit, q2: qubit) : unit; opaque measure(q: qubit): bool; operation CNOT(q1: qubit, q2: qubit) : unit { Y2M(q2); CZ(q1, q2); Y2P(q2); } operation bell_state() : unit { using(q0: qubit, q1: qubit) { H(q0); CNOT(q0, q1); measure(q0); measure(q1); } }

1.3.1. 如何快速编写一个量子内核程序?

事实上,从上述代码中可以看出青果量子内核程序最主要的两个部分:

  • 第一部分声明了一个不透明opaque)的原子操作列表。例如,在上述代码中使用关键字opaque声明了 H门操作。
  • 第二部分使用已经定义的原子操作来自定义更加复杂的操作,例如,在上述代码中定义了一个能够作用在两个量子比特上面的 CNOT门操作。

目前,青果量子内核程序中的量子操作必须使用以下两种方式进行定义:

  • 使用 opaque关键字定义原子操作。
  • 使用 operation关键字自定义复合操作。

1.3.2. 定义原子操作

所有原子操作必须使用 opaque关键字进行定义。事实上,编程语言本身无需关心底层硬件如何处理这些原子操作。使用opaque关键字进行定义原子操作一般采用如下形式:

opaque <operation_name>(...) : <returned_value_type>;

在上面的示例代码中,量子操作 CZ便使用 opaque关键字进行了定义:

opaque CZ(q1: qubit, q2: qubit) : unit;

对于上面定义的CZ操作,它有两个参数,分别为 q1q2,这两个参数的类型均为 qubitCZ操作返回值的类型是 unit,即表示没有返回值。

1.3.3. 自定义复合操作

1.3.3.1. 使用Operation定义复合操作

用户可以使用以下的形式对复合操作进行自定义:

operation <operation_name>(...) : <returned_value_type> { ... }

在上面的示例代码中,便使用了 operation关键字对量子操作 CNOT进行定义:

operation CNOT(q1: qubit, q2: qubit) : unit { ... }

CNOT操作同样有 q1q2两个参数,它们的类型均为 qubit。并且该操作无任何返回值。

1.3.3.2. 量子比特的分配

除了操作输入参数中声明的量子比特之外,量子内核程序中使用的任何量子比特都必须使用类似于 using(q0: qubit)的语句进行分配,并且分配后的量子比特只能在一对花括号 {...}包围的区域内进行使用。
例如,下面代码对自定义操作 bell_operation中所使用的量子比特进行了分配:

using (q0: qubit, q1: qubit) {
        ...
}

bell_operation使用了 q0q1两个量子比特,而 qubit则表示它们均是单量子比特。

2. 青果编程语言

青果语言有两个重要的组成部分,它们分别是:

  • 类型系统
  • 操作

2.1. 类型系统

青果编程语言是一种强类型的编程语言,它提供了基本数据类型, 元组, 数组, 以及 函数类型四种类型系统

2.1.1 基本数据类型

青果编程语言提供以下基本数据类型:

  • int: 32位的整型。例如以下变量为整型:1100-3
  • double: 双浮点型。例如以下变量为双浮点型:1.19.99
  • bool: 布尔型。布尔型的变量可以是 true或者是 false;
  • unit: 空值类型。该类型表示函数不返回任何内容;
  • qubit: 表示变量为量子比特。量子比特对于用户来说是不透明的,分配之后直接使用即可。

2.1.2. 元组

元组能够将不同类型的变量收集起来,方便对这些变量进行传递。一个元组的类型可以是 (type 1, type 2, type 3, ...)这种复合类型。例如,一个元组的类型可以是 (int, bool, int)或者 (int,(int, bool))等形式。下面给出了一个例子:

// 变量tuple1的类型为`(int, bool, double)` (int, bool, double) tuple1 = (1, false, 1.0);

2.1.3. 数组

对于任意的基本数据类型 T,均可以构造出一个所含元素类型为T的数组,则相应数组的类型为 T[]。例如,int[]表示一维整型数组的类型,bool[][]代表着二维布尔型数组的类型。下面定义了一些不同类型的数组变量:

int a = 1; // x为一维的整型数组,其长度为3 int[3] x; // y为二维布尔型数组 bool[][] y; // 将变量"a"赋值给数组"x"的第一个元素 x[0] = a; // 将{1, 2, a}赋值给数组"x" x = {1, 2, a}

目前,基于MLIR的青果编译器(版本为0.1)暂不支持定义锯齿状数组,即数组中每个维度的大小必须保持一致。例如:

int [][] x; // 定义了一个二维的整型数组 x = {{1, 2}, {1, 2, 3}}; // 错误: 暂不支持锯齿状数组

2.1.4. 函数类型

函数输入和输出变量的类型由其函数类型指定。例如,当函数类型的形式为 (<input type> -> <return type>)时,则 <input type>表示函数的输入变量类型,<return type>表示函数的返回值类型。所有函数都可以有一个或者多个输入值,或者没有输入值。并且函数可以返回单个值,而有些函数是没有返回值的,在这种情况下,返回值的类型为 unit

下面是一些有关函数类型的具体示例:

// 函数"H"的类型为`qubit -> unit` opaque H(q0 : qubit) : unit; // 函数"func1"的类型为`(int,int) -> bool` operation func1(a: int, b: int): bool{ a = 1; b = 2; bool b1 = false; return b1; } // 输入参数"c"的类型为`(int, int) -> bool` // 函数"func2"的类型为`((int, int) -> bool) -> bool` operation func2(c: (int, int) -> bool): bool{ bool b2 = false; return b2; } // 函数"func3"的类型为`unit -> bool` operation func3() : bool{ bool b3 = false; return b3; } // 函数"main"的类型为`unit -> unit` operation main() : unit{ int x; }

2.2. 操作

使用青果编程语言编写的量子内核程序是由诸多原子操作用户自定义操作组成的。

2.2.1. 原子操作

不同的量子技术可以定义不同的量子门操作集合。青果编程语言没有定义任何内置的量子操作原语。相反,青果编程语言提供了一种机制——可以定义依赖于平台特性的量子门操作,即所有的原子门操作都可以使用 opaque关键字进行定义。这些定义的原子操作需要青果编译器绑定具体的物理硬件才能产生相应的作用。原子操作采用如下形式进行定义:

opaque <operation_name>(...) : <returned_value_type>;

一个原子操作只包含一个函数头,以下是上述形式中各个部分的含义:

  • opaque: 定义原子操作的关键字。
  • operation_name: 表示原子操作的名称。
  • parameter_list: 表示操作所接受的零个或者多个参数。
  • return_type: 表示操作返回值的类型。

下面定义了名为H()的单量子门操作。该操作仅有一个参数q,且函数返回值的类型是unit

opaque H(q: qubit) : unit;

2.2.2. 用户自定义操作

用户自定义的操作一般由函数头和函数体组成,用户自定义操作的格式如下:

operation <operation_name>(parameter_list) : <return_type> { // body of the user-defined operation }

用户自定义操作的函数头以关键字 operation开头,后面依次是操作名称参数列表以及返回值类型。这些字段与原子操作中的具有相同的属性和限制。函数体中的基本元素是一条条语句,通过语句,可以将量子操作(可以使用 opaque关键字或者 operation关键字定义)与经典操作自由混合,以实现经典程序流控制量子操作的执行。在青果编程语言中,用户可以通过变量声明语句赋值语句程序流控制语句以及函数调用语句来构造函数体中的内容。在接下来的内容中,将会对这些语句进行更加详细的介绍。

下面给出了一个名为test()的用户自定义操作示例。该操作没有任何参数,函数返回值的类型为unit

operation test() : unit { // 应将表示量子逻辑以及经典逻辑的语句列表放在此处 }

2.2.2.1. 变量声明语句

变量声明语句用于声明具有指定类型的变量,其格式为<type> <var_name>。下面给出了一些变量声明语句的示例代码:

int a; double b; (int, double, bool) c;

变量声明之后可以进行初始化,示例代码如下:

int a = 5; (bool, int) c = func(b);

更多有关变量赋值的内容请见赋值语句

在青果编程语言中,量子比特变量的声明有别于其他变量的声明。若想分配和使用量子比特必须使用using语句using语句由关键字using、左括号(、量子比特的绑定、右括号)以及一个块语句组成,using语句的形式如下所示:

using(<qubit binding list>){ // 块语句 }

<qubit binding list>是一个混合着量子比特分配与量子比特数组分配的列表。其中,量子比特分配的格式为<name>: qubit,量子比特数组分配的格式为<name>: qubit[ _n ]。这里有一点需要注意的是,当用户想分配数个量子比特时,量子比特数组的大小应该是编译期间已知的值。除此之外,用户还可以同时分配单个量子比特和量子比特数组。例如下面所示:

using(q0: qubit, qs: qubit[], q1: qubit) { // 语句 }

假设在对量子比特进行分配前,有着一个存放量子比特的池子。using语句中被声明的量子比特变量在该语句执行之后,将立即从池中选择量子比特与之分配。并在退出using语句作用域时,量子比特资源便会被收集。此外,从这个量子比特池中分配的所有量子比特均处于未知的状态,程序员将负责对它们进行初始化。

此外,青果编程语言不允许使用普通变量的声明格式(如qubit q;)来对量子比特进行分配。但是,用户可以通过类似qubit qs1 = qs[1];qubit q0_alias = q0;的方式来为量子比特变量定义别名,而其中的变量q0qs必须是已经使用using语句定义过的量子比特变量。下面展示了一个简单的示例:

using(qs: qubit[2]){ qubit q1 = qs[1]; // 变量"q1"是量子比特"qs[1]"的别名。 }

2.2.2.2. 赋值语句

赋值语句能够将变量与特定类型的值绑定在一起。基本类型变量,元组,数组或者数组中任意元素均可以进行赋值操作。而赋值语句中的表达式可以是一个基本类型常数,另一个变量、某个数组元素或者函数调用等形式。赋值语句的定义如下:

<variable> = <right expression>;

在赋值语句中,等式左边的变量与等式右边的表达式必须保持相同的数据类型。因此,程序员在编写赋值语句时,必须确保等式两边类型一致,否则编译器将会报错。下面给出了一些简单的示例:

int b = 3; // 正确的变量赋值 int a = 1.0; // 错误:双浮点型数值不允许赋值给整型变量 int a = toInt(1.0); // 通过函数调用将返回值赋值给变量

此外,由于当前基于MLIR的编译器不支持锯齿状数组,因此对数组型变量赋值时,必须使用规则数组进行赋值。并且,青果语言中的内置函数.length可以对数组的长度进行检索。例如:

int len; int[2] array1; int[2][2] array2; array1 = {1, 2}; array2 = {{1, 2}, {3, 4}}; len = array1.length;

下面为有关赋值语句的其他内容:

  • 使用青果语言编写量子内核程序时,用户可以编写一些复合的赋值语句。例如,赋值语句a += b等价于a = a + b。操作符+-*/%允许与=结合以实现更加简洁的赋值操作,但是该语法糖仅适用于对float型以及int型数值进行操作。
  • 在条件表达式中,操作符<><====>!=可以作用于float型或int型数值。此外,操作符==!=仅适用于bool型数值。
  • 在逻辑表达式中,操作符&&||!只能作用于布尔值。

另外,青果语言中的一元运算符规定进行右结合,二元运算符则规定进行左结合。下面列举出了有关运算符的一些详细信息,其中操作符的顺序按照优先级从高到低进行排列:

Operator Arity Parameter type Description
! Unary Bool Logical Not
-, + Unary Int, Double Hold or reverse value
*, / Binary Int, Double Multiplication, division
% Binary Int integer modulus
-, + Binary Int, Double Addition, subtraction
<, <=, >, >= Binary Int, Double Less-than, less-than-or-equal, greater-than, and greater-than-or-equal comparisons
==, != Binary Int, Double, Bool equal and not-equal comparisons
&& Binary Bool Logical AND
|| Binary Bool Logical OR

在青果语言中,表达式中不允许混合使用floatint型的数值,例如3.0 + 5。但是,青果语言提供了一些内置函数,这些函数能够对int型和double型数值进行类型转换。其中:

  • 函数toInt():将表达式从double类型转换成int类型。
  • 函数toDouble():将表达式从int类型转换成double类型。

2.2.2.3. 程序流控制语句

青果语言支持使用关键字if-else/for/while进行程序流控制,这一点与C语言十分类似。此外,含有关键字forwhile的循环语句中允许使用关键字continuebreak

以下有关程序流控制语句的几点特性值得注意:

  1. 青果语言不支持使用关键字switch
  2. 与C语言不同的是,在使用if-else/for/while构建的控制结构中,花括号{...}不能省略,即使条件后面仅仅只有单个语句。
  3. 在青果语言中,允许将获取到的测量结果用于条件判断(这种条件被称为动态条件)。目前不同版本的青果编译器对于动态条件的支持略有不同。
    • 由于QCIS量子指令集不支持实时反馈控制,因此,目前基于MLIR的青果编译器暂时只支持静态已知条件的控制语句。未来版本的青果编译器将会全面支持动态条件的控制语句。
    • 通过借助eQASM量子指令集中的经典指令,基于Xtext的青果编译器(后端能够生成eQASM指令)能够支持动态条件的控制语句。

下面给出了一些有关程序流控制语句的简单示例:

operation main(): unit { bool a = true; int b; if(a) { b = 1; } else { b = 2; } if (false) { b = 3; } }
operation main(): unit { int a = 1; for(int i = 0; i < 10; i += 1) { a += 1; // 该for循环体将会重复执行10次 } }
operation main(): unit { int a = 0; while(a < 10) { a += 1; } // 该while循环体将会重复执行10次 while(false){ a = 100; } // 该while循环体不会执行 }

2.2.2.4. 函数调用语句

青果语言中的函数调用与C语言中的函数调用类似,其通过函数调用表达式和终止分号;进行定义。需要注意的是:

  • 在同一个青果文件中定义的操作可以相互调用,且无需前向声明。
  • 为了管理一个大型的青果工程,可以将多个操作集中在同一个青果文件中(该文件可以作为被其他文件调用)。某个文件若想导入其他文件中的操作,则可以使用import语句达成目的。
  • 基于MLIR的青果编译器将会根据预定义的规则,然后通过依次搜索以下路径来获取模块:
    • 文件所在的路劲。
    • 默认路径(例如~/quingo/)。
    • 由系统变量指定的路径(例如 QUINGO_MODULE_PATH)。
    • 系统模块路径(例如/user/lib/quingo/)。
  • 一个青果工程应当被组织在一个文件夹中。
    • 文件夹根目录应当包含一个后缀名为.project的文件,用来声明该文件夹是一个工程。
    • 模块的名称应当根据从库的根目录到文件的路径来进行定义。例如,文件<mypkg_dir>/a/b/c.qu对应的模块名称应为mypkg.a.b.c

下面展示了一些简单的函数调用示例:

opaque CNOT(q1: qubit, q2: qubit): unit; opaque H(q: qubit): unit; opaque measure(q: qubit): bool; operation bell_state(q1: qubit, q2: qubit) : unit { H(q1); CNOT(q1, q2); } operation main() : bool[] { bool[2] a; using(q1: qubit, q2: qubit) { bell_state(q1, q2); a[0] = measure(q1); a[1] = measure(q2); } return a; }

青果语言侧面支持高阶函数以及函数数组。编译器实现方面,目前基于Xtext的青果编译器可以支持高阶函数调用,但是暂时不支持函数数组功能。

完整Quingo语言手册

quingo.png

青果语言官方文档

1. 快速上手青果(Quingo)

快速上手部分将着重介绍青果(Quingo)采用的编程模型以及如何使用青果实现一个简单的量子算法。下面将通过一个简单的青果工程示例,来帮助您快速地了解青果的一些基本特性。

青果设计实现了全面的量子-经典异构编程框架,使用青果描述的应用程序应当包含以下两个部分:

  • 经典主程序:
    经典主程序采用经典的编程语言编写,如Python或者C++语言等(目前仅支持使用Python语言编写主程序)。经典主程序通过编译或者解释最终将会经典处理器上运行。
  • 量子内核程序:
    量子内核程序使用青果编程语言进行编写,其描述了应当在量子协处理器上执行的任务。并且量子内核程序通过编译能够生成量子指令集架构(QISA)中的量子汇编指令,最终这些指令将会被量子协处理器所执行。

总而言之,青果语言只用于描述在量子协处理器上执行的任务,而复杂的经典任务应该用经典语言进行描述。

1.1 如何快速创建一个青果工程?

用户可以通过创建一个包含以下两个部分内容的新目录来创建一个新的青果工程:

  1. 经典主程序部分。该部分可以由一系列Python文件组成。
    • 经典主程序部分负责描述经典逻辑任务,并且通过调用量子内核程序来获取计算结果。
  2. 量子内核程序部分。该部分由一系列后缀名为.qu的青果文件组成。
    • 量子内核程序部分描述了量子逻辑任务,并且能够通过支持经典逻辑来对量子逻辑进行更加高效的描述。

1.2 经典主程序

下面通过一个简洁的示例以开始对经典主程序的介绍。这里有两个接口函数值得关注,它们是 call_quingo函数(用于调用量子内核程序)以及 read_result函数(用于读取内核程序中的计算结果)。通过使用这两个函数,量子内核文件 qu_file中的量子操作函数 bell_state将被调用,其运行结果也随之会被读取出来。

from pathlib import Path from quingo import quingo_interface as qi # qi.set_compiler("mlir") # qi.connect_backend("pyqcisim_quantumsim") qu_file = Path(__file__).parent / "kernel.qu" if qi.call_quingo(qu_file, "bell_state"): print(qi.read_result()) else: print("Failed to call the Quingo kernel")

上述Python代码在第7行调用了文件qu_file中定义的量子操作bell_state,待该操作执行完毕之后,其结果将被读取出来并最终进行打印(见代码第8行)。

如上述代码所示,经典主程序与青果内核程序之间的交互主要包括以下四个步骤:

  1. 导入青果接口库(代码第2行)。
  2. 配置青果编译器以及量子模拟器(代码第4行和第5行)。该配置为可选配置,如果未配置将使用默认的编译器以及模拟器。
  3. 使用 call_quingo接口函数(代码第7行)调用量子内核程序。
  4. 使用 read_result接口函数(代码第8行)读回量子内核程序的计算结果。

在青果运行时系统的接口库Quingo_interface中定义了一系列的API,这些API旨在完成运行时系统和量子内核程序之间的交互。有关API的更多细节可在青果运行时系统的README文件中进行查看。

1.2.1. 调用量子内核程序

调用量子内核程序需要使用接口函数call_quingo,该函数形式如下:

call_quingo(<kernel_filename>, <kernel_main_func>, <param1>, <param2>, ...)

函数call_quingo中的几个参数含义如下:

  1. 第一个参数(必选)表示青果文件的名称。在上述示例代码中,该参数为"kernel.qu"
  2. 第二个参数(必选)为量子内核程序中入口操作函数的名称。在上述示例代码中,该入口操作函数名为"bell_state"
  3. 剩下的所有参数均是可选参数。这些参数将被转换为相应的数据结构,随即用作入口操作函数中的参数。

接口函数call_quingo读取相应的青果文件后,通过调用青果编译器对该文件进行编译,并将生成的二进制代码或汇编代码加载至量子硬件或者量子模拟器中执行。如果执行成功,call_quingo函数将会返回True,反之返回False

1.2.2. 返回内核程序计算结果

如果量子内核程序执行成功,可以使用read_result方法读回量子内核程序的执行结果(见上述代码第8行)。

这里有一点需要注意,在调用量子内核程序之前或者读取量子内核程序执行结果之后,可以进行任意的经典计算,例如准备参数,对量子内核执行结果进行后处理,或者根据后处理结果调用另一个量子内核程序等动作。这对于需要量子-经典混合计算的NISQ量子程序而言特别重要,例如变分量子特征求解器 (VQE) 算法便需要上述的经典计算步骤。

1.3. 量子内核程序

量子内核程序描述了在量子处理器上执行的任务。下面通过一个示例来展示如何编写量子内核程序:

// kernel.qu opaque H(q: qubit) : unit; opaque Y2P(q: qubit) : unit; opaque Y2M(q: qubit) : unit; opaque CZ(q1: qubit, q2: qubit) : unit; opaque measure(q: qubit): bool; operation CNOT(q1: qubit, q2: qubit) : unit { Y2M(q2); CZ(q1, q2); Y2P(q2); } operation bell_state() : unit { using(q0: qubit, q1: qubit) { H(q0); CNOT(q0, q1); measure(q0); measure(q1); } }

1.3.1. 如何快速编写一个量子内核程序?

事实上,从上述代码中可以看出青果量子内核程序最主要的两个部分:

  • 第一部分声明了一个不透明opaque)的原子操作列表。例如,在上述代码中使用关键字opaque声明了 H门操作。
  • 第二部分使用已经定义的原子操作来自定义更加复杂的操作,例如,在上述代码中定义了一个能够作用在两个量子比特上面的 CNOT门操作。

目前,青果量子内核程序中的量子操作必须使用以下两种方式进行定义:

  • 使用 opaque关键字定义原子操作。
  • 使用 operation关键字自定义复合操作。

1.3.2. 定义原子操作

所有原子操作必须使用 opaque关键字进行定义。事实上,编程语言本身无需关心底层硬件如何处理这些原子操作。使用opaque关键字进行定义原子操作一般采用如下形式:

opaque <operation_name>(...) : <returned_value_type>;

在上面的示例代码中,量子操作 CZ便使用 opaque关键字进行了定义:

opaque CZ(q1: qubit, q2: qubit) : unit;

对于上面定义的CZ操作,它有两个参数,分别为 q1q2,这两个参数的类型均为 qubitCZ操作返回值的类型是 unit,即表示没有返回值。

1.3.3. 自定义复合操作

1.3.3.1. 使用Operation定义复合操作

用户可以使用以下的形式对复合操作进行自定义:

operation <operation_name>(...) : <returned_value_type> { ... }

在上面的示例代码中,便使用了 operation关键字对量子操作 CNOT进行定义:

operation CNOT(q1: qubit, q2: qubit) : unit { ... }

CNOT操作同样有 q1q2两个参数,它们的类型均为 qubit。并且该操作无任何返回值。

1.3.3.2. 量子比特的分配

除了操作输入参数中声明的量子比特之外,量子内核程序中使用的任何量子比特都必须使用类似于 using(q0: qubit)的语句进行分配,并且分配后的量子比特只能在一对花括号 {...}包围的区域内进行使用。
例如,下面代码对自定义操作 bell_operation中所使用的量子比特进行了分配:

using (q0: qubit, q1: qubit) {
        ...
}

bell_operation使用了 q0q1两个量子比特,而 qubit则表示它们均是单量子比特。

2. 青果编程语言

青果语言有两个重要的组成部分,它们分别是:

  • 类型系统
  • 操作

2.1. 类型系统

青果编程语言是一种强类型的编程语言,它提供了基本数据类型, 元组, 数组, 以及 函数类型四种类型系统

2.1.1 基本数据类型

青果编程语言提供以下基本数据类型:

  • int: 32位的整型。例如以下变量为整型:1100-3
  • double: 双浮点型。例如以下变量为双浮点型:1.19.99
  • bool: 布尔型。布尔型的变量可以是 true或者是 false;
  • unit: 空值类型。该类型表示函数不返回任何内容;
  • qubit: 表示变量为量子比特。量子比特对于用户来说是不透明的,分配之后直接使用即可。

2.1.2. 元组

元组能够将不同类型的变量收集起来,方便对这些变量进行传递。一个元组的类型可以是 (type 1, type 2, type 3, ...)这种复合类型。例如,一个元组的类型可以是 (int, bool, int)或者 (int,(int, bool))等形式。下面给出了一个例子:

// 变量tuple1的类型为`(int, bool, double)` (int, bool, double) tuple1 = (1, false, 1.0);

2.1.3. 数组

对于任意的基本数据类型 T,均可以构造出一个所含元素类型为T的数组,则相应数组的类型为 T[]。例如,int[]表示一维整型数组的类型,bool[][]代表着二维布尔型数组的类型。下面定义了一些不同类型的数组变量:

int a = 1; // x为一维的整型数组,其长度为3 int[3] x; // y为二维布尔型数组 bool[][] y; // 将变量"a"赋值给数组"x"的第一个元素 x[0] = a; // 将{1, 2, a}赋值给数组"x" x = {1, 2, a}

目前,基于MLIR的青果编译器(版本为0.1)暂不支持定义锯齿状数组,即数组中每个维度的大小必须保持一致。例如:

int [][] x; // 定义了一个二维的整型数组 x = {{1, 2}, {1, 2, 3}}; // 错误: 暂不支持锯齿状数组

2.1.4. 函数类型

函数输入和输出变量的类型由其函数类型指定。例如,当函数类型的形式为 (<input type> -> <return type>)时,则 <input type>表示函数的输入变量类型,<return type>表示函数的返回值类型。所有函数都可以有一个或者多个输入值,或者没有输入值。并且函数可以返回单个值,而有些函数是没有返回值的,在这种情况下,返回值的类型为 unit

下面是一些有关函数类型的具体示例:

// 函数"H"的类型为`qubit -> unit` opaque H(q0 : qubit) : unit; // 函数"func1"的类型为`(int,int) -> bool` operation func1(a: int, b: int): bool{ a = 1; b = 2; bool b1 = false; return b1; } // 输入参数"c"的类型为`(int, int) -> bool` // 函数"func2"的类型为`((int, int) -> bool) -> bool` operation func2(c: (int, int) -> bool): bool{ bool b2 = false; return b2; } // 函数"func3"的类型为`unit -> bool` operation func3() : bool{ bool b3 = false; return b3; } // 函数"main"的类型为`unit -> unit` operation main() : unit{ int x; }

2.2. 操作

使用青果编程语言编写的量子内核程序是由诸多原子操作用户自定义操作组成的。

2.2.1. 原子操作

不同的量子技术可以定义不同的量子门操作集合。青果编程语言没有定义任何内置的量子操作原语。相反,青果编程语言提供了一种机制——可以定义依赖于平台特性的量子门操作,即所有的原子门操作都可以使用 opaque关键字进行定义。这些定义的原子操作需要青果编译器绑定具体的物理硬件才能产生相应的作用。原子操作采用如下形式进行定义:

opaque <operation_name>(...) : <returned_value_type>;

一个原子操作只包含一个函数头,以下是上述形式中各个部分的含义:

  • opaque: 定义原子操作的关键字。
  • operation_name: 表示原子操作的名称。
  • parameter_list: 表示操作所接受的零个或者多个参数。
  • return_type: 表示操作返回值的类型。

下面定义了名为H()的单量子门操作。该操作仅有一个参数q,且函数返回值的类型是unit

opaque H(q: qubit) : unit;

2.2.2. 用户自定义操作

用户自定义的操作一般由函数头和函数体组成,用户自定义操作的格式如下:

operation <operation_name>(parameter_list) : <return_type> { // body of the user-defined operation }

用户自定义操作的函数头以关键字 operation开头,后面依次是操作名称参数列表以及返回值类型。这些字段与原子操作中的具有相同的属性和限制。函数体中的基本元素是一条条语句,通过语句,可以将量子操作(可以使用 opaque关键字或者 operation关键字定义)与经典操作自由混合,以实现经典程序流控制量子操作的执行。在青果编程语言中,用户可以通过变量声明语句赋值语句程序流控制语句以及函数调用语句来构造函数体中的内容。在接下来的内容中,将会对这些语句进行更加详细的介绍。

下面给出了一个名为test()的用户自定义操作示例。该操作没有任何参数,函数返回值的类型为unit

operation test() : unit { // 应将表示量子逻辑以及经典逻辑的语句列表放在此处 }

2.2.2.1. 变量声明语句

变量声明语句用于声明具有指定类型的变量,其格式为<type> <var_name>。下面给出了一些变量声明语句的示例代码:

int a; double b; (int, double, bool) c;

变量声明之后可以进行初始化,示例代码如下:

int a = 5; (bool, int) c = func(b);

更多有关变量赋值的内容请见赋值语句

在青果编程语言中,量子比特变量的声明有别于其他变量的声明。若想分配和使用量子比特必须使用using语句using语句由关键字using、左括号(、量子比特的绑定、右括号)以及一个块语句组成,using语句的形式如下所示:

using(<qubit binding list>){ // 块语句 }

<qubit binding list>是一个混合着量子比特分配与量子比特数组分配的列表。其中,量子比特分配的格式为<name>: qubit,量子比特数组分配的格式为<name>: qubit[ _n ]。这里有一点需要注意的是,当用户想分配数个量子比特时,量子比特数组的大小应该是编译期间已知的值。除此之外,用户还可以同时分配单个量子比特和量子比特数组。例如下面所示:

using(q0: qubit, qs: qubit[], q1: qubit) { // 语句 }

假设在对量子比特进行分配前,有着一个存放量子比特的池子。using语句中被声明的量子比特变量在该语句执行之后,将立即从池中选择量子比特与之分配。并在退出using语句作用域时,量子比特资源便会被收集。此外,从这个量子比特池中分配的所有量子比特均处于未知的状态,程序员将负责对它们进行初始化。

此外,青果编程语言不允许使用普通变量的声明格式(如qubit q;)来对量子比特进行分配。但是,用户可以通过类似qubit qs1 = qs[1];qubit q0_alias = q0;的方式来为量子比特变量定义别名,而其中的变量q0qs必须是已经使用using语句定义过的量子比特变量。下面展示了一个简单的示例:

using(qs: qubit[2]){ qubit q1 = qs[1]; // 变量"q1"是量子比特"qs[1]"的别名。 }

2.2.2.2. 赋值语句

赋值语句能够将变量与特定类型的值绑定在一起。基本类型变量,元组,数组或者数组中任意元素均可以进行赋值操作。而赋值语句中的表达式可以是一个基本类型常数,另一个变量、某个数组元素或者函数调用等形式。赋值语句的定义如下:

<variable> = <right expression>;

在赋值语句中,等式左边的变量与等式右边的表达式必须保持相同的数据类型。因此,程序员在编写赋值语句时,必须确保等式两边类型一致,否则编译器将会报错。下面给出了一些简单的示例:

int b = 3; // 正确的变量赋值 int a = 1.0; // 错误:双浮点型数值不允许赋值给整型变量 int a = toInt(1.0); // 通过函数调用将返回值赋值给变量

此外,由于当前基于MLIR的编译器不支持锯齿状数组,因此对数组型变量赋值时,必须使用规则数组进行赋值。并且,青果语言中的内置函数.length可以对数组的长度进行检索。例如:

int len; int[2] array1; int[2][2] array2; array1 = {1, 2}; array2 = {{1, 2}, {3, 4}}; len = array1.length;

下面为有关赋值语句的其他内容:

  • 使用青果语言编写量子内核程序时,用户可以编写一些复合的赋值语句。例如,赋值语句a += b等价于a = a + b。操作符+-*/%允许与=结合以实现更加简洁的赋值操作,但是该语法糖仅适用于对float型以及int型数值进行操作。
  • 在条件表达式中,操作符<><====>!=可以作用于float型或int型数值。此外,操作符==!=仅适用于bool型数值。
  • 在逻辑表达式中,操作符&&||!只能作用于布尔值。

另外,青果语言中的一元运算符规定进行右结合,二元运算符则规定进行左结合。下面列举出了有关运算符的一些详细信息,其中操作符的顺序按照优先级从高到低进行排列:

Operator Arity Parameter type Description
! Unary Bool Logical Not
-, + Unary Int, Double Hold or reverse value
*, / Binary Int, Double Multiplication, division
% Binary Int integer modulus
-, + Binary Int, Double Addition, subtraction
<, <=, >, >= Binary Int, Double Less-than, less-than-or-equal, greater-than, and greater-than-or-equal comparisons
==, != Binary Int, Double, Bool equal and not-equal comparisons
&& Binary Bool Logical AND
|| Binary Bool Logical OR

在青果语言中,表达式中不允许混合使用floatint型的数值,例如3.0 + 5。但是,青果语言提供了一些内置函数,这些函数能够对int型和double型数值进行类型转换。其中:

  • 函数toInt():将表达式从double类型转换成int类型。
  • 函数toDouble():将表达式从int类型转换成double类型。

2.2.2.3. 程序流控制语句

青果语言支持使用关键字if-else/for/while进行程序流控制,这一点与C语言十分类似。此外,含有关键字forwhile的循环语句中允许使用关键字continuebreak

以下有关程序流控制语句的几点特性值得注意:

  1. 青果语言不支持使用关键字switch
  2. 与C语言不同的是,在使用if-else/for/while构建的控制结构中,花括号{...}不能省略,即使条件后面仅仅只有单个语句。
  3. 在青果语言中,允许将获取到的测量结果用于条件判断(这种条件被称为动态条件)。目前不同版本的青果编译器对于动态条件的支持略有不同。
    • 由于QCIS量子指令集不支持实时反馈控制,因此,目前基于MLIR的青果编译器暂时只支持静态已知条件的控制语句。未来版本的青果编译器将会全面支持动态条件的控制语句。
    • 通过借助eQASM量子指令集中的经典指令,基于Xtext的青果编译器(后端能够生成eQASM指令)能够支持动态条件的控制语句。

下面给出了一些有关程序流控制语句的简单示例:

operation main(): unit { bool a = true; int b; if(a) { b = 1; } else { b = 2; } if (false) { b = 3; } }
operation main(): unit { int a = 1; for(int i = 0; i < 10; i += 1) { a += 1; // 该for循环体将会重复执行10次 } }
operation main(): unit { int a = 0; while(a < 10) { a += 1; } // 该while循环体将会重复执行10次 while(false){ a = 100; } // 该while循环体不会执行 }

2.2.2.4. 函数调用语句

青果语言中的函数调用与C语言中的函数调用类似,其通过函数调用表达式和终止分号;进行定义。需要注意的是:

  • 在同一个青果文件中定义的操作可以相互调用,且无需前向声明。
  • 为了管理一个大型的青果工程,可以将多个操作集中在同一个青果文件中(该文件可以作为被其他文件调用)。某个文件若想导入其他文件中的操作,则可以使用import语句达成目的。
  • 基于MLIR的青果编译器将会根据预定义的规则,然后通过依次搜索以下路径来获取模块:
    • 文件所在的路劲。
    • 默认路径(例如~/quingo/)。
    • 由系统变量指定的路径(例如 QUINGO_MODULE_PATH)。
    • 系统模块路径(例如/user/lib/quingo/)。
  • 一个青果工程应当被组织在一个文件夹中。
    • 文件夹根目录应当包含一个后缀名为.project的文件,用来声明该文件夹是一个工程。
    • 模块的名称应当根据从库的根目录到文件的路径来进行定义。例如,文件<mypkg_dir>/a/b/c.qu对应的模块名称应为mypkg.a.b.c

下面展示了一些简单的函数调用示例:

opaque CNOT(q1: qubit, q2: qubit): unit; opaque H(q: qubit): unit; opaque measure(q: qubit): bool; operation bell_state(q1: qubit, q2: qubit) : unit { H(q1); CNOT(q1, q2); } operation main() : bool[] { bool[2] a; using(q1: qubit, q2: qubit) { bell_state(q1, q2); a[0] = measure(q1); a[1] = measure(q2); } return a; }

青果语言侧面支持高阶函数以及函数数组。编译器实现方面,目前基于Xtext的青果编译器可以支持高阶函数调用,但是暂时不支持函数数组功能。

完整Quingo语言手册

发表感想(评论支持markdown)


你需要登录后才可以回帖

游客

评论列表(0)

在线咨询