Coverage for /builds/debichem-team/python-ase/ase/calculators/openmx/openmx.py: 27.33%
450 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-03-06 04:00 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-03-06 04:00 +0000
1"""
2 The ASE Calculator for OpenMX <http://www.openmx-square.org>
3 A Python interface to the software package for nano-scale
4 material simulations based on density functional theories.
5 Copyright (C) 2017 Charles Thomas Johnson, Jae Hwan Shim and JaeJun Yu
7 This program is free software: you can redistribute it and/or modify
8 it under the terms of the GNU Lesser General Public License as published by
9 the Free Software Foundation, either version 2.1 of the License, or
10 (at your option) any later version.
12 This program is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU Lesser General Public License for more details.
17 You should have received a copy of the GNU Lesser General Public License
18 along with ASE. If not, see <http://www.gnu.org/licenses/>.
20"""
22import os
23import re
24import subprocess
25import time
26import warnings
28import numpy as np
30from ase.calculators.calculator import (
31 Calculator,
32 FileIOCalculator,
33 all_changes,
34 equal,
35 kptdensity2monkhorstpack,
36)
37from ase.calculators.openmx.default_settings import default_dictionary
38from ase.calculators.openmx.parameters import OpenMXParameters
39from ase.calculators.openmx.reader import get_file_name, read_openmx
40from ase.calculators.openmx.writer import write_openmx
41from ase.config import cfg
42from ase.geometry import cell_to_cellpar
45def parse_omx_version(txt):
46 """Parse version number from stdout header."""
47 match = re.search(r'Welcome to OpenMX\s+Ver\.\s+(\S+)', txt, re.M)
48 return match.group(1)
51class OpenMX(FileIOCalculator):
52 """
53 Calculator interface to the OpenMX code.
54 """
56 implemented_properties = [
57 'free_energy', # Same value with energy
58 'energy',
59 'energies',
60 'forces',
61 'stress',
62 'dipole',
63 'chemical_potential',
64 'magmom',
65 'magmoms',
66 'eigenvalues']
68 default_parameters = OpenMXParameters()
70 default_pbs = {
71 'processes': 1,
72 'walltime': "10:00:00",
73 'threads': 1,
74 'nodes': 1
75 }
77 default_mpi = {
78 'processes': 1,
79 'threads': 1
80 }
82 default_output_setting = {
83 'nohup': True,
84 'debug': False
85 }
87 def __init__(self, restart=None,
88 ignore_bad_restart_file=FileIOCalculator._deprecated,
89 label='./openmx', atoms=None, command=None, mpi=None,
90 pbs=None, **kwargs):
92 # Initialize and put the default parameters.
93 self.initialize_pbs(pbs)
94 self.initialize_mpi(mpi)
95 self.initialize_output_setting(**kwargs)
97 FileIOCalculator.__init__(self, restart, ignore_bad_restart_file,
98 label, atoms, command, **kwargs)
100 def __getitem__(self, key):
101 """Convenience method to retrieve a parameter as
102 calculator[key] rather than calculator.parameters[key]
104 Parameters:
105 -key : str, the name of the parameters to get.
106 """
107 return self.parameters[key]
109 def __setitem__(self, key, value):
110 self.parameters[key] = value
112 def initialize_output_setting(self, **kwargs):
113 output_setting = {}
114 self.output_setting = dict(self.default_output_setting)
115 for key, value in kwargs.items():
116 if key in self.default_output_setting:
117 output_setting[key] = value
118 self.output_setting.update(output_setting)
119 self.__dict__.update(self.output_setting)
121 def initialize_pbs(self, pbs):
122 if pbs:
123 self.pbs = dict(self.default_pbs)
124 for key in pbs:
125 if key not in self.default_pbs:
126 allowed = ', '.join(list(self.default_pbs.keys()))
127 raise TypeError('Unexpected keyword "{}" in "pbs" '
128 'dictionary. Must be one of: {}'
129 .format(key, allowed))
130 # Put dictionary into python variable
131 self.pbs.update(pbs)
132 self.__dict__.update(self.pbs)
133 else:
134 self.pbs = None
136 def initialize_mpi(self, mpi):
137 if mpi:
138 self.mpi = dict(self.default_mpi)
139 for key in mpi:
140 if key not in self.default_mpi:
141 allowed = ', '.join(list(self.default_mpi.keys()))
142 raise TypeError('Unexpected keyword "{}" in "mpi" '
143 'dictionary. Must be one of: {}'
144 .format(key, allowed))
145 # Put dictionary into python variable
146 self.mpi.update(mpi)
147 self.__dict__.update(self.mpi)
148 else:
149 self.mpi = None
151 def run(self):
152 '''Check Which Running method we r going to use and run it'''
153 if self.pbs is not None:
154 run = self.run_pbs
155 elif self.mpi is not None:
156 run = self.run_mpi
157 else:
158 run = self.run_openmx
159 run()
161 def run_openmx(self):
162 def isRunning(process=None):
163 ''' Check mpi is running'''
164 return process.poll() is None
165 runfile = get_file_name('.dat', self.label, absolute_directory=False)
166 outfile = get_file_name('.log', self.label)
167 olddir = os.getcwd()
168 abs_dir = os.path.join(olddir, self.directory)
169 try:
170 os.chdir(abs_dir)
171 if self.command is None:
172 self.command = 'openmx'
173 command = self.command + ' %s > %s'
174 command = command % (runfile, outfile)
175 self.prind(command)
176 p = subprocess.Popen(command, shell=True, universal_newlines=True)
177 self.print_file(file=outfile, running=isRunning, process=p)
178 finally:
179 os.chdir(olddir)
180 self.prind("Calculation Finished")
182 def run_mpi(self):
183 """
184 Run openmx using MPI method. If keyword `mpi` is declared, it will
185 run.
186 """
187 def isRunning(process=None):
188 ''' Check mpi is running'''
189 return process.poll() is None
190 processes = self.processes
191 threads = self.threads
192 runfile = get_file_name('.dat', self.label, absolute_directory=False)
193 outfile = get_file_name('.log', self.label)
194 olddir = os.getcwd()
195 abs_dir = os.path.join(olddir, self.directory)
196 try:
197 os.chdir(abs_dir)
198 command = self.get_command(processes, threads, runfile, outfile)
199 self.prind(command)
200 p = subprocess.Popen(command, shell=True, universal_newlines=True)
201 self.print_file(file=outfile, running=isRunning, process=p)
202 finally:
203 os.chdir(olddir)
204 self.prind("Calculation Finished")
206 def run_pbs(self, prefix='test'):
207 """
208 Execute the OpenMX using Plane Batch System. In order to use this,
209 Your system should have Scheduler. PBS
210 Basically, it does qsub. and wait until qstat signal shows c
211 Super computer user
212 """
213 nodes = self.nodes
214 processes = self.processes
216 prefix = self.prefix
217 olddir = os.getcwd()
218 try:
219 os.chdir(self.abs_directory)
220 except AttributeError:
221 os.chdir(self.directory)
223 def isRunning(jobNum=None, status='Q', qstat='qstat'):
224 """
225 Check submitted job is still Running
226 """
227 def runCmd(exe):
228 p = subprocess.Popen(exe, stdout=subprocess.PIPE,
229 stderr=subprocess.STDOUT,
230 universal_newlines=True)
231 while True:
232 line = p.stdout.readline()
233 if line != '':
234 # the real code does filtering here
235 yield line.rstrip()
236 else:
237 break
238 jobs = runCmd('qstat')
239 columns = None
240 for line in jobs:
241 if str(jobNum) in line:
242 columns = line.split()
243 self.prind(line)
244 if columns is not None:
245 return columns[-2] == status
246 else:
247 return False
249 inputfile = self.label + '.dat'
250 outfile = self.label + '.log'
252 bashArgs = "#!/bin/bash \n cd $PBS_O_WORKDIR\n"
253 jobName = prefix
254 cmd = bashArgs + \
255 'mpirun -hostfile $PBS_NODEFILE openmx {} > {}'.format(
256 inputfile, outfile)
257 echoArgs = ["echo", f"$' {cmd}'"]
258 qsubArgs = ["qsub", "-N", jobName, "-l", "nodes=%d:ppn=%d" %
259 (nodes, processes), "-l", "walltime=" + self.walltime]
260 wholeCmd = " ".join(echoArgs) + " | " + " ".join(qsubArgs)
261 self.prind(wholeCmd)
262 out = subprocess.Popen(wholeCmd, shell=True,
263 stdout=subprocess.PIPE, universal_newlines=True)
264 out = out.communicate()[0]
265 jobNum = int(re.match(r'(\d+)', out.split()[0]).group(1))
267 self.prind('Queue number is ' + str(jobNum) +
268 '\nWaiting for the Queue to start')
269 while isRunning(jobNum, status='Q'):
270 time.sleep(5)
271 self.prind('.')
272 self.prind('Start Calculating')
273 self.print_file(file=outfile, running=isRunning,
274 jobNum=jobNum, status='R', qstat='qstat')
276 os.chdir(olddir)
277 self.prind('Calculation Finished!')
278 return jobNum
280 def clean(self, prefix='test', queue_num=None):
281 """Method which cleans up after a calculation.
283 The default files generated OpenMX will be deleted IF this
284 method is called.
286 """
287 self.prind("Cleaning Data")
288 fileName = get_file_name('', self.label)
289 pbs_Name = get_file_name('', self.label)
290 files = [
291 # prefix+'.out',#prefix+'.dat',#prefix+'.BAND*',
292 fileName + '.cif',
293 fileName + '.dden.cube',
294 fileName + '.ene',
295 fileName + '.md',
296 fileName + '.md2',
297 fileName + '.tden.cube',
298 fileName + '.sden.cube',
299 fileName + '.v0.cube',
300 fileName + '.v1.cube',
301 fileName + '.vhart.cube',
302 fileName + '.den0.cube',
303 fileName + '.bulk.xyz',
304 fileName + '.den1.cube',
305 fileName + '.xyz',
306 pbs_Name + '.o' + str(queue_num),
307 pbs_Name + '.e' + str(queue_num)
308 ]
309 for f in files:
310 try:
311 self.prind("Removing" + f)
312 os.remove(f)
313 except OSError:
314 self.prind("There is no such file named " + f)
316 def calculate(self, atoms=None, properties=None,
317 system_changes=all_changes):
318 """
319 Capture the RuntimeError from FileIOCalculator.calculate
320 and add a little debug information from the OpenMX output.
321 See base FileIOCalculator for documentation.
322 """
323 if self.parameters.data_path is None:
324 if 'OPENMX_DFT_DATA_PATH' not in cfg:
325 warnings.warn('Please either set OPENMX_DFT_DATA_PATH as an'
326 'enviroment variable or specify "data_path" as'
327 'a keyword argument')
329 self.prind("Start Calculation")
330 if properties is None:
331 properties = self.implemented_properties
332 try:
333 Calculator.calculate(self, atoms, properties, system_changes)
334 self.write_input(atoms=self.atoms, parameters=self.parameters,
335 properties=properties,
336 system_changes=system_changes)
337 self.print_input(debug=self.debug, nohup=self.nohup)
338 self.run()
339 # self.read_results()
340 self.version = self.read_version()
341 output_atoms = read_openmx(filename=self.label, debug=self.debug)
342 self.output_atoms = output_atoms
343 # XXX The parameters are supposedly inputs, so it is dangerous
344 # to update them from the outputs. --askhl
345 self.parameters.update(output_atoms.calc.parameters)
346 self.results = output_atoms.calc.results
347 # self.clean()
348 except RuntimeError as e:
349 try:
350 with open(get_file_name('.log')) as fd:
351 lines = fd.readlines()
352 debug_lines = 10
353 print('##### %d last lines of the OpenMX output' % debug_lines)
354 for line in lines[-20:]:
355 print(line.strip())
356 print('##### end of openMX output')
357 raise e
358 except RuntimeError as e:
359 raise e
361 def write_input(self, atoms=None, parameters=None,
362 properties=[], system_changes=[]):
363 """Write input (dat)-file.
364 See calculator.py for further details.
366 Parameters:
367 - atoms : The Atoms object to write.
368 - properties : The properties which should be calculated.
369 - system_changes : List of properties changed since last run.
370 """
371 # Call base calculator.
372 if atoms is None:
373 atoms = self.atoms
374 FileIOCalculator.write_input(self, atoms, properties, system_changes)
375 write_openmx(label=self.label, atoms=atoms, parameters=self.parameters,
376 properties=properties, system_changes=system_changes)
378 def print_input(self, debug=None, nohup=None):
379 """
380 For a debugging purpose, print the .dat file
381 """
382 if debug is None:
383 debug = self.debug
384 if nohup is None:
385 nohup = self.nohup
386 self.prind('Reading input file' + self.label)
387 filename = get_file_name('.dat', self.label)
388 if not nohup:
389 with open(filename) as fd:
390 while True:
391 line = fd.readline()
392 print(line.strip())
393 if not line:
394 break
396 def read(self, label):
397 self.parameters = {}
398 self.set_label(label)
399 if label[-5:] in ['.dat', '.out', '.log']:
400 label = label[:-4]
401 atoms = read_openmx(filename=label, debug=self.debug)
402 self.update_atoms(atoms)
403 self.parameters.update(atoms.calc.parameters)
404 self.results = atoms.calc.results
405 self.parameters['restart'] = self.label
406 self.parameters['label'] = label
408 def read_version(self, label=None):
409 version = None
410 if label is None:
411 label = self.label
412 for line in open(get_file_name('.log', label)):
413 if line.find('Ver.') != -1:
414 version = line.split()[-1]
415 break
416 return version
418 def update_atoms(self, atoms):
419 self.atoms = atoms.copy()
421 def set(self, **kwargs):
422 """Set all parameters.
424 Parameters:
425 -kwargs : Dictionary containing the keywords defined in
426 OpenMXParameters.
427 """
429 for key, value in kwargs.items():
430 if key not in self.default_parameters.keys():
431 raise KeyError(f'Unkown keyword "{key}" and value "{value}".')
432 if key == 'xc' and value not in self.default_parameters.allowed_xc:
433 raise KeyError(f'Given xc "{value}" is not allowed')
434 if key in ['dat_arguments'] and isinstance(value, dict):
435 # For values that are dictionaries, verify subkeys, too.
436 default_dict = self.default_parameters[key]
437 for subkey in kwargs[key]:
438 if subkey not in default_dict:
439 allowed = ', '.join(list(default_dict.keys()))
440 raise TypeError('Unknown subkeyword "{}" of keyword '
441 '"{}". Must be one of: {}'
442 .format(subkey, key, allowed))
444 # Find out what parameter has been changed
445 changed_parameters = {}
446 for key, value in kwargs.items():
447 oldvalue = self.parameters.get(key)
448 if key not in self.parameters or not equal(value, oldvalue):
449 changed_parameters[key] = value
450 self.parameters[key] = value
452 # Set the parameters
453 for key, value in kwargs.items():
454 # print(' Setting the %s as %s'%(key, value))
455 self.parameters[key] = value
457 # If Changed Parameter is Critical, we have to reset the results
458 for key, value in changed_parameters.items():
459 if key in ['xc', 'kpts', 'energy_cutoff']:
460 self.results = {}
462 value = kwargs.get('energy_cutoff')
463 if value is not None and not (isinstance(value, (float, int))
464 and value > 0):
465 mess = "'{}' must be a positive number(in eV), \
466 got '{}'".format('energy_cutoff', value)
467 raise ValueError(mess)
469 atoms = kwargs.get('atoms')
470 if atoms is not None and self.atoms is None:
471 self.atoms = atoms.copy()
473 def set_results(self, results):
474 # Not Implemented fully
475 self.results.update(results)
477 def get_command(self, processes, threads, runfile=None, outfile=None):
478 # Contruct the command to send to the operating system
479 abs_dir = os.getcwd()
480 command = ''
481 self.prind(self.command)
482 if self.command is None:
483 self.command = 'openmx'
484 # run processes specified by the system variable OPENMX_COMMAND
485 if processes is None:
486 command += cfg.get('OPENMX_COMMAND')
487 if command is None:
488 warnings.warn('Either specify OPENMX_COMMAND as an environment\
489 variable or specify processes as a keyword argument')
490 else: # run with a specified number of processes
491 threads_string = ' -nt ' + str(threads)
492 if threads is None:
493 threads_string = ''
494 command += 'mpirun -np ' + \
495 str(processes) + ' ' + self.command + \
496 ' %s ' + threads_string + ' |tee %s'
497 # str(processes) + ' openmx %s' + threads_string + ' > %s'
499 if runfile is None:
500 runfile = os.path.join(abs_dir, f'{self.prefix} .dat')
501 if outfile is None:
502 outfile = os.path.join(abs_dir, f'{self.prefix} .log')
503 try:
504 command = command % (runfile, outfile)
505 # command += '" > ./%s &' % outfile # outputs
506 except TypeError: # in case the OPENMX_COMMAND is incompatible
507 raise ValueError(
508 "The 'OPENMX_COMMAND' environment must " +
509 "be a format string" +
510 " with four string arguments.\n" +
511 "Example : 'mpirun -np 4 openmx ./%s -nt 2 > ./%s'.\n" +
512 f"Got '{command}'")
513 return command
515 def get_stress(self, atoms=None):
516 if atoms is None:
517 atoms = self.atoms
519 # Note: Stress is only supported from OpenMX 3.8+.
520 stress = self.get_property('stress', atoms)
522 return stress
524 def get_band_structure(self, atoms=None, calc=None):
525 """
526 This is band structure function. It is compatible to
527 ase dft module """
528 from ase.dft import band_structure
529 if isinstance(self['kpts'], tuple):
530 self['kpts'] = self.get_kpoints(band_kpath=self['band_kpath'])
531 return band_structure.get_band_structure(self.atoms, self, )
533 def get_bz_k_points(self):
534 kgrid = self['kpts']
535 if type(kgrid) in [int, float]:
536 kgrid = kptdensity2monkhorstpack(self.atoms, kgrid, False)
537 bz_k_points = []
538 n1 = kgrid[0]
539 n2 = kgrid[1]
540 n3 = kgrid[2]
541 for i in range(n1):
542 for j in range(n2):
543 # Monkhorst Pack Grid [H.J. Monkhorst and J.D. Pack,
544 # Phys. Rev. B 13, 5188 (1976)]
545 for k in range(n3):
546 bz_k_points.append((0.5 * float(2 * i - n1 + 1) / n1,
547 0.5 * float(2 * j - n2 + 1) / n2,
548 0.5 * float(2 * k - n3 + 1) / n3))
549 return np.array(bz_k_points)
551 def get_ibz_k_points(self):
552 if self['band_kpath'] is None:
553 return self.get_bz_k_points()
554 else:
555 return self.get_kpoints(band_kpath=self['band_kpath'])
557 def get_kpoints(self, kpts=None, symbols=None, band_kpath=None, eps=1e-5):
558 """Convert band_kpath <-> kpts"""
559 if kpts is None:
560 kpts = []
561 band_kpath = np.array(band_kpath)
562 band_nkpath = len(band_kpath)
563 for i, kpath in enumerate(band_kpath):
564 end = False
565 nband = int(kpath[0])
566 if band_nkpath == i:
567 end = True
568 nband += 1
569 ini = np.array(kpath[1:4], dtype=float)
570 fin = np.array(kpath[4:7], dtype=float)
571 x = np.linspace(ini[0], fin[0], nband, endpoint=end)
572 y = np.linspace(ini[1], fin[1], nband, endpoint=end)
573 z = np.linspace(ini[2], fin[2], nband, endpoint=end)
574 kpts.extend(np.array([x, y, z]).T)
575 return np.array(kpts, dtype=float)
576 elif band_kpath is None:
577 band_kpath = []
578 points = np.asarray(kpts)
579 diffs = points[1:] - points[:-1]
580 kinks = abs(diffs[1:] - diffs[:-1]).sum(1) > eps
581 N = len(points)
582 indices = [0]
583 indices.extend(np.arange(1, N - 1)[kinks])
584 indices.append(N - 1)
585 for start, end, s_sym, e_sym in zip(indices[1:], indices[:-1],
586 symbols[1:], symbols[:-1]):
587 band_kpath.append({'start_point': start, 'end_point': end,
588 'kpts': 20,
589 'path_symbols': (s_sym, e_sym)})
590 return band_kpath
592 def get_lattice_type(self):
593 cellpar = cell_to_cellpar(self.atoms.cell)
594 abc = cellpar[:3]
595 angles = cellpar[3:]
596 min_lv = min(abc)
597 if np.ptp(abc) < 0.01 * min_lv:
598 if abs(angles - 90).max() < 1:
599 return 'cubic'
600 elif abs(angles - 60).max() < 1:
601 return 'fcc'
602 elif abs(angles - np.arccos(-1 / 3.) * 180 / np.pi).max < 1:
603 return 'bcc'
604 elif abs(angles - 90).max() < 1:
605 if abs(abc[0] - abc[1]).min() < 0.01 * min_lv:
606 return 'tetragonal'
607 else:
608 return 'orthorhombic'
609 elif abs(abc[0] - abc[1]) < 0.01 * min_lv and \
610 abs(angles[2] - 120) < 1 and abs(angles[:2] - 90).max() < 1:
611 return 'hexagonal'
612 else:
613 return 'not special'
615 def get_number_of_spins(self):
616 try:
617 magmoms = self.atoms.get_initial_magnetic_moments()
618 if self['scf_spinpolarization'] is None:
619 if isinstance(magmoms[0], float):
620 if abs(magmoms).max() < 0.1:
621 return 1
622 else:
623 return 2
624 else:
625 raise NotImplementedError
626 else:
627 if self['scf_spinpolarization'] == 'on':
628 return 2
629 elif self['scf_spinpolarization'] == 'nc' or \
630 np.any(self['initial_magnetic_moments_euler_angles']) \
631 is not None:
632 return 1
633 except KeyError:
634 return 1
636 def get_eigenvalues(self, kpt=None, spin=None):
637 if self.results.get('eigenvalues') is None:
638 self.calculate(self.atoms)
639 if kpt is None and spin is None:
640 return self.results['eigenvalues']
641 else:
642 return self.results['eigenvalues'][spin, kpt, :]
644 def get_fermi_level(self):
645 try:
646 fermi_level = self.results['chemical_potential']
647 except KeyError:
648 self.calculate()
649 fermi_level = self.results['chemical_potential']
650 return fermi_level
652 def get_number_of_bands(self):
653 pag = self.parameters.get
654 dfd = default_dictionary
655 if 'number_of_bands' not in self.results:
656 n = 0
657 for atom in self.atoms:
658 sym = atom.symbol
659 orbitals = pag('dft_data_dict', dfd)[sym]['orbitals used']
660 d = 1
661 for orbital in orbitals:
662 n += d * orbital
663 d += 2
664 self.results['number_of_bands'] = n
665 return self.results['number_of_bands']
667 def dirG(self, dk, bzone=(0, 0, 0)):
668 nx, ny, nz = self['wannier_kpts']
669 dx = dk // (ny * nz) + bzone[0] * nx
670 dy = (dk // nz) % ny + bzone[1] * ny
671 dz = dk % nz + bzone[2] * nz
672 return dx, dy, dz
674 def dk(self, dirG):
675 dx, dy, dz = dirG
676 nx, ny, nz = self['wannier_kpts']
677 return ny * nz * (dx % nx) + nz * (dy % ny) + dz % nz
679 def get_wannier_localization_matrix(self, nbands, dirG, nextkpoint=None,
680 kpoint=None, spin=0, G_I=(0, 0, 0)):
681 # only expected to work for no spin polarization
682 try:
683 self['bloch_overlaps']
684 except KeyError:
685 self.read_bloch_overlaps()
686 dirG = tuple(dirG)
687 nx, ny, nz = self['wannier_kpts']
688 nr3 = nx * ny * nz
689 if kpoint is None and nextkpoint is None:
690 return {kpoint: self['bloch_overlaps'
691 ][kpoint][dirG][:nbands, :nbands
692 ] for kpoint in range(nr3)}
693 if kpoint is None:
694 kpoint = (nextkpoint - self.dk(dirG)) % nr3
695 if nextkpoint is None:
696 nextkpoint = (kpoint + self.dk(dirG)) % nr3
697 if dirG not in self['bloch_overlaps'][kpoint].keys():
698 return np.zeros((nbands, nbands), complex)
699 return self['bloch_overlaps'][kpoint][dirG][:nbands, :nbands]
701 def prind(self, line, debug=None):
702 ''' Print the value if debugging mode is on.
703 Otherwise, it just ignored'''
704 if debug is None:
705 debug = self.debug
706 if debug:
707 print(line)
709 def print_file(self, file=None, running=None, **args):
710 ''' Print the file while calculation is running'''
711 prev_position = 0
712 last_position = 0
713 while not os.path.isfile(file):
714 self.prind(f'Waiting for {file} to come out')
715 time.sleep(5)
716 with open(file) as fd:
717 while running(**args):
718 fd.seek(last_position)
719 new_data = fd.read()
720 prev_position = fd.tell()
721 # self.prind('pos', prev_position != last_position)
722 if prev_position != last_position:
723 if not self.nohup:
724 print(new_data)
725 last_position = prev_position
726 time.sleep(1)