8.3.2. Working with Parameter Interfaces
****************************************


8.3.2.1. Parameter Interface Basics
===================================

The following examples showcase the use of the parameter interface to
ReaxFF, however, the methods discussed are available to all other
interfaces as well. The API is defined through the "BaseParameters"
base class.

>>> # The initialization parameters might be different for every interface
>>> # Here, we are reading the parameters from a force field file
>>> interface = ReaxFFParameters('my_ffield.ff')

A parameter interface behaves like a list. It stores instances of the
"Parameter" class:

>>> interface[0]
Parameter(VALUE, "NAME", IS_ACTIVE, RANGE)
>>> len(interface) # The total number of parameters in this instance
42

>>> # We can iterate over all stored parameters:
>>> for parameter in interface:
...   do_something(parameter)

Note:

  Although the underlying data structure of the parameter interface is
  a *list*, *dict*-like lookups are also supported. Instead of
  accessing an entry by its list index, it can also be accessed by the
  parameter's name string:

  >>> interface[121].name               # List-like behaviour, same as interface(121)
  "parameter_name"
  >>> interface["parameter_name"].name  # Dict-like behaviour
  "parameter_name"

  This works because name strings in the same interface are required
  to be unique, but is slower than a genuine dictionary lookup.


8.3.2.2. Working with Parameters
================================

The user can interact with a single "Parameter" by its attributes
"name", "value", "range", "is_active":

>>> interface[0]
Parameter(50, "x_1", True, (10,100))
>>> interface[0].value
50
>>> interface[0].name = 'y_1'
>>> interface[0].name
'y_1'
>>> interface[0].is_active
True
>>> interface[0].range
(10,100)

In addition to single-parameter interactions, the user can interact
with all parameters in an interface at once through the attributes
"names", "x", "range", "is_active":

>>> interface.x           # Returns the `Parameter.value` of every parameter in the interface
>>> [50, 1.23, ..., 321]
>>> interface.names       # Returns the `Parameter.name` of every parameter in the interface
['y_1', 'x_2', ..., 'x_N']
>>> interface.range       # Returns the `Parameter.range` of every parameter in the interface
[(10, 100), ..., (0,1)]
>>> interface.is_active   # Returns the `Parameter.is_active` of every parameter in the interface
[True, False, ..., True]

Note:

  The attributes "range", "is_active" are present in both, "Parameter"
  and "BaseParameters" classes.

Any of the above attributes can be used as a setter, to change or set
multiple parameter values at once:

>>> interface.x
[50, 1.23, ..., 321]
>>> new_x = interface.x
>>> new_x[0] = 1000.
>>> interface.x = new_x
>>> interface.x
[1000., 1.23, ..., 321]
>>>
>>> interface.is_active = len(interface)*[False] # Sets all parameters to is_active=False
>>> not all(interface.is_active)
True

Note:

  The above setters only work when the value they are being set to is
  of the same length as "len(interface)":

  >>> len(interface)
  100
  >>> interface.x = [1,2]
  ValueError: Len of passed values is not the same as size of this interface.


8.3.2.3. The Active Parameters Subset
=====================================

Important:

  During an optimization, only the *active* subset of parameters will
  be optimized.

The dynamic "active" attribute returns a subset of all parameters that
meet the  "Parameter.is_active==True" comparison:

>>> len(interface)
100
>>> interface.is_active.count(True) # Number of is_active parameters
50
>>> len(interface.active)
50
>>> interface.is_active = len(interface)*[False]
>>> len(interface.active)
0

This subset of a parameter interface behaves just like the main
interface:

>>> for active_param in interface.active:
...   do_something(active_param)
>>> interface.active.x     = new_x
>>> interface.active.range = new_ranges
>>> all(interface.active.is_active) # Obviously
True


8.3.2.4. Storage
================

Each parameter interface has a "write()" method which will write the
currently stored parameter values to disk in a format native to the
associated engine and readable by AMS. For more information check out
the respective parameter interface documentation.


8.3.2.4.1. Lossless Storage
---------------------------

In most cases, storing an interface with the "write()" method will not
preserve any information that might be relevant for future
parameterizations. In such cases, the "yaml_store()" and "yaml_load()"
methods can be used to store and load the complete instance in a text
format. This preserves all additional parameter attributes such as
"range" or "is_active":

>>> ljp1 = LennardJonesParameters()
>>> ljp1[0].value = 42
>>> ljp1[0].range = (0, 43)
>>> ljp1.yaml_store('ljp1.yaml')
>>> ljp2 = LennardJonesParameters.yaml_load('ljp1.yaml')
>>> ljp1 == ljp2
True

Note:

  The DFTBSplineRepulsivePotentialParameters can only be stored/loaded
  in binary ("pickle_dump()") format.


8.3.2.5. Relation to PLAMS Settings
===================================

Each parameter interface can be represented as a PLAMS Settings
instance. This happens through the "get_engine()" method, which
returns an intermediary Engine instance:

>>> ljp    = LennardJonesParameters()
>>> engine = ljp.get_engine()
>>> print(engine)
AMSInput: |
   Engine lennardjones
     eps 0.0003
     rmin 3.0
   EndEngine

Engine instances are intended to be stored in engine collections
whenever data needs to be reproduced or shared. The PLAMS Settings
instance associated with each Engine instance can be accessed through
the "Engine.settings" attribute:

>>> print(engine.settings)
input:
      lennardjones:
                   eps:         0.0003
                   rmin:        3.0


8.3.2.6. Parameter API
======================

class Parameter(value=None, name=None, is_active=True, range=[None, None], _id=None, _metadata=None)

   A helper class representing a single parameter in the
   "BaseParameters".

   Note:

     This class is not public in this module, as it is not possible to
     create these entries on their own. They can only be managed by an
     instance of a "BaseParameters" class.

   Attributes:
   name : str
      Parameter name

   value : float
      Parameter value

   range : Tuple[float, float]
      Upper and lower ranges for the parameter value. Anything outside
      of that range will not be considered by the optimizer.

      Note:

        Providing a range with the same lower and upper values does
        **not** automatically set the parameter to "is_active=False".

   atoms : List[str]
      If the parameter is atom-specific, will store a list of all
      atoms involved in this attribute, empty list otherwise.

   is_active : bool
      The optimizer will only optimize parameters set to
      "is_active==True". Within a "BaseParameters" instance, a
      "BaseParameters.active" subset is generated based on this
      attribute.


8.3.2.7. Interface Base Class API
=================================

Base class from which all engine-specific parameterizations should
derive:

class BaseParameters(names=None, active=None, values=None, ranges=None, parameters=None, _set_ids=True)

   A class representing the interface to an arbitrary AMS engine.
   Classes representing specific engines (e.g. ReaxFF) can derive from
   this abstract base class.

   Will generate entries of a "Parameter" helper class type. Each
   entry holds the following attributes: "value", "range", "name",
   "atoms" and "is_active".

   Note:

     The attributes "range" and "is_active" are available to both, the
     "Parameter" and "BaseParameters" classes.

     >>> # Init BaseParameters()-derived class
     >>> type(self[0])
     <class 'scm.params.parameter_interfaces.base.Parameter'>
     >>> self[0].range       # The attribute of a single entry, returns a single tuple
     (x,y)
     >>> self[0].is_active   # The attribute of a single entry, returns a single bool
     True
     >>> self.range          # Attributes of all entries in this class, return a list of tuples
     [(x1,y1), ..., (xn,yn)]
     >>> self.is_active      # Attributes of all entries in this class., return a list of bools
     [True, False ..., True]

   __init__(names=None, active=None, values=None, ranges=None, parameters=None, _set_ids=True)

      Base class constructor to be called from the derived classes.
      Performs a few basic checks.

      Parameters:

      names : list or tuple
         Data structure holding the **unique** names of each
         parameter.

      active : list or tuple of bools
         Data structure specifying, whether to optimize this parameter
         (True), or not.

      values : list or tuple of floats
         Data structure with the initial parameter values.

      ranges : list or tuple of (float,float) or [float,float]
         Data structure specifying the lower and upper bounds for each
         parameter.

      parameters: optional, List of Parameter instances
         Alternative constructor from a list of already initialized
         Parameter instances. Takes precendence if provided.

      _set_ids: bool
         Setting of each parameter's *_id* is disabled when generating
         the *active* subset.

   x

      Returns:
         A list of the current parameter values.

      *Note*: Batch-set all "Parameter" values with "self.x =
      List[float]".  Works only, if "len(List[float]) == len(self)".

   names

      Returns:
         A list of parameter names.

   is_active

      Returns:
         A list of bools, marking the parameter active (True) or
         inactive for an optimization.

      *Note*: Batch-set all "Parameter.is_active" values with
      "self.is_active = List[bool]".  Works only, if "len(List[bool])
      == len(self)".

   range

      Returns:
         A list of parameter ranges.

      *Note*: Batch-set all "Parameter" ranges with "self.is_active =
      List[Tuple(float,flaot)]".  Works only, if
      "len(List[Tuple(float,flaot)]) == len(self)"

   active

      Important:

        This subset contains all parameters that will be optimized.

      Given the initial set of all parameters in this class, generates
      and stores a subset in this attribute, which only contains the
      parameters marked as "is_active == True".

      The inherits the same properties and methods as its parent (with
      the exclusion of "write()" and "get_engine()"), *e.g.*:

      >>> self.range          # Returns the range  of ALL parameters in this class
      >>> self.active.range   # Returns the range  of the active parameters only
      >>> self.active.x       # Returns the values of the active parameters only
      >>> self.active[0]
      >>> len(self.active)

      Setting a "Parameter.is_active" attribute in the parent will
      include / exclude it from the "active" subset:

      >>> len(self)
      100
      >>> len(self.active)
      50
      >>> self('ParameterX').is_active
      False
      >>> self('ParameterX').is_active = True
      >>> len(self.active)
      51

   abstract get_engine(parameters=None, path=None, write=True, *args, **kwargs)

      Given a set of *parameters* returns a ready-to-run "Engine".

      Note:

        This abstract method has to be overwritten by a derived class.
        The parameter length check defined here should be propagated
        with "super().get_engine()".

      Note:

        *Parameters* can be *None*, in which case an instance holding
        the current paramers will be returned. "len(params)" should be
        equal to "len(self)" or "len(self.active)".

      Important:

        Children that inherit from this class should lock this method
        to make it thread-safe:

           with self._Lock: # This variable is generated by the base class
               ...

      Returns:
         an "Engine" instance for the given *parameters*.

   abstract write(path, parameters=None)

      Abstract method. Child class must have a "write()" method that
      stores the engine to disk. If *parameters* is provided, this
      method should call *super().get_engine(parameters)* prior to
      writing. before writing them to disk.

      Important:

        Children that inherit from this class should lock this method
        to make it thread-safe:

           with self._Lock: # This variable is generated by the base class
               ...

   yaml_store(yamlfile)

      Stores the entire parameter interface in a (compressed) human-
      readable *yamlfile*.

   classmethod yaml_load(yamlfile, force=False)

      Loads a parameter interface from a (compressed) human-readable
      *yamlfile*. Child classes should call super() in this method
      before implementing their own routines.

   __len__()

      Returns:
         Number of parameters.

   __iter__()

      Iterates over Parameter entries .

      Each entry has the attributes: "value", "range", "name" and
      "is_active".

   __getitem__(i)

      If *i* is a string, get the Parameter with name *i*,  if *i* is
      an int, get the Parameter at index *i*.

   __call__(i)

      Same as "__getitem__()"

   __eq__(other)

      Checks if two interfaces are the same

   index(i)

      Returns the index of parameter *i* in the interface, such that
      "i == self[self.index(i)]".
