繁体   English   中英

使用 f2py 在 Python GUI 中更新长时间运行的 Fortran 子例程

[英]Updating long-running Fortran subroutine in Python GUI using f2py

我有一个 Python GUI (wxPython),它使用 f2py 围绕一个 fortran“后端”。 有时,fortran 进程可能会运行很长时间,我们希望在 GUI 中放置一个进度条以通过 Fortran 例程更新进度。 有没有办法在不涉及文件 I/O 的情况下获取 Fortran 例程的状态/进度?

如果您对代码的哪些部分花费最多的时间有基本的了解,则可以包括进度指示器。 考虑以下示例代码:

program main
implicit none
integer, parameter :: N = 100000
integer :: i
real, allocatable :: a(:), b(:)

! -- Initialization
write(*,*) 'FORFTP: Beginning routine'

allocate(a(N), b(N))
a = 0.
b = 0.

write(*,*) 'FORFTP: Completed initialization'

do i=1,N
   call RANDOM_NUMBER(a(i))
   b(i) = exp(a(i))     ! Some expensive calculation

   if (mod(i,N/100)==0) then     ! -- Assumes N is evenly divisible by 100
      write(*,*) 'FORFTP: completed ', i*100/N, ' percent of the calculation'
   endif
enddo

write(*,*) 'FORFTP: completed routine'

end program main

然后,用户将在初始化之后以及完成“昂贵的计算”的每个百分比之后获得更新。

我不知道f2py是如何工作的,但是我认为python有某种方式可以读取在运行时fortran输出的内容,并在gui中显示它。 在此示例中,任何标记为FORFTP都将在gui中输出,而我使用的是标准输出。

但是,此示例说明了调查进度的问题。 很难理解分配与计算相比要花费多少时间。 因此,很难说初始化是例如总执行时间的15%。

但是,即使没有确切的进度表,也要对正在发生的事情进行更新仍然很有用。

编辑例程提供以下输出:

 >  pgfortran main.f90
 >  ./a.out 
 FORFTP: Beginning routine
 FORFTP: Completed initialization
 FORFTP: completed             1  percent of the calculation
 FORFTP: completed             2  percent of the calculation
  ...
 FORFTP: completed            99  percent of the calculation
 FORFTP: completed           100  percent of the calculation
 FORFTP: completed routine

如果您不了解每个任务将要花费多长时间,则很容易被否决,最简单的选择是根据任务的预期持续时间,根据任务开始以来经过的时间来确定进度。
为了使其与时俱进,您始终可以每次存储任务的运行持续时间,并将其或平均时间用作基本时间轴。
有时,我们可能会使事情变得过于复杂;)

我找到了一种方法来做到这一点 - 无论它是否是一个好方法,我不能说,但我已经尝试过了,它确实有效。 我在别处写的,我也贴在这里。 但是,此方法确实涉及拥有 Fortran 源代码,因此如果您只使用已编译的文件,它可能对您不起作用。

首先,在 Fortran 代码中,在模块中定义名为 progress 和 max_prog 的整数:

module fblock
  use iso_fortran_env
  integer(kind=int32), save :: progress = 0
  integer(kind=int32), save :: max_prog = 1
  
  contains
    subroutine long_runner(...)

此处使用 save 标志,因此变量不会在其范围之外未定义(即在调用/完成子例程之前或之后访问它们,这可能会在后续步骤中发生)。 如果您使用的是 Fortran 2008 或更高版本,则不需要保存标志,因为始终保存模块变量。

然后,在长时间运行的子程序中,添加一行告诉 f2py 解锁 python 的全局解释器锁(GIL):

module fblock
  use iso_fortran_env
  integer(kind=int32), save :: progress = 0
  integer(kind=int32), save :: max_prog = 1

  contains
    subroutine long_runner(...)
      !f2py threadsafe

解锁 GIL 可防止 Python 在此 Fortran 块运行时变得完全无响应,从而允许 Python 中的单独线程在其执行期间运行(这是此后的下一步;我对线程安全了解不多,无法多说) ,但是这一步有点需要使整个事情工作)。 最后,只需在代码中的进度变量中添加一个:

module fblock
  use iso_fortran_env
  integer(kind=int32), save :: progress = 0
  integer(kind=int32), save :: max_prog = 1

  contains
    subroutine long_runner(input_data, output_data)
      !f2py threadsafe

      ! other code ...

      max_prog = giant_number

      ! possibly more code...

      do i = 1, giant_number
        progress = progress + 1
        ! yet more code...

您必须根据代码是否在巨大的 do 循环中运行来使其适应代码的运行方式,但您只是在增加一个数字。 请注意,如果您使用 openmp 进行并行工作,请仅在第一个线程/处理器上总结进度:

subroutine long_runner(input_data, output_data)
  use omp

  ! code...

  max_prog = giant_number / omp_get_num_procs()

  !$OMP PARALLEL
  proc_num = omp_get_thread_num()
  ! ...

  !$OMP DO
  do i = 1, giant_number
    ! ...
    if (proc_num == 0) then
      progress = progress + 1
    end if
    ! ...

现在,一旦您使用 f2py 将其编译为 Python 模块,就可以进行第二步并处理 Python 端了。 例如,您的 Fortran 模块“fblock”和子程序“long_runner”已被编译到文件“pyblk.pyd”中。 导入 pyblk 以及线程和时间模块:

import pyblk  # your f2py compiled fortran block
import threading
import time
 
global bg_runn
bg_runn = True
 
def background():
    "background query/monitoring thread"
    time.sleep(0.1)  # wait a bit for foreground code to start
    while bg_runn:
        a = pyblk.fblock.progress
        b = pyblk.fblock.max_prog
        if a >= b: break
 
        print(a, 'of', b, '({:.3%})'.format(a / b))
        time.sleep(2)
 
# start the background thread
thrd = threading.Thread(target=background)
thrd.start()
 
print(time.ctime())
# then call the compiled fortran
output_data = pyblk.fblock.long_runner(init_data)
 
bg_runn = False  # stop the background thread once done
thrd.join()  # wait for it to stop (wait out the sleep cycle)
print(time.ctime())

在 Python 端的整个过程中,只有进度和 max_prog 被读取(读取不会修改)。 Fortran 块上的所有其他内容都在子例程内部,无论如何都没有设置任何东西来干扰这些 - progess 和 max_prog 是在子例程之外查看的唯一变量。 Python 的输出可能如下所示:

...
1026869 of 4793490 (21.422%)
1056318 of 4793490 (22.037%)
1086679 of 4793490 (22.670%)
1116830 of 4793490 (23.299%)
...

这对运行时间的增加可以忽略不计(如果有的话;我在测试时没有注意到时间上的差异)。


现在,要将其与带有精美进度条的 GUI 联系起来,事情会变得复杂得多,因为 GUI 和 Fortran 块都必须在前台运行。 你不能只在后台线程中运行 Fortran(至少,我不能 - Python 完全崩溃了)。 因此,您必须使用 Python 的多处理模块启动一个可以运行 Fortran 的完全独立的进程。

+===========+---------------+===========+
|  Primary  | -- Spawns --> | Secondary |
|  Process  |               |  Process  |
+===========+               +===========+
|foreground:|               |foreground:|
|    GUI    |               |  fortran  |
+- - - - - -+               +- - - - - -+
|background:|               |background:|
|           |  <-- Queue -- |   query   |
+-----------+               +-----------+                   

因此,设置是将 GUI 作为主要进程,它启动一个辅助进程,您的 Fortran 代码可以在其中使用自己的后台查询线程运行。 一个multiprocessing.Queue被设置(将信息传递回 GUI)并将其提供给一个multiprocessing.Process (辅助),然后启动查询线程并运行。 辅助节点上的这个查询线程将其发现放入队列,而不是像上面那样将它们打印出来。 回到主进程,将信息从队列中拉出并用于设置进度条。 我不熟悉 wxPython,但这里有一个使用另一个 GUI 库 PySide2 来说明整个复杂性的示例:

from multiprocessing import shared_memory
import multiprocessing as mp
import numpy as np
import threading
import sys, os
import time
 
import pyblk  # your f2py compiled fortran block
 
# to use PyQt5, replace 'PySide2' with 'PyQt5'
from PySide2.QtWidgets import (QApplication, QWidget, QVBoxLayout,
                               QProgressBar, QPushButton)
 
 
global bg_runn
bg_runn = True
 
# this query is run in the background on the secondary process 
def bg_query(prog_que):
    "background query thread for compiled fortran block"
    global bg_runn
    current, total = 0, 1
 
    # wait a bit for fortran code to initialize the queried variables
    time.sleep(0.25)
     
    while bg_runn:
        # query the progress
        current = pyblk.fblock.progress
        total   = pyblk.fblock.max_prog
 
        if current >= total: break
        prog_que.put((current, total))
        time.sleep(0.1)  # this can be more or less depending on need
         
    prog_que.put((current, total))
    prog_que.put('DONE')  # inform other end that this is complete
    return
 
# this fortran block is run on the secondary process
def run_fortran(prog_que, init_data):
    "call to run compiled fortran block"
    global bg_runn
     
    # setup/start background query thread
    thrd = threading.Thread(target=bg_query, args=(prog_que, ))
    thrd.start()
     
    # call the compiled fortran code
    results = pyblk.fblock.long_runner(init_data)
     
    bg_runn = False  # inform query to stop
    thrd.join()  # wait for it to stop (wait out the sleep cycle)
     
    # now, do something with the results or
    # copy the results out from this process
    ##shm = shared_memory.SharedMemory('results')  # connect to shared mem
    ##b = np.ndarray(results.shape, dtype=results.dtype, buffer=shm.buf)
    ##b[:] = img_arr[:]  # copy results (memory is now allocated)
    ##shm.close()  # disconnect from shared mem
    return
 
 
# this GUI is run on the primary process
class ProgTest(QWidget):
    "progess test of compiled fortran code through python"
    def __init__(self, parent=None):
        super().__init__()
        # setup/layout of widget
        self.pbar = QProgressBar()
        self.pbar.setTextVisible(False)
 
        self.start_button = QPushButton('Start')
        self.start_button.clicked.connect(self.run_the_thing)
         
        ly = QVBoxLayout()
        ly.addWidget(self.start_button)
        ly.addWidget(self.pbar)
        self.setLayout(ly)
         
    def run_the_thing(self):
        "called on clicking the start button"
        self.setEnabled(False)  # prevent interaction during run
        app.processEvents()
 
        t0 = time.time()
        print('start:', time.ctime(t0))
         
        prog_que = mp.Queue()  # progress queue
 
        # if wanting the results on the primary process:
        # create shared memory to later copy result array into
        # (array size is needed; no memory is used/allocated at this time)
        ##shm = shared_memory.SharedMemory('results', create=True,
        ##                                 size=np.int32(1).nbytes * amount)
 
        init_data = None  # your initial information, if any
        # if it's large and on disk, read it in on the secondary process
 
        # setup/start the secondary process with the compiled fortran code
        run = mp.Process(target=run_fortran, args=(prog_que, init_data))
        run.start()
 
        # listen in on the query through the Queue
        while True:
            res = prog_que.get()
            if res == 'DONE': break
            current, total = res  # unpack from queue
 
            if total != self.pbar.maximum(): self.pbar.setMaximum(total)
 
            self.pbar.setValue(current)
            self.setWindowTitle('{:.3%}'.format(current / total))
            app.processEvents()
        # this while loop can be done on a separate background thread
        # but isn't done for this example
             
        run.join()  # wait for the secondary process to complete
 
        # extract the results from secondary process with SharedMemory
        # (shape and dtype need to be known)
        ##results = np.ndarray(shape, dtype=np.int32, buffer=shm.buf)
 
        t1 = time.time()
        print('end:', time.ctime(t1))
        print('{:.3f} seconds'.format(t1 - t0))
 
        self.pbar.setValue(total)
        self.setWindowTitle('Done!')
        self.setEnabled(True)
        return
 
if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = ProgTest()
    window.show()
    sys.exit(app.exec_())

但是,这种在辅助进程上运行它的方法会产生一个新问题 - 您的结果在辅助进程上! 如何处理这完全取决于您的结果。 如果您可以在辅助设备上处理它们(例如,在计算它们后保存数据),最好在那里进行。 但是,如果您需要让它们回到主要流程进行交互,则必须将它们复制出来。

通常,使用 f2py 涉及numpy,因此您的结果可能是某种 numpy 数组。 这是我尝试的几种方法(使用 1600000000 字节数组)将其从辅助进程获取到主要进程:

  • 使用multiprocessing.Array创建和复制结果以将它们从次要到主要 - 这使整个运行时间的内存加倍,并增加了大约 30 到 45 秒的运行时间。
  • 将结果填充到multiprocessing.Queue 中以将它们从次要到主要 - 在填充期间使内存增加一倍以上,并增加约 5 到 10 秒的运行时间 - 不一定能很好地利用队列。
  • 使用shared_memory.SharedMemory复制结果以将它们从次要到主要 - 在复制期间将内存加倍并增加约 1 秒的运行时间。 这是在上面的 GUI 示例中注释掉的方法。 当问到这个问题时,我意识到这个选项不存在。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM