将旧的Fortran程序移植到Python+NumPy

6 投票
2 回答
1766 浏览
提问于 2025-04-18 06:03

我最近在研究一个很大的Fortran 77程序(我刚把它表面上移植到Fortran 90)。这个软件非常老旧,主要用于通过有限元方法进行建模。

  • 这个程序真是个庞然大物,大约有240,000行代码。
  • 因为它最初是用Fortran 77写的,所以在动态内存分配上使用了一些非常“肮脏”的黑科技;基本上是混合使用了C语言的标准库函数,既有C又有Fortran。我还没完全搞明白内存分配是怎么工作的。这个程序设计得很灵活,用户可以很方便地扩展,通常需要为后续使用分配一些全局可访问的数组。这是通过一个内存地址数组来实现的,这个数组指向动态可分配数组的起始地址。当然,地址数组的每个元素指向哪个信息,完全依赖于用户需要学习的约定,用户在真正开始编程之前必须先了解这些。这里有两个地址数组,一个用于整数,另一个用于浮点数。
  • 所谓“肮脏的黑科技”,是指不一致的做法。例如,GNU编译器优化算法的更新导致程序出现随机的内存泄漏。
  • 这个程序远没有优雅可言。全局变量的名字通常很短(3-4个字符),而且很难理解。不同的程序模块之间传递数据是通过使用公共块来实现的,这些公共块包含了所有的程序开关和前面提到的数组。
  • 这个程序的使用方式大致像一个交互式命令行,但很笨。首先,程序会读取一个输入文件,然后根据选择,用户会进入一个伪命令行,在这里用户需要输入宽度为4个字符的命令,后面跟上参数。解析器会解析这个命令,然后调用相应的子程序。你可能会猜到,这个伪解析器里有一个循环结构(实际上是大量的goto语句),把子程序的行为包装得比21世纪应该有的要复杂得多。
  • 输入文件的格式是相同的(命令,然后是参数),因为它使用的是同一个解析器。但语法并不一致(我的意思是缺乏控制结构,有些命令会导致有限状态机的行为与其他命令相矛盾;语法不明确),这时用户可能会发现一些陷阱。用户必须通过经验来学习这些陷阱;我在程序的任何文档中都没有看到过这些。这是一个用Python可以轻松避免的问题,甚至不需要实现一个解析器。

我想做的事情:

  • 把程序的一部分移植到Python,特别是那些与数值计算无关的部分。这包括:
    • 用面向对象的方法清理和抽象API,
    • 给变量起有意义的名字,
    • 把动态分配迁移到numpy或Fortran 90,去掉C部分,
    • 把非数值执行迁移到Python,并使用f2py包装数值对象,这样性能不会下降。对了,我提到过这个程序在当前状态下非常快吗?希望把数值子程序和输入输出的调用移植到Python后,不会让它变得慢到不可用的程度(或者会吗?)。
    • 利用Python的交互式命令行替代伪命令行。这样,最终用户就不会遇到任何不一致的情况。前面提到的命令将简单地被Python中定义的函数替代。这将允许用户真正访问数据。而且,用户可以在不深入的情况下扩展程序。

我想知道的事情:

  • f2py是否适合这个任务,能够清晰地包装多个子程序和公共块?我在网上只见过单文件的例子;我知道numpy用它来包装LAPACK等库,但我需要确认f2py是否足够一致,能够完成这个任务。
  • 是否有关于我应该遵循的一般策略的建议,或者我应该避免的陷阱。
  • 我该如何在这个Python包装的Fortran 90环境中实现一个系统,以便能够修改(分配和赋值)全局可访问的数组和变量。这最好能省去地址数组,并且我希望能够将文字表示注入到命名空间中。这些变量最好能在Python和Fortran中都能访问。

备注:

  • 我可能要求得太多,超出了可能的范围。如果是这样,请原谅我,我在这个编程方面还是个初学者;请随时纠正我。
  • 我提到的“程序”是开源的,但它是商业软件,许可证不允许分发,所以我决定不提及它的名字。不过,你可以从第二句话和我给出的描述中推测出它。

2 个回答

2

如果你想知道如何使用多个Fortran文件生成f2py接口库,可以参考这个帖子

f2py可能适合你的任务,但也有一些坑可能会导致问题。关于f2py的一些坑可以在这里找到,下面是一些总结:

  • 关于你的具体问题,你可能会在使用可分配数组时遇到麻烦,因为f2py是为Fortran77编写的,不支持很多Fortran90及以上版本的特性(比如可分配数组)。
  • 我还遇到过一个未记录的最大数组大小问题(大约400 x 200 x 20 x 20)。如果我使用的数组超过这个大小,f2py就无法生成Python库。尤其是在有限元代码中传递的大矩阵可能会太大,无法进行接口处理。因此,你在程序的Python部分将无法访问这些数据。
  • 对你来说,f2py在处理COMMON块等方面应该没有问题,因为它是专门为Fortran77编写的。
  • 如果你正确操作,通过接口将数据传递给Fortran例程后,应该不会有明显的速度下降。关键是要尽量减少每次运行时在Python部分的计算。这包括对数据数组的操作(如移动、旋转、复制等),但不包括传递这些数组(因为接口是按引用传递的)。

作为替代方案,你可以看看Cython(也可以参考上面的链接和其中的工作示例)。我认为从长远来看,这可能对你更有帮助。


实施建议

这个建议是基于我之前做过类似事情的经验(见下面的背景)。它应该在很大程度上独立于你如何将Python和Fortran代码连接起来(f2py、Cython等)。

当然,你应该非常小心,不要改变程序的行为,因此可能会影响结果。因此,生成一些测试及其对应的参考输入和输出文件,以及测试文档,包括重现这些结果所需的所有步骤、按键、命令等,应该是你的第一步。

在你的情况下,我会尽量少改动Fortran程序。我会尝试将“Fortran代码的伪壳”分离出来,比如将其做成一个独立的模块,并为这个模块构建一个接口。这样你就可以使用所有原始的Fortran代码,以及你同事的修改、修复和更新,甚至在未来也是如此。关键是不要让你的代码与原始代码相距太远,因为在科学界,通常并不是每个人都同意对源代码进行重大更改并相应更新他们的工作流程或源代码。因此,你同事的未来工作可能不会在你的版本中进行,而是在原始源代码中,你需要负责将这些更改合并到你的版本中,越少更改就越容易。

通过这个接口,你可以在Python环境中工作,甚至可以为其构建一个图形用户界面,而不必担心更改原始程序中的任何内容。这降低了引入错误或改变原始结果的风险。你的Shell/ GUI将作为原始程序的一个包装器,简化工作流程并消除不一致性。所有的“智能”和工具,比如用户输入的错误检查和交叉检查、帮助页面、教程等,都将在Python包装器中实现,它会解析这些输入,将其转换为相应的Fortran程序命令,发送并等待结果。

在你简化程序使用后,我会为测试(设置+评估)编写一些自动化工具,以完善你的工具套件。这样,即使是新手也能对代码进行修改,而不必担心无意中改变结果。这将使你的工具能够惠及社区,吸引新用户,从而鼓励社区内的进一步发展。

最后一步,我会用Fortran90及以上的方法替换代码中使用C的部分,以简化代码。这是对代码库的重大更改,需要进行大量测试,以确保每一个可能的命令组合在更改前后都经过检查和验证。

这种方法还有一个好处,就是你可能可以将你的接口/ GUI开源(当然你需要检查程序的许可证),只要它与Fortran程序的源代码是可分离的。Fortran - Python接口必须提供,或者在加载你的接口时通过一些简单的构建脚本从源文件安装/生成,如本帖第一个链接所示。

对于内部数据的操作,我会编写一个单独的包装例程,只处理数据接口。不过,这应该在Cython中完成,以便你能够使用可分配数组等功能。因为这个接口是按引用传递的,你应该能够使用Python(numpy)工具的完整集合来操作数组和数据。


背景

我曾经在我们的直升机转子动力学研究代码中做过类似的事情。这也是一个非常古老且庞大的程序,使用Fortran77编写(例如,goto的盛宴)。对代码的较新添加和修改通常是在Fortran90/2003中完成的。

使用这部分代码(几个子例程和模块文件),我生成了一个Python库,以将我们的图形用户界面(Python和Qt)与Fortran程序连接起来;主要用于后处理Fortran二进制输出文件。

3

我正在做一件让人沮丧的类似事情。我们没有像C语言那样动态分配内存,而是使用一个全局的数组,数组的索引是整数(也是全局的),但其他方面差不多。输入文件很奇怪,不一致,真让人头疼。

我建议不要试图重写大部分程序,无论是用Python还是其他语言。这样做既耗时又麻烦,而且大多数情况下其实没必要。一个更好的办法是让F77的代码能顺利编译到你可以信任的程度,然后再写一个接口程序。

现在我有一大堆丑陋的F77代码,它在一个接口后面运行。这个程序需要输入一个文本文件,所以接口的主要工作就是生成这个文本文件。除此之外,遗留代码被简化成一个单一的入口程序,它接受几个参数(包括识别文本文件的方法),然后返回结果。如果你使用Fortran 2003的iso_c_binding,你可以把接口以C语言能理解的格式暴露出来,这样你就可以把它链接到你想要的任何东西上。

至于现代代码(主要是优化程序),遗留代码就是C接口背后的那个单一子程序。这比继续修改旧代码要好得多,可能对你的情况也是一个有效的策略。

撰写回答