8.4. ParAMSJob
**************

Starting with AMS2023.1, if you want to run ParAMS through Python it
is recommended to use the "ParAMSJob" class, and to get the results
with the corresponding "ParAMSResults".

**To get started**, see the ParAMS Python examples. Many other
tutorials also give examples of ParAMSJob and ParAMSResults.

These classes can be used in the normal PLAMS way. See also: Python
Scripting with PLAMS See also: Run a ParAMSJob for Lennard-Jones

In particular,

* Specify the job settings in the "ParAMSJob.settings.input" like you
  would for other PLAMS jobs

* "ParAMSJob" has some convenient functions like "add_exit_condition"
  to easily add Exit Conditions, Optimizers, and Stoppers to the input
  settings.

* You can run several "params" runs in parallel by using a parallel
  "JobRunner" (and each "params" run may run multiple optimizers in
  parallel).

* Use functions like "ParAMSResults.get_loss()" to get the loss
  function and other results

class ParAMSJob(name='plamsjob', use_relative_paths=False, **kwargs)

   A class for running ParAMS jobs.

   There are some convenient ways of setting up the input file:

   To set training set, validation set, job collection, engine
   collection, and parameter interface, use the special attributes

   * "job_collection", "engine_collection", "parameter_interface", and
     "resume_checkpoint" map directly to the yaml (or checkpoint
     .tar.gz) file. Usage:

        job.job_collection = 'path/to/job_collection.yaml'

     Here the path is automatically converted to an absolute path.

   * "training_set" and "validation_set" : you can assign the path to
     these just as for the previous three:

        job.training_set = 'path/to/training_set.yaml' # can be a relative path

     However, "job.training_set" is actually a PLAMS "Settings"
     object, so you can also assign the path and other settings using

        job.training_set.path = '/path/to/training_set.yaml' # must be an absolute path
        job.training_set.LoggingInterval.General = 10

   Note: If you do not specify paths to the above files they will be
   read with the default names from the current working directory if
   they exist (converted to absolute paths in the input file).

   To add exit conditions, stoppers, or optimizers, you can use the
   "add_exit_condition()", "add_stopper()", and "add_optimizer()"
   functions. They are more convenient to use than accessing the
   settings directly because of the repeated keys in the input file.

   Similarly, you can use "set_optimizer_selector()" and
   "set_generator()" to set the Type (and any additional options)
   easily.

   Access the job settings with "job.settings", e.g. to set the Task
   you can set "job.settings.input.Task = 'Optimization'".

   Example:

      job = ParAMSJob(name="my_job")
      job.settings.input.Task =  'Optimization'
      job.parameter_interface = 'parameter_interface.yaml'
      job.training_set = 'training_set.yaml'
      job.job_collection = 'job_collection.yaml'
      job.add_optimizer("CMAES", {'Sigma0': 0.01, 'PopSize': 4})
      job.add_exit_condition("MaxTotalFunctionCalls", 20)
      job.set_generator("Perturbation", {'StandardDeviation': 0.03})
      job.settings.input.SkipX0 = "No"
      job.settings.input.ParallelLevels.Optimizations = 1

      print(job.get_input())

      job.run()

      loss = job.results.get_loss()
      print(loss)

   You can also create a ParAMSJob directly from a "params.in" file
   (e.g. created by a previous job, by the GUI, or by hand) using
   "from_inputfile()":

      # any relative paths in params.in get converted to absolute paths
      # this will load all settings from the input file
      job = ParAMSJob.from_inputfile("/path/to/params.in")
      print(job.get_input())

   If you have a directory with .yaml files (e.g. the jobname.params
   directory created by the GUI), you can create a ParAMSJob (with
   empty settings other than for the paths to the .yaml files) using
   "from_yaml()":

      # job.settings.input will contains paths to the .yaml files but no other settings
      job = ParAMSJob.from_yaml("/path/to/directory_with_yaml_files")
      print(job.get_input())

   To load a previously finished job (settings, and more importantly
   results), use "load_external()":

      # give the path to the results directory
      # If the original job was run with the GUI, it is called "jobname.results"
      # Otherwise, the directory is likely called "results"
      job = ParAMSJob.load_external("/path/to/jobname.results")
      print(job.results.get_loss())

   __init__(name='plamsjob', use_relative_paths=False, **kwargs)

      See PLAMS SingleJob.__init__()

      name : str
         The name of the job

      use_relative_paths : bool
         If True, paths to .yaml and checkpoint files will be copied
         to the job directory before running, and any absolute paths
         in the settings will be replaced with the corresponding
         relative paths. Note: the paths in the settings must still be
         absolute! They will be replaced when the job is created/run.

   classmethod from_yaml(path=None, name='plamsjob', use_relative_paths: bool = False, **kwargs) -> ParAMSJob

      Initializes a ParAMSJob with the paths to yaml files taken from
      a directory

      path: str
         A directory. Defaults to the current working directory

      name: str
         Job name

   classmethod from_inputfile(path, name='plamsjob', use_relative_paths: bool = False)

      Initializes a ParAMSJob with settings taken from an input file
      (e.g. params.in)

      Paths are replaced with absolute paths to enable the running of
      new jobs

      Note: if e.g. job_collection.yaml exists in the same directory
      as the params.in file, but no JobCollection is specified in the
      params.in, then the path to that job_collection.yaml file will
      be inserted into the new ParAMSJob settings. Similarly for
      training set, validation set, parameter interface, and engine
      collection.

   classmethod load_external(path, name=None, finalize=False)

      Method to load a Python ParAMSJob from a previously run (through
      the GUI or Python) ParAMS job.

      This loads the settings from the
      results/settings_and_initial_data/params.in.

      path : str
         This should be the path to the **results** folder from a
         previous ParAMS run. The folder is called "jobname.results"
         if  the job was run via the GUI, and likely called "results"
         otherwise.

      name : str
         Name of the new job.

   set_generator(generator_type, value=None)

      Sets the Generator. Example: set_generator("Perturbation",
      {'StandardDeviation': 0.03})

   set_optimizer_selector(selector_type, value=None)

      Sets the OptimizerSelector. Example:
      set_optimizer_selector("Cycle")

   set_parameters_kernel(kernel_type, value=None)

      Sets the kernel applied to the parameter values.

   set_lossvalues_kernel(kernel_type, value=None)

      Sets the kernel applied to the parameter values.

   add_exit_condition(key, value)

      Adds an exit condition to the list
      "self.settings.input.ExitCondition" (if the list does not yet
      exist it will be created).

      Example:

         job.add_exit_condition("MaxTotalFunctionCalls", 10)
         # the above will configure 'job' such that:
         # job.settings.input.ExitCondition[-1].Type == 'MaxTotalFunctionCalls'
         # job.settings.input.ExitCondition[-1].MaxTotalFunctionCalls == 10

      The above gives the following ExitCondition in the input file:

         ExitCondition
             Type MaxTotalFunctionCalls
             MaxTotalFunctionCalls 10
         End

      "value" can also be a dictionary:

         job.add_exit_condition("StopsAfterConvergence", {'OptimizersStopped': 10})

      The above gives

         ExitCondition
             Type StopsAfterConvergence
             StopsAfterConvergence
                 OptimizersStopped 10
             End
         End

      key: str
         The type of ExitCondition.

      value: int/float/str or dict
         The value

   add_stopper(key, value)

      Adds a Stopper to the list "self.settings.input.Stopper" (if it
      does not yet exist it will be created).

      For details, see "add_exit_condition()".

   add_optimizer(key: str, value: dict | None = None)

      Adds an optimizer to the list "self.settings.input.Optimizer"
      (if it does not yet exist it will be created).

      Example:

         job.add_optimizer("CMAES", {'Sigma0': 0.01, 'Popsize': 4})

      gives

         Optimizer
             Type CMAES
             CMAES
                 Sigma0 0.01
                 Popsize 4
             End
         End

      in the input file.

      key: str
         The type of Optimizer

      value: dict or Settings
         Settings for the type of optimizer.

   get_input(use_defaults=True, validate=True)

      use_defaults : bool
         Try to locate some yaml files in the current working
         directory if they are not explicitly specified in the
         settings

      validate : bool
         Validate the written input file, calls "validate_input()"

   classmethod validate_input(inp: str)

      inp: str
         The contents of a params.in file

      Will raise an error if something is wrong with the input (e.g.
      misspelled key names).

   load_settings(path)

      Initializes the job.settings from a params.in file

      path : str
         Path to params.in

   prerun()

      If self.use_relative_paths is set, call
      self._copy_to_relative_paths().

      If you override this method and want to maintain the
      use_relative_paths functionality, you need to call
      self._copy_to_relative_paths()

   get_errormsg() -> str

      Returns the contents of the jobname.err file if it exists. If
      the file does not exist an empty string is returned.

      Returns:
         The error message

      Return type:
         str
