Source code for autojob.calculation.parameters

"""Define calculator parameters."""

from __future__ import annotations

from collections.abc import Iterable
from collections.abc import Mapping
from collections.abc import Sequence
from copy import copy
from copy import deepcopy
from enum import Enum
import math
from typing import Any

from monty.json import MSONable


[docs] class CalculatorType(Enum): """A type of ASE calculator.""" ABINIT = "abinit" ACE = "ace" AIMS = "aims" AMBER = "amber" ASAP = "asap" CASTEP = "castep" CP2K = "cp2k" CRYSTAL = "crystal" DEMON = "demon" DEMON_NANO = "demonnano" DFTB = "dftb" DFTD3 = "dftd3" DMOL3 = "dmol" EAM = "eam" ELK = "elk" EMT = "emt" ESPRESSO = "espresso" EXCITING = "exciting" FORCE_FIELD = "ff" FLEUR = "fleur" GAMESS_US = "gamess_us" GAUSSIAN = "gaussian" GPAW = "gpaw" GROMACS = "gromacs" GULP = "gulp" HOTBIT = "hotbit" KIM = "kim" LAMMPS = "lammpsrun" LAMMPS_LIB = "lammpslib" LENNARD_JONES = "lj" MOPAC = "mopac" MORSE_POTENTIAL = "morse" NWCHEM = "nwchem" OCTOPUS = "octopus" ONETEP = "onetep" OPENMX = "openmx" ORCA = "orca" PSI4 = "psi4" QCHEM = "qchem" SIESTA = "siesta" TIP3P = "tip3p" TIP4P = "tip4p" TURBOMOLE = "turbomole" VASP = "vasp" DEFAULT = "vasp" # noqa: PIE796 def __str__(self) -> str: """Get a string representation of a `CalculatorType`.""" return self.value
[docs] def is_implemented(self) -> bool: """Return whether the `CalculatorType` is implemented or not.""" implemented_calculator_types = [ CalculatorType.VASP, CalculatorType.GAUSSIAN, CalculatorType.ESPRESSO, ] return self in implemented_calculator_types
# ! Remove all NumberRange and CalculatorParameters etc. # ! Use Pydantic.Field instead
[docs] class NumberRange(MSONable): """A range of numbers.""" def __init__( self, *, lower_bound: int | float = -math.inf, lower_bound_exclusive: bool = True, upper_bound: int | float = math.inf, upper_bound_exclusive: bool = True, ) -> None: """Initialize a `NumberRange`. Args: lower_bound: The lower bound for the range. lower_bound_exclusive: Whether the lower bound is excluded from the range. upper_bound: The upper bound for the range. upper_bound_exclusive: Whether the upper bound is excluded from the range. """ if math.nan in (lower_bound, upper_bound): msg = "Neither bound can be NaN" raise ValueError(msg) if lower_bound == math.inf: msg = "Lower bound must be less than positive infinity" raise ValueError(msg) if upper_bound == -math.inf: msg = "Upper bound must be greater than negative infinity" raise ValueError(msg) if lower_bound > upper_bound: msg = "Lower bound must be less than upper bound" raise ValueError(msg) self.lower_bound = lower_bound self.lower_bound_exclusive = lower_bound_exclusive self.upper_bound = upper_bound self.upper_bound_exclusive = upper_bound_exclusive def __repr__(self) -> str: """A string representation of the number range.""" return ( f"NumberRange(lower_bound={self.lower_bound}, " f"lower_bound_exclusive={self.lower_bound_exclusive}, " f"upper_bound={self.upper_bound}, " f"upper_bound_exclusive={self.upper_bound_exclusive})" ) def __eq__(self, __o: object) -> bool: """Determine if both bounds and their exclusivity are equal.""" if not isinstance(__o, NumberRange): return False return False not in ( __o.lower_bound == self.lower_bound, __o.lower_bound_exclusive == self.lower_bound_exclusive, __o.upper_bound == self.upper_bound, __o.upper_bound_exclusive == self.upper_bound_exclusive, )
[docs] def validate_number(self, number: int | float) -> bool: """Returns True if a number is within the range. False, otherwise. Args: number: A number to check. """ if number < self.lower_bound or number > self.upper_bound: return False if number == self.lower_bound and self.lower_bound_exclusive: return False return number != self.upper_bound or self.upper_bound_exclusive
# TODO: design method for checking compatibility and necessity of certain # input parameters # e.g., dipole-related parameters should either all be on or off # e.g., relation between NBANDS, NCORE, NPAR, KPAR, NELC, and NIONS
[docs] class CalculatorParameter(MSONable): """Abstraction of an calculator parameter for a supported ASE calculator. Attributes: name (str; defaults to ''): The name of the CalculatorParameter. allowed_types (Iterable[Type]; defaults to (Any,)): The allowed types for the CalculatorParameter. For validation and displaying (in the GUI application) purposes, if there exists special values for the CalculatorParameter (e.g., string values that correspond to particular values), the allowed types should not be designated so as to include these special values. For example, say that a particular CalculatorParameter accepts integer values, but that the string 'normal' corresponds to a particular value. The allowed types for the CalculatorParameter should be specified as (int,) and not (int, str). special_values (Iterable; defaults to tuple()): A tuple indicating the special values of a CalculatorParameter. These may be values whose types do not conform to the types specified in the attribute 'allowed_types'. _default (Any; defaults to None): A default value for the CalculatorParameter. description (str; defaults to ''): Returns a description of the CalculatorParameter to be used for displaying tooltips. """ def __init__( self, name: str = "", allowed_types: tuple[type] = (Any,), special_values: Iterable | None = None, default: Any = None, description: str = "", ): """Initialize a `CalculatorParameter`. Args: name: The name of the `CalculatorParameter`. Defaults to "". allowed_types: An iterable of the allowed types for the parameter. Defaults to (Any,). special_values: An iterable of the special values for the parameter. Defaults to None. default: The default value for the parameter. Defaults to None. description: A description of the parameter. Defaults to "". """ self.name = name self._allowed_types: tuple[type] = tuple(allowed_types) self._allowed_types: tuple[type] = tuple(allowed_types) self._special_values = tuple(special_values) if special_values else () self._special_values = tuple(special_values) if special_values else () self._default = default self.description = description def __eq__(self, __o: object) -> bool: """Determine if two parameters are equal.""" if not isinstance(__o, CalculatorParameter): return False return False not in ( __o.name == self.name, __o._allowed_types == self._allowed_types, __o._special_values == self._special_values, __o._default == self._default, __o.description == self.description, ) def __repr__(self) -> str: """Get a string representation of the parameter.""" return ( f"CalculatorParameter(name={self.name}, " f"allowed_types={self._allowed_types!r}, " f"special_values={self._special_values!r}, " f"default={self._default!r}, description={self.description})" ) def __hash__(self) -> int: """Return a hash of the string representation.""" return hash(repr(self)) @property def allowed_types(self) -> tuple: """The allowed types for the parameter.""" return copy(self._allowed_types) @property def special_values(self) -> tuple: """The special values of the parameter.""" return deepcopy(self._special_values) @property def default(self) -> Any: """The default value of the parameter.""" return deepcopy(self._default) def __str__(self) -> str: """The parameter name.""" return self.name
[docs] def validate(self, val: Any) -> bool: """Validate a parameter value.""" if val in self._special_values: return True return self._allowed_types is not None and isinstance( val, self._allowed_types )
[docs] class NumberParameter(CalculatorParameter): """A parameter that can be a number.""" __hash__ = CalculatorParameter.__hash__ def __init__( self, *, name: str = "", allow_floats: bool = False, special_values: Iterable[type] | None = None, default: Any = None, description: str = "", number_range: NumberRange = None, ) -> None: """Initialize a `NumberParameter`. Args: name: The name of the `NumberParameter`. Defaults to "". allow_floats: Whether to allow floats. Defaults to False. special_values: An iterable of the special values for the parameter. Defaults to None. default: The default value for the parameter. Defaults to None. description: A description of the parameter. Defaults to "". number_range: A `NumberRange` to use to limit the parameter. Defaults to None. """ self._number_range = number_range or NumberRange() allowed_types = (float, int) if allow_floats else (int,) super().__init__( name=name, allowed_types=allowed_types, special_values=special_values, default=default, description=description, ) def __eq__(self, __o: object) -> bool: """Determine if two parameters are equal.""" if not super().__eq__(__o): return False return __o._number_range == self._number_range def __repr__(self) -> str: """Get a string representation of the parameter.""" return ( f"NumberParameter(name={self.name}, " f"allowed_floats={float in self._allowed_types}, " f"special_values={self._special_values!r}, " f"default={self._default!r}, " f"description={self.description}, " f"number_range={self._number_range!r})" )
[docs] def as_dict(self) -> dict: """Return the `NumberParameter` as a dictionary.""" return { "name": self.name, "special_values": self._special_values, "default": self.default, "description": self.description, "_number_range": self._number_range.as_dict(), "allowed_types": self._allowed_types, "@class": self.__class__.__name__, "@module": self.__class__.__module__, }
[docs] @classmethod def from_dict(cls, d) -> NumberParameter: """Initiate a `NumberParameter` from a dictionary.""" return cls( name=d["name"], allow_floats=float in d["allowed_types"], special_values=d["special_values"], default=d["default"], description=d["description"], number_range=NumberRange.from_dict(d["_number_range"]), )
@property def number_range(self) -> NumberRange: """The number range of the parameter.""" return copy(self._number_range)
[docs] def validate(self, val: Any) -> bool: """Validate a value.""" if val in self._special_values: return True if not isinstance(val, self._allowed_types): return False if self._number_range is None: return True return self._number_range.validate_number(val)
[docs] class SequenceParameter(CalculatorParameter): """A parameter that can be a sequence.""" __hash__ = CalculatorParameter.__hash__ def __init__( self, member_types: Iterable[type], name: str = "", special_values: Iterable | None = None, default: Any = None, description: str = "", ): """Initialize a `SequenceParameter`. Args: name: The name of the `SequenceParameter`. Defaults to "". member_types: The allowed types of the items in the sequence. special_values: An iterable of the special values for the parameter. Defaults to None. default: The default value for the parameter. Defaults to None. description: A description of the parameter. Defaults to "". """ self._member_types = tuple(member_types) super().__init__( name=name, allowed_types=(Sequence,), special_values=special_values, default=default, description=description, ) def __eq__(self, __o: object) -> bool: """Determine if two parameters are equal.""" if not super().__eq__(__o): return False return __o._member_types == self._member_types def __repr__(self) -> str: """Get a string representation of the parameter.""" return ( f"SequenceParameter(member_types={self.member_types!r}, " f"name={self.name}, allowed_types={self._allowed_types!r}, " f"special_values={self._special_values!r}, " f"default={self._default!r}, description={self.description})" ) @property def member_types(self) -> Iterable: """The allowed member types of the parameter.""" return copy(self._member_types)
[docs] def as_dict(self) -> dict: """Return the `SequenceParameter` as a dictionary.""" return { "name": self.name, "special_values": self._special_values, "member_types": self.member_types, "default": self.default, "description": self.description, "allowed_types": self._allowed_types, "@class": self.__class__.__name__, "@module": self.__class__.__module__, }
[docs] @classmethod def from_dict(cls, d) -> SequenceParameter: """Initiate a `SequenceParameter` from a dictionary.""" return cls( member_types=d["member_types"], name=d["name"], special_values=d["special_values"], default=d["default"], description=d["description"], )
[docs] def validate(self, val: Any) -> bool: """Validate a value.""" if val in self._special_values: return True if not isinstance(val, self._allowed_types) or isinstance(val, str): return False return all(isinstance(x, self.member_types) for x in val)
[docs] class NumberSequenceParameter(SequenceParameter): """A parameter that can be a sequence of numbers.""" __hash__ = SequenceParameter.__hash__ def __init__( self, *, name: str = "", allow_floats: bool = False, special_values: Iterable | None = None, default: Any = None, description: str = "", number_range: NumberRange = None, ): """Initialize a `NumberSequenceParameter`. Args: name: The name of the `NumberSequenceParameter`. Defaults to "". allow_floats: Whether to allow floats. Defaults to False. special_values: An iterable of the special values for the parameter. Defaults to None. default: The default value for the parameter. Defaults to None. description: A description of the parameter. Defaults to "". number_range: A `NumberRange` to use to limit the parameter. Defaults to None. """ self._number_range = number_range or NumberRange() member_types = (float, int) if allow_floats else (int,) super().__init__( name=name, member_types=member_types, special_values=special_values, default=default, description=description, ) def __eq__(self, __o: object) -> bool: """Determine if two parameters are equal.""" if not super().__eq__(__o): return False return __o._number_range == self._number_range def __repr__(self) -> str: """Get a string representation of the parameter.""" return ( f"NumberSequenceParameter(name={self.name}, " f"allow_floats={float in self.member_types}, " f"special_values={self._special_values}, " f"default={self._default!r}, description={self.description}, " f"number_range={self._number_range!r})" )
[docs] def as_dict(self) -> dict: """Return the `NumberSequenceParameter` as a dictionary.""" d = super().as_dict() del d["member_types"] d["allow_floats"] = float in self.member_types d["_number_range"] = self._number_range.as_dict() return d
[docs] @classmethod def from_dict(cls, d) -> NumberSequenceParameter: """Initiate a `SequenceParameter` from a dictionary.""" return cls( name=d["name"], allow_floats=d["allow_floats"], special_values=d["special_values"], default=d["default"], description=d["description"], number_range=NumberRange.from_dict(d["_number_range"]), )
[docs] def validate(self, val: Any) -> bool: """Validate a value.""" if val in self._special_values: return True if not isinstance(val, self._allowed_types) or isinstance(val, str): return False for x in val: if not isinstance(x, self.member_types): return False if not self._number_range.validate_number(x): return False return True
[docs] class MappingParameter(CalculatorParameter): """A parameter that can be a mapping.""" __hash__ = CalculatorParameter.__hash__ def __init__( self, *, member_types: Iterable[type], name: str = "", special_values: Iterable | None = None, default: Any = None, description: str = "", ): """Initialize a `MappingParameter`. Args: name: The name of the `MappingParameter`. Defaults to "". member_types: The allowed types of the items in the sequence. special_values: An iterable of the special values for the parameter. Defaults to None. default: The default value for the parameter. Defaults to None. description: A description of the parameter. Defaults to "". """ self._member_types = tuple(member_types) super().__init__( name=name, allowed_types=(Mapping,), special_values=special_values, default=default, description=description, ) def __eq__(self, __o: object) -> bool: """Determine if two parameters are equal.""" if not super().__eq__(__o): return False return __o._member_types == self._member_types def __repr__(self) -> str: """Get a string representation of the parameter.""" return ( f"MappingParameter(name={self.name}, " f"member_types={self.member_types!r}, " f"special_values={self._special_values!r}, " f"default={self._default!r}, " f"description={self.description})" ) @property def member_types(self) -> Iterable: """The allowed member types of the parameter.""" return copy(self._member_types)
[docs] def as_dict(self) -> dict: """Return the `MappingParameter` as a dictionary.""" return { "member_types": self.member_types, "name": self.name, "special_values": self._special_values, "default": self.default, "description": self.description, "@class": self.__class__.__name__, "@module": self.__class__.__module__, }
[docs] @classmethod def from_dict(cls, d) -> MappingParameter: """Initiate a `MappingParameter` from a dictionary.""" return cls( member_types=d["member_types"], name=d["name"], special_values=d["special_values"], default=d["default"], description=d["description"], )
[docs] def validate(self, val: Any) -> bool: """Validate a value.""" if val in self._special_values: return True if not isinstance(val, self._allowed_types): return False return all(isinstance(x, self.member_types) for x in val.values())
[docs] class NumberMappingParameter(MappingParameter): """A parameter that can be a mapping to numbers.""" __hash__ = MappingParameter.__hash__ def __init__( self, *, name: str = "", allow_floats: bool = False, special_values: Iterable | None = None, default: Any = None, description: str = "", number_range: NumberRange = None, ): """Initialize a `NumberMappingParameter`. Args: name: The name of the `NumberMappingParameter`. Defaults to "". allow_floats: Whether to allow floats. Defaults to False. special_values: An iterable of the special values for the parameter. Defaults to None. default: The default value for the parameter. Defaults to None. description: A description of the parameter. Defaults to "". number_range: A `NumberRange` to use to limit the parameter. Defaults to None. """ self._number_range = number_range member_types = (float, int) if allow_floats else (int,) super().__init__( name=name, member_types=member_types, special_values=special_values, default=default, description=description, ) def __eq__(self, __o: object) -> bool: """Determine if two parameters are equal.""" if not super().__eq__(__o): return False return __o._number_range == self._number_range def __repr__(self) -> str: """Get a string representation of the parameter.""" return ( f"NumberMappingParameter(name={self.name}, " f"allow_floats=({float in self.member_types}, " f"special_values={self._special_values!r}, " f"default={self._default!r}, " f"description={self.description}, " f"number_range={self._number_range!r}))" )
[docs] def as_dict(self) -> dict: """Return the `NumberMappingParameter` as a dictionary.""" d = super().as_dict() del d["member_types"] d["allow_floats"] = float in self.member_types d["_number_range"] = self._number_range.as_dict() return d
[docs] @classmethod def from_dict(cls, d) -> NumberMappingParameter: """Initiate a `NumberMappingParameter` from a dictionary.""" return cls( name=d["name"], allow_floats=d["allow_floats"], special_values=d["special_values"], default=d["default"], description=d["description"], number_range=NumberRange.from_dict(d["_number_range"]), )
[docs] def validate(self, val: Any) -> bool: """Validate a value.""" if val in self._special_values: return True if not isinstance(val, self._allowed_types): return False if self._number_range is None: return True for x in val.values(): if not isinstance(x, self.member_types): return False if not self._number_range.validate_number(x): return False return True