Public functions
****************

This chapter gathers information about public functions that can be
used in PLAMS scripts.

init(path=None, folder=None, config_settings=None, quiet=False, use_existing_folder=False)

   Initialize PLAMS environment. Set up the global "config" and its
   default "JobManager".

   This function only needs to be called to override default values on
   the global "config", without interacting with it directly. For
   example, both of the following approaches are equivalent:

      # Call to init
      init(path="my/path", folder="my_folder", config_settings=my_settings)

      # Direct modification of config
      config.update(my_settings) # direct modification of config
      config.default_jobmanager = JobManager(config.jobmanager, "my/path", "my_folder")

   Note:

     A call to "init()" with no arguments is therefore a no-op, and is
     not required. It will however force initialisation of the lazy
     default "JobManager". This is in contrast to AMS2024 and earlier
     versions, where this function had to be called before any other
     PLAMS command could be executed.

   Parameters:
      * **path** (*str** | **None*) -- for the default "JobManager"

      * **folder** (*str** | **None*) -- for the default "JobManager"

      * **use_existing_folder** (*bool*) -- for the default
        "JobManager"

      * **config_settings** (*Dict** | **None*) -- "Settings" to
        update config with - these will overwrite any existing items

      * **quiet** (*bool*) -- do not log header with information about
        the PLAMS environment

   Return type:
      None

finish(otherJM=None)

   Clean up the PLAMS environment. This can be explicitly called for
   Cleaning job folder to take place. If you used some other job
   managers than just the default one, they need to be passed as
   *otherJM*.

   A call is registered to be performed at exit, which will perform
   cleaning for the default job manager.

   Note:

     A call to "finish()" with no arguments once at the end of a
     script is therefore a no-op, and is not required. This is in
     contrast to AMS2024 and earlier versions, where this function had
     to be called at the end of a script for Cleaning job folder to
     take place.

   Parameters:
      **otherJM** (*Iterable**[**JobManager**] **| **None*) --
      additional job managers used in a workflow to be cleaned

   Return type:
      None

jobs_in_directory(path)

   Enter a context which uses a directory with the given path when
   running new Jobs. The provided path is always relative to the
   "workdir".

   A "Job" run within this context will have its input and results
   files stored within the job directory in this directory.

   Note that:
      * For a "MultiJob", this will apply to the job directory of the
        top-most parent "MultiJob"

      * The directory is not created until a job is run in this
        context

      * Heriarchies of directories can be created by nesting a context
        within another

      * The absolute path of the directory is returned on entering the
        context, which assumes the default "JobManager" is used. If
        another job manager is used within the context, the actual
        directory will be relative to its "workdir".

      >>> with jobs_in_directory("GeometryOptimization") as go_dir:
      >>>     with jobs_in_directory("DFTB"):
      >>>        job1.run()
      >>>     with jobs_in_directory("ML/M3GNet"):
      >>>        job2.run()
      >>>     print(go_dir)
      >>>     print(job1.path)
      >>>     print(job2.path)
         path/plams_workdir/GeometryOptimization
         path/plams_workdir/GeometryOptimization/DFTB/job1
         path/plams_workdir/GeometryOptimization/ML/M3GNet/job2

   Note:

     Starting a new thread creates a new context, so the context
     configuration will not automatically be used in the new thread.
     To copy over the parent thread context to the new thread, instead
     use "ContextAwareThread"

   Parameters:
      **path** (*str** | **PathLike*) -- path of the run directory
      relative to the "workdir"

   Returns:
      absolute path of the directory, assuming the default
      "JobManager" is used

   Return type:
      *Generator*[*Path*, None, None]

load(filename)

   Load previously saved job from ".dill" file. This is just a
   shortcut for "load_job()" method of the default "JobManager"
   "config.default_jobmanager".

   Parameters:
      **filename** (*str*) --

   Return type:
      Job | None

load_all(path, jobmanager=None)

   Load all jobs from *path*.

   This function works as multiple executions of "load_job()". It
   searches for ".dill" files inside the directory given by *path*,
   yet not directly in it, but one level deeper. In other words, all
   files matching "path/*/*.dill" are used. That way a path to the
   main working folder of a previously run script can be used to
   import all the jobs run by that script.

   In case of partially failed "MultiJob" instances (some children
   jobs finished successfully, but not all) the function will search
   for ".dill" files in children folders. That means, if
   "path/[multijobname]/" contains some subfolders (for children jobs)
   but does not contail a ".dill" file (the "MultiJob" was not fully
   successful), it will look into these subfolders. This behavior is
   recursive up to any folder tree depth.

   The purpose of this function is to provide a quick way of
   restarting a script. Loading all successful jobs from the previous
   run prevents double work and allows the new execution of the script
   to proceed directly to the place where the previous execution
   failed.

   Jobs are loaded using default job manager stored in
   "config.default_jobmanager". If you wish to use a different one you
   can pass it as *jobmanager* argument of this function.

   Returned value is a dictionary containing all loaded jobs as values
   and absolute paths to ".dill" files as keys.

   Parameters:
      * **path** (*str*) --

      * **jobmanager** (*JobManager** | **None*) --

   Return type:
      *Dict*[str, Job | JobDict]

read_molecules(folder, formats=None)

   Read all molecules from *folder*.

   Read all the files present in *folder* with extensions compatible
   with "Molecule.read". Returned value is a dictionary with keys
   being molecule names (filename without extension) and values being
   "Molecule" instances.

   The optional argument *formats* can be used to narrow down the
   search to files with specified extensions:

      molecules = read_molecules('mymols', formats=['xyz', 'pdb'])

   Parameters:
      * **folder** (*str*) --

      * **formats** (*Sequence**[**str**] **| **None*) --

   Return type:
      *Dict*[str, Molecule]

read_all_molecules_in_xyz_file(filename)

   The .xyz format allows to store multiple geometries on a single
   file (such file is essentially a concatenated series of xyz files)

   This function returns a *list* of all molecules found in the file
   *filename*

   *filename*: path (absolute or relative to the current working
   directory) to the xyz file.

   Parameters:
      **filename** (*str*) --

   Return type:
      *List*[Molecule]


Logging
=======

PLAMS features a simple logging mechanism. All important actions
happening in functions and methods register their activity using log
messages. These messages can be printed to the standard output and/or
saved to the logfile located in the main working folder.

Every log message has its "verbosity" defined as an integer number:
the higher the number, the more detailed and descriptive the message
is. In other words, it is a measure of importance of the message.
Important events (like "job started", "job finished", "something went
wrong") should have low verbosity, whereas less crucial ones (for
example "pickling of job X successful") -- higher verbosity. The
purpose of that is to allow the user to choose how verbose the whole
logfile is. Each log output (either file or stdout) has an integer
number associated with it, defining which messages are printed to this
channel (for example, if this number is 3, all messages with verbosity
3 or less are printed). That way using a smaller number results in the
log being short and containing only the most relevant information
while larger numbers produce longer and more detailed log messages.

The behavior of the logging mechanism is adjusted by "config.log"
settings branch with the following keys:

* "file" (integer) -- verbosity of logs printed to the "logfile" in
  the main working folder.

* "stdout" (integer) -- verbosity of logs printed to the standard
  output.

* "time" (boolean) -- print time of each log event.

* "date" (boolean) -- print date of each log event.

Log messages used within the PLAMS code use four different levels of
verbosity:

* **1**: important

* **3**: normal

* **5**: verbose

* **7**: debug

Even levels are left empty for the user. For example, if you find
level 5 too verbose and still want to be able to switch on and off log
messages of your own code, you can log them with verbosity 4.

Note:

  Your own code can (and should) contain some "log()" calls. They are
  very important for debugging purposes.

log(message, level=0)

   Log a *message* with verbosity *level*.

   Logs are printed independently to the text logfile (a file called
   "logfile" in the main working folder) and to the standard output.
   If *level* is equal or lower than verbosity (defined by
   "config.log.file" or "config.log.stdout") the message is printed.
   By convention in PLAMS, level should be between 0-7, with 0
   indicating no logging and 7 indicating the most verbose logging.
   Date and/or time can be added based on "config.log.date" and
   "config.log.time". All logging activity is thread safe.

   Parameters:
      * **message** (*str*) --

      * **level** (*int*) --

   Return type:
      None


Binding decorators
==================

Sometimes one wants to expand the functionality of a class by adding a
new method or modifying an existing one. It can be done in a few
different ways:

* One can go directly to the source code defining the class and modify
  it there before running a script. Such a change is global -- it
  affects all the future scripts, so in most cases it is not a good
  thing (for defining "prerun()", for example).

* Creating a subclass with new or modified method definitions is
  usually the best solution. It can be done directly in your script
  before the work is done. The newly defined class can be then used
  instead of the old one. However, this solution fails in some rare
  cases when a method needs to differ for different instances or when
  it needs to be changed during the runtime of the script.

* PLAMS binding decorator "add_to_instance()" can be used.

Binding decorators associate methods to existing class instances
without having to define a subclass. Such changes are visible only
inside the script in which they are used.

The usage of the PLAMS "add_to_instance()" binding decorator is
straightforward. You simply define a regular function somewhere inside
your script and decorate it with "add_to_instance()". The function
needs to have a valid method syntax, so it should have "self" as the
first argument and use it to reference the class instance.

add_to_instance(instance)

   Add decorated function as a method to one particular *instance*.

   The decorated function should follow a method-like syntax, with the
   first argument "self" that references the class instance. Example
   usage:

      results = myjob.run()

      @add_to_instance(results)
      def get_energy(self):
          return self.readkf('Energy', 'Bond Energy')

      results.get_energy()

   The added method is accessible only for that one particular
   instance and it overrides any methods with the same name defined on
   a class level (in original class' source) or added with
   "add_to_class()" decorator.

   If *instance* is an instance of "Results" or any of its subclasses,
   the added method will be wrapped with the thread safety guard (see
   Synchronization of parallel job executions).

   Parameters:
      **instance** (*object*) --

   Return type:
      *Callable*

Technical:

  The above decorator is in fact a decorator factory that, given an
  object (class or instance), produces a decorator that binds the
  function as a method of that object. The decorator is for adding
  instance methods only, it cannot be used for static or class
  methods.
