"""Module interpret of viperleed.calc.files.parameters.

Initial version by @fkraushofer in 2020, major rewrite by @amimre
and @michele-riva in June 2023. This module used to be part of
parameters.py. Refactored in October 2023.

Functions and classes for interpreting the contents previously
read from a PARAMETERS file.
"""

__authors__ = (
    'Florian Kraushofer (@fkraushofer)',
    'Alexander M. Imre (@amimre)',
    'Michele Riva (@michele-riva)',
    )
__copyright__ = 'Copyright (c) 2019-2024 ViPErLEED developers'
__created__ = '2020-08-18'
__license__ = 'GPLv3+'

from collections.abc import Sequence
import copy
from functools import partialmethod, wraps
import logging
from pathlib import Path
import re

import numpy as np
try:
    from matplotlib.colors import is_color_like  # For PLOT_IV
except ImportError:
    _CAN_PLOT = False
else:
    _CAN_PLOT = True

from viperleed import __version__
from viperleed.calc.classes.rparams import EnergyRange
from viperleed.calc.classes.rparams import IVShiftRange
from viperleed.calc.classes.rparams import LayerCuts
from viperleed.calc.classes.rparams import SymmetryEps
from viperleed.calc.classes.rparams import TheoEnergies
from viperleed.calc.files.tenserleed import OLD_TL_VERSION_NAMES
from viperleed.calc.lib import periodic_table
from viperleed.calc.lib.base import parent_name
from viperleed.calc.lib.base import readIntRange, readVector
from viperleed.calc.lib.base import recombineListElements, splitSublists
from viperleed.calc.lib.version import Version
from viperleed.calc.lib.woods_notation import readWoodsNotation
from viperleed.calc.sections.calc_section import CalcSection as Section

from .checker import ParametersChecker
from .errors import ParameterBooleanConversionError
from .errors import ParameterConversionError
from .errors import ParameterError
from .errors import ParameterFloatConversionError
from .errors import ParameterHasNoValueError
from .errors import ParameterIntConversionError
from .errors import ParameterNeedsFlagError
from .errors import ParameterNeedsSlabError
from .errors import ParameterNotRecognizedError
from .errors import ParameterNumberOfInputsError
from .errors import ParameterParseError
from .errors import ParameterRangeError
from .errors import ParameterUnknownFlagError
from .errors import ParameterValueError
from .errors import SuperfluousParameterError
from .known_parameters import KNOWN_PARAMS, is_deprecated, warn_if_deprecated
from .utils import Assignment, NumericBounds, POSITIVE_FLOAT, POSITIVE_INT


_LOGGER = logging.getLogger(parent_name(__name__))


# Bool parameters for which to create interpret...() methods automatically.
# key is parameter name, value is tuple of keyword arguments that should be
# passed to interpret_bool_parameter, in order.
_SIMPLE_BOOL_PARAMS = {
    'LOG_SEARCH' : (),
    'PHASESHIFTS_CALC_OLD' : (),
    'PHASESHIFTS_OUT_OLD' : (),
    'R_FACTOR_LEGACY' : (),
    'STOP': (),
    'SUPPRESS_EXECUTION' : (),
    'SYMMETRIZE_INPUT' : (),
    'SYMMETRY_FIND_ORI' : (),
    'TL_IGNORE_CHECKSUM' : (),
    'LAYER_STACK_VERTICAL' : ({False: 'c', True: 'z'},),
    }


# Numerical parameters for which to create interpret...() methods
# automatically. Key is parameter name, value is a NumericBounds
_SIMPLE_NUMERICAL_PARAMS = {
    # Positive-only integers
    'BULKDOUBLING_MAX' : POSITIVE_INT,
    'N_CORES' : POSITIVE_INT,
    'SEARCH_MAX_GEN' : POSITIVE_INT,
    'TENSOR_INDEX' : POSITIVE_INT,
    # Positive-only floats
    'T_DEBYE' : POSITIVE_FLOAT,
    'T_EXPERIMENT' : POSITIVE_FLOAT,
    'V0_IMAG' : POSITIVE_FLOAT,
    # Other floats
    'V0_Z_ONSET' : NumericBounds(),
    'ATTENUATION_EPS' : NumericBounds(range_=(1e-6, 1),
                                      accept_limits=(True, False)),
    'BULKDOUBLING_EPS' : NumericBounds(range_=(1e-4, None),
                                       out_of_range_event='coerce'),
    'BULK_LIKE_BELOW': NumericBounds(range_=(0, 1),
                                     accept_limits=(False, False)),
    'SCREEN_APERTURE' : NumericBounds(range_=(0, 180)),
    # Other integers
    'HALTING' : NumericBounds(type_=int, range_=(1, 3)),
    'N_BULK_LAYERS' : NumericBounds(type_=int, range_=(1, 2)),
    'R_FACTOR_SMOOTH' : NumericBounds(type_=int, range_=(0, 999)),
    'R_FACTOR_TYPE' : NumericBounds(type_=int, range_=(1, 2)),
    'ZIP_COMPRESSION_LEVEL' : NumericBounds(type_=int, range_=(0, 9))
    }


# parameters that can be optimized in FD optimization
_OPTIMIZE_OPTIONS = {'theta', 'phi', 'v0i', 'a', 'b', 'c', 'ab', 'abc',}


def interpret(rpars, slab=None, silent=False):
    """Interpret rpars.readParams to actual values.

    Parameters
    ----------
    rpars : Rparams
        Object storing parameters for current run. Created previously
        by parameters.read, and should already contain raw string data.
    slab : Slab, optional
        Slab object with elements and atomic position data. If not
        passed, some parameters will not be interpreted.
    silent : bool, optional
        If True, less output will be printed. The default is False.

    Raises
    ------
    ParameterError
        If any parameter interpretation fails.
    """
    interpreter = ParameterInterpreter(rpars)
    interpreter.interpret(slab=slab, silent=silent)


def _with_dynamic_methods(cls):
    """Attach dynamically generated methods to the ParameterInterpreter cls."""

    def _interpret_deprecated(interpreter_method, param):
        """Interpret a deprecated PARAMETER."""
        @wraps(interpreter_method)
        def _wrapper_method(self, assignment, **kwargs):
            if warn_if_deprecated(param, __version__):
                self.rpars.setHaltingLevel(3)
            interpreter_method(self, assignment, **kwargs)
        return _wrapper_method

    # Dynamically produce methods for the 'simple parameters' listed
    # above. Notice that we should do this only once for the class,
    # and not for each instance separately. Calling the methods again
    # is possible, and would simply override the bindings
    cls.make_boolean_interpreter_methods()
    cls.make_numerical_interpreter_methods()

    # Now wrap all the interpreter methods for deprecated parameters
    for method_name, method in cls.__dict__.items():
        if not method_name.startswith('interpret_'):
            continue
        *_, param = method_name.split('interpret_')
        if not is_deprecated(param, __version__):
            continue
        setattr(cls, method_name, _interpret_deprecated(method, param))
    return cls


_BELOW_DEBUG = 2


# Disable 'too-many-public-methods' because of the mechanics of a
# ParameterInterpreter: a dispatcher for interpreting PARAMETERS.
# Methods are public, but are very rarely used. Most of the times
# they are used via the .interpret() interface.
@_with_dynamic_methods
class ParameterInterpreter:  # pylint: disable=too-many-public-methods
    """Class to interpret parameters from the PARAMETERS file.

    To add a new parameter, add a method with the name
    'interpret_<param>' and with a single assignment argument.
    """

    domains_ignore_params = {
        'BULK_REPEAT', 'ELEMENT_MIX', 'ELEMENT_RENAME',
        'LAYER_CUTS', 'N_BULK_LAYERS', 'SITE_DEF', 'SUPERLATTICE',
        'SYMMETRY_CELL_TRANSFORM', 'TENSOR_INDEX', 'TENSOR_OUTPUT'
        }

    grouplist = [                                                               # TODO: take from elsewhere
        'p1', 'p2', 'pm', 'pg', 'cm', 'rcm', 'pmm', 'pmg', 'pgg', 'cmm',
        'rcmm', 'p4', 'p4m', 'p4g', 'p3', 'p3m1', 'p31m', 'p6', 'p6m']

    bool_synonyms = {
        True: {'true', 'yes', '1', 't', 'y', 'on', 'enable', '.true.'},
        False: {'false', 'no', '0', 'f', 'n', 'off', 'disable', '.false.'}
        }

    def __init__(self, rpars):
        """Initialize interpreter instance from an Rparams."""
        self.rpars = rpars
        self.slab = None
        self._param_names = []  # In precedence order

        # Some flags
        self._search_conv_read = False

    def interpret(self, slab, silent=False):
        """Interpret all known parameters using slab."""
        self.slab = slab
        self._search_conv_read = False

        _backup_log_level = _LOGGER.level
        if silent:
            _LOGGER.setLevel(logging.ERROR)

        self._update_param_order()
        for param, assignment in self._get_param_assignments():
            self._complain_if_invalid_param_in_domain_calc(param)
            self._interpret_param(param, assignment)
            _LOGGER.log(_BELOW_DEBUG,
                        f'Successfully interpreted parameter {param}')

        # Make sure there is no inconsistent combination of parameters
        checker = ParametersChecker()
        checker.check_parameter_conflicts(self.rpars)

        # Finally set the log level back to what it was
        _LOGGER.setLevel(_backup_log_level)

    # ----------------  Helper methods for interpret() ----------------
    def _complain_if_invalid_param_in_domain_calc(self, param):
        """Raise if an invalid param is given in a DOMAINS calculation."""
        _is_domain_calc = 4 in self.rpars.RUN or self.rpars.domainParams
        if not _is_domain_calc or param not in self.domains_ignore_params:
            return
        err_ = (
            f'Found parameter {param} in the main PARAMETERS file of a '
            'DOMAIN calculation. This parameter can only be defined in '
            'the PARAMETERS files of the individual structural domains'
            )
        raise SuperfluousParameterError(param, message=err_)

    def _get_param_assignments(self):
        """Yield parameters and assignments for each PARAMETER read."""
        flat_params = (
            (param_name, assignment)
            for param_name in self._param_names
            for assignment in self.rpars.readParams[param_name]
            )
        yield from flat_params

    def _interpret_param(self, param, assignment):
        """Interpret the value of a single PARAMETER if known, or complain."""
        interpreter = getattr(self, f'interpret_{param.lower()}', None)
        if interpreter is None:
            # Complain about unknown parameter
            self.rpars.setHaltingLevel(2)
            raise ParameterNotRecognizedError(param)
        interpreter(assignment)

    def _update_param_order(self):
        """Define order in which parameters should be read."""
        ordered_params = 'LOG_LEVEL', 'RUN'
        self._param_names = [p for p in ordered_params
                             if p in self.rpars.readParams]
        self._param_names.extend(
            p for p in KNOWN_PARAMS
            if p in self.rpars.readParams and p not in self._param_names
            )

    # ---------- Methods for interpreting simple parameters -----------
    # Disable pylint warning since, while 6 is a bit many, we can't
    # really do much better than this. Merging some in a container
    # does not seem clearer.
    # pylint: disable-next=too-many-arguments
    def interpret_bool_parameter(self, assignment,
                                 allowed_values=None,
                                 param=None, return_only=False,
                                 no_flags=False):
        """Set a parameter to a boolean value.

        Parameters
        ----------
        assignment : Assignment
            The assignment of the parameter, read from PARAMETERS.
        allowed_values : dict, optional
            Additional string values which should be interpreted as
            False or True. E.g. by default, 'f' and 'false' are False,
            't' and 'true' are True. Keys should be True and False,
            values are Sequences of acceptable strings.
        param : str, optional
            The name of the parameter in PARAMETERS, if it differs
            from assignment.parameter. Also used as attribute name
            for self.rpars. Default is None.
        return_only: bool, optional
            If True, only return the value of the parameter, but do
            not set it. Default is False.
        no_flags : bool, optional
            If True, complain if assignment has flags. Default is
            False.

        Returns
        -------
        value : bool
            The value of the parameter.

        Raises
        ------
        ValueError
            If allowed_values contains keys other than True and False.
        ValueError
            If allowed_values contains identical aliases for True
            and False.
        ParameterUnknownFlagError
            When assignment has flags but no_flag is True.
        ParameterBooleanConversionError
            If the string value of the parameter does not
            have an acceptable boolean correspondent.
        """
        if allowed_values is None:
            allowed_values = {}
        if no_flags:
            self._ensure_no_flags_assignment(assignment, param)

        _bool_synonyms = copy.deepcopy(self.bool_synonyms)
        for option, values in allowed_values.items():
            try:
                _bool_synonyms[option].update(v.lower() for v in values)
            except KeyError:
                raise ValueError(f'Unexpected option={option} '
                                 'in allowed_values') from None

        # Make sure there is no intersection between the two sets
        if set(_bool_synonyms[True]) & set(_bool_synonyms[False]):
            raise ValueError('The sets of allowed values for '
                             'True and False must not overlap')

        # Check if the value is in the allowed ones
        str_value = assignment.value.lower()
        try:
            value = next(bool_ for bool_, synonyms in _bool_synonyms.items()
                         if str_value in synonyms)
        except StopIteration:  # Value is invalid
            self.rpars.setHaltingLevel(1)
            raise ParameterBooleanConversionError(assignment.parameter,
                                                  assignment.value) from None

        param = param or assignment.parameter
        if not return_only:
            setattr(self.rpars, param.upper(), value)
        return value

    # Disable pylint warning since, while 6 is a bit many, we can't
    # really do much better than this. Merging some in a container
    # does not seem clearer.
    # pylint: disable-next=too-many-arguments
    def interpret_numerical_parameter(self, assignment,
                                      param=None, return_only=False,
                                      bounds=NumericBounds(),                   # TODO: ideally one could default to actually using the limits known from self.rpars.get_limits!
                                      no_flags=False):
        """Set a parameter to a numeric (int or float) value.

        Parameters
        ----------
        assignment : Assignment
            The assignment of the parameter, read from PARAMETERS.
        param : str, optional
            The name of the parameter in PARAMETERS, if it differs
            from assignment.parameter. Also used as attribute name
            for self.rpars. Default is None.
        return_only: bool, optional
            If True, only return the value of the parameter, but
            do not set it. Default is False.
        bounds : NumericBounds, optional
            Acceptable limits for value, and what to do in case an
            out-of-bounds value is given. Default is an unbounded
            float.
        no_flags : bool, optional
            If True, complain if assignment has flags. Default is
            False.

        Returns
        ----------
        value : int or float
            The interpreted value.

        Raises
        ------
        ParameterFloatConversionError
            When conversion of a float-type value to float fails.
        ParameterIntConversionError
            When conversion of a int-type value to int fails.
        ParameterRangeError
            When a the value falls outside the constraints of bounds.
        ParameterUnknownFlagError
            When assignment has flags but no_flag is True.
        """
        if no_flags:
            self._ensure_no_flags_assignment(assignment, param)
        type_ = bounds.type_
        exc_cls = (ParameterFloatConversionError if type_ is float
                   else ParameterIntConversionError)
        exc = exc_cls(assignment.parameter, assignment.value)

        try:
            # First convert to float. Necessary for, e.g., 1e6 as int
            float_value = float(assignment.value)
        except ValueError:
            self.rpars.setHaltingLevel(1)
            raise exc from None

        value = type_(float_value)
        if type_ is int and not np.isclose(value, float_value):
            raise exc

        in_range = all(bounds.is_in_range(value))
        if not in_range and bounds.fail:
            self.rpars.setHaltingLevel(1)
            out_of_range = bounds.format_out_of_range(value)
            raise ParameterRangeError(assignment.parameter,
                                      message=out_of_range)

        if not in_range:
            in_range_value = bounds.make_in_range(value)
            out_of_range = bounds.format_out_of_range(value)
            _LOGGER.warning(f'PARAMETERS file: {assignment.parameter}: '
                            f'{out_of_range}. '
                            f'Value will be set to {in_range_value}.')
            value = in_range_value

        param = param or assignment.parameter
        if not return_only:
            setattr(self.rpars, param.upper(), value)
        return value

    @classmethod
    def _make_methods(cls, wrapped, kwargs_names, new_methods_info):
        """Dynamically add methods for this class.

        Parameters
        ----------
        wrapped : callable
            The callable that will be wrapped to create methods.
            The call to new_method(*args, **kwargs) becomes a
            wrapped(*args, **wrapped_kwargs, **kwargs) call,
            where wrapped_kwargs is generated here using
            kwargs_names and new_methods_info.
        kwargs_names : Sequence
            Names (str) of the keyword arguments that may be
            replaced. At most len(kwargs_names) keyword arguments
            will be given to wrapped.
        new_methods_info : dict
            Keys are names of the parameters for which methods are to
            be generated. They are used as the the first positional
            argument to wrapped. Also, the new methods will be
            named "interpret_<key>". Values are sequences of the
            values of keyword arguments to be passed on to wrapped,
            in the same order.

        Raises
        ------
        TypeError
            If too many keyword argument values are given in any of
            the new_methods_info
        """
        for param, kwargs_values in new_methods_info.items():
            method_name = f'interpret_{param.lower()}'
            if not isinstance(kwargs_values, Sequence):
                kwargs_values = (kwargs_values,)
            if len(kwargs_values) > len(kwargs_names):
                raise TypeError(
                    f'Too many keyword arguments for {method_name}. '
                    f'Expected at most values for {kwargs_names}'
                    )
            kwargs = dict(zip(kwargs_names, kwargs_values))
            kwargs['param'] = param
            kwargs['no_flags'] = True  # no flags for simple parameters
            method = partialmethod(wrapped, **kwargs)
            setattr(cls, method_name, method)

    @classmethod
    def make_boolean_interpreter_methods(cls):
        """Dynamically generate bool-setting methods."""
        cls._make_methods(cls.interpret_bool_parameter,
                          ('allowed_values',),
                          _SIMPLE_BOOL_PARAMS)

    @classmethod
    def make_numerical_interpreter_methods(cls):
        """Dynamically generate int/float-setting methods."""
        cls._make_methods(cls.interpret_numerical_parameter,
                          ('bounds',),
                          _SIMPLE_NUMERICAL_PARAMS)

    # ----- Methods to make sure no extra flags/values are given ------
    def _ensure_single_flag_assignment(self, assignment, param=None,
                                       message=None,
                                       must_have_exaclty_one=True):
        """Raise if assignment contains multiple or, optionally, no flags."""
        if param is None:
            param = assignment.parameter
        if must_have_exaclty_one and not assignment.flag:
            self.rpars.setHaltingLevel(1)
            raise ParameterNeedsFlagError(param, message)
        if message is None:
            message = assignment.flags_str
        if assignment.other_flags:
            self.rpars.setHaltingLevel(1)
            raise ParameterUnknownFlagError(param, message)

    def _ensure_single_value_assignment(self, assignment, param=None):
        """Raise if assignment contains multiple values."""
        n_values = len(assignment.values)
        if param is None:
            param = assignment.parameter
        if not n_values:
            self.rpars.setHaltingLevel(1)
            raise ParameterHasNoValueError(param)
        if n_values != 1:
            self.rpars.setHaltingLevel(1)
            raise ParameterNumberOfInputsError(param, (n_values, 1))

    def _ensure_no_flags_assignment(self, assignment, param=None):
        """Raise if assignment contains any flag."""
        if param is None:
            param = assignment.parameter
        if assignment.flags:
            self.rpars.setHaltingLevel(1)
            raise ParameterUnknownFlagError(param, assignment.flag)

    def _ensure_simple_assignment(self, assignment, param=None):
        """Raise if assignment is not simple (i.e., one value, no flags)."""
        self._ensure_no_flags_assignment(assignment, param)
        self._ensure_single_value_assignment(assignment, param)

    def _ensure_single_flag_and_value_assignment(self, assignment, param=None):
        """Raise if assignment does not have a single flag--value pair."""
        self._ensure_single_flag_assignment(assignment, param)
        self._ensure_single_value_assignment(assignment, param)

    # ----------- Methods to interpret individual parameters ----------
    def interpret_average_beams(self, assignment):                              # TODO: Issue #135
        """Assign parameter AVERAGE_BEAMS."""
        param = 'AVERAGE_BEAMS'
        self._ensure_no_flags_assignment(assignment)
        right_side = assignment.values_str.lower().strip()
        # trivial cases
        if right_side in {'off', 'none', 'false', 'f'}:
            self.rpars.AVERAGE_BEAMS = False
            return
        if right_side.lower() in {'all', 'perpendicular', 'perp'}:
            self.rpars.AVERAGE_BEAMS = (0., 0.)
            return

        # Otherwise, try to parse the value
        angles = self._parse_incidence_angles(param, assignment)
        self.rpars.AVERAGE_BEAMS = angles['THETA'], angles['PHI']

    def interpret_beam_incidence(self, assignment):
        """Assign parameter BEAM_INCIDENCE."""
        param = 'BEAM_INCIDENCE'
        self._ensure_no_flags_assignment(assignment)
        angles = self._parse_incidence_angles(param, assignment)
        self.rpars.THETA, self.rpars.PHI = angles['THETA'], angles['PHI']

    def interpret_bulk_repeat(self, assignment):
        """Assign parameter BULK_REPEAT."""
        param = 'BULK_REPEAT'
        if not self.slab:  # BULK_REPEAT is moot without a slab
            raise ParameterNeedsSlabError(param)

        bulk_repeat_str = assignment.values_str.lower()
        # (1) Vector
        if '[' in bulk_repeat_str:
            vec = readVector(bulk_repeat_str, self.slab.ucell)
            if vec is None:
                raise ParameterParseError(parameter=param)
            self.rpars.BULK_REPEAT = vec
            return

        # (2) Z distance
        if '(' not in bulk_repeat_str:
            try:
                self.rpars.BULK_REPEAT = abs(float(assignment.value))
            except ValueError:
                raise ParameterFloatConversionError(parameter=param) from None
            return

        # (3) C or Z distance. Should match, e.g., c(2.0) or z(2.0)
        match = re.match(r'\s*(c|z)\(\s*(?P<val>[0-9.]+)\s*\)',
                         bulk_repeat_str)
        if not match:
            raise ParameterParseError(parameter=param)
        try:
            val = abs(float(match.group('val')))
        except ValueError:
            raise ParameterFloatConversionError(parameter=param) from None

        val = self.slab.c_vector[2] * val if 'c' in bulk_repeat_str else val
        self.rpars.BULK_REPEAT = val

    def interpret_domain(self, assignment):
        """Set the domain path and name."""
        param = 'DOMAIN'
        # Check name
        name = assignment.flag
        if name in self.rpars.DOMAINS:  # Already defined
            error_message = f'Multiple sources defined for domain {name}'
            self.rpars.setHaltingLevel(3)
            raise ParameterValueError(param, message=error_message)
        if not name:  # Get unique name                                         # TODO: used in several other places
            i = 1
            while str(i) in self.rpars.DOMAINS:
                i += 1
            name = str(i)

        # Check path
        right_side = assignment.values_str.strip()
        if Path(right_side).exists():
            path = right_side
        elif Path(right_side).with_suffix('.zip').is_file():
            path = right_side + '.zip'
        else:
            error_message = (f'Value for DOMAIN {name} could not be '
                             'interpreted as either a path or a .zip file')
            self.rpars.setHaltingLevel(3)
            raise ParameterValueError(param, message=error_message)
        self.rpars.DOMAINS[name] = path

    def interpret_domain_step(self, assignment):
        """Assign parameter DOMAIN_STEP."""
        param = 'DOMAIN_STEP'
        self._ensure_simple_assignment(assignment)
        domain_step = self.interpret_numerical_parameter(
            assignment,
            bounds=NumericBounds(type_=int, range_=(1, 100)),
            return_only=True
            )

        # pylint: disable=compare-to-zero
        # Seems clearer this way than having "if 100 % domain_step"
        if 100 % domain_step != 0:
            j = domain_step - 1
            while 100 % j != 0:
                j -= 1
            message = (f'100 is not divisible by given value {domain_step}. '
                       f'Consider using {j} instead')
            self.rpars.setHaltingLevel(1)
            raise ParameterValueError(param, message=message)
        self.rpars.DOMAIN_STEP = domain_step

    def interpret_element_mix(self, assignment):
        """Assign parameter ELEMENT_MIX."""
        param = 'ELEMENT_MIX'
        self._ensure_single_flag_assignment(assignment)
        element = self._get_valid_slab_element_from_flag(param, assignment)

        self._ensure_chemical_elements(param, assignment.values)
        mix = [el.capitalize() for el in assignment.values]
        if len(mix) == 1:
            rename = mix[0]
            self.rpars.setHaltingLevel(3)
            message = (f'Only one mixed element found: {rename}. If you '
                       f'intended to rename {element} to {rename}, you should '
                       f'use "ELEMENT_RENAME {element} = {rename}"')
            raise ParameterNumberOfInputsError(param, message=message)
        self.rpars.ELEMENT_MIX[element] = mix

    def interpret_element_rename(self, assignment):
        """Assign parameter ELEMENT_RENAME."""
        param = 'ELEMENT_RENAME'
        self._ensure_single_flag_and_value_assignment(assignment)
        element = self._get_valid_slab_element_from_flag(param, assignment)
        self._ensure_chemical_elements(param, assignment.values)
        self.rpars.ELEMENT_RENAME[element] = assignment.value.capitalize()

    def interpret_filament_wf(self, assignment):
        """Assign parameter FILAMENT_WF."""
        param = 'FILAMENT_WF'
        self._ensure_simple_assignment(assignment)
        # Check common filaments (e.g., W), otherwise assume a float
        known_filaments = self.rpars.get_default(param)
        try:
            self.rpars.FILAMENT_WF = known_filaments[assignment.value.lower()]
        except KeyError:
            self.interpret_numerical_parameter(assignment)

    def interpret_fortran_comp(self, assignment):                               # TODO: Issue #136
        """Assign parameter FORTRAN_COMP."""
        param = 'FORTRAN_COMP'
        message = (f'Only one flag allowed for {param} per line. '
                   f'Got {assignment.flags}')
        self._ensure_single_flag_assignment(assignment, message=message,
                                            must_have_exaclty_one=False)
        flag, compiler_str = assignment.flag.lower(), assignment.values_str

        # (1) Default (i.e., non-MPI) compiler flags
        if not flag and compiler_str.lower() in {'ifort', 'gfortran'}:
            self.rpars.FORTRAN_COMP[0] = compiler_str.lower()
            return

        # (2) MPI compiler flags
        if flag == 'mpi' and compiler_str.lower() in {'mpifort', 'mpiifort'}:
            self.rpars.FORTRAN_COMP_MPI[0] = compiler_str.lower()
            return

        # (3) Custom compiler flags or full compilation string.
        #     Remove optional quotes
        delim = assignment.values_str[0]
        if delim in {'"', "'"}:
            compiler_str = assignment.values_str[1:]
            if not compiler_str.endswith(delim):
                raise ParameterValueError(param,
                                          message='No closing quotation mark')
            compiler_str = compiler_str[:-1]

        if not flag:
            self.rpars.FORTRAN_COMP[0] = compiler_str
        elif flag == 'post':
            self.rpars.FORTRAN_COMP[1] = compiler_str
        elif flag == 'mpi':
            self.rpars.FORTRAN_COMP_MPI[0] = compiler_str
        elif flag == 'mpipost':
            self.rpars.FORTRAN_COMP_MPI[1] = compiler_str
        else:
            raise ParameterUnknownFlagError(parameter=param,
                                            flag=assignment.flag)

    def interpret_intpol_deg(self, assignment):
        """Assign parameter INTPOL_DEG."""
        param = 'INTPOL_DEG'
        self._ensure_simple_assignment(assignment)
        intpol_deg = assignment.value
        if intpol_deg in self.rpars.get_limits(param):
            self.rpars.INTPOL_DEG = int(intpol_deg)
            return
        self.rpars.setHaltingLevel(1)
        message = 'Only degree 3 and 5 interpolation supported at the moment'
        raise ParameterValueError(param, message=message)

    def interpret_iv_shift_range(self, assignment):
        """Assign parameter IV_SHIFT_RANGE."""
        param = 'IV_SHIFT_RANGE'
        energies = self._parse_energy_range(assignment, assignment.values)
        if len(energies) not in (2, 3):
            self.rpars.setHaltingLevel(1)
            raise ParameterNumberOfInputsError(parameter=param)

        try:
            iv_range = IVShiftRange(*energies)
        except (ValueError, RuntimeError) as exc:
            # RuntimeError: was fixed, and we tried to unfix it
            self.rpars.setHaltingLevel(1)
            raise ParameterValueError(param, message=str(exc)) from exc

        # Make sure that the user gave us a reasonable
        # step. Inform them if we had to change stuff
        if iv_range.defined and not iv_range.is_equivalent(energies):
            _LOGGER.info(f'IV_SHIFT_RANGE bounds were not integer multiples '
                         f'of {iv_range.step}. Bounds have been adjusted to '
                         f'{iv_range.min} {iv_range.max}.')

        # Interpret underscores as defaults
        iv_range.set_undefined_values(self.rpars.get_default(param))
        self.rpars.IV_SHIFT_RANGE = iv_range

    def interpret_layer_cuts(self, assignment):
        """Assign parameter LAYER_CUTS."""
        param = 'LAYER_CUTS'
        try:
            cuts = LayerCuts.from_string(assignment.values_str)
        except ValueError as exc:
            # String has invalid syntax
            raise ParameterParseError(param, message=str(exc)) from None
        if not cuts:
            raise ParameterValueError(param,
                                      message='At least one layer cut needed')
        self.rpars.LAYER_CUTS = cuts

    @staticmethod
    def _tokenize_int_range(numeric_string):
        r"""Return integers representing range bounds in `numeric_string`.

        A token of the form '-\d+' is considered as a negative number
        unless it is preceded by another '\d+' without delimiters, or
        only by '-' delimiters. This means that
        >>> _tokenize_int_range('1-15')
        [1, 15]
        >>> _tokenize_int_range('-1---15')
        [-1, 15]
        >>> _tokenize_int_range('-1-- -15')
        [-1, -15]
        >>> _tokenize_int_range('1 -15')
        [1, -15]

        Acceptable delimiters are spaces, '-', or ':'.

        Parameters
        ----------
        numeric_string : str
            The string to be analysed.

        Returns
        -------
        integers : list
            Integers in `numeric_string`.

        Raises
        ------
        ValueError
            If any of the entries is neither a number or a delimiter
        ValueError
            If, after stripping spaces, `numeric_string` ends with a
            delimiter instead of a number
        """
        string = re.sub(r'\s', ' ', numeric_string.strip())
        delimiter_chars = ' :-'
        delimiters = tuple(delimiter_chars)
        # Remove all adjacent identical delimiters
        for delimiter in delimiter_chars:
            string = re.sub(fr'{delimiter}*{delimiter}', delimiter, string)
        if not string:
            return []
        _digits_or_delim_re = re.compile(fr'[\d{delimiter_chars}]+')
        _digits_re = re.compile(r'\d+')
        if not _digits_or_delim_re.fullmatch(string):
            raise ValueError('contains non-integers or invalid delimiters')
        if string.endswith(delimiters):
            raise ValueError('ends with a delimiter')
        integers = []
        for match_digits in _digits_re.finditer(string):
            token, start = match_digits.group(), match_digits.start()
            delimiter = string[start-1:start]
            if not delimiter:  # First token in string
                integers.append(int(token))
                continue
            if delimiter != '-':
                integers.append(int(token))
                continue
            # Figure out if the '-' is a delimiter or it means negative
            previous = string[:max(start-1, 0)]
            if not previous or previous.endswith(delimiters):
                # Negative number
                integers.append(int(delimiter + token))
            else:
                integers.append(int(token))
        return integers

    def interpret_lmax(self, assignment):
        """Assign parameter LMAX."""
        param = 'LMAX'
        try:
            values = self._tokenize_int_range(assignment.values_str)
        except ValueError as exc:
            raise ParameterValueError(
                param,
                message=f'Invalid {assignment.values_str!r}: {exc}'
                ) from None

        if not values:
            self.rpars.setHaltingLevel(1)
            raise ParameterHasNoValueError(param)
        if len(values) > 2:
            self.rpars.setHaltingLevel(1)
            raise ParameterNumberOfInputsError(param)
        try:
            self.rpars.LMAX = self.rpars.LMAX.from_value(values)
        except ValueError as exc:  # out-of-range
            raise ParameterRangeError(param, message=str(exc))

    def interpret_log_level(self, assignment):
        """Assign parameter LOG_LEVEL."""
        param = 'LOG_LEVEL'
        self._ensure_simple_assignment(assignment)

        # Try to interpret as bool. This is the way
        # the previous LOG_DEBUG used to work
        try:
            log_debug = self.interpret_bool_parameter(assignment,
                                                      return_only=True)
        except ParameterError:
            pass
        else:
            self.rpars.LOG_LEVEL = logging.DEBUG if log_debug else logging.INFO
            return

        # Try to interpret as string (e.g. 'verbose', 'vv')
        _defaults = self.rpars.get_default(param)
        if assignment.values_str.lower() in _defaults:
            self.rpars.LOG_LEVEL = _defaults[assignment.values_str.lower()]
            return

        # Otherwise interpret as int
        try:
            log_level = self.interpret_numerical_parameter(
                assignment,
                bounds=NumericBounds(type_=int, range_=(0, 50)),
                return_only=True
                )
        except ParameterError as exc:
            raise ParameterValueError(param, assignment.values_str) from exc
        self.rpars.LOG_LEVEL = log_level

    def interpret_optimize(self, assignment):
        """Assign parameter OPTIMIZE."""
        param = 'OPTIMIZE'
        if Section.FD_OPTIMIZATION.value not in self.rpars.RUN:                 # TODO: remove .value when using CalcSection in RUN
            msg_ = ('RUN does not include a full-dynamic-optimization section '
                    f'(RUN = {Section.FD_OPTIMIZATION.value}). It makes no '
                    'sense to provide an OPTIMIZE parameter. OPTIMIZE will be '
                    'ignored.')
            _LOGGER.warning(msg_)
            self.rpars.setHaltingLevel(1)
            return
        if not assignment.flag:
            message = 'Parameter to optimize not defined'
            self.rpars.setHaltingLevel(3)
            raise ParameterNeedsFlagError(param, message)
        which = assignment.flag.lower()
        if which not in _OPTIMIZE_OPTIONS:
            self.rpars.setHaltingLevel(3)
            raise ParameterUnknownFlagError(param, f'{which!r}')
        self.rpars.OPTIMIZE['which'] = which
        if not assignment.other_values:
            try:
                self.rpars.OPTIMIZE['step'] = float(assignment.value)
            except ValueError:
                self.rpars.setHaltingLevel(1)
                raise ParameterFloatConversionError(param,
                                                    assignment.value) from None
            return
        for flag_value_pair in assignment.values_str.split(','):
            self._interpret_optimize_flag_value_pair(param, flag_value_pair)

    def _interpret_optimize_flag_value_pair(self, param, flag_value_pair):
        """Interpret one 'flag value' pair for OPTIMIZE."""
        flag, value = self._get_flag_value_from_pair(param, flag_value_pair)
        value_error = f'Value {value!r} is invalid for flag {flag!r}'
        partype = {'step': float, 'convergence': float,
                   'minpoints': int, 'maxpoints': int,
                   'maxstep': float}
        try:
            numeric = partype[flag](value)
        except ValueError as exc:
            self.rpars.setHaltingLevel(1)
            raise ParameterValueError(param, message=value_error) from exc
        except KeyError:
            self.rpars.setHaltingLevel(2)
            raise ParameterUnknownFlagError(param, f'{flag!r}') from None
        self.rpars.OPTIMIZE[flag] = numeric

    def interpret_parabola_fit(self, assignment):                               # TODO: Issue #137
        """Assign parameter PARABOLA_FIT."""
        if assignment.values_str.lower() == 'off':
            self.rpars.PARABOLA_FIT['type'] = 'none'
            return
        for flag_value_pair in assignment.values_str.split(','):
            self._interpret_parabola_fit_flag_value_pair(flag_value_pair)

    def _interpret_parabola_fit_flag_value_pair(self, flag_value_pair):
        """Interpret one 'flag value' pair for PARABOLA_FIT."""
        param = 'PARABOLA_FIT'
        flag, value = self._get_flag_value_from_pair(param, flag_value_pair)
        if flag == 'localise':
            flag = 'localize'
        value_error = (f'Invalid value {value} for flag {flag}. '
                       'Value will be ignored')
        if flag not in ('type', 'alpha', 'mincurv', 'localize'):
            self.rpars.setHaltingLevel(1)
            raise ParameterValueError(param, f'Unknown flag={flag!r}')
        if flag == 'type' and value not in ('linear', 'linearregression',
                                            'lasso', 'ridge', 'elasticnet',
                                            'none'):
            self.rpars.setHaltingLevel(1)
            raise ParameterValueError(param, value_error)
        if flag == 'type':
            self.rpars.PARABOLA_FIT[flag] = value
            return
        try:
            value_float = float(value)
        except ValueError:
            self.rpars.setHaltingLevel(1)
            raise ParameterFloatConversionError(param, value) from None
        if value_float >= 0:
            self.rpars.PARABOLA_FIT[flag] = value_float
            return
        self.rpars.setHaltingLevel(1)
        raise ParameterRangeError(param,
                                  message=f'{flag} value must be non-negative')

    def interpret_phaseshift_eps(self, assignment):
        """Assign parameter PHASESHIFT_EPS."""
        param = 'PHASESHIFT_EPS'
        self._ensure_simple_assignment(assignment)
        try:
            ps_eps = float(assignment.value)
        except ValueError:
            # check if one of default values (e.g. 'fine')
            preset = assignment.value.lower()[0]
            _defaults = self.rpars.get_default(param)
            ps_eps = _defaults.get(preset, None)
            if ps_eps is None:
                self.rpars.setHaltingLevel(1)
                raise ParameterFloatConversionError(param) from None
        if 0 < ps_eps < 1:
            self.rpars.PHASESHIFT_EPS = ps_eps
        else:
            self.rpars.setHaltingLevel(1)
            raise ParameterRangeError(param, given_value=ps_eps,
                                      allowed_range=(0, 1))

    def interpret_plot_iv(self, assignment):
        """Assign parameter PLOT_IV."""
        if not _CAN_PLOT:
            # Cannot interpret this parameter
            return
        param = 'PLOT_IV'
        self._ensure_single_flag_assignment(assignment)
        flag_aliases = {
            'plot': 'plot',
            'axes': 'axes', 'border': 'axes', 'borders': 'axes',
            'colors': 'colors', 'colours': 'colors',
            'color': 'colors', 'colour': 'colors',
            'font': 'font_size', 'fontsize': 'font_size',
            'font_size': 'font_size',
            'legend': 'legend', 'legends': 'legend',
            'lw': 'line_width', 'linewidth': 'line_width',
            'line_width': 'line_width', 'line': 'line_width',
            'overbar': 'overbar', 'overline': 'overbar',
            'perpage': 'perpage', 'layout': 'perpage',
            }
        flag = assignment.flag.lower()
        try:
            flag = flag_aliases[flag]
        except KeyError:
            self.rpars.setHaltingLevel(1)
            raise ParameterUnknownFlagError(param, f'{flag!r}') from None
        setter = getattr(self, f'_interpret_plot_iv__{flag}')
        setter(assignment)

    def _interpret_plot_iv__axes(self, assignment):
        """Assign PLOT_IV['axes']."""
        self._ensure_single_value_assignment(assignment)
        synonyms = {'all': 'all',
                    'none': 'none',
                    'lb': 'lb', 'less': 'lb',
                    'b': 'b', 'bottom': 'b',}
        try:
            self.rpars.PLOT_IV['axes'] = synonyms[assignment.value.lower()]
        except KeyError:
            self.rpars.setHaltingLevel(1)
            raise ParameterParseError(assignment.parameter) from None

    def _interpret_plot_iv__colors(self, assignment):
        """Assign PLOT_IV['colors']."""
        colors = assignment.values
        if not colors:
            self.rpars.setHaltingLevel(1)
            raise ParameterHasNoValueError(assignment.parameter)
        if not all(is_color_like(c) for c in colors):
            err_ = ('Cannot interpret at least one of '
                    f'{assignment.values_str!r} as color(s)')
            raise ParameterValueError(assignment.parameter, message=err_)
        self.rpars.PLOT_IV['colors'] = colors

    def _interpret_plot_iv__font_size(self, assignment):
        """Assign PLOT_IV['font_size']."""
        self._ensure_single_value_assignment(assignment)
        try:
            font_size = float(assignment.value)
        except ValueError:
            self.rpars.setHaltingLevel(1)
            raise ParameterFloatConversionError(assignment.parameter,
                                                assignment.value) from None
        if font_size <= 0:
            self.rpars.setHaltingLevel(1)
            raise ParameterRangeError(assignment.parameter,
                                      message='Font size must be positive.')
        self.rpars.PLOT_IV['font_size'] = font_size

    def _interpret_plot_iv__legend(self, assignment):
        """Assign PLOT_IV['legend']."""
        self._ensure_single_value_assignment(assignment)
        value = assignment.value.lower()
        if value in {'all', 'first', 'none'}:
            self.rpars.PLOT_IV['legend'] = value
        elif value in {'topright', 'tr'}:
            self.rpars.PLOT_IV['legend'] = 'tr'
        else:
            self.rpars.setHaltingLevel(1)
            raise ParameterParseError(assignment.parameter)

    def _interpret_plot_iv__line_width(self, assignment):
        """Assign PLOT_IV['line_width']."""
        self._ensure_single_value_assignment(assignment)
        try:
            line_width = float(assignment.value)
        except ValueError:
            self.rpars.setHaltingLevel(1)
            raise ParameterFloatConversionError(assignment.parameter,
                                               assignment.value) from None
        if line_width <= 0:
            self.rpars.setHaltingLevel(1)
            raise ParameterRangeError(assignment.parameter,
                                      message='Line width must be > 0.')
        self.rpars.PLOT_IV['line_width'] = line_width

    def _interpret_plot_iv__overbar(self, assignment):
        """Assign PLOT_IV['overbar']."""
        self._ensure_single_value_assignment(assignment)
        value = assignment.value.lower()
        if value.startswith('t'):
            self.rpars.PLOT_IV['overbar'] = True
        elif value.startswith('f'):
            self.rpars.PLOT_IV['overbar'] = False
        else:
            message = f'Value for flag {assignment.flag!r} not recognized'
            self.rpars.setHaltingLevel(1)
            raise ParameterParseError(assignment.parameter, message)

    def _interpret_plot_iv__perpage(self, assignment):
        """Assign PLOT_IV['perpage']."""
        # Can be one or two integers
        if not assignment.values:
            self.rpars.setHaltingLevel(1)
            raise ParameterHasNoValueError(assignment.parameter)
        if len(assignment.values) > 2:
            self.rpars.setHaltingLevel(1)
            message = 'perpage accepts one or two positive integers'
            raise ParameterNumberOfInputsError(assignment.parameter,
                                               message=message)
        try:
            values = tuple(int(v) for v in assignment.values)
        except ValueError:
            self.rpars.setHaltingLevel(1)
            raise ParameterIntConversionError(assignment.parameter,
                                              assignment.values) from None
        if any(v <= 0 for v in values):
            _plural = 's' if len(values)>1 else ''
            message = f'perpage value{_plural} must be positive'
            raise ParameterRangeError(assignment.parameter, message=message)
        self.rpars.PLOT_IV['perpage'] = values if len(values)>1 else values[0]

    def _interpret_plot_iv__plot(self, assignment):
        """Assign PLOT_IV['plot']."""
        self._ensure_single_value_assignment(assignment)
        extra_synonyms = {False: ('none',)}
        self.rpars.PLOT_IV['plot'] = self.interpret_bool_parameter(
            assignment, extra_synonyms, return_only=True
            )

    def interpret_run(self, assignment):
        """Assign parameter RUN, inserting an initialization if needed."""
        param = 'RUN'
        segments = []
        for section_str in assignment.values:
            try:
                segments.extend(Section.sequence_from_string(section_str))
            except ValueError as exc:
                self.rpars.setHaltingLevel(2)
                raise ParameterValueError(param, section_str) from exc
        if Section.DOMAINS in segments:
            _LOGGER.info('Found domain search.')
        if not segments:
            self.rpars.setHaltingLevel(3)
            message = f'{param} was defined, but no values were read'
            raise ParameterHasNoValueError(param, message)
        # Insert initialization section if not present
        if segments[0] is not Section.INITIALIZATION:
            segments.insert(0, Section.INITIALIZATION)
        self.rpars.RUN = [s.value for s in segments]                            # TODO: replace with "segments" to keep Section objects

    def interpret_search_beams(self, assignment):
        """Assign parameter SEARCH_BEAMS."""
        param = 'SEARCH_BEAMS'
        self._ensure_simple_assignment(assignment)
        value = assignment.value.lower()
        if value.startswith(('0', 'a')):
            self.rpars.SEARCH_BEAMS = 0
        elif value.startswith(('1', 'i')):
            self.rpars.SEARCH_BEAMS = 1
        elif value.startswith(('2', 'f')):
            self.rpars.SEARCH_BEAMS = 2
        else:
            raise ParameterValueError(param, value)

    def interpret_search_convergence(self, assignment, is_updating=False):      # TODO: Issue #139
        """Interpret SEARCH_CONVERGENCE parameter.

        This method also sets rpars.searchConvInit if `is_updating`
        is True-thy and the values to be assigned have changed.

        Parameters
        ----------
        assignment : Assignment
            The assignment line, containing details of the
            flags and values to be interpreted.
        is_updating : bool, optional
            Whether this method is being called as part of a
            parameters.update (True) or a parameters.interpret
            (False). Default is False.

        Raises
        ------
        ParameterHasNoValueError
            If assignment carries no values.
        ParameterNumberOfInputsError
            If there are more than two values for this parameter.
        ParameterNeedsFlagError
            If more values are present, but no flag was given.
        ParameterUnknownFlagError
            If the first flag is neither 'dgen' nor 'gaussian'.
        ParameterUnknownFlagError
            If the first flag is 'gaussian' and there are more
            flags.
        ParameterUnknownFlagError
            If first flag is 'dgen' and there is more than one
            additional flag, or if the additional flag is not
            one of the acceptable ones ('dec', 'best', 'all').
        ParameterFloatConversionError
            If numeric values cannot be converted to float.
        ParameterRangeError
            If Gaussian width, its scaling factor, dgen, or its
            scaling factor are out of bounds
        """
        param = 'SEARCH_CONVERGENCE'
        if not assignment.values:
            raise ParameterHasNoValueError(param)
        if len(assignment.values) > 2:
            raise ParameterNumberOfInputsError(param)

        if (not assignment.flags
                and not assignment.other_values
                and assignment.value.lower() == 'off'):
            self.rpars.GAUSSIAN_WIDTH_SCALING = 1.
            return

        if not assignment.flags:
            raise ParameterNeedsFlagError(param)
        if assignment.flag.lower() not in ('dgen', 'gaussian'):
            raise ParameterUnknownFlagError(param, f'{assignment.flag!r}')

        numeric = [None, None]
        for i, value in enumerate(assignment.values):
            try:
                numeric[i] = float(value)
            except ValueError as err:
                raise ParameterFloatConversionError(param) from err

        if assignment.flag.lower() == 'gaussian':
            self._ensure_single_flag_assignment(assignment)
            self._interpret_search_convergence_gaussian(
                param, *numeric, is_updating
                )
        else:
            self._interpret_search_convergence_dgen(
                param, assignment.other_flags, *numeric, is_updating
                )

    # disable: The only option would be to pack max_dgen and scaling
    # into a single object, but I'm not sure this would make it
    # more understandable.
    # pylint: disable-next=too-many-arguments
    def _interpret_search_convergence_dgen(self, param, flags, max_dgen,
                                           scaling, is_updating):
        """Assign the SEARCH_MAX_DGEN/_SCLING parameters."""
        if len(flags) > 1:
            raise ParameterUnknownFlagError(param, ' '.join(flags[1:]))
        target = flags[0] if flags else 'dec'
        if target not in ['dec', 'best', 'all']:
            raise ParameterUnknownFlagError(param, f'{target!r}')
        should_store_new_value = (
            not is_updating
            or max_dgen != self.rpars.searchConvInit['dgen'][target]
            )
        valid = max_dgen is not None and max_dgen > 0
        if should_store_new_value and not valid:
            message = 'dgen should be a positive number'
            raise ParameterRangeError(param, message=message)

        if should_store_new_value and not self._search_conv_read:
            # Clear default values
            self.rpars.reset_default('SEARCH_MAX_DGEN')
            self._search_conv_read = True
        if should_store_new_value:
            self.rpars.SEARCH_MAX_DGEN[target] = max_dgen
        if should_store_new_value and is_updating:
            self.rpars.searchConvInit['dgen'][target] = max_dgen

        if scaling is not None and scaling >= 1:
            self.rpars.SEARCH_MAX_DGEN_SCALING[target] = scaling
        elif scaling:
            message = 'dgen scaling value cannot be smaller than 1'
            raise ParameterRangeError(param, message=message)

    def _interpret_search_convergence_gaussian(self, param, gauss_width,
                                               scaling, is_updating):
        """Assign the GAUSSIAN_WIDTH/_SCALING parameters."""
        should_store_new_value = (
            not is_updating
            or gauss_width != self.rpars.searchConvInit['gaussian']
            )
        valid = gauss_width is not None and gauss_width > 0
        if should_store_new_value and not valid:
            message = 'gaussian width should be a positive number'
            raise ParameterRangeError(param, message=message)

        if should_store_new_value:
            self.rpars.GAUSSIAN_WIDTH = gauss_width
        if should_store_new_value and is_updating:
            self.rpars.searchConvInit['gaussian'] = gauss_width

        if scaling is not None and 0 < scaling <= 1:
            self.rpars.GAUSSIAN_WIDTH_SCALING = scaling
        elif scaling is not None:
            raise ParameterRangeError(param, scaling, (0, 1))

    def interpret_search_cull(self, assignment):
        """Assign parameter SEARCH_CULL."""
        param = 'SEARCH_CULL'
        rpars = self.rpars
        if len(assignment.values) not in (1, 2):
            rpars.setHaltingLevel(1)
            raise ParameterNumberOfInputsError(param)
        try:
            cull = rpars.SEARCH_CULL.from_value(assignment.values)
        except TypeError as exc:   # Numeric value is not a number
            rpars.setHaltingLevel(1)
            raise ParameterFloatConversionError(param,
                                                message=str(exc)) from exc
        except ValueError as exc:  # Numeric or type are wrong
            rpars.setHaltingLevel(1)
            raise ParameterValueError(param, message=str(exc)) from exc

        if not cull.has_type:
            _default = rpars.get_default('SEARCH_CULL')
            cull.type_ = _default.type_
        rpars.SEARCH_CULL = cull

    def interpret_search_population(self, assignment):
        param = 'SEARCH_POPULATION'
        self.interpret_numerical_parameter(
            assignment,
            bounds=NumericBounds(type_=int, range_=(1, None))
            )
        if self.rpars.SEARCH_POPULATION < 16:
            _LOGGER.warning(f'{param} is very small. A minimum '
                            'value of 16 is recommended.')

    def interpret_search_start(self, assignment):
        param = 'SEARCH_START'
        # there should only be one values
        if assignment.other_values:
            self.rpars.setHaltingLevel(1)
            raise ParameterNumberOfInputsError(param)

        # there are only a few options, so we can just check them all
        search_start = assignment.value
        if search_start.startswith('rand'):
            self.rpars.SEARCH_START = 'random'
        elif search_start.startswith('center'):
            self.rpars.SEARCH_START = 'centered'
        elif search_start.startswith('control'):
            self.rpars.SEARCH_START = 'control'
        elif search_start.startswith('crandom'):
            self.rpars.SEARCH_START = 'crandom'
        else:
            self.rpars.setHaltingLevel(1)
            raise ParameterUnknownFlagError(param, search_start)

    def interpret_site_def(self, assignment):                                   # TODO: Issue #143
        """Assign the SITE_DEF for one POSCAR element."""
        param = 'SITE_DEF'
        if assignment.values_str.count('top(') > 1:
            self.rpars.setHaltingLevel(3)
            raise ParameterValueError(
                param,
                message='only a single top() allowed per SITE_DEF line'
                )
        self._ensure_single_flag_assignment(assignment)
        site_element = self._get_valid_slab_element_from_flag(param,
                                                              assignment)
        sorted_atoms = []
        if 'top(' in assignment.values_str:
            sorted_atoms = sorted(
                (at for at in self.slab if at.el == site_element),
                key=lambda atom: atom.pos[2],
                reverse=True
                )

        site_def_dict = {}
        for flag_and_values in assignment.values_str.strip().split(','):
            site_label, site_specs = self._get_valid_sitedef_spec(
                param, site_element, flag_and_values
                )
            try:
                atnums = self._get_atom_numbers_for_site(site_specs,
                                                         sorted_atoms)
            except ValueError as exc:  # Invalid syntax
                self.rpars.setHaltingLevel(3)
                err_ = f'Invalid syntax in {assignment.values_str!r}: {exc!r}'
                raise ParameterParseError(param, message=err_) from None
            try:
                self._ensure_sitedef_makes_sense(site_element,
                                                 site_label,
                                                 atnums)
            except ValueError as exc:
                self.rpars.setHaltingLevel(3)
                raise ParameterValueError(assignment.parameter,
                                          message=str(exc)) from None
            site_def_dict[site_label] = atnums
        self.rpars.SITE_DEF[site_element] = site_def_dict

    def _get_valid_sitedef_spec(self, param, site_element, label_and_specs):
        """Return a site label and atom selection specifications, or raise."""
        try:
            site_label, *site_specs = (s.strip()
                                       for s in label_and_specs.split())
        except ValueError:
            self.rpars.setHaltingLevel(2)
            err_ = ('Expected "site_label atom_selection_patterns ". '
                    f'Found "{label_and_specs}"')
            raise ParameterNumberOfInputsError(param, message=err_) from None
        if not site_specs:
            self.rpars.setHaltingLevel(2)
            err_ = ('No atom selection pattern found for site '
                    f'label {site_label}, element {site_element}.')
            raise ParameterParseError(param, message=err_)
        return site_label, site_specs

    @staticmethod
    def _get_atom_numbers_for_site(site_specs, sorted_atoms):
        """Return atom numbers given a selection specification.

        Parameters
        ----------
        site_specs : Sequence
            Items are strings. Each corresponds to a specification for
            selecting some atom numbers. Acceptable specifications are:
            - single integer
            - range of integers (e.g., '1-5', '10:28')
            - 'top(N)'
        sorted_atoms : Sequence
            Atom objects, with element already correct for the specific
            site, sorted by decreasing fractional position. Used only
            if one of the `site_specs` is 'top(N)'.

        Returns
        -------
        atnums : list
            Atom numbers that are selected by the given `site_specs`.

        Raises
        ------
        ValueError
            If any of the `site_specs` has invalid syntax.
        """
        atnums = []
        for site_spec in site_specs:
            extra_atom_numbers = readIntRange(site_spec)
            if extra_atom_numbers:
                atnums.extend(extra_atom_numbers)
                continue
            if 'top(' in site_spec:
                n_top = int(site_spec.split('(')[1].split(')')[0])
                atnums.extend(at.num for at in sorted_atoms[:n_top])
                continue
            raise ValueError(site_spec)
        return atnums

    def _ensure_sitedef_makes_sense(self, poscar_el, site_label, atom_nrs):
        """Raise ValueError if the atom_nrs selected for a site are wrong."""
        invalid_atom_nums = []
        invalid_elements = []
        for num in atom_nrs:
            try:
                atom = self.slab.atlist.get(num)
            except KeyError:
                invalid_atom_nums.append(num)
                continue
            if atom.el != poscar_el:
                invalid_elements.append(atom)
        if not invalid_atom_nums and not invalid_elements:
            return
        error = f'Invalid selection for {site_label}:'
        if invalid_atom_nums:
            error += f'\nAtom number(s) {invalid_atom_nums} not found in slab'
        if invalid_elements:
            error += (f'\nAtom(s) {[str(at) for at in invalid_elements]} '
                      f'do not have POSCAR element {poscar_el}')
        raise ValueError(error)

    def interpret_superlattice(self, assignment):
        """Assign parameter SUPERLATTICE (Wood's or matrix notation)."""
        param = 'SUPERLATTICE'
        self._interpret_wood_or_matrix(param, assignment)
        self.rpars.superlattice_defined = True

    def interpret_symmetry_bulk(self, assignment):
        """Assign parameter SYMMETRY_BULK."""
        param = 'SYMMETRY_BULK'

        # We accept mirrors with syntax "m[a b]", rotations with
        # syntax "rN" and plane groups. Here we only check that
        # the syntax is OK. We defer the complaints about invalid
        # plane group, directions, or rotation orders, to when we
        # know more about the slab
        self.rpars.SYMMETRY_BULK = {  # 'group' added below
            'mirror': set(),
            'rotation': set()
            }
        unrecognized = assignment.values_str
        unrecognized = self._interpret_symmetry_bulk_glides(unrecognized)
        unrecognized = self._interpret_symmetry_bulk_screws(unrecognized)
        unrecognized = self._interpret_symmetry_bulk_group(unrecognized)

        if unrecognized:
            self.rpars.setHaltingLevel(2)
            message = f'Could not recognize {unrecognized!r}: '
            if 'm' in unrecognized:
                message += ('Syntax for mirrors is "m[n1 n2]"; '
                            'n1 and n2 must be 0, +-1, or +-2; ')
            if 'r' in unrecognized:
                message += ('Syntax for rotations is rN; N must be '
                            '2, 3, 4, or 6')
            raise ParameterValueError(param, message=message)

    def _interpret_symmetry_bulk_glides(self, unrecognized):
        """Look up `unrecognized` for glide-plane-direction specifications."""
        _mirror_re = re.compile(r'(\s+m\[\s*(-?[012])\s*,?\s*(-?[012])\])',
                                re.IGNORECASE)
        for token, *directions in _mirror_re.findall(unrecognized):
            # All matches of mirrors are acceptable
            unrecognized = unrecognized.replace(token, '')
            direction = tuple(int(v) for v in directions)
            if (direction[0] < 0
                    or not direction[0] and direction[1] < 0):
                direction = -direction[0], -direction[1]
            self.rpars.SYMMETRY_BULK['mirror'].add(direction)
        return unrecognized

    def _interpret_symmetry_bulk_group(self, unrecognized):
        """Look up `unrecognized` for a symmetry group specification."""
        param = 'SYMMETRY_BULK'
        _group_re = re.compile(                                                 # TODO: For now borrowed from guilib. Eventually will try to instantiate a PlaneGroup
            r'(\s*(\w+)\s*(?:\[\s*-?[012]\s*-?[012]\s*\])?)',
            re.IGNORECASE
            )
        for token, group in _group_re.findall(unrecognized):
            if group not in self.grouplist:
                continue
            unrecognized = unrecognized.replace(token, '')
            if 'group' in self.rpars.SYMMETRY_BULK:
                message = 'Only one symmetry group can be given'
                raise ParameterValueError(param, message=message)
            self.rpars.SYMMETRY_BULK['group'] = token.strip().lower()

        if 'group' not in self.rpars.SYMMETRY_BULK:
            message = 'Need to specify exactly one symmetry group'
            raise ParameterValueError(param, message=message)
        return unrecognized

    def _interpret_symmetry_bulk_screws(self, unrecognized):
        """Look up `unrecognized` for screw-axis-order specifications."""
        _rotation_re = re.compile(r'(\s+r([2346]))', re.IGNORECASE)
        for token, order in _rotation_re.findall(unrecognized):
            unrecognized = unrecognized.replace(token, '')
            self.rpars.SYMMETRY_BULK['rotation'].add(int(order))
        return unrecognized

    def interpret_symmetry_cell_transform(self, assignment):
        """Assign parameter SYMMETRY_CELL_TRANSFORM (Wood's or matrix)."""
        param = 'SYMMETRY_CELL_TRANSFORM'
        self._interpret_wood_or_matrix(param, assignment)

    def interpret_symmetry_eps(self, assignment):
        """Assign parameters SYMMETRY_EPS and SYMMETRY_EPS.z."""
        param = 'SYMMETRY_EPS'
        self._ensure_no_flags_assignment(assignment)
        if len(assignment.values) not in (1, 2):
            self.rpars.setHaltingLevel(1)
            raise ParameterNumberOfInputsError(param)

        # warning specific to SYMMETRY_EPS
        warning_str = ('PARAMETERS file: SYMMETRY_EPS:\n'
                       'Given value {}is greater than one Ångström. This is a '
                       'very loose constraint and might lead to incorrect '
                       'symmetry detection. Be sure to check the output.')

        # interpret first value as SYMMETRY_EPS
        bounds = NumericBounds(type_=float, range_=(1e-100, None))
        eps_value = self.interpret_numerical_parameter(assignment,
                                                       bounds=bounds,
                                                       return_only=True)
        if eps_value > 1.0:
            # pylint: disable-next=logging-format-interpolation
            _LOGGER.warning(warning_str.format(''))
        # interpret possible second value as SYMMETRY_EPS.z
        if not assignment.other_values:
            self.rpars.SYMMETRY_EPS = SymmetryEps(eps_value)
            return

        z_assignment = Assignment(assignment.other_values, param)
        eps_z_value = self.interpret_numerical_parameter(z_assignment,
                                                         bounds=bounds,
                                                         return_only=True)
        self.rpars.SYMMETRY_EPS = SymmetryEps(eps_value, eps_z_value)
        if self.rpars.SYMMETRY_EPS.z > 1.0:
            # pylint: disable-next=logging-format-interpolation
            _LOGGER.warning(warning_str.format('for z '))

    def interpret_symmetry_fix(self, assignment):                               # TODO: use symmetry groups from elsewhere once symmetry and guilib are merged
        param = 'SYMMETRY_FIX'
        group = assignment.values_str.lower()
        if group.startswith('t'):  # determine symmetry automatically
            self.rpars.reset_default(param)
            return
        if group.startswith('f'):
            self.rpars.SYMMETRY_FIX = 'p1'
            return
        if group in self.grouplist and group in ('cm', 'pmg'):
            message = f'For group {group} direction needs to be specified'
            self.rpars.setHaltingLevel(1)
            raise ParameterParseError(param, message)
        if group in self.grouplist:
            self.rpars.SYMMETRY_FIX = group
            return
        if group.startswith(('pm', 'pg', 'cm', 'rcm', 'pmg')):
            # regex to read
            rgx = re.compile(
                r'\s*(?P<group>(pm|pg|cm|rcm|pmg))\s*'
                + r'\[\s*(?P<i1>[-012]+)\s+(?P<i2>[-012]+)\s*\]')
            m = rgx.match(assignment.values_str.lower())
            if not m:
                self.rpars.setHaltingLevel(2)
                raise ParameterParseError(param)
            i1 = i2 = -2
            group = m.group('group')
            try:
                i1 = int(m.group('i1'))
                i2 = int(m.group('i2'))
            except ValueError as err:
                self.rpars.setHaltingLevel(2)
                raise ParameterParseError(param) from err
            if (group in ['pm', 'pg', 'cm', 'rcm', 'pmg']
                    and i1 in range(-1, 3) and i2 in range(-1, 3)):
                self.rpars.SYMMETRY_FIX = m.group(0)
            else:
                self.rpars.setHaltingLevel(2)
                raise ParameterParseError(param)
        else:
            self.rpars.setHaltingLevel(2)
            raise ParameterParseError(param)

    def interpret_tensor_output(self, assignment):
        """Assign parameter TENSOR_OUTPUT."""
        param = 'TENSOR_OUTPUT'
        tensor_output = []
        # Acceptable format for each token is (repeats*)value,
        # with the bracketed part as optional and 'value' either
        # zero or one (or a bool that can be translated to those)
        rgx = re.compile(r'\s*((?P<repeats>\d+)\s*[*])?\s*(?P<value>[01])\s*$')
        tokens = recombineListElements(assignment.values, '*')
        for token in tokens:
            token_01 = re.sub(r'[Tt](rue|RUE)?', '1', token)
            token_01 = re.sub(r'[Ff](alse|ALSE)?', '0', token_01)

            _match = rgx.match(token_01)
            if not _match:
                self.rpars.setHaltingLevel(1)
                raise ParameterParseError(param,
                                          f'Invalid format in: {token!r}')
            repeats = int(_match['repeats'] or '1')
            value = int(_match['value'])
            tensor_output.extend((value,)*repeats)
        self.rpars.TENSOR_OUTPUT = tensor_output

    def interpret_theo_energies(self, assignment):
        """Assign parameter THEO_ENERGIES. Correct start if inappropriate."""
        param = 'THEO_ENERGIES'
        energies = assignment.values

        # (1) Single value input: treat as default if it is a single
        # underscore, otherwise as a request for a single energy
        if not assignment.other_values and assignment.value == '_':
            self.rpars.reset_default(param)
            return
        if not assignment.other_values:
            energies = (assignment.value, assignment.value, '1')

        # (2) Three values. Any can be an "_" meaning "default".
        # Internally, we store those as an rparams.NO_VALUE. The
        # others should be positive floats.
        theo_energies = self._parse_energy_range(assignment, energies)
        if len(theo_energies) != 3:
            raise ParameterNumberOfInputsError(param)

        try:
            self.rpars.THEO_ENERGIES = TheoEnergies(*theo_energies)
        except ValueError as exc:  # No TypeError, as all are floats
            self.rpars.setHaltingLevel(1)
            raise ParameterValueError(param, message=str(exc)) from None

        # Notify in case start was adjusted to fit step
        if not self.rpars.THEO_ENERGIES.is_equivalent(theo_energies):
            _LOGGER.info(
                f'{param} parameter: (Eto - Efrom) % Estep != 0, Efrom '
                f'was corrected to {self.rpars.THEO_ENERGIES.start}'
                )

    def interpret_tl_version(self, assignment):
        """Assign parameter TL_VERSION."""
        param = 'TL_VERSION'
        self._ensure_simple_assignment(assignment)
        version_str = assignment.value.lower()
        version_str = version_str.replace('v', '')
        if version_str in OLD_TL_VERSION_NAMES.keys():
            version_str = OLD_TL_VERSION_NAMES[version_str]
        try:
            self.rpars.TL_VERSION = Version(version_str)
        except ValueError:
            raise ParameterConversionError(param, version_str)

    def interpret_v0_real(self, assignment):
        """Assign parameter V0_REAL."""
        param = 'V0_REAL'
        v0r_type = assignment.value.lower()
        if v0r_type == 'rundgren':
            rundgren_constants = assignment.other_values
            if len(rundgren_constants) != 4:
                message = ('Rundgren-type function expects four '
                           'constants separated by whitespace')
                raise ParameterNumberOfInputsError(param, message=message)
            try:
                self.rpars.V0_REAL = [float(c) for c in rundgren_constants]
            except ValueError as exc:
                message = (f'Could not parse constants {rundgren_constants} '
                           'for Rundgren-type function')
                self.rpars.setHaltingLevel(1)
                raise ParameterFloatConversionError(param,
                                                    message=message) from exc
            return

        # Pass a specific function to FORTRAN, but replace
        # 'EE' with 'EEV+workfn' to get the energies right
        v0_real = re.sub('(?i)EE', 'EEV+workfn', assignment.values_str)
        self.rpars.V0_REAL = v0_real.rstrip()

    def interpret_vibr_amp_scale(self, assignment):
        """Store raw values: Requires sites for interpreting."""
        self.rpars.VIBR_AMP_SCALE.extend(assignment.values_str.split(','))

    def _interpret_wood_or_matrix(self, param, assignment):
        """Store a Wood- or matrix-notation parameter value."""
        if 'M' not in assignment.flags:
            self._ensure_no_flags_assignment(assignment)
            wood = self._read_woods_notation(param, assignment)
            setattr(self.rpars, param, wood)
        else:
            matrix = self._read_matrix_notation(param, assignment.values)
            setattr(self.rpars, param, matrix)

    # ------------------------ Helper methods -------------------------
    def _ensure_chemical_elements(self, param, elements):
        """Raise unless all entries are valid chemical elements."""
        if not elements:
            raise ParameterHasNoValueError(param)
        invalid = []
        for element in elements:
            try:
                _ = periodic_table.get_atomic_number(element)
            except ValueError:
                invalid.append(element)

        if invalid:
            message = f'Element(s) {invalid} not found in periodic table'
            self.rpars.setHaltingLevel(2)
            raise ParameterValueError(param, message=message)

    def _ensure_has_non_empty_slab(self, param):
        """Complain if self.slab is inappropriate for SITE_DEF."""
        if self.slab is None:
            self.rpars.setHaltingLevel(3)
            raise ParameterNeedsSlabError(param)
        if not self.slab.atlist:
            self.rpars.setHaltingLevel(3)
            raise ParameterError(param, 'Slab contains no atoms')

    def _ensure_valid_slab_element(self, param, element):
        """Raise unless element is one of the elements of self.slab."""
        self._ensure_has_non_empty_slab(param)
        known_elements = self.slab.elements
        if not known_elements:
            self.rpars.setHaltingLevel(3)
            raise ParameterError(param, 'Slab has no elements')
        if element not in known_elements:
            msg = (f'{element!r} is not a valid POSCAR element. '
                   'Known elements: ' + ', '.join(known_elements))
            self.rpars.setHaltingLevel(3)
            raise ParameterUnknownFlagError(param, message=msg)

    def _get_flag_value_from_pair(self, param, flag_value_pair):
        """Return lowercase flag and value from a pair, or complain."""
        try:
            flag, value = (s.strip()
                           for s in flag_value_pair.lower().strip().split())
        except ValueError:
            err_ = f'Expected "flag value" pairs, found "{flag_value_pair}"'
            self.rpars.setHaltingLevel(1)
            raise ParameterNumberOfInputsError(param, message=err_) from None
        return flag, value

    def _get_valid_slab_element_from_flag(self, param, assignment):
        """Return a valid POSCAR element from assignment; raise otherwise."""
        element = assignment.flag.capitalize()
        self._ensure_valid_slab_element(param, element)
        return element

    def _parse_energy_range(self, assignment, energies):
        """Return a list of floats for energies.

        Parameters
        ----------
        assignment : Assignment
            The assignment for the parameter. Used only for error
            reporting.
        energies : Sequence
            The energies that should be parsed. Elements are strings.
            They may also contain single underscore characters, which
            will be replaced with an Rparams.NO_VALUE.

        Returns
        -------
        float_energies : list
            The energies as a list of floats

        Raises
        ------
        ParameterFloatConversionError
            If conversion to float fails for some of the inputs
        ParameterParseError
            If other parsing errors occur (typically a malformed
            input that cannot be converted to a simple tuple of
            of numbers).
        """
        try:
            return EnergyRange.parse_string_sequence(energies)
        except (TypeError, ValueError) as exc:
            self.rpars.setHaltingLevel(1)
            new_exc = (ParameterParseError if isinstance(exc, ValueError)
                       else ParameterFloatConversionError)
            raise new_exc(assignment.parameter,
                          message=assignment.values_str) from exc

    def _parse_incidence_angles(self, param, assignment):                       # TODO: Issue #147
        """Return a dictionary with incidence angles from an assignment."""
        bounds = {'THETA': NumericBounds(type_=float, range_=(-90, 90),
                                         out_of_range_event='fail'),
                  'PHI': NumericBounds(type_=float, range_=(0, 360),
                                       out_of_range_event='modulo')}
        if ',' not in assignment.values_str and len(assignment.values) != 2:
            self.rpars.setHaltingLevel(1)
            raise ParameterNumberOfInputsError(
                parameter=param,
                found_and_expected=(len(assignment.values), 2)
                )
        right_side = assignment.values_str.upper()
        if ',' not in assignment.values_str:
            # pylint: disable=consider-using-f-string
            # disable: Better than repeating 'assignment.values' twice
            right_side = 'THETA {}, PHI {}'.format(*assignment.values)
        self._check_n_incidence_angles_ok(param, right_side)

        angles_specs = (spec.strip().split()
                        for spec in right_side.strip().split(','))
        angles = {}
        for name, *values in angles_specs:
            if name not in ('THETA', 'PHI'):
                self.rpars.setHaltingLevel(1)
                raise ParameterUnknownFlagError(param, name)
            if len(values) != 1:
                self.rpars.setHaltingLevel(1)
                raise ParameterNumberOfInputsError(
                    parameter=param,
                    message=f'Found {len(values)} values for angle {name}'
                    )
            angles[name] = self.interpret_numerical_parameter(
                Assignment(values[0], param),
                param=f'{param} {name}',
                bounds=bounds[name],
                return_only=True
                )

        # Check for negative theta and adjust phi
        if angles['THETA'] < 0:
            angles['THETA'] = abs(angles['THETA'])
            angles['PHI'] = (angles['PHI'] + 180) % 360
        return angles

    def _check_n_incidence_angles_ok(self, param, right_side):
        """Complain if right_side contains the wrong number of angles."""
        nr_angle_specs = len(right_side.split(','))
        if nr_angle_specs != 2:
            self.rpars.setHaltingLevel(1)
            raise ParameterNumberOfInputsError(
                parameter=param, found_and_expected=(nr_angle_specs, 2)
                )
        if (right_side.count('THETA'), right_side.count('PHI')) != (1, 1):
            self.rpars.setHaltingLevel(1)
            raise ParameterNumberOfInputsError(
                parameter=param,
                message='Expected exactly one entry for THETA and one for PHI'
                )

    def _read_woods_notation(self, param, assignment):
        """Return a Woods notation from an Assignment, if slab exists."""
        if self.slab is None:
            message = (f'{param} parameter appears to be in Wood '
                       'notation but no slab was passed. Cannot '
                       'calculate bulk unit cell!')
            self.rpars.setHaltingLevel(2)
            raise ParameterNeedsSlabError(param, message)
        return readWoodsNotation(assignment.values_str, self.slab.ucell)

    def _read_matrix_notation(self, param, values):
        """Try interpreting values as a 2x2 matrix."""
        matrix = splitSublists(values, ',')
        if len(matrix) != 2:
            message = 'Number of lines in matrix is not equal to 2'
            self.rpars.setHaltingLevel(2)
            raise ParameterParseError(param, message)
        if any(len(row) != 2 for row in matrix):
            message = 'Number of columns in matrix is not equal to 2'
            self.rpars.setHaltingLevel(2)
            raise ParameterParseError(param, message)
        try:
            return np.array(matrix).astype(float)
        except ValueError:
            self.rpars.setHaltingLevel(1)
            raise ParameterFloatConversionError(param, matrix) from None
