Fix the phase-1/2 review findings in recording, calibration and depth replay
What - record: one packet->row conversion (record/sample.py). A failed compute chain records no force value at all, a newton value is written only for an axis that really has a calibration curve, and mono_t (not the wall clock) orders history with a per-row calibration hash. - record lifecycle: explicit starting/recording/stopping/stopped/failed states owned by the writer thread; stop() reports completed=False on timeout and does not write ended_at; consecutive write failures stop acceptance immediately. - record capacity: byte budget for the video queue (512 MiB default); a full queue sheds the *frame* and parks the tiny data row, so a video overflow no longer deletes CSV rows. Lost rows leave a dropped=1 index row and are counted as dropped_gaps / unrecorded_gaps. - calibration identity: sha256 content hash instead of a path, minted from the calibration the sensor actually loaded (reported before the first frame) and confirmed by the acquisition thread; events are timestamped when they happen. - calibration diagnostics: the fit summary, residual and validation error now come from the model that export/apply would use (piecewise_linear); validation points are held out of the build; repeated reference loads are merged with an n-weighted mean and pooled std. - calibration record: every point keeps its full three-axis sample series (frame id, time, raw, newtons, valid) plus diagnostics, written to an independent calibration_session_*.json that can be reopened for inspection; save record / export coefficients / apply to device are three separate actions. - depth replay: the cursor's frame is bound to the depth result by request sequence and frame id, so a moved cursor never shows another frame's map; the SDK is stepped from its current position instead of re-reading from the start; the live chain (box deformation+contact, finger unroll+tactile) is replayed with the exact resolved config path; algorithm state is rebuilt from a reset marker or from the recorded pre-roll, and says so when it cannot be. Why The review (D:/orisys_ws/_review_phase12/review.md) showed the app writing raw counts into the newton columns, recording a stale value after a failed compute, losing CSV rows when the video queue filled, reporting success while a writer was still running, diagnosing a model nobody applies, applying a calibration before the device confirmed it, dropping the samples behind a calibration point, showing a stale depth map under a moved cursor and recomputing depth of the wrong frame. Verification - D:/orisys_ws/_review_phase12/reproduce.py: all ten acceptance lines flipped (log saved as _review_phase12/reproduce_after.log). Including 25/25 rows kept with 8 video frames shed, frames [2]->[3], depth 8.0/8.0, fit 2/2 points. - apps/studio/tools/ui_check.py: 249 checks, 0 failures (twice). - shared suites: 91 tests, 0 failures (new: test_record_semantics, test_record_lifecycle, test_calibration_model, test_calibration_record, test_depth_replay). - apps/studio/tests/test_ui_smoke.py: 23/30; the 7 remaining are harness artifacts of the sandbox (4 tmp_path PermissionError, 3 parametrized tests the shim cannot drive), unchanged from before this change. - compileall clean; locale parity zh/en + t() key coverage: 0 failures. Not in this change - P2#10 disk-based browsing of a long recording and range CSV export (next round, per your choice). - Stability-triggered calibration sampling, replay fixed colour scale, PyOpenGL three-dimensional view. - Hardware verification: no camera attached, so the SDK's real processing chain and the pre-roll's effect on a live tracker were exercised with fakes only.
This commit is contained in:
@@ -608,9 +608,12 @@ def main() -> int:
|
||||
"the fit summary is empty",
|
||||
)
|
||||
|
||||
# Three more points, one of them off-trend, so the fit has a residual
|
||||
# that excluding the outlier must reduce and enough left over to hold
|
||||
# one out as a validation point.
|
||||
# Four points, one of them off-trend (raw 400 where the others imply
|
||||
# ~200). The exported model is piecewise-linear, so it passes through
|
||||
# every point it is built from: the training residual is zero by
|
||||
# construction and the off-trend point can only show up as a
|
||||
# *validation* error. The summary must describe that model, not a
|
||||
# private least-squares line.
|
||||
for load, raw in ((2.0, 400.0), (3.0, 300.6), (5.0, 500.9)):
|
||||
window.cal_recorder.start(
|
||||
channel="fn", reference_load_n=load, duration_s=1.0
|
||||
@@ -621,22 +624,24 @@ def main() -> int:
|
||||
app.processEvents()
|
||||
check(window.cal_panel.table.rowCount() == 4, "calibration rows missing")
|
||||
|
||||
worst = window.cal_recorder.fit("fn").max_residual_n
|
||||
check(worst > 0.05, f"the off-trend point produced no residual: {worst}")
|
||||
window.cal_panel.table.setCurrentCell(1, 0)
|
||||
window.cal_panel._on_toggle_excluded()
|
||||
app.processEvents()
|
||||
fit = window.cal_recorder.fit("fn")
|
||||
check(
|
||||
window.cal_recorder.fit("fn").max_residual_n < worst,
|
||||
"excluding an outlier did not tighten the fit",
|
||||
fit.method == "piecewise_linear",
|
||||
f"the summary is not about the exported model: {fit.method!r}",
|
||||
)
|
||||
check(
|
||||
window.cal_panel.table.item(1, 5).text() == t("cal.state_excluded"),
|
||||
"the excluded row does not show its state",
|
||||
fit.breakpoints == 4,
|
||||
f"the exported model has {fit.breakpoints} breakpoints, not 4",
|
||||
)
|
||||
check(
|
||||
fit.max_residual_n <= 1e-9,
|
||||
"the exported model does not reproduce its own calibration points: "
|
||||
f"max residual {fit.max_residual_n}",
|
||||
)
|
||||
|
||||
# A validation point is held out of the fit and reported separately.
|
||||
window.cal_panel.table.setCurrentCell(2, 0)
|
||||
# Hold the off-trend point out: the exported model is then built
|
||||
# without it and the misfit becomes visible as a validation error.
|
||||
window.cal_panel.table.setCurrentCell(1, 0)
|
||||
window.cal_panel._on_toggle_validation()
|
||||
app.processEvents()
|
||||
check(
|
||||
@@ -644,6 +649,35 @@ def main() -> int:
|
||||
in window.cal_panel.fit_label.text(),
|
||||
"no validation summary after marking a validation point",
|
||||
)
|
||||
validation = window.cal_recorder.fit("fn").validation_error_n
|
||||
check(
|
||||
validation > 0.05,
|
||||
f"the off-trend point produced no validation error: {validation}",
|
||||
)
|
||||
window.cal_panel._on_toggle_validation()
|
||||
|
||||
# Excluding a point removes it from the build, so the exported model
|
||||
# no longer passes through it.
|
||||
window.cal_panel._on_toggle_excluded()
|
||||
app.processEvents()
|
||||
check(
|
||||
window.cal_panel.table.item(1, 5).text() == t("cal.state_excluded"),
|
||||
"the excluded row does not show its state",
|
||||
)
|
||||
from orisys_app_shared.force.calibration_io import (
|
||||
build_export_model,
|
||||
predict_channel,
|
||||
)
|
||||
|
||||
store = window.cal_panel._store
|
||||
model = build_export_model(
|
||||
{key: store.channel(key) for key in ("fn", "fx", "fy")}
|
||||
)
|
||||
predicted = predict_channel(model, "fn", 400.0)
|
||||
check(
|
||||
abs(predicted - 2.0) > 0.05,
|
||||
f"excluding a point left the exported model unchanged: {predicted}",
|
||||
)
|
||||
window.cal_recorder.reset_session()
|
||||
window.cal_panel._refresh_table()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user