2. Examples
***********


2.1. Basic Example
==================

The following basic example ("examples.ipynb") shows the general flow
of constructing input. You select the appropriate driver and (usually)
an engine. Key values are directly assigned to the attribute with the
same name as the corresponding block. The **get_input_string** method
produces a formatted text block representing the text input.

   from scm.input_classes import drivers, engines

   driver = drivers.AMS()
   driver.Task = "GeometryOptimization"
   driver.Properties.NormalModes = True
   driver.Engine = engines.DFTB()
   driver.Engine.Model = "SCC-DFTB"

   print(driver.get_input_string())

   Properties
     NormalModes True
   End
   Task GeometryOptimization

   Engine DFTB
     Model SCC-DFTB
   EndEngine


2.2. Finding out which keys and blocks are available
====================================================

Besides looking at the documentation pages on the input for the AMS
driver here, every "Block" object has a ".blocks" and a ".keys"
attribute, which contain all the available blocks and keys for that
block. Note that for ".blocks", it will display the class name first,
which is equal to the attribute name save the leading underscores (to
prevent name clashes). For ".keys", the class name is one of the 7
basic key types:

   print('-------Block-------')
   display(drivers.AMS().blocks)
   print('-------Keys-------')
   display(drivers.AMS().keys)

   -------Block-------

   (_BondOrders(name='BondOrders', comment="Configures details regarding the calculation/guessing of bond orders. To request the calculation of bond orders, use the 'Properties%BondOrders' key."),
    _Constraints(name='Constraints', comment='The Constraints block allows geometry optimizations and potential energy surface scans with constraints. The constraints do not have to be satisfied at the start of the calculation.', extra_info='not_in_fragment'),
    _ElasticTensor(name='ElasticTensor', comment='Options for numerical evaluation of the elastic tensor.'),
    _Engine(name='Engine', comment='The input for the computational engine. The header of the block determines the type of the engine.', header=True),
    _EngineAddons(name='EngineAddons', comment='This block configures all the engine add-ons.'),
    _EngineDebugging(name='EngineDebugging', comment='This block contains some options useful for debugging the computational engines.'),
    _ExitCondition(name='ExitCondition', comment='If any of the specified exitconditions are met, the AMS driver will exit cleanly.', unique=False),
    _GCMC(name='GCMC', comment='This block controls the Grand Canonical Monte Carlo (GCMC) task. nn By default, molecules are added at random positions in the simulation box. The initial position is controlled by '),
    _GeometryOptimization(name='GeometryOptimization', comment='Configures details of the geometry optimization and transition state searches.'),
    _IEEEExceptions(name='IEEEExceptions', comment='Whether this works or not depends on the way the program is compiled (compiler and settings). Only useful for developers debugging some numerical issue.', hidden=True),
    _IRC(name='IRC', comment='Configures details of the Intrinsic Reaction Coordinate optimization.'),
    _Idle(name='Idle', comment='Configures details of the idle task, which does nothing for a specified amount of time.', hidden=True),
    _LoadSystem(name='LoadSystem', comment='Block that controls reading the chemical system from a KF file instead of the [System] block.', unique=False, header=True, extra_info='not_in_fragment', shared_id='AMSLoadSystem'),
    _Log(name='Log', comment="Configures the debugging loggers. Syntax: 'Level LoggerName'. Possible Levels: All, Debug, Info, Warning, Error, Fatal."),
    _MolecularDynamics(name='MolecularDynamics', comment='Configures molecular dynamics (with the velocity-Verlet algorithm) with and without thermostats. This block allows to specify the details of the molecular dynamics calculation.'),
    _Molecules(name='Molecules', comment='Configures details of the molecular composition analysis enabled by the Properties%Molecules block.'),
    _NEB(name='NEB', comment='Configures details of the Nudged Elastic Band optimization.'),
    _NormalModes(name='NormalModes', comment='Configures details of a normal modes calculation.'),
    _NumericalDifferentiation(name='NumericalDifferentiation', comment='Define options for numerical differentiations, that is the numerical calculation of gradients, Hessian and the stress tensor for periodic systems.'),
    _NumericalPhonons(name='NumericalPhonons', comment='Configures details of a numerical phonons calculation.'),
    _PESExploration(name='PESExploration', comment='Configures details of the automated PES exploration methods.'),
    _PESPointCharacter(name='PESPointCharacter', comment='Options for the characterization of PES points.'),
    _PESScan(name='PESScan', comment='Configures the details of the potential energy surface scanning task.'),
    _Print(name='Print', comment='This block controls the printing of additional information to stdout.'),
    _Properties(name='Properties', comment='Configures which AMS level properties to calculate for SinglePoint calculations or other important geometries (e.g. at the end of an optimization).'),
    _Raman(name='Raman', comment='Configures details of the Raman or VROA calculation.'),
    _Replay(name='Replay', comment='Configures the details of the Replay task.'),
    _Restraints(name='Restraints', comment="The Restraints block allows to add soft constraints to the system. A restraint is a potential energy function (a spring) attached to a certain coordinate, for example, an interatomic distance, with its minimum at the specified optimal value. A restraint is defined using one or two parameters: the ForceConstant and, for some types, the F(Inf) value. The ForceConstant parameter corresponds to second derivative of the restraint potential energy d2V(x)/dx^2 for any x (harmonic restraints) or only at at x=0 (other restraints). Here, x is a deviation from the restraint's optimal value."),
    _RigidMotions(name='RigidMotions', comment='Specify which rigid motions of the total system are allowed. An external field is not considered part of the system. Normally the automatic option is doing what you want. However this feature can be used as a means of geometry constraint.'),
    _SCMMatrix(name='SCMMatrix', comment='Technical settings for programs using the AMT matrix system. Currently this is only used by DFTB'),
    _SteepestDescent(name='SteepestDescent', comment='Configures details of the steepest descent task.', hidden=True),
    _Symmetry(name='Symmetry', comment='Specifying details about the details of symmetry detection and usage.', shared_id='AMSSymmetry'),
    _System(name='System', comment='Specification of the chemical system. For some applications more than one system may be present in the input. In this case, all systems except one must have a non-empty string ID specified after the System keyword. The system without an ID is considered the main one.', unique=False, header=True, gui_type='Repeat at least once', shared_id='AMSSystem'),
    _Thermo(name='Thermo', comment='Options for thermodynamic properties (assuming an ideal gas). The properties are computed for all specified temperatures.'),
    _TransitionStateSearch(name='TransitionStateSearch', comment='Configures some details of the transition state search.'),
    _VibrationalAnalysis(name='VibrationalAnalysis', comment='Input data for all vibrational analysis utilities in the AMS driver.'))

   -------Keys-------

   (StringKey(name='EngineRestart', comment='The path to the file from which to restart the engine.nnShould be a proper engine result file (like adf.rkf, band.rkf etc), or the name of the results directory containing it.', ispath=True),
    BoolKey(name='FallbackSolveAfterEngineFailure', comment='If the engine fails to Solve, try to re-run the Solve without restarting the engine from the previous results. This generally decreases the engine falure rate. Only relevant certain tasks, such as GeometryOptimization, MolecularDynamics, Replay, IRC.', default=True),
    StringKey(name='LoadEngine', comment='The path to the file from which to load the engine configuration. Replaces the Engine block.', ispath=True),
    IntListKey(name='RNGSeed', comment='Initial seed for the (pseudo)random number generator. This should be omitted in most calculations to avoid introducing bias into the results. If this is unset, the generator will be seeded randomly from external sources of entropy. If you want to exactly reproduce an older calculation, set this to the numbers printed in its output.'),
    MultipleChoiceKey(name='Task', comment='Specify the computational task to perform:nn• Single Point: keep geometry as isn• Geometry Optimization: optimize the geometryn• Transition State: search for the transition staten• IRC: intrinsic reaction coordinaten• PES Scan: scan the potential energy surfacen• NEB: Nudged elastic band for reaction path optimizationn• Vibrational Analysis: perform one of the analysis types selected on the options pagen• Molecular Dynamics: perform MD simulationn• GCMC: Grand Canonical Monte Carlo simulationn• PES Exploration: automated potential energy surface explorationn• Replay: recompute frames from the trajectory of a previously run job', choices=['GCMC', 'GeometryOptimization', 'Idle', 'IRC', 'MolecularDynamics', 'NEB', 'PESExploration', 'PESScan', 'Pipe', 'Replay', 'SinglePoint', 'SteepestDescent', 'TestEngine', 'TestSymmetry', 'TransitionStateSearch', 'VibrationalAnalysis'], hiddenchoices=['Idle', 'SteepestDescent', 'TestEngine', 'TestSymmetry', 'Pipe']),
    MultipleChoiceKey(name='Test', comment='various technical tests that will be done for the system', hidden=True, default='Symmetry', choices=['Symmetry', 'Bonds', 'SecondOrderBonds', 'AtomTyping', 'SystemMatching']),
    BoolKey(name='UseSymmetry', comment="Whether to use the system's symmetry in AMS. Symmetry is recognized within a tolerance as given in the Symmetry key.", default=True))

The ".comment" attribute provides a description of the entry

   drivers.AMS().Engine.comment

   'The input for the computational engine. The header of the block determines the type of the engine.'


2.3. Default values
===================

Some keys have a default value defined. You can always access the
".default" attribute of any key. When no default is defined, this
attribute will be "None". Upon initialization of the "Key" object, its
value (".val" attribute) will be set to equal to its ".default"
attribute. You can always restore the default value with the
".set_to_default" method.

   adf = engines.ADF()
   print(f"{adf.Basis.Type.default=}")
   print(f"{adf.Basis.Type.val=}")
   adf.Basis.Type = 'SZ'
   print('Value changed to SZ')
   print(f"{adf.Basis.Type.default=}")
   print(f"{adf.Basis.Type.val=}")
   adf.Basis.Type.set_to_default()
   print('Value reset to default value')
   print(f"{adf.Basis.Type.default=}")
   print(f"{adf.Basis.Type.val=}")

   adf.Basis.Type.default='DZ'
   adf.Basis.Type.val='DZ'
   Value changed to SZ
   adf.Basis.Type.default='DZ'
   adf.Basis.Type.val='SZ'
   Value reset to default value
   adf.Basis.Type.default='DZ'
   adf.Basis.Type.val='DZ'

Note that whether the key is set to the default value or not has no
impact on whether it will appear in the text input. Every key that is
explicitly set in your input script will be present in the text input:

   ams = drivers.AMS()
   ams.Engine = engines.ADF()
   # set type to the default value
   ams.Engine.Basis.Type = 'DZ'
   assert ams.Engine.Basis.value_changed
   print(ams.get_input_string())

   Engine ADF
     Basis
       Type DZ
     End
   EndEngine


2.4. Runtime Checks
===================

Because of the extensive use of type hints, most mistakes in the
syntax can be caught using a type checker like pylance or mypy. PISA
will also perform a series or runtime checks, for mistakes that are
not possible to catch with a type checker and for users that are not
using type checking.


2.4.1. Attribute spelling errors
--------------------------------

If you mistype the name of an attribute of any "Block", an exception
will be raised that provides with existing attribute names that are
similar to the attribute you typed:

   typo_driver = drivers.AMS()
   typo_driver.Tassk = "GeometryOptimization"

   ---------------------------------------------------------------------------

   AttributeError                            Traceback (most recent call last)

   Cell In[6], line 2
         1 typo_driver = drivers.AMS()
   ----> 2 typo_driver.Tassk = "GeometryOptimization"


   File ~/src/svn_amshome/scripting/scm/pisa/block.py:372, in Block.__setattr__(self, _Block__name, _Block__value)
       370     raise Exception(f"Attribute '{__name}' not found, did you mean '{proper_case_name}'?")
       371 if not hasattr(self, __name):
   --> 372     super().__setattr__(__name, __value)
       373     return
       374 else:


   File ~/src/svn_amshome/scripting/scm/pisa/entry.py:124, in Entry.__setattr__(self, _Entry__name, _Entry__value)
       122     elif len(close_matches) > 1:
       123         msg += f" Did you possibly mean to access any of [{', '.join(close_matches)}]?"
   --> 124     raise AttributeError(msg)
       125 return super().__setattr__(__name, __value)


   AttributeError: Object of type <class 'scm.input_classes.drivers.ams.AMS'> has no attribute called 'Tassk'. Did you possibly mean to access any of [Task, Test, ElasticTensor]?


2.4.2. Invalid choice for a multiple-choice key
-----------------------------------------------

When setting a multiple-choice key to a value that is not a valid
choice, an exception will be raised that includes the list of valid
choices:

   adf = engines.ADF()
   adf.Basis.Type = 'TZQP'

   ---------------------------------------------------------------------------

   ValueError                                Traceback (most recent call last)

   File ~/src/svn_amshome/scripting/scm/pisa/key.py:257, in MultipleChoiceKey._convert_val(self, val)
       256 try:
   --> 257     idx = lower_choices.index(val.lower())
       258     return total_choices[idx]


   ValueError: 'tzqp' is not in list


   During handling of the above exception, another exception occurred:


   ValueError                                Traceback (most recent call last)

   Cell In[7], line 2
         1 adf = engines.ADF()
   ----> 2 adf.Basis.Type = 'TZQP'


   File ~/src/svn_amshome/scripting/scm/pisa/block.py:404, in Block.__setattr__(self, _Block__name, _Block__value)
       402 # allow syntax like AMS().Task = 'SinglePoint'
       403 elif isinstance(attr, Key) and not isinstance(__value, Key):
   --> 404     attr.val = __value
       405 else:
       406     super().__setattr__(__name, __value)


   File ~/src/svn_amshome/scripting/scm/pisa/entry.py:125, in Entry.__setattr__(self, _Entry__name, _Entry__value)
       123         msg += f" Did you possibly mean to access any of [{', '.join(close_matches)}]?"
       124     raise AttributeError(msg)
   --> 125 return super().__setattr__(__name, __value)


   File ~/src/svn_amshome/scripting/scm/pisa/key.py:58, in Key.val(self, value)
        55 @val.setter
        56 def val(self, value: Any) -> None:
        57     self._value_changed = True
   ---> 58     self._val = self._convert_val(value)


   File ~/src/svn_amshome/scripting/scm/pisa/key.py:261, in MultipleChoiceKey._convert_val(self, val)
       259 except ValueError:
       260     choice_list = "\n".join(total_choices)
   --> 261     raise ValueError(
       262         f"Can not set value to {val}, not present in choices: {choice_list}. \n"
       263         f"Did you possibly mean any of {difflib.get_close_matches(val, total_choices)}?"
       264     )


   ValueError: Can not set value to TZQP, not present in choices: SZ
   DZ
   DZP
   TZP
   TZ2P
   QZ4P
   TZ2P+
   TZ2P-J
   QZ4P-J
   mTZ2P
   ZORA/SZ
   ZORA/DZ
   ZORA/DZP
   ZORA/TZP
   ZORA/TZ2P
   ZORA/QZ4P
   ZORA/TZ2P+
   ZORA/TZ2P-J
   ZORA/QZ4P-J
   ZORA/mTZ2P
   ZORA/jcpl
   AUG/ASZ
   AUG/ADZ
   AUG/ADZP
   AUG/ATZP
   AUG/ATZ2P
   ET/ET-pVQZ
   ET/ET-QZ+5P
   ET/ET-QZ3P
   ET/ET-QZ3P-1DIFFUSE
   ET/ET-QZ3P-2DIFFUSE
   ET/ET-QZ3P-3DIFFUSE
   Corr/TZ3P
   Corr/QZ6P
   Corr/ATZ3P
   Corr/AQZ6P
   Corr/dATZ3P
   Corr/dAQZ6P
   POLTDDFT/DZ
   POLTDDFT/DZP
   POLTDDFT/TZP
   TZ2P+
   ZORA/SZ
   ZORA/DZ
   ZORA/DZP
   ZORA/TZP
   ZORA/TZ2P
   ZORA/QZ4P
   ZORA/TZ2P+
   ZORA/TZ2P-J
   ZORA/QZ4P-J
   ZORA/mTZ2P
   ZORA/jcpl
   ET/ET-QZ+5P
   Corr/dATZ3P
   Corr/dAQZ6P.
   Did you possibly mean any of ['TZP', 'TZ2P', 'mTZ2P']?


2.4.3. Invalid type for a key value
-----------------------------------

When providing a key with a value that is of the wrong type or can not
reasonably be coerced to the correct type, an exception will be
raised:

   adf = engines.ADF()
   adf.A1Fit = 'Wrong'

   ---------------------------------------------------------------------------

   ValueError                                Traceback (most recent call last)

   Cell In[8], line 2
         1 adf = engines.ADF()
   ----> 2 adf.A1Fit = 'Wrong'


   File ~/src/svn_amshome/scripting/scm/pisa/block.py:404, in Block.__setattr__(self, _Block__name, _Block__value)
       402 # allow syntax like AMS().Task = 'SinglePoint'
       403 elif isinstance(attr, Key) and not isinstance(__value, Key):
   --> 404     attr.val = __value
       405 else:
       406     super().__setattr__(__name, __value)


   File ~/src/svn_amshome/scripting/scm/pisa/entry.py:125, in Entry.__setattr__(self, _Entry__name, _Entry__value)
       123         msg += f" Did you possibly mean to access any of [{', '.join(close_matches)}]?"
       124     raise AttributeError(msg)
   --> 125 return super().__setattr__(__name, __value)


   File ~/src/svn_amshome/scripting/scm/pisa/key.py:58, in Key.val(self, value)
        55 @val.setter
        56 def val(self, value: Any) -> None:
        57     self._value_changed = True
   ---> 58     self._val = self._convert_val(value)


   File ~/src/svn_amshome/scripting/scm/pisa/key.py:135, in FloatKey._convert_val(self, val)
       125 def _convert_val(self, val: float) -> float:
       126     """Will accept anything that can be converted to a python float, but raises errors on values of
       127     type :class:`bool`, since the type hinter considers them a valid subclass of :class:`float`, but passing
       128     booleans as floats is never intended
      (...)
       133     :rtype: float
       134     """
   --> 135     return float(self._check_for_boolean(val))


   ValueError: could not convert string to float: 'Wrong'

Note that floating point values with a fractional component cannot be
set to integer keys:

   adf.VectorLength = 1
   adf.VectorLength = 2.0  # valid
   adf.VectorLength = 3.5  # invalid

   ---------------------------------------------------------------------------
   ValueError                                 Traceback (most recent call last)
   Cell In [8], line 3
         1 adf.VectorLength = 1
         2 adf.VectorLength = 2.0  # valid
   ----> 3 adf.VectorLength = 3.5

   File ~/src/svn_amshome/scripting/scm/pisa/block.py:388, in Block.__setattr__(self, _Block__name, _Block__value)
       386 # allow syntax like AMS().Task = 'SinglePoint'
       387 elif isinstance(attr, Key) and not isinstance(__value, Key):
   --> 388     attr.val = __value
       389 else:
       390     super().__setattr__(__name, __value)

   File ~/src/svn_amshome/scripting/scm/pisa/entry.py:125, in Entry.__setattr__(self, _Entry__name, _Entry__value)
       123         msg += f" Did you possibly mean to access any of [{', '.join(close_matches)}]?"
       124     raise AttributeError(msg)
   --> 125 return super().__setattr__(__name, __value)

   File ~/src/svn_amshome/scripting/scm/pisa/key.py:57, in Key.val(self, value)
        54 @val.setter
        55 def val(self, value: Any) -> None:
        56     self._value_changed = True
   ---> 57     self._val = self._convert_val(value)

   File ~/src/svn_amshome/scripting/scm/pisa/key.py:191, in IntKey._convert_val(self, val)
       183 def _convert_val(self, val: int) -> int:
       184     """Accepts anything that can be converted to an integer, except boolean values and floats with a fractional component.
       185
       186     :param val: value
      (...)
       189     :rtype: int
       190     """
   --> 191     return int(self._check_for_float(self._check_for_boolean(val)))

   File ~/src/svn_amshome/scripting/scm/pisa/key.py:123, in Key._check_for_float(self, val)
       121 def _check_for_float(self, val: T) -> T:
       122     if (isinstance(val, float) and not val.is_integer()) or (isinstance(val, str) and not float(val).is_integer()):
   --> 123         raise ValueError(f"Will not accept floating point value {val} for {self.__class__.__name__} {self}")
       124     return val

   ValueError: Will not accept floating point value 3.5 for IntKey <Key: 'VectorLength 2'>


2.4.4. Boolean key values
-------------------------

Boolean values represent a bit of a problem in Python. For historical
reasons, "bool" is a direct subclass of "int". This makes it
troublesome to provide type hints for "IntKey" and "FloatKey"
attributes that do not accept booleans. Hence there is an explicit
check for these keys to prevent accidentally passing booleans as
numbers:

   from scm.pisa.key import FloatKey, IntKey

   # instances of bool are also instances of int
   print(f"{isinstance(True, int)=}")
   # Coercing boolean values to floats or int will succeed
   print(f"{int(True)=}")
   print(f"{float(True)=}")
   # To prevent users mistaking a FloatKey for a BoolKey, an explicit runtime check if performed
   adf = engines.ADF()
   adf.VectorLength = 1     # this is allowed
   adf.VectorLength = True  # this is not allowed, even though it would evaluate to the same integer

   isinstance(True, int)=True
   int(True)=1
   float(True)=1.0

   ---------------------------------------------------------------------------

   TypeError                                 Traceback (most recent call last)

   Cell In[9], line 11
         9 adf = engines.ADF()
        10 adf.VectorLength = 1     # this is allowed
   ---> 11 adf.VectorLength = True  # this is not allowed, even though it would evaluate to the same integer


   File ~/src/svn_amshome/scripting/scm/pisa/block.py:404, in Block.__setattr__(self, _Block__name, _Block__value)
       402 # allow syntax like AMS().Task = 'SinglePoint'
       403 elif isinstance(attr, Key) and not isinstance(__value, Key):
   --> 404     attr.val = __value
       405 else:
       406     super().__setattr__(__name, __value)


   File ~/src/svn_amshome/scripting/scm/pisa/entry.py:125, in Entry.__setattr__(self, _Entry__name, _Entry__value)
       123         msg += f" Did you possibly mean to access any of [{', '.join(close_matches)}]?"
       124     raise AttributeError(msg)
   --> 125 return super().__setattr__(__name, __value)


   File ~/src/svn_amshome/scripting/scm/pisa/key.py:58, in Key.val(self, value)
        55 @val.setter
        56 def val(self, value: Any) -> None:
        57     self._value_changed = True
   ---> 58     self._val = self._convert_val(value)


   File ~/src/svn_amshome/scripting/scm/pisa/key.py:186, in IntKey._convert_val(self, val)
       178 def _convert_val(self, val: int) -> int:
       179     """Accepts anything that can be converted to an integer, except boolean values.
       180
       181     :param val: value
      (...)
       184     :rtype: int
       185     """
   --> 186     return int(self._check_for_boolean(val))


   File ~/src/svn_amshome/scripting/scm/pisa/key.py:118, in Key._check_for_boolean(self, val)
       116 def _check_for_boolean(self, val: T) -> T:
       117     if isinstance(val, bool):
   --> 118         raise TypeError(f"Will not accept boolean value {val} for {self.__class__.__name__} {self}")
       119     return val


   TypeError: Will not accept boolean value True for IntKey <Key: 'VectorLength 1'>

Similarly, since "bool(obj)" will work in Python on objects of any
type, so for "BoolKey" attributes, only the booleans "True" and
"False" are accepted, plus any case variation of some strings for
historic (fortran-related) reasons:

   adf = engines.ADF()

   # this is all accepted
   adf.AccurateGradients = True
   adf.AccurateGradients = False
   adf.AccurateGradients = 'YeS'
   adf.AccurateGradients = 'nO'
   adf.AccurateGradients = 'T'
   adf.AccurateGradients = 'f'

   # this will be rejected
   adf.AccurateGradients = 1

   ---------------------------------------------------------------------------

   ValueError                                Traceback (most recent call last)

   Cell In[10], line 12
         9 adf.AccurateGradients = 'f'
        11 # this will be rejected
   ---> 12 adf.AccurateGradients = 1


   File ~/src/svn_amshome/scripting/scm/pisa/block.py:404, in Block.__setattr__(self, _Block__name, _Block__value)
       402 # allow syntax like AMS().Task = 'SinglePoint'
       403 elif isinstance(attr, Key) and not isinstance(__value, Key):
   --> 404     attr.val = __value
       405 else:
       406     super().__setattr__(__name, __value)


   File ~/src/svn_amshome/scripting/scm/pisa/entry.py:125, in Entry.__setattr__(self, _Entry__name, _Entry__value)
       123         msg += f" Did you possibly mean to access any of [{', '.join(close_matches)}]?"
       124     raise AttributeError(msg)
   --> 125 return super().__setattr__(__name, __value)


   File ~/src/svn_amshome/scripting/scm/pisa/key.py:58, in Key.val(self, value)
        55 @val.setter
        56 def val(self, value: Any) -> None:
        57     self._value_changed = True
   ---> 58     self._val = self._convert_val(value)


   File ~/src/svn_amshome/scripting/scm/pisa/key.py:166, in BoolKey._convert_val(self, val)
       164     return False
       165 else:
   --> 166     raise ValueError(
       167         f"{str_val} not in accepted options for True: {accepted_true} or " f"False: {accepted_false}"
       168     )


   ValueError: 1 not in accepted options for True: ['true', 'yes', 't'] or False: ['false', 'no', 'f']


2.4.5. Overwriting a "Block" attribute that is not an "EngineBlock", "FreeBlock" or an "InputBlock"
---------------------------------------------------------------------------------------------------

Except for the block types mentioned in the title, block attributes
are not meant to be overwritten:

   from scm.pisa.block import EngineBlock

   ams = drivers.AMS()
   # The AMS.Engine attribute is of type EngineBlock
   print(f"{isinstance(ams.Engine, EngineBlock)=}")
   # And thus you are allowed to override it with an actual Engine
   ams.Engine = engines.ADF()
   # But you are not allowed to overwrite it with anything else
   ams.Engine = 'Foo'

   isinstance(ams.Engine, EngineBlock)=True

   ---------------------------------------------------------------------------

   TypeError                                 Traceback (most recent call last)

   Cell In[11], line 9
         7 ams.Engine = engines.ADF()
         8 # But you are not allowed to overwrite it with anything else
   ----> 9 ams.Engine = 'Foo'


   File ~/src/svn_amshome/scripting/scm/pisa/block.py:382, in Block.__setattr__(self, _Block__name, _Block__value)
       380         super().__setattr__(__name, __value)
       381     else:
   --> 382         raise TypeError(f"Expect type 'EngineBlock', received type {type(__value)}: {repr(__value)}")
       383 elif isinstance(attr, FreeBlock):
       384     is_iterable = False


   TypeError: Expect type 'EngineBlock', received type <class 'str'>: 'Foo'

   # Allowed to override free blocks with an iterable of strings
   ams.Log = ['Freeblock line 1', 'FreeBlock line 2']
   print(ams.Log.get_input_string())
   # Or with a single multiline string
   ams.Log = '''\
   Freeblock line 1
   FreeBlock line 2
   '''
   print(ams.Log.get_input_string())
   # But not with anything else
   ams.Log = True

   Log
     Freeblock line 1
     FreeBlock line 2
   End

   Log
     Freeblock line 1
     FreeBlock line 2

   End

   ---------------------------------------------------------------------------

   TypeError                                 Traceback (most recent call last)

   Cell In[12], line 11
         9 print(ams.Log.get_input_string())
        10 # But not with anything else
   ---> 11 ams.Log = True


   File ~/src/svn_amshome/scripting/scm/pisa/block.py:395, in Block.__setattr__(self, _Block__name, _Block__value)
       393         attr._multiline_string = "\n".join(str(line) for line in __value) + "\n"
       394     else:
   --> 395         raise TypeError(
       396             f"Expected a string or an iterable of strings to assign to FreeBlock attribute {attr}. "
       397             f"Received {__value}"
       398         )
       399     attr._value_changed = True
       400 elif isinstance(attr, Block):


   TypeError: Expected a string or an iterable of strings to assign to FreeBlock attribute Log
     Freeblock line 1
     FreeBlock line 2

   End
   . Received True

   # Not allowed to override general Block attributes
   ams.System = 'foo'

   ---------------------------------------------------------------------------

   AttributeError                            Traceback (most recent call last)

   Cell In[13], line 2
         1 # Not allowed to override general Block attributes
   ----> 2 ams.System = 'foo'


   File ~/src/svn_amshome/scripting/scm/pisa/block.py:401, in Block.__setattr__(self, _Block__name, _Block__value)
       399     attr._value_changed = True
       400 elif isinstance(attr, Block):
   --> 401     raise AttributeError(f"Not allowed to overwrite block attribute {attr}")
       402 # allow syntax like AMS().Task = 'SinglePoint'
       403 elif isinstance(attr, Key) and not isinstance(__value, Key):


   AttributeError: Not allowed to overwrite block attribute System
   End


2.4.6. Path keys
----------------

When the ".ispath" attribute of a "StringKey" is "True", any strings
passed to it will be checked to see if they represent an existing
relative path. If so, the string will be replaced by a string
representing the absolute path and the user will be warned of this.
The reason for this is that often a path is entered relative to the
location of the PLAMS script, but when the job is actually running the
working directory will be different and the path can no longer be
resolved.

   ! touch foo.rkf
   ams = drivers.AMS()
   # this will display a warning
   ams.EngineRestart = 'foo.rkf'
   print(f"{ams.get_input_string()=}")
   # non existing paths will be left alone
   ams.EngineRestart = 'bar.rkf'
   print(f"{ams.get_input_string()=}")
   ! rm foo.rkf

   ams.get_input_string()='EngineRestart foo.rkfn'
   ams.get_input_string()='EngineRestart bar.rkfn'


2.4.7. Repeated Entry indexing
------------------------------

Some keys/blocks have their ".unique" attributes set to "False",
meaning multiple occurrences of the entry are allowed in the input.
This can be accomplished using the Python index notation "[<index>]".
This indexing is not allowed on unique entries.

   ams = drivers.AMS()
   print(f"{ams.EngineRestart.unique=}")
   ams.EngineRestart[0] = 'bar.rkf'

   ams.EngineRestart.unique=True

   ---------------------------------------------------------------------------

   ValueError                                Traceback (most recent call last)

   Cell In[15], line 3
         1 ams = drivers.AMS()
         2 print(f"{ams.EngineRestart.unique=}")
   ----> 3 ams.EngineRestart[0] = 'bar.rkf'


   File ~/src/svn_amshome/scripting/scm/pisa/entry.py:27, in _check_unique.<locals>.wrapper(obj, *args, **kwargs)
        25 assert isinstance(obj, Entry)
        26 if obj.unique:
   ---> 27     raise ValueError(f"Calling method {meth.__name__} not allowed on unique entry {obj}")
        28 return meth(obj, *args, **kwargs)


   ValueError: Calling method __setitem__ not allowed on unique entry <Key: 'EngineRestart None'>

   hybrid = engines.Hybrid()
   hybrid.Engine[0] = engines.ADF()
   hybrid.Engine[1] = engines.BAND()
   print(hybrid.get_input_string())

   Engine Hybrid

     Engine ADF
     EndEngine

     Engine BAND
     EndEngine
   EndEngine

The repeated entries need to be created in order, with an indexing
starting at zero. If you accidentally skip an index, an exception will
be raised that will point you to the index you have to create:

   hybrid.Engine[3] = engines.DFTB()

   ---------------------------------------------------------------------------

   IndexError                                Traceback (most recent call last)

   File ~/src/svn_amshome/scripting/scm/pisa/block.py:425, in Block.__setitem__(self, index, value)
       424 try:
   --> 425     self._entry_list[index] = value
       426 except IndexError:


   IndexError: list assignment index out of range


   During handling of the above exception, another exception occurred:


   IndexError                                Traceback (most recent call last)

   File ~/src/svn_amshome/scripting/scm/pisa/entry.py:193, in Entry.__getitem__(self, index)
       192 try:
   --> 193     return self._entry_list[index]
       194 except IndexError:


   IndexError: list index out of range


   During handling of the above exception, another exception occurred:


   IndexError                                Traceback (most recent call last)

   Cell In[17], line 1
   ----> 1 hybrid.Engine[3] = engines.DFTB()


   File ~/src/svn_amshome/scripting/scm/pisa/entry.py:28, in _check_unique.<locals>.wrapper(obj, *args, **kwargs)
        26 if obj.unique:
        27     raise ValueError(f"Calling method {meth.__name__} not allowed on unique entry {obj}")
   ---> 28 return meth(obj, *args, **kwargs)


   File ~/src/svn_amshome/scripting/scm/pisa/block.py:430, in Block.__setitem__(self, index, value)
       428     self._entry_list = [*self._entry_list, value]
       429 else:
   --> 430     self[index]


   File ~/src/svn_amshome/scripting/scm/pisa/entry.py:28, in _check_unique.<locals>.wrapper(obj, *args, **kwargs)
        26 if obj.unique:
        27     raise ValueError(f"Calling method {meth.__name__} not allowed on unique entry {obj}")
   ---> 28 return meth(obj, *args, **kwargs)


   File ~/src/svn_amshome/scripting/scm/pisa/block.py:552, in EngineBlock.__getitem__(self, index)
       550 @_check_unique
       551 def __getitem__(self, index: int) -> Any:
   --> 552     return super().__getitem__(index)


   File ~/src/svn_amshome/scripting/scm/pisa/entry.py:28, in _check_unique.<locals>.wrapper(obj, *args, **kwargs)
        26 if obj.unique:
        27     raise ValueError(f"Calling method {meth.__name__} not allowed on unique entry {obj}")
   ---> 28 return meth(obj, *args, **kwargs)


   File ~/src/svn_amshome/scripting/scm/pisa/entry.py:222, in Entry.__getitem__(self, index)
       220     return self.__getitem__(index)
       221 else:
   --> 222     raise IndexError(
       223         f"Tried to access {self.name}[{index}], but {self.name}[{len(self)}] does not exist. "
       224         f"First create {self.name}[{len(self)}]"
       225     )


   IndexError: Tried to access Engine[3], but Engine[2] does not exist. First create Engine[2]


2.4.8. Setting of block headers
-------------------------------

Some blocks allow a header to be set, which will show up next to the
entry name in the text input. You can check if a header is allowed via
the ".allow_header" property. The header can simply be set by
assigning a string to the ".header" attribute.

   ams = drivers.AMS()

   print(f"{ams.System.allow_header=}")
   ams.System.header = 'MyHeader'
   ams.System.Symmetry = 'AUTO'
   print(ams.get_input_string())

   # An exception will be raised when setting the header blocks that don't allow headers
   ams.Symmetry.header = 'MyHeader'

   ams.System.allow_header=True
   System MyHeader
     Symmetry AUTO
   End

   ---------------------------------------------------------------------------

   ValueError                                Traceback (most recent call last)

   Cell In[18], line 9
         6 print(ams.get_input_string())
         8 # An exception will be raised when setting the header blocks that don't allow headers
   ----> 9 ams.Symmetry.header = 'MyHeader'


   File ~/src/svn_amshome/scripting/scm/pisa/block.py:406, in Block.__setattr__(self, _Block__name, _Block__value)
       404     attr.val = __value
       405 else:
   --> 406     super().__setattr__(__name, __value)


   File ~/src/svn_amshome/scripting/scm/pisa/entry.py:125, in Entry.__setattr__(self, _Entry__name, _Entry__value)
       123         msg += f" Did you possibly mean to access any of [{', '.join(close_matches)}]?"
       124     raise AttributeError(msg)
   --> 125 return super().__setattr__(__name, __value)


   File ~/src/svn_amshome/scripting/scm/pisa/block.py:76, in Block.header(self, val)
        74     self._header = str(val)
        75 else:
   ---> 76     raise ValueError(f"Block {self.name} does not allow setting of header")


   ValueError: Block Symmetry does not allow setting of header


2.5. Integration with PLAMS
===========================


2.5.1. Passing driver object to AMSJob
--------------------------------------

There are multiple ways to pass your driver object to an instance of
"AMSJob".

The first way is to create an empty "Settings" object, and insert your
driver object under the ".input" attribute. Then simply pass the
setting object to the job as usual. This allows you to also pass extra
configuration in the settings object:

   from scm.plams import AMSJob, Settings, Molecule, Atom

   settings = Settings()

   driver = drivers.AMS()
   driver.Task = "GeometryOptimization"
   driver.Properties.NormalModes = True
   driver.Engine = engines.DFTB()
   driver.Engine.Model = "SCC-DFTB"

   settings.input = driver

   molecule = Molecule()
   molecule.add_atom(Atom(symbol='O', coords=(0,0,0)))
   molecule.add_atom(Atom(symbol='H', coords=(1,0,0)))
   molecule.add_atom(Atom(symbol='H', coords=(0,1,0)))


   job = AMSJob(molecule=molecule, settings=settings, name='water_optimization')

   print(job.get_input())

   Properties
     NormalModes True
   End
   Task GeometryOptimization

   Engine DFTB
     Model SCC-DFTB
   EndEngine

   System
     Atoms
                 O       0.0000000000       0.0000000000       0.0000000000
                 H       1.0000000000       0.0000000000       0.0000000000
                 H       0.0000000000       1.0000000000       0.0000000000
     End
   End

Alternatively you can also pass your driver object as the "settings"
argument itself. The job object will then create an empty settings
object and insert the driver object under the ".input" attribute:

   job = AMSJob(molecule=molecule, settings=driver, name='water_optimization')
   print(f"{job.settings.input=}")

   job.settings.input=AMS(name='AMS')

Finally you can also create the job object first and start assigning
to the ".settings.input" attribute, leading to a slightly more verbose
syntax:

   job = AMSJob(molecule=molecule, name='water_optimization')
   job.settings.input = drivers.AMS()
   job.settings.input.Engine = engines.ADF()
   print(job.get_input())
   # etc

   Engine ADF
   EndEngine

   System
   Atoms
               O       0.0000000000       0.0000000000       0.0000000000
               H       1.0000000000       0.0000000000       0.0000000000
               H       0.0000000000       1.0000000000       0.0000000000
   End
   End


2.5.2. Conversion to and from PLAMS Settings
--------------------------------------------

For backwards compatibility and convenience, the "Block" objects have
a to_settings and from_settings method, which both work by passing
text input to the "InputParser" class. The "to_settings" method is
quite trustworthy, since it generates a more loosely defined object
from a more strictly defined object. The "from_settings" object works
with basic settings, but is not extensively tested with all possible
forms of the settings object.

See the examples below for simple conversions:

   from scm.input_classes import AMS, DFTB
   from scm.plams.interfaces.adfsuite.ams import AMSJob
   from scm.plams.core.settings import Settings

   ams = AMS()
   ams.Task = 'SinglePoint'
   ams.Engine = DFTB()

   s = Settings()
   s.input = ams.to_settings()
   job = AMSJob(settings=s)
   print(job.get_input())

   task SinglePoint

   Engine dftb
   EndEngine

   from scm.input_classes import AMS, DFTB
   from scm.plams.interfaces.adfsuite.ams import AMSJob
   from scm.plams.core.settings import Settings

   settings = Settings()
   settings.input
   settings.input.ams.Task = 'GeometryOptimization'
   settings.input.ams.Properties.NormalModes = 'Yes'
   settings.input.DFTB.Model = 'SCC-DFTB'

   ams = AMS.from_settings(settings)
   print(ams.get_input_string())

   # this is supported, but fragile:
   dftb = DFTB.from_settings(settings)
   print(dftb.get_input_string())
   # this is safer:
   ams = AMS.from_settings(settings)
   dftb = ams.engine

   Properties
     NormalModes True
   End
   Task GeometryOptimization

   Engine DFTB
     Model SCC-DFTB
   EndEngine

   /home/vdkolk/src/svn_amshome/scripting/scm/pisa/block.py:218: UserWarning: Could not generate <class 'scm.input_classes.engines.dftb.DFTB'> from textinput:
   Properties
     NormalModes Yes
   End

   Task GeometryOptimization

   Engine DFTB
     Model SCC-DFTB
   EndEngine


   Retrying (fragile!) with subset:

     Model SCC-DFTB

     warnings.warn(msg)

   Engine DFTB
     Model SCC-DFTB
   EndEngine


2.6. Repeated blocks and headers
================================

The following is a more complex example using repeated blocks and
headers. For blocks that allow headers (Engine blocks allow headers by
default), you can simply set a string value for the ".header"
attribute.

For repeating blocks, you can use the Python index notation with
square brackets to create and access new instances of the repeated
block on the fly. This requires you to access the instances in order,
starting at 0.

For repeating engine blocks, like in the following example, it is
beneficial to first create the engine block instance and assigning
values to the relevant attributes, before assigning the engine block
as a whole using the index notation. This ensures you can use the full
benefits of the typing system, since it can not dynamically infer the
different engine types.

   driver = drivers.AMS()

   driver.Task = "Replay"
   driver.Replay.File = "/foo/bar/ams.rkf"

   driver.Engine = engines.Hybrid()
   driver.Engine.Energy.DynamicFactors = "UseLowestEnergy"
   # repeated block, start indexing at 0
   driver.Engine.Energy.Term[0].Region = "*"
   driver.Engine.Energy.Term[0].EngineID = "Singlet"
   driver.Engine.Energy.Term[1].Region = "*"
   driver.Engine.Energy.Term[1].EngineID = "Triplet"

   # create the engine object first, to benefit from type hinting
   singlet_engine = engines.ADF()
   singlet_engine.header = "Singlet"
   singlet_engine.Unrestricted = "No"
   singlet_engine.XC.GGA = "PBE"
   singlet_engine.Basis.Type = "DZP"
   singlet_engine.SCF.Iterations = 100
   # do not forget to assign it to the repeated engine block
   driver.Engine.Engine[0] = singlet_engine

   triplet_engine = engines.ADF()
   triplet_engine.header = "Triplet"
   triplet_engine.Unrestricted = "Yes"
   triplet_engine.SpinPolarization = 2
   triplet_engine.XC.GGA = "PBE"
   triplet_engine.Basis.Type = "DZP"
   triplet_engine.SCF.Iterations = 100
   driver.Engine.Engine[1] = triplet_engine

   print(driver.get_input_string())

   Replay
     File /foo/bar/ams.rkf
   End
   Task Replay

   Engine Hybrid
     Energy
       DynamicFactors UseLowestEnergy
       Term
         EngineID Singlet
         Region *
       End
       Term
         EngineID Triplet
         Region *
       End
     End

     Engine ADF Singlet
       Basis
         Type DZP
       End
       SCF
         Iterations 100
       End
       Unrestricted False
       XC
         GGA PBE
       End
     EndEngine

     Engine ADF Triplet
       Basis
         Type DZP
       End
       SCF
         Iterations 100
       End
       SpinPolarization 2.0
       Unrestricted True
       XC
         GGA PBE
       End
     EndEngine
   EndEngine


2.7. Free blocks
================

Free blocks can be assigned with a either a multiline string or a
Sequence/Iterable of strings:

   driver = drivers.AMS()
   adf = engines.ADF()

   adf.RISM = """\
   H 0.0 0.0 0.0
   O 1.0 0.0 0.0
   O 0.0 1.0 0.0"""

   driver.Engine = adf

   print(driver.get_input_string())

   adf.RISM = (
       'H 0.0 1.0 0.0',
       'O 0.0 1.0 0.0',
       'O 1.0 1.0 1.0',
   )

   print(driver.get_input_string())

   Engine ADF
     RISM
       H 0.0 0.0 0.0
       O 1.0 0.0 0.0
       O 0.0 1.0 0.0
     End
   EndEngine

   Engine ADF
     RISM
       H 0.0 1.0 0.0
       O 0.0 1.0 0.0
       O 1.0 1.0 1.0
     End
   EndEngine


2.8. Instantiating block instances from existing text input
===========================================================

To instantiate a block instance from existing text, you can use the
**from_text** class method of the appropriate driver or engine class.
Note that for the *Engine* attribute of such dynamically generated
instances, some type hinters might generate false positives.

   text_input = """\
   Task SinglePoint

   Engine BAND
     Basis
       Type DZP
     End
     DOS
       CalcPDOS True
     End
     HubbardU
       Enabled True
       LValue 2 -1
       UValue 0.6 0.0
     End
     KSpace
       Quality Basic
     End
     NumericalQuality Normal
     Unrestricted True
     XC
       gga BP86
     End
   EndEngine
   """

   driver = drivers.AMS.from_text(text_input)

   driver.Engine.Basis.Type = 'SZ'

   print(driver.get_input_string())

   Task SinglePoint

   Engine BAND
     Basis
       Type SZ
     End
     DOS
       CalcPDOS True
     End
     HubbardU
       Enabled True
       LValue 2 -1
       UValue 0.6 0.0
     End
     KSpace
       Quality Basic
     End
     NumericalQuality Normal
     Unrestricted True
     XC
       gga BP86
     End
   EndEngine
