odxtools 7.1.1__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.
Files changed (130) hide show
  1. odxtools/__init__.py +6 -4
  2. odxtools/additionalaudience.py +3 -5
  3. odxtools/admindata.py +5 -7
  4. odxtools/audience.py +3 -5
  5. odxtools/basecomparam.py +3 -5
  6. odxtools/basicstructure.py +10 -17
  7. odxtools/cli/_parser_utils.py +1 -1
  8. odxtools/cli/_print_utils.py +3 -2
  9. odxtools/cli/compare.py +1 -1
  10. odxtools/companydata.py +5 -7
  11. odxtools/companydocinfo.py +7 -8
  12. odxtools/companyrevisioninfo.py +3 -5
  13. odxtools/companyspecificinfo.py +8 -9
  14. odxtools/comparam.py +4 -6
  15. odxtools/comparaminstance.py +6 -8
  16. odxtools/comparamspec.py +14 -13
  17. odxtools/comparamsubset.py +17 -16
  18. odxtools/complexcomparam.py +5 -7
  19. odxtools/compumethods/compuconst.py +31 -0
  20. odxtools/compumethods/compudefaultvalue.py +27 -0
  21. odxtools/compumethods/compuinternaltophys.py +39 -0
  22. odxtools/compumethods/compuinversevalue.py +7 -0
  23. odxtools/compumethods/compumethod.py +67 -12
  24. odxtools/compumethods/compuphystointernal.py +39 -0
  25. odxtools/compumethods/compuscale.py +15 -26
  26. odxtools/compumethods/createanycompumethod.py +14 -160
  27. odxtools/compumethods/identicalcompumethod.py +31 -6
  28. odxtools/compumethods/linearcompumethod.py +69 -189
  29. odxtools/compumethods/linearsegment.py +193 -0
  30. odxtools/compumethods/scalelinearcompumethod.py +132 -26
  31. odxtools/compumethods/tabintpcompumethod.py +119 -99
  32. odxtools/compumethods/texttablecompumethod.py +107 -43
  33. odxtools/createanydiagcodedtype.py +10 -67
  34. odxtools/database.py +68 -62
  35. odxtools/dataobjectproperty.py +10 -19
  36. odxtools/description.py +47 -0
  37. odxtools/determinenumberofitems.py +4 -5
  38. odxtools/diagcodedtype.py +29 -12
  39. odxtools/diagcomm.py +10 -6
  40. odxtools/diagdatadictionaryspec.py +20 -21
  41. odxtools/diaglayer.py +34 -5
  42. odxtools/diaglayercontainer.py +17 -11
  43. odxtools/diaglayerraw.py +20 -21
  44. odxtools/diagnostictroublecode.py +7 -8
  45. odxtools/diagservice.py +9 -7
  46. odxtools/docrevision.py +5 -7
  47. odxtools/dopbase.py +7 -8
  48. odxtools/dtcdop.py +5 -8
  49. odxtools/dynamicendmarkerfield.py +22 -9
  50. odxtools/dynamiclengthfield.py +5 -11
  51. odxtools/element.py +4 -3
  52. odxtools/endofpdufield.py +0 -2
  53. odxtools/environmentdatadescription.py +4 -6
  54. odxtools/exceptions.py +1 -1
  55. odxtools/field.py +9 -9
  56. odxtools/functionalclass.py +3 -5
  57. odxtools/inputparam.py +3 -5
  58. odxtools/leadinglengthinfotype.py +15 -2
  59. odxtools/loadfile.py +64 -0
  60. odxtools/minmaxlengthtype.py +20 -2
  61. odxtools/modification.py +3 -5
  62. odxtools/multiplexer.py +7 -14
  63. odxtools/multiplexercase.py +4 -6
  64. odxtools/multiplexerdefaultcase.py +4 -6
  65. odxtools/multiplexerswitchkey.py +4 -5
  66. odxtools/negoutputparam.py +3 -5
  67. odxtools/outputparam.py +3 -5
  68. odxtools/parameterinfo.py +3 -3
  69. odxtools/parameters/codedconstparameter.py +2 -14
  70. odxtools/parameters/lengthkeyparameter.py +3 -17
  71. odxtools/parameters/nrcconstparameter.py +2 -14
  72. odxtools/parameters/parameter.py +22 -22
  73. odxtools/parameters/parameterwithdop.py +6 -8
  74. odxtools/parameters/physicalconstantparameter.py +5 -8
  75. odxtools/parameters/reservedparameter.py +4 -3
  76. odxtools/parameters/tablekeyparameter.py +6 -9
  77. odxtools/parameters/tablestructparameter.py +6 -8
  78. odxtools/parameters/valueparameter.py +5 -8
  79. odxtools/paramlengthinfotype.py +19 -6
  80. odxtools/parentref.py +15 -1
  81. odxtools/physicaldimension.py +3 -5
  82. odxtools/progcode.py +18 -7
  83. odxtools/protstack.py +3 -5
  84. odxtools/relateddoc.py +7 -9
  85. odxtools/request.py +8 -0
  86. odxtools/response.py +8 -0
  87. odxtools/scaleconstr.py +3 -3
  88. odxtools/singleecujob.py +12 -10
  89. odxtools/snrefcontext.py +29 -0
  90. odxtools/specialdata.py +3 -5
  91. odxtools/specialdatagroup.py +5 -7
  92. odxtools/specialdatagroupcaption.py +3 -6
  93. odxtools/standardlengthtype.py +27 -2
  94. odxtools/state.py +3 -5
  95. odxtools/statechart.py +9 -11
  96. odxtools/statetransition.py +4 -9
  97. odxtools/staticfield.py +4 -8
  98. odxtools/table.py +7 -8
  99. odxtools/tablerow.py +7 -6
  100. odxtools/teammember.py +3 -5
  101. odxtools/templates/comparam-spec.odx-c.xml.jinja2 +2 -5
  102. odxtools/templates/comparam-subset.odx-cs.xml.jinja2 +2 -5
  103. odxtools/templates/macros/printCompanyData.xml.jinja2 +2 -5
  104. odxtools/templates/macros/printComparamRef.xml.jinja2 +5 -12
  105. odxtools/templates/macros/printCompuMethod.xml.jinja2 +153 -0
  106. odxtools/templates/macros/printDOP.xml.jinja2 +10 -132
  107. odxtools/templates/macros/printDescription.xml.jinja2 +18 -0
  108. odxtools/templates/macros/printElementId.xml.jinja2 +3 -3
  109. odxtools/templates/macros/printMux.xml.jinja2 +3 -2
  110. odxtools/templates/macros/printTable.xml.jinja2 +2 -3
  111. odxtools/unit.py +3 -5
  112. odxtools/unitgroup.py +3 -5
  113. odxtools/unitspec.py +9 -10
  114. odxtools/utils.py +1 -26
  115. odxtools/version.py +2 -2
  116. odxtools/{write_pdx_file.py → writepdxfile.py} +19 -10
  117. odxtools/xdoc.py +3 -5
  118. {odxtools-7.1.1.dist-info → odxtools-7.2.0.dist-info}/METADATA +1 -1
  119. odxtools-7.2.0.dist-info/RECORD +192 -0
  120. {odxtools-7.1.1.dist-info → odxtools-7.2.0.dist-info}/WHEEL +1 -1
  121. odxtools/createcompanydatas.py +0 -17
  122. odxtools/createsdgs.py +0 -19
  123. odxtools/load_file.py +0 -13
  124. odxtools/load_odx_d_file.py +0 -6
  125. odxtools/load_pdx_file.py +0 -8
  126. odxtools-7.1.1.dist-info/RECORD +0 -186
  127. /odxtools/templates/{index.xml.xml.jinja2 → index.xml.jinja2} +0 -0
  128. {odxtools-7.1.1.dist-info → odxtools-7.2.0.dist-info}/LICENSE +0 -0
  129. {odxtools-7.1.1.dist-info → odxtools-7.2.0.dist-info}/entry_points.txt +0 -0
  130. {odxtools-7.1.1.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 Optional
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 .compumethod import CompuMethod, CompuMethodCategory
8
- from .limit import Limit
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
- """Represents the decoding function d(y) = (offset + factor * y) / denominator
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
- def __post_init__(self) -> None:
67
- odxassert(self.denominator > 0)
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
- self.__compute_physical_limits()
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 category(self) -> CompuMethodCategory:
73
- return "LINEAR"
28
+ def segment(self) -> LinearSegment:
29
+ return self._segment
74
30
 
75
- @property
76
- def physical_lower_limit(self) -> Optional[Limit]:
77
- return self._physical_lower_limit
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
- @property
80
- def physical_upper_limit(self) -> Optional[Limit]:
81
- return self._physical_upper_limit
82
-
83
- def __compute_physical_limits(self) -> None:
84
- """Computes the physical limits and stores them in the properties
85
- self._physical_lower_limit and self._physical_upper_limit.
86
- This method is only called during the initialization of a LinearCompuMethod.
87
- """
88
-
89
- def convert_internal_to_physical_limit(internal_limit: Optional[Limit],
90
- is_upper_limit: bool) -> Optional[Limit]:
91
- """Helper method
92
-
93
- Parameters:
94
-
95
- internal_limit
96
- the internal limit to be converted to a physical limit
97
- is_upper_limit
98
- True iff limit is the internal upper limit
99
- """
100
- if internal_limit is None or internal_limit.value_raw is None:
101
- return None
102
-
103
- internal_value = self.internal_type.from_string(internal_limit.value_raw)
104
- physical_value = self._convert_internal_to_physical(internal_value)
105
-
106
- result = Limit(
107
- value_raw=str(physical_value),
108
- value_type=self.physical_type,
109
- interval_type=internal_limit.interval_type)
110
-
111
- return result
112
-
113
- self._physical_lower_limit = None
114
- self._physical_upper_limit = None
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
- odxassert(self.is_valid_internal_value(internal_value))
148
- return self._convert_internal_to_physical(internal_value)
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 isinstance(physical_value, (int, float)):
152
- odxraise(
153
- "The type of physical values of linear compumethods must "
154
- "either int or float", EncodeError)
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
- # Do type checks
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
- # Do type checks
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 ..odxtypes import AtomicOdxType
7
- from .compumethod import CompuMethod, CompuMethodCategory
8
- from .linearcompumethod import LinearCompuMethod
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
- linear_methods: List[LinearCompuMethod]
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 category(self) -> CompuMethodCategory:
17
- return "SCALE-LINEAR"
18
-
19
- def convert_physical_to_internal(self, physical_value: AtomicOdxType) -> AtomicOdxType:
20
- odxassert(
21
- self.is_valid_physical_value(physical_value),
22
- f"cannot convert the invalid physical value {physical_value!r} "
23
- f"of type {type(physical_value)}")
24
- lin_method = next(
25
- scale for scale in self.linear_methods if scale.is_valid_physical_value(physical_value))
26
- return lin_method.convert_physical_to_internal(physical_value)
27
-
28
- def convert_internal_to_physical(self, internal_value: AtomicOdxType) -> AtomicOdxType:
29
- lin_method = next(
30
- scale for scale in self.linear_methods if scale.is_valid_internal_value(internal_value))
31
- return lin_method.convert_internal_to_physical(internal_value)
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))