本章涵盖以下内容:
- 介绍量子计算模拟器
- 使用Strange进行高级和低级编程
- 使用Strange和StrangeFX调试量子应用程序
- 理解运行时目标:本地、云端和真实设备
- 软件开发工具有特定的目标。
有些工具有助于提高开发者的生产力;有些工具帮助管理依赖关系或者方便地访问特定的框架。使用这些工具的开发者应该了解工具能做什么以及它们的限制。在本章中,我们将解释量子计算模拟器的优势,并探索Strange的一些具体功能,使现有(Java)开发者能够轻松使用量子计算算法。Strange,像其他任何量子计算模拟器一样,不会通过将量子魔法应用于应用程序来解决所有问题。但它将帮助我们在不是量子计算专家的情况下受益于量子计算。为了最大限度地利用Strange提供的优势,对量子计算工具有一定的了解是有帮助的。这是本章的重点。
第2章中HelloWorld示例的Java代码对Java开发者来说是熟悉的。Strange的目标是提供一个对Java开发者来说既熟悉又能够利用之前章节中讨论的量子现象的库。
对于一些开发者来说,量子计算将是一个他们不需要担心的实现细节。对于其他人来说,在正确的位置使用正确的量子计算概念可能是其应用程序的主要区别。
使用Strange,两种选项都是可行的。我们讨论了在量子硬件之上的高级编程语言的典型堆栈。在此之前,我们将其与经典堆栈进行类比。我们这样做有两个原因:
从硬件到高级语言
通常,在计算机的硬件操作和开发人员使用的高级编程语言之间有许多步骤。图7.1中的流程示意图显示了在硬件(CPU)上运行的经典软件堆栈。
注意:经典堆栈的相关硬件不仅包括CPU。然而,本章的目标不是解释经典的硬件-软件堆栈;因此,我们进行了这种过度简化。
机器语言与CPU集成。不同类型的CPU具有不同类型的机器语言;汇编语言是一种更易读的格式,但它仍然取决于CPU类型。低级语言抽象了大部分与CPU特定架构有关的内容,但仍可能需要针对不同类型的CPU(如32位或64位)进行不同处理。而高级语言如Java根本不依赖于硬件。
图7.2显示了在两种不同的CPU(AMD64和AARCH64)上编写的代码在堆栈的不同层级中的代码重用情况。在低级层面,这两种CPU的机器语言是不同的,因此没有代码重用。在堆栈的较高层次,差异较小,可以重用更多的代码。最后,在Java层面,有100%的代码重用。在AMD64 CPU上运行的Java应用程序与在AARCH64 CPU上运行的Java应用程序具有相同的源代码。
编译器和链接器确保用高级编程语言编写的应用程序最终可以在属于特定计算机的特定硬件上执行。Java平台成功的原因之一是它允许我们用单一语言(如Java)编写应用程序,然后在各种硬件上执行这些应用程序,从云服务器到运行Windows、macOS或Linux的台式机,再到移动设备和嵌入式设备。在较低的层次,不同目标系统之间存在许多差异,但我们被屏蔽了这些差异。Java平台通过Java字节码的概念实现了这一点。Java开发人员创建的Java应用程序被翻译为Java字节码,它是应用程序的平台无关表示。当应用程序被执行时,这个平台无关的字节码被翻译成特定于每个平台的机器指令。
提示:如果我们能够专注于与我们的应用程序相关的问题,开发人员的生产力将会增加。工具(如量子库)有助于屏蔽与整个项目成功相关但与我们无关的实现概念。
不同层次的抽象
使用高级语言(如Java)编写的应用程序可以在不同类型的硬件上运行。Java应用程序可以在具有AMD 64 CPU的Linux系统上执行,也可以在具有AARCH64 CPU的Linux系统或具有AMD64 CPU的Windows系统上执行。
您可能会想知道量子芯片是否可以替代现有的经典芯片,并在这些量子芯片上运行现有的应用程序。如果是这样的话,图7.2中的模式也适用于CPU为量子计算机的情况。在这种情况下,我们可以保留所有现有的语言和库,并添加另一低级抽象层,将高级语言(如Java)转换为量子硬件的一种汇编语言。
然而,正如您在前面的章节中了解到的那样,量子硬件与经典硬件存在一些区别,比如叠加(第3章)和纠缠(第4章)。如果我们想要利用量子处理器的量子能力,则上层硬件层应该使用这些功能。这意味着我们需要在软件栈的较高层次中使用叠加和纠缠,并使高级应用语言能够使用它们。这些概念在经典的汇编语言中并不存在。
注意:为了充分发挥量子计算的真正威力,核心概念(如叠加和纠缠)需要在软件栈内使用。这并不意味着它们必须在任何高级语言的顶层暴露出来。
有几种方法:
微软采用了第一种方法,而大多数其他倡议则采用了第三种方法。使用Strange,我们也采用第三种方法。第二种方法将使大多数开发人员甚至无需了解量子计算的基础知识就可以使用量子计算。这在不久的将来并不现实,但是在语言足够强大以隐藏所有量子特性的情况下还需要很长时间。即使如此,仍然会有使用情况需要直接使用量子特性。
其他量子计算模拟器的编程语言
Strange并不是唯一的量子计算机模拟器。有越来越多的量子模拟器遵循相同或不同的方法。几家大型IT公司(如微软、IBM和谷歌)也开发了量子计算机模拟器。
方式
微软创建了一种称为Q#的特定领域语言(DSL),它类似于C#和F#。使用DSL的优点在于它允许我们在语言中添加特定功能,以便利用量子特性(如叠加和纠缠)优化应用程序。然而,这种方法的缺点是我们需要学习另一种新的语言,并且还需要对量子计算有深入的理解。
IBM和谷歌采取了不同的方法。他们创建了基于Python的模拟器,这显然是一种现有的语言。这种方法的优势在于Python开发人员无需学习新的语言就可以开始使用量子计算。这与使用Strange的Java开发人员享有相同的优势。
其他语言的资源
如前所述,与量子计算机模拟器相关的研究领域正在迅速发展。目前写出的列表将很可能在本书出版时已经不再完整;然而,一些在线资源会随着其演进而不断更新。以下是一些相关资源的指引,但请注意这些资源可能会过时或迁移到不同的位置:
可以在www.quantiki.org/wiki/list-q…上找到按编程语言分类的全面的量子模拟器列表。
IBM的Qiskit项目可以在qiskit.org找到。
Microsoft关于其Q#编程语言的信息可以在docs.microsoft.com/en-us/quant…找到。
Google创建的Python量子模拟器Cirq可以在quantumai.google找到。
Strange:高级和低级方法
在第三章中创建的HelloWorld示例使用了Strange的顶层API。你也学习到顶层API使用了低级API。为了方便起见,我们在图7.3中重复了高级架构图。
高级API主要专注于Java,而低级API处理量子门。如果你想使用高级API进行开发,你会专注于Java代码。如果你想使用低级API进行开发,你会专注于带有量子门的量子电路。在内部,高级API的实现依赖于低级API,如图7.4所示的解释。
因此,高级API和低级API最终使用相同的低级概念。区别在于高级API将这些概念的复杂性隐藏起来,使我们不需要关注这些细节。
顶层API
Strange的顶层API是一个典型的Java API,遵循普通的Java模式。该API位于org.redfx.strange.algorithm.Classic类中。
注意:截至撰写本文时,Strange的版本为0.1.0。在发布Strange 1.0版本之前,API可能会更改位置。
以下是该类中一些方法的示例:
public static int randomBit();
public static int qsum(int a, int b);
public static T search(List list, Function function);
该API处理了量子计算的一些限制。例如,一旦测量了一个量子比特,它就不能再在电路中使用。这个限制来自于真实的量子世界,其中测量量子比特的物理表示会破坏该量子比特中的信息。然而,我们无需担心这个限制。Strange的顶层API是以这样一种方式创建的,它们不能强制使用与真实量子系统不兼容的情况。
让我们重复一下第二章“HelloWorld”示例中最重要的一行代码,生成一个随机比特:
int randomBit = Classic.randomBit();
Classic.randomBit() 不会抛出异常。因此,我们可以假设该实现确保与量子世界没有不一致。此外,量子门的概念从未在顶层API中暴露出来。
注意:高级API的签名不依赖于特定于量子的对象。例如,高级API调用的返回值永远不是一个量子比特。
低级API
Strange的低级API分布在不同的包中。以下是使用这些API的典型步骤:
在第2章中,我们展示了高级API Classic.randomBit()
方法如何使用低级API,并承诺在本章中详细介绍。现在,您已经看到更多的低级代码示例,因此 Classic.randomBit()
方法的实现可能更加熟悉。让我们在此处重复代码:
public static int randomBit() {
Program program = new Program(1); ❶
Step s0 = new Step(); ❷
s0.addGate(new Hadamard(0)); ❸
program.addStep(s0); ❹
QuantumExecutionEnvironment qee = ❺
new SimpleQuantumExecutionEnvironment();
Result result = qee.runProgram(program); ❻
Qubit[] qubits = result.getQubits(); ❼
int answer = qubits[0].measure(); ❽
return answer; ❾
}
❶ 创建一个使用1个量子比特的新量子程序
❷ 创建一个新步骤,稍后将添加到量子程序中
❸ 向新步骤添加在第一个量子比特(索引为0)上操作的Hadamard门
❹ 将该步骤添加到量子程序中
❺ 创建一个运行时环境
❻ 执行量子程序
❼ 量子程序修改了量子比特的状态,在此语句中,我们请求得到结果状态。请注意,尽管这是一个数组,但数组包含的是一个单独的量子比特,因为程序最初使用了一个量子比特。
❽ 测量量子比特的值:可能为0或1
❾ 将测量结果返回给调用者
从这段代码可以看出,每当调用randomBit()函数时,都会创建一个新的量子程序并执行它。然而,返回值是一个普通的Java整数,没有与之关联的量子信息。这标志着低级API和高级API之间的明确分离。这是高级API和低级API之间的另一个重要区别,图7.5中有详细说明。
希望仅使用现有Java类型的Java开发者可以通过使用高级API来实现。如果您对量子概念更熟悉或想要尝试这些类型,您可以使用低级API。
何时使用何种API?
我们可以选择使用高级API或低级API。在前面的章节中,您了解了高级API和低级API之间的区别。在这里,我们总结一下使用高级API或低级API的原因。请记住,同时使用这两种方法是可以的。在某些情况下,高级API更合适,而在其他情况下,低级API更适用。
建议使用高级API的情况包括:
- 您需要在一个项目中使用现有的、众所周知、已实现的量子算法,从而获得优势,通常是性能上的优势。
- 您想要尝试使用能够从量子算法中获益的经典代码。
建议使用低级API的情况包括:
- 您想要了解量子计算。
- 您希望尝试使用现有的量子算法。
- 您想要开发新的量子算法。
StrangeFX是一个开发工具
大多数流行的经典编程语言之所以成功,部分原因在于有助于我们在该语言中提高生产力的工具。几乎所有的Java开发者在创建应用程序时都使用集成开发环境(IDE)。
同样地,为了提高编写量子应用程序的效率,需要一些能够简化开发的工具。StrangeFX可以轻松地可视化和调试量子应用程序。
电路可视化
在前面的章节中讨论的量子电路相对简单。我们可以很容易地理解编程方法。然而,通常有助于对创建的量子电路进行视觉概览。特别是当程序变得更加复杂时,可视化变得非常重要。正如我们在第三章中解释的那样,StrangeFX库允许快速可视化量子电路。通过调用
Renderer.renderProgram(program);
调用...生成一个包含电路图形概览的窗口。
例子存储库中randombit目录中的量子程序显示了可视化。它还包含下一节中讨论的调试元素;目前,我们展示的代码没有包含调试元素:
Program program = new Program(dim);
Step step0 = new Step(new Hadamard(0), new X(3));
Step step1 = new Step(new Cnot(0,1));
program.addSteps(step0, step1);
QuantumExecutionEnvironment qee = new SimpleQuantumExecutionEnvironment();
Result result = qee.runProgram(program);
Qubit[] qubits = result.getQubits();
for (int i = 0; i < dim; i++) {
System.err.println("Qubit["+i+"]: "+qubits[i].measure());
}
Renderer.renderProgram(program);
运行此程序会显示量子比特的测量结果以及电路图,包括量子比特的概率。测量结果的输出可能是
Qubit[0]: 0
Qubit[1]: 0
Qubit[2]: 0
Qubit[3]: 1
或者
Qubit[0]: 1
Qubit[1]: 1
Qubit[2]: 0
Qubit[3]: 1
电路的可视化如图7.6所示,帮助我们理解这两种可能的结果。
可视化显示量子程序从四个量子比特开始。在第一步中,电路添加了一个Hadamard门和一个NOT门。在图的右侧,显示了结果量子比特,并显示了它们被测量为1的概率。
调试Strange代码
在前面的章节中,您学习了如何创建简单和更复杂的量子电路。量子电路最重要的限制之一是,测量一个量子比特会影响它的状态。如果一个量子比特处于叠加态,并且被测量,它将退回到0或1。它无法回到之前被测量之前的状态。
虽然这为安全性提供了很大的机会(正如我们在下一章中所解释的),但这使得调试量子电路变得困难。在典型的经典应用程序中,您经常希望在程序流程中跟踪特定变量的值。调试器在开发人员中很受欢迎,并且检查变量的变化通常提供了有价值的见解,解释了为什么特定的应用程序的行为与我们的预期不一致。
然而,如果测量变量会改变应用程序的行为——就像在量子计算中一样——这种技术就无法使用。更复杂的是,即使我们能够在测量量子比特之后恢复其原始状态,测量本身得到的只是0或1,这并不能提供所有信息。正如我们之前多次解释的那样,量子程序的真正价值不在于量子比特的测量值,而主要在于概率分布。
幸运的是,Strange和StrangeFX允许以某种方式呈现概率分布。Strange允许我们使用虚构的门ProbabilitiesGate,在程序流程中的给定时刻可视化概率向量。
我们将重用前一节的程序,但这次我们使用ProbabilitiesGate来渲染给定步骤之后的概率。代码的前半部分更改如下:
Program program = new Program(dim);
Step p0 = new Step (new ProbabilitiesGate(0)); ❶
Step step0 = new Step(new Hadamard(0), new X(3)); ❷
Step p1 = new Step (new ProbabilitiesGate(0));
Step step1 = new Step(new Cnot(0,1));
Step p2 = new Step (new ProbabilitiesGate(0));
program.addSteps(p0, step0, p1, step1, p2);
❶ 创建一个包含ProbabilitiesGate的新步骤
❷ 原始步骤仍然被创建。
再次运行示例将显示相同的电路,但是这次您将在每个步骤后看到显示的概率向量(图7.7)。
让我们仔细看一下发生了什么。添加到程序中的第一个步骤包含ProbabilitiesGate。这不会在任何时刻更改概率向量,但它会触发渲染器显示该向量。放大可视输出的左侧,我们在图7.8中看到了在声明量子比特并在应用Hadamard和NOT门之前的概率向量。
概率向量以一个被分成16部分的矩形进行可视化。第一部分表示在此时进行测量时测量到0000的概率。第二部分对应于测量到0001的概率,依此类推。
部分被着色得越多,相应的概率就越高。在这种情况下,第一部分完全被着色,这意味着测量到0000的概率是100%。这确实符合预期,因为这是一个具有四个量子比特且没有门的量子电路。初始时所有量子比特都处于0000状态,这也是您将要测量的结果。
在应用第一个真正的步骤后,该步骤包含一个Hadamard门和一个CNOT门,另一个概率向量被渲染出来。图7.9中突出显示了该向量以及相应的量子比特测量结果。
这个图显示了在此时进行测量时,16种组合中测量到其中一种的概率。再次强调,我们谈论的是16个概率,而不是四个量子比特的各自值。
从图中可以清楚地看出,在此阶段进行测量有两种可能的结果:
- 50%的概率测量到1000
- 50%的概率测量到1001
这符合我们对到目前为止应用于此电路的单个步骤的分析。将NOT门应用于最高有效量子比特(q3)将导致该量子比特被测量为1。如果不将Hadamard门应用于量子比特q0,那么其状态将为1000。将Hadamard门应用于该量子比特将导致有50%的概率将其测量为0,50%的概率将其测量为1。总之,在此步骤之后,系统有50%的概率处于1000状态,50%的概率处于1001状态,这正是概率向量所显示的。
第二步在量子比特0和1上应用CNOT门。其结果概率向量显示在图7.10中。
该图表明在此处进行测量有两种可能的结果:
- 有50%的概率测量到1000
- 有50%的概率测量到1011
这与您在第5章中创建贝尔态时学到的内容相符。应用Hadamard门后再跟随一个CNOT门会将两个相关的量子比特(q0和q1)带入纠缠态。两个量子比特都可以是0,也都可以是1。如果两个量子比特都是0,那么量子电路的总状态将被测量为1000。如果两个量子比特都是1,那么总状态将被测量为1011。这与电路结果的最终可视化(如图7.11所示)相符。
从这个图中,我们知道q2将始终被测量为0,而q3将始终被测量为1。另外两个量子比特,q0和q1,可以是0也可以是1。乍一看,这可能与我们通过查看概率向量所学到的相符,但是在仅查看量子比特测量的可能结果时,缺少了一些重要的信息。实际上,概率向量表明前两个量子比特可以是00或11,但永远不会是01或10,因为它们是纠缠在一起的。只有两种可能的组合,而不是四种。这是通过仅查看量子比特测量的潜在结果是看不到的东西。
注意:概率向量包含比单独的量子比特及其各自的概率更多的信息。后者缺少可能或不可能的组合的信息,而这正是概率向量中的内容:其中每个条目都涉及所有量子比特。
使用Strange创建您自己的量子电路
到目前为止,您已经使用Strange创建了几个量子电路。在简单的电路和真正有用的量子应用之间还有一些步骤,但了解量子计算至关重要。在本节中,我们将编写一些基本代码介绍量子算术。您会注意到,即使是简单的操作,比如两个数字相加,在量子计算机上也是相当复杂的,尤其是与在经典计算机上执行相同操作的方式相比。您可能会想知道为什么我们不使用经典计算机来执行加法和乘法等操作。请记住,量子计算的关键优势之一是我们可以使量子位处于叠加态。因此,我们不仅可以添加一个量子位包含值0或1的简单状态,还可以添加任何可能的线性组合。这使得量子计算机具有强大的能力,可以同时在多个可能的值上执行相同的算术操作。
量子算术作为介绍Shor算法的一部分
量子计算最受欢迎的潜在应用之一是整数因子分解。今天大多数广泛使用的加密技术依赖于以下事实:计算两个大素数的乘积很容易,但对于经典计算机来说,逆操作却很难,几乎是不可能的。在本书的最后部分,我们将讨论整数因子分解,并介绍Shor算法。Shor算法的数学背景超出了本书的范围,但编程挑战同样具有一定难度。Shor算法依赖于有效计算模数幂的能力。总的来说,量子计算中的算术比经典计算机中的算术更加复杂。在本节中,我们将讨论在量子计算机上进行两个量子位的简单加法。通过这样做,您将学习如何使用Strange的低级API,并对量子算法的创建有所了解。虽然两个量子位相加的示例很基础,但相同的技术也可应用于创建更复杂的算术运算。
加法两个量子位
我们从一个简单情况的经典算法开始:我们有两个位,我们想知道这两个位的和。因为每个位可以是0或1,所以有四种可能的情况:
0 + 0 = 0
0 + 1 = 1
1 + 0 = 1
1 + 1 = 2
在典型的经典方法中,该电路的输入有两个位,输出也有两个位。其中一个输出位包含两个位的和,另一个位包含进位位。如果结果位应该是2(因为它包含了1 + 1的结果),则该位被设置为0,并且进位位被设置为1。该经典电路如图7.12所示,表7.1列出了输入和输出位的可能值。
创建一个量子加法器,我们将使这个简单的例子更加简单。现在我们暂时忘记进位位。单独考虑这一位并不能解决问题,因为如果我们只看输出位S,我们无法唯一地确定哪些位是输入位。然而,如果我们创建一个电路,保持第一个量子位不变,并在第二个量子位中得到两个输入值的和,我们总是可以从结果回到输入。对于每个可能的结果,都有一个唯一的可能输入。这个量子加法器如图7.13所示,表7.2列出了可能的输入值(x和y)和输出值(保持不变的x和和S)。
如果你看一下输出值(x和S),你会发现现在可以重新构造输入值(x和y),因为每个可能的x和S组合都对应一个唯一的x和y组合。这意味着将输入转换为输出的门是可逆的。如果你仔细看表格,你会发现这些值很熟悉:这就是我们在第5章中用来描述CNOT门的相同表格!因此,我们可以使用CNOT门来做两个量子位的简单加法。让我们写代码来实现这一点:
static int add(int a, int b) {
Program program = new Program(2); ❶
Step prep = new Step();
if (a > 0) prep.addGate(new X(0)); ❷
if (b > 0) prep.addGate(new X(1));
Step step0 = new Step(new Cnot(0,1)); ❸
program.addSteps(prep, step0);
QuantumExecutionEnvironment qee =
new SimpleQuantumExecutionEnvironment();
Result result = qee.runProgram(program); ❹
Qubit[] qubits = result.getQubits();
return qubits[1].measure(); ❺
}
❶ 在此程序中,我们需要两个量子位。最初,它们保存输入值。执行后,第一个量子位仍保持其原始值,而第二个量子位则保存了它们的和。
❷ 准备量子位。如果它们应该表示1的值,我们会应用一个X门。
❸ 应用CNOT门,结果是第二个量子位保存了输入量子位的和(模2)。
❹ 执行量子程序。
❺ 测量第二个量子位(qubits[1]),因为它保存了和,然后返回和的值。
带进位位的量子算术
在经典加法中,我们放弃了使用进位位,因为我们需要创建一个可逆的量子电路。作为下一步,我们现在重新引入这个进位位到结果中。我们向输出中添加第三个量子比特,因此需要将第三个量子比特添加到输入中。此外,我们需要确保能够将每个可能的输出转换回原始输入。我们通过添加一个输入量子比特,也称为辅助量子比特(Ancilla Qubit)来实现这一点。辅助量子比特是常规量子比特,它们不直接帮助解决功能目标,但通常用于使量子电路可逆。我们的新加法电路将使用这个辅助量子比特来计算进位位。记住,当且仅当输入比特x和y处于1状态时,进位位被设置为1。这是介绍Toffoli门的好时机,因为这个门正好可以满足我们的需求。我们之前讨论的门操作是在单个量子比特上进行的(如X门和H门),或者在两个量子比特上进行的(CNOT门)。Toffoli门在三个量子比特上进行操作,可以看作是CNOT门的扩展版本。符号上,该门如图7.14所示。
这个图像与CNOT门的符号进行比较,已经暗示了它的功能:如果第一个量子比特和第二个量子比特都处于1状态,则第三个量子比特将被翻转。前两个量子比特保持不变。
我们用三种方式描述这个门的功能:
- 我们给出一些伪代码。
- 我们显示一个包含可能输入/输出组合的表格。
- 我们显示解释电路中发生的情况的矩阵。 在这三种方法中,只有最后一种方法是正确的。
在前两种情况下,我们考虑了0和1的值,但是正如您所知,量子比特可以处于这些值的线性组合中。然而,伪代码和组合表格通常有助于直观地理解发生的情况。
用伪代码描述这个门的行为如下:
out[0] = in[0];
out[1] = in[1];
if ((in[0] == 1) && (in[1] == 1)) {
out[2] = !in[2];
} else {
out[2] = in[2];
}
从这个表格可以清楚地看出,只有当in[0]和out[0]都是1时,第三个输入量子比特才会翻转。
最后,给出描述这个门的矩阵:
我们将应用这个Toffoli门来增强我们简单的量子加法器,以便它可以跟踪进位位。请记住,当两个输入位都为1时,进位位应为真。这对应于将Toffoli门应用于两个输入量子比特和一个初始值为0的第三个输入量子比特。请注意,在应用执行加法的CNOT门之前,我们必须先应用这个Toffoli门,因为CNOT门可能会改变第二个量子比特的值。
现在,加法算法的结果代码如下所示:
static int add(int a, int b) {
Program program = new Program(3); ❶
Step prep = new Step();
if (a > 0) {
prep.addGate(new X(0)); ❷
}
if (b > 0) {
prep.addGate(new X(1));
}
Step step0 = new Step(new Toffoli(0,1,2)); ❸
Step step1 = new Step(new Cnot(0,1)); ❹
program.addSteps(prep, step0, step1);
QuantumExecutionEnvironment qee =
new SimpleQuantumExecutionEnvironment();
Result result = qee.runProgram(program); ❺
Qubit[] qubits = result.getQubits();
return qubits[1].measure()
+ (qubits[2].measure()