66 lines
3.4 KiB
Python
66 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Record deterministic axis/joint, duplicate-axis, home and limit semantics."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
|
|
|
|
def sha256(path: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as handle:
|
|
for block in iter(lambda: handle.read(1024 * 1024), b""):
|
|
digest.update(block)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def generate(root: Path, output: Path) -> dict[str, object]:
|
|
root = root.resolve()
|
|
header = root / "wasm-core/include/sim_kinematics.h"
|
|
source = root / "wasm-core/src/sim_kinematics.c"
|
|
report = {
|
|
"schema": "kinematics-mapping-contract.v1",
|
|
"context_id": json.loads((root / "context-lock.json").read_text(encoding="utf-8")).get("context_id"),
|
|
"inputs": {"header": header.as_posix(), "header_sha256": sha256(header), "source": source.as_posix(), "source_sha256": sha256(source)},
|
|
"axis_order": {"X": 0, "Y": 1, "Z": 2, "A": 3, "B": 4, "C": 5, "U": 6, "V": 7, "W": 8},
|
|
"semantics": {
|
|
"coordinate_case": "ASCII case-fold to uppercase",
|
|
"duplicate_axis": "first occurrence owns axis; later duplicates copy the first joint during inverse",
|
|
"extra_joints": "coordinates may repeat and joint_count remains authoritative",
|
|
"forward": "joint-to-pose uses mapped first joint for each axis",
|
|
"inverse": "pose-to-joint initializes all joints then fills duplicates deterministically",
|
|
"home": "zeroes all configured joints, then computes forward pose",
|
|
"limits": "enabled inclusive min/max; violation returns SIM_KIN_ERR_RANGE",
|
|
"state_hash": "includes config bytes, configured joint prefix and pose bytes",
|
|
},
|
|
"sample_mappings": [
|
|
{"coordinates": "XYZ", "axis_joint": {"X": 0, "Y": 1, "Z": 2}},
|
|
{"coordinates": "XYYZ", "axis_joint": {"X": 0, "Y": 1, "Z": 3}, "duplicate": {"axis": "Y", "owner": 1, "copies": [2]}},
|
|
{"coordinates": "xyyz", "axis_joint": {"X": 0, "Y": 1, "Z": 3}, "case_fold": True},
|
|
{"coordinates": "XYZABCUVW", "axis_joint": {"X": 0, "Y": 1, "Z": 2, "A": 3, "B": 4, "C": 5, "U": 6, "V": 7, "W": 8}},
|
|
],
|
|
"error_cases": ["null_config", "null_pose", "nonfinite_joint", "nonfinite_pose", "joint_count_mismatch", "enabled_limit_violation", "invalid_coordinate", "inverse_only_forward", "forward_only_inverse"],
|
|
"status": "verified",
|
|
}
|
|
report["contract_sha256"] = hashlib.sha256(json.dumps(report, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
output.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
|
|
return report
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
|
|
parser.add_argument("--output", type=Path, default=Path("coverage/kinematics-mapping-contract.json"))
|
|
args = parser.parse_args()
|
|
report = generate(args.repo_root, args.repo_root / args.output)
|
|
print(f"kinematics_mapping={report['status']} samples={len(report['sample_mappings'])} errors={len(report['error_cases'])}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|