Coverage for ase / calculators / openmx / openmx.py: 27.33%
450 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-30 08:22 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-30 08:22 +0000
1# fmt: off
3"""
4 The ASE Calculator for OpenMX <https://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 <https://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 ----------
108 key : str
109 Name of the parameters to get.
110 """
111 return self.parameters[key]
113 def __setitem__(self, key, value):
114 self.parameters[key] = value
116 def initialize_output_setting(self, **kwargs):
117 output_setting = {}
118 self.output_setting = dict(self.default_output_setting)
119 for key, value in kwargs.items():
120 if key in self.default_output_setting:
121 output_setting[key] = value
122 self.output_setting.update(output_setting)
123 self.__dict__.update(self.output_setting)
125 def initialize_pbs(self, pbs):
126 if pbs:
127 self.pbs = dict(self.default_pbs)
128 for key in pbs:
129 if key not in self.default_pbs:
130 allowed = ', '.join(list(self.default_pbs.keys()))
131 raise TypeError('Unexpected keyword "{}" in "pbs" '
132 'dictionary. Must be one of: {}'
133 .format(key, allowed))
134 # Put dictionary into python variable
135 self.pbs.update(pbs)
136 self.__dict__.update(self.pbs)
137 else:
138 self.pbs = None
140 def initialize_mpi(self, mpi):
141 if mpi:
142 self.mpi = dict(self.default_mpi)
143 for key in mpi:
144 if key not in self.default_mpi:
145 allowed = ', '.join(list(self.default_mpi.keys()))
146 raise TypeError('Unexpected keyword "{}" in "mpi" '
147 'dictionary. Must be one of: {}'
148 .format(key, allowed))
149 # Put dictionary into python variable
150 self.mpi.update(mpi)
151 self.__dict__.update(self.mpi)
152 else:
153 self.mpi = None
155 def run(self):
156 '''Check Which Running method we r going to use and run it'''
157 if self.pbs is not None:
158 run = self.run_pbs
159 elif self.mpi is not None:
160 run = self.run_mpi
161 else:
162 run = self.run_openmx
163 run()
165 def run_openmx(self):
166 def isRunning(process=None):
167 ''' Check mpi is running'''
168 return process.poll() is None
169 runfile = get_file_name('.dat', self.label, absolute_directory=False)
170 outfile = get_file_name('.log', self.label)
171 olddir = os.getcwd()
172 abs_dir = os.path.join(olddir, self.directory)
173 try:
174 os.chdir(abs_dir)
175 if self.command is None:
176 self.command = 'openmx'
177 command = self.command + ' %s > %s'
178 command = command % (runfile, outfile)
179 self.prind(command)
180 p = subprocess.Popen(command, shell=True, universal_newlines=True)
181 self.print_file(file=outfile, running=isRunning, process=p)
182 finally:
183 os.chdir(olddir)
184 self.prind("Calculation Finished")
186 def run_mpi(self):
187 """
188 Run openmx using MPI method. If keyword `mpi` is declared, it will
189 run.
190 """
191 def isRunning(process=None):
192 ''' Check mpi is running'''
193 return process.poll() is None
194 processes = self.processes
195 threads = self.threads
196 runfile = get_file_name('.dat', self.label, absolute_directory=False)
197 outfile = get_file_name('.log', self.label)
198 olddir = os.getcwd()
199 abs_dir = os.path.join(olddir, self.directory)
200 try:
201 os.chdir(abs_dir)
202 command = self.get_command(processes, threads, runfile, outfile)
203 self.prind(command)
204 p = subprocess.Popen(command, shell=True, universal_newlines=True)
205 self.print_file(file=outfile, running=isRunning, process=p)
206 finally:
207 os.chdir(olddir)
208 self.prind("Calculation Finished")
210 def run_pbs(self, prefix='test'):
211 """
212 Execute the OpenMX using Plane Batch System. In order to use this,
213 Your system should have Scheduler. PBS
214 Basically, it does qsub. and wait until qstat signal shows c
215 Super computer user
216 """
217 nodes = self.nodes
218 processes = self.processes
220 prefix = self.prefix
221 olddir = os.getcwd()
222 try:
223 os.chdir(self.abs_directory)
224 except AttributeError:
225 os.chdir(self.directory)
227 def isRunning(jobNum=None, status='Q', qstat='qstat'):
228 """
229 Check submitted job is still Running
230 """
231 def runCmd(exe):
232 p = subprocess.Popen(exe, stdout=subprocess.PIPE,
233 stderr=subprocess.STDOUT,
234 universal_newlines=True)
235 while True:
236 line = p.stdout.readline()
237 if line != '':
238 # the real code does filtering here
239 yield line.rstrip()
240 else:
241 break
242 jobs = runCmd('qstat')
243 columns = None
244 for line in jobs:
245 if str(jobNum) in line:
246 columns = line.split()
247 self.prind(line)
248 if columns is not None:
249 return columns[-2] == status
250 else:
251 return False
253 inputfile = self.label + '.dat'
254 outfile = self.label + '.log'
256 bashArgs = "#!/bin/bash \n cd $PBS_O_WORKDIR\n"
257 jobName = prefix
258 cmd = bashArgs + \
259 'mpirun -hostfile $PBS_NODEFILE openmx {} > {}'.format(
260 inputfile, outfile)
261 echoArgs = ["echo", f"$' {cmd}'"]
262 qsubArgs = ["qsub", "-N", jobName, "-l", "nodes=%d:ppn=%d" %
263 (nodes, processes), "-l", "walltime=" + self.walltime]
264 wholeCmd = " ".join(echoArgs) + " | " + " ".join(qsubArgs)
265 self.prind(wholeCmd)
266 out = subprocess.Popen(wholeCmd, shell=True,
267 stdout=subprocess.PIPE, universal_newlines=True)
268 out = out.communicate()[0]
269 jobNum = int(re.match(r'(\d+)', out.split()[0]).group(1))
271 self.prind('Queue number is ' + str(jobNum) +
272 '\nWaiting for the Queue to start')
273 while isRunning(jobNum, status='Q'):
274 time.sleep(5)
275 self.prind('.')
276 self.prind('Start Calculating')
277 self.print_file(file=outfile, running=isRunning,
278 jobNum=jobNum, status='R', qstat='qstat')
280 os.chdir(olddir)
281 self.prind('Calculation Finished!')
282 return jobNum
284 def clean(self, prefix='test', queue_num=None):
285 """Method which cleans up after a calculation.
287 The default files generated OpenMX will be deleted IF this
288 method is called.
290 """
291 self.prind("Cleaning Data")
292 fileName = get_file_name('', self.label)
293 pbs_Name = get_file_name('', self.label)
294 files = [
295 # prefix+'.out',#prefix+'.dat',#prefix+'.BAND*',
296 fileName + '.cif',
297 fileName + '.dden.cube',
298 fileName + '.ene',
299 fileName + '.md',
300 fileName + '.md2',
301 fileName + '.tden.cube',
302 fileName + '.sden.cube',
303 fileName + '.v0.cube',
304 fileName + '.v1.cube',
305 fileName + '.vhart.cube',
306 fileName + '.den0.cube',
307 fileName + '.bulk.xyz',
308 fileName + '.den1.cube',
309 fileName + '.xyz',
310 pbs_Name + '.o' + str(queue_num),
311 pbs_Name + '.e' + str(queue_num)
312 ]
313 for f in files:
314 try:
315 self.prind("Removing" + f)
316 os.remove(f)
317 except OSError:
318 self.prind("There is no such file named " + f)
320 def calculate(self, atoms=None, properties=None,
321 system_changes=all_changes):
322 """
323 Capture the RuntimeError from FileIOCalculator.calculate
324 and add a little debug information from the OpenMX output.
325 See base FileIOCalculator for documentation.
326 """
327 if self.parameters.data_path is None:
328 if 'OPENMX_DFT_DATA_PATH' not in cfg:
329 warnings.warn('Please either set OPENMX_DFT_DATA_PATH as an'
330 'enviroment variable or specify "data_path" as'
331 'a keyword argument')
333 self.prind("Start Calculation")
334 if properties is None:
335 properties = self.implemented_properties
336 try:
337 Calculator.calculate(self, atoms, properties, system_changes)
338 self.write_input(atoms=self.atoms, parameters=self.parameters,
339 properties=properties,
340 system_changes=system_changes)
341 self.print_input(debug=self.debug, nohup=self.nohup)
342 self.run()
343 # self.read_results()
344 self.version = self.read_version()
345 output_atoms = read_openmx(filename=self.label, debug=self.debug)
346 self.output_atoms = output_atoms
347 # XXX The parameters are supposedly inputs, so it is dangerous
348 # to update them from the outputs. --askhl
349 self.parameters.update(output_atoms.calc.parameters)
350 self.results = output_atoms.calc.results
351 # self.clean()
352 except RuntimeError as e:
353 try:
354 with open(get_file_name('.log')) as fd:
355 lines = fd.readlines()
356 debug_lines = 10
357 print('##### %d last lines of the OpenMX output' % debug_lines)
358 for line in lines[-20:]:
359 print(line.strip())
360 print('##### end of openMX output')
361 raise e
362 except RuntimeError as e:
363 raise e
365 def write_input(self, atoms=None, parameters=None,
366 properties=[], system_changes=[]):
367 """Write input (dat)-file.
368 See calculator.py for further details.
370 Parameters
371 ----------
372 atoms : :class:`~ase.Atoms`
373 ASE :class:`~ase.Atoms` object to write.
374 properties : list[str]
375 Properties which should be calculated.
376 system_changes : list[str]
377 Properties changed since last run.
378 """
379 # Call base calculator.
380 if atoms is None:
381 atoms = self.atoms
382 FileIOCalculator.write_input(self, atoms, properties, system_changes)
383 write_openmx(label=self.label, atoms=atoms, parameters=self.parameters,
384 properties=properties, system_changes=system_changes)
386 def print_input(self, debug=None, nohup=None):
387 """
388 For a debugging purpose, print the .dat file
389 """
390 if debug is None:
391 debug = self.debug
392 if nohup is None:
393 nohup = self.nohup
394 self.prind('Reading input file' + self.label)
395 filename = get_file_name('.dat', self.label)
396 if not nohup:
397 with open(filename) as fd:
398 while True:
399 line = fd.readline()
400 print(line.strip())
401 if not line:
402 break
404 def read(self, label):
405 self.parameters = {}
406 self.set_label(label)
407 if label[-5:] in ['.dat', '.out', '.log']:
408 label = label[:-4]
409 atoms = read_openmx(filename=label, debug=self.debug)
410 self.update_atoms(atoms)
411 self.parameters.update(atoms.calc.parameters)
412 self.results = atoms.calc.results
413 self.parameters['restart'] = self.label
414 self.parameters['label'] = label
416 def read_version(self, label=None):
417 version = None
418 if label is None:
419 label = self.label
420 for line in open(get_file_name('.log', label)):
421 if line.find('Ver.') != -1:
422 version = line.split()[-1]
423 break
424 return version
426 def update_atoms(self, atoms):
427 self.atoms = atoms.copy()
429 def set(self, **kwargs):
430 """Set all parameters.
432 Parameters
433 ----------
434 kwargs : dict
435 Dictionary containing the keywords in :class:`OpenMXParameters`.
436 """
438 for key, value in kwargs.items():
439 if key not in self.default_parameters.keys():
440 raise KeyError(f'Unkown keyword "{key}" and value "{value}".')
441 if key == 'xc' and value not in self.default_parameters.allowed_xc:
442 raise KeyError(f'Given xc "{value}" is not allowed')
443 if key in ['dat_arguments'] and isinstance(value, dict):
444 # For values that are dictionaries, verify subkeys, too.
445 default_dict = self.default_parameters[key]
446 for subkey in kwargs[key]:
447 if subkey not in default_dict:
448 allowed = ', '.join(list(default_dict.keys()))
449 raise TypeError('Unknown subkeyword "{}" of keyword '
450 '"{}". Must be one of: {}'
451 .format(subkey, key, allowed))
453 # Find out what parameter has been changed
454 changed_parameters = {}
455 for key, value in kwargs.items():
456 oldvalue = self.parameters.get(key)
457 if key not in self.parameters or not equal(value, oldvalue):
458 changed_parameters[key] = value
459 self.parameters[key] = value
461 # Set the parameters
462 for key, value in kwargs.items():
463 # print(' Setting the %s as %s'%(key, value))
464 self.parameters[key] = value
466 # If Changed Parameter is Critical, we have to reset the results
467 for key, value in changed_parameters.items():
468 if key in ['xc', 'kpts', 'energy_cutoff']:
469 self.results = {}
471 value = kwargs.get('energy_cutoff')
472 if value is not None and not (isinstance(value, (float, int))
473 and value > 0):
474 mess = "'{}' must be a positive number(in eV), \
475 got '{}'".format('energy_cutoff', value)
476 raise ValueError(mess)
478 atoms = kwargs.get('atoms')
479 if atoms is not None and self.atoms is None:
480 self.atoms = atoms.copy()
482 def set_results(self, results):
483 # Not Implemented fully
484 self.results.update(results)
486 def get_command(self, processes, threads, runfile=None, outfile=None):
487 # Contruct the command to send to the operating system
488 abs_dir = os.getcwd()
489 command = ''
490 self.prind(self.command)
491 if self.command is None:
492 self.command = 'openmx'
493 # run processes specified by the system variable OPENMX_COMMAND
494 if processes is None:
495 command += cfg.get('OPENMX_COMMAND')
496 if command is None:
497 warnings.warn('Either specify OPENMX_COMMAND as an environment\
498 variable or specify processes as a keyword argument')
499 else: # run with a specified number of processes
500 threads_string = ' -nt ' + str(threads)
501 if threads is None:
502 threads_string = ''
503 command += 'mpirun -np ' + \
504 str(processes) + ' ' + self.command + \
505 ' %s ' + threads_string + ' |tee %s'
506 # str(processes) + ' openmx %s' + threads_string + ' > %s'
508 if runfile is None:
509 runfile = os.path.join(abs_dir, f'{self.prefix} .dat')
510 if outfile is None:
511 outfile = os.path.join(abs_dir, f'{self.prefix} .log')
512 try:
513 command = command % (runfile, outfile)
514 # command += '" > ./%s &' % outfile # outputs
515 except TypeError: # in case the OPENMX_COMMAND is incompatible
516 raise ValueError(
517 "The 'OPENMX_COMMAND' environment must " +
518 "be a format string" +
519 " with four string arguments.\n" +
520 "Example : 'mpirun -np 4 openmx ./%s -nt 2 > ./%s'.\n" +
521 f"Got '{command}'")
522 return command
524 def get_stress(self, atoms=None):
525 if atoms is None:
526 atoms = self.atoms
528 # Note: Stress is only supported from OpenMX 3.8+.
529 stress = self.get_property('stress', atoms)
531 return stress
533 def get_band_structure(self, atoms=None, calc=None):
534 """
535 This is band structure function. It is compatible to
536 ase dft module """
537 from ase.dft import band_structure
538 if isinstance(self['kpts'], tuple):
539 self['kpts'] = self.get_kpoints(band_kpath=self['band_kpath'])
540 return band_structure.get_band_structure(self.atoms, self, )
542 def get_bz_k_points(self):
543 kgrid = self['kpts']
544 if type(kgrid) in [int, float]:
545 kgrid = kptdensity2monkhorstpack(self.atoms, kgrid, False)
546 bz_k_points = []
547 n1 = kgrid[0]
548 n2 = kgrid[1]
549 n3 = kgrid[2]
550 for i in range(n1):
551 for j in range(n2):
552 # Monkhorst Pack Grid [H.J. Monkhorst and J.D. Pack,
553 # Phys. Rev. B 13, 5188 (1976)]
554 for k in range(n3):
555 bz_k_points.append((0.5 * float(2 * i - n1 + 1) / n1,
556 0.5 * float(2 * j - n2 + 1) / n2,
557 0.5 * float(2 * k - n3 + 1) / n3))
558 return np.array(bz_k_points)
560 def get_ibz_k_points(self):
561 if self['band_kpath'] is None:
562 return self.get_bz_k_points()
563 else:
564 return self.get_kpoints(band_kpath=self['band_kpath'])
566 def get_kpoints(self, kpts=None, symbols=None, band_kpath=None, eps=1e-5):
567 """Convert band_kpath <-> kpts"""
568 if kpts is None:
569 kpts = []
570 band_kpath = np.array(band_kpath)
571 band_nkpath = len(band_kpath)
572 for i, kpath in enumerate(band_kpath):
573 end = False
574 nband = int(kpath[0])
575 if band_nkpath == i:
576 end = True
577 nband += 1
578 ini = np.array(kpath[1:4], dtype=float)
579 fin = np.array(kpath[4:7], dtype=float)
580 x = np.linspace(ini[0], fin[0], nband, endpoint=end)
581 y = np.linspace(ini[1], fin[1], nband, endpoint=end)
582 z = np.linspace(ini[2], fin[2], nband, endpoint=end)
583 kpts.extend(np.array([x, y, z]).T)
584 return np.array(kpts, dtype=float)
585 elif band_kpath is None:
586 band_kpath = []
587 points = np.asarray(kpts)
588 diffs = points[1:] - points[:-1]
589 kinks = abs(diffs[1:] - diffs[:-1]).sum(1) > eps
590 N = len(points)
591 indices = [0]
592 indices.extend(np.arange(1, N - 1)[kinks])
593 indices.append(N - 1)
594 for start, end, s_sym, e_sym in zip(indices[1:], indices[:-1],
595 symbols[1:], symbols[:-1]):
596 band_kpath.append({'start_point': start, 'end_point': end,
597 'kpts': 20,
598 'path_symbols': (s_sym, e_sym)})
599 return band_kpath
601 def get_lattice_type(self):
602 cellpar = cell_to_cellpar(self.atoms.cell)
603 abc = cellpar[:3]
604 angles = cellpar[3:]
605 min_lv = min(abc)
606 if np.ptp(abc) < 0.01 * min_lv:
607 if abs(angles - 90).max() < 1:
608 return 'cubic'
609 elif abs(angles - 60).max() < 1:
610 return 'fcc'
611 elif abs(angles - np.arccos(-1 / 3.) * 180 / np.pi).max < 1:
612 return 'bcc'
613 elif abs(angles - 90).max() < 1:
614 if abs(abc[0] - abc[1]).min() < 0.01 * min_lv:
615 return 'tetragonal'
616 else:
617 return 'orthorhombic'
618 elif abs(abc[0] - abc[1]) < 0.01 * min_lv and \
619 abs(angles[2] - 120) < 1 and abs(angles[:2] - 90).max() < 1:
620 return 'hexagonal'
621 else:
622 return 'not special'
624 def get_number_of_spins(self):
625 try:
626 magmoms = self.atoms.get_initial_magnetic_moments()
627 if self['scf_spinpolarization'] is None:
628 if isinstance(magmoms[0], float):
629 if abs(magmoms).max() < 0.1:
630 return 1
631 else:
632 return 2
633 else:
634 raise NotImplementedError
635 else:
636 if self['scf_spinpolarization'] == 'on':
637 return 2
638 elif self['scf_spinpolarization'] == 'nc' or \
639 np.any(self['initial_magnetic_moments_euler_angles']) \
640 is not None:
641 return 1
642 except KeyError:
643 return 1
645 def get_eigenvalues(self, kpt=None, spin=None):
646 if self.results.get('eigenvalues') is None:
647 self.calculate(self.atoms)
648 if kpt is None and spin is None:
649 return self.results['eigenvalues']
650 else:
651 return self.results['eigenvalues'][spin, kpt, :]
653 def get_fermi_level(self):
654 try:
655 fermi_level = self.results['chemical_potential']
656 except KeyError:
657 self.calculate()
658 fermi_level = self.results['chemical_potential']
659 return fermi_level
661 def get_number_of_bands(self):
662 pag = self.parameters.get
663 dfd = default_dictionary
664 if 'number_of_bands' not in self.results:
665 n = 0
666 for atom in self.atoms:
667 sym = atom.symbol
668 orbitals = pag('dft_data_dict', dfd)[sym]['orbitals used']
669 d = 1
670 for orbital in orbitals:
671 n += d * orbital
672 d += 2
673 self.results['number_of_bands'] = n
674 return self.results['number_of_bands']
676 def dirG(self, dk, bzone=(0, 0, 0)):
677 nx, ny, nz = self['wannier_kpts']
678 dx = dk // (ny * nz) + bzone[0] * nx
679 dy = (dk // nz) % ny + bzone[1] * ny
680 dz = dk % nz + bzone[2] * nz
681 return dx, dy, dz
683 def dk(self, dirG):
684 dx, dy, dz = dirG
685 nx, ny, nz = self['wannier_kpts']
686 return ny * nz * (dx % nx) + nz * (dy % ny) + dz % nz
688 def get_wannier_localization_matrix(self, nbands, dirG, nextkpoint=None,
689 kpoint=None, spin=0, G_I=(0, 0, 0)):
690 # only expected to work for no spin polarization
691 try:
692 self['bloch_overlaps']
693 except KeyError:
694 self.read_bloch_overlaps()
695 dirG = tuple(dirG)
696 nx, ny, nz = self['wannier_kpts']
697 nr3 = nx * ny * nz
698 if kpoint is None and nextkpoint is None:
699 return {kpoint: self['bloch_overlaps'
700 ][kpoint][dirG][:nbands, :nbands
701 ] for kpoint in range(nr3)}
702 if kpoint is None:
703 kpoint = (nextkpoint - self.dk(dirG)) % nr3
704 if nextkpoint is None:
705 nextkpoint = (kpoint + self.dk(dirG)) % nr3
706 if dirG not in self['bloch_overlaps'][kpoint].keys():
707 return np.zeros((nbands, nbands), complex)
708 return self['bloch_overlaps'][kpoint][dirG][:nbands, :nbands]
710 def prind(self, line, debug=None):
711 ''' Print the value if debugging mode is on.
712 Otherwise, it just ignored'''
713 if debug is None:
714 debug = self.debug
715 if debug:
716 print(line)
718 def print_file(self, file=None, running=None, **args):
719 ''' Print the file while calculation is running'''
720 prev_position = 0
721 last_position = 0
722 while not os.path.isfile(file):
723 self.prind(f'Waiting for {file} to come out')
724 time.sleep(5)
725 with open(file) as fd:
726 while running(**args):
727 fd.seek(last_position)
728 new_data = fd.read()
729 prev_position = fd.tell()
730 # self.prind('pos', prev_position != last_position)
731 if prev_position != last_position:
732 if not self.nohup:
733 print(new_data)
734 last_position = prev_position
735 time.sleep(1)