Skip to content

Commit 392529d

Browse files
committed
fix(tools): add velocity and range guards to offline baro thrust replay
The offline CF+RLS replay lacked the firmware's soft guards, so landing dynamics (ground effect, thrust cutoff) corrupted the K estimate. Add velocity (|vz|>2, vxy>5) and range proximity (<0.5m) guards matching the firmware, applied to both the replay and the CF bandwidth sweep.
1 parent 0c03e71 commit 392529d

File tree

1 file changed

+56
-20
lines changed

1 file changed

+56
-20
lines changed

Tools/baro_compensation/baro_thrust_calibration.py

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,15 @@ def extract_ekf_z(ulog):
159159
d = get_topic(ulog, "vehicle_local_position")
160160
if d is None:
161161
return None
162-
return {
162+
result = {
163163
"time_s": us_to_s(d.data["timestamp"], ulog.start_timestamp),
164164
"z": d.data["z"],
165165
}
166+
if "vz" in d.data:
167+
result["vz"] = d.data["vz"]
168+
if "vx" in d.data and "vy" in d.data:
169+
result["vxy"] = np.sqrt(d.data["vx"]**2 + d.data["vy"]**2)
170+
return result
166171

167172

168173
def extract_ekf_baro_obs(ulog):
@@ -311,9 +316,47 @@ def thrust_std(self):
311316
return float(np.sqrt(max(self._thrust_var, 0.0)))
312317

313318

319+
def build_estimation_mask(baro_t, armed_start, armed_end, landed,
320+
ekf_z=None, range_data=None):
321+
"""Build sample mask matching firmware hard + soft guards.
322+
323+
Hard gates: armed AND not landed.
324+
Soft gates (skip RLS but keep CF in firmware — here we skip both since
325+
the offline replay doesn't separate CF from RLS per-sample):
326+
- |vz| > 2 m/s
327+
- vxy > 5 m/s
328+
- range sensor < 0.5 m (ground effect proximity)
329+
"""
330+
armed = (baro_t >= armed_start) & (baro_t <= armed_end)
331+
332+
if landed is not None:
333+
is_landed = (np.interp(baro_t, landed["time_s"],
334+
landed["landed"].astype(float)) > 0.5)
335+
else:
336+
is_landed = np.zeros(len(baro_t), dtype=bool)
337+
338+
mask = armed & ~is_landed
339+
340+
if ekf_z is not None and "vz" in ekf_z:
341+
vz = np.interp(baro_t, ekf_z["time_s"], ekf_z["vz"])
342+
mask &= np.abs(vz) <= 2.0
343+
344+
if ekf_z is not None and "vxy" in ekf_z:
345+
vxy = np.interp(baro_t, ekf_z["time_s"], ekf_z["vxy"])
346+
mask &= vxy <= 5.0
347+
348+
if range_data is not None:
349+
rng = np.interp(baro_t, range_data["time_s"],
350+
range_data["distance_m"])
351+
mask &= rng > 0.5
352+
353+
return mask
354+
355+
314356
def run_offline_cf_rls(baro, accel, attitude, thrust, landed,
315357
armed_start, armed_end,
316-
cf_bandwidth=None, rls_lambda=None):
358+
cf_bandwidth=None, rls_lambda=None,
359+
ekf_z=None, range_data=None):
317360
"""Replay CF+RLS on logged sensor data. Returns K trace and residuals."""
318361
baro_t = baro["time_s"]
319362
baro_alt = baro["alt_m"]
@@ -329,14 +372,8 @@ def run_offline_cf_rls(baro, accel, attitude, thrust, landed,
329372
accel_up = np.interp(baro_t, accel["time_s"], accel_up_all)
330373
thrust_interp = np.interp(baro_t, thrust["time_s"], thrust["thrust"])
331374

332-
# Airborne mask: armed AND not landed (matches firmware hard gates)
333-
armed = (baro_t >= armed_start) & (baro_t <= armed_end)
334-
if landed is not None:
335-
is_landed = (np.interp(baro_t, landed["time_s"],
336-
landed["landed"].astype(float)) > 0.5)
337-
else:
338-
is_landed = np.zeros(len(baro_t), dtype=bool)
339-
airborne = armed & ~is_landed
375+
airborne = build_estimation_mask(baro_t, armed_start, armed_end, landed,
376+
ekf_z, range_data)
340377

341378
est = CfRls(cf_bandwidth=cf_bandwidth, rls_lambda=rls_lambda)
342379
n = len(baro_t)
@@ -464,7 +501,8 @@ def run_range_calibration(baro, range_data, thrust,
464501
# ---------------------------------------------------------------------------
465502

466503
def sweep_cf_params(baro, accel, attitude, thrust, landed,
467-
armed_start, armed_end, range_cal, existing_pcoef):
504+
armed_start, armed_end, range_cal, existing_pcoef,
505+
ekf_z=None, range_data=None):
468506
"""Sweep CF bandwidth at two lambda values, evaluate against range truth."""
469507
bandwidths = np.logspace(np.log10(0.01), np.log10(1.0), 30)
470508
lambdas = [("lambda_0.998", 0.998), ("lambda_1.0", 1.0)]
@@ -482,13 +520,9 @@ def sweep_cf_params(baro, accel, attitude, thrust, landed,
482520
accel_up = np.interp(baro_t, accel["time_s"], accel_up_all)
483521
thrust_interp = np.interp(baro_t, thrust["time_s"], thrust["thrust"])
484522

485-
armed = (baro_t >= armed_start) & (baro_t <= armed_end)
486-
if landed is not None:
487-
is_landed = (np.interp(baro_t, landed["time_s"],
488-
landed["landed"].astype(float)) > 0.5)
489-
else:
490-
is_landed = np.zeros(len(baro_t), dtype=bool)
491-
airborne_idx = np.where(armed & ~is_landed)[0]
523+
mask = build_estimation_mask(baro_t, armed_start, armed_end, landed,
524+
ekf_z, range_data)
525+
airborne_idx = np.where(mask)[0]
492526

493527
raw_err = range_cal["raw_err_fit"]
494528
thr_fit = range_cal["thrust_fit"]
@@ -1009,7 +1043,8 @@ def main():
10091043
offline = None
10101044
if accel is not None and attitude is not None:
10111045
offline = run_offline_cf_rls(baro, accel, attitude, thrust, landed,
1012-
armed_start, armed_end)
1046+
armed_start, armed_end,
1047+
ekf_z=ekf_z, range_data=range_data)
10131048
summary.append(f"Offline CF+RLS:")
10141049
summary.append(f" Final K = {offline['final_k']:.3f}")
10151050
summary.append(f" Total PCOEF: {existing_pcoef:+.2f} "
@@ -1099,7 +1134,8 @@ def main():
10991134
print("\nSweeping CF bandwidth...")
11001135
sweep = sweep_cf_params(baro, accel, attitude, thrust, landed,
11011136
armed_start, armed_end, range_cal,
1102-
existing_pcoef)
1137+
existing_pcoef,
1138+
ekf_z=ekf_z, range_data=range_data)
11031139
fig, best_bw, min_bw, min_std = plot_cf_tuning(
11041140
sweep, range_cal, existing_pcoef)
11051141
figures.append(fig)

0 commit comments

Comments
 (0)