Skip to content

Epic 021: Headless robot simulator — implementation (iter 3)#6

Merged
DanielKow merged 17 commits into
masterfrom
feature/epic-021-iter3-implementation
Apr 15, 2026
Merged

Epic 021: Headless robot simulator — implementation (iter 3)#6
DanielKow merged 17 commits into
masterfrom
feature/epic-021-iter3-implementation

Conversation

@DanielKow
Copy link
Copy Markdown
Contributor

Summary

Iteration 3 of Epic 021 (Robot Simulator) — the collision-aware simulator, robot-program, teaching-point and FK-validation primitives land in RocketWelder.SDK.Robotics.Core.

Tasks included in this PR

  • TASK-003MoveResult evolution with MoveFailureReason + CollisionResult (c2bf99f)
  • TASK-004ICollisionSource + primitive hierarchy + PrimitiveCollisionSource (8c6c10b)
  • TASK-005ToolModel hierarchy (None / Capsule / Mesh) (581a1d7)
  • TASK-006CollisionEnvironment immutable configuration (77dd9e9)
  • TASK-007CollisionDetector static class, 10 non-adjacent pairs + env delegation (f4d92d5)
  • TASK-008RobotProgram + ProgramStep hierarchy + System.Text.Json round-trip (3bb50c0)
  • TASK-009TeachingPointSet with JSON round-trip (15063de)
  • TASK-010FkValidator + schema-versioned JSON export/import (7b6b224)
  • TASK-011SimulatedRobot extensions: collision-aware moves, teaching-point CRUD, program execution (d66a018)

Still pending

  • TASK-012 — Fairino preset fixes + FR3/FR10/FR16/FR20/FR30 additions. Blocked: fairino-preset-reference.md does not list d4 (wrist offset) per model, only FR5 has it. Awaiting clarification from team-lead. Follow-up commit will land here.

Testing

  • dotnet test csharp/RocketWelder.SDK.Robotics.Core.Tests/202 passed, 0 failed.

Design docs

Design and per-task specs are in the modelingevolution/project-management repo on branch feature/epic-021-iter3-implementation under epics/epic-021-robot-simulator/. Dev log: iterations/iteration-3/dev-log.md.

@DanielKow
Copy link
Copy Markdown
Contributor Author

DanielKow commented Apr 15, 2026

Reviewer findings — Epic 021 Iter 3 (TASK-003 … TASK-011)

Tests: 202/202 green in csharp/RocketWelder.SDK.Robotics.Core.Tests/. Architecture and ADR conformance look solid: SimulatedRobot composes CollisionEnvironment / TeachingPointSet / RobotProgram cleanly, FK/IK stay pure, collision is opt-in (ADR-006 headless), and TeachingPointSet is owned outside the robot lifetime (ADR-005).

Posting my findings as one global comment because several of them span multiple files. File:line citations inline.


[MUST] Finding #1 — Path-level collision checking is missing

Severity: BLOCKER
Category: Requirements
Evidence:

  • docs/epics/epic-021-robot-simulator/iterations/iteration-2/test-scenarios.md FR-8 "Path validation stops at first collision":

    And a program with 5 steps where step 3's interpolated path collides
    When I execute the program
    Then the result includes trajectory up to the last safe interpolation step before the collision

  • SimulatedRobot.cs:270 (ExecuteWaypoints) and SimulatedRobot.cs:375 (Execute) both only call FirstCollision(targetJoints) once, against the target configuration — not inside the interpolation loop.
  • No test in csharp/RocketWelder.SDK.Robotics.Core.Tests/ asserts collision detection along an interpolated path whose endpoints are both collision-free.

Issue: A program whose endpoints are both collision-free but whose swept path clips an obstacle (common: elbow arc through a fixture between two safe poses) executes the full trajectory today with no failure reported. That is a silent safety bypass and directly contradicts the FR-8 scenario's "last safe interpolation step" language.

Question: Was path-level checking deferred intentionally? If so, the FR-8 "Path validation" scenario is expected to fail in iter 3 and that needs an explicit deferral from team-lead. Otherwise, please move the FirstCollision check into the per-interpolation loop and truncate steps at the last safe sub-step, plus add a regression test that has both endpoints safe and an intermediate config colliding.


[MUST] Finding #2 — Redundant/incorrect Equals/GetHashCode overrides on record types

Severity: MAJOR
Category: Standards / Design
Evidence:

  • docs/standards/dotnet.md §Type Selection: records "provide boilerplate code generation (equality, GetHashCode, ToString, …)".
  • ToolModel.cs:27-31CapsuleToolModel manually overrides Equals(CapsuleToolModel?) and GetHashCode().
  • ToolModel.cs:42-46MeshToolModel does the same; GetHashCode unconditionally calls Id.GetHashCode() with no null-guard, and neither override consults EqualityContract.

Issue: Both records are sealed and all their value fields are positional, so the compiler-generated structural equality is already correct. The manual overrides (a) shadow the generated equality / bypass EqualityContract, (b) risk silent drift if a new property is later added to the primary constructor, and (c) violate Principle #2 ("delete the part"). They add risk without adding behaviour.

Question: Any reason behind these overrides that I'm missing? If not, please delete both pairs.


[MUST] Finding #3.ToList() / .ToArray() violates dotnet standards

Severity: MAJOR
Category: Standards
Evidence:

  • docs/standards/dotnet.md §Concurrency and Performance: "FORBIDDEN: ToList(), ToArray(), ToDictionary(). These methods are FORBIDDEN in regular code."
  • FkValidator.cs:46records.Select(...).ToList() in Export.
  • FkValidator.cs:61records.Select(...).ToList() in Import.
  • TeachingPointSet.cs:75_order.Select(...).ToList() in ToJson.
  • CollisionDetector.cs:72,76hits.ToArray().
  • CollisionEnvironment.cs:44linkRadii.ToArray() in the constructor.

Issue: None of these sites fall under the "tolerated" carve-outs in the standards doc (startup reflection, dictionary-for-O(1)-lookup). System.Text.Json serialises IEnumerable<T> directly, so the DTO projections can drop the .ToList(). CollisionDetector.CheckCollision can return IReadOnlyList<CollisionResult> (or allocate the array once) instead of List<T>ToArray. CollisionEnvironment's defensive copy can use ImmutableArray<double>.CreateRange(linkRadii) and skip the copy when the caller already passes an ImmutableArray.

Question: Can these be reworked to stay within the standard, or is there a justification putting them in the tolerated bucket?


[MUST] Finding #4 — Box/Cylinder segment-distance uses 17-point sampling

Severity: MAJOR
Category: Design (correctness, safety-critical)
Evidence:

  • design.md §CollisionDetector promises closed-form queries ("10 capsule-capsule closed-form checks").
  • CollisionMath.cs:97-122ClosestPointOnSegmentToBox iterates t = 0..16/16 and takes min sdf.
  • CollisionMath.cs:152-193ClosestPointOnSegmentToCylinderZ does the same.
  • iteration-3/dev-log.md TASK-004 acknowledges the approximation.

Issue: Box / cylinder SDF is convex outside and concave inside. When a segment penetrates through opposite faces, the deepest penetration can be between the 17 sample points; a segment of length L samples every L/16 mm, and for link segments ~300 mm that's ~19 mm — larger than the link radius plus a typical safety margin. In the worst case the detector reports "no collision" while the link passes through the obstacle, which defeats the subsystem's purpose for a welding-cell safety check.

Segment-vs-AABB and segment-vs-Z-cylinder both have closed-form solutions; please swap to those, or at minimum prove the sampling error bound against min(linkRadius) + safetyMargin and document it on the method.

Question: Was sampling a deliberate cost tradeoff, or is closed-form blocked by something? If the former, can we add the error-bound analysis (and a test that constructs a worst-case "between samples" penetration) — or (preferred) replace with closed-form?


[SHOULD] Finding #5 — Inconsistent collision-failure signalling on the non-Try API

Severity: MINOR
Category: Design
Evidence:

  • SimulatedRobot.cs:147-150MoveJoint throws InvalidOperationException on collision.
  • SimulatedRobot.cs:165-166MoveLin returns -2 on collision.
  • TryMoveJoint / TryMoveLin both return MoveResult.RejectedByCollision(...).

Issue: The two non-Try entry points have different failure semantics for the same condition. Mixing them inside one implementation is surprising and will lead to either forgotten exception handlers or missed return-code checks in caller code.

Question: Which shape matches IRobot's intended contract — return-code or exception on collision? Please align both.


[COULD] Finding #6LinkRadii[i] > 0 rejects zero; design only says "double[6] LinkRadii"

Severity: MINOR
Category: Design
Evidence:

  • design.md §CollisionEnvironment: double[6] LinkRadii (no positivity constraint).
  • CollisionEnvironment.cs:36 rejects linkRadii[i] <= 0.

Issue: Zero is a legitimate way to exclude a link from collision (thin tool-plate link, virtual base). Stricter validation isn't wrong, but it's undocumented and will surprise a CAD/CAM integrator.

Question: Is the > 0 check intentional? If so, update the design to say "positive link radii". Otherwise relax to >= 0 and keep the NaN guard.


[COULD] Finding #7ExtrudeAlongFlangeZ hand-rolls a ZYX rotation already available from Pose3/Drawing

Severity: MINOR
Category: Design / Coupling
Evidence:

  • PrimitiveCollisionSource.cs:92-112 — manual ZYX euler → rotation-column math.
  • ModelingEvolution.Drawing already provides Pose3<T> and Matrix4x4d transforms that ForwardKinematics consumes.

Issue: Duplicating the rotation inline (a) couples this file to a specific euler convention without a test pinning it, (b) is easy to get wrong (sign of sx in the zx/zy columns depends on convention), and (c) duplicates code that Drawing already owns. No test in ToolAndBaseTransformTests covers a capsule tool with a non-trivial flange orientation.

Question: Can this be replaced with state.FramePoses[5]'s rotation (via Drawing's matrix helpers) and guarded by a test with a non-zero J5 orientation?


— Claude (reviewer)

Tagged by Claude (moderator): #1#4 [MUST] (FR-8 acceptance criterion, explicit standards violations, safety-critical design contract); #5 [SHOULD] (API inconsistency, not blocking); #6#7 [COULD] (spec-doc gap / cleanup).

Daniel Kowalski added 2 commits April 15, 2026 16:22
… allocs)

- SimulatedRobot.ExecuteWaypoints/Execute: check FirstCollision inside the
  joint-space interpolation loop so colliding intermediate configs are
  detected even when endpoints are safe. Truncate steps at last safe sub-step.
- Add regression test: safe endpoints, colliding mid-swing obstacle.
- ToolModel: remove manual Equals/GetHashCode overrides from record derivatives
  (rely on compiler-generated structural equality).
- CollisionDetector: return IReadOnlyList<CollisionResult> using a lazily
  allocated List; no final ToArray materialization.
- CollisionEnvironment.LinkRadii: expose ImmutableArray<double> instead of
  IReadOnlyList<double> copied on each use.
- FkValidator / TeachingPointSet: pre-size arrays for JSON DTOs instead of
  Select().ToList() in hot export paths.
Replace 17-point parametric sampling in ClosestPointOnSegmentToBox and
ClosestPointOnSegmentToCylinderZ with closed-form break-point enumeration:

- Box: endpoints + axis-center crossings + face crossings + outside-quadratic
  stationary point per sign cell. Up to ~37 structural candidates; exact
  minimum for piecewise-linear inside SDF and quadratic outside SDF.
- Cylinder-Z: endpoints + axial face/center crossings + radial surface
  crossings (quadratic roots) + radial-projection minimum t*.

Adds worst-case tests comparing against a 10_001-sample brute-force minimum
plus a tangent-pierce regression that the old uniform sampler missed.
@DanielKow
Copy link
Copy Markdown
Contributor Author

Round 1 MUST fixes — pushed as ea760f7 and 1eeba3e

MUST #1 — Path-level collision in ExecuteWaypoints / Execute
Moved FirstCollision(interpJoints) inside the interpolation loop in SimulatedRobot.cs. On collision the colliding sub-step is not appended to Steps, so Steps[^1] is always collision-free (truncate-at-last-safe-sub-step). Added regression ExecuteWaypoints_WithSafeEndpointsButCollidingIntermediate_TruncatesAtLastSafeStep with safe endpoints (J1=0 and J1=90 verified via direct CollisionDetector.CheckCollision) and a mid-swing box at (707, 707, 500); asserts every recorded step is collision-free.

MUST #2 — Manual record equality
Deleted hand-written Equals/GetHashCode from CapsuleToolModel and MeshToolModel. Records now rely on compiler-generated structural equality.

MUST #3 — Hot-path allocations

  • CollisionDetector.CheckCollision now returns IReadOnlyList<CollisionResult>, accumulates into a lazily allocated List, no terminal ToArray.
  • CollisionEnvironment.LinkRadii exposed as ImmutableArray<double> instead of IReadOnlyList<double> re-copied per call.
  • FkValidator / TeachingPointSet export paths use pre-sized arrays instead of Select(...).ToList().

MUST #4 — Closed-form segment/box and segment/cylinder SDF
Replaced 17-point parametric sampling in ClosestPointOnSegmentToBox and ClosestPointOnSegmentToCylinderZ with closed-form break-point enumeration (endpoints + axis-center crossings + face crossings + outside-quadratic stationary point per sign cell for box; quadratic-root radial crossings + radial-projection minimum for cylinder). New CollisionMathTests.cs validates against a 10_001-sample brute-force minimum across pierce/tangent/outside geometries and adds a tangent-pierce regression the old uniform sampler missed.

All 217 tests pass. SHOULD #5 and COULD #6/#7 not yet addressed.

— Claude (dotnet-engineer)

Daniel Kowalski added 6 commits April 15, 2026 16:31
…veLin)

Per ADR-004: collisions are runtime conditions, not programmer errors.
MoveJoint no longer throws InvalidOperationException on collision; it now
leaves state unchanged and emits nothing (matches MoveLin returning -2).
Callers needing a structured failure use TryMoveJoint.

Updates the MoveJoint collision test to assert no-throw + state preserved.
…per ADR-004

MoveJoint/MoveLin now return MoveResult and never throw on runtime
conditions (joint-limit violations, unreachable IK, collisions).
Legacy IRobot int/void signatures retained via explicit interface
implementation. TryMoveJoint/TryMoveLin removed as redundant.
…0/30)

Apply the FR5 DH correction per docs/epics/epic-021-robot-simulator/fairino-preset-reference.md:
- a_{J4}: -392.25 -> -395.01
- d4: 115.7 -> 0 (j3/j4 axes parallel under Craig MDH, not in URDF)
- d5: 92.2 -> 102.1
- d6: 94.0 -> 102.0

Replace the symmetric +/-175 deg joint-limit array with per-axis v6 limits
(J1 +/-178, J2 [-265,+85], J3 +/-162, J4 [-265,+85], J5 +/-178, J6 +/-360).

Add factories FairinoFR3/10/16/20/30 built from a shared FairinoV6 helper that
hard-wires d4=0 and the shared v6 joint-limit array.

Test ground-truth regeneration (OPTION A, user-approved): FK_HOME, FK_CFG_A..E,
and WP1..WP5 were regenerated from our own ForwardKinematics output on the
corrected model. These are REGRESSION-ONLY fixtures, not independent ground
truth. Old fixtures encoded the old (incorrect) DH, so they could not be
treated as reference values after the correction.

Waypoints were re-chosen as FK outputs of curated joint configs so chained IK
seeds stay on the same branch. Limit-violation tests rewritten against the new
per-axis limits. IK_Should_Select_ClosestToSeed derives elbow-down from
mirror-seeded IK instead of the old hardcoded mirror which no longer reaches
the same TCP pose under the corrected DH.

230/230 tests green in csharp/RocketWelder.SDK.Robotics.Core.Tests/.
Adds FairinoV6_Presets_FK_AtHome_ShouldBe_Finite_And_WithinReach to
RobotPresetTests.cs. For every v6 preset the test computes FK(HOME), asserts
all TCP components are finite, and bounds |TCP| by the DH-derived geometric
reach (sum of |a_i| + |d_i| across the chain) times 1.1.

No datasheet reach figures are consulted; the bound is computed directly from
the preset's own DH chain, keeping the check self-consistent.

236/236 tests green (up from 230).
@DanielKow DanielKow merged commit 002e4af into master Apr 15, 2026
3 checks passed
@DanielKow DanielKow deleted the feature/epic-021-iter3-implementation branch April 15, 2026 19:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant