Skip to content

Commit aebaa12

Browse files
Convenience API for modifying Single Run Data
1 parent 7ed9cc0 commit aebaa12

2 files changed

Lines changed: 158 additions & 76 deletions

File tree

perda/core_data_structures/single_run_data.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,93 @@ def __getitem__(
7575
else:
7676
raise ValueError("Input must be a string, int, or DataInstance.")
7777

78+
def __setitem__(self, cpp_name: str, di: DataInstance) -> None:
79+
"""
80+
Add or replace a variable using dictionary-style assignment.
81+
82+
Dispatches to ``replace`` if ``cpp_name`` already exists, else ``add``.
83+
84+
Parameters
85+
----------
86+
cpp_name : str
87+
C++ variable name to insert or overwrite.
88+
di : DataInstance
89+
DataInstance to store. Must have non-None ``label`` and ``cpp_name``.
90+
91+
Examples
92+
--------
93+
>>> data["my.new.var"] = DataInstance(timestamp_np=ts, value_np=vals, label="My var", cpp_name="my.new.var")
94+
>>> data["my.existing.var"] = updated_di
95+
"""
96+
if cpp_name in self:
97+
self.replace(cpp_name, di)
98+
else:
99+
self.add(cpp_name, di)
100+
101+
def add(self, cpp_name: str, di: DataInstance) -> None:
102+
"""
103+
Insert a new derived DataInstance using a synthetic negative ID.
104+
105+
Parameters
106+
----------
107+
cpp_name : str
108+
C++ variable name key for the new variable.
109+
di : DataInstance
110+
DataInstance to insert. Must have non-None ``label`` and ``cpp_name``.
111+
"""
112+
if di.label is None:
113+
raise ValueError("DataInstance.label must be set before calling add().")
114+
if di.cpp_name is None:
115+
raise ValueError("DataInstance.cpp_name must be set before calling add().")
116+
if cpp_name in self:
117+
raise KeyError(f"'{cpp_name}' already exists; use replace() to overwrite.")
118+
119+
synthetic_id = -(len(self.id_to_instance) + 1)
120+
stored = DataInstance(
121+
timestamp_np=di.timestamp_np,
122+
value_np=di.value_np,
123+
label=di.label,
124+
var_id=synthetic_id,
125+
cpp_name=cpp_name,
126+
)
127+
self.id_to_instance[synthetic_id] = stored
128+
self.cpp_name_to_id[cpp_name] = synthetic_id
129+
self.id_to_cpp_name[synthetic_id] = cpp_name
130+
self.id_to_descript[synthetic_id] = di.label
131+
132+
def replace(self, cpp_name: str, di: DataInstance) -> None:
133+
"""
134+
Overwrite the values of an existing variable in-place.
135+
136+
Parameters
137+
----------
138+
cpp_name : str
139+
C++ variable name of the variable to replace.
140+
di : DataInstance
141+
DataInstance whose ``value_np`` (and optionally updated timestamps) replaces the stored one.
142+
Must have non-None ``label`` and ``cpp_name``.
143+
"""
144+
if di.label is None:
145+
raise ValueError("DataInstance.label must be set before calling replace().")
146+
if di.cpp_name is None:
147+
raise ValueError(
148+
"DataInstance.cpp_name must be set before calling replace()."
149+
)
150+
if cpp_name not in self:
151+
raise KeyError(
152+
f"'{cpp_name}' not found; use add() to insert a new variable."
153+
)
154+
155+
var_id = self.cpp_name_to_id[cpp_name]
156+
old = self.id_to_instance[var_id]
157+
self.id_to_instance[var_id] = DataInstance(
158+
timestamp_np=di.timestamp_np,
159+
value_np=di.value_np,
160+
label=di.label,
161+
var_id=old.var_id,
162+
cpp_name=old.cpp_name,
163+
)
164+
78165
def __contains__(self, input_var_id_name: Union[str, int]) -> bool:
79166
"""
80167
Check if variable ID or variable name exists in the data.

perda/utils/preprocessing.py

Lines changed: 71 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -39,41 +39,6 @@
3939
PreprocessingStep = Callable[[SingleRunData], SingleRunData]
4040

4141

42-
def _replace(data: SingleRunData, cpp_name: str, new_values: NDArray[float64]) -> None:
43-
"""Overwrite the value_np of an existing DataInstance in-place (mutates dict)."""
44-
var_id = data.cpp_name_to_id[cpp_name]
45-
old = data.id_to_instance[var_id]
46-
data.id_to_instance[var_id] = DataInstance(
47-
timestamp_np=old.timestamp_np,
48-
value_np=new_values,
49-
label=old.label,
50-
var_id=old.var_id,
51-
cpp_name=old.cpp_name,
52-
)
53-
54-
55-
def _add(
56-
data: SingleRunData,
57-
cpp_name: str,
58-
label: str,
59-
timestamp_np: NDArray[float64],
60-
value_np: NDArray[float64],
61-
) -> None:
62-
"""Insert a new derived DataInstance using a synthetic negative ID."""
63-
synthetic_id = -(len(data.id_to_instance) + 1)
64-
di = DataInstance(
65-
timestamp_np=timestamp_np,
66-
value_np=value_np,
67-
label=label,
68-
var_id=synthetic_id,
69-
cpp_name=cpp_name,
70-
)
71-
data.id_to_instance[synthetic_id] = di
72-
data.cpp_name_to_id[cpp_name] = synthetic_id
73-
data.id_to_cpp_name[synthetic_id] = cpp_name
74-
data.id_to_descript[synthetic_id] = label
75-
76-
7742
def patch_ned_velocity(data: SingleRunData) -> SingleRunData:
7843
"""Correct a VectorNav bug where velocityBody.x/y/z contains NED instead of body-frame velocities.
7944
@@ -108,14 +73,39 @@ def patch_ned_velocity(data: SingleRunData) -> SingleRunData:
10873
vel_d = vel_d1.value_np
10974
yaw_rad = np.radians(yaw_deg.value_np)
11075

111-
_add(data, "velN", "NED North velocity (raw)", vel_n1.timestamp_np, vel_n.copy())
112-
_add(data, "velE", "NED East velocity (raw)", vel_e1.timestamp_np, vel_e.copy())
113-
_add(data, "velD", "NED Down velocity (raw)", vel_d1.timestamp_np, vel_d.copy())
76+
data["velN"] = DataInstance(
77+
timestamp_np=vel_n1.timestamp_np,
78+
value_np=vel_n.copy(),
79+
label="NED North velocity (raw)",
80+
cpp_name="velN",
81+
)
82+
data["velE"] = DataInstance(
83+
timestamp_np=vel_e1.timestamp_np,
84+
value_np=vel_e.copy(),
85+
label="NED East velocity (raw)",
86+
cpp_name="velE",
87+
)
88+
data["velD"] = DataInstance(
89+
timestamp_np=vel_d1.timestamp_np,
90+
value_np=vel_d.copy(),
91+
label="NED Down velocity (raw)",
92+
cpp_name="velD",
93+
)
11494

11595
cos_y = np.cos(yaw_rad)
11696
sin_y = np.sin(yaw_rad)
117-
_replace(data, VECTORNAV_BODY_VEL_X, vel_n * cos_y + vel_e * sin_y) # forward
118-
_replace(data, VECTORNAV_BODY_VEL_Y, -vel_n * sin_y + vel_e * cos_y) # right
97+
data[VECTORNAV_BODY_VEL_X] = DataInstance(
98+
timestamp_np=vel_n1.timestamp_np,
99+
value_np=vel_n * cos_y + vel_e * sin_y,
100+
label=data[VECTORNAV_BODY_VEL_X].label,
101+
cpp_name=VECTORNAV_BODY_VEL_X,
102+
) # forward
103+
data[VECTORNAV_BODY_VEL_Y] = DataInstance(
104+
timestamp_np=vel_e1.timestamp_np,
105+
value_np=-vel_n * sin_y + vel_e * cos_y,
106+
label=data[VECTORNAV_BODY_VEL_Y].label,
107+
cpp_name=VECTORNAV_BODY_VEL_Y,
108+
) # right
119109
# vel_z (down) is identical in NED and FRD — no change needed
120110

121111
print(
@@ -147,14 +137,18 @@ def convert_wheelspeeds_to_m_per_s(data: SingleRunData) -> SingleRunData:
147137
di = data[col]
148138
backup_name = col + "_mph"
149139
if backup_name not in data:
150-
_add(
151-
data,
152-
backup_name,
153-
(di.label or col) + " (mph backup)",
154-
di.timestamp_np,
155-
di.value_np.copy(),
140+
data[backup_name] = DataInstance(
141+
timestamp_np=di.timestamp_np,
142+
value_np=di.value_np.copy(),
143+
label=(di.label or col) + " (mph backup)",
144+
cpp_name=backup_name,
156145
)
157-
_replace(data, col, mph_to_m_per_s(di.value_np))
146+
data[col] = DataInstance(
147+
timestamp_np=di.timestamp_np,
148+
value_np=mph_to_m_per_s(di.value_np),
149+
label=di.label,
150+
cpp_name=col,
151+
)
158152

159153
print(
160154
f"convert_wheelspeeds_to_m_per_s: converted {len(cols)} channels mph → m/s, backups in *_mph"
@@ -182,27 +176,30 @@ def correct_motor_data(data: SingleRunData) -> SingleRunData:
182176

183177
backup_name = MOTOR_RPM + "_raw"
184178
if backup_name not in data:
185-
_add(
186-
data,
187-
backup_name,
188-
"Motor RPM raw (pre-flip)",
189-
di.timestamp_np,
190-
raw_rpm.copy(),
179+
data[backup_name] = DataInstance(
180+
timestamp_np=di.timestamp_np,
181+
value_np=raw_rpm.copy(),
182+
label="Motor RPM raw (pre-flip)",
183+
cpp_name=backup_name,
191184
)
192185

193186
flipped = -raw_rpm
194-
_replace(data, MOTOR_RPM, flipped)
187+
data[MOTOR_RPM] = DataInstance(
188+
timestamp_np=di.timestamp_np,
189+
value_np=flipped,
190+
label=di.label,
191+
cpp_name=MOTOR_RPM,
192+
)
195193

196194
tire_radius_m = in_to_m(TIRE_RADIUS_IN)
197195
wheel_speed: NDArray[float64] = (
198196
flipped * 2.0 * np.pi * tire_radius_m / (60.0 * GEAR_RATIO)
199197
)
200-
_add(
201-
data,
202-
MOTOR_WHEELSPEED,
203-
"Driven wheel speed from motor RPM (m/s)",
204-
di.timestamp_np,
205-
wheel_speed,
198+
data[MOTOR_WHEELSPEED] = DataInstance(
199+
timestamp_np=di.timestamp_np,
200+
value_np=wheel_speed,
201+
label="Driven wheel speed from motor RPM (m/s)",
202+
cpp_name=MOTOR_WHEELSPEED,
206203
)
207204

208205
print(
@@ -275,24 +272,19 @@ def __call__(
275272
backup_name = STEERING_ANGLE + "_original"
276273
if STEERING_ANGLE in data and backup_name not in data:
277274
orig = data[STEERING_ANGLE]
278-
_add(
279-
data,
280-
backup_name,
281-
(orig.label or STEERING_ANGLE) + " (original)",
282-
orig.timestamp_np,
283-
orig.value_np.copy(),
275+
data[backup_name] = DataInstance(
276+
timestamp_np=orig.timestamp_np,
277+
value_np=orig.value_np.copy(),
278+
label=(orig.label or STEERING_ANGLE) + " (original)",
279+
cpp_name=backup_name,
284280
)
285281

286-
if STEERING_ANGLE in data:
287-
_replace(data, STEERING_ANGLE, recomputed)
288-
else:
289-
_add(
290-
data,
291-
STEERING_ANGLE,
292-
"Steering angle recomputed from raw voltage (deg)",
293-
raw_di.timestamp_np,
294-
recomputed,
295-
)
282+
data[STEERING_ANGLE] = DataInstance(
283+
timestamp_np=raw_di.timestamp_np,
284+
value_np=recomputed,
285+
label="Steering angle recomputed from raw voltage (deg)",
286+
cpp_name=STEERING_ANGLE,
287+
)
296288

297289
cal_str = ", ".join(f"({v:.2f}V, {a:+.1f}°)" for v, a in self.pts)
298290
print(
@@ -302,6 +294,9 @@ def __call__(
302294
return data
303295

304296

297+
# This is the global callable that should actually be used in the preprocessing pipeline.
298+
# Either pass it directly, or with a custom calibration correct_steering_angle(calibration=...)
299+
# This should be treated like a function that supports partial application.
305300
correct_steering_angle = CorrectSteeringAngleLambda()
306301

307302

0 commit comments

Comments
 (0)