yostlabs 2025.1.16__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.
- yostlabs/__init__.py +0 -0
- yostlabs/communication/__init__.py +0 -0
- yostlabs/communication/base.py +90 -0
- yostlabs/communication/serial.py +141 -0
- yostlabs/math/__init__.py +1 -0
- yostlabs/math/quaternion.py +149 -0
- yostlabs/math/vector.py +31 -0
- yostlabs/tss3/__init__.py +0 -0
- yostlabs/tss3/api.py +1712 -0
- yostlabs/tss3/consts.py +24 -0
- yostlabs/tss3/eepts.py +228 -0
- yostlabs/tss3/utils/__init__.py +0 -0
- yostlabs/tss3/utils/calibration.py +153 -0
- yostlabs/tss3/utils/parser.py +256 -0
- yostlabs/tss3/utils/streaming.py +435 -0
- yostlabs/tss3/utils/version.py +76 -0
- yostlabs-2025.1.16.dist-info/METADATA +50 -0
- yostlabs-2025.1.16.dist-info/RECORD +20 -0
- yostlabs-2025.1.16.dist-info/WHEEL +4 -0
- yostlabs-2025.1.16.dist-info/licenses/LICENSE +21 -0
yostlabs/tss3/consts.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
THREESPACE_HEADER_STATUS_BIT_POS = 0
|
|
2
|
+
THREESPACE_HEADER_TIMESTAMP_BIT_POS = 1
|
|
3
|
+
THREESPACE_HEADER_ECHO_BIT_POS = 2
|
|
4
|
+
THREESPACE_HEADER_CHECKSUM_BIT_POS = 3
|
|
5
|
+
THREESPACE_HEADER_SERIAL_BIT_POS = 4
|
|
6
|
+
THREESPACE_HEADER_LENGTH_BIT_POS = 5
|
|
7
|
+
THREESPACE_HEADER_NUM_BITS = 6
|
|
8
|
+
|
|
9
|
+
THREESPACE_HEADER_STATUS_BIT = (1 << THREESPACE_HEADER_STATUS_BIT_POS)
|
|
10
|
+
THREESPACE_HEADER_TIMESTAMP_BIT = (1 << THREESPACE_HEADER_TIMESTAMP_BIT_POS)
|
|
11
|
+
THREESPACE_HEADER_ECHO_BIT = (1 << THREESPACE_HEADER_ECHO_BIT_POS)
|
|
12
|
+
THREESPACE_HEADER_CHECKSUM_BIT = (1 << THREESPACE_HEADER_CHECKSUM_BIT_POS)
|
|
13
|
+
THREESPACE_HEADER_SERIAL_BIT = (1 << THREESPACE_HEADER_SERIAL_BIT_POS)
|
|
14
|
+
THREESPACE_HEADER_LENGTH_BIT = (1 << THREESPACE_HEADER_LENGTH_BIT_POS)
|
|
15
|
+
|
|
16
|
+
FIRMWARE_VALID_BIT = 0x01
|
|
17
|
+
|
|
18
|
+
PASSIVE_CALIBRATE_GYRO = (1 << 0)
|
|
19
|
+
PASSIVE_CALIBRATE_MAG_REF = (1 << 1)
|
|
20
|
+
|
|
21
|
+
STREAMING_MAX_HZ = 2000
|
|
22
|
+
|
|
23
|
+
THREESPACE_OUTPUT_MODE_ASCII = 1
|
|
24
|
+
THREESPACE_OUTPUT_MODE_BINARY = 2
|
yostlabs/tss3/eepts.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
from dataclasses import dataclass, field, fields
|
|
2
|
+
from typing import ClassVar
|
|
3
|
+
|
|
4
|
+
YL_SENSOR_BIT_GYRO: int = 1
|
|
5
|
+
YL_SENSOR_BIT_ACCEL: int = 2
|
|
6
|
+
YL_SENSOR_BIT_MAG: int = 4
|
|
7
|
+
YL_SENSOR_BIT_BARO: int = 8
|
|
8
|
+
YL_SENSOR_BIT_TEMP: int = 16
|
|
9
|
+
YL_SENSOR_BIT_GPS: int = 32
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class YL_EEPTS_INPUT_DATA:
|
|
13
|
+
gyro_data: list[float] = None # XYZ in radians/sec. only needs to be float32
|
|
14
|
+
accel_data: list[float] = None # XYZ in G force units(9.8 m/s^2). only needs to be float32
|
|
15
|
+
mag_data: list[float] = None # XYZ in Gauss. only needs to be float32
|
|
16
|
+
orient_data: list[float] = None # XYZW Optional, may not be here if YL_BUILTIN_ORIENT is false
|
|
17
|
+
barometer_data: float = 0 # in hPa. only needs to be float32
|
|
18
|
+
temperature: float = 0 # in degrees C. only needs to be float32
|
|
19
|
+
gps_longitude: float = 0 # in decimal degrees (positive east, negative west). needs to be float64
|
|
20
|
+
gps_latitude: float = 0 # in decimal degrees (positive north, negative south). needs to be float64
|
|
21
|
+
gps_altitude: float = 0 # in meters. only needs to be float32
|
|
22
|
+
timestamp: int = 0 # in microseconds. uint32_t
|
|
23
|
+
|
|
24
|
+
#Locomotion Modes
|
|
25
|
+
YL_LOCOMOTION_IDLE: int = 0
|
|
26
|
+
YL_LOCOMOTION_WALKING: int = 1
|
|
27
|
+
YL_LOCOMOTION_JOGGING: int = 2
|
|
28
|
+
YL_LOCOMOTION_RUNNING: int = 3
|
|
29
|
+
YL_LOCOMOTION_CRAWLING: int = 4
|
|
30
|
+
YL_LOCOMOTION_OTHER: int = 6
|
|
31
|
+
YL_LOCOMOTION_UNKNOWN: int = 5
|
|
32
|
+
|
|
33
|
+
#Sensor Locations
|
|
34
|
+
YL_SENSOR_RSHOULDER: int = 5
|
|
35
|
+
YL_SENSOR_BACK: int = 3
|
|
36
|
+
YL_SENSOR_RHAND: int = 4
|
|
37
|
+
YL_SENSOR_WAIST: int = 2
|
|
38
|
+
YL_SENSOR_CHEST: int = 1
|
|
39
|
+
YL_SENSOR_UNKNOWN: int = 0
|
|
40
|
+
|
|
41
|
+
LocomotionModes = {
|
|
42
|
+
YL_LOCOMOTION_IDLE: "Idle",
|
|
43
|
+
YL_LOCOMOTION_WALKING: "Walking",
|
|
44
|
+
YL_LOCOMOTION_JOGGING: "Jogging",
|
|
45
|
+
YL_LOCOMOTION_RUNNING: "Running",
|
|
46
|
+
YL_LOCOMOTION_CRAWLING: "Crawling",
|
|
47
|
+
YL_LOCOMOTION_OTHER: "Other",
|
|
48
|
+
YL_LOCOMOTION_UNKNOWN: "Unknown"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
SensorLocations = {
|
|
52
|
+
YL_SENSOR_RSHOULDER: "RShoulder",
|
|
53
|
+
YL_SENSOR_BACK: "Back",
|
|
54
|
+
YL_SENSOR_RHAND: "RHand",
|
|
55
|
+
YL_SENSOR_WAIST: "Waist",
|
|
56
|
+
YL_SENSOR_CHEST: "Chest",
|
|
57
|
+
YL_SENSOR_UNKNOWN: "Unknown"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
PresetMotionWR = 0
|
|
61
|
+
PresetMotionWRC = 1
|
|
62
|
+
|
|
63
|
+
PresetHandDisabled = 0
|
|
64
|
+
PresetHandEnabled = 1
|
|
65
|
+
PresetHandOnly = 2
|
|
66
|
+
|
|
67
|
+
PresetHeadingDynamic = 0
|
|
68
|
+
PresetHeadingStatic = 1
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class YL_EEPTS_OUTPUT_DATA:
|
|
72
|
+
|
|
73
|
+
segment_count: int = 0 #number of segments (a segment refers to a single step action)
|
|
74
|
+
timestamp: int = 0 #in microseconds, set to end of last estimated segment
|
|
75
|
+
estimated_gps_longitude: float = 0 #DDMM.MMMM (positive east, negative west)
|
|
76
|
+
estimated_gps_latitude: float = 0 #DDMM.MMMM (positive north, negative south)
|
|
77
|
+
estimated_gps_altitude: float = 0 #in meters
|
|
78
|
+
estimated_heading_angle: float = 0 #in degrees (0 north, 90 east, 180 south, 270 west)
|
|
79
|
+
estimated_distance_travelled: float = 0 #in meters, since last reset (A reset is a start command)
|
|
80
|
+
estimated_distance_x: float = 0 #in meters since last update, along positive east/negative west (An update is a read)
|
|
81
|
+
estimated_distance_y: float = 0 #in meters, since last update, along positive north/negative south
|
|
82
|
+
estimated_distance_z: float = 0 #in meters, since last update, along positive up/negative down
|
|
83
|
+
estimated_locomotion_mode: int = 0 #0=idle, 1=walking, etc...
|
|
84
|
+
estimated_receiver_location: int = 0 #0=unknown, 1=chest, etc...
|
|
85
|
+
last_event_confidence: float = 1
|
|
86
|
+
overall_confidence: float = 1
|
|
87
|
+
|
|
88
|
+
LOCMOTION_DICT: ClassVar[dict[int, str]] = { YL_LOCOMOTION_IDLE : "Idle", YL_LOCOMOTION_WALKING : "Walking", YL_LOCOMOTION_JOGGING : "Jogging", YL_LOCOMOTION_RUNNING : "Running", YL_LOCOMOTION_CRAWLING: "Crawling", YL_LOCOMOTION_OTHER: "Other", YL_LOCOMOTION_UNKNOWN: "Unknown" }
|
|
89
|
+
LOCATION_DICT: ClassVar[dict[int, str]] = { YL_SENSOR_UNKNOWN : "Unknown", YL_SENSOR_CHEST : "Chest", YL_SENSOR_WAIST : "Waist", YL_SENSOR_BACK : "Back", YL_SENSOR_RHAND : "RHand", YL_SENSOR_RSHOULDER : "RShoulder" }
|
|
90
|
+
|
|
91
|
+
def clone(self, other: "YL_EEPTS_OUTPUT_DATA"):
|
|
92
|
+
self.segment_count = other.segment_count
|
|
93
|
+
self.timestamp = other.timestamp
|
|
94
|
+
self.estimated_gps_longitude = other.estimated_gps_longitude
|
|
95
|
+
self.estimated_gps_latitude = other.estimated_gps_latitude
|
|
96
|
+
self.estimated_gps_altitude = other.estimated_gps_altitude
|
|
97
|
+
self.estimated_heading_angle = other.estimated_heading_angle
|
|
98
|
+
self.estimated_distance_travelled = other.estimated_distance_travelled
|
|
99
|
+
self.estimated_distance_x = other.estimated_distance_x
|
|
100
|
+
self.estimated_distance_y = other.estimated_distance_y
|
|
101
|
+
self.estimated_distance_z = other.estimated_distance_z
|
|
102
|
+
self.estimated_locomotion_mode = other.estimated_locomotion_mode
|
|
103
|
+
self.estimated_receiver_location = other.estimated_receiver_location
|
|
104
|
+
self.last_event_confidence = other.last_event_confidence
|
|
105
|
+
self.overall_confidence = other.overall_confidence
|
|
106
|
+
|
|
107
|
+
def get_locomotion_string(self):
|
|
108
|
+
return YL_EEPTS_OUTPUT_DATA.LOCMOTION_DICT[self.estimated_locomotion_mode]
|
|
109
|
+
|
|
110
|
+
def get_location_string(self):
|
|
111
|
+
return YL_EEPTS_OUTPUT_DATA.LOCATION_DICT[self.estimated_receiver_location]
|
|
112
|
+
|
|
113
|
+
def __str__(self):
|
|
114
|
+
return f"{self.segment_count},{self.timestamp}," \
|
|
115
|
+
f"{self.estimated_gps_longitude},{self.estimated_gps_latitude}," \
|
|
116
|
+
f"{self.estimated_gps_altitude},{self.estimated_heading_angle}," \
|
|
117
|
+
f"{self.estimated_distance_travelled},{self.estimated_distance_x}," \
|
|
118
|
+
f"{self.estimated_distance_y},{self.estimated_distance_z}," \
|
|
119
|
+
f"{self.estimated_locomotion_mode},{self.estimated_receiver_location}"
|
|
120
|
+
|
|
121
|
+
def print_fancy(self):
|
|
122
|
+
print("Segment Count:", self.segment_count)
|
|
123
|
+
print("Timestamp:", self.timestamp)
|
|
124
|
+
print("Longitude:", self.estimated_gps_longitude)
|
|
125
|
+
print("Latitude:", self.estimated_gps_latitude)
|
|
126
|
+
print("Altitude:", self.estimated_gps_altitude)
|
|
127
|
+
print("Heading:", self.estimated_heading_angle)
|
|
128
|
+
print("Distance Travelled:", self.estimated_distance_travelled)
|
|
129
|
+
print("Delta_X:", self.estimated_distance_x)
|
|
130
|
+
print("Delta_Y:", self.estimated_distance_y)
|
|
131
|
+
print("Delta_Z:", self.estimated_distance_z)
|
|
132
|
+
print("Locomotion:", self.get_locomotion_string())
|
|
133
|
+
print("Sensor Location:", self.get_location_string())
|
|
134
|
+
print("Confidence:", self.last_event_confidence)
|
|
135
|
+
print("Overall Confidence:", self.overall_confidence)
|
|
136
|
+
|
|
137
|
+
@dataclass
|
|
138
|
+
class SensorData:
|
|
139
|
+
accel_x : float
|
|
140
|
+
accel_y : float
|
|
141
|
+
accel_z : float
|
|
142
|
+
|
|
143
|
+
gyro_x : float
|
|
144
|
+
gyro_y : float
|
|
145
|
+
gyro_z : float
|
|
146
|
+
|
|
147
|
+
mag_x : float
|
|
148
|
+
mag_y : float
|
|
149
|
+
mag_z : float
|
|
150
|
+
|
|
151
|
+
accel : list[float] = field(init=False, default_factory=list)
|
|
152
|
+
gyro : list[float] = field(init=False, default_factory=list)
|
|
153
|
+
mag : list[float] = field(init=False, default_factory=list)
|
|
154
|
+
|
|
155
|
+
def __post_init__(self):
|
|
156
|
+
self.accel = [self.accel_x, self.accel_y, self.accel_z]
|
|
157
|
+
self.gyro = [self.gyro_x, self.gyro_y, self.gyro_z]
|
|
158
|
+
self.mag = [self.mag_x, self.mag_y, self.mag_z]
|
|
159
|
+
|
|
160
|
+
def __str__(self):
|
|
161
|
+
return f"{self.accel_x},{self.accel_y},{self.accel_z}," \
|
|
162
|
+
f"{self.gyro_x},{self.gyro_y}, {self.gyro_z}," \
|
|
163
|
+
f"{self.mag_x},{self.mag_y},{self.mag_z}"
|
|
164
|
+
|
|
165
|
+
def print_fancy(self):
|
|
166
|
+
print("Accel:", self.accel)
|
|
167
|
+
print("Gyro:", self.gyro)
|
|
168
|
+
print("Mag:", self.mag)
|
|
169
|
+
|
|
170
|
+
@dataclass
|
|
171
|
+
class DebugMessage:
|
|
172
|
+
level: int = 0
|
|
173
|
+
module: int = 0
|
|
174
|
+
msg: str = ""
|
|
175
|
+
|
|
176
|
+
def get_display_str(self):
|
|
177
|
+
return f"Level: {self.level} Module: {self.module} {self.msg}"
|
|
178
|
+
|
|
179
|
+
@dataclass
|
|
180
|
+
class Segment:
|
|
181
|
+
#Output Structure
|
|
182
|
+
segment_count: int = 0
|
|
183
|
+
timestamp: int = 0
|
|
184
|
+
estimated_gps_longitude: float = 0
|
|
185
|
+
estimated_gps_latitude: float = 0
|
|
186
|
+
estimated_gps_altitude: float = 0
|
|
187
|
+
estimated_heading_angle: float = 0
|
|
188
|
+
estimated_distance_travelled: float = 0
|
|
189
|
+
estimated_distance_x: float = 0
|
|
190
|
+
estimated_distance_y: float = 0
|
|
191
|
+
estimated_distance_z: float = 0
|
|
192
|
+
estimated_locomotion_mode: int = 0
|
|
193
|
+
estimated_receiver_location: int = 0
|
|
194
|
+
last_event_confidence: float = 0
|
|
195
|
+
overall_confidence: float = 0
|
|
196
|
+
|
|
197
|
+
#Segment Info Structure
|
|
198
|
+
start_global_index: int = 0
|
|
199
|
+
end_global_index: int = 0
|
|
200
|
+
f_start_index_offset: float = 0
|
|
201
|
+
f_end_index_offset: float = 0
|
|
202
|
+
len: int = 0
|
|
203
|
+
f_len: float = 0
|
|
204
|
+
dir: int = 0
|
|
205
|
+
|
|
206
|
+
#Debug Messages:
|
|
207
|
+
debug_msgs: list[DebugMessage] = field(default_factory=lambda: [])
|
|
208
|
+
|
|
209
|
+
@classmethod
|
|
210
|
+
def from_only_output_obj(cls, output: YL_EEPTS_OUTPUT_DATA):
|
|
211
|
+
new_obj = cls()
|
|
212
|
+
output_fields = [f for f in dir(output) if not f.startswith('__') and not callable(getattr(output, f))]
|
|
213
|
+
for field in output_fields: #Can NOT just assign. That will cause unintentional problems if ever saving segment info
|
|
214
|
+
setattr(new_obj, field, getattr(output, field))
|
|
215
|
+
|
|
216
|
+
return new_obj
|
|
217
|
+
|
|
218
|
+
def __str__(self):
|
|
219
|
+
s = ""
|
|
220
|
+
for field in fields(self):
|
|
221
|
+
attr = getattr(self, field.name)
|
|
222
|
+
if field.name == "estimated_locomotion_mode":
|
|
223
|
+
s += f"{field.name}: {LocomotionModes[attr]}\n"
|
|
224
|
+
elif field.name == "estimated_receiver_location":
|
|
225
|
+
s += f"{field.name}: {SensorLocations[attr]}\n"
|
|
226
|
+
else:
|
|
227
|
+
s += f"{field.name}: {attr}\n"
|
|
228
|
+
return s
|
|
File without changes
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import yostlabs.math.quaternion as quat
|
|
2
|
+
import yostlabs.math.vector as vec
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import copy
|
|
7
|
+
|
|
8
|
+
class ThreespaceGradientDescentCalibration:
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class StageInfo:
|
|
12
|
+
start_vector: int
|
|
13
|
+
end_vector: int
|
|
14
|
+
stage: int
|
|
15
|
+
scale: float
|
|
16
|
+
|
|
17
|
+
count: int = 0
|
|
18
|
+
|
|
19
|
+
MAX_SCALE = 1000000000
|
|
20
|
+
MIN_SCALE = 1
|
|
21
|
+
STAGES = [
|
|
22
|
+
StageInfo(0, 6, 0, MAX_SCALE),
|
|
23
|
+
StageInfo(0, 12, 1, MAX_SCALE),
|
|
24
|
+
StageInfo(0, 24, 2, MAX_SCALE)
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
#Note that each entry has a positive and negative vector included in this list
|
|
28
|
+
CHANGE_VECTORS = [
|
|
29
|
+
np.array([0,0,0,0,0,0,0,0,0,.0001,0,0], dtype=np.float64),
|
|
30
|
+
np.array([0,0,0,0,0,0,0,0,0,-.0001,0,0], dtype=np.float64),
|
|
31
|
+
np.array([0,0,0,0,0,0,0,0,0,0,.0001,0], dtype=np.float64),
|
|
32
|
+
np.array([0,0,0,0,0,0,0,0,0,0,-.0001,0], dtype=np.float64),
|
|
33
|
+
np.array([0,0,0,0,0,0,0,0,0,0,0,.0001], dtype=np.float64),
|
|
34
|
+
np.array([0,0,0,0,0,0,0,0,0,0,0,-.0001], dtype=np.float64), #First 6 only try to change the bias
|
|
35
|
+
np.array([.001,0,0,0,0,0,0,0,0,0,0,0], dtype=np.float64),
|
|
36
|
+
np.array([-.001,0,0,0,0,0,0,0,0,0,0,0], dtype=np.float64),
|
|
37
|
+
np.array([0,0,0,0,.001,0,0,0,0,0,0,0], dtype=np.float64),
|
|
38
|
+
np.array([0,0,0,0,-.001,0,0,0,0,0,0,0], dtype=np.float64),
|
|
39
|
+
np.array([0,0,0,0,0,0,0,0,.001,0,0,0], dtype=np.float64),
|
|
40
|
+
np.array([0,0,0,0,0,0,0,0,-.001,0,0,0], dtype=np.float64), #Next 6 only try to change the scale
|
|
41
|
+
np.array([0,.0001,0,0,0,0,0,0,0,0,0,0], dtype=np.float64),
|
|
42
|
+
np.array([0,-.0001,0,0,0,0,0,0,0,0,0,0], dtype=np.float64),
|
|
43
|
+
np.array([0,0,.0001,0,0,0,0,0,0,0,0,0], dtype=np.float64),
|
|
44
|
+
np.array([0,0,-.0001,0,0,0,0,0,0,0,0,0], dtype=np.float64),
|
|
45
|
+
np.array([0,0,0,.0001,0,0,0,0,0,0,0,0], dtype=np.float64),
|
|
46
|
+
np.array([0,0,0,-.0001,0,0,0,0,0,0,0,0], dtype=np.float64),
|
|
47
|
+
np.array([0,0,0,0,0,.0001,0,0,0,0,0,0], dtype=np.float64),
|
|
48
|
+
np.array([0,0,0,0,0,-.0001,0,0,0,0,0,0], dtype=np.float64),
|
|
49
|
+
np.array([0,0,0,0,0,0,.0001,0,0,0,0,0], dtype=np.float64),
|
|
50
|
+
np.array([0,0,0,0,0,0,-.0001,0,0,0,0,0], dtype=np.float64),
|
|
51
|
+
np.array([0,0,0,0,0,0,0,.0001,0,0,0,0], dtype=np.float64),
|
|
52
|
+
np.array([0,0,0,0,0,0,0,-.0001,0,0,0,0], dtype=np.float64), #Next 12 only try to change the shear
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
def __init__(self, relative_sensor_orients: list[np.ndarray[float]], no_inverse=False):
|
|
56
|
+
"""
|
|
57
|
+
Params
|
|
58
|
+
------
|
|
59
|
+
relative_sensor_orients : The orientation of the sensor during which each sample is taken if it was tared as if pointing into the screen.
|
|
60
|
+
The inverse of these will be used to calculate where the axes should be located relative to the sensor
|
|
61
|
+
no_inverse : The relative_sensor_orients will be treated as the sample_rotations
|
|
62
|
+
"""
|
|
63
|
+
if no_inverse:
|
|
64
|
+
self.rotation_quats = relative_sensor_orients
|
|
65
|
+
else:
|
|
66
|
+
self.rotation_quats = [np.array(quat.quat_inverse(orient)) for orient in relative_sensor_orients]
|
|
67
|
+
|
|
68
|
+
def apply_parameters(self, sample: np.ndarray[float], params: np.ndarray[float]):
|
|
69
|
+
bias = params[9:]
|
|
70
|
+
scale = params[:9]
|
|
71
|
+
scale = scale.reshape((3, 3))
|
|
72
|
+
return scale @ (sample + bias)
|
|
73
|
+
|
|
74
|
+
def rate_parameters(self, params: np.ndarray[float], samples: list[np.ndarray[float]], targets: list[np.ndarray[float]]):
|
|
75
|
+
total_error = 0
|
|
76
|
+
for i in range(len(samples)):
|
|
77
|
+
sample = samples[i]
|
|
78
|
+
target = targets[i]
|
|
79
|
+
|
|
80
|
+
sample = self.apply_parameters(sample, params)
|
|
81
|
+
|
|
82
|
+
error = target - sample
|
|
83
|
+
total_error += vec.vec_len(error)
|
|
84
|
+
return total_error
|
|
85
|
+
|
|
86
|
+
def generate_target_list(self, origin: np.ndarray):
|
|
87
|
+
targets = []
|
|
88
|
+
for orient in self.rotation_quats:
|
|
89
|
+
new_vec = np.array(quat.quat_rotate_vec(orient, origin), dtype=np.float64)
|
|
90
|
+
targets.append(new_vec)
|
|
91
|
+
return targets
|
|
92
|
+
|
|
93
|
+
def __get_stage(self, stage_number: int):
|
|
94
|
+
if stage_number >= len(self.STAGES):
|
|
95
|
+
return None
|
|
96
|
+
#Always get a shallow copy of the stage so can modify without removing the initial values
|
|
97
|
+
return copy.copy(self.STAGES[stage_number])
|
|
98
|
+
|
|
99
|
+
def calculate(self, samples: list[np.ndarray[float]], origin: np.ndarray[float], verbose=False, max_cycles_per_stage=1000):
|
|
100
|
+
targets = self.generate_target_list(origin)
|
|
101
|
+
initial_params = np.array([1,0,0,0,1,0,0,0,1,0,0,0], dtype=np.float64)
|
|
102
|
+
stage = self.__get_stage(0)
|
|
103
|
+
|
|
104
|
+
best_params = initial_params
|
|
105
|
+
best_rating = self.rate_parameters(best_params, samples, targets)
|
|
106
|
+
count = 0
|
|
107
|
+
while True:
|
|
108
|
+
last_best_rating = best_rating
|
|
109
|
+
params = best_params
|
|
110
|
+
|
|
111
|
+
#Apply all the changes to see if any improve the result
|
|
112
|
+
for change_index in range(stage.start_vector, stage.end_vector):
|
|
113
|
+
change_vector = self.CHANGE_VECTORS[change_index]
|
|
114
|
+
new_params = params + (change_vector * stage.scale)
|
|
115
|
+
rating = self.rate_parameters(new_params, samples, targets)
|
|
116
|
+
|
|
117
|
+
#A better rating, store it
|
|
118
|
+
if rating < best_rating:
|
|
119
|
+
best_params = new_params
|
|
120
|
+
best_rating = rating
|
|
121
|
+
|
|
122
|
+
if verbose and count % 100 == 0:
|
|
123
|
+
print(f"Round {count}: {best_rating=} {stage=}")
|
|
124
|
+
|
|
125
|
+
#Decide if need to go to the next stage or not
|
|
126
|
+
count += 1
|
|
127
|
+
stage.count += 1
|
|
128
|
+
if stage.count >= max_cycles_per_stage:
|
|
129
|
+
stage = self.__get_stage(stage.stage + 1)
|
|
130
|
+
if stage is None:
|
|
131
|
+
if verbose: print("Done from reaching count limit")
|
|
132
|
+
break
|
|
133
|
+
if verbose: print("Going to next stage from count limit")
|
|
134
|
+
|
|
135
|
+
if best_rating == last_best_rating: #The rating did not improve
|
|
136
|
+
if stage.scale == self.MIN_SCALE: #Go to the next stage since can't get any better in this stage!
|
|
137
|
+
stage = self.__get_stage(stage.stage + 1)
|
|
138
|
+
if stage is None:
|
|
139
|
+
if verbose: print("Done from exhaustion")
|
|
140
|
+
break
|
|
141
|
+
if verbose: print("Going to next stage from exhaustion")
|
|
142
|
+
else: #Reduce the size of the changes to hopefully get more accurate tuning
|
|
143
|
+
stage.scale *= 0.1
|
|
144
|
+
if stage.scale < self.MIN_SCALE:
|
|
145
|
+
stage.scale = self.MIN_SCALE
|
|
146
|
+
else: #Rating got better! To help avoid falling in a local minimum, increase the size of the change to see if that could make it better
|
|
147
|
+
stage.scale *= 1.1
|
|
148
|
+
|
|
149
|
+
if verbose:
|
|
150
|
+
print(f"Final Rating: {best_rating}")
|
|
151
|
+
print(f"Final Params: {best_params}")
|
|
152
|
+
|
|
153
|
+
return best_params
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
from yostlabs.tss3.api import *
|
|
2
|
+
import math
|
|
3
|
+
|
|
4
|
+
class ThreespaceBufferInputStream(ThreespaceInputStream):
|
|
5
|
+
"""
|
|
6
|
+
Default Input Stream for the binary parser.
|
|
7
|
+
Ignores timeout since this is only used synchronously for now.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(self):
|
|
11
|
+
self.buffer = bytearray()
|
|
12
|
+
|
|
13
|
+
"""Reads specified number of bytes."""
|
|
14
|
+
def read(self, num_bytes) -> bytes:
|
|
15
|
+
num_bytes = min(len(self.buffer), num_bytes)
|
|
16
|
+
result = self.buffer[:num_bytes]
|
|
17
|
+
del self.buffer[:num_bytes]
|
|
18
|
+
return result
|
|
19
|
+
|
|
20
|
+
def read_all(self):
|
|
21
|
+
return self.read(self.length)
|
|
22
|
+
|
|
23
|
+
def read_until(self, expected: bytes) -> bytes:
|
|
24
|
+
if expected not in self.buffer:
|
|
25
|
+
return self.read_all()
|
|
26
|
+
|
|
27
|
+
length = self.buffer.index(expected) + len(expected)
|
|
28
|
+
result = self.buffer[:length]
|
|
29
|
+
del self.buffer[:length]
|
|
30
|
+
raise result
|
|
31
|
+
|
|
32
|
+
"""Allows reading without removing the data from the buffer"""
|
|
33
|
+
def peek(self, num_bytes) -> bytes:
|
|
34
|
+
num_bytes = min(len(self.buffer), num_bytes)
|
|
35
|
+
return self.buffer[:num_bytes]
|
|
36
|
+
|
|
37
|
+
def peek_until(self, expected: bytes, max_length=None) -> bytes:
|
|
38
|
+
if expected in self.buffer: #Read until the expected
|
|
39
|
+
length = self.buffer.index(expected) + len(expected)
|
|
40
|
+
if max_length is not None and length > max_length:
|
|
41
|
+
length = max_length
|
|
42
|
+
return self.buffer[:length]
|
|
43
|
+
|
|
44
|
+
#There is no expected, so read as far as possible
|
|
45
|
+
length = len(self.buffer)
|
|
46
|
+
if max_length is not None:
|
|
47
|
+
length = min(length, max_length)
|
|
48
|
+
return self.buffer[:length]
|
|
49
|
+
|
|
50
|
+
def readline(self) -> bytes:
|
|
51
|
+
return self.read_until(b"\n")
|
|
52
|
+
|
|
53
|
+
def peekline(self, max_length=None) -> bytes:
|
|
54
|
+
return self.peek_until(b"\n", max_length=max_length)
|
|
55
|
+
|
|
56
|
+
def insert(self, data: bytes):
|
|
57
|
+
self.buffer.extend(data)
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def length(self) -> int:
|
|
61
|
+
return len(self.buffer)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def timeout(self) -> float:
|
|
65
|
+
raise NotImplementedError()
|
|
66
|
+
|
|
67
|
+
@timeout.setter
|
|
68
|
+
def timeout(self, timeout: float):
|
|
69
|
+
raise NotImplementedError()
|
|
70
|
+
|
|
71
|
+
class ThreespaceBinaryParser:
|
|
72
|
+
"""
|
|
73
|
+
A class that can be used to parse a stream of binary data
|
|
74
|
+
that could contain multiple different command responses and validates
|
|
75
|
+
the responses to handle misalignment/data corruption.
|
|
76
|
+
|
|
77
|
+
Requires all expected responses to have the same header enabled.
|
|
78
|
+
|
|
79
|
+
The header should contain the cmd_echo, checksum, and data_length fields
|
|
80
|
+
for full functionality. The lack of any of those fields could limit functionality
|
|
81
|
+
of the parser.
|
|
82
|
+
|
|
83
|
+
If cmd_echo is missing, only one command can be registered with the binary parser as it has no way
|
|
84
|
+
of knowing what of verifying what the current incoming response is.
|
|
85
|
+
|
|
86
|
+
If checksum is missing, data integrity can not be checked.
|
|
87
|
+
|
|
88
|
+
If data_length is missing, commands that do not return a static length may cause blocking operations
|
|
89
|
+
or errors while parsing. (These are planned to be fixed in a future version)
|
|
90
|
+
|
|
91
|
+
NOTE: For speed, a custom implementation will be better then this parser class. This parser handles allowing
|
|
92
|
+
multiple commands as well as data validation and misalignment correction. It also formats the data into the ThreespaceCmdResult
|
|
93
|
+
response type. The overhead added because of all these additional checks/calculations can add a significant amount of time
|
|
94
|
+
to processing binary data compared to just reading a known amount of data and instantly unpacking it to a tuple in the desired format.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
COMMAND_EXCEPTIONS = [84, 177] #getStreamingBatch and fileReadBytes need additional info and so need registered via the
|
|
98
|
+
|
|
99
|
+
def __init__(self, data_stream: ThreespaceInputStream = None, verbose=False):
|
|
100
|
+
"""
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
data_stream - (optional) The data stream to use with the Binary Parser. If not supplied, will default to a new ThreespaceBufferInputStream
|
|
104
|
+
"""
|
|
105
|
+
self.data_stream = data_stream
|
|
106
|
+
self.registered_commands: dict[int,ThreespaceCommand] = {}
|
|
107
|
+
|
|
108
|
+
if self.data_stream is None:
|
|
109
|
+
self.data_stream = ThreespaceBufferInputStream()
|
|
110
|
+
self.header_info = None
|
|
111
|
+
|
|
112
|
+
self.__parsing_header: ThreespaceHeader = None #Used to optimize preventing reading to much by cacheing the header seperately from the cmd data
|
|
113
|
+
self.__parsing_command: ThreespaceCommand = None
|
|
114
|
+
self.__parsing_msg_length: int = None #Used seperately from the __parsing_header so can handle msg lengths that are static without modifying the header
|
|
115
|
+
|
|
116
|
+
self.misaligned = False
|
|
117
|
+
self.verbose = verbose
|
|
118
|
+
|
|
119
|
+
def register_command(self, cmd: int|ThreespaceCommand, **kwargs):
|
|
120
|
+
"""
|
|
121
|
+
Registers the given cmd number/cmd with the binary parser.
|
|
122
|
+
|
|
123
|
+
Some commands may require additional information:
|
|
124
|
+
stream_slots - list[int] Required when registering command 84 (getStreamingBatch) a list of command numbers that are being streamed.
|
|
125
|
+
read_size - 'auto' or int Required when registering a command that requires a given length such as fileReadBytes. If 'auto' will use the header length to determine length.
|
|
126
|
+
"""
|
|
127
|
+
if isinstance(cmd, int):
|
|
128
|
+
cmd = threespaceCommandGet(cmd)
|
|
129
|
+
if cmd is None:
|
|
130
|
+
raise ValueError(f"Invalid Cmd {cmd}")
|
|
131
|
+
|
|
132
|
+
if cmd.info.num in self.registered_commands:
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
#These command types are special and need additional info
|
|
136
|
+
if cmd.info.num == THREESPACE_FILE_READ_BYTES_COMMAND_NUM:
|
|
137
|
+
if "read_size" not in kwargs:
|
|
138
|
+
raise ValueError("Missing arguement 'read_size' when registering the fileReadBytes command with the binary parser")
|
|
139
|
+
raise NotImplementedError("The fileReadBytes command has yet to be implemented for the ThreespaceBinaryParser")
|
|
140
|
+
elif cmd.info.num == THREESPACE_GET_STREAMING_BATCH_COMMAND_NUM:
|
|
141
|
+
if "stream_slots" not in kwargs:
|
|
142
|
+
raise ValueError("Missing arguement 'stream_slots' when registering the getStreamingBatch command with the binary parser")
|
|
143
|
+
cmd = ThreespaceGetStreamingBatchCommand(kwargs['stream_slots'])
|
|
144
|
+
|
|
145
|
+
self.registered_commands[cmd.info.num] = cmd
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
def unregister_command(self, cmd: int|ThreespaceCommand):
|
|
149
|
+
if cmd.info.num not in self.registered_commands:
|
|
150
|
+
return False
|
|
151
|
+
del self.registered_commands[cmd.info.num]
|
|
152
|
+
return True
|
|
153
|
+
|
|
154
|
+
def set_header(self, header_info: ThreespaceHeaderInfo):
|
|
155
|
+
self.header_info = header_info
|
|
156
|
+
|
|
157
|
+
def insert_data(self, data: bytes):
|
|
158
|
+
"""
|
|
159
|
+
Add the given data to the default ThreespaceBufferInputStream.
|
|
160
|
+
This method will raise an exception if used on a different type of InputStream
|
|
161
|
+
"""
|
|
162
|
+
if not isinstance(self.data_stream, ThreespaceBufferInputStream):
|
|
163
|
+
raise Exception("Insert data with Binary Parser only valid when using the default data_stream")
|
|
164
|
+
self.data_stream.insert(data)
|
|
165
|
+
|
|
166
|
+
def parse_message(self) -> ThreespaceCmdResult:
|
|
167
|
+
if self.__parsing_header is None:
|
|
168
|
+
self.__parse_header()
|
|
169
|
+
|
|
170
|
+
if self.__parsing_command is None:
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
return self.__parse_command()
|
|
174
|
+
|
|
175
|
+
def __parse_header(self):
|
|
176
|
+
if self.data_stream.length < self.header_info.size:
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
header = self.data_stream.peek(self.header_info.size)
|
|
180
|
+
header = ThreespaceHeader.from_bytes(header, self.header_info)
|
|
181
|
+
|
|
182
|
+
cmd_found = False
|
|
183
|
+
if self.header_info.echo_enabled: #Search for the command to parse
|
|
184
|
+
for command in self.registered_commands.values():
|
|
185
|
+
if header.echo != command.info.num: continue
|
|
186
|
+
|
|
187
|
+
#Command matches! Attempt to parse
|
|
188
|
+
self.__parsing_command = command
|
|
189
|
+
self.__parsing_header = header
|
|
190
|
+
|
|
191
|
+
cmd_found = True
|
|
192
|
+
else: #Can only parse one command
|
|
193
|
+
if len(self.registered_commands) > 1:
|
|
194
|
+
raise Exception("Only one command type can be parsed when the 'cmd echo' is not enabled in the header")
|
|
195
|
+
self.__parsing_command = list(self.registered_commands.values())[0]
|
|
196
|
+
self.__parsing_header = header
|
|
197
|
+
cmd_found = True
|
|
198
|
+
|
|
199
|
+
if cmd_found:
|
|
200
|
+
if self.header_info.length_enabled:
|
|
201
|
+
self.__parsing_msg_length = self.__parsing_header.length
|
|
202
|
+
else:
|
|
203
|
+
self.__parsing_msg_length = self.__parsing_command.info.out_size
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
#This header is not related to any command, so it needs skipped
|
|
207
|
+
if self.verbose and not self.misaligned:
|
|
208
|
+
print("Unexpected header:", header)
|
|
209
|
+
self.misaligned = True
|
|
210
|
+
self.data_stream.read(1)
|
|
211
|
+
|
|
212
|
+
def __peek_checksum(self):
|
|
213
|
+
header_len = len(self.__parsing_header.raw_binary)
|
|
214
|
+
data = self.data_stream.peek(header_len + self.__parsing_msg_length)[header_len:]
|
|
215
|
+
checksum = sum(data) % 256
|
|
216
|
+
return checksum == self.__parsing_header.checksum
|
|
217
|
+
|
|
218
|
+
def __parse_command(self):
|
|
219
|
+
#Not enough data to parse yet
|
|
220
|
+
if self.data_stream.length < self.header_info.size + self.__parsing_msg_length:
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
if self.header_info.checksum_enabled and not math.isnan(self.__parsing_msg_length): #Can validate checksum before parsing
|
|
224
|
+
print("Pre validating checksum")
|
|
225
|
+
if not self.__peek_checksum():
|
|
226
|
+
#Data corruption/Misalignment error
|
|
227
|
+
if self.verbose and not self.misaligned:
|
|
228
|
+
print("Checksum mismatch for command", self.__parsing_command.info.num)
|
|
229
|
+
self.misaligned = True
|
|
230
|
+
self.data_stream.read(1)
|
|
231
|
+
self.__parsing_command = None
|
|
232
|
+
self.__parsing_header = None
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
#Header and pre validation checksum checks out! Now just parse the actual command result and return it
|
|
236
|
+
header = self.__parsing_header
|
|
237
|
+
self.data_stream.read(len(header.raw_binary)) #Skip these bytes since they are already parsed
|
|
238
|
+
result, raw = self.__parsing_command.read_command(self.data_stream)
|
|
239
|
+
|
|
240
|
+
#Validate checksum if couldn't pre-validate due to unknown message length
|
|
241
|
+
if math.isnan(self.__parsing_msg_length) and self.header_info.checksum_enabled:
|
|
242
|
+
checksum = sum(raw) % 256
|
|
243
|
+
if checksum != header.checksum:
|
|
244
|
+
if self.verbose and not self.misaligned:
|
|
245
|
+
print("Checksum mismatch for command", self.__parsing_command.info.num)
|
|
246
|
+
self.misaligned = True
|
|
247
|
+
self.__parsing_command = None
|
|
248
|
+
self.__parsing_header = None
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
#Reset and return
|
|
252
|
+
self.__parsing_header = None
|
|
253
|
+
self.__parsing_command = None
|
|
254
|
+
self.misaligned = False
|
|
255
|
+
return ThreespaceCmdResult(result, header, data_raw_binary=raw)
|
|
256
|
+
|