[英]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 字节数组)将其从辅助进程获取到主要进程:
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.