1. Examples

1.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 of 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

1.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.'

1.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, it’s value (.val attribute) will be set to equal to it’s .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

1.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.

1.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]?

1.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']?

1.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'>

1.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']

1.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

1.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'

1.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]

1.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

1.5. Integration with PLAMS

1.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

1.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

1.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

1.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

1.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