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