Skip to content

Commit

Permalink
scale linear compu methods: improve detection of invertibility
Browse files Browse the repository at this point in the history
this makes the code conform to section 7.3.6.6.4 of the spec. Note
that DOPs using non-invertible SCALE-LINEAR compu methods currently
cannot be encoded.

Signed-off-by: Andreas Lauser <andreas.lauser@mbition.io>
Signed-off-by: Katja Köhler <katja.koehler@mbition.io>
  • Loading branch information
andlaus committed May 23, 2024
1 parent eea043c commit 7cf8993
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 4 deletions.
16 changes: 16 additions & 0 deletions odxtools/compumethods/linearsegment.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ class LinearSegment:
internal_lower_limit: Optional[Limit]
internal_upper_limit: Optional[Limit]

inverse_value: Union[int, float] # value used as inverse if factor is 0

internal_type: DataType
physical_type: DataType

Expand All @@ -42,6 +44,15 @@ def from_compu_scale(scale: CompuScale, *, internal_type: DataType,
if len(coeffs.denominators) > 0:
denominator = coeffs.denominators[0]

inverse_value: Union[int, float] = 0
if scale.compu_inverse_value is not None:
if abs(factor) < 1e-10:
odxraise(f"COMPU-INVERSE-VALUE for non-zero slope ({factor}) defined")
x = odxrequire(scale.compu_inverse_value).value
if not isinstance(x, (int, float)):
odxraise(f"Non-numeric COMPU-INVERSE-VALUE specified ({x!r})")
inverse_value = x

internal_lower_limit = scale.lower_limit
internal_upper_limit = scale.upper_limit

Expand All @@ -51,6 +62,7 @@ def from_compu_scale(scale: CompuScale, *, internal_type: DataType,
denominator=denominator,
internal_lower_limit=internal_lower_limit,
internal_upper_limit=internal_upper_limit,
inverse_value=inverse_value,
internal_type=internal_type,
physical_type=physical_type)

Expand Down Expand Up @@ -85,6 +97,10 @@ def convert_physical_to_internal(self, physical_value: AtomicOdxType) -> Union[f
odxraise(f"Physical values of linear compumethods must "
f"either be int or float (is: {type(physical_value).__name__})")

if abs(self.factor) < 1e-10:
# "If factor = 0 then COMPU-INVERSE-VALUE shall be specified.
return self.inverse_value

result = (physical_value * self.denominator - self.offset) / self.factor

if self.internal_type in [
Expand Down
58 changes: 54 additions & 4 deletions odxtools/compumethods/scalelinearcompumethod.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from ..odxtypes import AtomicOdxType, DataType
from ..utils import dataclass_fields_asdict
from .compumethod import CompuCategory, CompuMethod
from .limit import IntervalType
from .linearsegment import LinearSegment


Expand Down Expand Up @@ -62,17 +63,68 @@ def __post_init__(self) -> None:
LinearSegment.from_compu_scale(
scale, internal_type=self.internal_type, physical_type=self.physical_type))

# find out if the transfer function is invertible (i.e. if it
# can be encoded by normal means). section 7.3.6.6.4 of the
# ODX specification states that the condition for
# invertibility is that adjacent COMPU-SCALES exhibit the same
# values on their common boundaries and that the slope in all
# intervals exhibit the same sign (or are 0). For segments
# with a slope of zero, COMPU-INVERSE-VALUE shall be used.
self._is_invertible = True
ref_factor = self._segments[0].factor
for i in range(0, len(self._segments) - 1):
s0 = self.segments[i]
s1 = self.segments[i + 1]

if ref_factor * s1.factor < 0:
self._is_invertible = False
break
if s1.factor != 0:
ref_factor = s1.factor

# both interval boundaries must not be infinite
if s0.internal_upper_limit is None or \
s1.internal_lower_limit is None:
self._is_invertible = False
break
elif s0.internal_upper_limit.value is None or \
s1.internal_lower_limit.value is None or \
s0.internal_upper_limit.interval_type == IntervalType.INFINITE or \
s1.internal_lower_limit.interval_type == IntervalType.INFINITE:
self._is_invertible = False
break

# the intervals must use the same reference point
if (x := s0.internal_upper_limit.value) != s1.internal_lower_limit.value:
self._is_invertible = False
break

if not isinstance(x, (int, float)):
odxraise("Linear segments must use int or float for all quantities")

# the respective function value at the interval's
# reference point must be identical
y0 = s0.convert_internal_to_physical(x)
y1 = s1.convert_internal_to_physical(x)
if abs(y0 - y1) < 1e-10:
self._is_invertible = False
break

def convert_physical_to_internal(self, physical_value: AtomicOdxType) -> Union[float, int]:
if not self._is_invertible:
odxraise(
f"Trying to encode value {physical_value!r} using a non-invertible "
f"SCALE-LINEAR transfer function", EncodeError)

applicable_segments = [
seg for seg in self._segments if seg.physical_applies(physical_value)
]
if not applicable_segments:
odxraise(r"No applicable segment for value {physical_value} found", EncodeError)
return cast(int, None)
elif len(applicable_segments):
odxraise(r"Multiple applicable segments for value {physical_value} found", EncodeError)

seg = applicable_segments[0]

return seg.convert_physical_to_internal(physical_value)

def convert_internal_to_physical(self, internal_value: AtomicOdxType) -> Union[float, int]:
Expand All @@ -82,8 +134,6 @@ def convert_internal_to_physical(self, internal_value: AtomicOdxType) -> Union[f
if not applicable_segments:
odxraise(r"No applicable segment for value {internal_value} found", DecodeError)
return cast(int, None)
elif len(applicable_segments):
odxraise(r"Multiple applicable segments for value {internal_value} found", DecodeError)

seg = applicable_segments[0]
return seg.convert_internal_to_physical(internal_value)
Expand Down

0 comments on commit 7cf8993

Please sign in to comment.