科学计算最佳实践

本文探讨了科学家在进行科学计算时应遵循的最佳实践,旨在提高生产力和软件可靠性。建议包括编写可读性强的代码、自动化重复任务、使用版本控制、避免重复工作、进行错误规划、使用测试和调试工具、合作开发以及文档化。这些实践能够减少错误,增强代码的可重复使用性,从而加速科学研究进程并提升结果的可信度。
摘要由CSDN通过智能技术生成

科学计算最佳实践

http://tommwq.tech/blog/2020/10/23/172

来源:http://blog.sina.com.cn/s/blog_5f234d470101c8i4.html

原文:https://arxiv.org/pdf/1210.0530v3.pdf

王邦柱 译

摘要:科学家们花费越来越多的时间来编写和使用软件。然而,大多数科学家从未闻及如何高效做这件事。结果,很多科学家根本不知道可以事半功倍写出更稳定、更易维护的代码的工具和方法。这里我们描述了一系列具有坚实研究和经验基础的科学软件开发的最佳实践。这些实践将提高科学家的生产率,也将提高他们开发软件的可靠性。

就现代科研而言,软件和显微镜与试管同等重要。从那些单纯研究计算问题的小组,到传统的实验室和野外科学家们,越来越多的日常科学工作都要使用计算机。这些工作包括开发新的算法、管理和分析研究项目产生的大量数据、组合全异的数据集评估合成问题等。

科学家一般都是自己开发需要的软件,因为开发这样的软件需要大量的本领域知识。结果,最近的一个研究发现科学家们花费30%或者更多的时间来开发软件。然而,90%或者更多的科学家都是自学的,缺少基本的软件开发实践诸如:编写可维护的代码、使用版本控制和问题追踪器、代码评议、单元测试和任务自动化等。

我们认为,软件不过是另一种实验设备,应当像任何物理设备一样,小心地制造、检查和使用。尽管大多数科学家仔细地检验他们的实验室和现场设备,但是他们却并不知道他们的软件的可靠性。这将会导致严重错误,影响到发表的研究的中心论点:最近出现的引人注目的因为计算方法错误而撤稿或增加技术评论、订正的就包括Science,PNAS,Journal of Moleclar Biology等。

此外,因为软件常常会被多个项目使用,也常被其他科学家重用,所以,计算错误可能会对科学的进程带来巨大的影响。这种级联式的影响已导致多起因另一个小组的代码在结果发表后才发现存在错误而撤稿的事件。由于设计了后续的试验,并不要求所有的条目都要按照最准确的软件实践标准完成。不过,科学家应该明了提高自己的和评论他人的科学计算工作的最佳实践。

本文描述了一系列容易采用并已被很多研究证明有效的实践方法。我们的建议基于多年编写软件和教授科学家科学计算、其他研究组的报告、商业和开源软件开发的指导书、通用科学计算和软件开发等的经验积累。这些实践中的任何一个都不能保证高效无错的软件开发,但是协力使用这些实践将减少科学软件中的错误,使软件易于重用。也将节省软件作者的时间和精力,让科学家把精力集中在科学问题背后的方程上。

1.为人而非为计算机写程序

写软件的科学家应该写既能准确执行又能容易被其他程序员阅读和理解的代码(特别是未来作者自己)。如果软件很难被读懂,那么我们也很难明白这代码实际上是不是做了该做的事情。因此,为了高效,软件开发者应该考虑人类认知的几个方面:特别的,人类工作记忆是有限的;人类模式匹配能力是精调输入的;人类注意力集中时间是短的。

第一,程序不能要求其读者同时记忆大量的事实。人类的工作记忆一次只能保持很少的几个条目,每一个条目或者为单独的事实或者为有关联的多个事实聚集体。所以,程序员应该限制完成一项工作需要记住的条目的总数。达到这一目的的最主要方法就是将程序分解为容易理解的函数,每一个函数处理一个容易理解的任务。这种使程序的每一块容易理解的方式就像将科学论文分节、分段一样。例如,某用来计算矩形面积的函数可以使用四个分离的坐标:

def rect_area(x1,y1,x2,y2):

...calculation...

也可以使用两个点:

def rect_area(point1,point2):

...calculation...

后者更容易阅读和记忆;前者更容易导致错误,至少它可能发生在调用时的参数错位:

surface = rect_area(x1, x2, y1, y2)

第二,名字应该一致、有特色且有意义。例如,使用非描述性的名字如a和foo,或者非常近似的名字,像result和result2,都将导致混淆。

第三,代码风格和格式应该一致。如果一篇论文的不同部分使用了不同的格式和大小写习惯,将令论文更难阅读。同样的,如果程序不同部分使用了不同的对齐方式或者程序员混合使用了CamelCaseNaming和pothole_case_naming,代码将花费更多的阅读时间,读者也会犯更多的错误。

最后,如果可能,软件开发的所有方面都要分解成一个小时左右可完成的任务。我们的大脑会疲劳:短期情况下,一个小时到九十分钟之后,注意力强度开始下降,错误率迅速提高。实际中,这意味着一次只能处理50-200行的代码;长期的情况,每周工作40个小时左右,总的生产率最大。超过这个点之后,错误率迅速上升。

2.自动化重复的工作

计算机最初的设计就是做这类重复性的工作的,但是直到今天,仍有很多科学家一遍遍的敲着同样的命令或者用鼠标重复的点着同一个按钮。这除了浪费时间外,迟早有一天,甚至最细心的研究者也会失去注意力,犯下错误。

实践中,科学家应该依靠计算机来完成重复的工作,保存常用的命令为文件而重用。例如,大多数命令行工具有一个“历史”选项,允许用户查看和少量改动文件名或参数来重复执行最近的命令。这是命令行界面持续流行的一个原因:“再做一次”节省时间也减少错误。

交互系统中一个包含多条命令的文件被称为脚本,尽管在实际中脚本和程序并无区别。Unix的shell,python,R和MATLAB的解释器都让用户轻易的使用命令,并能记录这些命令。就如我们将在第10部分讨论的,这也能提高可重现性。

当这些脚本被重复同样使用或组合使用的时候,可以使用工作流管理工具。范例就是程序语言的编译和链接,不管是Fortran、C++,java还是C#。完成这个任务最流行的工具是Make,尽管现在也有很多替代工具可用。所有这些工具都允许用户表达文件的依赖性。比如,如果A和B被改动了,那么C需要使用一组命令进行升级。这些工具已经在科学工作流中得以成功采用了。

为了避免手工执行重复命令的错误和低效率,科学家应该使用一个build工具来自动化科学工作流。比如,制定好中间数据文件和结果的依赖关系、程序间的依赖关系,就可以使用一条命令来产生所有需要产生的东西了!

3.使用计算机记录历史

对于所有的科学门类,小心的保持纪录都是非常重要的。就像实验记录本对于记录实验是十分关键的一样,详细的记录使用计算机进行的数据操作和计算也是至关重要。因此,应该使用软件工具自动跟踪计算工作,让每一步都能被精确捕捉。

为了最大化复用率,每一个会产生结果的程序都要自动记录一个可被其他程度读取的格式的文件。自动化搜集这些信息、标准化其格式基于多个出发点。现在不使用任何额外工具记录以下内容都是可能的:

  • 原始数据的唯一标识符和版本号
  • 程序和库的唯一标识符和版本号
  • 用来产生任意给定输出的参数值和
  • 用来产生这些输出的程序的名字和版本号

实践中,很多结果是由交互式作业产生的。这种情况下,解释器的“历史”命令可以用来保存最近的命令到一个文件中,记录一个特定的结果是如何产生的。这种文件通常作为写脚本自动化未来工作的出发点。

4.做增量的改变

“科学”程序员不像传统的商业软件开发者,而有点像开源项目的开发者,需求不是来自用户的。他们的需求很少是定下来的。实际上,科学家经常在当前版本已经产生了一些结果之前并不知道他们的程序该干什么。这挑战了依赖于事先给定特定需求的设计方法。

现在很多软件开发团队确信程序员应该小步工作、经常反馈、路线修正,而不是提前规划一月或者一年的工作。由于不同开发组具体情况不同,这些开发者的典型分步是把工作长度定在1小时左右。这些步骤通常再被组合为约一周的迭代。这种方法适应了认知约束(第一节),承认了现实需求总在变化的事实。这种方法的目标是在每一次迭代后产生可以工作的(不一定是完整的)代码。这种方法已经被使用了几十年,然而在1990s末打着“敏捷开发”的旗帜开始被特别凸显。

5.使用版本控制

科学家和其他程序员在处理代码和数据时面临的两个最大的挑战是1)追踪改变(如果新版本有错误能恢复到旧版本);2)合作开发同一程序或处理同一数据集。典型的“解决方案”是将软件发邮件给同事,或者拷贝后继版本放到共享文件夹,如Dropbox。然而,以上两种方法都是脆弱的,都可能导致混淆,都可能因重要改变被覆盖或使用了过期的文件而丢失工作。同样,这个方法也难于找出哪个版本有哪些改变或准确给出较早的特定结果是如何给出的。

工业界和开源界标准的解决方案是使用一种版本控制系统(VCS)。VCS在一个仓库(或者一系列仓库)中存储项目文件的快照。程序员可以任意修改项目中的工作拷贝,然后在他们对结果感到满意后提交改变到仓库,和同事共享。

特别的,如果多人同时编辑了某文件,VCS会高亮不同,要求他们在改变被接受前解决任何可能的冲突。VCS也存储这些文件的整个历史,允许任意的版本和元数据被检索和比较。元数据包括对改变的注释和做出改变的作者等。所有这些信息都能抽出来以提供代码和数据的出处。

很多优秀的VCS是开源的,可免费获得,诸如Subversion,Git和Mercurial。同时还有很多可免费获得的主机(SourceForge,Google Code,GitHub是最流行的)。至于编码风格,最好的几乎总是你的同时已经使用的那种。

实践中,任何手工创立的东西都该进行版本控制,包括程序,原始的现场观察数据,论文的原始文件。自动化的输出和中间文件可以在需要的时候产生。二进制文件也可以进入版本控制,但是更理智的方法是为其使用一个文档系统,然后在版本控制中存储描述它们的元数据。

6.不要重复自己(别人)

任何在两个或多个地点使用的东西都更难以维护。每次做出了一个改变或者修正,多个地点都要更新,这增加了错误和不一致的机会。为了避免这个问题,程序员应当在数据和代码上都要遵循DRY(d’not repeat yourself)原则。

对于数据,本规则意指保持每一条数据必须有一个单一的权威表示。比如,物理常数应当仅精确定义一次,确保整个程序使用同一个值。原始数据文件应有一个单一的标准版本。类似的,应该记录数据采集点并分配其一个ID,改点的任何观测值应包含该点的ID而不是重复其经纬度。

DRY原则在两个尺度上应用于代码。在小尺度上,代码应该成为模块化的,而不是“复制粘贴”的。避免“代码克隆”已经表明可以减少错误率:代码发生了改变或者修补了bug,该改变或者修补在任何位置都起作用,人的大脑中的程序模型得以保持准确。模块化让人记住代码作为一个单一块的功能,反过来也让代码容易理解。模块化的代码能更容易的用于其他项目的其他用途。

在大尺度上,“科学”程序员应重用而不是重写代码。数以百万行计的高质量开源软件可在网络上免费获得,至少覆盖了商业软件的领域。典型的,找到一个成熟的库或包解决问题要比试图自己写例程解决这些经典的问题(比如数值积分、矩阵求逆)更高明。

7.对失误的规划

失误在所难免,所以验证和维护代码的正确性长期工作面临巨大挑战。虽然没有单一的实践方法表明可以捕捉或者防止所有的失误,但是组合使用多种已被证明非常有效。

防御性程序设计

def bradford_transfer(grid, point, smoothing):

assert grid.contains(point),

’Point is not located in grid’

assert grid.is_local_maximum(point),

’Point is not a local maximum in grid’

assert len(smoothing) > FILTER_LENGTH,

’Not enough smoothing parameters’

...do calculations...

assert 0.0 < result <= 1.0,

’Bradford transfer value out of legal range’

return result

防御的第一要义就是防御性编程。程序员应该在程序中增加断言以检查操作。断言就是一个简单的描述,在程序中只在特别的点上保持“真”。下面的例子表明,断言可以用来确保输入的合法性,输出的一致性之类的问题。

优雅书写的应用程序的代码中含有可观数量的断言,就像实验室的设备中校正设备占了很大部分一样。这些断言有两方面的作用。第一,他们确保一旦发生了错误,程序能立即挂起,从而简化调试。

第二,断言是可执行的文档。换句话说,他们在检查程序行为的时候同时也解释了程序。这也让断言在很多情况下比注释更有用,因为读者可以确定他们的行为是正确的、符合情况的。

编写和运行测试

防御的第二层含义是自动化测试。自动测试能够检查并确保一个单一单元的代码能够返回正确的结果,或者检查并确保在细节被修改的时候程序的行为没有改变。这些测试是由计算机进行的,所以每次程序修改后都能在运行一次,确保这个改变没有不幸的的引入bug。

测试的核心是单元测试,本测试检查软件单一单元(通常是指单个函数或方法)的正确性。单元测试是任何质量保证的基石:可以说,如果程序中的一个组件是不可靠的,那么也不指望整个程序是可靠的。大规模的完整性测试(integration testing)检查程序段组合到一起之后能正常工作;科学计算中,通常和实验数据的或者早期的可靠程序输出结果进行比较而获知其测试结果。

在任何尺度,退化测试(regression testing)是在程序被改变后运行已进行过的测试的测试行为,以确保程序没有退化,即工作没有被破坏。通过提供这种反馈,退化测试让程序员确信他们做出的改变确实是一种进步。因此,每一个项目都应努力让退化测试简单,这样程序员才会实际去做这件事。

为了管理测试,程序员应该使用现成的单元测试库来初始化输入,运行测试和用统一的模式报告结果。所有主要程序语言,如Fortan,C/C++,IDL,MATLAB,python,R和其他经常用于科学计算的语言都能找到现成的库。究竟库如何检查程序的准确性取决于研究者对于问题的理解。测试所做的是自动的检查代码是否符合研究者期望的行为。优秀的自动测试提高我们的信心,说明代码运行正常,程序产生的结果可信。

使用测试库的一个重要好处是它鼓励程序员设计和构建可测试的代码。实践中,这意味着创造具有精心定义的接口的、基本不互相依赖的自包含的函数和类。采用这种形式设计的代码更易理解,更易重用(第六部分)。

如果一个团队准备测试不是用这种方式构建的软件,第一件事是提炼遗留的代码使其可测试。换句话说,不改变其行为且按照上面论述的方式重新组织或者重写代码,不过这可能会导致不紧密耦合的块。由于小的代码单元通常有简单的行为,提炼遗留代码通常就是一个分解函数、类或者模块为更小更可测试单元的过程。这可以使用增量式和系统化的方法做到。比如,通过在“高度不确定”区域引入测试或者使用新算法替换旧算法。

使用不同的“神谕”(oracle)

“神谕”告诉开发者程序如何行为或者它的输出应该是什么样。商业软件开发中,“神谕”通常是由商业专家写的合同或者说明书。科研领域,“神谕”包括分析结果、实验结果和由早期可信的程序产生的结果。这些都能提供有用的检查,所以程序员在测试的时候应该使用所有可用的“神谕”。

变bug为测试例子

无论软件经历多么仔细的测试,一些bug总是不能避免、需要修补。为了防止这些bug重复出现,程序员应该通过写可以触发该bug的测试,将bug变成测试例子。做这个工作是建立一套退化测试的方法,特别是对遗留程序。

使用符号化调试器(symbolic debugger)

如果承认一些bug总能逃过我们的防御,那么我们下面推荐的就是程序员应该使用符号化的调试器来追踪他们。这种工具的一个更好的名字是“交互式程序检查器”。调试器允许用户在程序的任意行暂停,检查变量值,上下查找函数调用一遍找出为何出现这种行为。

调试器通常要比添加/移除打印语句或者翻看上千行的log输出更高效,因为它允许用户仔细查看程序是如何执行的而不是仅查看程序某几个时间点的状态快照。换句话说,调试器允许科学家亲自看到哪里错了,而不是非非要用间接地证据来推断错误。

测试驱动的开发(TDD)是一个我们不倡议的实践,尽管很多人依赖于此。使用TDD,程序员在写代码本身之前先要写测试案例。这看起来是反向的,然而写测试帮助程序员在大脑中明确了代码的目的,也帮助确保确实写了测试。

我们不倡议的原因是这种方法是,2010年进行的有效性分析研究没有找到对程序高效性的任何影响。

8.仅在软件工作的时候优化它

现今的计算机和软件是如此复杂以至于甚至专家也很难预测某特定程序的哪一部分是性能瓶颈。使代码变快的最高效方法是让它正确的工作,进而决定是否值得为其提高运行速度——在需要提速的情况——使用一个效能评测器来找到瓶颈。

这个策略同样对于程序语言的选择有有意思的影响。研究确认,多数程序员尽管使用不同的语言,单位时间内写出几乎相同的代码行数。由于快速、低级的语言实现同样的功能要求更多的行数,科学家应该尽可能使用高级语言,仅在他们确定提高速度是必须的情况下使用Fortran、C等低级语言。使用这个方法将在同样的时间内写出更多的代码。即使是在开始编码之前才知道必须使用低级语言,高级语言的快速原型也会帮助程序员快速完成设计。程序员也可以使用高级语言原型作为高性能低级语言实现的测试“神谕”,即比较优化的程序和未经优化的程序的结果来检查其正确性。

9.写关于设计和目的、而非实现机制的文档

就像书写规范的实验草案让研究方法容易重现一样,优秀的文档帮人理解代码。这让代码更具重用性,降低了维护的费用。总之,有优秀文档的代码让其易于传播。比如,研究生或者博士生在实验室写的代码更传递到下一个事业阶段。参考文档和设计决策的描述是提高对代码理解的关键。然而,概括代码的内置文档是没用的。因此,我们推荐科学程序员为接口和缘由写文档,不要为实现写文档。例如,在函数开头处清晰的描述函数的功能和它的输入输出是有用的,而如下所示的代码片段的注释是无助于理解的:

i=i+1 #让变量i加1

如果一段代码需要大量的描述实现才能被理解,通常我们建议去精炼代码而不是解释它如何工作。换句话说,与其写一段文字来解释复杂的代码,不如重组织代码本身使其不需要这个解释。这可能在有些情况下不可能——有些代码可能是固有的困难——但是我们应尽量做到。

最佳的创建和维护参考文档的方法是将文档嵌入软件。做这个工作,增加程序员在改动程序的时候同时更新注释的可能性。

嵌入式的文档通常采用特殊的格式和放置位置进行注释。典型的文档生成器比如javadoc,doxygen,sphinx,将抽取注释并产生良好格式化的网页或者其他用户友好的文档。

10.合作

正如把手稿交给同行科学家评论会减少错误,让研究容易理解一样,代码评议能排除bug、提高可读性。大量的研究都已表明代码评议是最省钱的找到bug的方式。这也是一个在团队内提高知识和技能的方法。在那些人员流动的项目中,比如科研机构的实验室,代码评议能确保学生或者博士后离开实验室后关键知识不丢失。

代码可以在被提交到共享版本控制仓库前或者后进行评议。经验表明,如果在入库前没有做代码评议,那么之后几乎不会做这个评议。因此,我们建议项目使用入库前的代码评议。

一个极端的代码评议形式是结对编程,这是一种两个程序员坐在一起编程的方式。一个实际写代码,另一个提供实时的反馈。多个研究表明,结对编程提高了生产率。不过很多程序员也认为有侵入感。因此我们建议,在帮助新手快速成才和解决特定问题的时候使用结对编程。

一旦一个团队达到一定规模,追踪什么需要评议或者谁在干什么变得困难。因此,团队应该使用问题追踪工具,维护一个需要完成的任务和需要修复的bug的列表。这有助于避免重复工作,也让任务传到不同的人手中更容易。在线工具如GitHub,本地化的工具如Trac。

结论

我们基于大量的研究和我们搜集的经验,已经给出了一系列建议的科学计算的最佳实践。这些实践可被用于个人的工作,也可用于团队的工作;单独的,也可组合的用。它们提高了科学计算的生产率和结果代码的可靠性,因此加快了我们得到结果的速度、提高了我们的信心。我们确信,它们也是可再现计算研究的先决条件:如果软件不进行版本控制、缺乏可读性、不被测试,其作者再次得到结果将触不可及。

研究建议科学计算中用于实现这些工具和方法的时间花费会被程序员的高效率立即补偿。尽管如此,上文描述的建议实现起来看上去很吓人。幸运的是,不同的实践之间互相加强和支持,所以需要的努力少于单个实践之和。尽管这样,我们不建议研究团队一次性实现所有的建议,而是建议这些工具能在一段时间内渐进的被引入。

如何实现这些建议的实践可以很容易的从很多优秀的网络教程或者研讨会、专门课程中学到。

计算现在是实践科学的中心。从科学研究整个过程中要求的精确级别看,科学家有必要采用提高软件质量和效率的工具和方法。这样做将提高我们对科学计算结果的信心,将允许我们在重要科学问题上做出快速的进步。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值