• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pyiron / pyiron_base / 9471909594

11 Jun 2024 07:52PM UTC coverage: 71.339% (+0.06%) from 71.284%
9471909594

Pull #1472

github

web-flow
Merge faae3f5cc into df9b3d159
Pull Request #1472: Add get_input_file_dict() in GenericJob

14 of 21 new or added lines in 2 files covered. (66.67%)

92 existing lines in 3 files now uncovered.

7166 of 10045 relevant lines covered (71.34%)

0.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

71.43
/pyiron_base/jobs/job/generic.py
1
# coding: utf-8
2
# Copyright (c) Max-Planck-Institut für Eisenforschung GmbH - Computational Materials Design (CM) Department
3
# Distributed under the terms of "New BSD License", see the LICENSE file.
4
"""
5
Generic Job class extends the JobCore class with all the functionality to run the job object.
6
"""
7

8
from concurrent.futures import Future, Executor
1✔
9
from datetime import datetime
1✔
10
from inspect import isclass
1✔
11
import os
1✔
12
import platform
1✔
13
import posixpath
1✔
14
import shutil
1✔
15
import warnings
1✔
16

17
from h5io_browser.base import _read_hdf, _write_hdf
1✔
18
from pyiron_snippets.deprecate import deprecate
1✔
19

20
from pyiron_base.state import state
1✔
21
from pyiron_base.state.signal import catch_signals
1✔
22
from pyiron_base.jobs.job.core import (
1✔
23
    JobCore,
24
    _doc_str_job_core_args,
25
    _doc_str_job_core_attr,
26
)
27
from pyiron_base.jobs.job.extension.executable import Executable
1✔
28
from pyiron_base.jobs.job.extension.files import File
1✔
29
from pyiron_base.jobs.job.extension.jobstatus import JobStatus
1✔
30
from pyiron_base.jobs.job.runfunction import (
1✔
31
    run_job_with_parameter_repair,
32
    run_job_with_status_initialized,
33
    run_job_with_status_created,
34
    run_job_with_status_submitted,
35
    run_job_with_status_running,
36
    run_job_with_status_refresh,
37
    run_job_with_status_busy,
38
    run_job_with_status_collect,
39
    run_job_with_status_suspended,
40
    run_job_with_status_finished,
41
    run_job_with_runmode_modal,
42
    run_job_with_runmode_queue,
43
    execute_job_with_external_executable,
44
)
45
from pyiron_base.jobs.job.util import (
1✔
46
    _get_restart_copy_dict,
47
    _kill_child,
48
    _job_store_before_copy,
49
    _job_reload_after_copy,
50
)
51
from pyiron_base.utils.instance import static_isinstance, import_class
1✔
52
from pyiron_base.jobs.job.extension.server.generic import Server
1✔
53
from pyiron_base.database.filetable import FileTable
1✔
54
from pyiron_base.interfaces.has_dict import HasDict
1✔
55

56
__author__ = "Joerg Neugebauer, Jan Janssen"
1✔
57
__copyright__ = (
1✔
58
    "Copyright 2020, Max-Planck-Institut für Eisenforschung GmbH - "
59
    "Computational Materials Design (CM) Department"
60
)
61
__version__ = "1.0"
1✔
62
__maintainer__ = "Jan Janssen"
1✔
63
__email__ = "janssen@mpie.de"
1✔
64
__status__ = "production"
1✔
65
__date__ = "Sep 1, 2017"
1✔
66

67
# Modular Docstrings
68
_doc_str_generic_job_attr = (
1✔
69
    _doc_str_job_core_attr
70
    + "\n"
71
    + """\
72
        .. attribute:: version
73

74
            Version of the hamiltonian, which is also the version of the executable unless a custom executable is used.
75

76
        .. attribute:: executable
77

78
            Executable used to run the job - usually the path to an external executable.
79

80
        .. attribute:: library_activated
81

82
            For job types which offer a Python library pyiron can use the python library instead of an external
83
            executable.
84

85
        .. attribute:: server
86

87
            Server object to handle the execution environment for the job.
88

89
        .. attribute:: queue_id
90

91
            the ID returned from the queuing system - it is most likely not the same as the job ID.
92

93
        .. attribute:: logger
94

95
            logger object to monitor the external execution and internal pyiron warnings.
96

97
        .. attribute:: restart_file_list
98

99
            list of files which are used to restart the calculation from these files.
100

101
        .. attribute:: exclude_nodes_hdf
102

103
            list of nodes which are excluded from storing in the hdf5 file.
104

105
        .. attribute:: exclude_groups_hdf
106

107
            list of groups which are excluded from storing in the hdf5 file.
108

109
        .. attribute:: job_type
110

111
            Job type object with all the available job types: ['ExampleJob', 'ParallelMaster',
112
                                                               'ScriptJob', 'ListMaster']
113
"""
114
)
115

116

117
class GenericJob(JobCore, HasDict):
1✔
118
    __doc__ = (
1✔
119
        """
120
    Generic Job class extends the JobCore class with all the functionality to run the job object. From this class
121
    all specific job types are derived. Therefore it should contain the properties/routines common to all jobs.
122
    The functions in this module should be as generic as possible.
123

124
    Sub classes that need to add special behavior after :method:`.copy_to()` can override
125
    :method:`._after_generic_copy_to()`.
126
"""
127
        + "\n"
128
        + _doc_str_job_core_args
129
        + "\n"
130
        + _doc_str_generic_job_attr
131
    )
132

133
    def __init__(self, project, job_name):
1✔
134
        super(GenericJob, self).__init__(project, job_name)
1✔
135
        self.__name__ = type(self).__name__
1✔
136
        self.__version__ = "0.4"
1✔
137
        self.__hdf_version__ = "0.1.0"
1✔
138
        self._server = Server()
1✔
139
        self._logger = state.logger
1✔
140
        self._executable = None
1✔
141
        if not state.database.database_is_disabled:
1✔
142
            self._status = JobStatus(db=project.db, job_id=self.job_id)
1✔
143
            self.refresh_job_status()
1✔
144
        elif os.path.exists(self.project_hdf5.file_name):
1✔
145
            initial_status = _read_hdf(
1✔
146
                # in most cases self.project_hdf5.h5_path == / + self.job_name but not for child jobs of GenericMasters
147
                self.project_hdf5.file_name,
148
                self.project_hdf5.h5_path + "/status",
149
            )
150
            self._status = JobStatus(initial_status=initial_status)
1✔
151
            if "job_id" in self.list_nodes():
1✔
152
                self._job_id = _read_hdf(
1✔
153
                    # in most cases self.project_hdf5.h5_path == / + self.job_name but not for child jobs of GenericMasters
154
                    self.project_hdf5.file_name,
155
                    self.project_hdf5.h5_path + "/job_id",
156
                )
157
        else:
158
            self._status = JobStatus()
1✔
159
        self._restart_file_list = list()
1✔
160
        self._restart_file_dict = dict()
1✔
161
        self._exclude_nodes_hdf = list()
1✔
162
        self._exclude_groups_hdf = list()
1✔
163
        self._executor_type = None
1✔
164
        self._process = None
1✔
165
        self._compress_by_default = False
1✔
166
        self._python_only_job = False
1✔
167
        self._write_work_dir_warnings = True
1✔
168
        self.interactive_cache = None
1✔
169
        self.error = GenericError(working_directory=self.project_hdf5.working_directory)
1✔
170

171
    @property
1✔
172
    def version(self):
1✔
173
        """
174
        Get the version of the hamiltonian, which is also the version of the executable unless a custom executable is
175
        used.
176

177
        Returns:
178
            str: version number
179
        """
180
        if self.__version__:
1✔
181
            return self.__version__
1✔
182
        else:
183
            self._executable_activate()
×
184
            if self._executable is not None:
×
185
                return self._executable.version
×
186
            else:
187
                return None
×
188

189
    @version.setter
1✔
190
    def version(self, new_version):
1✔
191
        """
192
        Set the version of the hamiltonian, which is also the version of the executable unless a custom executable is
193
        used.
194

195
        Args:
196
            new_version (str): version
197
        """
198
        self._executable_activate()
×
199
        self._executable.version = new_version
×
200

201
    @property
1✔
202
    def executable(self):
1✔
203
        """
204
        Get the executable used to run the job - usually the path to an external executable.
205

206
        Returns:
207
            (str/pyiron_base.job.executable.Executable): exectuable path
208
        """
209
        self._executable_activate()
1✔
210
        return self._executable
1✔
211

212
    @executable.setter
1✔
213
    def executable(self, exe):
1✔
214
        """
215
        Set the executable used to run the job - usually the path to an external executable.
216

217
        Args:
218
            exe (str): executable path, if no valid path is provided an executable is chosen based on version.
219
        """
220
        self._executable_activate()
1✔
221
        self._executable.executable_path = exe
1✔
222

223
    @property
1✔
224
    def server(self):
1✔
225
        """
226
        Get the server object to handle the execution environment for the job.
227

228
        Returns:
229
            Server: server object
230
        """
231
        return self._server
1✔
232

233
    @server.setter
1✔
234
    def server(self, server):
1✔
235
        """
236
        Set the server object to handle the execution environment for the job.
237
        Args:
238
            server (Server): server object
239
        """
240
        self._server = server
×
241

242
    @property
1✔
243
    def queue_id(self):
1✔
244
        """
245
        Get the queue ID, the ID returned from the queuing system - it is most likely not the same as the job ID.
246

247
        Returns:
248
            int: queue ID
249
        """
250
        return self.server.queue_id
×
251

252
    @queue_id.setter
1✔
253
    def queue_id(self, qid):
1✔
254
        """
255
        Set the queue ID, the ID returned from the queuing system - it is most likely not the same as the job ID.
256

257
        Args:
258
            qid (int): queue ID
259
        """
260
        self.server.queue_id = qid
×
261

262
    @property
1✔
263
    def logger(self):
1✔
264
        """
265
        Get the logger object to monitor the external execution and internal pyiron warnings.
266

267
        Returns:
268
            logging.getLogger(): logger object
269
        """
270
        return self._logger
1✔
271

272
    @property
1✔
273
    def restart_file_list(self):
1✔
274
        """
275
        Get the list of files which are used to restart the calculation from these files.
276

277
        Returns:
278
            list: list of files
279
        """
280
        self._restart_file_list = [
1✔
281
            str(f) if isinstance(f, File) else f for f in self._restart_file_list
282
        ]
283
        return self._restart_file_list
1✔
284

285
    @restart_file_list.setter
1✔
286
    def restart_file_list(self, filenames):
1✔
287
        """
288
        Append new files to the restart file list - the list of files which are used to restart the calculation from.
289

290
        Args:
291
            filenames (list):
292
        """
293
        for f in filenames:
×
294
            if isinstance(f, File):
×
295
                f = str(f)
×
296
            if not (os.path.isfile(f)):
×
297
                raise IOError("File: {} does not exist".format(f))
×
298
            self.restart_file_list.append(f)
×
299

300
    @property
1✔
301
    def restart_file_dict(self):
1✔
302
        """
303
        A dictionary of the new name of the copied restart files
304
        """
305
        for actual_name in [os.path.basename(f) for f in self.restart_file_list]:
×
306
            if actual_name not in self._restart_file_dict.keys():
×
307
                self._restart_file_dict[actual_name] = actual_name
×
308
        return self._restart_file_dict
×
309

310
    @restart_file_dict.setter
1✔
311
    def restart_file_dict(self, val):
1✔
312
        if not isinstance(val, dict):
×
313
            raise ValueError("restart_file_dict should be a dictionary!")
×
314
        else:
315
            self._restart_file_dict = {}
×
316
            for k, v in val.items():
×
317
                if isinstance(k, File):
×
318
                    k = str(k)
×
319
                if isinstance(v, File):
×
320
                    v = str(v)
×
321
                self._restart_file_dict[k] = v
×
322

323
    @property
1✔
324
    def exclude_nodes_hdf(self):
1✔
325
        """
326
        Get the list of nodes which are excluded from storing in the hdf5 file
327

328
        Returns:
329
            nodes(list)
330
        """
331
        return self._exclude_nodes_hdf
×
332

333
    @exclude_nodes_hdf.setter
1✔
334
    def exclude_nodes_hdf(self, val):
1✔
335
        if isinstance(val, str):
×
336
            val = [val]
×
337
        elif not hasattr(val, "__len__"):
×
338
            raise ValueError("Wrong type of variable.")
×
339
        self._exclude_nodes_hdf = val
×
340

341
    @property
1✔
342
    def exclude_groups_hdf(self):
1✔
343
        """
344
        Get the list of groups which are excluded from storing in the hdf5 file
345

346
        Returns:
347
            groups(list)
348
        """
349
        return self._exclude_groups_hdf
×
350

351
    @exclude_groups_hdf.setter
1✔
352
    def exclude_groups_hdf(self, val):
1✔
353
        if isinstance(val, str):
×
354
            val = [val]
×
355
        elif not hasattr(val, "__len__"):
×
356
            raise ValueError("Wrong type of variable.")
×
357
        self._exclude_groups_hdf = val
×
358

359
    @property
1✔
360
    def job_type(self):
1✔
361
        """
362
        Job type object with all the available job types: ['ExampleJob', 'ParallelMaster', 'ScriptJob',
363
                                                           'ListMaster']
364
        Returns:
365
            JobTypeChoice: Job type object
366
        """
367
        return self.project.job_type
×
368

369
    @property
1✔
370
    def working_directory(self):
1✔
371
        """
372
        Get the working directory of the job is executed in - outside the HDF5 file. The working directory equals the
373
        path but it is represented by the filesystem:
374
            /absolute/path/to/the/file.h5/path/inside/the/hdf5/file
375
        becomes:
376
            /absolute/path/to/the/file_hdf5/path/inside/the/hdf5/file
377

378
        Returns:
379
            str: absolute path to the working directory
380
        """
381
        if self._import_directory is not None:
1✔
382
            return self._import_directory
×
383
        elif not self.project_hdf5.working_directory:
1✔
384
            self._create_working_directory()
×
385
        return self.project_hdf5.working_directory
1✔
386

387
    @property
1✔
388
    def executor_type(self):
1✔
389
        return self._executor_type
×
390

391
    @executor_type.setter
1✔
392
    def executor_type(self, exe):
1✔
393
        if exe is None:
1✔
394
            self._executor_type = exe
1✔
395
        elif isinstance(exe, str):
1✔
396
            try:
1✔
397
                exe_class = import_class(exe)  # Make sure it's available
1✔
398
                if not (
1✔
399
                    isclass(exe_class) and issubclass(exe_class, Executor)
400
                ):  # And what we want
401
                    raise TypeError(
×
402
                        f"{exe} imported OK, but {exe_class} is not a subclass of {Executor}"
403
                    )
404
            except Exception as e:
1✔
405
                raise ImportError("Something went wrong trying to import {exe}") from e
1✔
406
            else:
407
                self._executor_type = exe
1✔
408
        elif isclass(exe) and issubclass(exe, Executor):
1✔
409
            self._executor_type = f"{exe.__module__}.{exe.__name__}"
1✔
410
        elif isinstance(exe, Executor):
×
411
            raise NotImplementedError(
×
412
                "We don't want to let you pass an entire executor, because you might think its state comes "
413
                "with it. Try passing `.__class__` on this object instead."
414
            )
415
        else:
416
            raise TypeError(
×
417
                f"Expected an executor class or string representing one, but got {exe}"
418
            )
419

420
    def collect_logfiles(self):
1✔
421
        """
422
        Collect the log files of the external executable and store the information in the HDF5 file. This method has
423
        to be implemented in the individual hamiltonians.
424
        """
425
        pass
1✔
426

427
    def write_input(self):
1✔
428
        """
429
        Call routines that generate the code specific input files
430
        Returns:
431
        """
432
        input_dict = self.get_input_file_dict()
1✔
433
        for file_name, content in input_dict["files_to_create"].items():
1✔
434
            with open(os.path.join(self.working_directory, file_name), "w") as f:
1✔
435
                f.writelines(content)
1✔
436
        for file_name, source in input_dict["files_to_copy"].items():
1✔
NEW
437
            shutil.copy(source, os.path.join(self.working_directory, file_name))
×
438

439
    def get_input_file_dict(self) -> dict:
1✔
440
        """
441
        Get an hierarchical dictionary of input files. On the first level the dictionary is divided in file_to_create
442
        and files_to_copy. Both are dictionaries use the file names as keys. In file_to_create the values are strings
443
        which represent the content which is going to be written to the corresponding file. In files_to_copy the values
444
        are the paths to the source files to be copied.
445

446
        Returns:
447
            dict: hierarchical dictionary of input files
448
        """
449
        if (
1✔
450
            state.settings.configuration["write_work_dir_warnings"]
451
            and self._write_work_dir_warnings
452
            and not self._python_only_job
453
        ):
454
            content = [
1✔
455
                "Files in this directory are intended to be written and read by pyiron. \n\n",
456
                "pyiron may transform user input to enhance performance, thus, use these files with care!\n",
457
                "Consult the log and/or the documentation to gain further information.\n\n",
458
                "To disable writing these warning files, specify \n",
459
                "WRITE_WORK_DIR_WARNINGS=False in the .pyiron configuration file (or set the ",
460
                "PYIRONWRITEWORKDIRWARNINGS environment variable accordingly).",
461
            ]
462
            return {
1✔
463
                "files_to_create": {
464
                    "WARNING_pyiron_modified_content": "".join(content)
465
                },
466
                "files_to_copy": _get_restart_copy_dict(job=self),
467
            }
468
        else:
469
            return {
1✔
470
                "files_to_create": {},
471
                "files_to_copy": _get_restart_copy_dict(job=self),
472
            }
473

474
    def collect_output(self):
1✔
475
        """
476
        Collect the output files of the external executable and store the information in the HDF5 file. This method has
477
        to be implemented in the individual hamiltonians.
478
        """
479
        raise NotImplementedError(
×
480
            "read procedure must be defined for derived Hamilton!"
481
        )
482

483
    def suspend(self):
1✔
484
        """
485
        Suspend the job by storing the object and its state persistently in HDF5 file and exit it.
486
        """
487
        self.to_hdf()
×
488
        self.status.suspended = True
×
489
        self._logger.info(
×
490
            "{}, status: {}, job has been suspended".format(
491
                self.job_info_str, self.status
492
            )
493
        )
494
        self.clear_job()
×
495

496
    def refresh_job_status(self):
1✔
497
        """
498
        Refresh job status by updating the job status with the status from the database if a job ID is available.
499
        """
500
        if self.job_id:
1✔
501
            self._status = JobStatus(
1✔
502
                initial_status=self.project.db.get_job_status(self.job_id),
503
                db=self.project.db,
504
                job_id=self.job_id,
505
            )
506
        elif state.database.database_is_disabled:
1✔
507
            self._status = JobStatus(
×
508
                initial_status=_read_hdf(
509
                    self.project_hdf5.file_name, self.job_name + "/status"
510
                )
511
            )
512
        if (
1✔
513
            isinstance(self.server.future, Future)
514
            and not self.status.finished
515
            and self.server.future.done()
516
        ):
517
            if self.server.future.cancelled():
1✔
518
                self.status.aborted = True
1✔
519
            else:
520
                self.status.finished = True
×
521

522
    def clear_job(self):
1✔
523
        """
524
        Convenience function to clear job info after suspend. Mimics deletion of all the job info after suspend in a
525
        local test environment.
526
        """
527
        del self.__name__
×
528
        del self.__version__
×
529
        del self._executable
×
530
        del self._server
×
531
        del self._logger
×
532
        del self._import_directory
×
533
        del self._status
×
534
        del self._restart_file_list
×
535
        del self._restart_file_dict
×
536

537
    def copy(self):
1✔
538
        """
539
        Copy the GenericJob object which links to the job and its HDF5 file
540

541
        Returns:
542
            GenericJob: New GenericJob object pointing to the same job
543
        """
544
        # Store all job arguments in the HDF5 file
545
        delete_file_after_copy = _job_store_before_copy(job=self)
1✔
546

547
        # Copy Python object - super().copy() causes recursion error for serial master
548
        copied_self = self.__class__(
1✔
549
            job_name=self.job_name, project=self.project_hdf5.open("..")
550
        )
551
        copied_self.reset_job_id()
1✔
552

553
        # Reload object from HDF5 file
554
        _job_reload_after_copy(
1✔
555
            job=copied_self, delete_file_after_copy=delete_file_after_copy
556
        )
557

558
        # Copy executor - it cannot be copied and is just linked instead
559
        if self.server.executor is not None:
1✔
560
            copied_self.server.executor = self.server.executor
1✔
561
        if self.server.future is not None and not self.server.future.done():
1✔
562
            raise RuntimeError(
1✔
563
                "Jobs whose server has executor and future attributes cannot be copied unless the future is `done()`"
564
            )
565
        return copied_self
1✔
566

567
    def _internal_copy_to(
1✔
568
        self,
569
        project=None,
570
        new_job_name=None,
571
        new_database_entry=True,
572
        copy_files=True,
573
        delete_existing_job=False,
574
    ):
575
        # Store all job arguments in the HDF5 file
576
        delete_file_after_copy = _job_store_before_copy(job=self)
1✔
577

578
        # Call the copy_to() function defined in the JobCore
579
        new_job_core, file_project, hdf5_project, reloaded = super(
1✔
580
            GenericJob, self
581
        )._internal_copy_to(
582
            project=project,
583
            new_job_name=new_job_name,
584
            new_database_entry=new_database_entry,
585
            copy_files=copy_files,
586
            delete_existing_job=delete_existing_job,
587
        )
588
        if reloaded:
1✔
589
            return new_job_core, file_project, hdf5_project, reloaded
1✔
590

591
        # Reload object from HDF5 file
592
        if not static_isinstance(
1✔
593
            obj=project.__class__, obj_type="pyiron_base.jobs.job.core.JobCore"
594
        ):
595
            _job_reload_after_copy(
1✔
596
                job=new_job_core, delete_file_after_copy=delete_file_after_copy
597
            )
598
        if delete_file_after_copy:
1✔
599
            self.project_hdf5.remove_file()
1✔
600
        return new_job_core, file_project, hdf5_project, reloaded
1✔
601

602
    def copy_to(
1✔
603
        self,
604
        project=None,
605
        new_job_name=None,
606
        input_only=False,
607
        new_database_entry=True,
608
        delete_existing_job=False,
609
        copy_files=True,
610
    ):
611
        """
612
        Copy the content of the job including the HDF5 file to a new location.
613

614
        Args:
615
            project (JobCore/ProjectHDFio/Project/None): The project to copy the job to.
616
                (Default is None, use the same project.)
617
            new_job_name (str): The new name to assign the duplicate job. Required if the project is `None` or the same
618
                project as the copied job. (Default is None, try to keep the same name.)
619
            input_only (bool): [True/False] Whether to copy only the input. (Default is False.)
620
            new_database_entry (bool): [True/False] Whether to create a new database entry. If input_only is True then
621
                new_database_entry is False. (Default is True.)
622
            delete_existing_job (bool): [True/False] Delete existing job in case it exists already (Default is False.)
623
            copy_files (bool): If True copy all files the working directory of the job, too
624

625
        Returns:
626
            GenericJob: GenericJob object pointing to the new location.
627
        """
628
        # Update flags
629
        if input_only and new_database_entry:
1✔
630
            warnings.warn(
×
631
                "input_only conflicts new_database_entry; setting new_database_entry=False"
632
            )
633
            new_database_entry = False
×
634

635
        # Call the copy_to() function defined in the JobCore
636
        new_job_core, file_project, hdf5_project, reloaded = self._internal_copy_to(
1✔
637
            project=project,
638
            new_job_name=new_job_name,
639
            new_database_entry=new_database_entry,
640
            copy_files=copy_files,
641
            delete_existing_job=delete_existing_job,
642
        )
643

644
        # Remove output if it should not be copied
645
        if input_only:
1✔
646
            for group in new_job_core.project_hdf5.list_groups():
1✔
647
                if "output" in group:
1✔
648
                    del new_job_core.project_hdf5[
×
649
                        posixpath.join(new_job_core.project_hdf5.h5_path, group)
650
                    ]
651
            new_job_core.status.initialized = True
1✔
652
        new_job_core._after_generic_copy_to(
1✔
653
            self, new_database_entry=new_database_entry, reloaded=reloaded
654
        )
655
        return new_job_core
1✔
656

657
    def _after_generic_copy_to(self, original, new_database_entry, reloaded):
1✔
658
        """
659
        Called in :method:`.copy_to()` after :method`._internal_copy_to()` to allow sub classes to modify copy behavior.
660

661
        Args:
662
            original (:class:`.GenericJob`): job that this job was copied from
663
            new_database_entry (bool): Whether to create a new database entry was created.
664
            reloaded (bool): True if this job was reloaded instead of copied.
665
        """
666
        pass
1✔
667

668
    def copy_file_to_working_directory(self, file):
1✔
669
        """
670
        Copy a specific file to the working directory before the job is executed.
671

672
        Args:
673
            file (str): path of the file to be copied.
674
        """
675
        if os.path.isabs(file):
×
676
            self.restart_file_list.append(file)
×
677
        else:
678
            self.restart_file_list.append(os.path.abspath(file))
×
679

680
    def copy_template(self, project=None, new_job_name=None):
1✔
681
        """
682
        Copy the content of the job including the HDF5 file but without the output data to a new location
683

684
        Args:
685
            project (JobCore/ProjectHDFio/Project/None): The project to copy the job to.
686
                (Default is None, use the same project.)
687
            new_job_name (str): The new name to assign the duplicate job. Required if the project is `None` or the same
688
                project as the copied job. (Default is None, try to keep the same name.)
689

690
        Returns:
691
            GenericJob: GenericJob object pointing to the new location.
692
        """
693
        return self.copy_to(
1✔
694
            project=project,
695
            new_job_name=new_job_name,
696
            input_only=True,
697
            new_database_entry=False,
698
        )
699

700
    def remove(self, _protect_childs=True):
1✔
701
        """
702
        Remove the job - this removes the HDF5 file, all data stored in the HDF5 file an the corresponding database entry.
703

704
        Args:
705
            _protect_childs (bool): [True/False] by default child jobs can not be deleted, to maintain the consistency
706
                                    - default=True
707
        """
708
        if isinstance(self.server.future, Future) and not self.server.future.done():
1✔
709
            self.server.future.cancel()
×
710
        super().remove(_protect_childs=_protect_childs)
1✔
711

712
    def remove_child(self):
1✔
713
        """
714
        internal function to remove command that removes also child jobs.
715
        Do never use this command, since it will destroy the integrity of your project.
716
        """
717
        _kill_child(job=self)
1✔
718
        super(GenericJob, self).remove_child()
1✔
719

720
    def remove_and_reset_id(self, _protect_childs=True):
1✔
721
        if self.job_id is not None:
1✔
722
            master_id, parent_id = self.master_id, self.parent_id
1✔
723
            self.remove(_protect_childs=_protect_childs)
1✔
724
            self.reset_job_id()
1✔
725
            self.master_id, self.parent_id = master_id, parent_id
1✔
726
        else:
727
            self.remove(_protect_childs=_protect_childs)
1✔
728

729
    def kill(self):
1✔
730
        if self.status.running or self.status.submitted:
×
731
            self.remove_and_reset_id()
×
732
        else:
733
            raise ValueError(
×
734
                "The kill() function is only available during the execution of the job."
735
            )
736

737
    def validate_ready_to_run(self):
1✔
738
        """
739
        Validate that the calculation is ready to be executed. By default no generic checks are performed, but one could
740
        check that the input information is complete or validate the consistency of the input at this point.
741

742
        Raises:
743
            ValueError: if ready check is unsuccessful
744
        """
745
        pass
1✔
746

747
    def check_setup(self):
1✔
748
        """
749
        Checks whether certain parameters (such as plane wave cutoff radius in DFT) are changed from the pyiron standard
750
        values to allow for a physically meaningful results. This function is called manually or only when the job is
751
        submitted to the queueing system.
752
        """
753
        pass
×
754

755
    def reset_job_id(self, job_id=None):
1✔
756
        """
757
        Reset the job id sets the job_id to None in the GenericJob as well as all connected modules like JobStatus.
758
        """
759
        super().reset_job_id(job_id=job_id)
1✔
760
        self._status = JobStatus(db=self.project.db, job_id=self._job_id)
1✔
761

762
    @deprecate(
1✔
763
        run_again="Either delete the job via job.remove() or use delete_existing_job=True.",
764
        version="0.4.0",
765
    )
766
    def run(
1✔
767
        self,
768
        delete_existing_job=False,
769
        repair=False,
770
        debug=False,
771
        run_mode=None,
772
        run_again=False,
773
    ):
774
        """
775
        This is the main run function, depending on the job status ['initialized', 'created', 'submitted', 'running',
776
        'collect','finished', 'refresh', 'suspended'] the corresponding run mode is chosen.
777

778
        Args:
779
            delete_existing_job (bool): Delete the existing job and run the simulation again.
780
            repair (bool): Set the job status to created and run the simulation again.
781
            debug (bool): Debug Mode - defines the log level of the subprocess the job is executed in.
782
            run_mode (str): ['modal', 'non_modal', 'queue', 'manual'] overwrites self.server.run_mode
783
            run_again (bool): Same as delete_existing_job (deprecated)
784
        """
785
        if not isinstance(delete_existing_job, bool):
1✔
786
            raise ValueError(
1✔
787
                f"We got delete_existing_job = {delete_existing_job}. If you"
788
                " meant to delete the job, set delete_existing_job"
789
                " = True"
790
            )
791
        with catch_signals(self.signal_intercept):
1✔
792
            if run_again:
1✔
793
                delete_existing_job = True
×
794
            try:
1✔
795
                self._logger.info(
1✔
796
                    "run {}, status: {}".format(self.job_info_str, self.status)
797
                )
798
                status = self.status.string
1✔
799
                if run_mode is not None:
1✔
800
                    self.server.run_mode = run_mode
×
801
                if delete_existing_job:
1✔
802
                    status = "initialized"
1✔
803
                    self.remove_and_reset_id(_protect_childs=False)
1✔
804
                if repair and self.job_id and not self.status.finished:
1✔
805
                    self._run_if_repair()
×
806
                elif status == "initialized":
1✔
807
                    self._run_if_new(debug=debug)
1✔
808
                elif status == "created":
1✔
809
                    self._run_if_created()
1✔
810
                elif status == "submitted":
1✔
811
                    run_job_with_status_submitted(job=self)
×
812
                elif status == "running":
1✔
813
                    self._run_if_running()
×
814
                elif status == "collect":
1✔
815
                    self._run_if_collect()
1✔
816
                elif status == "suspend":
1✔
817
                    self._run_if_suspended()
×
818
                elif status == "refresh":
1✔
819
                    self.run_if_refresh()
×
820
                elif status == "busy":
1✔
821
                    self._run_if_busy()
×
822
                elif status == "finished":
1✔
823
                    run_job_with_status_finished(job=self)
1✔
824
                elif status == "aborted":
1✔
825
                    raise ValueError(
1✔
826
                        "Running an aborted job with `delete_existing_job=False` is meaningless."
827
                    )
828
            except Exception:
1✔
829
                self.drop_status_to_aborted()
1✔
830
                raise
1✔
831

832
    def run_if_modal(self):
1✔
833
        """
834
        The run if modal function is called by run to execute the simulation, while waiting for the output. For this we
835
        use subprocess.check_output()
836
        """
837
        run_job_with_runmode_modal(job=self)
×
838

839
    def run_static(self):
1✔
840
        """
841
        The run static function is called by run to execute the simulation.
842
        """
843
        return execute_job_with_external_executable(job=self)
1✔
844

845
    def run_if_scheduler(self):
1✔
846
        """
847
        The run if queue function is called by run if the user decides to submit the job to and queing system. The job
848
        is submitted to the queuing system using subprocess.Popen()
849
        Returns:
850
            int: Returns the queue ID for the job.
851
        """
852
        return run_job_with_runmode_queue(job=self)
×
853

854
    def transfer_from_remote(self):
1✔
855
        state.queue_adapter.get_job_from_remote(
×
856
            working_directory="/".join(self.working_directory.split("/")[:-1]),
857
        )
858
        state.queue_adapter.transfer_file_to_remote(
×
859
            file=self.project_hdf5.file_name,
860
            transfer_back=True,
861
            delete_file_on_remote=True,
862
        )
863
        if state.database.database_is_disabled:
×
864
            self.project.db.update()
×
865
        else:
866
            ft = FileTable(index_from=self.project_hdf5.path + "_hdf5/")
×
867
            df = ft.job_table(
×
868
                sql_query=None,
869
                user=state.settings.login_user,
870
                project_path=None,
871
                all_columns=True,
872
            )
873
            db_dict_lst = []
×
874
            for j, st, sj, p, h, hv, c, ts, tp, tc in zip(
×
875
                df.job.values,
876
                df.status.values,
877
                df.subjob.values,
878
                df.project.values,
879
                df.hamilton.values,
880
                df.hamversion.values,
881
                df.computer.values,
882
                df.timestart.values,
883
                df.timestop.values,
884
                df.totalcputime.values,
885
            ):
886
                gp = self.project._convert_str_to_generic_path(p)
×
887
                db_dict_lst.append(
×
888
                    {
889
                        "username": state.settings.login_user,
890
                        "projectpath": gp.root_path,
891
                        "project": gp.project_path,
892
                        "job": j,
893
                        "subjob": sj,
894
                        "hamversion": hv,
895
                        "hamilton": h,
896
                        "status": st,
897
                        "computer": c,
898
                        "timestart": datetime.utcfromtimestamp(ts.tolist() / 1e9),
899
                        "timestop": datetime.utcfromtimestamp(tp.tolist() / 1e9),
900
                        "totalcputime": tc,
901
                        "masterid": self.master_id,
902
                        "parentid": None,
903
                    }
904
                )
905
            _ = [self.project.db.add_item_dict(d) for d in db_dict_lst]
×
906
        self.status.string = self.project_hdf5["status"]
×
907
        if self.master_id is not None:
×
908
            self._reload_update_master(project=self.project, master_id=self.master_id)
×
909

910
    def run_if_interactive(self):
1✔
911
        """
912
        For jobs which executables are available as Python library, those can also be executed with a library call
913
        instead of calling an external executable. This is usually faster than a single core python job.
914
        """
915
        raise NotImplementedError(
×
916
            "This function needs to be implemented in the specific class."
917
        )
918

919
    def run_if_interactive_non_modal(self):
1✔
920
        """
921
        For jobs which executables are available as Python library, those can also be executed with a library call
922
        instead of calling an external executable. This is usually faster than a single core python job.
923
        """
924
        raise NotImplementedError(
×
925
            "This function needs to be implemented in the specific class."
926
        )
927

928
    def interactive_close(self):
1✔
929
        """
930
        For jobs which executables are available as Python library, those can also be executed with a library call
931
        instead of calling an external executable. This is usually faster than a single core python job. After the
932
        interactive execution, the job can be closed using the interactive_close function.
933
        """
934
        raise NotImplementedError(
×
935
            "This function needs to be implemented in the specific class."
936
        )
937

938
    def interactive_fetch(self):
1✔
939
        """
940
        For jobs which executables are available as Python library, those can also be executed with a library call
941
        instead of calling an external executable. This is usually faster than a single core python job. To access the
942
        output data during the execution the interactive_fetch function is used.
943
        """
944
        raise NotImplementedError(
×
945
            "This function needs to be implemented in the specific class."
946
        )
947

948
    def interactive_flush(self, path="generic", include_last_step=True):
1✔
949
        """
950
        For jobs which executables are available as Python library, those can also be executed with a library call
951
        instead of calling an external executable. This is usually faster than a single core python job. To write the
952
        interactive cache to the HDF5 file the interactive flush function is used.
953
        """
954
        raise NotImplementedError(
×
955
            "This function needs to be implemented in the specific class."
956
        )
957

958
    def send_to_database(self):
1✔
959
        """
960
        if the jobs should be store in the external/public database this could be implemented here, but currently it is
961
        just a placeholder.
962
        """
963
        if self.server.send_to_db:
1✔
964
            pass
×
965

966
    def _init_child_job(self, parent):
1✔
967
        """
968
        Finalize job initialization when job instance is created as a child from another one.
969

970
        Master jobs use this to set their own reference job, when created from that reference job.
971

972
        Args:
973
            parent (:class:`.GenericJob`): job instance that this job was created from
974
        """
975
        pass
×
976

977
    def create_job(self, job_type, job_name, delete_existing_job=False):
1✔
978
        """
979
        Create one of the following jobs:
980
        - 'StructureContainer’:
981
        - ‘StructurePipeline’:
982
        - ‘AtomisticExampleJob’: example job just generating random number
983
        - ‘ExampleJob’: example job just generating random number
984
        - ‘Lammps’:
985
        - ‘KMC’:
986
        - ‘Sphinx’:
987
        - ‘Vasp’:
988
        - ‘GenericMaster’:
989
        - ‘ParallelMaster’: series of jobs run in parallel
990
        - ‘KmcMaster’:
991
        - ‘ThermoLambdaMaster’:
992
        - ‘RandomSeedMaster’:
993
        - ‘MeamFit’:
994
        - ‘Murnaghan’:
995
        - ‘MinimizeMurnaghan’:
996
        - ‘ElasticMatrix’:
997
        - ‘ConvergenceEncutParallel’:
998
        - ‘ConvergenceKpointParallel’:
999
        - ’PhonopyMaster’:
1000
        - ‘DefectFormationEnergy’:
1001
        - ‘LammpsASE’:
1002
        - ‘PipelineMaster’:
1003
        - ’TransformationPath’:
1004
        - ‘ThermoIntEamQh’:
1005
        - ‘ThermoIntDftEam’:
1006
        - ‘ScriptJob’: Python script or jupyter notebook job container
1007
        - ‘ListMaster': list of jobs
1008

1009
        Args:
1010
            job_type (str): job type can be ['StructureContainer’, ‘StructurePipeline’, ‘AtomisticExampleJob’,
1011
                                             ‘ExampleJob’, ‘Lammps’, ‘KMC’, ‘Sphinx’, ‘Vasp’, ‘GenericMaster’,
1012
                                             ‘ParallelMaster’, ‘KmcMaster’,
1013
                                             ‘ThermoLambdaMaster’, ‘RandomSeedMaster’, ‘MeamFit’, ‘Murnaghan’,
1014
                                             ‘MinimizeMurnaghan’, ‘ElasticMatrix’,
1015
                                             ‘ConvergenceEncutParallel’, ‘ConvergenceKpointParallel’, ’PhonopyMaster’,
1016
                                             ‘DefectFormationEnergy’, ‘LammpsASE’, ‘PipelineMaster’,
1017
                                             ’TransformationPath’, ‘ThermoIntEamQh’, ‘ThermoIntDftEam’, ‘ScriptJob’,
1018
                                             ‘ListMaster']
1019
            job_name (str): name of the job
1020
            delete_existing_job (bool): delete an existing job - default false
1021

1022
        Returns:
1023
            GenericJob: job object depending on the job_type selected
1024
        """
1025
        job = self.project.create_job(
1✔
1026
            job_type=job_type,
1027
            job_name=job_name,
1028
            delete_existing_job=delete_existing_job,
1029
        )
1030
        job._init_child_job(self)
1✔
1031
        return job
1✔
1032

1033
    def update_master(self, force_update=False):
1✔
1034
        """
1035
        After a job is finished it checks whether it is linked to any metajob - meaning the master ID is pointing to
1036
        this jobs job ID. If this is the case and the master job is in status suspended - the child wakes up the master
1037
        job, sets the status to refresh and execute run on the master job. During the execution the master job is set to
1038
        status refresh. If another child calls update_master, while the master is in refresh the status of the master is
1039
        set to busy and if the master is in status busy at the end of the update_master process another update is
1040
        triggered.
1041

1042
        Args:
1043
            force_update (bool): Whether to check run mode for updating master
1044
        """
1045
        if not state.database.database_is_disabled:
1✔
1046
            master_id = self.master_id
1✔
1047
            project = self.project
1✔
1048
            self._logger.info(
1✔
1049
                "update master: {} {} {}".format(
1050
                    master_id, self.get_job_id(), self.server.run_mode
1051
                )
1052
            )
1053
            if master_id is not None and (
1✔
1054
                force_update
1055
                or not (
1056
                    self.server.run_mode.thread
1057
                    or self.server.run_mode.modal
1058
                    or self.server.run_mode.interactive
1059
                    or self.server.run_mode.worker
1060
                )
1061
            ):
1062
                self._reload_update_master(project=project, master_id=master_id)
×
1063

1064
    def job_file_name(self, file_name, cwd=None):
1✔
1065
        """
1066
        combine the file name file_name with the path of the current working directory
1067

1068
        Args:
1069
            file_name (str): name of the file
1070
            cwd (str): current working directory - this overwrites self.project_hdf5.working_directory - optional
1071

1072
        Returns:
1073
            str: absolute path to the file in the current working directory
1074
        """
1075
        if cwd is None:
×
1076
            cwd = self.project_hdf5.working_directory
×
1077
        return posixpath.join(cwd, file_name)
×
1078

1079
    def _set_hdf(self, hdf=None, group_name=None):
1✔
1080
        if hdf is not None:
1✔
1081
            self._hdf5 = hdf
1✔
1082
        if group_name is not None and self._hdf5 is not None:
1✔
1083
            self._hdf5 = self._hdf5.open(group_name)
1✔
1084

1085
    def to_dict(self):
1✔
1086
        data_dict = self._type_to_dict()
1✔
1087
        data_dict["status"] = self.status.string
1✔
1088
        data_dict["input/generic_dict"] = {
1✔
1089
            "restart_file_list": self.restart_file_list,
1090
            "restart_file_dict": self._restart_file_dict,
1091
            "exclude_nodes_hdf": self._exclude_nodes_hdf,
1092
            "exclude_groups_hdf": self._exclude_groups_hdf,
1093
        }
1094
        data_dict["server"] = self._server.to_dict()
1✔
1095
        self._executable_activate_mpi()
1✔
1096
        if self._executable is not None:
1✔
1097
            data_dict["executable"] = self._executable.to_dict()
1✔
1098
        if self._import_directory is not None:
1✔
1099
            data_dict["import_directory"] = self._import_directory
×
1100
        if self._executor_type is not None:
1✔
1101
            data_dict["executor_type"] = self._executor_type
1✔
1102
        if len(self._files_to_compress) > 0:
1✔
1103
            data_dict["files_to_compress"] = self._files_to_compress
×
1104
        if len(self._files_to_remove) > 0:
1✔
1105
            data_dict["files_to_compress"] = self._files_to_remove
×
1106
        return data_dict
1✔
1107

1108
    def from_dict(self, job_dict):
1✔
1109
        self._type_from_dict(type_dict=job_dict)
1✔
1110
        if "import_directory" in job_dict.keys():
1✔
1111
            self._import_directory = job_dict["import_directory"]
×
1112
        self._server.from_dict(server_dict=job_dict["server"])
1✔
1113
        if "executable" in job_dict.keys() and job_dict["executable"] is not None:
1✔
1114
            self.executable.from_dict(job_dict["executable"])
1✔
1115
        input_dict = job_dict["input"]
1✔
1116
        if "generic_dict" in input_dict.keys():
1✔
1117
            generic_dict = input_dict["generic_dict"]
1✔
1118
            self._restart_file_list = generic_dict["restart_file_list"]
1✔
1119
            self._restart_file_dict = generic_dict["restart_file_dict"]
1✔
1120
            self._exclude_nodes_hdf = generic_dict["exclude_nodes_hdf"]
1✔
1121
            self._exclude_groups_hdf = generic_dict["exclude_groups_hdf"]
1✔
1122
        # Backwards compatbility
1123
        if "restart_file_list" in input_dict.keys():
1✔
1124
            self._restart_file_list = input_dict["restart_file_list"]
×
1125
        if "restart_file_dict" in input_dict.keys():
1✔
1126
            self._restart_file_dict = input_dict["restart_file_dict"]
×
1127
        if "exclude_nodes_hdf" in input_dict.keys():
1✔
1128
            self._exclude_nodes_hdf = input_dict["exclude_nodes_hdf"]
×
1129
        if "exclude_groups_hdf" in input_dict.keys():
1✔
1130
            self._exclude_groups_hdf = input_dict["exclude_groups_hdf"]
×
1131
        if "executor_type" in input_dict.keys():
1✔
1132
            self._executor_type = input_dict["executor_type"]
×
1133

1134
    def to_hdf(self, hdf=None, group_name=None):
1✔
1135
        """
1136
        Store the GenericJob in an HDF5 file
1137

1138
        Args:
1139
            hdf (ProjectHDFio): HDF5 group object - optional
1140
            group_name (str): HDF5 subgroup name - optional
1141
        """
1142
        self._set_hdf(hdf=hdf, group_name=group_name)
1✔
1143
        self._hdf5.write_dict(data_dict=self.to_dict())
1✔
1144

1145
    @classmethod
1✔
1146
    def from_hdf_args(cls, hdf):
1✔
1147
        """
1148
        Read arguments for instance creation from HDF5 file
1149

1150
        Args:
1151
            hdf (ProjectHDFio): HDF5 group object
1152
        """
1153
        job_name = posixpath.splitext(posixpath.basename(hdf.file_name))[0]
1✔
1154
        project_hdf5 = type(hdf)(
1✔
1155
            project=hdf.create_project_from_hdf5(), file_name=job_name
1156
        )
1157
        return {"job_name": job_name, "project": project_hdf5}
1✔
1158

1159
    def from_hdf(self, hdf=None, group_name=None):
1✔
1160
        """
1161
        Restore the GenericJob from an HDF5 file
1162

1163
        Args:
1164
            hdf (ProjectHDFio): HDF5 group object - optional
1165
            group_name (str): HDF5 subgroup name - optional
1166
        """
1167
        self._set_hdf(hdf=hdf, group_name=group_name)
1✔
1168
        job_dict = self._hdf5.read_dict_from_hdf()
1✔
1169
        with self._hdf5.open("input") as hdf5_input:
1✔
1170
            job_dict["input"] = hdf5_input.read_dict_from_hdf(recursive=True)
1✔
1171
        # Backwards compatibility to the previous HasHDF based interface
1172
        if "executable" in self._hdf5.list_groups():
1✔
1173
            exe_dict = self._hdf5["executable/executable"].to_object().to_builtin()
×
1174
            exe_dict["READ_ONLY"] = self._hdf5["executable/executable/READ_ONLY"]
×
1175
            job_dict["executable"] = {"executable": exe_dict}
×
1176
        self.from_dict(job_dict=job_dict)
1✔
1177

1178
    def save(self):
1✔
1179
        """
1180
        Save the object, by writing the content to the HDF5 file and storing an entry in the database.
1181

1182
        Returns:
1183
            (int): Job ID stored in the database
1184
        """
1185
        self.to_hdf()
1✔
1186
        if not state.database.database_is_disabled:
1✔
1187
            job_id = self.project.db.add_item_dict(self.db_entry())
1✔
1188
            self._job_id = job_id
1✔
1189
            _write_hdf(
1✔
1190
                hdf_filehandle=self.project_hdf5.file_name,
1191
                data=job_id,
1192
                h5_path=self.job_name + "/job_id",
1193
                overwrite="update",
1194
            )
1195
            self.refresh_job_status()
1✔
1196
        else:
1197
            job_id = self.job_name
1✔
1198
        if self._check_if_input_should_be_written():
1✔
1199
            self.project_hdf5.create_working_directory()
1✔
1200
            self.write_input()
1✔
1201
        self.status.created = True
1✔
1202
        print(
1✔
1203
            "The job "
1204
            + self.job_name
1205
            + " was saved and received the ID: "
1206
            + str(job_id)
1207
        )
1208
        return job_id
1✔
1209

1210
    def convergence_check(self):
1✔
1211
        """
1212
        Validate the convergence of the calculation.
1213

1214
        Returns:
1215
             (bool): If the calculation is converged
1216
        """
1217
        return True
1✔
1218

1219
    def db_entry(self):
1✔
1220
        """
1221
        Generate the initial database entry for the current GenericJob
1222

1223
        Returns:
1224
            (dict): database dictionary {"username", "projectpath", "project", "job", "subjob", "hamversion",
1225
                                         "hamilton", "status", "computer", "timestart", "masterid", "parentid"}
1226
        """
1227
        db_dict = {
1✔
1228
            "username": state.settings.login_user,
1229
            "projectpath": self.project_hdf5.root_path,
1230
            "project": self.project_hdf5.project_path,
1231
            "job": self.job_name,
1232
            "subjob": self.project_hdf5.h5_path,
1233
            "hamversion": self.version,
1234
            "hamilton": self.__name__,
1235
            "status": self.status.string,
1236
            "computer": self._db_server_entry(),
1237
            "timestart": datetime.now(),
1238
            "masterid": self.master_id,
1239
            "parentid": self.parent_id,
1240
        }
1241
        return db_dict
1✔
1242

1243
    def restart(self, job_name=None, job_type=None):
1✔
1244
        """
1245
        Create an restart calculation from the current calculation - in the GenericJob this is the same as create_job().
1246
        A restart is only possible after the current job has finished. If you want to run the same job again with
1247
        different input parameters use job.run(delete_existing_job=True) instead.
1248

1249
        Args:
1250
            job_name (str): job name of the new calculation - default=<job_name>_restart
1251
            job_type (str): job type of the new calculation - default is the same type as the exeisting calculation
1252

1253
        Returns:
1254

1255
        """
1256
        if self.job_id is None:
1✔
UNCOV
1257
            self.save()
×
1258
        if job_name is None:
1✔
1259
            job_name = "{}_restart".format(self.job_name)
1✔
1260
        if job_type is None:
1✔
1261
            job_type = self.__name__
1✔
1262
        if job_type == self.__name__ and job_name not in self.project.list_nodes():
1✔
1263
            new_ham = self.copy_to(
1✔
1264
                new_job_name=job_name,
1265
                new_database_entry=False,
1266
                input_only=True,
1267
                copy_files=False,
1268
            )
1269
        else:
UNCOV
1270
            new_ham = self.create_job(job_type, job_name)
×
1271
        new_ham.parent_id = self.job_id
1✔
1272
        # ensuring that the new job does not inherit the restart_file_list from the old job
1273
        new_ham._restart_file_list = list()
1✔
1274
        new_ham._restart_file_dict = dict()
1✔
1275
        return new_ham
1✔
1276

1277
    def _list_all(self):
1✔
1278
        """
1279
        List all groups and nodes of the HDF5 file - where groups are equivalent to directories and nodes to files.
1280

1281
        Returns:
1282
            dict: {'groups': [list of groups], 'nodes': [list of nodes]}
1283
        """
UNCOV
1284
        h5_dict = self.project_hdf5.list_all()
×
1285
        if self.server.new_hdf:
×
1286
            h5_dict["groups"] += self._list_ext_childs()
×
1287
        return h5_dict
×
1288

1289
    def signal_intercept(self, sig):
1✔
1290
        """
1291
        Abort the job and log signal that caused it.
1292

1293
        Expected to be called from
1294
        :func:`pyiron_base.state.signal.catch_signals`.
1295

1296
        Args:
1297
            sig (int): the signal that triggered the abort
1298
        """
UNCOV
1299
        try:
×
1300
            self._logger.info(
×
1301
                "Job {} intercept signal {}, job is shutting down".format(
1302
                    self._job_id, sig
1303
                )
1304
            )
UNCOV
1305
            self.drop_status_to_aborted()
×
1306
        except:
×
1307
            raise
×
1308

1309
    def drop_status_to_aborted(self):
1✔
1310
        """
1311
        Change the job status to aborted when the job was intercepted.
1312
        """
1313
        self.refresh_job_status()
1✔
1314
        if not (self.status.finished or self.status.suspended):
1✔
1315
            self.status.aborted = True
1✔
1316
            self.project_hdf5["status"] = self.status.string
1✔
1317

1318
    def _run_if_new(self, debug=False):
1✔
1319
        """
1320
        Internal helper function the run if new function is called when the job status is 'initialized'. It prepares
1321
        the hdf5 file and the corresponding directory structure.
1322

1323
        Args:
1324
            debug (bool): Debug Mode
1325
        """
1326
        run_job_with_status_initialized(job=self, debug=debug)
1✔
1327

1328
    def _run_if_created(self):
1✔
1329
        """
1330
        Internal helper function the run if created function is called when the job status is 'created'. It executes
1331
        the simulation, either in modal mode, meaning waiting for the simulation to finish, manually, or submits the
1332
        simulation to the que.
1333

1334
        Returns:
1335
            int: Queue ID - if the job was send to the queue
1336
        """
1337
        return run_job_with_status_created(job=self)
1✔
1338

1339
    def _run_if_repair(self):
1✔
1340
        """
1341
        Internal helper function the run if repair function is called when the run() function is called with the
1342
        'repair' parameter.
1343
        """
UNCOV
1344
        run_job_with_parameter_repair(job=self)
×
1345

1346
    def _run_if_running(self):
1✔
1347
        """
1348
        Internal helper function the run if running function is called when the job status is 'running'. It allows the
1349
        user to interact with the simulation while it is running.
1350
        """
UNCOV
1351
        run_job_with_status_running(job=self)
×
1352

1353
    def run_if_refresh(self):
1✔
1354
        """
1355
        Internal helper function the run if refresh function is called when the job status is 'refresh'. If the job was
1356
        suspended previously, the job is going to be started again, to be continued.
1357
        """
UNCOV
1358
        run_job_with_status_refresh(job=self)
×
1359

1360
    def set_input_to_read_only(self):
1✔
1361
        """
1362
        This function enforces read-only mode for the input classes, but it has to be implemented in the individual
1363
        classes.
1364
        """
1365
        self.server.lock()
1✔
1366

1367
    def _run_if_busy(self):
1✔
1368
        """
1369
        Internal helper function the run if busy function is called when the job status is 'busy'.
1370
        """
UNCOV
1371
        run_job_with_status_busy(job=self)
×
1372

1373
    def _run_if_collect(self):
1✔
1374
        """
1375
        Internal helper function the run if collect function is called when the job status is 'collect'. It collects
1376
        the simulation output using the standardized functions collect_output() and collect_logfiles(). Afterwards the
1377
        status is set to 'finished'
1378
        """
1379
        run_job_with_status_collect(job=self)
1✔
1380

1381
    def _run_if_suspended(self):
1✔
1382
        """
1383
        Internal helper function the run if suspended function is called when the job status is 'suspended'. It
1384
        restarts the job by calling the run if refresh function after setting the status to 'refresh'.
1385
        """
UNCOV
1386
        run_job_with_status_suspended(job=self)
×
1387

1388
    def _executable_activate(self, enforce=False, codename=None):
1✔
1389
        """
1390
        Internal helper function to koad the executable object, if it was not loaded already.
1391

1392
        Args:
1393
            enforce (bool): Force the executable module to reinitialize
1394
            codename (str): Name of the resource directory and run script.
1395
        """
1396
        if self._executable is None or enforce:
1✔
1397
            if codename is not None:
1✔
UNCOV
1398
                self._executable = Executable(
×
1399
                    codename=codename,
1400
                    module=codename,
1401
                    path_binary_codes=None,
1402
                )
1403
            elif len(self.__module__.split(".")) > 1:
1✔
1404
                self._executable = Executable(
1✔
1405
                    codename=self.__name__,
1406
                    module=self.__module__.split(".")[-2],
1407
                    path_binary_codes=None,
1408
                )
UNCOV
1409
            elif self.__module__ == "__main__":
×
1410
                # Special case when the job classes defined in Jupyter notebooks
UNCOV
1411
                parent_class = self.__class__.__bases__[0]
×
1412
                self._executable = Executable(
×
1413
                    codename=parent_class.__name__,
1414
                    module=parent_class.__module__.split(".")[-2],
1415
                    path_binary_codes=None,
1416
                )
1417
            else:
UNCOV
1418
                self._executable = Executable(
×
1419
                    codename=self.__name__,
1420
                    path_binary_codes=None,
1421
                )
1422

1423
    def _type_to_dict(self):
1✔
1424
        """
1425
        Internal helper function to save type and version in HDF5 file root
1426
        """
1427
        data_dict = super()._type_to_dict()
1✔
1428
        if self._executable:  # overwrite version - default self.__version__
1✔
1429
            data_dict["VERSION"] = self.executable.version
1✔
1430
        if hasattr(self, "__hdf_version__"):
1✔
1431
            data_dict["HDF_VERSION"] = self.__hdf_version__
1✔
1432
        return data_dict
1✔
1433

1434
    def _type_from_dict(self, type_dict):
1✔
1435
        self.__obj_type__ = type_dict["TYPE"]
1✔
1436
        if self._executable is None:
1✔
1437
            self.__obj_version__ = type_dict["VERSION"]
1✔
1438

1439
    def _type_from_hdf(self):
1✔
1440
        """
1441
        Internal helper function to load type and version from HDF5 file root
1442
        """
UNCOV
1443
        self._type_from_dict(
×
1444
            type_dict={
1445
                "TYPE": self._hdf5["TYPE"],
1446
                "VERSION": self._hdf5["VERSION"],
1447
            }
1448
        )
1449

1450
    def run_time_to_db(self):
1✔
1451
        """
1452
        Internal helper function to store the run_time in the database
1453
        """
1454
        if not state.database.database_is_disabled and self.job_id is not None:
1✔
1455
            self.project.db.item_update(self._runtime(), self.job_id)
1✔
1456

1457
    def _runtime(self):
1✔
1458
        """
1459
        Internal helper function to calculate runtime by substracting the starttime, from the stoptime.
1460

1461
        Returns:
1462
            (dict): Database dictionary db_dict
1463
        """
1464
        start_time = self.project.db.get_item_by_id(self.job_id)["timestart"]
1✔
1465
        stop_time = datetime.now()
1✔
1466
        return {
1✔
1467
            "timestop": stop_time,
1468
            "totalcputime": int((stop_time - start_time).total_seconds()),
1469
        }
1470

1471
    def _db_server_entry(self):
1✔
1472
        """
1473
        Internal helper function to connect all the info regarding the server into a single word that can be used
1474
        e.g. as entry in a database
1475

1476
        Returns:
1477
            (str): server info as single word
1478

1479
        """
1480
        return self._server.db_entry()
1✔
1481

1482
    def _executable_activate_mpi(self):
1✔
1483
        """
1484
        Internal helper function to switch the executable to MPI mode
1485
        """
1486
        try:
1✔
1487
            if self.server.cores > 1:
1✔
1488
                self.executable.mpi = True
1✔
UNCOV
1489
        except ValueError:
×
1490
            self.server.cores = 1
×
1491
            warnings.warn(
×
1492
                "No multi core executable found falling back to the single core executable.",
1493
                RuntimeWarning,
1494
            )
1495

1496
    @deprecate("Use job.save()")
1✔
1497
    def _create_job_structure(self, debug=False):
1✔
1498
        """
1499
        Internal helper function to create the input directories, save the job in the database and write the wrapper.
1500

1501
        Args:
1502
            debug (bool): Debug Mode
1503
        """
UNCOV
1504
        self.save()
×
1505

1506
    def _check_if_input_should_be_written(self):
1✔
1507
        if self._python_only_job:
1✔
1508
            return False
1✔
1509
        else:
1510
            return not (
1✔
1511
                self.server.run_mode.interactive
1512
                or self.server.run_mode.interactive_non_modal
1513
            )
1514

1515
    def _before_successor_calc(self, ham):
1✔
1516
        """
1517
        Internal helper function which is executed based on the hamiltonian of the successor job, before it is executed.
1518
        This function is used to execute a series of jobs based on their parent relationship - marked by the parent ID.
1519
        Mainly used by the ListMaster job type.
1520
        """
UNCOV
1521
        pass
×
1522

1523
    def _reload_update_master(self, project, master_id):
1✔
UNCOV
1524
        queue_flag = self.server.run_mode.queue
×
UNCOV
1525
        master_db_entry = project.db.get_item_by_id(master_id)
×
UNCOV
1526
        if master_db_entry["status"] == "suspended":
×
1527
            project.db.set_job_status(job_id=master_id, status="refresh")
×
1528
            self._logger.info("run_if_refresh() called")
×
1529
            del self
×
1530
            master_inspect = project.inspect(master_id)
×
1531
            if master_inspect["server"]["run_mode"] == "non_modal" or (
×
1532
                master_inspect["server"]["run_mode"] == "modal" and queue_flag
1533
            ):
UNCOV
1534
                master = project.load(master_id)
×
UNCOV
1535
                master.run_if_refresh()
×
UNCOV
1536
        elif master_db_entry["status"] == "refresh":
×
UNCOV
1537
            project.db.set_job_status(job_id=master_id, status="busy")
×
UNCOV
1538
            self._logger.info("busy master: {} {}".format(master_id, self.get_job_id()))
×
UNCOV
1539
            del self
×
1540

1541
    def _get_executor(self, max_workers=None):
1✔
1542
        if self._executor_type is None:
1✔
1543
            raise ValueError(
1✔
1544
                "No executor type defined - Please set self.executor_type."
1545
            )
1546
        elif (
1✔
1547
            self._executor_type == "pympipool.Executor"
1548
            and platform.system() == "Darwin"
1549
        ):
1550
            # The Mac firewall might prevent connections based on the network address - especially Github CI
UNCOV
1551
            return import_class(self._executor_type)(
×
1552
                max_cores=max_workers, hostname_localhost=True
1553
            )
1554
        elif self._executor_type == "pympipool.Executor":
1✔
1555
            # The pympipool Executor defines max_cores rather than max_workers
1556
            return import_class(self._executor_type)(max_cores=max_workers)
1✔
1557
        elif isinstance(self._executor_type, str):
1✔
1558
            return import_class(self._executor_type)(max_workers=max_workers)
1✔
1559
        else:
UNCOV
1560
            raise TypeError("The self.executor_type has to be a string.")
×
1561

1562

1563
class GenericError(object):
1✔
1564
    def __init__(self, working_directory):
1✔
1565
        self._working_directory = working_directory
1✔
1566

1567
    def __repr__(self):
1✔
1568
        all_messages = ""
1✔
1569
        for message in [self.print_message(), self.print_queue()]:
1✔
1570
            if message is True:
1✔
1571
                all_messages += message
×
1572
        if len(all_messages) == 0:
1✔
1573
            all_messages = "There is no error/warning"
1✔
1574
        return all_messages
1✔
1575

1576
    def print_message(self, string=""):
1✔
1577
        return self._print_error(file_name="error.msg", string=string)
1✔
1578

1579
    def print_queue(self, string=""):
1✔
1580
        return self._print_error(file_name="error.out", string=string)
1✔
1581

1582
    def _print_error(self, file_name, string="", print_yes=True):
1✔
1583
        if not os.path.exists(os.path.join(self._working_directory, file_name)):
1✔
1584
            return ""
1✔
UNCOV
1585
        elif print_yes:
×
UNCOV
1586
            with open(os.path.join(self._working_directory, file_name)) as f:
×
UNCOV
1587
                return string.join(f.readlines())
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc