odxtools 6.6.1__py3-none-any.whl → 9.3.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 (222) hide show
  1. odxtools/__init__.py +7 -5
  2. odxtools/additionalaudience.py +3 -5
  3. odxtools/admindata.py +5 -7
  4. odxtools/audience.py +10 -13
  5. odxtools/basecomparam.py +3 -5
  6. odxtools/basicstructure.py +55 -241
  7. odxtools/cli/_parser_utils.py +16 -1
  8. odxtools/cli/_print_utils.py +169 -134
  9. odxtools/cli/browse.py +127 -103
  10. odxtools/cli/compare.py +114 -87
  11. odxtools/cli/decode.py +2 -1
  12. odxtools/cli/dummy_sub_parser.py +3 -1
  13. odxtools/cli/find.py +2 -1
  14. odxtools/cli/list.py +26 -16
  15. odxtools/cli/main.py +1 -0
  16. odxtools/cli/snoop.py +32 -6
  17. odxtools/codec.py +211 -0
  18. odxtools/commrelation.py +122 -0
  19. odxtools/companydata.py +5 -7
  20. odxtools/companydocinfo.py +7 -8
  21. odxtools/companyrevisioninfo.py +3 -5
  22. odxtools/companyspecificinfo.py +8 -9
  23. odxtools/comparam.py +4 -6
  24. odxtools/comparaminstance.py +14 -14
  25. odxtools/comparamspec.py +16 -54
  26. odxtools/comparamsubset.py +22 -62
  27. odxtools/complexcomparam.py +5 -7
  28. odxtools/compumethods/compucodecompumethod.py +63 -0
  29. odxtools/compumethods/compuconst.py +31 -0
  30. odxtools/compumethods/compudefaultvalue.py +27 -0
  31. odxtools/compumethods/compuinternaltophys.py +56 -0
  32. odxtools/compumethods/compuinversevalue.py +7 -0
  33. odxtools/compumethods/compumethod.py +94 -15
  34. odxtools/compumethods/compuphystointernal.py +56 -0
  35. odxtools/compumethods/compurationalcoeffs.py +20 -9
  36. odxtools/compumethods/compuscale.py +67 -32
  37. odxtools/compumethods/createanycompumethod.py +31 -172
  38. odxtools/compumethods/identicalcompumethod.py +31 -6
  39. odxtools/compumethods/limit.py +70 -36
  40. odxtools/compumethods/linearcompumethod.py +70 -181
  41. odxtools/compumethods/linearsegment.py +190 -0
  42. odxtools/compumethods/ratfunccompumethod.py +106 -0
  43. odxtools/compumethods/ratfuncsegment.py +87 -0
  44. odxtools/compumethods/scalelinearcompumethod.py +132 -26
  45. odxtools/compumethods/scaleratfunccompumethod.py +113 -0
  46. odxtools/compumethods/tabintpcompumethod.py +123 -92
  47. odxtools/compumethods/texttablecompumethod.py +117 -57
  48. odxtools/createanydiagcodedtype.py +10 -67
  49. odxtools/database.py +167 -87
  50. odxtools/dataobjectproperty.py +25 -32
  51. odxtools/decodestate.py +14 -17
  52. odxtools/description.py +47 -0
  53. odxtools/determinenumberofitems.py +4 -5
  54. odxtools/diagcodedtype.py +37 -106
  55. odxtools/diagcomm.py +24 -12
  56. odxtools/diagdatadictionaryspec.py +120 -96
  57. odxtools/diaglayercontainer.py +46 -54
  58. odxtools/diaglayers/basevariant.py +128 -0
  59. odxtools/diaglayers/basevariantraw.py +123 -0
  60. odxtools/diaglayers/diaglayer.py +432 -0
  61. odxtools/{diaglayerraw.py → diaglayers/diaglayerraw.py} +105 -120
  62. odxtools/diaglayers/diaglayertype.py +42 -0
  63. odxtools/diaglayers/ecushareddata.py +96 -0
  64. odxtools/diaglayers/ecushareddataraw.py +87 -0
  65. odxtools/diaglayers/ecuvariant.py +124 -0
  66. odxtools/diaglayers/ecuvariantraw.py +129 -0
  67. odxtools/diaglayers/functionalgroup.py +110 -0
  68. odxtools/diaglayers/functionalgroupraw.py +106 -0
  69. odxtools/{diaglayer.py → diaglayers/hierarchyelement.py} +273 -472
  70. odxtools/diaglayers/hierarchyelementraw.py +58 -0
  71. odxtools/diaglayers/protocol.py +64 -0
  72. odxtools/diaglayers/protocolraw.py +91 -0
  73. odxtools/diagnostictroublecode.py +8 -9
  74. odxtools/diagservice.py +57 -44
  75. odxtools/diagvariable.py +113 -0
  76. odxtools/docrevision.py +5 -7
  77. odxtools/dopbase.py +15 -15
  78. odxtools/dtcdop.py +170 -50
  79. odxtools/dynamicendmarkerfield.py +134 -0
  80. odxtools/dynamiclengthfield.py +47 -42
  81. odxtools/dyndefinedspec.py +177 -0
  82. odxtools/dynenddopref.py +38 -0
  83. odxtools/ecuvariantmatcher.py +6 -7
  84. odxtools/element.py +13 -15
  85. odxtools/encodestate.py +199 -22
  86. odxtools/endofpdufield.py +31 -18
  87. odxtools/environmentdata.py +8 -1
  88. odxtools/environmentdatadescription.py +198 -36
  89. odxtools/exceptions.py +11 -2
  90. odxtools/field.py +10 -10
  91. odxtools/functionalclass.py +3 -5
  92. odxtools/inputparam.py +3 -12
  93. odxtools/internalconstr.py +14 -5
  94. odxtools/isotp_state_machine.py +14 -6
  95. odxtools/leadinglengthinfotype.py +37 -18
  96. odxtools/library.py +66 -0
  97. odxtools/loadfile.py +64 -0
  98. odxtools/matchingparameter.py +3 -3
  99. odxtools/message.py +0 -7
  100. odxtools/minmaxlengthtype.py +61 -33
  101. odxtools/modification.py +3 -5
  102. odxtools/multiplexer.py +135 -75
  103. odxtools/multiplexercase.py +39 -18
  104. odxtools/multiplexerdefaultcase.py +15 -12
  105. odxtools/multiplexerswitchkey.py +4 -5
  106. odxtools/nameditemlist.py +33 -8
  107. odxtools/negoutputparam.py +3 -5
  108. odxtools/odxcategory.py +83 -0
  109. odxtools/odxlink.py +62 -53
  110. odxtools/odxtypes.py +93 -8
  111. odxtools/outputparam.py +5 -16
  112. odxtools/parameterinfo.py +219 -61
  113. odxtools/parameters/codedconstparameter.py +45 -32
  114. odxtools/parameters/createanyparameter.py +19 -193
  115. odxtools/parameters/dynamicparameter.py +25 -4
  116. odxtools/parameters/lengthkeyparameter.py +83 -25
  117. odxtools/parameters/matchingrequestparameter.py +48 -18
  118. odxtools/parameters/nrcconstparameter.py +76 -54
  119. odxtools/parameters/parameter.py +97 -73
  120. odxtools/parameters/parameterwithdop.py +41 -38
  121. odxtools/parameters/physicalconstantparameter.py +41 -20
  122. odxtools/parameters/reservedparameter.py +36 -18
  123. odxtools/parameters/systemparameter.py +74 -7
  124. odxtools/parameters/tableentryparameter.py +47 -7
  125. odxtools/parameters/tablekeyparameter.py +142 -55
  126. odxtools/parameters/tablestructparameter.py +79 -58
  127. odxtools/parameters/valueparameter.py +39 -21
  128. odxtools/paramlengthinfotype.py +56 -33
  129. odxtools/parentref.py +20 -3
  130. odxtools/physicaldimension.py +3 -8
  131. odxtools/progcode.py +26 -11
  132. odxtools/protstack.py +3 -5
  133. odxtools/py.typed +0 -0
  134. odxtools/relateddoc.py +7 -9
  135. odxtools/request.py +120 -10
  136. odxtools/response.py +123 -23
  137. odxtools/scaleconstr.py +14 -8
  138. odxtools/servicebinner.py +1 -1
  139. odxtools/singleecujob.py +12 -10
  140. odxtools/snrefcontext.py +29 -0
  141. odxtools/specialdata.py +3 -5
  142. odxtools/specialdatagroup.py +7 -9
  143. odxtools/specialdatagroupcaption.py +3 -6
  144. odxtools/standardlengthtype.py +80 -14
  145. odxtools/state.py +3 -5
  146. odxtools/statechart.py +13 -19
  147. odxtools/statetransition.py +8 -18
  148. odxtools/staticfield.py +107 -0
  149. odxtools/subcomponent.py +288 -0
  150. odxtools/swvariable.py +21 -0
  151. odxtools/table.py +9 -9
  152. odxtools/tablerow.py +30 -15
  153. odxtools/teammember.py +3 -5
  154. odxtools/templates/comparam-spec.odx-c.xml.jinja2 +4 -24
  155. odxtools/templates/comparam-subset.odx-cs.xml.jinja2 +5 -26
  156. odxtools/templates/diag_layer_container.odx-d.xml.jinja2 +15 -31
  157. odxtools/templates/{index.xml.xml.jinja2 → index.xml.jinja2} +1 -1
  158. odxtools/templates/macros/printAudience.xml.jinja2 +1 -1
  159. odxtools/templates/macros/printBaseVariant.xml.jinja2 +53 -0
  160. odxtools/templates/macros/printCompanyData.xml.jinja2 +4 -7
  161. odxtools/templates/macros/printComparam.xml.jinja2 +6 -4
  162. odxtools/templates/macros/printComparamRef.xml.jinja2 +5 -12
  163. odxtools/templates/macros/printCompuMethod.xml.jinja2 +147 -0
  164. odxtools/templates/macros/printDOP.xml.jinja2 +27 -137
  165. odxtools/templates/macros/printDescription.xml.jinja2 +18 -0
  166. odxtools/templates/macros/printDiagComm.xml.jinja2 +1 -1
  167. odxtools/templates/macros/printDiagLayer.xml.jinja2 +222 -0
  168. odxtools/templates/macros/printDiagVariable.xml.jinja2 +66 -0
  169. odxtools/templates/macros/printDynDefinedSpec.xml.jinja2 +48 -0
  170. odxtools/templates/macros/printDynamicEndmarkerField.xml.jinja2 +16 -0
  171. odxtools/templates/macros/printDynamicLengthField.xml.jinja2 +1 -1
  172. odxtools/templates/macros/printEcuSharedData.xml.jinja2 +30 -0
  173. odxtools/templates/macros/printEcuVariant.xml.jinja2 +53 -0
  174. odxtools/templates/macros/printEcuVariantPattern.xml.jinja2 +1 -1
  175. odxtools/templates/macros/printElementId.xml.jinja2 +8 -3
  176. odxtools/templates/macros/printEndOfPdu.xml.jinja2 +1 -1
  177. odxtools/templates/macros/printEnvDataDesc.xml.jinja2 +1 -1
  178. odxtools/templates/macros/printFunctionalClass.xml.jinja2 +1 -1
  179. odxtools/templates/macros/printFunctionalGroup.xml.jinja2 +40 -0
  180. odxtools/templates/macros/printHierarchyElement.xml.jinja2 +24 -0
  181. odxtools/templates/macros/printLibrary.xml.jinja2 +21 -0
  182. odxtools/templates/macros/printMux.xml.jinja2 +5 -3
  183. odxtools/templates/macros/printOdxCategory.xml.jinja2 +28 -0
  184. odxtools/templates/macros/printParam.xml.jinja2 +18 -19
  185. odxtools/templates/macros/printProtStack.xml.jinja2 +1 -1
  186. odxtools/templates/macros/printProtocol.xml.jinja2 +30 -0
  187. odxtools/templates/macros/printRequest.xml.jinja2 +1 -1
  188. odxtools/templates/macros/printResponse.xml.jinja2 +1 -1
  189. odxtools/templates/macros/printService.xml.jinja2 +3 -2
  190. odxtools/templates/macros/printSingleEcuJob.xml.jinja2 +5 -26
  191. odxtools/templates/macros/printSpecialData.xml.jinja2 +1 -1
  192. odxtools/templates/macros/printState.xml.jinja2 +1 -1
  193. odxtools/templates/macros/printStateChart.xml.jinja2 +1 -1
  194. odxtools/templates/macros/printStateTransition.xml.jinja2 +1 -1
  195. odxtools/templates/macros/printStaticField.xml.jinja2 +15 -0
  196. odxtools/templates/macros/printStructure.xml.jinja2 +1 -1
  197. odxtools/templates/macros/printSubComponent.xml.jinja2 +104 -0
  198. odxtools/templates/macros/printTable.xml.jinja2 +4 -5
  199. odxtools/templates/macros/printUnitSpec.xml.jinja2 +3 -5
  200. odxtools/uds.py +2 -10
  201. odxtools/unit.py +4 -8
  202. odxtools/unitgroup.py +3 -5
  203. odxtools/unitspec.py +17 -17
  204. odxtools/utils.py +38 -20
  205. odxtools/variablegroup.py +32 -0
  206. odxtools/version.py +2 -2
  207. odxtools/{write_pdx_file.py → writepdxfile.py} +22 -12
  208. odxtools/xdoc.py +3 -5
  209. {odxtools-6.6.1.dist-info → odxtools-9.3.0.dist-info}/METADATA +44 -33
  210. odxtools-9.3.0.dist-info/RECORD +228 -0
  211. {odxtools-6.6.1.dist-info → odxtools-9.3.0.dist-info}/WHEEL +1 -1
  212. odxtools/createcompanydatas.py +0 -17
  213. odxtools/createsdgs.py +0 -19
  214. odxtools/diaglayertype.py +0 -30
  215. odxtools/load_file.py +0 -13
  216. odxtools/load_odx_d_file.py +0 -6
  217. odxtools/load_pdx_file.py +0 -8
  218. odxtools/templates/macros/printVariant.xml.jinja2 +0 -208
  219. odxtools-6.6.1.dist-info/RECORD +0 -180
  220. {odxtools-6.6.1.dist-info → odxtools-9.3.0.dist-info}/LICENSE +0 -0
  221. {odxtools-6.6.1.dist-info → odxtools-9.3.0.dist-info}/entry_points.txt +0 -0
  222. {odxtools-6.6.1.dist-info → odxtools-9.3.0.dist-info}/top_level.txt +0 -0
@@ -1,203 +1,92 @@
1
1
  # SPDX-License-Identifier: MIT
2
2
  from dataclasses import dataclass
3
- from typing import cast
3
+ from typing import List, cast
4
+ from xml.etree import ElementTree
4
5
 
5
- from ..exceptions import DecodeError, EncodeError, odxassert
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 IntervalType, 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: Limit
64
- internal_upper_limit: 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) -> 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) -> 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_to_limit_to_physical(limit: Limit, is_upper_limit: bool) -> Limit:
90
- """Helper method
91
-
92
- Parameters:
93
-
94
- limit
95
- the internal limit to be converted to a physical limit
96
- is_upper_limit
97
- True iff limit is the internal upper limit
98
- """
99
- odxassert(isinstance(limit.value, (int, float)))
100
- if limit.interval_type == IntervalType.INFINITE:
101
- return limit
102
- elif (limit.interval_type == limit.interval_type.OPEN and
103
- self.internal_type.as_python_type() == int):
104
- closed_limit = limit.value - 1 if is_upper_limit else limit.value + 1
105
- return Limit(
106
- value=self._convert_internal_to_physical(closed_limit),
107
- interval_type=IntervalType.CLOSED,
108
- )
109
- else:
110
- return Limit(
111
- value=self._convert_internal_to_physical(limit.value),
112
- interval_type=limit.interval_type,
113
- )
114
-
115
- if self.factor >= 0:
116
- self._physical_lower_limit = convert_to_limit_to_physical(self.internal_lower_limit,
117
- False)
118
- self._physical_upper_limit = convert_to_limit_to_physical(self.internal_upper_limit,
119
- True)
120
- else:
121
- # If the factor is negative, the lower and upper limit are swapped
122
- self._physical_lower_limit = convert_to_limit_to_physical(self.internal_upper_limit,
123
- True)
124
- self._physical_upper_limit = convert_to_limit_to_physical(self.internal_lower_limit,
125
- False)
126
-
127
- if self.physical_type == DataType.A_UINT32:
128
- # If the data type is unsigned, the physical lower limit should be at least 0.
129
- if (self._physical_lower_limit.interval_type == IntervalType.INFINITE or
130
- cast(float, self._physical_lower_limit.value) < 0):
131
- self._physical_lower_limit = Limit(value=0, interval_type=IntervalType.CLOSED)
132
-
133
- def _convert_internal_to_physical(self, internal_value: AtomicOdxType) -> AtomicOdxType:
134
- if not isinstance(internal_value, (int, float)):
135
- raise DecodeError("The type of internal values of linear compumethods must "
136
- "either int or float")
137
-
138
- if self.denominator is None:
139
- result = self.offset + self.factor * internal_value
140
- else:
141
- result = (self.offset + self.factor * internal_value) / self.denominator
142
-
143
- if self.internal_type == DataType.A_FLOAT64 and self.physical_type in [
144
- DataType.A_INT32,
145
- DataType.A_UINT32,
146
- ]:
147
- result = round(result)
148
- 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)
149
75
 
150
76
  def convert_internal_to_physical(self, internal_value: AtomicOdxType) -> AtomicOdxType:
151
- odxassert(self.is_valid_internal_value(internal_value))
152
- return self._convert_internal_to_physical(internal_value)
77
+ if not self._segment.internal_applies(internal_value):
78
+ odxraise(f"Cannot decode internal value {internal_value!r}", DecodeError)
79
+
80
+ return self._segment.convert_internal_to_physical(internal_value)
153
81
 
154
82
  def convert_physical_to_internal(self, physical_value: AtomicOdxType) -> AtomicOdxType:
155
- if not isinstance(physical_value, (int, float)):
156
- raise EncodeError("The type of physical values of linear compumethods must "
157
- "either int or float")
158
-
159
- odxassert(
160
- self.is_valid_physical_value(physical_value),
161
- f"physical value {physical_value} of type {type(physical_value)} "
162
- f"is not valid. Expected type {self.physical_type} with internal "
163
- f"range {self.internal_lower_limit} to {self.internal_upper_limit}")
164
- if self.denominator is None:
165
- result = (physical_value - self.offset) / self.factor
166
- else:
167
- result = ((physical_value * self.denominator) - self.offset) / self.factor
168
-
169
- if self.physical_type == DataType.A_FLOAT64 and self.internal_type in [
170
- DataType.A_INT32,
171
- DataType.A_UINT32,
172
- ]:
173
- result = round(result)
174
- return self.internal_type.make_from(result)
83
+ if not self._segment.physical_applies(physical_value):
84
+ odxraise(f"Cannot decode physical value {physical_value!r}", EncodeError)
85
+
86
+ return self._segment.convert_physical_to_internal(physical_value)
175
87
 
176
88
  def is_valid_physical_value(self, physical_value: AtomicOdxType) -> bool:
177
- # Do type checks
178
- expected_type = self.physical_type.as_python_type()
179
- if expected_type == float and not isinstance(physical_value, (int, float)):
180
- return False
181
- elif expected_type != float and not isinstance(physical_value, expected_type):
182
- return False
183
-
184
- # Compare to the limits
185
- if not self.physical_lower_limit.complies_to_lower(physical_value):
186
- return False
187
- if not self.physical_upper_limit.complies_to_upper(physical_value):
188
- return False
189
- return True
89
+ return self._segment.physical_applies(physical_value)
190
90
 
191
91
  def is_valid_internal_value(self, internal_value: AtomicOdxType) -> bool:
192
- expected_type = self.internal_type.as_python_type()
193
- if expected_type == float and not isinstance(internal_value, (int, float)):
194
- return False
195
- elif expected_type != float and not isinstance(internal_value, expected_type):
196
- return False
197
-
198
- if not self.internal_lower_limit.complies_to_lower(internal_value):
199
- return False
200
- if not self.internal_upper_limit.complies_to_upper(internal_value):
201
- return False
202
-
203
- return True
92
+ return self._segment.internal_applies(internal_value)
@@ -0,0 +1,190 @@
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 = 0 if len(coeffs.numerators) == 1 else 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
+ x = odxrequire(scale.compu_inverse_value).value
50
+ if not isinstance(x, (int, float)):
51
+ odxraise(f"Non-numeric COMPU-INVERSE-VALUE specified ({x!r})")
52
+ inverse_value = x
53
+
54
+ internal_lower_limit = scale.lower_limit
55
+ internal_upper_limit = scale.upper_limit
56
+
57
+ return LinearSegment(
58
+ offset=offset,
59
+ factor=factor,
60
+ denominator=denominator,
61
+ internal_lower_limit=internal_lower_limit,
62
+ internal_upper_limit=internal_upper_limit,
63
+ inverse_value=inverse_value,
64
+ internal_type=internal_type,
65
+ physical_type=physical_type)
66
+
67
+ @property
68
+ def physical_lower_limit(self) -> Optional[Limit]:
69
+ return self._physical_lower_limit
70
+
71
+ @property
72
+ def physical_upper_limit(self) -> Optional[Limit]:
73
+ return self._physical_upper_limit
74
+
75
+ def __post_init__(self) -> None:
76
+ self.__compute_physical_limits()
77
+
78
+ def convert_internal_to_physical(self, internal_value: AtomicOdxType) -> Union[float, int]:
79
+ if not isinstance(internal_value, (int, float)):
80
+ odxraise(f"Internal values of linear compumethods must "
81
+ f"either be int or float (is: {type(internal_value).__name__})")
82
+
83
+ result = (self.offset + self.factor * internal_value) / self.denominator
84
+
85
+ if self.physical_type in [
86
+ DataType.A_INT32,
87
+ DataType.A_UINT32,
88
+ ]:
89
+ result = round(result)
90
+
91
+ return result
92
+
93
+ def convert_physical_to_internal(self, physical_value: AtomicOdxType) -> Union[float, int]:
94
+ if not isinstance(physical_value, (int, float)):
95
+ odxraise(f"Physical values of linear compumethods must "
96
+ f"either be int or float (is: {type(physical_value).__name__})")
97
+
98
+ if abs(self.factor) < 1e-10:
99
+ # "If factor = 0 then COMPU-INVERSE-VALUE shall be specified.
100
+ return self.inverse_value
101
+
102
+ result = (physical_value * self.denominator - self.offset) / self.factor
103
+
104
+ if self.internal_type in [
105
+ DataType.A_INT32,
106
+ DataType.A_UINT32,
107
+ ]:
108
+ result = round(result)
109
+
110
+ return result
111
+
112
+ def __compute_physical_limits(self) -> None:
113
+ """Computes the physical limits and stores them in the properties
114
+ self._physical_lower_limit and self._physical_upper_limit.
115
+ This method is called by `__post_init__()`.
116
+ """
117
+
118
+ def convert_internal_to_physical_limit(internal_limit: Optional[Limit]) -> Optional[Limit]:
119
+ """Helper method to convert a single internal limit
120
+ """
121
+ if internal_limit is None or internal_limit.value_raw is None:
122
+ return None
123
+
124
+ internal_value = self.internal_type.from_string(internal_limit.value_raw)
125
+ physical_value = self.convert_internal_to_physical(internal_value)
126
+
127
+ result = Limit(
128
+ value_raw=str(physical_value),
129
+ value_type=self.physical_type,
130
+ interval_type=internal_limit.interval_type)
131
+
132
+ return result
133
+
134
+ self._physical_lower_limit = None
135
+ self._physical_upper_limit = None
136
+
137
+ if self.factor >= 0:
138
+ self._physical_lower_limit = convert_internal_to_physical_limit(
139
+ self.internal_lower_limit)
140
+ self._physical_upper_limit = convert_internal_to_physical_limit(
141
+ self.internal_upper_limit)
142
+ else:
143
+ # If the scaling factor is negative, the lower and upper
144
+ # limit are swapped
145
+ self._physical_lower_limit = convert_internal_to_physical_limit(
146
+ self.internal_upper_limit)
147
+ self._physical_upper_limit = convert_internal_to_physical_limit(
148
+ self.internal_lower_limit)
149
+
150
+ def physical_applies(self, physical_value: AtomicOdxType) -> bool:
151
+ """Returns True iff the segment is applicable to a given physical value"""
152
+ # Do type checks
153
+ expected_type = self.physical_type.python_type
154
+ if issubclass(expected_type, float):
155
+ if not isinstance(physical_value, (int, float)):
156
+ return False
157
+ else:
158
+ if not isinstance(physical_value, expected_type):
159
+ return False
160
+
161
+ if self._physical_lower_limit is not None and \
162
+ not self._physical_lower_limit.complies_to_lower(physical_value):
163
+ return False
164
+
165
+ if self._physical_upper_limit is not None and \
166
+ not self._physical_upper_limit.complies_to_upper(physical_value):
167
+ return False
168
+
169
+ return True
170
+
171
+ def internal_applies(self, internal_value: AtomicOdxType) -> bool:
172
+ """Returns True iff the segment is applicable to a given internal value"""
173
+ # Do type checks
174
+ expected_type = self.internal_type.python_type
175
+ if issubclass(expected_type, float):
176
+ if not isinstance(internal_value, (int, float)):
177
+ return False
178
+ else:
179
+ if not isinstance(internal_value, expected_type):
180
+ return False
181
+
182
+ if self.internal_lower_limit is not None and \
183
+ not self.internal_lower_limit.complies_to_lower(internal_value):
184
+ return False
185
+
186
+ if self.internal_upper_limit is not None and \
187
+ not self.internal_upper_limit.complies_to_upper(internal_value):
188
+ return False
189
+
190
+ return True
@@ -0,0 +1,106 @@
1
+ # SPDX-License-Identifier: MIT
2
+ from dataclasses import dataclass
3
+ from typing import List, Optional, cast
4
+ from xml.etree import ElementTree
5
+
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 .ratfuncsegment import RatFuncSegment
12
+
13
+
14
+ @dataclass
15
+ class RatFuncCompuMethod(CompuMethod):
16
+ """A compu method using a rational function
17
+
18
+ i.e. internal values are converted to physical ones using the
19
+ function `f(x) = (a0 + a1*x + a2*x^2 ...)/(b0 + b0*x^2 ...)` where `f(x)`
20
+ is the physical value and `x` is the internal value. In contrast
21
+ to `ScaleRatFuncCompuMethod`, this compu method only exhibits a
22
+ single segment)
23
+
24
+ For details, refer to ASAM specification MCD-2 D (ODX), section 7.3.6.6.5.
25
+ """
26
+
27
+ @property
28
+ def int_to_phys_segment(self) -> RatFuncSegment:
29
+ return self._int_to_phys_segment
30
+
31
+ @property
32
+ def phys_to_int_segment(self) -> Optional[RatFuncSegment]:
33
+ return self._phys_to_int_segment
34
+
35
+ @staticmethod
36
+ def compu_method_from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment], *,
37
+ internal_type: DataType,
38
+ physical_type: DataType) -> "RatFuncCompuMethod":
39
+ cm = CompuMethod.compu_method_from_et(
40
+ et_element, doc_frags, internal_type=internal_type, physical_type=physical_type)
41
+ kwargs = dataclass_fields_asdict(cm)
42
+
43
+ return RatFuncCompuMethod(**kwargs)
44
+
45
+ def __post_init__(self) -> None:
46
+ odxassert(self.category == CompuCategory.RAT_FUNC,
47
+ "RatFuncCompuMethod must exhibit RAT-FUNC category")
48
+
49
+ odxassert(self.physical_type in [
50
+ DataType.A_FLOAT32,
51
+ DataType.A_FLOAT64,
52
+ DataType.A_INT32,
53
+ DataType.A_UINT32,
54
+ ])
55
+ odxassert(self.internal_type in [
56
+ DataType.A_FLOAT32,
57
+ DataType.A_FLOAT64,
58
+ DataType.A_INT32,
59
+ DataType.A_UINT32,
60
+ ])
61
+
62
+ if self.compu_internal_to_phys is None:
63
+ odxraise("RAT-FUNC compu methods require COMPU-INTERNAL-TO-PHYS")
64
+ return
65
+
66
+ int_to_phys_scales = self.compu_internal_to_phys.compu_scales
67
+ if len(int_to_phys_scales) != 1:
68
+ odxraise("RAT-FUNC compu methods expect exactly one compu scale within "
69
+ "COMPU-INTERNAL-TO-PHYS")
70
+ return cast(None, RatFuncCompuMethod)
71
+
72
+ self._int_to_phys_segment = RatFuncSegment.from_compu_scale(
73
+ int_to_phys_scales[0], value_type=self.physical_type)
74
+
75
+ self._phys_to_int_segment = None
76
+ if self.compu_phys_to_internal is not None:
77
+ phys_to_int_scales = self.compu_phys_to_internal.compu_scales
78
+ if len(phys_to_int_scales) != 1:
79
+ odxraise("RAT-FUNC compu methods expect exactly one compu scale within "
80
+ "COMPU-PHYS-TO-INTERNAL")
81
+ return cast(None, RatFuncCompuMethod)
82
+
83
+ self._phys_to_int_segment = RatFuncSegment.from_compu_scale(
84
+ phys_to_int_scales[0], value_type=self.internal_type)
85
+
86
+ def convert_internal_to_physical(self, internal_value: AtomicOdxType) -> AtomicOdxType:
87
+ if not self._int_to_phys_segment.applies(internal_value):
88
+ odxraise(f"Cannot decode internal value {internal_value!r}", DecodeError)
89
+ return cast(AtomicOdxType, None)
90
+
91
+ return self._int_to_phys_segment.convert(internal_value)
92
+
93
+ def convert_physical_to_internal(self, physical_value: AtomicOdxType) -> AtomicOdxType:
94
+ if self._phys_to_int_segment is None or not self._phys_to_int_segment.applies(
95
+ physical_value):
96
+ odxraise(f"Cannot encode physical value {physical_value!r}", EncodeError)
97
+ return cast(AtomicOdxType, None)
98
+
99
+ return self._phys_to_int_segment.convert(physical_value)
100
+
101
+ def is_valid_physical_value(self, physical_value: AtomicOdxType) -> bool:
102
+ return self._phys_to_int_segment is not None and self._phys_to_int_segment.applies(
103
+ physical_value)
104
+
105
+ def is_valid_internal_value(self, internal_value: AtomicOdxType) -> bool:
106
+ return self._int_to_phys_segment.applies(internal_value)
@@ -0,0 +1,87 @@
1
+ # SPDX-License-Identifier: MIT
2
+ from dataclasses import dataclass
3
+ from typing import List, 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 RatFuncSegment:
13
+ """Helper class to represent a segment of a piecewise rational function.
14
+ """
15
+ value_type: DataType
16
+
17
+ numerator_coeffs: List[Union[int, float]]
18
+ denominator_coeffs: List[Union[int, float]]
19
+
20
+ lower_limit: Optional[Limit]
21
+ upper_limit: Optional[Limit]
22
+
23
+ @staticmethod
24
+ def from_compu_scale(scale: CompuScale, value_type: DataType) -> "RatFuncSegment":
25
+ coeffs = odxrequire(scale.compu_rational_coeffs,
26
+ "Scales for rational functions must exhibit "
27
+ "COMPU-RATIONAL-COEFFS")
28
+
29
+ numerator_coeffs = coeffs.numerators
30
+ denominator_coeffs = coeffs.denominators
31
+
32
+ lower_limit = scale.lower_limit
33
+ upper_limit = scale.upper_limit
34
+
35
+ return RatFuncSegment(
36
+ numerator_coeffs=numerator_coeffs,
37
+ denominator_coeffs=denominator_coeffs,
38
+ lower_limit=lower_limit,
39
+ upper_limit=upper_limit,
40
+ value_type=scale.range_type)
41
+
42
+ def convert(self, value: AtomicOdxType) -> Union[float, int]:
43
+ if not isinstance(value, (int, float)):
44
+ odxraise(f"Internal values of linear compumethods must "
45
+ f"either be int or float (is: {type(value).__name__})")
46
+
47
+ numerator = 0.0
48
+ x = float(value)
49
+ for numerator_coeff in reversed(self.numerator_coeffs):
50
+ numerator *= x
51
+ numerator += float(numerator_coeff)
52
+
53
+ denominator = 0.0
54
+ for denominator_coeff in reversed(self.denominator_coeffs):
55
+ denominator *= x
56
+ denominator += float(denominator_coeff)
57
+
58
+ result = numerator / denominator
59
+
60
+ if self.value_type in [
61
+ DataType.A_INT32,
62
+ DataType.A_UINT32,
63
+ ]:
64
+ result = round(result)
65
+
66
+ return result
67
+
68
+ def applies(self, value: AtomicOdxType) -> bool:
69
+ """Returns True iff the segment is applicable to a given internal value"""
70
+ # Do type checks
71
+ expected_type = self.value_type.python_type
72
+ if issubclass(expected_type, float):
73
+ if not isinstance(value, (int, float)):
74
+ return False
75
+ else:
76
+ if not isinstance(value, expected_type):
77
+ return False
78
+
79
+ if self.lower_limit is not None and \
80
+ not self.lower_limit.complies_to_lower(value):
81
+ return False
82
+
83
+ if self.upper_limit is not None and \
84
+ not self.upper_limit.complies_to_upper(value):
85
+ return False
86
+
87
+ return True