odxtools 7.1.0__py3-none-any.whl → 7.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- odxtools/__init__.py +6 -4
- odxtools/additionalaudience.py +3 -5
- odxtools/admindata.py +5 -7
- odxtools/audience.py +3 -5
- odxtools/basecomparam.py +3 -5
- odxtools/basicstructure.py +10 -17
- odxtools/cli/_parser_utils.py +1 -1
- odxtools/cli/_print_utils.py +3 -2
- odxtools/cli/compare.py +1 -1
- odxtools/companydata.py +5 -7
- odxtools/companydocinfo.py +7 -8
- odxtools/companyrevisioninfo.py +3 -5
- odxtools/companyspecificinfo.py +8 -9
- odxtools/comparam.py +4 -6
- odxtools/comparaminstance.py +6 -8
- odxtools/comparamspec.py +14 -13
- odxtools/comparamsubset.py +17 -16
- odxtools/complexcomparam.py +5 -7
- odxtools/compumethods/compuconst.py +31 -0
- odxtools/compumethods/compudefaultvalue.py +27 -0
- odxtools/compumethods/compuinternaltophys.py +39 -0
- odxtools/compumethods/compuinversevalue.py +7 -0
- odxtools/compumethods/compumethod.py +67 -12
- odxtools/compumethods/compuphystointernal.py +39 -0
- odxtools/compumethods/compuscale.py +15 -26
- odxtools/compumethods/createanycompumethod.py +14 -160
- odxtools/compumethods/identicalcompumethod.py +31 -6
- odxtools/compumethods/linearcompumethod.py +69 -189
- odxtools/compumethods/linearsegment.py +193 -0
- odxtools/compumethods/scalelinearcompumethod.py +132 -26
- odxtools/compumethods/tabintpcompumethod.py +119 -99
- odxtools/compumethods/texttablecompumethod.py +107 -43
- odxtools/createanydiagcodedtype.py +10 -67
- odxtools/database.py +68 -62
- odxtools/dataobjectproperty.py +10 -19
- odxtools/description.py +47 -0
- odxtools/determinenumberofitems.py +4 -5
- odxtools/diagcodedtype.py +29 -12
- odxtools/diagcomm.py +10 -6
- odxtools/diagdatadictionaryspec.py +20 -21
- odxtools/diaglayer.py +34 -5
- odxtools/diaglayercontainer.py +17 -11
- odxtools/diaglayerraw.py +20 -21
- odxtools/diagnostictroublecode.py +7 -8
- odxtools/diagservice.py +9 -7
- odxtools/docrevision.py +5 -7
- odxtools/dopbase.py +7 -8
- odxtools/dtcdop.py +5 -8
- odxtools/dynamicendmarkerfield.py +22 -9
- odxtools/dynamiclengthfield.py +5 -11
- odxtools/element.py +4 -3
- odxtools/endofpdufield.py +0 -2
- odxtools/environmentdatadescription.py +4 -6
- odxtools/exceptions.py +1 -1
- odxtools/field.py +9 -9
- odxtools/functionalclass.py +3 -5
- odxtools/inputparam.py +3 -5
- odxtools/leadinglengthinfotype.py +15 -2
- odxtools/loadfile.py +64 -0
- odxtools/minmaxlengthtype.py +20 -2
- odxtools/modification.py +3 -5
- odxtools/multiplexer.py +7 -14
- odxtools/multiplexercase.py +4 -6
- odxtools/multiplexerdefaultcase.py +4 -6
- odxtools/multiplexerswitchkey.py +4 -5
- odxtools/negoutputparam.py +3 -5
- odxtools/outputparam.py +3 -5
- odxtools/parameterinfo.py +3 -3
- odxtools/parameters/codedconstparameter.py +2 -14
- odxtools/parameters/lengthkeyparameter.py +3 -17
- odxtools/parameters/nrcconstparameter.py +2 -14
- odxtools/parameters/parameter.py +22 -22
- odxtools/parameters/parameterwithdop.py +6 -8
- odxtools/parameters/physicalconstantparameter.py +5 -8
- odxtools/parameters/reservedparameter.py +4 -3
- odxtools/parameters/tablekeyparameter.py +6 -9
- odxtools/parameters/tablestructparameter.py +6 -8
- odxtools/parameters/valueparameter.py +5 -8
- odxtools/paramlengthinfotype.py +19 -6
- odxtools/parentref.py +15 -1
- odxtools/physicaldimension.py +3 -5
- odxtools/progcode.py +18 -7
- odxtools/protstack.py +3 -5
- odxtools/py.typed +0 -0
- odxtools/relateddoc.py +7 -9
- odxtools/request.py +8 -0
- odxtools/response.py +8 -0
- odxtools/scaleconstr.py +3 -3
- odxtools/singleecujob.py +12 -10
- odxtools/snrefcontext.py +29 -0
- odxtools/specialdata.py +3 -5
- odxtools/specialdatagroup.py +5 -7
- odxtools/specialdatagroupcaption.py +3 -6
- odxtools/standardlengthtype.py +27 -2
- odxtools/state.py +3 -5
- odxtools/statechart.py +9 -11
- odxtools/statetransition.py +4 -9
- odxtools/staticfield.py +4 -8
- odxtools/table.py +7 -8
- odxtools/tablerow.py +7 -6
- odxtools/teammember.py +3 -5
- odxtools/templates/comparam-spec.odx-c.xml.jinja2 +2 -5
- odxtools/templates/comparam-subset.odx-cs.xml.jinja2 +2 -5
- odxtools/templates/macros/printCompanyData.xml.jinja2 +2 -5
- odxtools/templates/macros/printComparamRef.xml.jinja2 +5 -12
- odxtools/templates/macros/printCompuMethod.xml.jinja2 +153 -0
- odxtools/templates/macros/printDOP.xml.jinja2 +10 -132
- odxtools/templates/macros/printDescription.xml.jinja2 +18 -0
- odxtools/templates/macros/printElementId.xml.jinja2 +3 -3
- odxtools/templates/macros/printMux.xml.jinja2 +3 -2
- odxtools/templates/macros/printTable.xml.jinja2 +2 -3
- odxtools/unit.py +3 -5
- odxtools/unitgroup.py +3 -5
- odxtools/unitspec.py +9 -10
- odxtools/utils.py +1 -26
- odxtools/version.py +2 -2
- odxtools/{write_pdx_file.py → writepdxfile.py} +19 -10
- odxtools/xdoc.py +3 -5
- {odxtools-7.1.0.dist-info → odxtools-7.2.0.dist-info}/METADATA +1 -1
- odxtools-7.2.0.dist-info/RECORD +192 -0
- {odxtools-7.1.0.dist-info → odxtools-7.2.0.dist-info}/WHEEL +1 -1
- odxtools/createcompanydatas.py +0 -17
- odxtools/createsdgs.py +0 -19
- odxtools/load_file.py +0 -13
- odxtools/load_odx_d_file.py +0 -6
- odxtools/load_pdx_file.py +0 -8
- odxtools-7.1.0.dist-info/RECORD +0 -185
- /odxtools/templates/{index.xml.xml.jinja2 → index.xml.jinja2} +0 -0
- {odxtools-7.1.0.dist-info → odxtools-7.2.0.dist-info}/LICENSE +0 -0
- {odxtools-7.1.0.dist-info → odxtools-7.2.0.dist-info}/entry_points.txt +0 -0
- {odxtools-7.1.0.dist-info → odxtools-7.2.0.dist-info}/top_level.txt +0 -0
@@ -1,212 +1,92 @@
|
|
1
1
|
# SPDX-License-Identifier: MIT
|
2
2
|
from dataclasses import dataclass
|
3
|
-
from typing import
|
3
|
+
from typing import List, cast
|
4
|
+
from xml.etree import ElementTree
|
4
5
|
|
5
6
|
from ..exceptions import DecodeError, EncodeError, odxassert, odxraise
|
7
|
+
from ..odxlink import OdxDocFragment
|
6
8
|
from ..odxtypes import AtomicOdxType, DataType
|
7
|
-
from
|
8
|
-
from .
|
9
|
+
from ..utils import dataclass_fields_asdict
|
10
|
+
from .compumethod import CompuCategory, CompuMethod
|
11
|
+
from .linearsegment import LinearSegment
|
9
12
|
|
10
13
|
|
11
14
|
@dataclass
|
12
15
|
class LinearCompuMethod(CompuMethod):
|
13
|
-
"""
|
14
|
-
where d(y) is the physical value and y is the internal value.
|
15
|
-
|
16
|
-
Examples
|
17
|
-
--------
|
18
|
-
|
19
|
-
Define the decoding function `d(y) = 4+2*y` (or equivalent encoding `e(x) = floor((x-4)/2)`)
|
20
|
-
on all integers `y` in the range -10..10 (and `x` in -16..25).
|
21
|
-
|
22
|
-
```python
|
23
|
-
method = LinearCompuMethod(
|
24
|
-
offset=4,
|
25
|
-
factor=2,
|
26
|
-
internal_type=DataType.A_INT32,
|
27
|
-
physical_type=DataType.A_INT32,
|
28
|
-
internal_lower_limit = Limit(-10, IntervalType.CLOSED),
|
29
|
-
internal_upper_limit = Limit(11, IntervalType.OPEN)
|
30
|
-
)
|
31
|
-
```
|
32
|
-
|
33
|
-
Decode an internal value:
|
34
|
-
|
35
|
-
```python
|
36
|
-
>>> method.convert_internal_to_physical(6) # == 4+2*6
|
37
|
-
16
|
38
|
-
```
|
39
|
-
|
40
|
-
Encode a physical value:
|
41
|
-
|
42
|
-
```python
|
43
|
-
>>> method.convert_physical_to_internal(6) # == 6/2-2
|
44
|
-
1
|
45
|
-
```
|
46
|
-
|
47
|
-
Get physical limits:
|
48
|
-
|
49
|
-
```python
|
50
|
-
>>> method.physical_lower_limit
|
51
|
-
Limit(value=-16, interval_type=IntervalType.CLOSED)
|
52
|
-
>>> method.physical_upper_limit
|
53
|
-
Limit(value=26, interval_type=IntervalType.OPEN)
|
54
|
-
```
|
55
|
-
|
56
|
-
(Note that there may be additional restrictions to valid physical values by the surrounding data object prop.
|
57
|
-
For example, limits given by the bit length are not considered in the compu method.)
|
58
|
-
"""
|
59
|
-
|
60
|
-
offset: float
|
61
|
-
factor: float
|
62
|
-
denominator: float
|
63
|
-
internal_lower_limit: Optional[Limit]
|
64
|
-
internal_upper_limit: Optional[Limit]
|
16
|
+
"""A compu method which does linear interpoation
|
65
17
|
|
66
|
-
|
67
|
-
|
18
|
+
i.e. internal values are converted to physical ones using the
|
19
|
+
function `f(x) = (offset + factor * x)/denominator` where `f(x)`
|
20
|
+
is the physical value and `x` is the internal value. In contrast
|
21
|
+
to `ScaleLinearCompuMethod`, this compu method only exhibits a
|
22
|
+
single segment)
|
68
23
|
|
69
|
-
|
24
|
+
For details, refer to ASAM specification MCD-2 D (ODX), section 7.3.6.6.3.
|
25
|
+
"""
|
70
26
|
|
71
27
|
@property
|
72
|
-
def
|
73
|
-
return
|
28
|
+
def segment(self) -> LinearSegment:
|
29
|
+
return self._segment
|
74
30
|
|
75
|
-
@
|
76
|
-
def
|
77
|
-
|
31
|
+
@staticmethod
|
32
|
+
def compu_method_from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment], *,
|
33
|
+
internal_type: DataType,
|
34
|
+
physical_type: DataType) -> "LinearCompuMethod":
|
35
|
+
cm = CompuMethod.compu_method_from_et(
|
36
|
+
et_element, doc_frags, internal_type=internal_type, physical_type=physical_type)
|
37
|
+
kwargs = dataclass_fields_asdict(cm)
|
78
38
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
self.
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
""
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
self.
|
114
|
-
|
115
|
-
|
116
|
-
if self.factor >= 0:
|
117
|
-
self._physical_lower_limit = convert_internal_to_physical_limit(
|
118
|
-
self.internal_lower_limit, False)
|
119
|
-
self._physical_upper_limit = convert_internal_to_physical_limit(
|
120
|
-
self.internal_upper_limit, True)
|
121
|
-
else:
|
122
|
-
# If the factor is negative, the lower and upper limit are swapped
|
123
|
-
self._physical_lower_limit = convert_internal_to_physical_limit(
|
124
|
-
self.internal_upper_limit, True)
|
125
|
-
self._physical_upper_limit = convert_internal_to_physical_limit(
|
126
|
-
self.internal_lower_limit, False)
|
127
|
-
|
128
|
-
def _convert_internal_to_physical(self, internal_value: AtomicOdxType) -> AtomicOdxType:
|
129
|
-
if not isinstance(internal_value, (int, float)):
|
130
|
-
raise DecodeError(f"The type of internal values of linear compumethods must "
|
131
|
-
f"either int or float (is: {type(internal_value).__name__})")
|
132
|
-
|
133
|
-
if self.denominator is None:
|
134
|
-
result = self.offset + self.factor * internal_value
|
135
|
-
else:
|
136
|
-
result = (self.offset + self.factor * internal_value) / self.denominator
|
137
|
-
|
138
|
-
if self.physical_type in [
|
139
|
-
DataType.A_INT32,
|
140
|
-
DataType.A_UINT32,
|
141
|
-
]:
|
142
|
-
result = round(result)
|
143
|
-
|
144
|
-
return self.physical_type.make_from(result)
|
39
|
+
return LinearCompuMethod(**kwargs)
|
40
|
+
|
41
|
+
def __post_init__(self) -> None:
|
42
|
+
odxassert(self.category == CompuCategory.LINEAR,
|
43
|
+
"LinearCompuMethod must exhibit LINEAR category")
|
44
|
+
|
45
|
+
odxassert(self.physical_type in [
|
46
|
+
DataType.A_FLOAT32,
|
47
|
+
DataType.A_FLOAT64,
|
48
|
+
DataType.A_INT32,
|
49
|
+
DataType.A_UINT32,
|
50
|
+
])
|
51
|
+
odxassert(self.internal_type in [
|
52
|
+
DataType.A_FLOAT32,
|
53
|
+
DataType.A_FLOAT64,
|
54
|
+
DataType.A_INT32,
|
55
|
+
DataType.A_UINT32,
|
56
|
+
])
|
57
|
+
|
58
|
+
if self.compu_internal_to_phys is None:
|
59
|
+
odxraise("LINEAR compu methods require COMPU-INTERNAL-TO-PHYS")
|
60
|
+
return
|
61
|
+
|
62
|
+
compu_scales = self.compu_internal_to_phys.compu_scales
|
63
|
+
|
64
|
+
if len(compu_scales) == 0:
|
65
|
+
odxraise("LINEAR compu methods expect at least one compu scale within "
|
66
|
+
"COMPU-INTERNAL-TO-PHYS")
|
67
|
+
return cast(None, LinearCompuMethod)
|
68
|
+
elif len(compu_scales) > 1:
|
69
|
+
odxraise("LINEAR compu methods expect at most one compu scale within "
|
70
|
+
"COMPU-INTERNAL-TO-PHYS")
|
71
|
+
|
72
|
+
scale = compu_scales[0]
|
73
|
+
self._segment = LinearSegment.from_compu_scale(
|
74
|
+
scale, internal_type=self.internal_type, physical_type=self.physical_type)
|
145
75
|
|
146
76
|
def convert_internal_to_physical(self, internal_value: AtomicOdxType) -> AtomicOdxType:
|
147
|
-
|
148
|
-
|
77
|
+
if not self._segment.internal_applies(internal_value):
|
78
|
+
odxraise(r"Cannot decode internal value {internal_value}", DecodeError)
|
79
|
+
|
80
|
+
return self._segment.convert_internal_to_physical(internal_value)
|
149
81
|
|
150
82
|
def convert_physical_to_internal(self, physical_value: AtomicOdxType) -> AtomicOdxType:
|
151
|
-
if not
|
152
|
-
odxraise(
|
153
|
-
|
154
|
-
|
155
|
-
return 0
|
156
|
-
|
157
|
-
odxassert(
|
158
|
-
self.is_valid_physical_value(physical_value),
|
159
|
-
f"physical value {physical_value} of type {type(physical_value)} "
|
160
|
-
f"is not valid. Expected type {self.physical_type} with internal "
|
161
|
-
f"range {self.internal_lower_limit} to {self.internal_upper_limit}")
|
162
|
-
if self.denominator is None:
|
163
|
-
result = (physical_value - self.offset) / self.factor
|
164
|
-
else:
|
165
|
-
result = ((physical_value * self.denominator) - self.offset) / self.factor
|
166
|
-
|
167
|
-
if self.internal_type in [
|
168
|
-
DataType.A_INT32,
|
169
|
-
DataType.A_UINT32,
|
170
|
-
]:
|
171
|
-
result = round(result)
|
172
|
-
return self.internal_type.make_from(result)
|
83
|
+
if not self._segment.physical_applies(physical_value):
|
84
|
+
odxraise(r"Cannot decode physical value {physical_value}", EncodeError)
|
85
|
+
|
86
|
+
return self._segment.convert_physical_to_internal(physical_value)
|
173
87
|
|
174
88
|
def is_valid_physical_value(self, physical_value: AtomicOdxType) -> bool:
|
175
|
-
|
176
|
-
expected_type = self.physical_type.python_type
|
177
|
-
if issubclass(expected_type, float):
|
178
|
-
if not isinstance(physical_value, (int, float)):
|
179
|
-
return False
|
180
|
-
else:
|
181
|
-
if not isinstance(physical_value, expected_type):
|
182
|
-
return False
|
183
|
-
|
184
|
-
# Check the limits
|
185
|
-
if self.physical_lower_limit is not None and not self.physical_lower_limit.complies_to_lower(
|
186
|
-
physical_value):
|
187
|
-
return False
|
188
|
-
if self.physical_upper_limit is not None and not self.physical_upper_limit.complies_to_upper(
|
189
|
-
physical_value):
|
190
|
-
return False
|
191
|
-
|
192
|
-
return True
|
89
|
+
return self._segment.physical_applies(physical_value)
|
193
90
|
|
194
91
|
def is_valid_internal_value(self, internal_value: AtomicOdxType) -> bool:
|
195
|
-
|
196
|
-
expected_type = self.internal_type.python_type
|
197
|
-
if issubclass(expected_type, float):
|
198
|
-
if not isinstance(internal_value, (int, float)):
|
199
|
-
return False
|
200
|
-
else:
|
201
|
-
if not isinstance(internal_value, expected_type):
|
202
|
-
return False
|
203
|
-
|
204
|
-
# Check the limits
|
205
|
-
if self.internal_lower_limit is not None and not self.internal_lower_limit.complies_to_lower(
|
206
|
-
internal_value):
|
207
|
-
return False
|
208
|
-
if self.internal_upper_limit is not None and not self.internal_upper_limit.complies_to_upper(
|
209
|
-
internal_value):
|
210
|
-
return False
|
211
|
-
|
212
|
-
return True
|
92
|
+
return self._segment.internal_applies(internal_value)
|
@@ -0,0 +1,193 @@
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
2
|
+
from dataclasses import dataclass
|
3
|
+
from typing import Optional, Union
|
4
|
+
|
5
|
+
from ..exceptions import odxraise, odxrequire
|
6
|
+
from ..odxtypes import AtomicOdxType, DataType
|
7
|
+
from .compuscale import CompuScale
|
8
|
+
from .limit import Limit
|
9
|
+
|
10
|
+
|
11
|
+
@dataclass
|
12
|
+
class LinearSegment:
|
13
|
+
"""Helper class to represent a segment of a piecewise-linear interpolation.
|
14
|
+
|
15
|
+
Multiple compu methods (LINEAR, SCALE-LINEAR, TAB-INTP) require
|
16
|
+
linear interpolation. This class centralizes the required
|
17
|
+
parameters for a single such segment. (The required parameters are
|
18
|
+
extracted from the respective compu method's
|
19
|
+
COMPU-INTERNAL-TO-PHYS objects. We do it this way because the
|
20
|
+
internal-to-phys objects are rather clunky to work with and
|
21
|
+
feature a lot of irrelevant information.)
|
22
|
+
|
23
|
+
"""
|
24
|
+
offset: float
|
25
|
+
factor: float
|
26
|
+
denominator: float
|
27
|
+
internal_lower_limit: Optional[Limit]
|
28
|
+
internal_upper_limit: Optional[Limit]
|
29
|
+
|
30
|
+
inverse_value: Union[int, float] # value used as inverse if factor is 0
|
31
|
+
|
32
|
+
internal_type: DataType
|
33
|
+
physical_type: DataType
|
34
|
+
|
35
|
+
@staticmethod
|
36
|
+
def from_compu_scale(scale: CompuScale, *, internal_type: DataType,
|
37
|
+
physical_type: DataType) -> "LinearSegment":
|
38
|
+
coeffs = odxrequire(scale.compu_rational_coeffs)
|
39
|
+
|
40
|
+
offset = coeffs.numerators[0]
|
41
|
+
factor = coeffs.numerators[1]
|
42
|
+
|
43
|
+
denominator = 1.0
|
44
|
+
if len(coeffs.denominators) > 0:
|
45
|
+
denominator = coeffs.denominators[0]
|
46
|
+
|
47
|
+
inverse_value: Union[int, float] = 0
|
48
|
+
if scale.compu_inverse_value is not None:
|
49
|
+
if abs(factor) < 1e-10:
|
50
|
+
odxraise(f"COMPU-INVERSE-VALUE for non-zero slope ({factor}) defined")
|
51
|
+
x = odxrequire(scale.compu_inverse_value).value
|
52
|
+
if not isinstance(x, (int, float)):
|
53
|
+
odxraise(f"Non-numeric COMPU-INVERSE-VALUE specified ({x!r})")
|
54
|
+
inverse_value = x
|
55
|
+
|
56
|
+
internal_lower_limit = scale.lower_limit
|
57
|
+
internal_upper_limit = scale.upper_limit
|
58
|
+
|
59
|
+
return LinearSegment(
|
60
|
+
offset=offset,
|
61
|
+
factor=factor,
|
62
|
+
denominator=denominator,
|
63
|
+
internal_lower_limit=internal_lower_limit,
|
64
|
+
internal_upper_limit=internal_upper_limit,
|
65
|
+
inverse_value=inverse_value,
|
66
|
+
internal_type=internal_type,
|
67
|
+
physical_type=physical_type)
|
68
|
+
|
69
|
+
@property
|
70
|
+
def physical_lower_limit(self) -> Optional[Limit]:
|
71
|
+
return self._physical_lower_limit
|
72
|
+
|
73
|
+
@property
|
74
|
+
def physical_upper_limit(self) -> Optional[Limit]:
|
75
|
+
return self._physical_upper_limit
|
76
|
+
|
77
|
+
def __post_init__(self) -> None:
|
78
|
+
self.__compute_physical_limits()
|
79
|
+
|
80
|
+
def convert_internal_to_physical(self, internal_value: AtomicOdxType) -> Union[float, int]:
|
81
|
+
if not isinstance(internal_value, (int, float)):
|
82
|
+
odxraise(f"Internal values of linear compumethods must "
|
83
|
+
f"either be int or float (is: {type(internal_value).__name__})")
|
84
|
+
|
85
|
+
result = (self.offset + self.factor * internal_value) / self.denominator
|
86
|
+
|
87
|
+
if self.physical_type in [
|
88
|
+
DataType.A_INT32,
|
89
|
+
DataType.A_UINT32,
|
90
|
+
]:
|
91
|
+
result = round(result)
|
92
|
+
|
93
|
+
return result
|
94
|
+
|
95
|
+
def convert_physical_to_internal(self, physical_value: AtomicOdxType) -> Union[float, int]:
|
96
|
+
if not isinstance(physical_value, (int, float)):
|
97
|
+
odxraise(f"Physical values of linear compumethods must "
|
98
|
+
f"either be int or float (is: {type(physical_value).__name__})")
|
99
|
+
|
100
|
+
if abs(self.factor) < 1e-10:
|
101
|
+
# "If factor = 0 then COMPU-INVERSE-VALUE shall be specified.
|
102
|
+
return self.inverse_value
|
103
|
+
|
104
|
+
result = (physical_value * self.denominator - self.offset) / self.factor
|
105
|
+
|
106
|
+
if self.internal_type in [
|
107
|
+
DataType.A_INT32,
|
108
|
+
DataType.A_UINT32,
|
109
|
+
]:
|
110
|
+
result = round(result)
|
111
|
+
|
112
|
+
return result
|
113
|
+
|
114
|
+
def __compute_physical_limits(self) -> None:
|
115
|
+
"""Computes the physical limits and stores them in the properties
|
116
|
+
self._physical_lower_limit and self._physical_upper_limit.
|
117
|
+
This method is called by `__post_init__()`.
|
118
|
+
"""
|
119
|
+
|
120
|
+
def convert_internal_to_physical_limit(internal_limit: Optional[Limit],
|
121
|
+
is_upper_limit: bool) -> Optional[Limit]:
|
122
|
+
"""Helper method to convert a single internal limit
|
123
|
+
"""
|
124
|
+
if internal_limit is None or internal_limit.value_raw is None:
|
125
|
+
return None
|
126
|
+
|
127
|
+
internal_value = self.internal_type.from_string(internal_limit.value_raw)
|
128
|
+
physical_value = self.convert_internal_to_physical(internal_value)
|
129
|
+
|
130
|
+
result = Limit(
|
131
|
+
value_raw=str(physical_value),
|
132
|
+
value_type=self.physical_type,
|
133
|
+
interval_type=internal_limit.interval_type)
|
134
|
+
|
135
|
+
return result
|
136
|
+
|
137
|
+
self._physical_lower_limit = None
|
138
|
+
self._physical_upper_limit = None
|
139
|
+
|
140
|
+
if self.factor >= 0:
|
141
|
+
self._physical_lower_limit = convert_internal_to_physical_limit(
|
142
|
+
self.internal_lower_limit, False)
|
143
|
+
self._physical_upper_limit = convert_internal_to_physical_limit(
|
144
|
+
self.internal_upper_limit, True)
|
145
|
+
else:
|
146
|
+
# If the scaling factor is negative, the lower and upper
|
147
|
+
# limit are swapped
|
148
|
+
self._physical_lower_limit = convert_internal_to_physical_limit(
|
149
|
+
self.internal_upper_limit, True)
|
150
|
+
self._physical_upper_limit = convert_internal_to_physical_limit(
|
151
|
+
self.internal_lower_limit, False)
|
152
|
+
|
153
|
+
def physical_applies(self, physical_value: AtomicOdxType) -> bool:
|
154
|
+
"""Returns True iff the segment is applicable to a given physical value"""
|
155
|
+
# Do type checks
|
156
|
+
expected_type = self.physical_type.python_type
|
157
|
+
if issubclass(expected_type, float):
|
158
|
+
if not isinstance(physical_value, (int, float)):
|
159
|
+
return False
|
160
|
+
else:
|
161
|
+
if not isinstance(physical_value, expected_type):
|
162
|
+
return False
|
163
|
+
|
164
|
+
if self._physical_lower_limit is not None and \
|
165
|
+
not self._physical_lower_limit.complies_to_lower(physical_value):
|
166
|
+
return False
|
167
|
+
|
168
|
+
if self._physical_upper_limit is not None and \
|
169
|
+
not self._physical_upper_limit.complies_to_upper(physical_value):
|
170
|
+
return False
|
171
|
+
|
172
|
+
return True
|
173
|
+
|
174
|
+
def internal_applies(self, internal_value: AtomicOdxType) -> bool:
|
175
|
+
"""Returns True iff the segment is applicable to a given internal value"""
|
176
|
+
# Do type checks
|
177
|
+
expected_type = self.internal_type.python_type
|
178
|
+
if issubclass(expected_type, float):
|
179
|
+
if not isinstance(internal_value, (int, float)):
|
180
|
+
return False
|
181
|
+
else:
|
182
|
+
if not isinstance(internal_value, expected_type):
|
183
|
+
return False
|
184
|
+
|
185
|
+
if self.internal_lower_limit is not None and \
|
186
|
+
not self.internal_lower_limit.complies_to_lower(internal_value):
|
187
|
+
return False
|
188
|
+
|
189
|
+
if self.internal_upper_limit is not None and \
|
190
|
+
not self.internal_upper_limit.complies_to_upper(internal_value):
|
191
|
+
return False
|
192
|
+
|
193
|
+
return True
|
@@ -1,39 +1,145 @@
|
|
1
1
|
# SPDX-License-Identifier: MIT
|
2
2
|
from dataclasses import dataclass
|
3
|
-
from typing import List
|
3
|
+
from typing import List, Union, cast
|
4
|
+
from xml.etree import ElementTree
|
4
5
|
|
5
|
-
from ..exceptions import odxassert
|
6
|
-
from ..
|
7
|
-
from
|
8
|
-
from
|
6
|
+
from ..exceptions import DecodeError, EncodeError, odxassert, odxraise
|
7
|
+
from ..odxlink import OdxDocFragment
|
8
|
+
from ..odxtypes import AtomicOdxType, DataType
|
9
|
+
from ..utils import dataclass_fields_asdict
|
10
|
+
from .compumethod import CompuCategory, CompuMethod
|
11
|
+
from .limit import IntervalType
|
12
|
+
from .linearsegment import LinearSegment
|
9
13
|
|
10
14
|
|
11
15
|
@dataclass
|
12
16
|
class ScaleLinearCompuMethod(CompuMethod):
|
13
|
-
|
17
|
+
"""A piecewise linear compu method which may feature discontinuities.
|
18
|
+
|
19
|
+
For details, refer to ASAM specification MCD-2 D (ODX), section 7.3.6.6.4.
|
20
|
+
"""
|
14
21
|
|
15
22
|
@property
|
16
|
-
def
|
17
|
-
return
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
23
|
+
def segments(self) -> List[LinearSegment]:
|
24
|
+
return self._segments
|
25
|
+
|
26
|
+
@staticmethod
|
27
|
+
def compu_method_from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment], *,
|
28
|
+
internal_type: DataType,
|
29
|
+
physical_type: DataType) -> "ScaleLinearCompuMethod":
|
30
|
+
cm = CompuMethod.compu_method_from_et(
|
31
|
+
et_element, doc_frags, internal_type=internal_type, physical_type=physical_type)
|
32
|
+
kwargs = dataclass_fields_asdict(cm)
|
33
|
+
|
34
|
+
return ScaleLinearCompuMethod(**kwargs)
|
35
|
+
|
36
|
+
def __post_init__(self) -> None:
|
37
|
+
self._segments: List[LinearSegment] = []
|
38
|
+
|
39
|
+
odxassert(self.category == CompuCategory.SCALE_LINEAR,
|
40
|
+
"ScaleLinearCompuMethod must exibit SCALE-LINEAR category")
|
41
|
+
|
42
|
+
odxassert(self.physical_type in [
|
43
|
+
DataType.A_FLOAT32,
|
44
|
+
DataType.A_FLOAT64,
|
45
|
+
DataType.A_INT32,
|
46
|
+
DataType.A_UINT32,
|
47
|
+
])
|
48
|
+
odxassert(self.internal_type in [
|
49
|
+
DataType.A_FLOAT32,
|
50
|
+
DataType.A_FLOAT64,
|
51
|
+
DataType.A_INT32,
|
52
|
+
DataType.A_UINT32,
|
53
|
+
])
|
54
|
+
|
55
|
+
if self.compu_internal_to_phys is None:
|
56
|
+
odxraise("SCALE-LINEAR compu methods require COMPU-INTERNAL-TO-PHYS")
|
57
|
+
return
|
58
|
+
|
59
|
+
compu_scales = self.compu_internal_to_phys.compu_scales
|
60
|
+
|
61
|
+
for scale in compu_scales:
|
62
|
+
self._segments.append(
|
63
|
+
LinearSegment.from_compu_scale(
|
64
|
+
scale, internal_type=self.internal_type, physical_type=self.physical_type))
|
65
|
+
|
66
|
+
# find out if the transfer function is invertible (i.e. if it
|
67
|
+
# can be encoded by normal means). section 7.3.6.6.4 of the
|
68
|
+
# ODX specification states that the condition for
|
69
|
+
# invertibility is that adjacent COMPU-SCALES exhibit the same
|
70
|
+
# values on their common boundaries and that the slope in all
|
71
|
+
# intervals exhibit the same sign (or are 0). For segments
|
72
|
+
# with a slope of zero, COMPU-INVERSE-VALUE shall be used.
|
73
|
+
self._is_invertible = True
|
74
|
+
ref_factor = self._segments[0].factor
|
75
|
+
for i in range(0, len(self._segments) - 1):
|
76
|
+
s0 = self.segments[i]
|
77
|
+
s1 = self.segments[i + 1]
|
78
|
+
|
79
|
+
if ref_factor * s1.factor < 0:
|
80
|
+
self._is_invertible = False
|
81
|
+
break
|
82
|
+
if s1.factor != 0:
|
83
|
+
ref_factor = s1.factor
|
84
|
+
|
85
|
+
# both interval boundaries must not be infinite
|
86
|
+
if s0.internal_upper_limit is None or \
|
87
|
+
s1.internal_lower_limit is None:
|
88
|
+
self._is_invertible = False
|
89
|
+
break
|
90
|
+
elif s0.internal_upper_limit.value is None or \
|
91
|
+
s1.internal_lower_limit.value is None or \
|
92
|
+
s0.internal_upper_limit.interval_type == IntervalType.INFINITE or \
|
93
|
+
s1.internal_lower_limit.interval_type == IntervalType.INFINITE:
|
94
|
+
self._is_invertible = False
|
95
|
+
break
|
96
|
+
|
97
|
+
# the intervals must use the same reference point
|
98
|
+
if (x := s0.internal_upper_limit.value) != s1.internal_lower_limit.value:
|
99
|
+
self._is_invertible = False
|
100
|
+
break
|
101
|
+
|
102
|
+
if not isinstance(x, (int, float)):
|
103
|
+
odxraise("Linear segments must use int or float for all quantities")
|
104
|
+
|
105
|
+
# the respective function value at the interval's
|
106
|
+
# reference point must be identical
|
107
|
+
y0 = s0.convert_internal_to_physical(x)
|
108
|
+
y1 = s1.convert_internal_to_physical(x)
|
109
|
+
if abs(y0 - y1) < 1e-10:
|
110
|
+
self._is_invertible = False
|
111
|
+
break
|
112
|
+
|
113
|
+
def convert_physical_to_internal(self, physical_value: AtomicOdxType) -> Union[float, int]:
|
114
|
+
if not self._is_invertible:
|
115
|
+
odxraise(
|
116
|
+
f"Trying to encode value {physical_value!r} using a non-invertible "
|
117
|
+
f"SCALE-LINEAR transfer function", EncodeError)
|
118
|
+
|
119
|
+
applicable_segments = [
|
120
|
+
seg for seg in self._segments if seg.physical_applies(physical_value)
|
121
|
+
]
|
122
|
+
if not applicable_segments:
|
123
|
+
odxraise(r"No applicable segment for value {physical_value} found", EncodeError)
|
124
|
+
return cast(int, None)
|
125
|
+
|
126
|
+
seg = applicable_segments[0]
|
127
|
+
|
128
|
+
return seg.convert_physical_to_internal(physical_value)
|
129
|
+
|
130
|
+
def convert_internal_to_physical(self, internal_value: AtomicOdxType) -> Union[float, int]:
|
131
|
+
applicable_segments = [
|
132
|
+
seg for seg in self._segments if seg.internal_applies(internal_value)
|
133
|
+
]
|
134
|
+
if not applicable_segments:
|
135
|
+
odxraise(r"No applicable segment for value {internal_value} found", DecodeError)
|
136
|
+
return cast(int, None)
|
137
|
+
|
138
|
+
seg = applicable_segments[0]
|
139
|
+
return seg.convert_internal_to_physical(internal_value)
|
32
140
|
|
33
141
|
def is_valid_physical_value(self, physical_value: AtomicOdxType) -> bool:
|
34
|
-
return any(
|
35
|
-
True for scale in self.linear_methods if scale.is_valid_physical_value(physical_value))
|
142
|
+
return any(True for seg in self._segments if seg.physical_applies(physical_value))
|
36
143
|
|
37
144
|
def is_valid_internal_value(self, internal_value: AtomicOdxType) -> bool:
|
38
|
-
return any(
|
39
|
-
True for scale in self.linear_methods if scale.is_valid_internal_value(internal_value))
|
145
|
+
return any(True for seg in self._segments if seg.internal_applies(internal_value))
|