修正朝上判断、接触高度与过期姿态目标

朝上判断误用平台轴、接触高度报旋转前数值、多点对准复用缓存姿态,且多点会话被复核入口拒绝;
现按修正计划改正并接通 autocal-multipoint-1.0 只读复核,缺失帧源如实报告而不补零。

离线验证:新回归在 dc5a6f8 上失败、此处通过,panel_check 通过,两截图点接触高度复现
19.0510 / 21.5781 mm;实机项未做,详见 .scratch/autocal-motion-fix/verification.md。
This commit is contained in:
ykcheungab
2026-10-15 06:52:28 +08:00
parent 5339624ffa
commit bcc55a3b97
29 changed files with 2179 additions and 209 deletions
@@ -96,6 +96,10 @@ finger_F001_<时间>_<标识>/
参考窗口采用起点包含、终点不包含的时间范围。换档没有恒定力标签,视频和算法输出仍保存。`trial_finished` 表示请求正常结束;最终是否 complete 以排空后的 manifest 为准。保存失败不能当作成功;重试不覆盖原文件。
### 多点会话格式
多点标定按测点组织(`sites/<site_id>/repeat-00N/`),复核输入见[多点标定会话格式](multipoint-data-format.zh.md)。
## 软件数据流及后续
| 输入 → 处理 → 输出 | 所有者 |
+12 -6
View File
@@ -53,7 +53,7 @@ With the corrected envelope, solved by closed form
| Envelope | Worst residual | Within 0.5 deg | In range |
|---|---|---|---|
| pitch 360 deg, **yaw +-180 deg** (the machine) | **1.2e-06 deg** | 100% | **1032 / 1032** |
| pitch 90 deg, **yaw +-180 deg** (the machine) | **1.2e-06 deg** | 100% | **1032 / 1032** |
Every vertex is reachable, and the residual is at the floating-point floor rather
than at any clamp. The "0.21 deg worst, genuinely clamped vertex" claim above was
@@ -65,11 +65,17 @@ the reachability evidence. Whether they help *repeatability* — a shorter pitch
move, a better-conditioned contact — is a separate bench question, not a
kinematic one.
One thing the corrected solve did surface: **51 of 1032 vertices end inverted**,
whichever closed-form branch is chosen, because the tool direction is
antiparallel to the sensor's own up axis. That is the tool-polarity question, it
needs the bench, and it is *not* visible in the residual. See
`inverse-kinematics.md` → *Still open*.
**Correction (motion-correction-plan.zh.md task 1).** This note used to claim that
**51 of 1032 vertices end inverted** whichever branch is chosen, because the tool
direction was antiparallel to the sensor's own up axis. That count came from
`uprightness` comparing a platform axis through `to_sensor(np.eye(3)[1])`, which is
the sensor's own **-Z** — a horizontal axis — so a body rolled about the vertical
was reported as turned over. Measured against the post-mount nominal up (the tool
axis) no exported vertex is inverted, and the corrected flag changes no vertex's
branch selection. What did invert poses was the `approach_sign` polarity bug
recorded above; the residual could not see it, and neither could that up test. See
`inverse-kinematics.md` → *the face is aimed at +Z* and *the two exact poses of one
normal*.
## What was wrong before, and why it is worth recording
+1 -1
View File
@@ -67,7 +67,7 @@ Three buttons, and where each one lives is deliberate:
| Button | Where | What it does |
| --- | --- | --- |
| **执行动作** Execute | this screen, next to 播放动作 | Sends the selected point's solved pose to the platform: **yaw, then pitch, then table X/Y**. The press axis **z is not commanded** — pressing is P7, with contact detection and press depth, neither of which exists here, so a move to the contact height would be a press with none of its guards. |
| **执行动作** Execute | this screen, next to 播放动作 | Sends the selected point's solved pose to the platform: the stages together with the table X/Y, and the press axis **only when its zero is calibrated** (设备设置 → 压轴). The geometric contact height is not a controller coordinate, so an uncalibrated press axis is *held* and reported as held rather than commanded with a number the machine has not measured. |
| **停止** Stop | here *and* the strip's 全部停止 | The same command. Two access points, one implementation: during a move the operator's eyes are on this pane, and an emergency stop has to be where they are. |
| **回初始位** Return to zero | the axis rail, on every tab | Every drive to 0 — press axis first, then the stages, then the table. A machine-level action, wanted after work in any tab, so it is not repeated inside one. |
+79 -40
View File
@@ -17,7 +17,7 @@ the contact point ends up, and Z is reported, never solved.
|---|---|---|
| Tool axis | `z` | `motion/frames.py` |
| Yaw axis | `z`, travel **±180°** | `YAW_AXIS`, `finger_yaw_limit_deg` |
| Pitch axis | `y`, travel 360° | `PITCH_AXIS`, `finger_pitch_limit_deg` |
| Pitch axis | `y`, travel **90°** | `PITCH_AXIS`, `finger_pitch_limit_deg` |
| Stage order | `R_pitch @ R_yaw` | `stage_rotation` |
| Mount map | sensor `x→x, y→z, z→-y` | `AXIS_MAP` |
| Aim direction | `(0, 0, +1)` in the platform frame | `approach_sign = +1` |
@@ -29,9 +29,10 @@ tool feeds **down** onto it. So the sensing face — and the surface normal —
along **`+Z`**, and the solver's aim is to bring the normal onto `+Z`.
`approach_sign` was **`-1`**, aiming every normal at `-Z` and asking the machine to
turn the sensor over. It survived because the residual is sign-blind
(`arccos` maps a face turned fully over to `0.0000°`, the same as perfectly
aligned) and because every test asserted only that the residual was *small*. The
turn the sensor over. Nothing caught it, because the residual is measured against
the aim vector itself: `acos(turned · tool_direction)` reports `0.0000°` for a face
that landed *on the aim*, and the aim had been flipped — so a pose turned fully over
scored as perfect. Every test asserted only that the residual was *small*. The
failure was visible **only** as the model flipping upside down.
Measured over the export, counting vertices whose face ends up pointing away from
@@ -40,12 +41,15 @@ the tool:
| `approach_sign` | Face ends inverted | Nominal pose (`yaw = pitch = 0`) |
|---|---|---|
| `-1` (was) | **981 / 1032** | pitch **180°** needed; lands on `(0, 0, -1)` |
| `+1` (now) | **22 / 1032** | **no rotation**; lands on `(0, 0, +1)` |
| `+1` (now) | **0 / 1032** | **no rotation**; lands on `(0, 0, +1)` |
The 22 are the rim of the dome: their normals sit 60–90° off the tool axis, so
aligning them necessarily rolls the body about that normal and the sign is
decided by rounding. They are edge-on, not turned over — the most negative
`uprightness` among them is a hair past zero, not `-1`.
An earlier revision of this document reported 22 vertices not `upright` under `+1`.
That count came from `uprightness` comparing a **platform Y** direction — a
horizontal axis — so a pose that merely rolled the body about the vertical was
reported as turned over. `motion-correction-plan.zh.md` task 1 replaced the
reference with the post-mount nominal up (the tool axis) in the same frame; with
that reference the exported model reports `upright` for **all 1032 vertices**, and
the corrected flag does not change which branch any of them selects.
The nominal pose needing **no rotation at all** is the tell: a machine that has to
make a half-turn to reach its own commissioned configuration has the polarity
@@ -127,9 +131,9 @@ The closed form is **~200,000× more accurate** and **~7× faster**. Verified at
That residual is the floating-point floor: `acos` near 1 loses about `sqrt(eps)`
in radians, so "exact" cannot be reported as more precise than this.
All 1032 vertices are **in range**, and 1010 report `upright`. The 22 that do not
are the rim of the dome, where the normal is 60–90° off the tool axis — see
*the face is aimed at +Z* above and *still open* below.
All 1032 vertices are **in range**, and all 1032 report `upright` under the
tool-axis reference task 1 introduced. Reachability is the yaw limit, not the rim:
see *the face is aimed at +Z* above and *the two exact poses of one normal* below.
### The two pitch bindings are one family
@@ -154,43 +158,78 @@ everything the machine needs to know:
| Field | Meaning |
|---|---|
| `residual_deg` | angle between the rotated normal and the tool direction |
| `upright` | whether the pose leaves the sensing face pointing along the tool |
| `upright` | whether the mounted body's up axis still points along the tool |
| `in_range` | whether the pose is inside the stage travel |
| `reason` | why not, when it is not |
| `mounted_point_mm` | the picked point after mounting, before the stages turn |
| `contact_z_mm` | the contact height **after** the stages turn |
| `contact_point_mm` | where the move ends: `(tool_x, tool_y, contact_z_mm)` |
| `branch_reason` | which key chose this branch among the exact ones |
A pose that aligns perfectly and turns the sensor over is a different thing from
one that aligns perfectly and does not, and the residual alone cannot tell them
apart. Likewise a pose needing more travel than the stage has: `in_range` says so,
and the solver reports the **best pose the limits do allow** with its real
residual, rather than an angle the machine cannot reach or a residual it cannot
achieve.
`upright` used to be measured against the platform's Y axis, which reports a body
rolled about the vertical as turned over; it now compares the tool axis with the
same axis after the stages turn. A pose that aligns perfectly and turns the sensor
over is a different thing from one that aligns perfectly and does not, and the
residual alone cannot tell them apart — but the up test can only do that if it uses
the axis the machine actually calls up.
## History: the sign-blind residual
Three positions belong to one solve and they are not interchangeable: where the
point sits on the mounted sensor, where the stages leave it, and where the tool has
to be. `mounted_point_mm` answers the first, `contact_z_mm`/`contact_point_mm` the
second and third. The contact height is the **rotated** one: mounting a plate or
offsetting the mount shifts the presented point, so the height after the stages
turn is what the press axis has to reach.
The previous residual was `acos(turned · tool_direction)`, which is **sign-blind**.
A rotation putting the normal *on* the tool axis and one putting it *opposite*
both give `|cos| = 1`, so both scored `0.0000°`. That is how an upside-down pose
read as perfect, and it is why this document previously distrusted the solver's
"100% within 0.5°".
### From contact height to a press axis command
A geometry height is not a controller coordinate. `motion/pose_move.py::PressAxis`
holds the measured relation (`zero_mm` is the reading where the tool meets the
table's zero plane, `rising` the drive direction) and turns a contact height into
an axis target only when it is marked `calibrated`. An uncalibrated press axis is
**held** — `press_targets` removes it from the move and reports the role as held —
rather than being commanded with a geometric number. The other four axes move
either way.
## History: a residual that measured the wrong aim
The previous residual was `acos(turned · tool_direction)`, where `tool_direction`
carried `approach_sign`. With the sign wrong, the aim itself pointed at `-Z`, so a
rotation putting the normal *on* that aim and one putting it on `+Z` were not
distinguished: the first scored `0.0000°` because it landed exactly where the
flipped aim asked. It is not that `arccos` cannot tell a direction from its
opposite — `acos(-1)` is `180°` — it is that the reference it measured against had
been inverted. An upside-down pose read as perfect, and that is why this document
previously distrusted the solver's "100% within 0.5°".
The angle itself is correct and is still what `residual_deg` reports; what was
missing was `upright`, which now travels with every pose so the distinction cannot
be lost. The lesson is recorded rather than deleted: a scalar that cannot express
the failure mode it is used to detect will not detect it.
missing was an aim the machine could trust plus `upright`, which now travels with
every pose so the distinction cannot be lost. The lesson is recorded rather than
deleted: a scalar that cannot express the failure mode it is used to detect will
not detect it.
## Still open
## The two exact poses of one normal
**22 of 1032 vertices report not-upright** (16 by more than the tie band). These
are the rim of the dome, where the surface normal is 60–90° from the tool axis.
Aligning an edge-on face necessarily rolls the body about that normal, and which
side of zero it lands on is settled by rounding — the most negative uprightness is
`-0.997` for a vertex whose normal is `90.4°` from the tool axis, i.e. a face being
presented side-on rather than turned over.
Every solvable normal has two exact poses, and they are a **180° roll about the
vertical**, not an upright/inverted pair: `R_branch2 = R_z(180°) @ R_branch1`
agrees with the measured branches to `2.2e-16`. Both leave the body up (the same
tool-axis component), both reach the same contact height, and both put the contact
back under the tool — the table XY differs, because the roll moves the point
sideways and the table compensates.
They are not a solver defect: the closed form is exact for all of them
(`residual < 1.3e-06°`). Whether they matter is a coverage question — a contact
made on the rim is the least well-conditioned one on the patch anyway — and it is
worth a look at the bench rather than a change in the maths.
So the choice is a machine convention, and the solver states it instead of hiding
it: among the branches with the best residual (within `1e-6°`), it prefers one that
is in range, then upright, then the smaller total travel `|yaw| + |pitch|`, and
`Pose.branch_reason` reports which of those keys decided (`only`, `alignment`,
`in_range`, `upright`, `smallest`). An equivalent solution never wins on a
floating-point residue.
On the two bench screenshots this picks `yaw +36.42° / pitch −47.69°` on the right
and `yaw −61.32° / pitch +45.64°` on the left; the other exact pair is
`−143.58° / +47.69°` and `+118.68° / −45.64°`. A fixed approach sign on the rim
(which of the two the operator wants there) still needs the platform lead's rule —
it is not something the solver may invent, and it is not derivable from the UV
position. A contact made on the rim is the least well-conditioned one on the patch
anyway, so it is worth a look at the bench rather than a change in the maths.
The polarity question that used to live here is **settled**: see *the face is aimed
at +Z* above.
@@ -212,7 +251,7 @@ normals, points = bundle.vertex_normals(), bundle.vertices_mm
analytical = [stack.solve(p, n, method="analytical") for p, n in zip(points, normals)]
residuals = np.array([pose.residual_deg for pose in analytical])
print(residuals.max()) # ~1.2e-06
print(sum(pose.upright for pose in analytical), "/", len(analytical)) # 1010 / 1032
print(sum(pose.upright for pose in analytical), "/", len(analytical)) # 1032 / 1032
print(all(pose.in_range for pose in analytical)) # True
```
+44 -18
View File
@@ -1,6 +1,6 @@
# Autocal 运动与多点标定修正计划
状态:待实施,2026-10-14。审查基准:`orisys-app` 的 `dc5a6f8`,范围为 `d06501d..dc5a6f8`。本文件是工程师任务清单和后续复查依据,不表示实机问题已经修复。整体范围沿用[阶段一计划](implementation-plan.zh.md),数据含义以[标定数据格式](calibration-data-format.zh.md)为准。
状态:任务 1–4 的软件部分已实施并继续复核(2026-10-14),任务 0 与任务 5 的实机部分待平台负责人参加。工程师证据保留在 `orisys-app/.scratch/autocal-motion-fix/verification.md`,接手后的结果见 `orisys-app/.scratch/autocal-motion-takeover/verification.md`。审查基准:`dc5a6f8`;接手 HEAD:`5339624`。本计划不表示实机错向已修复,整体范围沿用[阶段一计划](implementation-plan.zh.md),字段以[标定数据格式](calibration-data-format.zh.md)为准。
## 已知事实与待确认项
@@ -31,10 +31,10 @@
| 完成 | 任务 | 依赖 | 估计人时 |
|---|---|---|---:|
| [ ] | 0:固定坐标和实机约定 | 无 | 2–3 |
| [ ] | 1:修正姿态判断和选解 | 0 | 4–6 |
| [ ] | 2:统一接触高度与执行目标 | 0、1 | 4–6 |
| [ ] | 3:消除旧姿态执行 | 1、2 | 3–4 |
| [ ] | 4:接通多点记录复核 | 多点字段确认 | 6–8 |
| [x] | 1:修正姿态判断和选解 | 0 | 4–6 |
| [x] | 2:统一接触高度与执行目标 | 0、1 | 4–6 |
| [x] | 3:消除旧姿态执行 | 1、2 | 3–4 |
| [x] | 4:接通多点记录复核 | 多点字段确认 | 6–8 |
| [ ] | 5:端到端验收和交接 | 1–4 | 3–4 |
软件和离线验证预计 22–31 人时,约单人 3–4 个工作日;额外预留 2–4 小时设备联调,不含硬件故障等待。估算需根据任务 0 的测量结果调整。
@@ -49,9 +49,14 @@
修改 `motion/kinematics.py`,补充 `test_kinematics.py` 与 `test_ik_analytical.py`。
- [ ] 朝上参考来自真正的安装后名义朝上方向,在同一坐标系中比较;默认映射下为平台 +Z,而不是平台 +Y。
- [ ] 候选先满足方向、残差容差、行程和实机约定,再决定分支。等价解不因浮点残差的微小差异改变选择。若两解均允许,采用稳定且有说明的选择规则;需要固定接近姿态时,由平台负责人确认规则,不按 UV 左右硬编码符号。
- [ ] 测试名义法向、左右截图、中心/边缘和行程边界。用独立旋转矩阵检查 `R @ n` 指向 +Z,角误差不大于 `1e-5°`,并验证平台朝上向量。截图角度允许因舍入与界面值相差 0.1°。
- [x] 朝上参考来自真正的安装后名义朝上方向,在同一坐标系中比较;默认映射下为平台 +Z,而不是平台 +Y。
- [x] 候选先满足方向、残差容差、行程和实机约定,再决定分支。等价解不因浮点残差的微小差异改变选择。若两解均允许,采用稳定且有说明的选择规则;需要固定接近姿态时,由平台负责人确认规则,不按 UV 左右硬编码符号。
- [x] 测试名义法向、左右截图、中心/边缘和行程边界。用独立旋转矩阵检查 `R @ n` 指向 +Z,角误差不大于 `1e-5°`,并验证平台朝上向量。截图角度允许因舍入与界面值相差 0.1°。
实施:`uprightness` 改用安装后名义朝上(工具轴)在同一坐标系比较;`select_branch` 依次取
`in_range`→`upright`→最小 `|yaw|+|pitch|`,`Pose.branch_reason` 报出依据,等价解不因残差改选;
rim 是否需要固定接近姿态仍属任务 0。证据:`test_kinematics.py` 的独立旋转与选解小节、
`.scratch/autocal-motion-fix/probe_motion_chain.txt`。
在界面显示所选分支及理由,无法满足约定时拒绝执行。不要仅让原测试保持通过;新回归须在审查基准上失败。
@@ -59,9 +64,15 @@
修改 `PlatformStack.solve` 和两个 UI 执行入口,补充 `test_kinematics.py`、`test_pose_move.py`。
- [ ] 分开表示安装后、旋转前的点与最终接触点。XY 平移后,最终点应落在工具 XY;接触高度取旋转后坐标。默认偏移 `(0,0,8)` 下,两截图的旋转后 Z 约为 19.051 和 21.578 mm,不是 13.444 和 13.186 mm。
- [ ] 明确接触高度到压头轴绝对目标的关系,包括机械零位和工具高度。只有该关系确认后才生成运动目标;不要把几何 Z 直接视为已标定的控制器零位。保留接触高度、无接触预定位和加载深度的不同含义。
- [ ] 预览、目标表、确认框和执行计划使用同一求解结果。以非零安装偏移、安装角和工具 XY 测试,独立计算坐标误差应小于 `1e-6 mm`;实机误差阈值另由设备负责人确定。
- [x] 分开表示安装后、旋转前的点与最终接触点。XY 平移后,最终点应落在工具 XY;接触高度取旋转后坐标。默认偏移 `(0,0,8)` 下,两截图的旋转后 Z 约为 19.051 和 21.578 mm,不是 13.444 和 13.186 mm。
- [x] 明确接触高度到压头轴绝对目标的关系,包括机械零位和工具高度。只有该关系确认后才生成运动目标;不要把几何 Z 直接视为已标定的控制器零位。保留接触高度、无接触预定位和加载深度的不同含义。
- [x] 预览、目标表、确认框和执行计划使用同一求解结果。以非零安装偏移、安装角和工具 XY 测试,独立计算坐标误差应小于 `1e-6 mm`;实机误差阈值另由设备负责人确定。
实施:`Pose` 增加 `mounted_point_mm`/`contact_z_mm`/`contact_point_mm`/`table_xy_mm`;
`motion/pose_move.py::PressAxis`(`zero_mm`、`rising`、`calibrated`)把几何高度换算为压轴目标,
未标定时 `press_targets` 把该轴从动作中移除并报为保持;设备设置新增压轴页。证据:
`test_pose_move.py`、`test_device_setup.py`、`probe_motion_chain.txt`(两截图点 19.0510/21.5781)。
配置中的校准标志必须为布尔 `true`,零位必须有限,方向必须为布尔值;无效配置保持 Z。
更新[逆解说明](inverse-kinematics.md)中的旋转、高度和行程描述;修正其中把 `arccos` 称为正负方向不可区分的说法。不要把单纯改变多轴下发顺序当作修正机械旋转链。
@@ -69,9 +80,14 @@
修改 `ui/multipoint_panel.py` 和必要的 `SiteSpec` 状态,新增对应 UI 回归测试。
- [ ] 每次对准前按当前几何和安装设置重新求解,而不是只看缓存的 `in_range`。优先直接复用现有快速求解器,暂不建立第二套缓存失效机制。
- [ ] 几何版本变化时不能拿旧 UV 映射直接执行;旧 XYZ、法向和当前映射不一致时要求重新确认测点。新的目标须重新经过现有 `MotionService` 行程与连接检查。
- [ ] 修改安装 X/Y/Z、安装角和工具位置后,确认框及实际提交的目标都更新;无解、越界或非有限值时不发送命令。记录当前求解参数及目标,保持期间不能对准其他点。
- [x] 每次对准前按当前几何和安装设置重新求解,而不是只看缓存的 `in_range`。优先直接复用现有快速求解器,暂不建立第二套缓存失效机制。
- [x] 几何版本变化时不能拿旧 UV 映射直接执行;旧 XYZ、法向和当前映射不一致时要求重新确认测点。新的目标须重新经过现有 `MotionService` 行程与连接检查。
- [x] 修改安装 X/Y/Z、安装角和工具位置后,确认框及实际提交的目标都更新;无解、越界或非有限值时不发送命令。记录当前求解参数及目标,保持期间不能对准其他点。
实施:`ui/multipoint_panel.py::_align` 每次都重新求解并重新写入缓存,测点或法向偏移超过
`0.05 mm`/`1e-3` 时报 `multipoint.point_moved`/`normal_moved` 要求重取,无可用来源报
`point_unmapped`。加载或保持期间拒绝对准;repeat 关闭后的换点阶段只允许对准待采测点。
日志位置来自轮询缓存,不为日志在调用线程额外读取控制器。证据:`test_multipoint_align.py`、`test_poller.py`。
数据记录留在 Autocal,纯几何计算留在 kinematics,轴单位和驱动映射留在 motion service。UI 不自行复制矩阵、反转轴符号或导入 Studio 私有模块。
@@ -79,9 +95,19 @@
修改 `session_review.py` 的读取入口及需要的复核 UI;复用既有保持窗口统计、诊断和拟合能力,不另建一套分析器。
- [ ] 将 `autocal-multipoint-1.0` 的实际目录、trial、参考窗口和录像索引转换为既有分析输入。先确认时间基准、力单位、模拟标记及帧来源;缺失字段显式报告,不补成有效零值。
- [ ] 全新进程打开两个测点、每点两个 trial 的会话,显示采集覆盖、有效窗口、加载/卸载曲线和初步法向灵敏度。保持原有 finger 会话读取行为。
- [ ] 未完成 trial、模拟数据、缺失视频或无效参考不得产生正式标定结论。分析结果写入 derived,原始文件哈希不变。增加完整、缺失、中止及原格式的回归样本。
- [x] 将 `autocal-multipoint-1.0` 的实际目录、trial、参考窗口和录像索引转换为既有分析输入。先确认时间基准、力单位、模拟标记及帧来源;缺失字段显式报告,不补成有效零值。
- [x] 全新进程打开两个测点、每点两个 trial 的会话,显示采集覆盖、有效窗口、加载/卸载曲线和初步法向灵敏度。保持原有 finger 会话读取行为。
- [x] 未完成 trial、模拟数据、缺失视频或无效参考不得产生正式标定结论。分析结果写入 derived,原始文件哈希不变。增加完整、缺失、中止及原格式的回归样本。
实施:`review_session()` 按 `format_version` 分流到 `review_multipoint()`,复用窗口统计、拟合与
留一 trial 验证;缺 `capture/`、时间基准非 `host_camera`、repeat 未完成、窗口非法对应
`no_frame_source`/`time_basis_not_comparable`/`incomplete_trial`/`invalid reference window`,
`formal_conclusion` 恒为 false,结果只写 `derived/review_<时间>_<标识>/review.json`。格式与复核
输入契约见[多点标定会话格式](multipoint-data-format.zh.md)。证据:`test_session_review_multipoint.py`、
`.scratch/autocal-motion-fix/multipoint-review.png`、`multipoint-review-response.png`、
`multipoint-overview.png`。
复核读取实际的 `{u,v}`,时间曲线从 `capture/` 取信号;缺少对应视频、模拟或未完成 repeat
不进入拟合。已保存诊断原始信号仍可查看,原始记录不修改。
当前拟合仍为诊断结果。本任务不自动写回 SDK 标定系数,也不引入新采集格式;确有字段歧义时先更新数据契约并说明旧记录的解释限制。
@@ -105,7 +131,7 @@
```powershell
cd D:\orisys_ws\orisys-app
& .venv-build/Scripts/python.exe -m pytest apps/autocal/tests/test_kinematics.py apps/autocal/tests/test_ik_analytical.py apps/autocal/tests/test_pose_move.py apps/autocal/tests/test_multipoint.py apps/autocal/tests/test_session_review.py apps/autocal/tests/test_record_overview.py -p no:cacheprovider --basetemp=.scratch/autocal-motion-fix/pytest-01
& .venv-build/Scripts/python.exe -m pytest apps/autocal/tests/test_kinematics.py apps/autocal/tests/test_ik_analytical.py apps/autocal/tests/test_pose_move.py apps/autocal/tests/test_multipoint.py apps/autocal/tests/test_multipoint_align.py apps/autocal/tests/test_session_review.py apps/autocal/tests/test_session_review_multipoint.py apps/autocal/tests/test_record_overview.py apps/autocal/tests/test_device_setup.py apps/autocal/tests/test_finger_panel.py -p no:cacheprovider --basetemp=.scratch/autocal-motion-fix/pytest-01
& .venv-build/Scripts/python.exe tools/lint_gate.py
```
@@ -0,0 +1,61 @@
# Autocal 多点标定会话格式(`autocal-multipoint-1.0`)
状态:2026-10-14 记录格式与只读复核已接通,仅软件验证,待实机验收。坐标与载荷约定、finger
会话格式见[标定操作与数据格式](calibration-data-format.zh.md);姿态与接触高度见
[逆运动学说明](inverse-kinematics.md);运动边界与停机见[操作流程](workflow.md)。本页不会自动移动平台。
## 目录布局
多点标定按测点组织,不按 trial 平铺:每个测点重复若干次,每次一个 repeat 目录。
```text
sites/<site_id>/repeat-00N/
session.json
reference.csv
events.csv
capture/ (接了相机时才有)
session.json
frames.csv
forces.csv
raw_0001.mp4
```
| 文件 | 内容 |
|---|---|
| 根 `session.json` | `format_version=autocal-multipoint-1.0`、session_id、型号/SN、`coordinate_frame`、`force_to_tool_rotation`、`normal_projection`、力范围、`sync`、sites、trials、`completion`、`status`/`reason`;`platform_motion` 与 `automatic_force_control` 均为 false |
| site 记录 | `site_id`、`position`/`position_frame`、`normal_sensor`、`pose`、`uv_px`、`levels_n`、`hold_s`、`repeats`;`pose` 为空或 `in_range=false` 的点只保存、不下发 |
| repeat `session.json` | `trial_id`、site、`pose`、`uv_px`、`steps[{step_index, branch, target_load_n}]`、`sync`、`images`、`status` |
| `reference.csv` | 每个实际开始的保持窗口一行:`step_index`、`branch`、`target_load_n`、`t_start_host_mono_ns`、`t_end_host_mono_ns`、`F_ref_normal_N`、site 坐标与坐标系、`repeat_index`/`repeat_count`、`hold_complete`、`reference_source`、`fx_N`/`fy_N`/`fz_N`、力到工具的旋转、`nx`/`ny`/`nz`、`pose_*`、`normal_aligned_to` |
| `events.csv` | 电脑单调时间、UTC、事件类型与明细 |
| `capture/` | 共享记录器的录像会话(`session.json`、`frames.csv`、`forces.csv`、`raw_*.mp4`);模拟运行或未接相机时不产生,原因记在该 repeat 的 `images.reason` |
## 取值约定
| 项目 | 约定 |
|---|---|
| branch | 零载窗口写 `baseline`,其余为 `load`/`unload`,位次由 `load_steps()` 决定;没有 `stability_verified` 列,该标志只由复核时选中的子区间产生 |
| 参考力 | 称重模式是操作员输入的法向力;六轴模式是 `F_ref_normal_N = (R @ F) · tool_axis`,`R` 为本次会话输入力到工具的旋转。两者都是人工声明的参考,不是独立测量,也不能替代 FT |
| 时间关联 | 帧属于某个保持窗口,当且仅当它的 `t_host_mono_ns` 落在 `[t_start_host_mono_ns, t_end_host_mono_ns)`;不按行号配对 |
| 姿态 | `pose` 是求解结果快照(含 `in_range`、`upright`、`residual_deg`、`branch_reason`);执行前按当前安装设置重新求解,记录值只作对照 |
| 目录名 | 会话目录带型号、SN、时间与唯一标识;`repeat-00N` 编号同点重复与中止重试 |
## 只读复核输入
`session_review.review_session()` 按根清单的 `format_version` 分流,多点会话走
`review_multipoint()`:读取每个 repeat 的声明窗口,并把 `capture/forces.csv` 的 `FN_raw` 按
`frame_id` 与 `t_host_mono_ns` 关联成实测样本后,复用与 finger 会话相同的窗口统计、线性拟合与
留一 trial 验证。
| 情况 | 复核行为 |
|---|---|
| 缺 `capture/`(模拟运行、未接相机) | 记 `no_frame_source`,窗口标同名状态,样本数为 0、均值为空,不生成拟合 |
| 对应视频分段缺失、为空,或没有保存到录像的帧 | 记 `no_frame_source` 并说明原因;不生成拟合 |
| 模拟会话即使存在帧文件 | 标 `simulation`,仅供诊断,不参与拟合 |
| 帧时间基准不是 `host_camera` | 记 `time_basis_not_comparable`,该 repeat 的窗口不参与计算 |
| repeat 未完成 | 记 `incomplete_trial`,只展示采集与缺失文件,不参与拟合 |
| 窗口起止非法(终点不晚于起点、非有限值、参考力超出会话声明的最大力) | 抛 `invalid reference window`,不静默跳过 |
| 人工选中的子区间 | 必须落在记录窗口内,`stability_verified` 由选中决定;窗口外选择被拒绝 |
多点复核的 `formal_conclusion` 恒为 `false`:参考力是人工声明,帧来源可能缺失,格式里也没有独立
验证。结果只写 `derived/review_<时间>_<标识>/review.json`,原始文件哈希不变;未接入 FT/ECAT
前不写回 SDK 系数,也不引入第二种采集格式。
+9 -9
View File
@@ -237,15 +237,15 @@ measured time per point.
guard violation.
**The single-point dry run is built**, in the finger debug screen: 执行动作 sends the
selected point's *solved pose* to the platform — yaw, then pitch, then table X/Y — and
does **not** command the press axis, so it cannot reach the sensor. The order is the
model's own (`R_pitch · R_yaw`), so every intermediate pose is one the operator
watched in the preview. Before it moves it checks the mapping, the target against the
P0 limits (millimetres for x/y/z, degrees for yaw/pitch) and the rotary calibration —
all in `motion/service.py`, so a later caller cannot walk around them — and the drives
are commanded one at a time, each waiting for the previous to arrive, with a stall or
an alarm aborting the rest. 停止 next to it is the same command as the strip's
全部停止; 回初始位 in the rail sends every axis to zero with the press axis first.
selected point's *solved pose* to the platform — the stages together with the table
X/Y, and the press axis only once its zero is calibrated (设备设置 → 压轴); until then
it is held, so the move cannot reach the sensor by accident. The order follows the
model's own composition (`R_pitch · R_yaw`), so every intermediate pose is one the
operator watched in the preview. Before it moves it checks the mapping, the target
against the P0 limits (millimetres for x/y/z, degrees for yaw/pitch) and the rotary
calibration — all in `motion/service.py`, so a later caller cannot walk around them.
停止 next to it is the same command as the strip's 全部停止; 回初始位 in the rail sends
every axis to zero with the press axis first.
---