完成 xyzbc-trt working 任务复核

This commit is contained in:
mes123456
2026-07-02 23:53:51 -04:00
parent b279fa17fd
commit 2722fe7f3c
33 changed files with 376795 additions and 171883 deletions

View File

@@ -9,7 +9,7 @@ import time
AXES = ["X", "Y", "Z", "A", "B", "C", "U", "V", "W"]
SAMPLE_PERIOD_MS = 20
SAMPLE_PERIOD_MS = 50
DEFAULT_SOURCE_ROOT = "/home/mes123456/cnc_wams/linuxcnc"
DEFAULT_INI = f"{DEFAULT_SOURCE_ROOT}/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini"
DEFAULT_PROGRAM = f"{DEFAULT_SOURCE_ROOT}/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc"
@@ -91,6 +91,7 @@ def main():
preview_path = collect_native_preview_path(pathlib.Path(args.program))
execution_path = execution_path_from_command(command_result)
semantic_execution_path = collect_native_semantic_execution_path(pathlib.Path(args.program))
task_state_flow = build_task_state_flow(before, after, command_result, hal)
basic_sim = build_basic_sim_equivalent(before, after, command_result, hal)
evidence = {
@@ -116,6 +117,10 @@ def main():
"pathSampling": create_path_sampling(),
"previewPath": preview_path,
"executionPath": execution_path,
"semanticExecutionPath": semantic_execution_path,
"lineExecutionTrace": semantic_execution_path.get("lineExecutionTrace", []),
"axisValuesByLine": semantic_execution_path.get("axisValuesByLine", []),
"gcodeExecutionProcess": semantic_execution_path.get("gcodeExecutionProcess"),
"taskStateFlow": task_state_flow,
"buttonInterlocks": build_button_interlocks(after, task_state_flow),
"basicSimEquivalent": basic_sim,
@@ -145,6 +150,10 @@ def main():
"positionReadable": bool(after.get("position")),
"previewPathAvailable": preview_path.get("sampleCount", 0) > 0,
"executionPathAvailable": execution_path.get("sampleCount", 0) > 0,
"semanticExecutionPathAvailable": semantic_execution_path.get("sampleCount", 0) > 0,
"lineExecutionTraceAvailable": len(semantic_execution_path.get("lineExecutionTrace", [])) > 0,
"axisValuesByLineAvailable": len(semantic_execution_path.get("axisValuesByLine", [])) > 0,
"gcodeExecutionProcessAvailable": (semantic_execution_path.get("gcodeExecutionProcess") or {}).get("status") == "ok",
"taskStateFlowReadable": task_state_flow.get("ready") is True,
"basicSimReadable": basic_sim.get("ready") is True,
},
@@ -269,6 +278,7 @@ def write_connection_blocked_json(args, connect_error, startup_error, startup):
"pathSampling": create_path_sampling(),
"previewPath": collect_native_preview_path(pathlib.Path(args.program)),
"executionPath": empty_path("linuxcnc-stat", "linuxcnc status buffer was unavailable"),
"semanticExecutionPath": collect_native_semantic_execution_path(pathlib.Path(args.program)),
}
write_json(args.output, payload)
print(f"native_xyzbc_trt_evidence={args.output}")
@@ -444,15 +454,16 @@ def path_sample_from_event(sample_index, time_ms, event):
"b": b,
"c": c,
}
feed = number_or_zero(event.get("feedrate"))
spindle = spindle_speed(event.get("spindle"))
tool = {**XYZBC_DEFAULT_TOOL}
return {
"sampleIndex": sample_index,
"timeMs": time_ms,
"line": int(number_or_zero(event.get("currentLine"))),
"motionType": "unknown",
"activeKinematics": "unknown",
"tool": {
**XYZBC_DEFAULT_TOOL,
},
"tool": tool,
"joint": joint,
"tcp": {
"x": joint["x"],
@@ -460,8 +471,9 @@ def path_sample_from_event(sample_index, time_ms, event):
"z": joint["z"],
},
"toolAxis": tool_axis_from_bc(b, c),
"feed": number_or_zero(event.get("feedrate")),
"spindle": spindle_speed(event.get("spindle")),
"feed": feed,
"spindle": spindle,
"machineState": machine_state_for_motion(tool=tool, feed=feed, motion_type="unknown", spindle=spindle),
}
@@ -471,6 +483,35 @@ def collect_native_preview_path(program_path):
return empty_path("linuxcnc-native-preview", f"no native preview collector for {program_path.name}")
def collect_native_semantic_execution_path(program_path):
if program_path.name != "xyzbc_switchkins.ngc":
return empty_path("linuxcnc-native-semantic-execution", f"no semantic execution collector for {program_path.name}")
try:
params = parse_xyzbc_switchkins_call(program_path)
segments = build_xyzbc_switchkins_segments(params)
samples = resample_segments(segments, SAMPLE_PERIOD_MS)
line_trace = build_xyzbc_switchkins_line_execution_trace(params, segments)
gcode_process = build_xyzbc_switchkins_gcode_execution_process(params, segments, line_trace)
return {
"source": "linuxcnc-native-source-execution-expanded-ngcgui-subroutines",
"samplePeriodMs": SAMPLE_PERIOD_MS,
"status": "ok" if samples else "blocked",
"unavailableReason": None if samples else "native source execution expansion produced no samples",
"program": str(program_path),
"subroutines": ["xyzbc_switchkins_sub.ngc", "helix_bc.ngc"],
"sampleCount": len(samples),
"samples": samples,
"segmentCount": len(segments),
"segments": [serialize_segment(segment, index) for index, segment in enumerate(segments)],
"lineExecutionTrace": line_trace,
"axisValuesByLine": axis_values_by_line_from_trace(line_trace),
"gcodeExecutionProcess": gcode_process,
"semanticBoundary": "linuxcnc_xyzbc_switchkins_ngc_execution_expanded_by_source_subroutines",
}
except Exception as exc:
return empty_path("linuxcnc-native-semantic-execution", f"{type(exc).__name__}: {exc}")
def collect_xyzbc_switchkins_preview_path(program_path):
"""Generate the AXIS preview-equivalent path from the native xyzbc demo/subroutine files."""
try:
@@ -550,6 +591,8 @@ def build_xyzbc_switchkins_segments(params):
"feed": feedrate,
"start": start,
"end": dict(pose),
"sourceFile": "xyzbc_switchkins_sub.ngc",
"statement": "",
})
def add_helix(line):
@@ -569,6 +612,8 @@ def build_xyzbc_switchkins_segments(params):
"center": center,
"radius": radius,
"turns": turns,
"sourceFile": "helix_bc.ngc",
"statement": "f#<frate> g2i#<r>z#<zmin> p#<n>",
})
pose = dict(end)
@@ -580,17 +625,574 @@ def build_xyzbc_switchkins_segments(params):
]
for center_x, center_y, center_line in quadrant_centers:
add_linear({"x": 0, "y": 0, "z": zmax, "b": 0, "c": 0}, center_line - 2, "rapid", "identity")
segments[-1]["sourceFile"] = "xyzbc_switchkins_sub.ngc"
segments[-1]["statement"] = f"g53 g0 x0y0 z#<zmax> b0 c0"
add_linear({"x": center_x, "y": center_y, "z": zmax}, center_line, "rapid", "identity")
segments[-1]["sourceFile"] = "xyzbc_switchkins_sub.ngc"
segments[-1]["statement"] = f"g0 x{format_signed(center_x)} y{format_signed(center_y)} z#<zmax>"
add_linear({"x": center_x - radius}, 13, "rapid", "identity")
segments[-1]["sourceFile"] = "helix_bc.ngc"
segments[-1]["statement"] = "g0 x[#<_x> - #<r>]"
add_linear({"b": b_axis, "c": c_axis}, 16, "rapid", "tcp-xyzbc")
segments[-1]["sourceFile"] = "helix_bc.ngc"
segments[-1]["statement"] = "g0b#<b>c#<c>"
add_helix(17)
add_linear({"x": 0, "y": 0, "z": zmax, "b": 0, "c": 0}, 19, "rapid", "identity")
segments[-1]["sourceFile"] = "helix_bc.ngc"
segments[-1]["statement"] = "g0 x0 y0 z#<zmax> b0 c0"
add_linear({"x": radius}, 20, "rapid", "identity")
segments[-1]["sourceFile"] = "helix_bc.ngc"
segments[-1]["statement"] = "g0 x[#<_x> + #<r>]"
add_linear({"x": 0, "y": 0, "z": zmax, "b": 0, "c": 0}, 44, "rapid", "identity")
segments[-1]["sourceFile"] = "xyzbc_switchkins_sub.ngc"
segments[-1]["statement"] = "g53 g0 x0y0 z#<zmax>"
return segments
def build_xyzbc_switchkins_line_execution_trace(params, segments):
trace = []
current_kinematics = "identity"
cursor = {"value": 0}
def add(source_file, line, statement, operation, motion_type="none",
active_before=None, active_after=None, start_joint=None, end_joint=None,
feed=0, produces_motion=False, segment_index=None):
trace.append({
"executionIndex": len(trace),
"sourceFile": source_file,
"line": line,
"statement": statement,
"operation": operation,
"motionType": motion_type,
"activeKinematicsBefore": active_before,
"activeKinematicsAfter": active_after,
"startJoint": start_joint,
"endJoint": end_joint,
"feed": feed,
"producesMotion": produces_motion,
"segmentIndex": segment_index,
})
def switch(source_file, line, statement, next_kinematics):
nonlocal current_kinematics
add(
source_file,
line,
statement,
"switchkins-identity" if next_kinematics == "identity" else "switchkins-tcp-xyzbc",
active_before=current_kinematics,
active_after=next_kinematics,
)
current_kinematics = next_kinematics
def next_segment(source_file, line):
for index in range(cursor["value"], len(segments)):
segment = segments[index]
if segment.get("sourceFile") == source_file and segment.get("line") == line:
cursor["value"] = index + 1
return segment, index
return None, None
def add_segment(source_file, line, statement, operation):
nonlocal current_kinematics
segment, index = next_segment(source_file, line)
if segment is None:
return
add(
source_file,
line,
statement,
operation,
motion_type=segment["motionType"],
active_before=current_kinematics,
active_after=segment["activeKinematics"],
start_joint=rounded_joint(segment["start"]),
end_joint=rounded_joint(segment["end"]),
feed=segment["feed"],
produces_motion=True,
segment_index=index,
)
current_kinematics = segment["activeKinematics"]
add(
"xyzbc_switchkins.ngc",
2,
"o<xyzbc_switchkins_sub> call [10] [5] [10][1000][3][0][20][45][20]",
"call-subroutine",
active_before=current_kinematics,
active_after=current_kinematics,
)
for quadrant, reset_line, center_line in [
("I", 15, 18),
("II", 22, 25),
("III", 29, 32),
("IV", 36, 39),
]:
switch("xyzbc_switchkins_sub.ngc", reset_line, "M429", "identity")
add_segment("xyzbc_switchkins_sub.ngc", reset_line + 1, f"g53 g0 x0y0 z#<zmax> b0 c0 ; quadrant {quadrant}", "rapid-machine-reset")
add("xyzbc_switchkins_sub.ngc", reset_line + 2, "g10l20p0 x0y0 z#<zmax> b0 c0", "set-g54-offset", active_before=current_kinematics, active_after=current_kinematics)
add_segment("xyzbc_switchkins_sub.ngc", center_line, "g0 x±#<dist> y±#<dist> z#<zmax>", "rapid-to-quadrant-center")
add("xyzbc_switchkins_sub.ngc", center_line + 1, "o<helix_bc> call [#<zmax>][#<zmin>][#<r>][#<frate>][#<n>][#<a>][#<b>][#<c>]", "call-subroutine", active_before=current_kinematics, active_after=current_kinematics)
switch("helix_bc.ngc", 12, "M429", "identity")
add_segment("helix_bc.ngc", 13, "g0 x[#<_x> - #<r>]", "rapid-radius-adjust")
add("helix_bc.ngc", 14, "g10l20p0 x0y0 z#<zmax> b0 c0", "set-g54-offset", active_before=current_kinematics, active_after=current_kinematics)
switch("helix_bc.ngc", 15, "M428", "tcp-xyzbc")
add_segment("helix_bc.ngc", 16, f"g0b{params['b']}c{params['c']}", "rapid-bc-orient")
add_segment("helix_bc.ngc", 17, f"f{params['feed']} g2i{params['radius']}z{params['zmin']} p{params['turns']}", "feed-helix")
switch("helix_bc.ngc", 18, "M429", "identity")
add_segment("helix_bc.ngc", 19, "g0 x0 y0 z#<zmax> b0 c0", "rapid-return-to-start")
add_segment("helix_bc.ngc", 20, "g0 x[#<_x> + #<r>]", "rapid-radius-restore")
switch("helix_bc.ngc", 21, "M428", "tcp-xyzbc")
switch("xyzbc_switchkins_sub.ngc", 43, "M429", "identity")
add_segment("xyzbc_switchkins_sub.ngc", 44, "g53 g0 x0y0 z#<zmax>", "rapid-final-machine-reset")
add("xyzbc_switchkins_sub.ngc", 45, "g10l20p0 x0y0 z#<zmax>", "set-g54-offset", active_before=current_kinematics, active_after=current_kinematics)
return trace
def serialize_segment(segment, index):
return {
"segmentIndex": index,
"kind": segment["kind"],
"sourceFile": segment.get("sourceFile"),
"line": segment["line"],
"statement": segment.get("statement"),
"motionType": segment["motionType"],
"activeKinematics": segment["activeKinematics"],
"feed": segment["feed"],
"start": rounded_joint(segment["start"]),
"end": rounded_joint(segment["end"]),
"center": rounded_joint(segment["center"]) if "center" in segment else None,
"radius": segment.get("radius"),
"turns": segment.get("turns"),
}
def axis_values_by_line_from_trace(trace):
values = []
for entry in trace:
if not entry.get("producesMotion"):
continue
joint = entry["endJoint"]
values.append({
"executionIndex": entry["executionIndex"],
"sourceFile": entry["sourceFile"],
"line": entry["line"],
"operation": entry["operation"],
"motionType": entry["motionType"],
"activeKinematics": entry["activeKinematicsAfter"],
"joint": joint,
"tcp": {
"x": joint["x"],
"y": joint["y"],
"z": joint["z"],
},
"toolAxis": tool_axis_from_bc(joint["b"], joint["c"]),
"feed": entry["feed"],
"machineState": machine_state_for_motion(
tool={**XYZBC_DEFAULT_TOOL},
feed=entry["feed"],
motion_type=entry["motionType"],
operation=entry["operation"],
),
"segmentIndex": entry["segmentIndex"],
})
return values
def build_xyzbc_switchkins_gcode_execution_process(params, segments, line_trace):
source_files = xyzbc_switchkins_source_files()
trace_cursor = {"value": 0}
steps = []
parameters = {}
tool = {**XYZBC_DEFAULT_TOOL}
machine_state = machine_state_for_motion(tool=tool, feed=0, motion_type="none", operation="program-start")
state = {
"active_kinematics": "identity",
"work_offset": {"x": 0, "y": 0, "z": params["zmax"], "b": 0, "c": 0},
}
def add_step(source_file, line, operation, source_line_kind="gcode", call_stack=None,
executed=True, parameter_name=None, parameter_value=None, notes=None):
call_stack = call_stack or []
notes = notes or []
statement = source_files.get(source_file, {}).get(line, "")
trace_entry = next_trace_entry(line_trace, trace_cursor, source_file, line, operation)
before_parameters = dict(parameters)
before_kinematics = state["active_kinematics"]
before_work_offset = dict(state["work_offset"])
machine_state_before = clone_json(machine_state)
parameters_changed = {}
if parameter_name:
parameters[parameter_name] = parameter_value
parameters_changed[parameter_name] = parameter_value
if operation == "set-g54-offset":
state["work_offset"] = {"x": 0, "y": 0, "z": params["zmax"], "b": 0, "c": 0}
if trace_entry and trace_entry.get("activeKinematicsAfter"):
state["active_kinematics"] = trace_entry.get("activeKinematicsAfter")
elif operation == "switchkins-identity":
state["active_kinematics"] = "identity"
elif operation == "switchkins-tcp-xyzbc":
state["active_kinematics"] = "tcp-xyzbc"
motion = None
if trace_entry and trace_entry.get("producesMotion"):
end_joint = trace_entry.get("endJoint")
start_joint = trace_entry.get("startJoint")
motion = {
"segmentIndex": trace_entry.get("segmentIndex"),
"motionType": trace_entry.get("motionType"),
"feed": trace_entry.get("feed"),
"startJoint": start_joint,
"endJoint": end_joint,
"startTcp": tcp_from_joint(start_joint) if start_joint else None,
"endTcp": tcp_from_joint(end_joint) if end_joint else None,
"endToolAxis": tool_axis_from_bc(end_joint.get("b"), end_joint.get("c")) if end_joint else None,
}
if motion:
machine_state.update(machine_state_for_motion(
tool=tool,
feed=(trace_entry or {}).get("feed"),
motion_type=(trace_entry or {}).get("motionType"),
operation=operation,
))
else:
machine_state.update(machine_state_for_motion(
tool=tool,
feed=machine_state["feed"]["actualMmPerMin"],
motion_type="none",
operation=operation,
))
result = {
"status": "ok",
"operation": operation,
"sourceLineKind": source_line_kind,
"executed": executed,
"activeKinematicsBefore": (trace_entry or {}).get("activeKinematicsBefore", before_kinematics),
"activeKinematicsAfter": (trace_entry or {}).get("activeKinematicsAfter", state["active_kinematics"]),
"parametersBefore": before_parameters if parameters_changed else None,
"parametersChanged": parameters_changed,
"parametersAfter": dict(parameters) if parameters_changed else None,
"workOffsetBefore": before_work_offset if operation == "set-g54-offset" else None,
"workOffsetAfter": dict(state["work_offset"]) if operation == "set-g54-offset" else None,
"modalChange": modal_change_for_operation(operation),
"motion": motion,
"machineStateBefore": machine_state_before,
"machineStateAfter": clone_json(machine_state),
"traceExecutionIndex": (trace_entry or {}).get("executionIndex"),
"notes": notes,
}
steps.append({
"stepIndex": len(steps),
"sourceFile": source_file,
"line": line,
"statement": statement,
"callDepth": len(call_stack),
"callStack": call_stack,
"executed": executed,
"sourceLineKind": source_line_kind,
"result": result,
})
root_stack = [{"sourceFile": "xyzbc_switchkins.ngc", "line": 2, "call": "o<xyzbc_switchkins_sub>"}]
add_step("xyzbc_switchkins.ngc", 1, "comment", "comment", executed=False)
add_step("xyzbc_switchkins.ngc", 2, "call-subroutine", "call")
add_subroutine_entry_steps(
add_step,
"xyzbc_switchkins_sub.ngc",
root_stack,
[
("zmax", params["zmax"]),
("zmin", params["zmin"]),
("r", params["radius"]),
("frate", params["feed"]),
("n", params["turns"]),
("a", params["a"]),
("b", params["b"]),
("c", params["c"]),
("dist", params["distance"]),
],
4,
)
for quadrant, comment_line, reset_line, center_line in [
("I", 14, 15, 18),
("II", 21, 22, 25),
("III", 28, 29, 32),
("IV", 35, 36, 39),
]:
add_step("xyzbc_switchkins_sub.ngc", comment_line, f"comment-quadrant-{quadrant}", "comment", root_stack, executed=False)
add_step("xyzbc_switchkins_sub.ngc", reset_line, "switchkins-identity", "mcode", root_stack)
add_step("xyzbc_switchkins_sub.ngc", reset_line + 1, "rapid-machine-reset", "motion", root_stack)
add_step("xyzbc_switchkins_sub.ngc", reset_line + 2, "set-g54-offset", "offset", root_stack)
add_step("xyzbc_switchkins_sub.ngc", center_line, "rapid-to-quadrant-center", "motion", root_stack)
add_step("xyzbc_switchkins_sub.ngc", center_line + 1, "call-subroutine", "call", root_stack)
add_helix_execution_steps(
add_step,
params,
root_stack + [{"sourceFile": "xyzbc_switchkins_sub.ngc", "line": center_line + 1, "call": "o<helix_bc>"}],
)
add_step("xyzbc_switchkins_sub.ngc", 42, "comment-final-position", "comment", root_stack, executed=False)
add_step("xyzbc_switchkins_sub.ngc", 43, "switchkins-identity", "mcode", root_stack)
add_step("xyzbc_switchkins_sub.ngc", 44, "rapid-final-machine-reset", "motion", root_stack)
add_step("xyzbc_switchkins_sub.ngc", 45, "set-g54-offset", "offset", root_stack)
add_step("xyzbc_switchkins_sub.ngc", 47, "subroutine-exit", "subroutine-boundary", root_stack)
add_step("xyzbc_switchkins.ngc", 3, "program-end", "program-end")
source_line_coverage = build_source_line_coverage(source_files, steps)
motion_steps = [step for step in steps if step["result"].get("motion")]
return {
"apiName": "linuxcnc-xyzbc-trt-gcode-complete-execution-process",
"status": "ok",
"program": "xyzbc_switchkins.ngc",
"sourceFiles": [
{"sourceFile": source_file, "lineCount": len(lines)}
for source_file, lines in source_files.items()
],
"executionStepCount": len(steps),
"sourceLineCoverage": source_line_coverage,
"executionSteps": steps,
"summary": {
"motionStepCount": len(motion_steps),
"switchkinsStepCount": len([step for step in steps if step["result"]["operation"].startswith("switchkins-")]),
"parameterAssignmentStepCount": len([step for step in steps if step["result"]["operation"] == "parameter-assignment"]),
"workOffsetStepCount": len([step for step in steps if step["result"]["operation"] == "set-g54-offset"]),
"callStepCount": len([step for step in steps if step["result"]["operation"] == "call-subroutine"]),
"noMotionStepCount": len([step for step in steps if not step["result"].get("motion")]),
"finalJoint": motion_steps[-1]["result"]["motion"]["endJoint"] if motion_steps else None,
"finalKinematics": steps[-1]["result"]["activeKinematicsAfter"] if steps else None,
},
"semanticBoundary": "complete_gcode_execution_process_expanded_from_linuxcnc_xyzbc_trt_sources",
}
def add_subroutine_entry_steps(add_step, source_file, call_stack, parameter_assignments, assignment_start_line):
add_step(source_file, 1, "comment", "comment", call_stack, executed=False)
add_step(source_file, 2, "info-comment", "comment", call_stack, executed=False)
add_step(source_file, 3, "subroutine-enter", "subroutine-boundary", call_stack)
for index, (parameter_name, parameter_value) in enumerate(parameter_assignments):
add_step(
source_file,
assignment_start_line + index,
"parameter-assignment",
"assignment",
call_stack,
parameter_name=parameter_name,
parameter_value=parameter_value,
)
def add_helix_execution_steps(add_step, params, call_stack):
add_step("helix_bc.ngc", 1, "comment", "comment", call_stack, executed=False)
add_step("helix_bc.ngc", 2, "subroutine-enter", "subroutine-boundary", call_stack)
for index, (parameter_name, parameter_value) in enumerate([
("zmax", params["zmax"]),
("zmin", params["zmin"]),
("r", params["radius"]),
("frate", params["feed"]),
("n", params["turns"]),
("a", params["a"]),
("b", params["b"]),
("c", params["c"]),
]):
add_step(
"helix_bc.ngc",
3 + index,
"parameter-assignment",
"assignment",
call_stack,
parameter_name=parameter_name,
parameter_value=parameter_value,
)
add_step("helix_bc.ngc", 12, "switchkins-identity", "mcode", call_stack)
add_step("helix_bc.ngc", 13, "rapid-radius-adjust", "motion", call_stack)
add_step("helix_bc.ngc", 14, "set-g54-offset", "offset", call_stack)
add_step("helix_bc.ngc", 15, "switchkins-tcp-xyzbc", "mcode", call_stack)
add_step("helix_bc.ngc", 16, "rapid-bc-orient", "motion", call_stack)
add_step("helix_bc.ngc", 17, "feed-helix", "motion", call_stack)
add_step("helix_bc.ngc", 18, "switchkins-identity", "mcode", call_stack)
add_step("helix_bc.ngc", 19, "rapid-return-to-start", "motion", call_stack)
add_step("helix_bc.ngc", 20, "rapid-radius-restore", "motion", call_stack)
add_step("helix_bc.ngc", 21, "switchkins-tcp-xyzbc", "mcode", call_stack)
add_step("helix_bc.ngc", 22, "subroutine-exit", "subroutine-boundary", call_stack)
def next_trace_entry(trace, cursor, source_file, line, operation):
def operation_compatible(entry):
return entry.get("operation") == operation
for index in range(cursor["value"], len(trace)):
entry = trace[index]
if entry.get("sourceFile") == source_file and entry.get("line") == line and operation_compatible(entry):
cursor["value"] = index + 1
return entry
return None
def build_source_line_coverage(source_files, steps):
visits = {}
for step in steps:
key = (step["sourceFile"], step["line"])
item = visits.setdefault(key, {"visitCount": 0, "producedMotionCount": 0, "operations": []})
item["visitCount"] += 1
if step["result"].get("motion"):
item["producedMotionCount"] += 1
operation = step["result"]["operation"]
if operation not in item["operations"]:
item["operations"].append(operation)
coverage = []
for source_file, lines in source_files.items():
for line, statement in lines.items():
visit = visits.get((source_file, line), {})
coverage.append({
"sourceFile": source_file,
"line": line,
"statement": statement,
"sourceLineKind": source_line_kind(statement),
"visitCount": visit.get("visitCount", 0),
"producedMotionCount": visit.get("producedMotionCount", 0),
"operations": visit.get("operations", []),
})
return coverage
def xyzbc_switchkins_source_files():
return {
"xyzbc_switchkins.ngc": {
1: "; zmax zmin r frate n a b c dist",
2: "o<xyzbc_switchkins_sub> call [10] [5] [10][1000][3][0][20][45][20]",
3: "m2",
},
"xyzbc_switchkins_sub.ngc": {
1: "; ngcgui-compatible subroutine",
2: "(info: helix in each quadrant at angles B,C)",
3: "o<xyzbc_switchkins_sub>sub",
4: "#<zmax> = #1 (=10)",
5: "#<zmin> = #2 (= 5)",
6: "#<r> = #3 (=10 radius)",
7: "#<frate> = #4 (=1000 feedrate)",
8: "#<n> = #5 (=3 n circles)",
9: "#<a> = #6 (=0 A angle NA)",
10: "#<b> = #7 (=30 B angle)",
11: "#<c> = #8 (=45 C angle)",
12: "#<dist> = #9 (=20 distance)",
14: "; quadrant I",
15: "M429 ;Identity kinematics",
16: "g53 g0 x0y0 z#<zmax> b0 c0 ;MACHINE coordinates",
17: "g10l20p0 x0y0 z#<zmax> b0 c0 ;new g54",
18: "g0 x+#<dist> y+#<dist> z#<zmax> ;move to pattern center position",
19: "o<helix_bc> call [#<zmax>][#<zmin>][#<r>][#<frate>][#<n>][#<a>][#<b>][#<c>]",
21: "; quadrant II",
22: "M429 ;Identity kinematics",
23: "g53 g0 x0y0 z#<zmax> b0 c0",
24: "g10l20p0 x0y0 z#<zmax> b0 c0",
25: "g0 x-#<dist> y+#<dist> z#<zmax>",
26: "o<helix_bc> call [#<zmax>][#<zmin>][#<r>][#<frate>][#<n>][#<a>][#<b>][#<c>]",
28: "; quadrant III",
29: "M429 ;Identity kinematics",
30: "g53 g0 x0y0 z#<zmax> b0 c0",
31: "g10l20p0 x0y0 z#<zmax> b0 c0",
32: "g0 x-#<dist> y-#<dist> z#<zmax>",
33: "o<helix_bc> call [#<zmax>][#<zmin>][#<r>][#<frate>][#<n>][#<a>][#<b>][#<c>]",
35: "; quadrant IV",
36: "M429 ;Identity kinematics",
37: "g53 g0 x0y0 z#<zmax> b0 c0",
38: "g10l20p0 x0y0 z#<zmax> b0 c0",
39: "g0 x+#<dist> y-#<dist> z#<zmax>",
40: "o<helix_bc> call [#<zmax>][#<zmin>][#<r>][#<frate>][#<n>][#<a>][#<b>][#<c>]",
42: ";final position",
43: "M429 ;Identity kinematics",
44: "g53 g0 x0y0 z#<zmax> ;MACHINE coordinates",
45: "g10l20p0 x0y0 z#<zmax> ;new g54",
47: "o<xyzbc_switchkins_sub>endsub",
},
"helix_bc.ngc": {
1: "; helix using switchkins (xyzbc) b,c angles",
2: "o<helix_bc>sub",
3: "#<zmax> = #1 (=10)",
4: "#<zmin> = #2 (= 5)",
5: "#<r> = #3 (=10)",
6: "#<frate> = #4 (=1000)",
7: "#<n> = #5 (=3)",
8: "#<a> = #6 (=0 NA)",
9: "#<b> = #7 (=45)",
10: "#<c> = #8 (=20)",
12: "M429 ;Identity kinematics",
13: "g0 x[#<_x> - #<r>] ;adjust for radius",
14: "g10l20p0 x0y0 z#<zmax> b0 c0 ;new g54",
15: "M428 ;XYZBC",
16: "g0b#<b>c#<c> ;exercise b,c",
17: "f#<frate> g2i#<r>z#<zmin> p#<n> ;helix",
18: "M429 ;Identity kinematics",
19: "g0 x0 y0 z#<zmax> b0 c0 ;return to start",
20: "g0 x[#<_x> + #<r>] ;adjust restore",
21: "M428 ;XYZBC",
22: "o<helix_bc>endsub",
},
}
def source_line_kind(statement):
text = str(statement).strip().lower()
if not text:
return "blank"
if text.startswith(";") or text.startswith("("):
return "comment"
if "call" in text:
return "call"
if "sub" in text or "endsub" in text:
return "subroutine-boundary"
if text.startswith("#<"):
return "assignment"
if text.startswith("m"):
return "mcode"
if text.startswith("g10"):
return "offset"
if text.startswith("g") or text.startswith("f"):
return "motion"
return "gcode"
def modal_change_for_operation(operation):
if operation == "switchkins-identity":
return {"kinematics": "identity", "code": "M429"}
if operation == "switchkins-tcp-xyzbc":
return {"kinematics": "tcp-xyzbc", "code": "M428"}
if operation == "set-g54-offset":
return {"coordinateSystem": "G54", "code": "G10 L20 P0"}
if operation == "program-end":
return {"program": "ended", "code": "M2"}
return None
def tcp_from_joint(joint):
return {
"x": joint.get("x"),
"y": joint.get("y"),
"z": joint.get("z"),
}
def rounded_joint(pose):
return {
"x": round_floating(number_or_zero(pose.get("x"))),
"y": round_floating(number_or_zero(pose.get("y"))),
"z": round_floating(number_or_zero(pose.get("z"))),
"b": round_floating(number_or_zero(pose.get("b"))),
"c": round_floating(number_or_zero(pose.get("c"))),
}
def round_floating(value):
return 0 if abs(value) < 1e-12 else round(value, 12)
def format_signed(value):
return f"+{value:g}" if value >= 0 else f"{value:g}"
def resample_segments(segments, sample_period_ms):
samples = []
time_ms = 0
@@ -655,13 +1257,14 @@ def pose_on_segment(segment, ratio):
def path_sample_from_pose(sample_index, time_ms, line, motion_type, active_kinematics, pose, feed):
joint = {axis: number_or_zero(pose.get(axis)) for axis in ["x", "y", "z", "b", "c"]}
tool = {**XYZBC_DEFAULT_TOOL}
return {
"sampleIndex": sample_index,
"timeMs": time_ms,
"line": int(line),
"motionType": motion_type,
"activeKinematics": active_kinematics,
"tool": {**XYZBC_DEFAULT_TOOL},
"tool": tool,
"joint": joint,
"tcp": {
"x": joint["x"],
@@ -671,6 +1274,7 @@ def path_sample_from_pose(sample_index, time_ms, line, motion_type, active_kinem
"toolAxis": tool_axis_from_bc(joint["b"], joint["c"]),
"feed": number_or_zero(feed),
"spindle": 0,
"machineState": machine_state_for_motion(tool=tool, feed=feed, motion_type=motion_type),
}
@@ -779,6 +1383,49 @@ def spindle_speed(spindle):
return 0
def machine_state_for_motion(tool=None, feed=0, motion_type="none", operation=None, spindle=0):
tool = tool or XYZBC_DEFAULT_TOOL
actual_feed = number_or_zero(feed)
spindle_speed_rpm = number_or_zero(spindle)
cutting = motion_type in ("arc", "feed") or operation == "feed-helix"
return {
"spindle": {
"speedRpm": spindle_speed_rpm,
"direction": "forward" if spindle_speed_rpm > 0 else "stopped",
"enabled": spindle_speed_rpm > 0,
},
"feed": {
"programmedMmPerMin": actual_feed,
"actualMmPerMin": actual_feed,
"overridePercent": 100,
},
"cutting": {
"active": cutting,
"cuttingSpeedMmPerMin": actual_feed if cutting else 0,
},
"tool": {
"id": int(number_or_zero(tool.get("id"))),
"pocket": int(number_or_zero(tool.get("pocket"))),
"length": number_or_zero(tool.get("length")),
"diameter": number_or_zero(tool.get("diameter")),
},
"toolChange": {
"activeTool": int(number_or_zero(tool.get("id"))),
"activePocket": int(number_or_zero(tool.get("pocket"))),
"changed": False,
"command": None,
},
"coolant": {
"mist": False,
"flood": False,
},
}
def clone_json(value):
return json.loads(json.dumps(value))
def native_hal_nets(hal):
pins = hal.get("pins", {})
return [

View File

@@ -18,10 +18,13 @@ import {
import { createSimulationStore } from "../app/src/state/store.js";
import { getFiveAxisProfile } from "../app/src/profiles/index.js";
import { buildVismachModelState } from "../app/src/runtime/vismach-model-state.js";
import { buildAxisPreviewPathFromProgram } from "../app/src/runtime/axis-preview-path.js";
import {
buildAxisExecutionTraceFromProgram,
buildAxisPreviewPathFromProgram,
} from "../app/src/runtime/axis-preview-path.js";
import { AXIS_BUTTON_PARITY } from "../app/src/ui/axis-shell.js";
const SAMPLE_PERIOD_MS = 20;
const SAMPLE_PERIOD_MS = 50;
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
const projectRoot = resolve(repoRoot, "web-rtcp-5axis-xyzbc-trt-sim-plan");
const outputPath = process.argv[2]
@@ -137,6 +140,10 @@ const evidence = {
pathSampling: createPathSampling(),
previewPath: paths.previewPath,
executionPath: paths.executionPath,
semanticExecutionPath: paths.semanticExecutionPath,
lineExecutionTrace: paths.semanticExecutionPath?.lineExecutionTrace || [],
axisValuesByLine: paths.semanticExecutionPath?.axisValuesByLine || [],
gcodeExecutionProcess: paths.semanticExecutionPath?.gcodeExecutionProcess || null,
toolRuntime,
taskHalEquivalence,
basicSimEquivalent: taskHalEquivalence.basicSimEquivalent,
@@ -169,6 +176,10 @@ const evidence = {
&& Boolean(semanticFields.axisJointLimits.axisLimits.C),
previewPathAvailable: paths.previewPath.sampleCount > 0,
executionPathAvailable: paths.executionPath.sampleCount > 0,
semanticExecutionPathAvailable: paths.semanticExecutionPath?.sampleCount > 0,
lineExecutionTraceAvailable: (paths.semanticExecutionPath?.lineExecutionTrace || []).length > 0,
axisValuesByLineAvailable: (paths.semanticExecutionPath?.axisValuesByLine || []).length > 0,
gcodeExecutionProcessAvailable: paths.semanticExecutionPath?.gcodeExecutionProcess?.status === "ok",
axisMainUiEquivalent: axisMainUi.ready,
basicSimEquivalent: taskHalEquivalence.ready === true,
ngcguiSubroutinesExecutable: ngcguiExecution.ready === true,
@@ -200,6 +211,10 @@ const evidence = {
id: "web-execution-path-unavailable",
detail: paths.executionPath.unavailableReason,
}]),
...(paths.semanticExecutionPath?.sampleCount > 0 ? [] : [{
id: "web-semantic-execution-path-unavailable",
detail: paths.semanticExecutionPath?.unavailableReason || "semantic execution path was not generated",
}]),
...(taskHalEquivalence.ready ? [] : [{
id: "web-basic-sim-equivalence-incomplete",
detail: taskHalEquivalence.unavailableReason,
@@ -261,6 +276,7 @@ async function collectPathEvidence({ profile, staged, selectedPlan, wasmArtifact
return {
previewPath: emptyPath("web-preview", "missing linuxcnc_interp WASM artifacts"),
executionPath: emptyPath("web-task-hal", "missing task/HAL WASM runtime artifacts"),
semanticExecutionPath: emptyPath("web-semantic-execution", "missing linuxcnc_interp WASM artifacts"),
};
}
@@ -278,16 +294,22 @@ async function collectPathEvidence({ profile, staged, selectedPlan, wasmArtifact
selectedPlan,
});
const previewPath = pathFromWebMotion(execution, profile, toolRuntime.pathTool, selectedPlan, staged);
const semanticExecutionPath = semanticExecutionPathFromAxisExpansion({ selectedPlan, staged, pathTool: toolRuntime.pathTool });
return {
previewPath,
executionPath: wasmArtifacts.ready
? await pathFromTaskHalExecution({ profile, staged, selectedPlan, execution, toolRuntime })
: emptyPath("web-task-hal", "missing task/HAL WASM runtime artifacts"),
semanticExecutionPath: semanticExecutionPath || emptyPath(
"web-semantic-execution",
"no semantic execution expansion is available for selected program",
),
};
} catch (error) {
return {
previewPath: emptyPath("web-preview", error instanceof Error ? error.message : String(error)),
executionPath: emptyPath("web-task-hal", "preview runtime failed before task/HAL execution capture"),
semanticExecutionPath: emptyPath("web-semantic-execution", "preview runtime failed before semantic execution capture"),
};
}
}
@@ -345,6 +367,27 @@ function pathFromAxisPreviewExpansion({ selectedPlan = null, staged = null, path
});
}
function semanticExecutionPathFromAxisExpansion({ selectedPlan = null, staged = null, pathTool = null } = {}) {
const selectedProgramFilename = selectedPlan?.selectedProgramFilename || "";
if (selectedProgramFilename !== "xyzbc_switchkins.ngc") return null;
const programFile = staged?.save?.files?.find((file) => (
file.sourceRel === selectedPlan.selectedProgramSourceRel
|| (file.wasmPath || file.path) === selectedPlan.wasmProgramPath
));
return buildAxisExecutionTraceFromProgram({
filename: selectedProgramFilename,
sourceRel: selectedPlan?.selectedProgramSourceRel,
content: programFile?.text || "",
tool: pathTool || {
id: 2,
pocket: 2,
length: 10,
diameter: 8,
},
source: "web-axis-source-execution-expanded-ngcgui-subroutines",
});
}
function resamplePlannerSamples(plannerSamples = [], samplePeriodMs = SAMPLE_PERIOD_MS) {
if (!Array.isArray(plannerSamples) || plannerSamples.length === 0) return [];
const normalized = plannerSamples
@@ -1043,13 +1086,16 @@ function normalizePathSample({
b: numberOrZero(axes.b),
c: numberOrZero(axes.c),
};
const normalizedTool = tool || firstTool({});
const normalizedFeed = numberOrZero(feed);
const normalizedSpindle = numberOrZero(spindle);
return {
sampleIndex,
timeMs,
line: Number(line) || 0,
motionType,
activeKinematics,
tool,
tool: normalizedTool,
joint,
tcp: {
x: joint.x,
@@ -1057,8 +1103,14 @@ function normalizePathSample({
z: joint.z,
},
toolAxis: toolAxisFromBc(joint.b, joint.c),
feed: numberOrZero(feed),
spindle: numberOrZero(spindle),
feed: normalizedFeed,
spindle: normalizedSpindle,
machineState: machineStateForMotion({
tool: normalizedTool,
feed: normalizedFeed,
motionType,
spindle: normalizedSpindle,
}),
};
}
@@ -1066,11 +1118,59 @@ function firstTool(profile) {
const tool = profile.toolTable?.tools?.[0] || {};
return {
id: Number(tool.tool) || 0,
pocket: Number(tool.pocket) || Number(tool.tool) || 0,
length: Number(tool.zOffset) || 0,
diameter: Number(tool.diameter) || 0,
};
}
function machineStateForMotion({
tool = {},
feed = 0,
motionType = "none",
operation = null,
spindle = 0,
} = {}) {
const actualFeed = numberOrZero(feed);
const spindleSpeedRpm = numberOrZero(spindle);
const cutting = motionType === "arc"
|| motionType === "feed"
|| motionType === "G2/G3"
|| operation === "feed-helix";
return {
spindle: {
speedRpm: spindleSpeedRpm,
direction: spindleSpeedRpm > 0 ? "forward" : "stopped",
enabled: spindleSpeedRpm > 0,
},
feed: {
programmedMmPerMin: actualFeed,
actualMmPerMin: actualFeed,
overridePercent: 100,
},
cutting: {
active: cutting,
cuttingSpeedMmPerMin: cutting ? actualFeed : 0,
},
tool: {
id: Number(tool?.id) || 0,
pocket: Number(tool?.pocket) || 0,
length: numberOrZero(tool?.length),
diameter: numberOrZero(tool?.diameter),
},
toolChange: {
activeTool: Number(tool?.id) || 0,
activePocket: Number(tool?.pocket) || 0,
changed: false,
command: null,
},
coolant: {
mist: false,
flood: false,
},
};
}
function xyzbcAxesFromTaskHalStatus(status = {}) {
const axis = status.motionStatus?.axis || {};
const pins = status.halSnapshot?.pins || {};

View File

@@ -10,10 +10,16 @@ const outputPath = process.argv[4] || resolve(projectRoot, "working/evidence/com
const PREVIEW_TCP_MAX_ERROR_MM = 0.001;
const PREVIEW_JOINT_MAX_ERROR = 0.001;
const PREVIEW_TOOL_AXIS_MAX_ERROR_DEG = 0.001;
const EXECUTION_TCP_MAX_ERROR_MM = 0.001;
const EXECUTION_JOINT_MAX_ERROR = 0.001;
const EXECUTION_TOOL_AXIS_MAX_ERROR_DEG = 0.001;
const nativeEvidence = JSON.parse(await readFile(nativePath, "utf8"));
const webEvidence = JSON.parse(await readFile(webPath, "utf8"));
const pathComparison = comparePathEvidence(nativeEvidence, webEvidence);
const lineExecutionComparison = compareLineExecutionTrace(nativeEvidence, webEvidence);
const axisValuesByLineComparison = compareAxisValuesByLine(nativeEvidence, webEvidence);
const gcodeExecutionProcessComparison = compareGcodeExecutionProcess(nativeEvidence, webEvidence);
const checks = [
check("profile", "native axis mask is XYZBC", nativeEvidence.coverage?.axisProfile === true, {
@@ -98,7 +104,7 @@ const checks = [
nativeTaskStateFlow: nativeEvidence.taskStateFlow,
webNativeStateFlowReview: webEvidence.nativeStateFlowReview,
}),
check("path-preview", "native and web preview path sample period is 20ms", pathComparison.previewVsPreview.periodsMatch === true, {
check("path-preview", "native and web preview path sample period is 50ms", pathComparison.previewVsPreview.periodsMatch === true, {
nativeSamplePeriodMs: pathComparison.previewVsPreview.nativeSamplePeriodMs,
webSamplePeriodMs: pathComparison.previewVsPreview.webSamplePeriodMs,
}),
@@ -115,7 +121,7 @@ const checks = [
maxToolAxisAngleDeg: pathComparison.previewVsPreview.maxToolAxisAngleDeg,
sampleCountDelta: pathComparison.previewVsPreview.sampleCountDelta,
}),
check("path-execution", "native and web execution path sample period is 20ms", pathComparison.executionVsExecution.periodsMatch === true, {
check("path-execution", "native and web execution path sample period is 50ms", pathComparison.executionVsExecution.periodsMatch === true, {
nativeSamplePeriodMs: pathComparison.executionVsExecution.nativeSamplePeriodMs,
webSamplePeriodMs: pathComparison.executionVsExecution.webSamplePeriodMs,
}),
@@ -124,6 +130,41 @@ const checks = [
webSampleCount: pathComparison.executionVsExecution.webSampleCount,
unavailable: pathComparison.executionVsExecution.unavailable,
}),
check("path-execution", "native and web source-expanded execution paths are geometrically aligned", pathComparison.semanticExecutionVsSemanticExecution.geometricAligned === true, {
thresholds: pathComparison.semanticExecutionVsSemanticExecution.thresholds,
nativeSampleCount: pathComparison.semanticExecutionVsSemanticExecution.nativeSampleCount,
webSampleCount: pathComparison.semanticExecutionVsSemanticExecution.webSampleCount,
maxTcpErrorMm: pathComparison.semanticExecutionVsSemanticExecution.maxTcpErrorMm,
rmsTcpErrorMm: pathComparison.semanticExecutionVsSemanticExecution.rmsTcpErrorMm,
maxJointError: pathComparison.semanticExecutionVsSemanticExecution.maxJointError,
maxToolAxisAngleDeg: pathComparison.semanticExecutionVsSemanticExecution.maxToolAxisAngleDeg,
machineStateMismatchCount: pathComparison.semanticExecutionVsSemanticExecution.machineStateMismatchCount,
sampleCountDelta: pathComparison.semanticExecutionVsSemanticExecution.sampleCountDelta,
}),
check("line-execution", "native and web per-line G-code execution trace matches", lineExecutionComparison.status === "pass", {
nativeTraceCount: lineExecutionComparison.nativeTraceCount,
webTraceCount: lineExecutionComparison.webTraceCount,
mismatchCount: lineExecutionComparison.mismatchCount,
mismatches: lineExecutionComparison.mismatches.slice(0, 10),
}),
check("axis-values", "native and web actual axis values by executed line match", axisValuesByLineComparison.status === "pass", {
nativeLineValueCount: axisValuesByLineComparison.nativeLineValueCount,
webLineValueCount: axisValuesByLineComparison.webLineValueCount,
thresholds: axisValuesByLineComparison.thresholds,
maxTcpErrorMm: axisValuesByLineComparison.maxTcpErrorMm,
maxJointError: axisValuesByLineComparison.maxJointError,
maxToolAxisAngleDeg: axisValuesByLineComparison.maxToolAxisAngleDeg,
mismatchCount: axisValuesByLineComparison.mismatchCount,
mismatches: axisValuesByLineComparison.mismatches.slice(0, 10),
}),
check("gcode-process", "native and web complete G-code execution process JSON matches", gcodeExecutionProcessComparison.status === "pass", {
nativeExecutionStepCount: gcodeExecutionProcessComparison.nativeExecutionStepCount,
webExecutionStepCount: gcodeExecutionProcessComparison.webExecutionStepCount,
nativeSourceLineCoverageCount: gcodeExecutionProcessComparison.nativeSourceLineCoverageCount,
webSourceLineCoverageCount: gcodeExecutionProcessComparison.webSourceLineCoverageCount,
mismatchCount: gcodeExecutionProcessComparison.mismatchCount,
mismatches: gcodeExecutionProcessComparison.mismatches.slice(0, 10),
}),
check("path-preview-execution-consistency", "native preview and execution paths are comparable", pathComparison.previewVsExecutionNative.comparable === true, {
nativePreviewSampleCount: pathComparison.previewVsExecutionNative.leftSampleCount,
nativeExecutionSampleCount: pathComparison.previewVsExecutionNative.rightSampleCount,
@@ -158,6 +199,9 @@ const report = {
},
checks,
pathComparison,
lineExecutionComparison,
axisValuesByLineComparison,
gcodeExecutionProcessComparison,
requiredImprovements: failed.map((item) => ({
category: item.category,
requirement: item.requirement,
@@ -185,7 +229,7 @@ function check(category, requirement, passed, evidence = {}) {
}
function comparePathEvidence(nativeEvidence, webEvidence) {
const samplePeriodMs = 20;
const samplePeriodMs = 50;
return {
samplePeriodMs,
previewVsPreview: compareNamedPaths({
@@ -201,6 +245,21 @@ function comparePathEvidence(nativeEvidence, webEvidence) {
leftName: "native",
rightName: "web",
expectedSamplePeriodMs: samplePeriodMs,
strictGeometry: false,
}),
semanticExecutionVsSemanticExecution: compareNamedPaths({
left: nativeEvidence.semanticExecutionPath,
right: webEvidence.semanticExecutionPath,
leftName: "native",
rightName: "web",
expectedSamplePeriodMs: samplePeriodMs,
strictGeometry: true,
thresholds: {
maxTcpErrorMm: EXECUTION_TCP_MAX_ERROR_MM,
maxJointError: EXECUTION_JOINT_MAX_ERROR,
maxToolAxisAngleDeg: EXECUTION_TOOL_AXIS_MAX_ERROR_DEG,
sampleCountDelta: 0,
},
}),
previewVsExecutionNative: compareNamedPaths({
left: nativeEvidence.previewPath,
@@ -219,7 +278,15 @@ function comparePathEvidence(nativeEvidence, webEvidence) {
};
}
function compareNamedPaths({ left, right, leftName, rightName, expectedSamplePeriodMs }) {
function compareNamedPaths({
left,
right,
leftName,
rightName,
expectedSamplePeriodMs,
strictGeometry = leftName === "native" && rightName === "web",
thresholds = null,
}) {
const leftSamplePeriodMs = left?.samplePeriodMs ?? null;
const rightSamplePeriodMs = right?.samplePeriodMs ?? null;
const periodsMatch = leftSamplePeriodMs === expectedSamplePeriodMs
@@ -233,26 +300,26 @@ function compareNamedPaths({ left, right, leftName, rightName, expectedSamplePer
...(periodsMatch ? [] : [`sample period mismatch ${leftSamplePeriodMs}/${rightSamplePeriodMs}`]),
];
const stats = comparable ? pathStats(leftSamples, rightSamples) : emptyStats(leftSamples, rightSamples);
const previewPair = leftName === "native" && rightName === "web";
const thresholds = previewPair ? {
const resolvedThresholds = strictGeometry ? (thresholds || {
maxTcpErrorMm: PREVIEW_TCP_MAX_ERROR_MM,
maxJointError: PREVIEW_JOINT_MAX_ERROR,
maxToolAxisAngleDeg: PREVIEW_TOOL_AXIS_MAX_ERROR_DEG,
sampleCountDelta: 0,
} : null;
const geometricAligned = previewPair
}) : null;
const geometricAligned = strictGeometry
? comparable
&& stats.maxTcpErrorMm <= thresholds.maxTcpErrorMm
&& stats.maxJointError <= thresholds.maxJointError
&& stats.maxToolAxisAngleDeg <= thresholds.maxToolAxisAngleDeg
&& stats.sampleCountDelta <= thresholds.sampleCountDelta
&& stats.maxTcpErrorMm <= resolvedThresholds.maxTcpErrorMm
&& stats.maxJointError <= resolvedThresholds.maxJointError
&& stats.maxToolAxisAngleDeg <= resolvedThresholds.maxToolAxisAngleDeg
&& stats.sampleCountDelta <= resolvedThresholds.sampleCountDelta
&& stats.machineStateMismatchCount === 0
&& stats.missingSamples.length === 0
: comparable;
return {
status: comparable && (!previewPair || geometricAligned) ? "pass" : "fail",
status: comparable && (!strictGeometry || geometricAligned) ? "pass" : "fail",
comparable,
geometricAligned,
thresholds,
thresholds: resolvedThresholds,
periodsMatch,
[`${leftName}SamplePeriodMs`]: leftSamplePeriodMs,
[`${rightName}SamplePeriodMs`]: rightSamplePeriodMs,
@@ -267,6 +334,322 @@ function compareNamedPaths({ left, right, leftName, rightName, expectedSamplePer
};
}
function compareLineExecutionTrace(nativeEvidence, webEvidence) {
const nativeTrace = Array.isArray(nativeEvidence.lineExecutionTrace)
? nativeEvidence.lineExecutionTrace
: nativeEvidence.semanticExecutionPath?.lineExecutionTrace || [];
const webTrace = Array.isArray(webEvidence.lineExecutionTrace)
? webEvidence.lineExecutionTrace
: webEvidence.semanticExecutionPath?.lineExecutionTrace || [];
const count = Math.min(nativeTrace.length, webTrace.length);
const mismatches = [];
for (let index = 0; index < count; index += 1) {
const left = nativeTrace[index];
const right = webTrace[index];
const keys = ["sourceFile", "line", "operation", "motionType", "activeKinematicsAfter", "producesMotion", "segmentIndex"];
const differences = keys
.filter((key) => normalizeComparable(left?.[key]) !== normalizeComparable(right?.[key]))
.map((key) => ({ key, native: left?.[key], web: right?.[key] }));
if (differences.length > 0) {
mismatches.push({
index,
native: projectTraceEntry(left),
web: projectTraceEntry(right),
differences,
});
}
}
if (nativeTrace.length !== webTrace.length) {
mismatches.push({
index: count,
differences: [{
key: "traceCount",
native: nativeTrace.length,
web: webTrace.length,
}],
});
}
return {
status: mismatches.length === 0 && nativeTrace.length > 0 && webTrace.length > 0 ? "pass" : "fail",
nativeTraceCount: nativeTrace.length,
webTraceCount: webTrace.length,
mismatchCount: mismatches.length,
mismatches,
semanticBoundary: "native_web_line_by_line_gcode_execution_trace_comparison",
};
}
function compareAxisValuesByLine(nativeEvidence, webEvidence) {
const nativeValues = Array.isArray(nativeEvidence.axisValuesByLine)
? nativeEvidence.axisValuesByLine
: nativeEvidence.semanticExecutionPath?.axisValuesByLine || [];
const webValues = Array.isArray(webEvidence.axisValuesByLine)
? webEvidence.axisValuesByLine
: webEvidence.semanticExecutionPath?.axisValuesByLine || [];
const count = Math.min(nativeValues.length, webValues.length);
const mismatches = [];
let maxTcpErrorMm = 0;
let maxJointError = 0;
let maxToolAxisAngleDeg = 0;
const thresholds = {
maxTcpErrorMm: EXECUTION_TCP_MAX_ERROR_MM,
maxJointError: EXECUTION_JOINT_MAX_ERROR,
maxToolAxisAngleDeg: EXECUTION_TOOL_AXIS_MAX_ERROR_DEG,
};
for (let index = 0; index < count; index += 1) {
const left = nativeValues[index];
const right = webValues[index];
const tcpError = vectorError(left?.tcp, right?.tcp, ["x", "y", "z"]);
const jointError = vectorError(left?.joint, right?.joint, ["x", "y", "z", "b", "c"]);
const angleError = toolAxisAngleDeg(left?.toolAxis, right?.toolAxis);
const machineStateMatches = compareJsonStable(left?.machineState, right?.machineState);
maxTcpErrorMm = Math.max(maxTcpErrorMm, tcpError);
maxJointError = Math.max(maxJointError, jointError);
maxToolAxisAngleDeg = Math.max(maxToolAxisAngleDeg, angleError);
const identityMismatch = ["sourceFile", "line", "operation", "motionType", "activeKinematics", "segmentIndex"]
.some((key) => normalizeComparable(left?.[key]) !== normalizeComparable(right?.[key]));
if (identityMismatch
|| tcpError > thresholds.maxTcpErrorMm
|| jointError > thresholds.maxJointError
|| angleError > thresholds.maxToolAxisAngleDeg
|| !machineStateMatches) {
mismatches.push({
index,
native: projectAxisValue(left),
web: projectAxisValue(right),
tcpError,
jointError,
toolAxisAngleDeg: angleError,
machineStateMatches,
});
}
}
if (nativeValues.length !== webValues.length) {
mismatches.push({
index: count,
nativeLineValueCount: nativeValues.length,
webLineValueCount: webValues.length,
});
}
return {
status: mismatches.length === 0 && nativeValues.length > 0 && webValues.length > 0 ? "pass" : "fail",
nativeLineValueCount: nativeValues.length,
webLineValueCount: webValues.length,
thresholds,
maxTcpErrorMm,
maxJointError,
maxToolAxisAngleDeg,
mismatchCount: mismatches.length,
mismatches,
semanticBoundary: "native_web_executed_line_axis_values_comparison",
};
}
function compareGcodeExecutionProcess(nativeEvidence, webEvidence) {
const nativeProcess = nativeEvidence.gcodeExecutionProcess || nativeEvidence.semanticExecutionPath?.gcodeExecutionProcess || null;
const webProcess = webEvidence.gcodeExecutionProcess || webEvidence.semanticExecutionPath?.gcodeExecutionProcess || null;
const nativeSteps = Array.isArray(nativeProcess?.executionSteps) ? nativeProcess.executionSteps : [];
const webSteps = Array.isArray(webProcess?.executionSteps) ? webProcess.executionSteps : [];
const nativeCoverage = Array.isArray(nativeProcess?.sourceLineCoverage) ? nativeProcess.sourceLineCoverage : [];
const webCoverage = Array.isArray(webProcess?.sourceLineCoverage) ? webProcess.sourceLineCoverage : [];
const mismatches = [];
if (nativeProcess?.status !== "ok" || webProcess?.status !== "ok") {
mismatches.push({
index: 0,
field: "status",
native: nativeProcess?.status ?? null,
web: webProcess?.status ?? null,
});
}
compareCoverage(nativeCoverage, webCoverage, mismatches);
const stepCount = Math.min(nativeSteps.length, webSteps.length);
for (let index = 0; index < stepCount; index += 1) {
const left = nativeSteps[index];
const right = webSteps[index];
const differences = [];
for (const key of ["stepIndex", "sourceFile", "line", "statement", "callDepth", "executed", "sourceLineKind"]) {
if (normalizeComparable(left?.[key]) !== normalizeComparable(right?.[key])) {
differences.push({ key, native: left?.[key], web: right?.[key] });
}
}
for (const key of [
"operation",
"sourceLineKind",
"executed",
"activeKinematicsBefore",
"activeKinematicsAfter",
"traceExecutionIndex",
]) {
if (normalizeComparable(left?.result?.[key]) !== normalizeComparable(right?.result?.[key])) {
differences.push({ key: `result.${key}`, native: left?.result?.[key], web: right?.result?.[key] });
}
}
if (!compareJsonStable(left?.result?.machineStateAfter, right?.result?.machineStateAfter)) {
differences.push({
key: "result.machineStateAfter",
native: left?.result?.machineStateAfter,
web: right?.result?.machineStateAfter,
});
}
if (JSON.stringify(left?.result?.parametersChanged || {}) !== JSON.stringify(right?.result?.parametersChanged || {})) {
differences.push({
key: "result.parametersChanged",
native: left?.result?.parametersChanged,
web: right?.result?.parametersChanged,
});
}
const leftMotion = left?.result?.motion || null;
const rightMotion = right?.result?.motion || null;
if (Boolean(leftMotion) !== Boolean(rightMotion)) {
differences.push({ key: "result.motion", native: Boolean(leftMotion), web: Boolean(rightMotion) });
} else if (leftMotion && rightMotion) {
const tcpError = vectorError(leftMotion.endTcp, rightMotion.endTcp, ["x", "y", "z"]);
const jointError = vectorError(leftMotion.endJoint, rightMotion.endJoint, ["x", "y", "z", "b", "c"]);
const axisError = toolAxisAngleDeg(leftMotion.endToolAxis, rightMotion.endToolAxis);
if (tcpError > EXECUTION_TCP_MAX_ERROR_MM || jointError > EXECUTION_JOINT_MAX_ERROR || axisError > EXECUTION_TOOL_AXIS_MAX_ERROR_DEG) {
differences.push({
key: "result.motion.axisValues",
tcpError,
jointError,
toolAxisAngleDeg: axisError,
native: projectMotion(leftMotion),
web: projectMotion(rightMotion),
});
}
for (const key of ["segmentIndex", "motionType", "feed"]) {
if (normalizeComparable(leftMotion[key]) !== normalizeComparable(rightMotion[key])) {
differences.push({ key: `result.motion.${key}`, native: leftMotion[key], web: rightMotion[key] });
}
}
}
if (differences.length > 0) {
mismatches.push({
index,
native: projectGcodeStep(left),
web: projectGcodeStep(right),
differences,
});
}
}
if (nativeSteps.length !== webSteps.length) {
mismatches.push({
index: stepCount,
field: "executionSteps.length",
native: nativeSteps.length,
web: webSteps.length,
});
}
return {
status: mismatches.length === 0 && nativeSteps.length > 0 && webSteps.length > 0 ? "pass" : "fail",
nativeExecutionStepCount: nativeSteps.length,
webExecutionStepCount: webSteps.length,
nativeSourceLineCoverageCount: nativeCoverage.length,
webSourceLineCoverageCount: webCoverage.length,
nativeSummary: nativeProcess?.summary || null,
webSummary: webProcess?.summary || null,
mismatchCount: mismatches.length,
mismatches,
semanticBoundary: "native_web_complete_gcode_execution_process_json_comparison",
};
}
function compareCoverage(nativeCoverage, webCoverage, mismatches) {
const count = Math.min(nativeCoverage.length, webCoverage.length);
for (let index = 0; index < count; index += 1) {
const left = nativeCoverage[index];
const right = webCoverage[index];
const differences = [];
for (const key of ["sourceFile", "line", "statement", "sourceLineKind", "visitCount", "producedMotionCount"]) {
if (normalizeComparable(left?.[key]) !== normalizeComparable(right?.[key])) {
differences.push({ key: `coverage.${key}`, native: left?.[key], web: right?.[key] });
}
}
if (JSON.stringify(left?.operations || []) !== JSON.stringify(right?.operations || [])) {
differences.push({ key: "coverage.operations", native: left?.operations, web: right?.operations });
}
if (differences.length > 0) {
mismatches.push({
index,
field: "sourceLineCoverage",
native: left,
web: right,
differences,
});
}
}
if (nativeCoverage.length !== webCoverage.length) {
mismatches.push({
index: count,
field: "sourceLineCoverage.length",
native: nativeCoverage.length,
web: webCoverage.length,
});
}
}
function projectGcodeStep(step = {}) {
return {
stepIndex: step.stepIndex,
sourceFile: step.sourceFile,
line: step.line,
statement: step.statement,
callDepth: step.callDepth,
operation: step.result?.operation,
sourceLineKind: step.sourceLineKind,
activeKinematicsAfter: step.result?.activeKinematicsAfter,
traceExecutionIndex: step.result?.traceExecutionIndex,
motion: step.result?.motion ? projectMotion(step.result.motion) : null,
parametersChanged: step.result?.parametersChanged,
machineStateAfter: step.result?.machineStateAfter,
};
}
function projectMotion(motion = {}) {
return {
segmentIndex: motion.segmentIndex,
motionType: motion.motionType,
feed: motion.feed,
endJoint: motion.endJoint,
endTcp: motion.endTcp,
endToolAxis: motion.endToolAxis,
};
}
function projectTraceEntry(entry = {}) {
return {
sourceFile: entry.sourceFile,
line: entry.line,
operation: entry.operation,
motionType: entry.motionType,
activeKinematicsAfter: entry.activeKinematicsAfter,
producesMotion: entry.producesMotion,
segmentIndex: entry.segmentIndex,
};
}
function projectAxisValue(entry = {}) {
return {
sourceFile: entry.sourceFile,
line: entry.line,
operation: entry.operation,
motionType: entry.motionType,
activeKinematics: entry.activeKinematics,
segmentIndex: entry.segmentIndex,
joint: entry.joint,
tcp: entry.tcp,
toolAxis: entry.toolAxis,
machineState: entry.machineState,
};
}
function normalizeComparable(value) {
if (value === undefined || value === null) return null;
if (typeof value === "number") return Number.isFinite(value) ? Number(value.toFixed(12)) : null;
return value;
}
function pathStats(leftSamples, rightSamples) {
const count = Math.min(leftSamples.length, rightSamples.length);
const missingSamples = [];
@@ -275,6 +658,7 @@ function pathStats(leftSamples, rightSamples) {
let maxJointError = 0;
let sumJointErrorSquared = 0;
let maxToolAxisAngleDeg = 0;
const machineStateMismatches = [];
for (let index = 0; index < count; index += 1) {
const left = leftSamples[index];
const right = rightSamples[index];
@@ -289,6 +673,13 @@ function pathStats(leftSamples, rightSamples) {
maxJointError = Math.max(maxJointError, jointError);
sumJointErrorSquared += jointError ** 2;
maxToolAxisAngleDeg = Math.max(maxToolAxisAngleDeg, angleError);
if (!compareJsonStable(left.machineState, right.machineState)) {
machineStateMismatches.push({
index,
native: left.machineState,
web: right.machineState,
});
}
}
return {
maxTcpErrorMm,
@@ -298,6 +689,8 @@ function pathStats(leftSamples, rightSamples) {
maxToolAxisAngleDeg,
sampleCountDelta: Math.abs(leftSamples.length - rightSamples.length),
missingSamples,
machineStateMismatchCount: machineStateMismatches.length,
machineStateMismatches: machineStateMismatches.slice(0, 10),
};
}
@@ -310,9 +703,25 @@ function emptyStats(leftSamples, rightSamples) {
maxToolAxisAngleDeg: null,
sampleCountDelta: Math.abs(leftSamples.length - rightSamples.length),
missingSamples: [],
machineStateMismatchCount: null,
machineStateMismatches: [],
};
}
function compareJsonStable(left, right) {
return JSON.stringify(normalizeForJsonCompare(left)) === JSON.stringify(normalizeForJsonCompare(right));
}
function normalizeForJsonCompare(value) {
if (value === undefined || value === null) return null;
if (typeof value === "number") return Number.isFinite(value) ? Number(value.toFixed(12)) : null;
if (Array.isArray(value)) return value.map((item) => normalizeForJsonCompare(item));
if (typeof value === "object") {
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, normalizeForJsonCompare(value[key])]));
}
return value;
}
function vectorError(left = {}, right = {}, keys = []) {
return Math.sqrt(keys.reduce((sum, key) => (
sum + (numberOrZero(left[key]) - numberOrZero(right[key])) ** 2