AKFReader
*********

The "AKFReader" Python class provides a convenient way to read
annotated data from the binary KF files generated by AMS.

Below is an example demonstrating some of the "AKFReader" class's
functionality.

   #!/usr/bin/env amspython

   from scm.akfreader import AKFReader

   # Load a kf file.
   akf = AKFReader("test.results/dftb.rkf")


   # Print a summary of the content in the kf file:
   print(akf.table_of_content())

   # Also print comments and extra info for all variables
   print(akf.table_of_content(verbose=True))


   # Read a specific variable from file.
   coordinates = akf.read("Molecule%Coords")
   print("Coordinates:")
   print(coordinates)

   # Coordinates:
   # [[-1.65367945e+00  4.59827802e+00 -9.59515212e-09]
   #  ...
   #  [-2.21256221e+00  5.05007997e+00 -1.93285786e+00]]


   # Retrieve the description, units, and additional information for a variable:
   description_dict = akf.description("Molecule%Coords")
   print("'Molecule%Coords' description:")
   print(description_dict)

   # 'Molecule%Coords' description:
   # {'_type': 'float_array',
   #  '_shape': [3, 'nAtoms'],
   #  '_comment': 'Coordinates of the nuclei (x,y,z)',
   #  '_unit': 'bohr'}


   # Read another variable, and do a unit conversion:
   energy_in_eV = akf.read("AMSResults%Energy", units="eV")
   print(f"Energy: {energy_in_eV} [eV]")


   # Read all variables matching the pattern:
   history_energies = akf.read("History%Energy(#)")
   print(history_energies)

   # [('History%Energy(1)', -5.758015498298242),
   #  ('History%Energy(2)', -5.765039556660788),
   #  ('History%Energy(3)', -5.766261662994689),
   #  ('History%Energy(4)', -5.766288141072665)]

Note:

  The KFFile Python class can also be used to read KF files. Key
  differences between the "AKFReader" and the "KFFile":

  * The "AKFReader" can provide extra information about the content of
    the KF files: it can give descriptions of sections, variables,
    units, and automatically reshapes matrices to the proper
    size/shape.

  * The "KFFile" can also be used to write KF files (while the
    "AKFReader" can only read).

See also:

  A command-line utility "$AMSBIN/akf" (described here) offers an
  alternative way of using the "AKFReader" tool.

See also:

  The list of possible variables for the various kf files can be found
  in the respective user manuals: ams.rkf, adf.rkf, band.rkf, dftb.rkf

**API documentation:**

class AKFReader(path: str, kf_def_path=None, strict=False, verbose=False)

   A python class for reading AMS binary KF files.

   Parameters:
      * path (str): the path of the KF file (.rkf) to open. Can be
        wither relative or absolute path. (the other optional
        constructor parameters are expert options and are best left
        untouched)

   Example:

      from scm.akfreader import AKFReader

      akf = AKFReader("test.results/dftb.rkf") # <- path to ams .rkf result file

      print(akf.table_of_content())

      coordinates = akf.read("Molecule%Coords")
      energy = akf.read("AMSResults%Energy", units="eV")

   read(name: str, units=None)

      Read and return the value of a variable on file.

      Parameters:
         * name (str): The name of the variable to read. This should
           be of the form "SectionName%VariableName", so for example
           akf.read("Molecule%Coords") means "read the variable
           'Coords' from Section 'Molecule')

         * units (str): the desired output units. See the :Units:
           class for more info on avaiable units.

      Returns:
         * The content of the variable. The return type depends on the
           variable you are reading. So, if the variable is an
           integer, and int will be return. If it's an array, a numpy
           array will be returned.

      Example:

         >>> energy = akf.read("AMSResults%Energy")
         >>> energy_in_eV = akf.read("AMSResults%Energy", units="eV")

      Note: in case the variable is a "pattern" (i.e. the variable
      name contains "#" or "*" characters, the result will be a list
      of tuples of all variables matching the pattern. E.g.: 'foo(#)'
      will return [('foo(1)', value1), ('foo(2)', value2)]

   table_of_content(verbose=False) -> str

      Returns the table of content of the KF file (i.e. the list of
      all sections and variables on file) in text format.

      Parameters:
         * verbose (bool): if verbose is True, the full description of
           all variables will be included

      Example:

         >>> print(akf.table_of_content())

         Content for KF file: /test/dftb.rkf

         == AMSResults ==

             - AMSResults%Energy
             - AMSResults%Gradients
             - AMSResults%DipoleMoment
             ...

         >>> print(akf.table_of_content(verbose=True))

         Content for KF file: /test/dftb.rkf

         == AMSResults ==

             comment   : Generic results of the DFTB evaluation.
             type      : section

             - AMSResults%Energy
                 comment   : The energy computed by the engine.
                 type      : float
                 unit      : hartree

             - AMSResults%Gradients
                 comment   : The nuclear gradients.
                 shape     : [3, 'Molecule%nAtoms']
                 type      : float_array
                 unit      : hartree/bohr

             - AMSResults%DipoleMoment
                 comment   : Dipole moment vector (x,y,z)
                 shape     : [3]
                 type      : float_array
                 unit      : e*bohr
             ...

   variables(include_hidden=False) -> List[str]

      Returns the list of all variables on the KF file (in the form
      "section%variable").

      Example:

         >>> all_variables = akf.variables()
         >>> print(all_variables)

         ['AMSResults%Charges',
         'AMSResults%DipoleMoment',
         ...
         ...
         'Orbitals%Energies(#)'
         'Orbitals%Occupations(#)']

   sections() -> List[str]

      Returns the list of all sections on the KF file.

      Example:

         >>> print(akf.sections())

         ['General', 'AMSResults', 'Molecule', 'Orbitals', ...]

   variables_in_section(section: str) -> List[str]

      Returns the list of all variables in the specified section.

      Example:

         >>> print(akf.variables_in_section("Molecule"))

         ['Molecule%AtomMasses', 'Molecule%Coords', 'Molecule%AtomicNumbers', ...]

         >>> for v in akf.variables_in_section("Molecule"):
         >>>     print(f"{v}: {akf.read(v)}")

   __contains__(name: str)

      Whether or not 'name' is present in the KF file. This can either
      be a section (e.g. "Molecule") or a variable (e.g.
      "Molecule%Coords") Example:

         if "Molecule%Coords" in akf:
             coords = akf.read("Molecule%Coords")
         else:
             print("Variable 'Molecule%Coords' not present of file")
             print("Is the 'Molecule' section present? ", "Molecule" in akf)

   description(name: str) -> Dict[str, Any]

      Return the description (meta-data) of the variable as a
      dictionary.

      Parameters:
         * name (str): The name of the variable (this should be in the
           form "Section%Variable")

      Returns:
         * dictionary containing the available meta-data for the
           entry. Note that not meta-data entries are defined for all
           variables. For example, if a variable does not have a unit,
           the "_unit" key will not be present in the resulting
           dictionary

      Example:

         >>> d = akf.description("Molecule%Coords")
         >>> print(type(d))

         <class 'dict'>

         >>> print(d)
         {'_type': 'float_array',
          '_shape': [3, 'nAtoms'],
          '_comment': 'Coordinates of the nuclei (x,y,z)',
          '_unit': 'bohr'}

   str_description(name: str) -> str

      Return the description of the variable as a string.

      Example:

         >>> print(akf.str_description("Molecule%Coords"))

         comment   : Coordinates of the nuclei (x,y,z)
         shape     : [3, 'nAtoms']
         type      : float_array
         unit      : bohr

   units(name: str) -> str | None

      Return the unit of measurement of the variable if specified,
      otherwise returns None.
