diff --git a/.gitignore b/.gitignore index d6ed2f08..acfa5bb1 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,16 @@ __pycache__/ env venv /odxtools/version.py + +# editor and git backup files +*~ +*.orig +*.rej + +# files usually stemming from deflated PDX archives +*.odx-d +*.odx-c +*.odx-cs +*.jar +index.xml +!examples/data/* diff --git a/odxtools/compumethods/compucodecompumethod.py b/odxtools/compumethods/compucodecompumethod.py new file mode 100644 index 00000000..23beb978 --- /dev/null +++ b/odxtools/compumethods/compucodecompumethod.py @@ -0,0 +1,63 @@ +# SPDX-License-Identifier: MIT +from dataclasses import dataclass +from typing import List, Optional, cast +from xml.etree import ElementTree + +from ..exceptions import DecodeError, EncodeError, odxassert, odxraise +from ..odxlink import OdxDocFragment +from ..odxtypes import AtomicOdxType, DataType +from ..progcode import ProgCode +from ..utils import dataclass_fields_asdict +from .compumethod import CompuCategory, CompuMethod + + +@dataclass +class CompuCodeCompuMethod(CompuMethod): + """A compu method specifies the tranfer functions using Java bytecode + + For details, refer to ASAM specification MCD-2 D (ODX), section 7.3.6.6.9. + """ + + @property + def internal_to_phys_code(self) -> Optional[ProgCode]: + if self.compu_internal_to_phys is None: + return None + + return self.compu_internal_to_phys.prog_code + + @property + def phys_to_internal_code(self) -> Optional[ProgCode]: + if self.compu_phys_to_internal is None: + return None + + return self.compu_phys_to_internal.prog_code + + @staticmethod + def compu_method_from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment], *, + internal_type: DataType, + physical_type: DataType) -> "CompuCodeCompuMethod": + cm = CompuMethod.compu_method_from_et( + et_element, doc_frags, internal_type=internal_type, physical_type=physical_type) + kwargs = dataclass_fields_asdict(cm) + + return CompuCodeCompuMethod(**kwargs) + + def __post_init__(self) -> None: + odxassert(self.category == CompuCategory.COMPUCODE, + "CompuCodeCompuMethod must exhibit COMPUCODE category") + + def convert_internal_to_physical(self, internal_value: AtomicOdxType) -> AtomicOdxType: + odxraise(r"CompuCodeCompuMethod cannot be executed", DecodeError) + return cast(AtomicOdxType, None) + + def convert_physical_to_internal(self, physical_value: AtomicOdxType) -> AtomicOdxType: + odxraise(r"CompuCodeCompuMethod cannot be executed", EncodeError) + return cast(AtomicOdxType, None) + + def is_valid_physical_value(self, physical_value: AtomicOdxType) -> bool: + odxraise(r"CompuCodeCompuMethod cannot be executed", NotImplementedError) + return False + + def is_valid_internal_value(self, internal_value: AtomicOdxType) -> bool: + odxraise(r"CompuCodeCompuMethod cannot be executed", NotImplementedError) + return False diff --git a/odxtools/compumethods/compuinternaltophys.py b/odxtools/compumethods/compuinternaltophys.py index ef76b699..c7786113 100644 --- a/odxtools/compumethods/compuinternaltophys.py +++ b/odxtools/compumethods/compuinternaltophys.py @@ -1,11 +1,12 @@ # SPDX-License-Identifier: MIT from dataclasses import dataclass -from typing import List, Optional +from typing import Any, Dict, List, Optional from xml.etree import ElementTree -from ..odxlink import OdxDocFragment +from ..odxlink import OdxDocFragment, OdxLinkDatabase, OdxLinkId from ..odxtypes import DataType from ..progcode import ProgCode +from ..snrefcontext import SnRefContext from .compudefaultvalue import CompuDefaultValue from .compuscale import CompuScale @@ -37,3 +38,19 @@ def compu_internal_to_phys_from_et(et_element: ElementTree.Element, return CompuInternalToPhys( compu_scales=compu_scales, prog_code=prog_code, compu_default_value=compu_default_value) + + def _build_odxlinks(self) -> Dict[OdxLinkId, Any]: + result = {} + + if self.prog_code is not None: + result.update(self.prog_code._build_odxlinks()) + + return result + + def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None: + if self.prog_code is not None: + self.prog_code._resolve_odxlinks(odxlinks) + + def _resolve_snrefs(self, context: SnRefContext) -> None: + if self.prog_code is not None: + self.prog_code._resolve_snrefs(context) diff --git a/odxtools/compumethods/compumethod.py b/odxtools/compumethods/compumethod.py index 88f50f92..182043a6 100644 --- a/odxtools/compumethods/compumethod.py +++ b/odxtools/compumethods/compumethod.py @@ -1,12 +1,13 @@ # SPDX-License-Identifier: MIT from dataclasses import dataclass from enum import Enum -from typing import List, Optional +from typing import Any, Dict, List, Optional from xml.etree import ElementTree from ..exceptions import odxraise -from ..odxlink import OdxDocFragment +from ..odxlink import OdxDocFragment, OdxLinkDatabase, OdxLinkId from ..odxtypes import AtomicOdxType, DataType +from ..snrefcontext import SnRefContext from .compuinternaltophys import CompuInternalToPhys from .compuphystointernal import CompuPhysToInternal @@ -77,6 +78,31 @@ def compu_method_from_et(et_element: ElementTree.Element, doc_frags: List[OdxDoc physical_type=physical_type, internal_type=internal_type) + def _build_odxlinks(self) -> Dict[OdxLinkId, Any]: + result = {} + + if self.compu_internal_to_phys is not None: + result.update(self.compu_internal_to_phys._build_odxlinks()) + + if self.compu_phys_to_internal is not None: + result.update(self.compu_phys_to_internal._build_odxlinks()) + + return result + + def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None: + if self.compu_internal_to_phys is not None: + self.compu_internal_to_phys._resolve_odxlinks(odxlinks) + + if self.compu_phys_to_internal is not None: + self.compu_phys_to_internal._resolve_odxlinks(odxlinks) + + def _resolve_snrefs(self, context: SnRefContext) -> None: + if self.compu_internal_to_phys is not None: + self.compu_internal_to_phys._resolve_snrefs(context) + + if self.compu_phys_to_internal is not None: + self.compu_phys_to_internal._resolve_snrefs(context) + def convert_physical_to_internal(self, physical_value: AtomicOdxType) -> AtomicOdxType: raise NotImplementedError() diff --git a/odxtools/compumethods/compuphystointernal.py b/odxtools/compumethods/compuphystointernal.py index 97011680..aa01c40b 100644 --- a/odxtools/compumethods/compuphystointernal.py +++ b/odxtools/compumethods/compuphystointernal.py @@ -1,11 +1,12 @@ # SPDX-License-Identifier: MIT from dataclasses import dataclass -from typing import List, Optional +from typing import Any, Dict, List, Optional from xml.etree import ElementTree -from ..odxlink import OdxDocFragment +from ..odxlink import OdxDocFragment, OdxLinkDatabase, OdxLinkId from ..odxtypes import DataType from ..progcode import ProgCode +from ..snrefcontext import SnRefContext from .compudefaultvalue import CompuDefaultValue from .compuscale import CompuScale @@ -37,3 +38,19 @@ def compu_phys_to_internal_from_et(et_element: ElementTree.Element, return CompuPhysToInternal( compu_scales=compu_scales, prog_code=prog_code, compu_default_value=compu_default_value) + + def _build_odxlinks(self) -> Dict[OdxLinkId, Any]: + result = {} + + if self.prog_code is not None: + result.update(self.prog_code._build_odxlinks()) + + return result + + def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None: + if self.prog_code is not None: + self.prog_code._resolve_odxlinks(odxlinks) + + def _resolve_snrefs(self, context: SnRefContext) -> None: + if self.prog_code is not None: + self.prog_code._resolve_snrefs(context) diff --git a/odxtools/compumethods/createanycompumethod.py b/odxtools/compumethods/createanycompumethod.py index af758b20..dcc009b4 100644 --- a/odxtools/compumethods/createanycompumethod.py +++ b/odxtools/compumethods/createanycompumethod.py @@ -5,10 +5,13 @@ from ..exceptions import odxraise, odxrequire from ..odxlink import OdxDocFragment from ..odxtypes import DataType +from .compucodecompumethod import CompuCodeCompuMethod from .compumethod import CompuMethod from .identicalcompumethod import IdenticalCompuMethod from .linearcompumethod import LinearCompuMethod +from .ratfunccompumethod import RatFuncCompuMethod from .scalelinearcompumethod import ScaleLinearCompuMethod +from .scaleratfunccompumethod import ScaleRatFuncCompuMethod from .tabintpcompumethod import TabIntpCompuMethod from .texttablecompumethod import TexttableCompuMethod @@ -27,9 +30,18 @@ def create_any_compu_method_from_et(et_element: ElementTree.Element, elif compu_category == "SCALE-LINEAR": return ScaleLinearCompuMethod.compu_method_from_et( et_element, doc_frags, internal_type=internal_type, physical_type=physical_type) + elif compu_category == "RAT-FUNC": + return RatFuncCompuMethod.compu_method_from_et( + et_element, doc_frags, internal_type=internal_type, physical_type=physical_type) + elif compu_category == "SCALE-RAT-FUNC": + return ScaleRatFuncCompuMethod.compu_method_from_et( + et_element, doc_frags, internal_type=internal_type, physical_type=physical_type) elif compu_category == "TEXTTABLE": return TexttableCompuMethod.compu_method_from_et( et_element, doc_frags, internal_type=internal_type, physical_type=physical_type) + elif compu_category == "COMPUCODE": + return CompuCodeCompuMethod.compu_method_from_et( + et_element, doc_frags, internal_type=internal_type, physical_type=physical_type) elif compu_category == "TAB-INTP": return TabIntpCompuMethod.compu_method_from_et( et_element, doc_frags, internal_type=internal_type, physical_type=physical_type) diff --git a/odxtools/compumethods/linearcompumethod.py b/odxtools/compumethods/linearcompumethod.py index a795924c..44cd0ac5 100644 --- a/odxtools/compumethods/linearcompumethod.py +++ b/odxtools/compumethods/linearcompumethod.py @@ -75,13 +75,13 @@ def __post_init__(self) -> None: def convert_internal_to_physical(self, internal_value: AtomicOdxType) -> AtomicOdxType: if not self._segment.internal_applies(internal_value): - odxraise(r"Cannot decode internal value {internal_value}", DecodeError) + odxraise(f"Cannot decode internal value {internal_value!r}", DecodeError) return self._segment.convert_internal_to_physical(internal_value) def convert_physical_to_internal(self, physical_value: AtomicOdxType) -> AtomicOdxType: if not self._segment.physical_applies(physical_value): - odxraise(r"Cannot decode physical value {physical_value}", EncodeError) + odxraise(f"Cannot decode physical value {physical_value!r}", EncodeError) return self._segment.convert_physical_to_internal(physical_value) diff --git a/odxtools/compumethods/ratfunccompumethod.py b/odxtools/compumethods/ratfunccompumethod.py new file mode 100644 index 00000000..5708c1f7 --- /dev/null +++ b/odxtools/compumethods/ratfunccompumethod.py @@ -0,0 +1,106 @@ +# SPDX-License-Identifier: MIT +from dataclasses import dataclass +from typing import List, Optional, cast +from xml.etree import ElementTree + +from ..exceptions import DecodeError, EncodeError, odxassert, odxraise +from ..odxlink import OdxDocFragment +from ..odxtypes import AtomicOdxType, DataType +from ..utils import dataclass_fields_asdict +from .compumethod import CompuCategory, CompuMethod +from .ratfuncsegment import RatFuncSegment + + +@dataclass +class RatFuncCompuMethod(CompuMethod): + """A compu method using a rational function + + i.e. internal values are converted to physical ones using the + function `f(x) = (a0 + a1*x + a2*x^2 ...)/(b0 + b0*x^2 ...)` where `f(x)` + is the physical value and `x` is the internal value. In contrast + to `ScaleRatFuncCompuMethod`, this compu method only exhibits a + single segment) + + For details, refer to ASAM specification MCD-2 D (ODX), section 7.3.6.6.5. + """ + + @property + def int_to_phys_segment(self) -> RatFuncSegment: + return self._int_to_phys_segment + + @property + def phys_to_int_segment(self) -> Optional[RatFuncSegment]: + return self._phys_to_int_segment + + @staticmethod + def compu_method_from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment], *, + internal_type: DataType, + physical_type: DataType) -> "RatFuncCompuMethod": + cm = CompuMethod.compu_method_from_et( + et_element, doc_frags, internal_type=internal_type, physical_type=physical_type) + kwargs = dataclass_fields_asdict(cm) + + return RatFuncCompuMethod(**kwargs) + + def __post_init__(self) -> None: + odxassert(self.category == CompuCategory.RAT_FUNC, + "RatFuncCompuMethod must exhibit RAT-FUNC category") + + odxassert(self.physical_type in [ + DataType.A_FLOAT32, + DataType.A_FLOAT64, + DataType.A_INT32, + DataType.A_UINT32, + ]) + odxassert(self.internal_type in [ + DataType.A_FLOAT32, + DataType.A_FLOAT64, + DataType.A_INT32, + DataType.A_UINT32, + ]) + + if self.compu_internal_to_phys is None: + odxraise("RAT-FUNC compu methods require COMPU-INTERNAL-TO-PHYS") + return + + int_to_phys_scales = self.compu_internal_to_phys.compu_scales + if len(int_to_phys_scales) != 1: + odxraise("RAT-FUNC compu methods expect exactly one compu scale within " + "COMPU-INTERNAL-TO-PHYS") + return cast(None, RatFuncCompuMethod) + + self._int_to_phys_segment = RatFuncSegment.from_compu_scale( + int_to_phys_scales[0], value_type=self.physical_type) + + self._phys_to_int_segment = None + if self.compu_phys_to_internal is not None: + phys_to_int_scales = self.compu_phys_to_internal.compu_scales + if len(phys_to_int_scales) != 1: + odxraise("RAT-FUNC compu methods expect exactly one compu scale within " + "COMPU-PHYS-TO-INTERNAL") + return cast(None, RatFuncCompuMethod) + + self._phys_to_int_segment = RatFuncSegment.from_compu_scale( + phys_to_int_scales[0], value_type=self.internal_type) + + def convert_internal_to_physical(self, internal_value: AtomicOdxType) -> AtomicOdxType: + if not self._int_to_phys_segment.applies(internal_value): + odxraise(f"Cannot decode internal value {internal_value!r}", DecodeError) + return cast(AtomicOdxType, None) + + return self._int_to_phys_segment.convert(internal_value) + + def convert_physical_to_internal(self, physical_value: AtomicOdxType) -> AtomicOdxType: + if self._phys_to_int_segment is None or not self._phys_to_int_segment.applies( + physical_value): + odxraise(f"Cannot encode physical value {physical_value!r}", EncodeError) + return cast(AtomicOdxType, None) + + return self._phys_to_int_segment.convert(physical_value) + + def is_valid_physical_value(self, physical_value: AtomicOdxType) -> bool: + return self._phys_to_int_segment is not None and self._phys_to_int_segment.applies( + physical_value) + + def is_valid_internal_value(self, internal_value: AtomicOdxType) -> bool: + return self._int_to_phys_segment.applies(internal_value) diff --git a/odxtools/compumethods/ratfuncsegment.py b/odxtools/compumethods/ratfuncsegment.py new file mode 100644 index 00000000..e0984539 --- /dev/null +++ b/odxtools/compumethods/ratfuncsegment.py @@ -0,0 +1,87 @@ +# SPDX-License-Identifier: MIT +from dataclasses import dataclass +from typing import List, Optional, Union + +from ..exceptions import odxraise, odxrequire +from ..odxtypes import AtomicOdxType, DataType +from .compuscale import CompuScale +from .limit import Limit + + +@dataclass +class RatFuncSegment: + """Helper class to represent a segment of a piecewise rational function. + """ + value_type: DataType + + numerator_coeffs: List[Union[int, float]] + denominator_coeffs: List[Union[int, float]] + + lower_limit: Optional[Limit] + upper_limit: Optional[Limit] + + @staticmethod + def from_compu_scale(scale: CompuScale, value_type: DataType) -> "RatFuncSegment": + coeffs = odxrequire(scale.compu_rational_coeffs, + "Scales for rational functions must exhibit " + "COMPU-RATIONAL-COEFFS") + + numerator_coeffs = coeffs.numerators + denominator_coeffs = coeffs.denominators + + lower_limit = scale.lower_limit + upper_limit = scale.upper_limit + + return RatFuncSegment( + numerator_coeffs=numerator_coeffs, + denominator_coeffs=denominator_coeffs, + lower_limit=lower_limit, + upper_limit=upper_limit, + value_type=scale.range_type) + + def convert(self, value: AtomicOdxType) -> Union[float, int]: + if not isinstance(value, (int, float)): + odxraise(f"Internal values of linear compumethods must " + f"either be int or float (is: {type(value).__name__})") + + numerator = 0.0 + x = float(value) + for numerator_coeff in reversed(self.numerator_coeffs): + numerator *= x + numerator += float(numerator_coeff) + + denominator = 0.0 + for denominator_coeff in reversed(self.denominator_coeffs): + denominator *= x + denominator += float(denominator_coeff) + + result = numerator / denominator + + if self.value_type in [ + DataType.A_INT32, + DataType.A_UINT32, + ]: + result = round(result) + + return result + + def applies(self, value: AtomicOdxType) -> bool: + """Returns True iff the segment is applicable to a given internal value""" + # Do type checks + expected_type = self.value_type.python_type + if issubclass(expected_type, float): + if not isinstance(value, (int, float)): + return False + else: + if not isinstance(value, expected_type): + return False + + if self.lower_limit is not None and \ + not self.lower_limit.complies_to_lower(value): + return False + + if self.upper_limit is not None and \ + not self.upper_limit.complies_to_upper(value): + return False + + return True diff --git a/odxtools/compumethods/scaleratfunccompumethod.py b/odxtools/compumethods/scaleratfunccompumethod.py new file mode 100644 index 00000000..aa468936 --- /dev/null +++ b/odxtools/compumethods/scaleratfunccompumethod.py @@ -0,0 +1,113 @@ +# SPDX-License-Identifier: MIT +from dataclasses import dataclass +from typing import List, Optional, cast +from xml.etree import ElementTree + +from ..exceptions import DecodeError, EncodeError, odxassert, odxraise +from ..odxlink import OdxDocFragment +from ..odxtypes import AtomicOdxType, DataType +from ..utils import dataclass_fields_asdict +from .compumethod import CompuCategory, CompuMethod +from .ratfuncsegment import RatFuncSegment + + +@dataclass +class ScaleRatFuncCompuMethod(CompuMethod): + """A compu method using a piecewise rational function + + For details, refer to ASAM specification MCD-2 D (ODX), section 7.3.6.6.5. + """ + + @property + def int_to_phys_segments(self) -> List[RatFuncSegment]: + return self._int_to_phys_segments + + @property + def phys_to_int_segments(self) -> Optional[List[RatFuncSegment]]: + return self._phys_to_int_segments + + @staticmethod + def compu_method_from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment], *, + internal_type: DataType, + physical_type: DataType) -> "ScaleRatFuncCompuMethod": + cm = CompuMethod.compu_method_from_et( + et_element, doc_frags, internal_type=internal_type, physical_type=physical_type) + kwargs = dataclass_fields_asdict(cm) + + return ScaleRatFuncCompuMethod(**kwargs) + + def __post_init__(self) -> None: + odxassert(self.category == CompuCategory.SCALE_RAT_FUNC, + "ScaleRatFuncCompuMethod must exhibit SCALE-RAT-FUNC category") + + odxassert(self.physical_type in [ + DataType.A_FLOAT32, + DataType.A_FLOAT64, + DataType.A_INT32, + DataType.A_UINT32, + ]) + odxassert(self.internal_type in [ + DataType.A_FLOAT32, + DataType.A_FLOAT64, + DataType.A_INT32, + DataType.A_UINT32, + ]) + + if self.compu_internal_to_phys is None: + odxraise("RAT-FUNC compu methods require COMPU-INTERNAL-TO-PHYS") + return + + int_to_phys_scales = self.compu_internal_to_phys.compu_scales + if len(int_to_phys_scales) < 1: + odxraise("RAT-FUNC compu methods expect at least one compu scale within " + "COMPU-INTERNAL-TO-PHYS") + return + + self._int_to_phys_segments = [ + RatFuncSegment.from_compu_scale(scale, value_type=self.physical_type) + for scale in int_to_phys_scales + ] + + self._phys_to_int_segments = None + if self.compu_phys_to_internal is not None: + phys_to_int_scales = self.compu_phys_to_internal.compu_scales + if len(phys_to_int_scales) < 1: + odxraise("SCALE-RAT-FUNC compu methods expect at least one compu scale within " + "COMPU-PHYS-TO-INTERNAL") + return + + self._phys_to_int_segments = [ + RatFuncSegment.from_compu_scale(scale, value_type=self.internal_type) + for scale in phys_to_int_scales + ] + + def convert_internal_to_physical(self, internal_value: AtomicOdxType) -> AtomicOdxType: + for seg in self._int_to_phys_segments: + if seg.applies(internal_value): + return seg.convert(internal_value) + + odxraise(f"Internal value {internal_value!r} be decoded using this compumethod", + DecodeError) + return cast(AtomicOdxType, None) + + def convert_physical_to_internal(self, physical_value: AtomicOdxType) -> AtomicOdxType: + if self._phys_to_int_segments is None: + odxraise(f"Physical values cannot be encoded using this compumethod", EncodeError) + return cast(AtomicOdxType, None) + + for seg in self._phys_to_int_segments: + if seg.applies(physical_value): + return seg.convert(physical_value) + + odxraise(f"Physical values {physical_value!r} be decoded using this compumethod", + EncodeError) + return cast(AtomicOdxType, None) + + def is_valid_internal_value(self, internal_value: AtomicOdxType) -> bool: + return any(seg.applies(internal_value) for seg in self._int_to_phys_segments) + + def is_valid_physical_value(self, physical_value: AtomicOdxType) -> bool: + if self._phys_to_int_segments is None: + return False + + return any(seg.applies(physical_value) for seg in self._phys_to_int_segments) diff --git a/odxtools/dataobjectproperty.py b/odxtools/dataobjectproperty.py index 9416b8d8..6774b59d 100644 --- a/odxtools/dataobjectproperty.py +++ b/odxtools/dataobjectproperty.py @@ -84,6 +84,7 @@ def from_et(et_element: ElementTree.Element, def _build_odxlinks(self) -> Dict[OdxLinkId, Any]: result = super()._build_odxlinks() result.update(self.diag_coded_type._build_odxlinks()) + result.update(self.compu_method._build_odxlinks()) return result def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None: @@ -91,6 +92,7 @@ def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None: super()._resolve_odxlinks(odxlinks) self.diag_coded_type._resolve_odxlinks(odxlinks) + self.compu_method._resolve_odxlinks(odxlinks) self._unit: Optional[Unit] = None if self.unit_ref: @@ -100,6 +102,7 @@ def _resolve_snrefs(self, context: SnRefContext) -> None: super()._resolve_snrefs(context) self.diag_coded_type._resolve_snrefs(context) + self.compu_method._resolve_snrefs(context) @property def unit(self) -> Optional[Unit]: diff --git a/odxtools/dtcdop.py b/odxtools/dtcdop.py index 49ce8c1b..0fd6cc2a 100644 --- a/odxtools/dtcdop.py +++ b/odxtools/dtcdop.py @@ -178,6 +178,8 @@ def encode_into_pdu(self, physical_value: Optional[ParameterValue], def _build_odxlinks(self) -> Dict[OdxLinkId, Any]: odxlinks = super()._build_odxlinks() + odxlinks.update(self.compu_method._build_odxlinks()) + for dtc_proxy in self.dtcs_raw: if isinstance(dtc_proxy, DiagnosticTroubleCode): odxlinks.update(dtc_proxy._build_odxlinks()) @@ -187,6 +189,8 @@ def _build_odxlinks(self) -> Dict[OdxLinkId, Any]: def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None: super()._resolve_odxlinks(odxlinks) + self.compu_method._resolve_odxlinks(odxlinks) + self._dtcs = NamedItemList[DiagnosticTroubleCode]() for dtc_proxy in self.dtcs_raw: if isinstance(dtc_proxy, DiagnosticTroubleCode): @@ -202,6 +206,8 @@ def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None: def _resolve_snrefs(self, context: SnRefContext) -> None: super()._resolve_snrefs(context) + self.compu_method._resolve_snrefs(context) + for dtc_proxy in self.dtcs_raw: if isinstance(dtc_proxy, DiagnosticTroubleCode): dtc_proxy._resolve_snrefs(context) diff --git a/tests/test_compu_methods.py b/tests/test_compu_methods.py index bc54f009..ad710314 100644 --- a/tests/test_compu_methods.py +++ b/tests/test_compu_methods.py @@ -7,18 +7,23 @@ import jinja2 import odxtools +from odxtools.compumethods.compucodecompumethod import CompuCodeCompuMethod from odxtools.compumethods.compuconst import CompuConst from odxtools.compumethods.compuinternaltophys import CompuInternalToPhys from odxtools.compumethods.compumethod import CompuCategory +from odxtools.compumethods.compuphystointernal import CompuPhysToInternal from odxtools.compumethods.compurationalcoeffs import CompuRationalCoeffs from odxtools.compumethods.compuscale import CompuScale from odxtools.compumethods.createanycompumethod import create_any_compu_method_from_et from odxtools.compumethods.limit import IntervalType, Limit from odxtools.compumethods.linearcompumethod import LinearCompuMethod +from odxtools.compumethods.ratfunccompumethod import RatFuncCompuMethod +from odxtools.compumethods.scaleratfunccompumethod import ScaleRatFuncCompuMethod from odxtools.compumethods.tabintpcompumethod import TabIntpCompuMethod from odxtools.exceptions import DecodeError, EncodeError, OdxError from odxtools.odxlink import OdxDocFragment from odxtools.odxtypes import DataType +from odxtools.progcode import ProgCode from odxtools.writepdxfile import (get_parent_container_name, jinja2_odxraise_helper, make_bool_xml_attrib, make_xml_attrib) @@ -37,6 +42,7 @@ def _get_jinja_environment() -> jinja2.environment.Environment: jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader(templates_dir)) + jinja_env.globals["getattr"] = getattr jinja_env.globals["hasattr"] = hasattr jinja_env.globals["odxraise"] = jinja2_odxraise_helper jinja_env.globals["make_xml_attrib"] = make_xml_attrib @@ -407,6 +413,337 @@ def test_linear_compu_method_physical_limits(self) -> None: self.assertFalse(compu_method.is_valid_physical_value(-9)) +class TestCompuCodeCompuMethod(unittest.TestCase): + + def test_compu_code_compu_method(self) -> None: + compu_method = CompuCodeCompuMethod( + category=CompuCategory.COMPUCODE, + compu_internal_to_phys=CompuInternalToPhys( + compu_scales=[], + prog_code=ProgCode( + code_file="nice_computation.java", + syntax="JAVA", + revision="1.0", + encryption=None, + entrypoint=None, + library_refs=[]), + compu_default_value=None), + compu_phys_to_internal=CompuPhysToInternal( + compu_scales=[], + prog_code=ProgCode( + code_file="nice_inverse_computation.java", + syntax="JAVA", + revision="1.0", + encryption=None, + entrypoint=None, + library_refs=[]), + compu_default_value=None), + internal_type=DataType.A_FLOAT32, + physical_type=DataType.A_FLOAT32) + + # COMPUCODE compu methods can only be used from Java projects + # (i.e., not odxtools) + with self.assertRaises(DecodeError): + compu_method.convert_internal_to_physical(1) + with self.assertRaises(EncodeError): + compu_method.convert_physical_to_internal(2) + + +class TestRatFuncCompuMethod(unittest.TestCase): + + def test_rat_func_compu_method(self) -> None: + compu_method = RatFuncCompuMethod( + category=CompuCategory.RAT_FUNC, + compu_internal_to_phys=CompuInternalToPhys( + compu_scales=[ + CompuScale( + short_label=None, + description=None, + lower_limit=Limit( + value_raw="2", + value_type=DataType.A_FLOAT32, + interval_type=IntervalType.CLOSED), + upper_limit=Limit( + value_raw="3", + value_type=DataType.A_FLOAT32, + interval_type=IntervalType.CLOSED), + compu_inverse_value=None, + compu_const=None, + compu_rational_coeffs=CompuRationalCoeffs( + value_type=DataType.A_FLOAT32, + # f(x) = 3.14*(2*x^2 + 4x + 6) / 3.14 + numerators=[3.14 * 6, 3.14 * 4, 3.14 * 2], + denominators=[3.14], + ), + domain_type=DataType.A_FLOAT32, + range_type=DataType.A_FLOAT32), + ], + prog_code=None, + compu_default_value=None), + compu_phys_to_internal=None, + internal_type=DataType.A_FLOAT32, + physical_type=DataType.A_FLOAT32) + + self.assertTrue(abs(float(compu_method.convert_internal_to_physical(2.5)) - 28.5) < 1e-5) + + # out of range + with self.assertRaises(DecodeError): + compu_method.convert_internal_to_physical(3.01) + with self.assertRaises(DecodeError): + compu_method.convert_internal_to_physical(1.99) + + # if the inverse function is not explicitly specified, + # inversion is not allowed! + with self.assertRaises(EncodeError): + compu_method.convert_physical_to_internal(2.5) + + def test_rat_func_compu_method_with_inverse(self) -> None: + compu_method = RatFuncCompuMethod( + category=CompuCategory.RAT_FUNC, + compu_internal_to_phys=CompuInternalToPhys( + compu_scales=[ + CompuScale( + short_label=None, + description=None, + lower_limit=Limit( + value_raw="2", + value_type=DataType.A_FLOAT32, + interval_type=IntervalType.CLOSED), + upper_limit=Limit( + value_raw="3", + value_type=DataType.A_FLOAT32, + interval_type=IntervalType.CLOSED), + compu_inverse_value=None, + compu_const=None, + compu_rational_coeffs=CompuRationalCoeffs( + value_type=DataType.A_FLOAT32, + # f(x) = x^2 + numerators=[0, 0, 1], + denominators=[1], + ), + domain_type=DataType.A_FLOAT32, + range_type=DataType.A_FLOAT32), + ], + prog_code=None, + compu_default_value=None), + compu_phys_to_internal=CompuPhysToInternal( + compu_scales=[ + CompuScale( + short_label=None, + description=None, + lower_limit=Limit( + value_raw="4", + value_type=DataType.A_FLOAT32, + interval_type=IntervalType.CLOSED), + upper_limit=Limit( + value_raw="9", + value_type=DataType.A_FLOAT32, + interval_type=IntervalType.CLOSED), + compu_inverse_value=None, + compu_const=None, + compu_rational_coeffs=CompuRationalCoeffs( + value_type=DataType.A_FLOAT32, + # first three terms of Taylor expansion of + # f(x) = sqrt(x) at evaluation point 1 + numerators=[2.72 * 3 / 8, 2.72 * 3 / 4, 2.72 * -1 / 8], + denominators=[2.72], + ), + domain_type=DataType.A_FLOAT32, + range_type=DataType.A_FLOAT32), + ], + prog_code=None, + compu_default_value=None), + internal_type=DataType.A_FLOAT32, + physical_type=DataType.A_FLOAT32) + + self.assertEqual(compu_method.convert_internal_to_physical(2), 4) + self.assertEqual(compu_method.convert_internal_to_physical(2.5), 6.25) + self.assertTrue(abs(float(compu_method.convert_internal_to_physical(3)) - 9) < 1e-8) + + # note that the inverse values are pretty inaccurate because + # the Taylor series was cut off way quite early. + self.assertTrue(abs(float(compu_method.convert_physical_to_internal(4)) - 1.375) < 1e-4) + self.assertTrue( + abs(float(compu_method.convert_physical_to_internal(6.25)) - 0.17969) < 1e-4) + self.assertEqual(compu_method.convert_physical_to_internal(9), -3) + + # ensure that we stay in bounds + with self.assertRaises(DecodeError): + compu_method.convert_internal_to_physical(3.99) + with self.assertRaises(DecodeError): + compu_method.convert_internal_to_physical(9.01) + + +class TestScaleRatFuncCompuMethod(unittest.TestCase): + + def test_scale_rat_func_compu_method(self) -> None: + compu_method = ScaleRatFuncCompuMethod( + category=CompuCategory.SCALE_RAT_FUNC, + compu_internal_to_phys=CompuInternalToPhys( + compu_scales=[ + CompuScale( + short_label=None, + description=None, + lower_limit=Limit( + value_raw="2", + value_type=DataType.A_FLOAT32, + interval_type=IntervalType.CLOSED), + upper_limit=Limit( + value_raw="3", + value_type=DataType.A_FLOAT32, + interval_type=IntervalType.CLOSED), + compu_inverse_value=None, + compu_const=None, + compu_rational_coeffs=CompuRationalCoeffs( + value_type=DataType.A_FLOAT32, + # f(x) = 3.14*(2*x^2 + 4x + 6) / 3.14 + numerators=[3.14 * 6, 3.14 * 4, 3.14 * 2], + denominators=[3.14], + ), + domain_type=DataType.A_FLOAT32, + range_type=DataType.A_FLOAT32), + ], + prog_code=None, + compu_default_value=None), + compu_phys_to_internal=None, + internal_type=DataType.A_FLOAT32, + physical_type=DataType.A_FLOAT32) + + self.assertTrue(abs(float(compu_method.convert_internal_to_physical(2.5)) - 28.5) < 1e-5) + + # out of range + with self.assertRaises(DecodeError): + compu_method.convert_internal_to_physical(3.01) + with self.assertRaises(DecodeError): + compu_method.convert_internal_to_physical(1.99) + + # if the inverse function is not explicitly specified, + # inversion is not allowed! + with self.assertRaises(EncodeError): + compu_method.convert_physical_to_internal(2.5) + + def test_scale_rat_func_compu_method_with_inverse(self) -> None: + compu_method = ScaleRatFuncCompuMethod( + category=CompuCategory.SCALE_RAT_FUNC, + compu_internal_to_phys=CompuInternalToPhys( + compu_scales=[ + CompuScale( + short_label=None, + description=None, + lower_limit=Limit( + value_raw="2", + value_type=DataType.A_FLOAT32, + interval_type=IntervalType.CLOSED), + upper_limit=Limit( + value_raw="3", + value_type=DataType.A_FLOAT32, + interval_type=IntervalType.CLOSED), + compu_inverse_value=None, + compu_const=None, + compu_rational_coeffs=CompuRationalCoeffs( + value_type=DataType.A_FLOAT32, + # f(x) = x^2 + numerators=[0, 0, 1], + denominators=[1], + ), + domain_type=DataType.A_FLOAT32, + range_type=DataType.A_FLOAT32), + CompuScale( + short_label=None, + description=None, + lower_limit=Limit( + value_raw="4", + value_type=DataType.A_FLOAT32, + interval_type=IntervalType.CLOSED), + upper_limit=Limit( + value_raw="5", + value_type=DataType.A_FLOAT32, + interval_type=IntervalType.CLOSED), + compu_inverse_value=None, + compu_const=None, + compu_rational_coeffs=CompuRationalCoeffs( + value_type=DataType.A_FLOAT32, + # f(x) = 1.38e-23*(3*x + 10)/1.38e-23 + numerators=[1.38e-23 * 10, 1.38e-23 * 3], + denominators=[1.38e-23], + ), + domain_type=DataType.A_FLOAT32, + range_type=DataType.A_FLOAT32), + ], + prog_code=None, + compu_default_value=None), + compu_phys_to_internal=CompuPhysToInternal( + compu_scales=[ + CompuScale( + short_label=None, + description=None, + lower_limit=Limit( + value_raw="4", + value_type=DataType.A_FLOAT32, + interval_type=IntervalType.CLOSED), + upper_limit=Limit( + value_raw="9", + value_type=DataType.A_FLOAT32, + interval_type=IntervalType.CLOSED), + compu_inverse_value=None, + compu_const=None, + compu_rational_coeffs=CompuRationalCoeffs( + value_type=DataType.A_FLOAT32, + # first three terms of Taylor expansion of + # f(x) = sqrt(x) at evaluation point 1 + numerators=[2.72 * 3 / 8, 2.72 * 3 / 4, 2.72 * -1 / 8], + denominators=[2.72], + ), + domain_type=DataType.A_FLOAT32, + range_type=DataType.A_FLOAT32), + CompuScale( + short_label=None, + description=None, + lower_limit=Limit( + value_raw="22", + value_type=DataType.A_FLOAT32, + interval_type=IntervalType.CLOSED), + upper_limit=Limit( + value_raw="25", + value_type=DataType.A_FLOAT32, + interval_type=IntervalType.CLOSED), + compu_inverse_value=None, + compu_const=None, + compu_rational_coeffs=CompuRationalCoeffs( + value_type=DataType.A_FLOAT32, + # f(x) = (x - 10)/3 + numerators=[-10, 1], + denominators=[3], + ), + domain_type=DataType.A_FLOAT32, + range_type=DataType.A_FLOAT32), + ], + prog_code=None, + compu_default_value=None), + internal_type=DataType.A_FLOAT32, + physical_type=DataType.A_FLOAT32) + + self.assertEqual(compu_method.convert_internal_to_physical(2), 4) + self.assertEqual(compu_method.convert_internal_to_physical(2.5), 6.25) + self.assertTrue(abs(float(compu_method.convert_internal_to_physical(3)) - 9) < 1e-8) + + self.assertTrue(abs(float(compu_method.convert_internal_to_physical(4)) - 22) < 1e-8) + self.assertTrue(abs(float(compu_method.convert_physical_to_internal(22)) - 4) < 1e-8) + + # note that the inverse values are pretty inaccurate because + # the Taylor series was cut off way quite early. + self.assertTrue(abs(float(compu_method.convert_physical_to_internal(4)) - 1.375) < 1e-4) + self.assertTrue( + abs(float(compu_method.convert_physical_to_internal(6.25)) - 0.17969) < 1e-4) + self.assertEqual(compu_method.convert_physical_to_internal(9), -3) + + # make sure that we stay in bounds + with self.assertRaises(DecodeError): + compu_method.convert_internal_to_physical(3.99) + with self.assertRaises(DecodeError): + compu_method.convert_internal_to_physical(9.01) + + class TestTabIntpCompuMethod(unittest.TestCase): def setUp(self) -> None: @@ -424,6 +761,7 @@ def _get_jinja_environment() -> jinja2.environment.Environment: jinja_env.filters["odxtools_collapse_xml_attribute"] = (lambda x: " " + x.strip() if x.strip() else "") + jinja_env.globals["getattr"] = getattr jinja_env.globals["hasattr"] = hasattr return jinja_env