本章涵盖以下内容:
- 比较量子位(Qubits)与(经典)比特(Bits)
- 学习两种量子位的表示方法
- 理解量子门(Quantum gates)如何允许我们在量子位上执行操作
- 使用StrangeFX来可视化简单量子门的效果
在创建使用经典计算机的典型应用程序时,大多数开发人员不会考虑底层的晶体管和操作,这些操作最终使应用程序能够在硬件上执行。经典硬件在某种程度上是商品化的,大多数开发人员认为它是理所当然的,不予过多关注。硬件的工作细节对大多数应用程序来说并不相关。高级编程语言让我们免于接触底层(汇编)代码,而芯片设计标准使我们更不需要理解计算机硬件的物理工作原理。
但这种情况并非始终如此。在经典计算的早期阶段,没有高级编程语言,开发人员更加接近底层硬件。随着经典计算机硬件越来越普及和标准化,焦点转向了更高级的编程语言。
可以预期,量子计算也会走类似的道路。将来,使用量子计算的开发人员将不需要了解量子计算(QC)的基本概念。与经典计算机类似,高级语言和中间层将使我们免于接触硬件的实现细节。但现在,如果我们想要使用量子计算,至少了解一些基本原理肯定会有所帮助。
在本章中,我们介绍这些基本概念。我们讨论量子位和量子门,并简要涉及实现它们的与物理世界的联系。这并不是量子力学的入门章节;对于感兴趣的读者,建议参考专业文献。
经典比特 vs. 量子位
假设你在一家银行工作,你需要确保每个客户的账号号码和余额都被存储并能够被检索。开发人员需要能够处理这些信息。在经典计算机上,你可以如何表示这些信息呢?
以前,计算机能够处理以计算机理解的方式表示的信息(数字、文本、图像、视频等)。经典比特是最常见的低级结构之一,被大多数开发人员所理解和使用。一个比特包含了经典计算中最细粒度的信息;它的值可以是0或1,如图3.1所示。
通常,比特按顺序分组成8个一组,称为字节(Byte,如图3.2所示)。
在经典算法执行的任意时刻,每个比特都处于特定的状态:0或1。因此,一个字节在任何给定的时刻也处于特定的状态。字节中的每个8个比特要么是0,要么是1。
计算机的内存大小通常用处理器可以访问的比特数来表示。内存的大小是影响计算机质量和性能的主要因素之一。计算机的内存越大,可以容纳的数据量就越多。比特数是描述CPU指令长度和数值精度的重要特征。(例如,Java的long有64个比特,而Java的int有32个比特。)
比特的核心概念——即其在给定时刻的值要么是0要么是1——也是它的局限之一。在量子计算中,与比特相当的是量子位(qubit)。与比特类似,量子位可以保存值0和1。但与比特不同的是,量子位还可以保存0和1状态的组合值。当出现这种情况时,量子位处于所谓的叠加态。虽然这一点一开始可能听起来让人费解,但实际上它与一些最基本的粒子在自然界中发生的现象密切相关,并直接与量子力学的核心概念相连。量子位的叠加态在自然界的基本粒子中出现是建造量子计算机是现实的有力证据。经典计算机忽略这些量子效应;因此,经典硬件在没有涉及量子效应时无法无限地缩小。
当测量量子位时,它会返回0或1,而不是介于两者之间的值。量子位的叠加态与测量时实际值之间的关系在下一章中进行了解释。大致而言,叠加态与给定的量子位在测量时保存值0或值1的可能性有关,如图3.3所示。
我们将在第4章中讨论量子位0和1态的叠加态概念。目前最重要的部分是,由于单个量子位包含的信息比0或1更复杂,因此一组量子位可以包含比相同数量的经典比特更多的信息。这对于那些理论上需要指数级增加比特数才能处理线性增加复杂性的问题或算法来说非常重要。
量子位(Qubit)表示法
虽然我们尚未详细讨论叠加态,但前面的段落表明,我们不能(总是)通过单一的0或1来确定量子位的状态,这意味着我们需要一种不同的符号来描述量子位的状态。有多种量子位表示法,根据使用情况(展示电路状态,解释门操作等等),可能会有一种表示法优先于其他表示法。
我们将涵盖两种表示法:Dirac符号和向量符号。在本章中,我们只考虑简单的情况。当我们在下一章中讨论叠加态时,我们将回到这些表示法并进行扩展。目前,我们只考虑量子位的基本态,它们代表了值0和1。
一量子比特
对于量子比特处于其基本状态之一的简单情况,单量子比特的向量表示是直接的。我们将量子比特表示为一个具有两个元素的向量。如果量子比特保存值0,向量的第一个元素是1,而第二个元素是0,如方程式3.1所示。
这个量子比特的狄拉克符号表示如下:
由于这两种表示是可互换的,我们也可以写成:
类似地,如果量子比特保存值1,我们可以用一个向量表示,其中第一个元素为0,第二个元素为1。这个单量子比特的狄拉克表示为|1〉;因此,这两种表示可以写成:
多个量子比特
在一个拥有多个量子比特的系统中,使用狄拉克符号表示量子比特的状态是通过将各个量子比特连接起来实现的。两个量子比特,每个量子比特的值都为0,可以用如下表示:
这通常简写为:
多量子比特系统的向量表示需要进行一些向量运算。代表多量子比特系统的结果向量是通过对每个量子比特的向量进行张量乘法得到的。张量乘法在附录B中有解释。尽管附录提供了更多的了解,但您不需要知道这些向量是如何得到的:
在第一个量子比特为1,第二个量子比特为0的系统中,量子比特的符号表示如下:
因此,序列中的每个位表示是否应将相应的2的幂加到十进制数中。当位为1时,加上相应的2的幂;当位为0时,不加。序列最右边的位被称为索引为0的位,其左边的位的索引为1,依此类推。通常,具有索引i的位对应于2的i次幂的值。
狄拉克符号和向量符号之间还有另一种方便的关系。如果我们将量子比特看作位(bit),则狄拉克符号中的位等于一个整数值:
让我们将这个表示法与方程式3.8中的向量表示进行比较。在那个向量中,唯一一个值为1的元素出现在位置2(假设我们从位置0开始计数)。正如之前所示,|10〉对应于十进制值2,而在相应的向量中,这匹配了第二个元素(再次假设我们从位置0开始计数)。这在图3.4中展示出来。因此,如果我们将狄拉克符号中的位读作一个十进制数N,相应的向量除了位置N(从0开始计数)处的元素为1之外,其余全为零。
如果我们向系统添加另一个量子比特,我们需要进行另一个张量乘法。一个三量子比特系统,其中第一个量子比特为1,第二个量子比特为0,第三个量子比特为1,可以表示如下:
请注意,之前提到的关系仍然成立:|101〉是整数5的数字表示,如果我们将向量的第一行视为第0行开始计数,那么第5行的元素等于1。
随着位数的增加,得到的结果向量的大小迅速增长。一般而言,对于n个量子比特,得到的向量包含个元素。
您可能会想知道为什么我们让它变得如此复杂。为什么在使用只有三个量子比特时我们需要一个包含八个元素的向量,而且其中只有一个元素为1?答案将在第4章中给出。到目前为止,我们讨论的是处于基本态的量子比特;当我们谈论处于叠加态的量子比特时,用这种方式表示量子比特将变得有用,甚至是必要的。
门(Gates):操纵和测量量子比特
能够表示和存储数据是好的,但在计算中,我们需要能够操纵数据:需要处理表格,需要应用利率,需要改变颜色等等。在像Java这样的高级软件语言中,有大量的库可用于操纵输入数据。在最低层次上,所有这些操作都归结为对计算机系统中的位进行一系列简单的操作。这些低级操作是通过门来实现的。可以证明,通过有限数量的门,可以实现所有可能的情况。
门通常使用简单的图示来表示。一个简单的经典门是NOT门,也称为反相器。该门在图3.5中展示出来。
该门有一个输入位和一个输出位。该门的输出位是输入位的反相。如果输入为0,则输出为1。如果输入为1,则输出为0。
通常通过简单的表格来解释门的行为,其中列出了输入位的所有可能组合,并且结果输出显示在最后一列中。表3.1显示了NOT门的行为。
| 输入 | 输出 |
|---|---|
| A | NOT A |
| 0 | 1 |
| 1 | 0 |
当门的输入为0时,输出为1。当门的输入为1时,输出为0。
NOT门涉及一个位,但其他门涉及更多位。例如,XOR门接受两个位的输入,并输出一个值,当且仅当两个输入位中只有一个位为1时,输出为1,否则为0。该门在图3.6中展示出来。
表3.2显示了XOR门的行为。
| 输入 | 输出 | |
|---|---|---|
| A | B | A XOR B |
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
量子门具有与经典门类似的特征,但也存在重要的区别。与经典门一样,量子门操作的核心概念是量子比特:它们可以改变量子比特的值。经典门和量子门之间的一个重要区别是,量子门应该是可逆的。也就是说,始终应该有可能应用另一个门,并回到在第一个门应用之前的系统状态。这一限制源于自然规律:量子力学是可逆的。如果我们希望使用量子力学提供的硬件,我们的软件规则应与硬件限制保持一致。因此,创建一个不可逆的门将使得在量子硬件上实现软件堆栈成为不可能。
而在经典门中没有这样的限制。例如,XOR门是不可逆的。如果XOR门的结果是1,我们无法知道第一个位或第二个位是否为0。
由于门操作需要是可逆的,量子系统需要不同于经典系统的门。因此,低级别的量子应用需要与低级别的经典应用采用不同的方法。
第一个[量子]门:Pauli-X门
让我们继续上面银行的例子。你创建了一个存储数据(账号和余额)的系统。现在有人要求你修改余额,例如通过应用利息。你需要操纵数据。你将如何做?
软件开发的核心理念之一是编写能够操作数据的功能,比如给所有余额增加1欧元。这需要有能力修改数据,在传统计算机中,这种操作会在大规模上进行。
如果你希望量子计算机执行你的算法,那么这些计算机应该能够操作数据。从底层来看,这就是量子门所做的。第一个量子门的例子是Pauli-X门,如图3.7所示。
这个门会颠倒量子比特(qubit)的值。当我们在下一章深入讨论叠加态时,我们将回到这个例子。目前,我们只考虑特殊情况,其中量子比特处于0或1的状态。Pauli-X门会将0的值翻转为1,反之亦然。
这个过程是可逆的。如果在应用Pauli-X门后,一个量子比特的值变为1,我们就知道在门应用之前它的值是0。相反,如果最终的值是0,我们就知道原始值是1。因此,可逆门的原则目前仍然成立。通过在第一个Pauli-X门应用后再应用第二个Pauli-X门,我们可以保存系统的原始状态,如图3.8所示。
在奇妙的世界中玩弄量子比特
目前我们还没有讨论叠加态和纠缠态,而且我们对量子比特的介绍也比较基础。当我们解释叠加态和纠缠态的概念时,量子计算的真正威力将得到展示。然而,现在我们已经可以使用Strange创建一个简单的应用程序,来观察Pauli-X门的作用。
在第二章中,我们使用了Strange的高级API来创建一个应用程序,使用量子算法返回随机值。在接下来的演示中,我们使用了Strange的低级API,直接与量子比特和门进行交互。列表3.1中的代码创建了一个单一的量子比特(初始值为0),然后应用Pauli-X门,并测量得到的值。
提示:这个演示的源代码可以在示例代码库的ch03/paulix目录中找到。有关如何获取示例代码的更多信息,请参阅附录A。
public static void main(String[] args) {
QuantumExecutionEnvironment simulator =
new SimpleQuantumExecutionEnvironment(); ❶
Program program = new Program(1); ❷
Step step = new Step();
step.addGate(new X(0));
program.addStep(step);
Result result = simulator.runProgram(program); ❸
Qubit[] qubits = result.getQubits();
Qubit zero = qubits[0];
int value = zero.measure();
System.out.println("Value = "+value); ❹
}
❶ 创建一个用于声明和执行量子应用程序的环境
❷ 定义一个程序
❸ 在定义了程序之后,可以在环境中执行该程序,并获得一个结果。
❹ 对结果进行处理并返回给用户。
现在,让我们运行代码:
mvn javafx:run
如预期的那样,程序的输出如下所示:
Value = 1
在这段代码中,我们介绍了一些Strange中的概念。我们谈论了执行环境,由步骤组成的程序以及一些结果。请注意,这些概念通常在各种量子计算模拟器和编辑器中都会被使用到。
量子执行环境接口(QuantumExecutionEnvironment interface)
量子应用程序的执行物理位置和条件与开发者无关,并且选择仍在不断发展中。已经有云服务提供真实的量子基础设施(例如IBM和Rigetti),但也可以假设量子协处理器能够执行量子应用程序。目前,大多数量子应用程序在量子模拟器上运行,这些模拟器可以在本地或云环境中运行。总之,有多种不同的执行环境可以执行量子应用程序。
Strange对执行环境的差异进行了抽象,并在org.redfx.strange包中提供了一个名为QuantumExecutionEnvironment的接口,为量子应用程序与执行环境进行交互提供API。Strange包含了几个QuantumExecutionEnvironment的实现,但最重要的是,使用Strange编写的量子应用程序可以在所有当前和未来的实现中运行,无需进行修改。
最简单的执行环境使用内置的模拟器,并可以通过以下方式实例化:
QuantumExecutionEnvironment simulator = new
SimpleQuantumExecutionEnvironment();
org.redfx.strange.local包中的SimpleQuantumExecutionEnvironment提供了一个量子模拟器,它使用经典软件执行量子操作。显然,它比真实的硬件慢,而且由于量子模拟器在处理大量量子比特时需要大量内存,因此不建议在大量量子比特的情况下使用。对于本书中的演示,SimpleQuantumExecutionEnvironment已经足够好用。
Program类
如果你想在Strange中创建一个量子应用程序,你需要创建一个Program类的新实例。Program类位于org.redfx.strange包中,它为你想编写的量子应用程序提供了一个入口点。
Program类的构造函数需要一个整数参数,用于定义你在应用程序中将使用的量子比特数量。对于我们的简单应用程序,我们将使用一个量子比特,这就解释了这行代码:
Program program = new Program(1)
步骤和门
一个量子程序由一个或多个作用于量子比特的步骤组成。每个步骤由Step类的一个实例来定义。Step类也位于org.redfx.strange包中,并具有无参数的构造函数。在一个步骤内,你可以定义使用哪些门。
在我们的例子中,我们有一个单一的步骤,创建方式如下:
Step step = new Step()
步骤通过添加门进一步定义。在这里,我们使用Pauli-X门,它由org.redfx.strange.gate包中的X类定义。Pauli-X门的构造函数需要传递一个整数,这个整数是门作用的量子比特的索引。在这个例子中,由于我们有一个单一的量子比特,索引为0。创建这个门并将其添加到Step实例的代码如下所示:
step.addGate(new X(0));
在单个步骤中,每个量子比特最多只能受到一个门的影响。一个门可以作用于多个量子比特,但是在同一个步骤中,两个门不能作用于同一个量子比特。以下代码片段是错误的,因为我们在同一个步骤中添加了两个门,而且这两个门都作用于相同的量子比特(索引为0):
step.addGate(new X(0));
step.addGate(new H(0));
如果你尝试在应用程序中使用这个代码片段,Strange将会抛出一个IllegalArgumentException异常,其中包含消息“Adding gate that affects a qubit already involved in this step.”(添加影响已经参与此步骤的量子比特的门)。
请注意,我们在这里引入了另一个门:Hadamard门,由H类表示。我们将在下一章介绍这个门;在这里我们只是用它来展示在一个单独的步骤中不允许有两个门作用于同一个量子比特。
在这一点上,我们程序中的单个执行步骤已经准备好了。现在我们需要告诉Program实例,我们的Step实例应该被添加到程序中:
program.addStep(step);
结果(Results)
当一个量子应用程序或程序被执行后,可以获得一个结果。我们简要提到过一个量子比特可以处于所谓的叠加态,但一旦它被测量,它将保持值0或值1。因此,在量子应用程序中不可能有中间结果。然而,并非使用真实物理量子比特的量子模拟器不具有这个限制;出于调试目的,可以使用中间值,并且在第7章中我们将展示它们是如何有用的。
Strange在org.redfx.strange包中定义了Result类,它的实例是由执行环境创建的。当在QuantumExecutionEnvironment上调用runProgram()方法时,结果将会返回:
Result result = simulator.runProgram(program);
Result类的实例包含关于量子系统最终状态的信息。我们在接下来的章节中会详细讨论这一点。目前,我们只关注我们系统中的单个量子比特的状态。
Result类包含一个获取量子比特的方法:
Qubit[] qubits = result.getQubits();
因为我们系统中只有一个量子比特,可以通过以下方式获取它:
Qubit zero = qubits[0];
在程序执行后,我们可以查询该量子比特的值:
int value = zero.measure();
最后,我们使用简单的Java命令打印该值:
System.out.println("Value = "+value);
最初,量子比特处于0状态。我们的简单应用程序将量子比特通过一个Pauli-X门,并且测量新的值,结果总是等于1。对于某些算法,你可能希望量子比特最初处于1状态:这可以通过在算法的第一步使用Pauli-X门来轻松实现。
可视化量子电路
在前面的例子中的代码并不难理解和跟踪,但它表示一个只涉及单个量子比特和单个门的简单量子电路。当应用程序变得更复杂时,阅读代码并清楚地理解发生的事情可能会变得困难。许多量子模拟器或允许我们生成量子应用程序的应用程序都配备了可视化工具。
Strange库有一个附带的库叫做StrangeFX,它允许我们直观地呈现程序。StrangeFX也是用Java编写的,并且它使用JavaFX,标准的Java UI平台,进行渲染。本章代码中的paulixui示例展示了这个库的使用。
当你有一个Program时,可视化它非常简单。如果你使用的是Maven构建系统,包含StrangeFX库很简单。我们只需要在pom.xml文件中添加两个新的依赖:
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId> ❶
<version>15</version>
</dependency>
<dependency>
<groupId>org.redfx</groupId>
<artifactId>strangefx</artifactId> ❷
<version>0.0.10</version>
</dependency>
❶ javafx.controls模块允许我们创建图形控件组件。该模块依赖于一些其他javafx模块,这些模块会通过传递加载。
❷ 获取org.redfx.strangefx artifact。它包含了可视化量子电路所需的代码。
当使用Gradle构建系统时,情况类似。你需要修改build.gradle文件并添加StrangeFX的依赖,如下所示:
plugins {
id 'application'
id 'org.openjfx.javafxplugin' version '0.0.10' ❶
}
repositories {
mavenCentral();
jcenter();
}
dependencies {
compile 'org.redfx:strange:0.0.17'
compile 'org.redfx:strangefx:0.0.10' ❷
}
javafx {
modules = [ 'javafx.controls' ] ❸
}
mainClassName = 'org.redfx.javaqc.ch03.paulixui.Main'
❶ 添加JavaFX插件给Gradle
❷ 依赖StrangeFX
❸ 在应用程序中使用javafx.control模块
请注意,我们将此行添加到插件部分:
id 'org.openjfx.javafxplugin' version '0.0.10'
这个插件确保可以使用运行JavaFX应用程序所需的所有代码。此外,我们还需要将StrangeFX的依赖添加到依赖项列表中:
compile 'org.redfx:strangefx:0.0.10'
最后,由于我们的应用程序使用了JavaFX的controls模块,我们需要告诉Java系统加载它:
javafx {
modules = [ 'javafx.controls' ]
}
渲染一个程序所需的代码很简单。StrangeFX包含一个名为org.redfx.strangefx.render.Renderer的类,该类具有以下静态方法:
Renderer.renderProgram(Program program);
该方法会分析程序,并在每个量子比特的线上创建其可视化表示。初始状态,即所有量子比特都处于|0〉状态,位于左侧。向右移动时,遇到量子门时会显示它们的图像。在该线的末尾,将显示测量结果为1的概率。因此,如果我们想要渲染之前组成的电路,我们需要修改我们应用程序的结尾,如下所示:
int value = zero.measure();
System.out.println("Value = "+value);
Renderer.renderProgram(program);
}
运行这个程序会渲染出图3.9所示的用户界面。在这个图中,可视化组件指的是应用程序的不同组件,我们已经进行了标记。
总结
- 量子计算的基本概念是量子比特(qubits,或称为量子位)和量子门。
- 这些概念与经典计算中的对应概念有相似之处,也有不同之处。
- 量子比特的状态可以用不同的表示法来表达:Dirac表示法或向量表示法。
- 使用基于Java的Strange量子模拟器,可以将量子门组合成量子应用程序。
- Pauli-X门是其中一种量子门,可以在量子应用程序中使用。