Source code for cantools.database.conversion

from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Dict, Optional, Union

from ..typechecking import Choices, SignalValueType

if TYPE_CHECKING:
    from .namedsignalvalue import NamedSignalValue


[docs]class BaseConversion(ABC): """The BaseConversion class defines the interface for all signal conversion classes.""" #: the scaling factor of the conversion scale: float #: the offset value of the conversion offset: float #: ``True`` if the raw/internal value is a floating datatype #: ``False`` if it is an integer datatype is_float: bool #: an optional mapping of raw values to their corresponding text value choices: Optional[Choices]
[docs] @staticmethod def factory( scale: float = 1, offset: float = 0, choices: Optional[Choices] = None, is_float: bool = False, ) -> "BaseConversion": """Factory method that returns an instance of a conversion subclass based on the given parameters. :param scale: The scale factor to use for the conversion. :param offset: The offset to use for the conversion. :param choices: A dictionary of named signal choices, mapping raw values to string labels. :param is_float: A boolean flag indicating whether the raw value is a float or an integer. :returns: An instance of a conversion subclass, either an `IdentityConversion`, a `LinearIntegerConversion`, a `LinearConversion`or a `NamedSignalConversion`. :raises TypeError: If the given parameters are of the wrong type. """ if choices is None: if scale == 1 and offset == 0: return IdentityConversion(is_float=is_float) if _is_integer(scale) and _is_integer(offset) and not is_float: return LinearIntegerConversion(scale=int(scale), offset=int(offset)) return LinearConversion( scale=scale, offset=offset, is_float=is_float, ) return NamedSignalConversion( scale=scale, offset=offset, choices=choices, is_float=is_float )
[docs] @abstractmethod def raw_to_scaled( self, raw_value: Union[int, float], decode_choices: bool = True, ) -> SignalValueType: """Convert an internal raw value according to the defined scaling or value table. :param raw_value: The raw value :param decode_choices: If `decode_choices` is ``False`` scaled values are not converted to choice strings (if available). :return: The calculated scaled value """ raise NotImplementedError
[docs] @abstractmethod def scaled_to_raw(self, scaled_value: SignalValueType) -> Union[int, float]: """Convert a scaled value to the internal raw value. :param scaled_value: The scaled value. :return: The internal raw value. """ raise NotImplementedError
[docs] @abstractmethod def numeric_scaled_to_raw( self, scaled_value: Union[int, float] ) -> Union[int, float]: """Convert a numeric scaled value to the internal raw value. :param scaled_value: The numeric scaled value. :return: The internal raw value. """ raise NotImplementedError
def choice_to_number(self, choice: Union[str, "NamedSignalValue"]) -> int: raise KeyError @abstractmethod def __repr__(self) -> str: raise NotImplementedError
class IdentityConversion(BaseConversion): scale = 1 offset = 0 choices = None def __init__(self, is_float: bool) -> None: self.is_float = is_float def raw_to_scaled( self, raw_value: Union[int, float], decode_choices: bool = True, ) -> Union[int, float]: return raw_value def scaled_to_raw(self, scaled_value: SignalValueType) -> Union[int, float]: if not isinstance(scaled_value, (int, float)): raise TypeError( f"'scaled_value' must have type 'int' or 'float' (is {type(scaled_value)})" ) return self.numeric_scaled_to_raw(scaled_value) def numeric_scaled_to_raw( self, scaled_value: Union[int, float] ) -> Union[int, float]: return scaled_value if self.is_float else round(scaled_value) def __repr__(self) -> str: return f"{self.__class__.__name__}(is_float={self.is_float})" class LinearIntegerConversion(BaseConversion): is_float = False choices = None def __init__(self, scale: int, offset: int) -> None: self.scale: int = scale self.offset: int = offset def raw_to_scaled( self, raw_value: Union[int, float], decode_choices: bool = True, ) -> SignalValueType: return raw_value * self.scale + self.offset def scaled_to_raw(self, scaled_value: SignalValueType) -> Union[int, float]: if not isinstance(scaled_value, (int, float)): raise TypeError( f"'scaled_value' must have type 'int' or 'float' (is {type(scaled_value)})" ) return self.numeric_scaled_to_raw(scaled_value) def numeric_scaled_to_raw( self, scaled_value: Union[int, float] ) -> Union[int, float]: # try to avoid a loss of precision whenever possible _raw = scaled_value - self.offset quotient, remainder = divmod(_raw, self.scale) if remainder == 0: _raw = quotient else: _raw /= self.scale return round(_raw) def __repr__(self) -> str: return f"{self.__class__.__name__}(scale={self.scale}, offset={self.offset})" class LinearConversion(BaseConversion): choices = None def __init__(self, scale: float, offset: float, is_float: bool) -> None: self.scale = scale self.offset = offset self.is_float = is_float def raw_to_scaled( self, raw_value: Union[int, float], decode_choices: bool = True, ) -> SignalValueType: return raw_value * self.scale + self.offset def scaled_to_raw(self, scaled_value: SignalValueType) -> Union[int, float]: if not isinstance(scaled_value, (int, float)): raise TypeError( f"'scaled_value' must have type 'int' or 'float' (is {type(scaled_value)})" ) return self.numeric_scaled_to_raw(scaled_value) def numeric_scaled_to_raw( self, scaled_value: Union[int, float] ) -> Union[int, float]: _raw = (scaled_value - self.offset) / self.scale return _raw if self.is_float else round(_raw) def __repr__(self) -> str: return ( f"{self.__class__.__name__}(" f"scale={self.scale}, " f"offset={self.offset}, " f"is_float={self.is_float})" ) class NamedSignalConversion(BaseConversion): def __init__( self, scale: float, offset: float, choices: Choices, is_float: bool ) -> None: self.scale = scale self.offset = offset self.is_float = is_float self._inverse_choices: Dict[str, int] = {} self.choices: Choices = choices self._update_choices() self._conversion = BaseConversion.factory( scale=self.scale, offset=self.offset, choices=None, is_float=is_float, ) # monkeypatch method to avoid unnecessary function call self.numeric_scaled_to_raw = self._conversion.numeric_scaled_to_raw # type: ignore[method-assign] def raw_to_scaled( self, raw_value: Union[int, float], decode_choices: bool = True, ) -> SignalValueType: if decode_choices and (choice := self.choices.get(raw_value)) is not None: # type: ignore[arg-type] return choice return self._conversion.raw_to_scaled(raw_value, False) def scaled_to_raw(self, scaled_value: SignalValueType) -> Union[int, float]: if isinstance(scaled_value, (int, float)): return self._conversion.scaled_to_raw(scaled_value) if hasattr(scaled_value, "value"): # scaled_value is NamedSignalValue return scaled_value.value if isinstance(scaled_value, str): return self.choice_to_number(scaled_value) raise TypeError def numeric_scaled_to_raw( self, scaled_value: Union[int, float] ) -> Union[int, float]: return self._conversion.scaled_to_raw(scaled_value) def set_choices(self, choices: Choices) -> None: self.choices = choices self._update_choices() def _update_choices(self) -> None: # we simply assume that the choices are invertible self._inverse_choices = {str(x[1]): x[0] for x in self.choices.items()} def choice_to_number(self, choice: Union[str, "NamedSignalValue"]) -> int: return self._inverse_choices[str(choice)] def __repr__(self) -> str: list_of_choices = ", ".join( [f"{value}: '{text}'" for value, text in self.choices.items()] ) choices = f"{{{list_of_choices}}}" return ( f"{self.__class__.__name__}(" f"scale={self.scale}, " f"offset={self.offset}, " f"choices={choices}, " f"is_float={self.is_float})" ) def _is_integer(value: Union[int, float]) -> bool: if isinstance(value, int) or (hasattr(value, "is_integer") and value.is_integer()): return True elif isinstance(value, float): return False err_msg = f"`value` must be of type `int` or `float`, is {type(value)}" raise TypeError(err_msg)