Skip to content

Commit a0f3f91

Browse files
authored
Merge pull request #45 from KLBa/add_tab_intp
Add `TAB-INTP` compu method
2 parents b66f8fb + bd1281c commit a0f3f91

File tree

4 files changed

+279
-16
lines changed

4 files changed

+279
-16
lines changed

odxtools/compumethods/readcompumethod.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,16 @@ def read_compu_method_from_odx(et_element, internal_type: DataType, physical_typ
168168
return ScaleLinearCompuMethod(linear_methods)
169169

170170
elif compu_category == "TAB-INTP":
171-
return TabIntpCompuMethod(internal_type=internal_type, physical_type=physical_type)
171+
172+
scales = et_element.findall(
173+
"COMPU-INTERNAL-TO-PHYS/COMPU-SCALES/COMPU-SCALE")
174+
internal = [scale.findtext("LOWER-LIMIT") for scale in scales]
175+
physical = [scale.findtext("COMPU-CONST/V") for scale in scales]
176+
177+
internal = [internal_type.from_string(x) for x in internal]
178+
physical = [physical_type.from_string(x) for x in physical]
179+
180+
return TabIntpCompuMethod(internal_type=internal_type, physical_type=physical_type, internal_points=internal, physical_points=physical)
172181

173182
# TODO: Implement other categories (never instantiate CompuMethod)
174183
logger.warning(

odxtools/compumethods/tabintpcompumethod.py

Lines changed: 133 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,148 @@
22
# Copyright (c) 2022 MBition GmbH
33

44

5+
from typing import List, Tuple, Union
6+
7+
from ..exceptions import EncodeError, DecodeError
58
from ..globals import logger
9+
from ..odxtypes import DataType
610

711
from .compumethodbase import CompuMethod
12+
from .limit import IntervalType, Limit
813

914

1015
class TabIntpCompuMethod(CompuMethod):
16+
"""
17+
A compu method of type Tab Interpolated is used for linear interpolation.
18+
19+
A `TabIntpCompuMethod` is defined by a set of points. Each point is an (internal, physical) value pair.
20+
When converting from internal to physical or vice-versa, the result is linearly interpolated.
21+
22+
The function defined by a `TabIntpCompuMethod` is similar to the one of a `ScaleLinearCompuMethod` with the following differences:
23+
24+
* `TabIntpCompuMethod`s are always continuous whereas `ScaleLinearCompuMethod` might have jumps
25+
* `TabIntpCompuMethod`s are always invertible: Even if the linear interpolation is not monotonic, the first matching interval is taken.
26+
27+
Refer to ASAM MCD-2 D (ODX) Specification, section 7.3.6.6.8 for details.
28+
29+
Examples
30+
--------
31+
32+
Create a TabIntpCompuMethod defined by the points (0, -1), (10, 1), (30, 2)::
33+
34+
method = TabIntpCompuMethod(
35+
internal_type=DataType.A_UINT32,
36+
physical_type=DataType.A_UINT32,
37+
internal_points=[0, 10, 30],
38+
physical_points=[-1, 1, 2]
39+
)
40+
41+
Note that the points are given as two lists. The equivalent odx definition is::
42+
43+
<COMPU-METHOD>
44+
<CATEGORY>TAB-INTP</CATEGORY>
45+
<COMPU-INTERNAL-TO-PHYS>
46+
<COMPU-SCALES>
47+
<COMPU-SCALE>
48+
<LOWER-LIMIT INTERVAL-TYPE = "CLOSED">0</LOWER-LIMIT>
49+
<COMPU-CONST>
50+
<V>-1</V>
51+
</COMPU-CONST>
52+
</COMPU-SCALE>
53+
<COMPU-SCALE>
54+
<LOWER-LIMIT INTERVAL-TYPE = "CLOSED">10</LOWER-LIMIT>
55+
<COMPU-CONST>
56+
<V>1</V>
57+
</COMPU-CONST>
58+
</COMPU-SCALE>
59+
<COMPU-SCALE>
60+
<LOWER-LIMIT INTERVAL-TYPE = "CLOSED">30</LOWER-LIMIT>
61+
<COMPU-CONST>
62+
<V>2</V>
63+
</COMPU-CONST>
64+
</COMPU-SCALE>
65+
</COMPU-SCALES>
66+
</COMPU-INTERNAL-TO-PHYS>
67+
</COMPU-METHOD>
68+
69+
"""
70+
1171
def __init__(self,
12-
internal_type,
13-
physical_type):
72+
internal_type: Union[DataType, str],
73+
physical_type: Union[DataType, str],
74+
internal_points: List[Union[float, int]],
75+
physical_points: List[Union[float, int]]):
1476
super().__init__(internal_type, physical_type, "TAB-INTP")
15-
logger.debug("Created table interpolation compu method!")
16-
logger.warning(
17-
"TODO: Implement table interpolation compu method properly!")
1877

19-
def convert_physical_to_internal(self, physical_value):
20-
return self.internal_type.make_from(physical_value)
78+
self.internal_points = internal_points
79+
self.physical_points = physical_points
80+
81+
self._physical_lower_limit = Limit(
82+
min(physical_points), IntervalType.CLOSED)
83+
self._physical_upper_limit = Limit(
84+
max(physical_points), IntervalType.CLOSED)
85+
86+
logger.debug("Created compu method of type tab interpolated !")
87+
self._assert_validity()
88+
89+
@property
90+
def physical_lower_limit(self) -> Limit:
91+
return self._physical_lower_limit
92+
93+
@property
94+
def physical_upper_limit(self) -> Limit:
95+
return self._physical_upper_limit
96+
97+
def _assert_validity(self) -> None:
98+
assert len(self.internal_points) == len(self.physical_points)
99+
100+
assert self.internal_type in [DataType.A_INT32, DataType.A_UINT32,
101+
DataType.A_FLOAT32, DataType.A_FLOAT64], \
102+
("Internal data type of tab-intp compumethod must be one of"
103+
" [DataType.A_INT32, DataType.A_UINT32, DataType.A_FLOAT32, DataType.A_FLOAT64]")
104+
assert self.physical_type in [DataType.A_INT32, DataType.A_UINT32,
105+
DataType.A_FLOAT32, DataType.A_FLOAT64], \
106+
("Physical data type of tab-intp compumethod must be one of"
107+
" [DataType.A_INT32, DataType.A_UINT32, DataType.A_FLOAT32, DataType.A_FLOAT64]")
108+
109+
def _piecewise_linear_interpolate(self,
110+
x: Union[int, float],
111+
points: List[Tuple[Union[int, float], Union[int, float]]]) \
112+
-> Union[float, None]:
113+
for ((x0, y0), (x1, y1)) in zip(points[:-1], points[1:]):
114+
if x0 <= x and x <= x1:
115+
return y0 + (x - x0) * (y1 - y0) / (x1 - x0)
116+
117+
return None
118+
119+
def convert_physical_to_internal(self, physical_value: Union[int, float]) -> Union[int, float]:
120+
reference_points = list(zip(
121+
self.physical_points, self.internal_points))
122+
result = self._piecewise_linear_interpolate(
123+
physical_value, reference_points)
124+
125+
if result is None:
126+
raise EncodeError(f"Internal value {physical_value} must be inside the range"
127+
f" [{min(self.physical_points)}, {max(self.physical_points)}]")
128+
res = self.internal_type.make_from(result)
129+
assert isinstance(res, (int, float))
130+
return res
131+
132+
def convert_internal_to_physical(self, internal_value: Union[int, float]) -> Union[int, float]:
133+
reference_points = list(zip(
134+
self.internal_points, self.physical_points))
135+
result = self._piecewise_linear_interpolate(
136+
internal_value, reference_points)
21137

22-
def convert_internal_to_physical(self, internal_value):
23-
return self.physical_type.make_from(internal_value)
138+
if result is None:
139+
raise DecodeError(f"Internal value {internal_value} must be inside the range"
140+
f" [{min(self.internal_points)}, {max(self.internal_points)}]")
141+
res = self.physical_type.make_from(result)
142+
assert isinstance(res, (int, float))
143+
return res
24144

25-
def is_valid_physical_value(self, physical_value):
26-
return True
145+
def is_valid_physical_value(self, physical_value: Union[int, float]) -> bool:
146+
return min(self.physical_points) <= physical_value and physical_value <= max(self.physical_points)
27147

28-
def is_valid_internal_value(self, internal_value):
29-
return True
148+
def is_valid_internal_value(self, internal_value: Union[int, float]) -> bool:
149+
return min(self.internal_points) <= internal_value and internal_value <= max(self.internal_points)

odxtools/pdx_stub/macros/printDOP.tpl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,19 @@
137137
{%- endfor %}
138138
</COMPU-SCALES>
139139
</COMPU-INTERNAL-TO-PHYS>
140+
{%- elif cm.category == "TAB-INTP" %}
141+
<COMPU-INTERNAL-TO-PHYS>
142+
<COMPU-SCALES>
143+
{%- for idx in range( cm.internal_points | length ) %}
144+
<COMPU-SCALE>
145+
<LOWER-LIMIT INTERVAL-TYPE="CLOSED">{{ cm.internal_points[idx] }}</LOWER-LIMIT>
146+
<COMPU-CONST>
147+
<V>{{ cm.physical_points[idx] }}</V>
148+
</COMPU-CONST>
149+
</COMPU-SCALE>
150+
{%- endfor %}
151+
</COMPU-SCALES>
152+
</COMPU-INTERNAL-TO-PHYS>
140153
{%- endif %}
141154
</COMPU-METHOD>
142155
{%- endmacro -%}

tests/test_compu_methods.py

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
# SPDX-License-Identifier: MIT
22
# Copyright (c) 2022 MBition GmbH
33

4+
import inspect
5+
import os
46
import unittest
7+
from xml.etree import ElementTree
58

6-
from odxtools.compumethods import Limit, LinearCompuMethod, IntervalType
9+
import jinja2
10+
import odxtools
11+
12+
from odxtools.compumethods import Limit, LinearCompuMethod, IntervalType, TabIntpCompuMethod
13+
from odxtools.compumethods.readcompumethod import read_compu_method_from_odx
14+
from odxtools.exceptions import DecodeError, EncodeError
15+
from odxtools.odxtypes import DataType
716

817

918
class TestLinearCompuMethod(unittest.TestCase):
10-
def test_linear_compu_method(self):
19+
def test_linear_compu_method_type_int_int(self):
1120
compu_method = LinearCompuMethod(1, 3, "A_INT32", "A_INT32")
1221

1322
self.assertEqual(compu_method.convert_internal_to_physical(4), 13)
@@ -90,5 +99,117 @@ def test_linear_compu_method_physical_limits(self):
9099
self.assertFalse(compu_method.is_valid_physical_value(-13))
91100

92101

102+
class TestTabIntpCompuMethod(unittest.TestCase):
103+
def setUp(self) -> None:
104+
"""Prepares the jinja environment and the sample tab-intp compumethod"""
105+
106+
def _get_jinja_environment():
107+
__module_filname = inspect.getsourcefile(odxtools)
108+
assert isinstance(__module_filname, str)
109+
stub_dir = os.path.sep.join([os.path.dirname(__module_filname),
110+
"pdx_stub"])
111+
112+
jinja_env = jinja2.Environment(
113+
loader=jinja2.FileSystemLoader(stub_dir))
114+
115+
# allows to put XML attributes on a separate line while it is
116+
# collapsed with the previous line in the rendering
117+
jinja_env.filters["odxtools_collapse_xml_attribute"] = lambda x: " " + \
118+
x.strip() if x.strip() else ""
119+
return jinja_env
120+
121+
self.jinja_env = _get_jinja_environment()
122+
123+
self.compumethod = TabIntpCompuMethod(DataType.A_INT32,
124+
DataType.A_FLOAT32,
125+
internal_points=[0, 10, 30],
126+
physical_points=[-1, 1, 2]
127+
)
128+
129+
self.compumethod_odx = f"""
130+
<COMPU-METHOD>
131+
<CATEGORY>TAB-INTP</CATEGORY>
132+
<COMPU-INTERNAL-TO-PHYS>
133+
<COMPU-SCALES>
134+
<COMPU-SCALE>
135+
<LOWER-LIMIT INTERVAL-TYPE="CLOSED">{self.compumethod.internal_points[0]}</LOWER-LIMIT>
136+
<COMPU-CONST>
137+
<V>{self.compumethod.physical_points[0]}</V>
138+
</COMPU-CONST>
139+
</COMPU-SCALE>
140+
<COMPU-SCALE>
141+
<LOWER-LIMIT INTERVAL-TYPE="CLOSED">{self.compumethod.internal_points[1]}</LOWER-LIMIT>
142+
<COMPU-CONST>
143+
<V>{self.compumethod.physical_points[1]}</V>
144+
</COMPU-CONST>
145+
</COMPU-SCALE>
146+
<COMPU-SCALE>
147+
<LOWER-LIMIT INTERVAL-TYPE="CLOSED">{self.compumethod.internal_points[2]}</LOWER-LIMIT>
148+
<COMPU-CONST>
149+
<V>{self.compumethod.physical_points[2]}</V>
150+
</COMPU-CONST>
151+
</COMPU-SCALE>
152+
</COMPU-SCALES>
153+
</COMPU-INTERNAL-TO-PHYS>
154+
</COMPU-METHOD>
155+
"""
156+
157+
def test_tabintp_convert_type_int_float(self):
158+
method = self.compumethod
159+
160+
for internal, physical in [
161+
(0, -1),
162+
(2, -0.6),
163+
(3, -0.4),
164+
(5, 0),
165+
(10, 1),
166+
(20, 1.5),
167+
(25, 1.75),
168+
(30, 2)
169+
]:
170+
self.assertTrue(method.is_valid_internal_value(internal))
171+
self.assertTrue(method.is_valid_physical_value(physical))
172+
self.assertEqual(method.convert_internal_to_physical(internal),
173+
physical)
174+
self.assertEqual(method.convert_physical_to_internal(physical),
175+
internal)
176+
177+
self.assertRaises(DecodeError,
178+
method.convert_internal_to_physical, -2)
179+
self.assertRaises(DecodeError,
180+
method.convert_internal_to_physical, 31)
181+
self.assertRaises(EncodeError,
182+
method.convert_physical_to_internal, -2)
183+
self.assertRaises(EncodeError,
184+
method.convert_physical_to_internal, 2.1)
185+
186+
def test_read_odx(self):
187+
expected = self.compumethod
188+
189+
et_element = ElementTree.fromstring(self.compumethod_odx)
190+
actual = read_compu_method_from_odx(et_element,
191+
expected.internal_type,
192+
expected.physical_type)
193+
self.assertIsInstance(actual, TabIntpCompuMethod)
194+
self.assertEqual(expected.physical_type, actual.physical_type)
195+
self.assertEqual(expected.internal_type, actual.internal_type)
196+
self.assertEqual(expected.internal_points, actual.internal_points)
197+
self.assertEqual(expected.physical_points, actual.physical_points)
198+
199+
def test_write_odx(self):
200+
dlc_tpl = self.jinja_env.get_template("macros/printDOP.tpl")
201+
module = dlc_tpl.make_module()
202+
203+
out = module.printCompuMethod(self.compumethod)
204+
205+
expected_odx = self.compumethod_odx
206+
207+
# We ignore spaces
208+
def remove_spaces(string):
209+
return "".join(string.split())
210+
211+
self.assertEqual(remove_spaces(out), remove_spaces(expected_odx))
212+
213+
93214
if __name__ == '__main__':
94215
unittest.main()

0 commit comments

Comments
 (0)