Skip to content

Multiaxis kinematics and limits - #4374

Draft
grandixximo wants to merge 20 commits into
LinuxCNC:masterfrom
grandixximo:multiaxis-kins-limits
Draft

Multiaxis kinematics and limits#4374
grandixximo wants to merge 20 commits into
LinuxCNC:masterfrom
grandixximo:multiaxis-kins-limits

Conversation

@grandixximo

@grandixximo grandixximo commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Draft. Where the multiaxis kinematics work is going, open early so it can be argued with. As pieces are agreed I split them into small PRs, rebase this, and carry on, so nothing stalls behind it.

Plan, in order:

  1. Switchkins: one implementation, used by .comp, .c, in tree and out of tree. The registration half merged as switchkins: allow more than three kinematics types #4372; the rest is in this branch.

  2. Small fixes that stand on their own: 5axiskins gets a .tool-offset pin like every other module, maxkins gets one or a doc note saying it has none, and G53 stops ignoring the rotary tool offset on non-wrapped axes.

  3. The kinematics interface learns to answer two more questions: where the tool points, and how joint motion maps to world motion. One entry point each, both weak, both defaulting to today's behaviour, and a tool frame convention written down so every module answers the same way rather than each author choosing one.

  4. Those answers, and forward and inverse alongside them, become pure functions of what the caller passes in: joint values, a parameter block carrying the tool and the kinematics type, and no state kept inside the module. Today the maths reads its geometry from HAL pins and keeps scratch in haldata, so the only way to call it from anywhere else is to write a second copy of it. One implementation, parameters supplied at the call site.

  5. Two stages the offset chain is missing. Both came out of the discussion below, and both want 3 and 4 first.

    A frame composed on top of the active coordinate system, with its own cancel, so the blocks in between are programmed in the tilted plane while G54 itself is untouched. Fanuc spells this G68.2; Siemens puts frames above the kinematic transformation for the same reason. In tree that is a stage in rotate_and_offset_pos() plus the modal state to go with it. Nothing is rewritten, so nothing needs restoring, and on_abort_with_twp_reset.ngc has nothing left to do.

    A move whose target is Cartesian but whose interpolation is in joint space: run the inverse once at the endpoint, then interpolate the joints to reach it. Coordinates and units never change, so the offsets keep applying and there is nothing to switch. That is how the commercial controls cross a singularity without changing coordinate system, and KINEMATICS_INVERSE_FLAGS already carries enough to pin which solution branch the endpoint lands on. It needs a new move type from canon into the trajectory planner, and a way to ask for it in G-code.

    Between them these are the reason not to add a set of offsets per kinematics type. Controls do let you add coordinate systems, Fanuc G54.1 P1 to P48, Siemens G505 to G599, and none of them ties a set to the active kinematics or transformation: selection is explicit in the program, or it belongs to a channel. More coordinate systems is a fair request on its own; binding them to the kinstype is the workaround for these two absences.

  6. Limits taken from the joints instead of from static per-axis INI values. Joint limits are the physical fact. The region they imply in axis space depends on the tool and on where the rotaries are, and is not a box, which is why a box in the INI cannot describe it and config authors have to pick a conservative one and lose travel. [AXIS_L] keeps the job it is good for, a restriction the user imposes in axis space, normally set inside what is reachable; the effective bound is the intersection of the two, and on non-trivial kinematics neither one contains the other, so both are checked. Canon asks per move, which gives TP0 and TP1 the result with no planner change and costs 28 us a move on a closed-form five-axis module. Every commercial five-axis control applies its limits in machine-axis space after the transformation; this is the piece LinuxCNC does not have. Asking also makes the limits per kinematics type, which retires a workaround the docs currently prescribe: switchkins.adoc says the INI limits apply to type 0 only and tells you to setp ini.L.min_limit from M-codes at each switch, with the puma sim shipping M128, M129 and M130 to do it by hand. The realtime seam and TP2 after, and only with an analytic Jacobian: differentiating numerically is eighteen inverse calls, 2 us on a closed-form module and milliseconds on an iterative one.

    This item is travel only. [AXIS_L] also carries MAX_VELOCITY and MAX_ACCELERATION, which have the same defect a static per-axis scalar always has here: it cannot describe a bound that depends on where the rotaries are. Those come off the Jacobian in 3 rather than off the reachability query in this item, and are a stage of their own.

  7. The joint limit check moves earlier and samples inside segments. It exists already, in inRange(), but only at move endpoints and only once the block has reached motion, so a program that will overtravel says so partway into the cut rather than before it starts. With 4 in place the same check runs at load time, against the tool and the kinematics type each block will actually use. A verify button in a GUI is then a presentation of that, not a subsystem of its own.

    Two things this needs pinned before it is written. The interpreter has no seam for the query: all 66 GET_EXTERNAL_* accessors take at most an index, an axis, a spindle, a pocket, and never geometry, so one taking a segment is a new shape for that interface. And a load-time answer can go stale, since the interpreter runs ahead and motion.switchkins-type is a HAL_IN pin, so until 9 lands the type a block is checked against is not necessarily the type it runs under.

    Where a module cannot answer, cheaply or at all, the check skips rather than refuses. Refusing a reachable program leaves the user no recourse; missing an unreachable one is still caught by inRange() at runtime. That asymmetry decides every ambiguous case in 6 and 7.

    Checking a program that switches type raises a question the tree currently leaves implicit: whether a type's coordinates are geometric. Most are, and for those a scan is one stream of world points with only the inverse changing at the switch. identity is not, since world becomes the joints, so a datum in millimetres lands on a rotary joint in degrees; nothing notices, which is why switchkins.adoc asks the operator to clear the offsets before switching.

    A possible direction rather than part of the plan: a kinematics type could declare its space, cartesian or joint. In practice one type is the joint space and it is the one that wants treating specially, with no offsets applied, limits that are the joint limits directly, and possibly its own syntax later rather than borrowing the axis letters. Everything else stays as it is. I have no mechanism to propose yet and would rather hear objections to the idea first.

  8. Per-mode restrictions. G28, G30, G53, G92, G52 and M6 do not mean the same thing in every kinematics type, and today each one quietly does whatever the module happens to leave it doing. Refusing outright beats guessing, and with 4 in place the control knows enough to decide rather than to be told.

  9. Retire motion.switchkins-type, once the grace period has run. Which release that is has still to be decided.

G12.1 and G13.1 are #4414, split out of this branch and standing on their own.

On tool length specifically: the offset is a nine-axis pose from the interpreter, canon adds it into the coordinate, and each rotary kinematics takes the Z component back through a HAL pin and re-applies it along the real tool direction. That round trip is exact when the config wires the pin, and silently wrong otherwise: a missing net costs the full tool length, and an X or Y offset is applied in the rotating work frame while the tool is bolted to the machine. The mechanism works; it has no owner.

Why 4 is worth the trouble: every other way of answering "will this program overtravel" builds a second model of the machine, in the verification package or in the post, and the two descriptions drift apart. If the kinematics module can be called with parameters the caller chooses, LinuxCNC can check a program against the same code that will run it, with no second model to keep in sync. That is not available to anyone whose kinematics is locked inside the control.

In this branch now

Switchkins becomes one implementation that everything talks to:

  • the dispatch splits from rtapi_app_main(), so a module can take its main from anywhere and call switchkinsInit() itself
  • millturn, xyzab_tdr_kins, xyzacb_trsrn and xyzbca_trsrn drop their private copies of the switch and call it from EXTRA_SETUP(). The generated per-comp .mak takes a <component>-extra-objs list, and switchkins.h is installed
  • switchkinscomp.comp is the out-of-tree template: TOPDIR plus #include, the pattern tpcomp and homecomp already use, so no ABI

User visible

  • The four components gain the coordinates= module parameter, their identity type now coming from kins_util.c.
  • millturn loses its in and out example pins, template scaffolding referenced by no config. fpin and fdemo stay.
  • No pin, G-code or INI name changes.

Testing

The four comp sim configs give identical positions to master through the same MDI sequence, in every kinematics type.

@grandixximo
grandixximo force-pushed the multiaxis-kins-limits branch 2 times, most recently from 9e1c9ed to 773c3e9 Compare August 12, 2026 07:22
Comment thread src/emc/kinematics/switchkins.h Outdated
Comment thread src/emc/motion/motion.h
Comment thread src/emc/nml_intf/emc_nml.hh Outdated
@grandixximo
grandixximo force-pushed the multiaxis-kins-limits branch from 773c3e9 to 34ab07b Compare August 12, 2026 08:34
Comment thread docs/src/man/man9/motion.9.adoc Outdated
Comment thread src/emc/kinematics/switchkins.c Outdated
@BsAtHome

Copy link
Copy Markdown
Contributor

What is the migration path for people having their own out-of-tree kinematics switch component?

@grandixximo

grandixximo commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

What is the migration path for people having their own out-of-tree kinematics switch component?

Their module needs no source change. G12.1 and G13.1 reach a module through the same kinematicsSwitch() the pin does, so a component that is switchable today is driven by G12.1 today without knowing G12.1 exists.

I checked rather than assumed. I built an out-of-tree component that deliberately does not use switchkins.h or switchkins.c: its own switchkins type, its own kinematicsSwitch(), its own kinstype pins, the shape somebody would have written before there was a supported way to reach the in-tree code. Against this branch:

G12.1 P1   kins-type=1, its type 1 kinematics active
G13.1      kins-type=0
G12.1 P7   its own BAD VALUE, motion stays on 0

with no source change, and no deprecation warning, since the pin is never written.

The config keeps working as well. The pin is deprecated, not gone, so an existing M68/M66 config runs as before with one warning per session. Moving off it is a config edit, not code:

M68 E3 Q1 / M66 E0 L0    ->   G12.1 P1
M68 E3 Q0 / M66 E0 L0    ->   G13.1

and the [HAL] net from the analog output goes with them. The sim configs in this PR are worked examples, halui buttons and remapped M-codes included.

One group does have something to do, and it is not the one you asked about: anybody keeping a copy of switchkins.c in their own tree. Their current build is unaffected, but rtapi_app_main() has moved out to switchkins_main.c, so next time they re-sync that copy they either take that file too or call switchkinsInit() themselves. They also no longer need the copy, since switchkins.h is installed now and switchkinscomp.comp shows both routes.

Announcing it is worth raising at a weekly meeting. Nothing is removed in this PR.

Comment thread docs/src/gcode/g-code.adoc Outdated
Comment thread src/emc/kinematics/5axiskins.c Outdated
Comment thread src/hal/components/switchkinscomp.comp
@Sigma1912

Copy link
Copy Markdown
Contributor

Just a thought:
Work offsets in one kinematics are usually not applicable in another one. So when using switchable kinematics I have often found it would be nice to have different sets of G54..G59 offsets.
Maybe a parameter offset set by the 'motion.kins-type' value?

@grandixximo

Copy link
Copy Markdown
Contributor Author

That matches what switchkins.adoc already warns about: coordinate system offsets are described as generally applicable only to the type 0 startup kinematics, with advice to reset them or update them before switching. So the problem is real, and today it rests on the operator remembering.

Can I ask what you switch between, and whether you cut in both types? I am trying to tell two situations apart. If both types leave the axis letters describing the same physical point, then your G54 values are still correct after the switch, and what is missing is having a different one become active. If one of the types is identity, the stored numbers stop describing anything, since X becomes joint 0 and may not even carry the same units, and then a different set does not help either; the values want clearing.

Which of these is it for you: does the zero point of the part sit somewhere different once you switch, or is the part zero the same and you simply want a different one of G54 to G59.3 selected?

On the mechanism, do you mean an association living in the INI, a coordinate system named per kinematics type? There is nothing like that today. Or do you mean it read at runtime? kins-type is a status pin, so it reports rather than commands, but you could net it into a motion analog input and branch on it with M66 in a sub, which would work now. I am unsure whether that belongs in the core or stays the sort of thing an integrator wires up, and your answer would tell me which problem I am actually looking at.

@Sigma1912

Copy link
Copy Markdown
Contributor

Which of these is it for you: does the zero point of the part sit somewhere different once you switch, or is the part zero the same and you simply want a different one of G54 to G59.3 selected?

Here are two examples, the tool may cut in all kinematic modes.

Example1: Serial manipulator arm
https://github.com/LinuxCNC/linuxcnc/tree/master/configs/sim/axis/vismach/melfa-sim
To overcome singularities in industrial robots we need to be able to switch between joint and cartesian mode inside a gcode program. Here the values as well as the units change from one kinematic mode the other.

Example2: Tilted work plane
https://github.com/LinuxCNC/linuxcnc/tree/master/configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating
The switch to TOOL/TWP mode requires a transformation of the offset values from one coordinate frame to another (rotated AND translated). The new work offset (that describes the same point on the work piece) has thus different values that must be activated after the switch. The original offsets must be restored after switching back.

Currently I have two ways of dealing with this:

  1. Keep the offsets in separate WCS systems (eg use G54 for one and G59 for the other mode). Switching from one to the other is easily handled in the custom M command used to switch the kinematics

  2. Stash the offset values in parameters (so they can be restored when switching back) and then overwrite the old ones.
    This can also be handled in the custom M command but requires much thought to ensure that the correct offset values are restored in all circumstances. Generally I VERY much dislike to alter values in the offset table in the background. There is a lot that can go wrong (eg in case of an abort)

On the mechanism, do you mean an association living in the INI, a coordinate system named per kinematics type? There is nothing like that today. Or do you mean it read at runtime? kins-type is a status pin, so it reports rather than commands, but you could net it into a motion analog input and branch on it with M66 in a sub, which would work now.

Maybe a different set of G54...G59.3 values. One set for each kins-type.
I have also thought about having G54...G58 for kins-type 0, G54.1 ... G58.1 for kins-type 1, G54.2 ... G58.2 for kins-type 2 and so on (for backwards compatibility G59..g59.3 maybe the same for all)

There are also some other things to think about when working in different kinematic modes ( eg G28 , G30, G53, G92/G52, M6) it might be useful to have way of blocking certain Gcodes / Axiswords in a given mode.
I have made a patch that allows blocking certain axis letters at runtime using a magic-comment. (Just as an illustration, not suggesting to use it)
ca9c334

@grandixximo

Copy link
Copy Markdown
Contributor Author

Both examples are clear and they moved my view: I had been assuming identity was mostly a jogging mode, and you cut in all modes. I looked at how the commercial controls handle both cases, and in each of them the closest thing we could do in LinuxCNC turns out not to be an offset set.

For the singularity case, the target stays in Cartesian coordinates and only the interpolation changes: a move that runs the inverse once at the endpoint and then interpolates the joints linearly to reach it. Coordinates and units never change, so the offsets keep applying and there is nothing to switch. In tree that is a new move type from canon into the trajectory planner, and the existing KINEMATICS_INVERSE_FLAGS already carry enough to pin which solution branch it lands on. G-code would need a way to ask for it.

For the tilted plane, an extra stage in the offset chain rather than new values in it: a frame composed on top of the active G54, with its own cancel, so the blocks in between are programmed in the tilted plane while G54 itself is untouched. That is a stage in rotate_and_offset_pos() plus the modal state to go with it. Nothing is rewritten, so nothing needs restoring and on_abort_with_twp_reset.ngc has nothing left to do. Orienting the tool normal to the plane, your G53.1, does need the kinematics to say where the tool points and to solve for the rotaries, which is item 3 above.

On having more of them: that part has plenty of precedent and I would be happy to look at it separately. Fanuc adds G54.1 P1 to P48, or to P300 on the 30i, and Siemens adds G505 to G599. What I cannot find anywhere is a control that ties a set to the active kinematics or transformation. Selection is always explicit in the program, or it belongs to a channel or path. So more coordinate systems yes, but I would rather not bind them to the kinstype.

Which leaves me reading one set per kinstype as paying for the two absences above rather than as the thing you actually want. You have run this in production and I have not, so I would rather hear where that is wrong. Would a composing frame cover your case 2, or would it miss something?

Your other point stands either way. Per-mode restrictions on G28, G30, G53, G92 and M6 look right to me, and refusing outright beats guessing.

@Sigma1912

Copy link
Copy Markdown
Contributor

Nice that you have an overview of what other controllers are doing. Makes more sense adopting something that is established rather than coming up with something quirky that falls on our feet somewhere down the road.
My big deficit (or at least one of them) is that I often have zero experience how things work
on commercial machines so I end up reinventing the wheel and often do it in a slightly odd manner because I don't really know what I'm doing. :)

Which leaves me reading one set per kinstype as paying for the two absences above rather than as the thing you actually want. You have run this in production and I have not, so I would rather hear where that is wrong. Would a composing frame cover your case 2, or would it miss something?

That is how I started out, an offset built into the kinematics. The reason I moved away from that was that in it's current form LinuxCNC is totally unaware of what is going on in a custom kinematics.
So if we can handle things like G53 etc. internally then I'm probably happy with not having different sets of offsets.

Basically what I'd like is to NOT have to tamper directly with the values in the offset table.

@grandixximo

Copy link
Copy Markdown
Contributor Author

That is the same conclusion from the other side, and it names the dependency. A composing frame is only worth having if motion knows the frame is there; otherwise G53 and G28 keep meaning whatever each module happens to do with them, and we are back to the operator remembering. That is items 3 and 4 above, which is why they come before anything in the offset chain.

What "aware" has to mean concretely: the module answers where the tool points and how joint motion maps to world motion, both as plain functions of the joint values and a parameter block, with no state hidden in HAL pins. Once the control can call the kinematics rather than only drive it, G53 resolves in machine axis space, the per-mode restrictions you listed get checked rather than guessed, and a frame composes on top of G54 with nothing rewriting the table.

So no per-kinstype offset sets, nothing tampering with the table in the background, and the tilted plane becomes a frame that cancels. Your two examples are the cases I will hold it to.

@Sigma1912

Copy link
Copy Markdown
Contributor

Sounds great!

@grandixximo
grandixximo force-pushed the multiaxis-kins-limits branch 2 times, most recently from ffd07b8 to fea53f7 Compare August 19, 2026 09:51
@grandixximo

Copy link
Copy Markdown
Contributor Author

One more thing the tool round trip does not cover, found while giving 5axiskins and maxkins the .tool-offset pin every other rotary module has.

Both modules add the W coordinate to the pivot to tip distance, so W is already a tool-length route: G43.1 W5 followed by a block that commands W0 puts the kinematics on the right radius and lands on the same joint solution as a Z-column offset does. It is easy to miss, because G43.1 W5 on its own changes nothing. convert_tool_length_offset() pushes w_current down by the offset, canon adds it back, and a block with no W word hands the module zero. Command W once and it is correct from there on.

So on these two modules the tool length has three spellings: the W column with W commanded, the sum2 into pivot-length that the bridge mill sim uses, and now a .tool-offset pin. They all compute the same radius and they all add, so using two double counts. None of them is written down anywhere, which is the actual problem.

This is the same shape as the rest of items 3 and 4. Each module infers the tool from whatever it happens to be handed, every config picks a different one of the three, and nobody owns which is meant. Once the tool arrives as a parameter at the call site there is one answer instead of three.

@Sigma1912

Copy link
Copy Markdown
Contributor

I have a set of kinematics that probably cover most 5 axis machine (tool side rotation, work side rotation and mixed) including nutating joints and tilted work plane. I have long wonder if I should add those to the collection:
https://github.com/Sigma1912/LinuxCNC_Demo_Configs/tree/main/5axis-twp

@grandixximo

Copy link
Copy Markdown
Contributor Author

Yes, and I would take the kinematics first.

The nutating ones are what items 3 and 4 need most. A nutating head is where "where does the tool point" stops being obvious, so a weak default every module answers is only worth having if something in tree can break it. Today the only nutating configs are the two trsrn ones, which are yours already.

On the TWP remap I would rather you decide with item 5 in front of you. I am proposing to move that mechanism into rotate_and_offset_pos() as a frame that composes on top of G54 and cancels, which would leave the Python doing less or nothing. That does not make the configs useless, the opposite: they are the executable spec for what the frame has to do, and the baseline for checking whether a C version matches. But adding them as tree configs while I am proposing to replace the mechanism should be a decision rather than a side effect. I would rather have them in than not, and you know the ground better than I do.

One request if the kins come in: could they take the tool length through a .tool-offset pin the way the trt modules do? I have just been through this on 5axiskins and maxkins and found three spellings of the same quantity with none of them written down. Three more modules each picking their own makes that worse, and settling it now is cheap.

@Sigma1912

Copy link
Copy Markdown
Contributor

I'm not in hurry it's been sitting here for a while now.
I have no problem making adjustments to make it fit better.

@grandixximo
grandixximo force-pushed the multiaxis-kins-limits branch 2 times, most recently from d32027c to 87c645b Compare August 22, 2026 08:00
@grandixximo

grandixximo commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

I had a go at the split on the two trsrn configs in tree, to see how it lands: https://github.com/grandixximo/linuxcnc/tree/twp-remap-split

Your generic remap.py plus a remap_funcs_twp.py beside each config. Your two snrtr files carry exactly the two branches the tree had inline, xyzacb-snrtr is the (C, A) maths and xyzbca-snrtr is the (C, B) maths, so it was mostly substitution. Adapted for the tree: ini read through linuxcnc.ini, kinematics switched with G12.1, and kins_set_values converts the two joint angles to degrees because the modules in tree read those pins in degrees. Pin names unchanged, so neither .comp is touched.

I ran both configs through eight orientations under G53.1 P0/P1/P2, G53.3, G53.6 and G68.3 before and after, and it looks right: nothing that worked stopped working, and P1, P2, G53.6 and G68.3 now do something where they used to fail or silently no-op.

Three things I hit in your files:

xyzbca-snrtr has the wrong default tool-x. Both files aim for the same thing in the comment, the oriented x-vector parallel to the machine xy-plane. Tool x is the first column of Rp*Rs*Rz(tc), so that is entry [2][0] == 0, and since Rp is Rz only the bottom row of Rs enters:

bottom row [-Sv*Ss, t, s]  ->  tc = atan2(Sv*Ss, t)
bottom row [t, Sv*Ss, s]   ->  tc = atan2(-t, Sv*Ss)

xyzacb-snrtr has the second row and the second formula and is right. xyzbca-snrtr has the first row and the second formula, so it orients tool y horizontal instead. Looks copied from the sibling. Invisible in use, which is presumably why it survived: the tool still points where it was told to, only x and y in the tilted plane swap, so G68.3 R starts a quarter turn off. I used the tree's form in the port.

xyzacb_snrtr_kins.comp declares hal_float_t *virtual_rot; twice, lines 34 and 38, so it does not compile.

xyzacb-snrtr/remap_funcs_twp.py, kins_calc_primary, vertical tool branch sets theta_1 = 0 and then returns the empty list, so any G53.n to vertical fails. xyzbca-snrtr says return [0] there. I took return [0].

The commit is authored by me at the moment and the code is yours, so I would set you as author before it goes anywhere. Happy either way: take the branch and send it yourself, or tell me it looks right and I will.

@Sigma1912

Copy link
Copy Markdown
Contributor

Thanks for looking through this. I'll need to have a good look and report back.

@Sigma1912

Copy link
Copy Markdown
Contributor

I have found a problem with G13.1/G12.1 when either is in a 'on_abort' call.

Testing with configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp

This config has [RS274NGC]ON_ABORT_COMMAND = o <on_abort_with_twp_reset> call

I have noticed strange behavior with the remapped gcodes after issuing MDI commands that trigger this routine. For example, it looks like the g682 function gets stuck on yield INTERP_EXECUTE_FINISH

You can get an indication of the issue if you uncomment the first line in the on_abort sub:

o<on_abort_with_twp_reset> sub
(msg, on_abort START)    ; <<<< UNCOMMENT this line so the msg shows on start
M68 E2 Q0       ; reset twp-state to 'undefined' (0)
G13.1           ; back to identity kins
G64 P0.01       ; reset the toolpath tolerance as this sometimes gets set to zero on estop events
G54             ; switch to G54
(msg, on_abort END)
o<on_abort_with_twp_reset> endsub
M2

Now start the config, clear estop and switch the machine on which calls the on_abort routine. Note though that it does apparently NOT reach the end since we don't get the message on the last line:

on abort

Further testing has shown that having either a G13.1 or G12.1 P0 in the on_abort routine triggers the problem while the former M68 E3 Q0 does not.

@grandixximo
grandixximo force-pushed the multiaxis-kins-limits branch from 87c645b to 5bf0103 Compare August 23, 2026 14:11
@BsAtHome

Copy link
Copy Markdown
Contributor

If you want to be on the safe side, then you need to round to the statistical bound, which is 8 decimal digits or half the binary digits

For a casual programmer like me, is that a general rule of thumb for any language (eg python)?

Just for starters, normally a casual user/programmer does not care. Only when it starts to hurt, then you need to care and act.

It is in principle applicable to all systems that use a non-exact representation. The problem is always repeating fractions and irrational numbers. Note that repeating fractions depend on the number base used (number base 2 represents 1/10 as a repeating fraction, but number base 10 is exact in that case).

The "8 digits" come from the statistics of error generation and propagation. When you try to converge a calculation, then the accuracy bound is typical sqrt(floating_point_accuracy). And yes, it is a rule-of-thumb, but i can be backed up. It can be improved when you know exactly how errors are generated and propagated. For example (last time I looked), the Fast Fourier Transform library fftw uses some smarts that makes it have about 12 decimal digits of accuracy.

And, yes, Python has the double precision floating point as underlying type, so it suffers from the same error propagation problems as most other programming languages. Python has the fractions and decimal modules for specialized calculation when normal floating point is insufficient.

Mostly, you can get away with some EPSILON calculation. You often see that in direct or indirect comparison with zero (0.0) because matching zero exactly is very hard:

#define EPSILON 1e-8

if(fabs(valuea - valueb) < EPSILON) {
    // Both valuea and valueb are closer together than EPSILON
}

Similar tricks you can also apply when working with asin/acos and friends.

@grandixximo

Copy link
Copy Markdown
Contributor Author

Both fixed and pushed.

The print was mine. gcodemodule.cc is empty now like the canon stubs around it. saicanon.cc had the same line, and there it should report, but through ECHO_WITH_ARGS like the rest of that file rather than a raw printf.

The arc problem is bigger than the case you found. Putting every reachable orientation back through kins_calc_possible_joint_angles, primary and secondary swept in five degree steps, failures out of 5184 are 64, 44, 47, 25, 107 and 20 at nutation 15, 30, 45, 60, 75 and 90. So it is not about 90 degrees: it fails at the nutation the configs ship with, and 90 is one of the milder ones. Every failure is in kins_calc_primary at primary 0 or 180, so every failing tool vector has a component at zero, which is what G68.2 ... I0 asks for.

Worth fixing this much, then, even though the python is on its way out: the replacement has no caller yet, so these configs are what ships meanwhile, and a clamp and an early return leave nothing to unpick. Going through the whole file for error propagation I would leave.

@BsAtHome thank you, that is the right diagnosis. I clamped rather than rounded. The argument is exactly plus or minus one in exact arithmetic, so pulling it back when it is already within a rounding error is exact where it applies, and anything further out is an orientation the machine cannot reach and should still raise; rounding to the statistical bound would swallow those too. 1e-9 for the width, against an observed overshoot of about 2e-16.

One more thing turned up while testing: kins_calc_possible_joint_angles logs the failure and then falls through to return (theta_1_calcd, theta_2_calcd) with that variable never assigned, so the domain error arrives as an UnboundLocalError on top of the real one. It returns no solution now, which is what the caller already handles.

With both changes all 5184 orientations solve at every nutation angle tried, on both machines.

@Sigma1912

Copy link
Copy Markdown
Contributor

The commit is authored by me at the moment and the code is yours, so I would set you as author before it goes anywhere. Happy either way: take the branch and send it yourself, or tell me it looks right and I will.

Apologies, forgot, just go ahead. Glad if it finds a use somehow.

G12.1 P- selects one of the kinematics offered by a switchable
kinematics module and G13.1 cancels back to kinematics 0.  Both are
queue synchronisation points, so no motion is ever planned in one
kinematics and executed in another.

Until now the only way to switch from a program was to write
motion.switchkins-type through an analog output and force a sync by
hand, typically M68 E3 Q1 followed by M66 E0 L0, wrapped in a
subroutine or a remapped M-code.  That also costs the #5399 variable
on every switch, because M66 writes it.

G13.1 cancels to kinematics 0 rather than restoring whatever was
selected before, which is how every other cancel in the language
behaves and keeps a block's meaning independent of the path taken
through the program.  To put back a caller's selection, read
#<_kins_type>:

    #<saved> = #<_kins_type>
    G12.1 P2
    ( ... )
    G12.1 P#<saved>

Nothing cancels the selection implicitly.  It survives program end and
abort so that the kinematics keeps matching the position readout, since
switching re-derives world position from the joints and would otherwise
move the readout while the machine stands still.

Motion takes the G-code request and the motion.switchkins-type pin on
their edges, so whichever asked most recently wins and a config can use
either or both.  Writing the pin from motion instead does not work: the
configs source it from an analog output that would put its own value
back on the next servo cycle.  motion.kins-type reports the selection
now in force.

Q was parsed and carried all the way to motion without anything ever
reading it, so it is gone.

EMC_ADJUST_KINS_OFFSET_DATA is registered in the NML format and name
tables and has the update() its declaration promised, without which the
message could not cross the channel.
The interpreter tracked the kinematics it had selected itself, which is
not always the one motion is running.  An abort clears the interpreter
list, so a G12.1 that was queued but not yet sent is dropped while the
interpreter keeps the type it converted.  A config that drives
motion.switchkins-type from HAL changes the kinematics without the
interpreter hearing about it at all.  Either way #<_kins_type> reports
something that is not running, and the save and restore idiom

  #<saved> = #<_kins_type>
  G12.1 P3
  ( ... )
  G12.1 P#<saved>

puts back the wrong kinematics.

Carry the kinematics motion is running up into status and read it back
in Interp::synch(), which already runs after an abort and after every
completed switch.  Task no longer writes the requested value into
status, so the field has a single writer and always reports what motion
is actually running.
handle_kinematicsSwitch() assigned the requested type, published it on
motion.kins-type, stored it in the status, and only then asked the
module to switch.  A module that refuses a type it does not provide
goes on running the one it has, so the readout named a kinematics that
was not in force, and G12.1 P#<_kins_type> put that wrong number back.

Ask first, record after.  A refused switch leaves the type, the pin and
#<_kins_type> on the kinematics still running, and still raises the
motion error.  The refusal reached the operator as nothing at all, only
a line in the realtime log, which was survivable while switching came
from HAL and is not once a G-code block can ask: say which type was
refused and which one is still running.  The failure message names the
type that was asked for rather than the HAL pin, which is not where the
request came from when it came from G-code.

G12.1 P7 on xyzab_tdr_kins, which provides two types, left
motion.kins-type reading 7 while kinstype.is-0 stayed true.  It reads 0.
motion.switchkins-type cannot be the general way to choose kinematics.
The interpreter never sees it, so a program is read, its limits
checked and its path looked ahead in whatever kinematics the
interpreter last knew about, which need not be the one that ends up
running it.  Nothing in the pin can fix that; the interpreter has to be
told, which is what G12.1 and G13.1 are for.

Motion says so once per session, the first time the pin is used to
change the type.  A configuration that never switches never sees it,
and the G-code route never triggers it.  The pin is in a grace period:
it keeps working for now, and is meant to go.

Both the man page and the switchkins chapter claimed G12.1 and G13.1
write this pin.  They do not, and cannot: the configs source it from an
analog output that would put its own value back on the next servo
cycle.  They ask motion directly.
The G12.1 plumbing arrived from the out-of-tree patch with names that
describe nothing.  `adjustKinsVar0` is the kinematics type, there is no
Var1, and nothing adjusts an offset.  `kinsType` is not a type at all:
it was a char toggling between 'r' and 's' so the servo cycle could
notice that a new request had arrived.  The field named like a type was
a flag and the field with the opaque name was the type.

So:

  adjustKinsVar0              -> switchkins_type, an int
  kinsType ('r'/'s' toggle)   -> switchkins_seq, a counter
  trajKinsType                -> switchkins_seq in EMC_TRAJ_STAT
  trajKinsTypeModified        -> switchkins_changed in EMC_TRAJ_STAT
  ADJUST_KINS_OFFSET(double)  -> SELECT_KINS_TYPE(int)
  EMC_ADJUST_KINS_OFFSET_DATA -> EMC_TRAJ_SELECT_KINS
  EMCMOT_ADJUST_KINS_OFFSET_DATA -> EMCMOT_SELECT_KINS_TYPE
  emcAdjustKinsOffset()       -> emcSelectKinsType()

switchkins_type rather than kinsType because EMC_TRAJ_STAT already has
kinematics_type, which is the identity/serial/parallel/custom kind and
a different thing entirely.  switchkins_type is what the HAL pin and
switchkins.c already call it.

The three status fields were prefixed traj but lived in EMC_MOTION_STAT.
They are trajectory status, so they move into EMC_TRAJ_STAT and lose the
prefix, which also means EMC_TRAJ_STAT::update() carries them.

A counter instead of a two-state toggle keeps the property the toggle
had, that asking for the type already in force is still seen as a
request, without pretending to be an enum.

No G-code, HAL pin or INI name changes.
The four sim configs whose kinematics components now use the switchkins
core chose their kinematics by writing motion.switchkins-type through an
analog output, the route motion has just deprecated.  Each of them would
have met the user with the deprecation warning the first time they
pressed a kinematics button.

The M428, M429 and M430 remaps, the TWP wrappers behind G53.1, G53.3,
G53.6 and G69, the abort handler and remap.py now use G12.1 and G13.1.
That drops the M66 sync either side of every switch, the test that the
HAL pin exists at all, and the #5399 clobber each M66 costs, since G12.1
and G13.1 synchronise interpreter and motion themselves.  The check that
the switch took reads #<_kins_type> instead of the pin.

millturn keeps the M66 at the end of M428 and M429.  That one is not
there for the switch: M128 and M129 change the axis limits from a Tcl
script, which reaches motion through inihal, so read-ahead has to stop
until the new limits have landed.

The vismach guis for the two trsrn configs were reading the value
requested through the analog output.  They now take motion.kins-type,
which is the kinematics actually in force.

Eight other sim config directories still select kinematics from HAL:
bridgemill, table-rotary-tilting, hexapod-sim, melfa-sim, puma, and the
three copies of scara.  They are untouched here, and still work.
The rest of the sim configs that shipped with switchkins chose their
kinematics by writing motion.switchkins-type through an analog output,
which motion now reports as deprecated: bridgemill, table-rotary-tilting,
hexapod-sim, melfa-sim, puma and the three copies of scara.

Same change as the comp sims got.  The M428, M429 and M430 remaps use
G12.1 and G13.1, which drops the M66 sync either side of every switch,
the test for the hal pin, and the #5399 clobber each M66 costs.  The
check that the switch took reads #<_kins_type>.  The [HAL] net from
motion.analog-out-03 goes with them, and the two halshow watch lists
follow motion.kins-type instead of the pin that used to drive it.

No sim config selects kinematics from HAL now.
The G-code chapter told the reader a config may select the kinematics
"from G-code, from that pin, or from both", and the switchkins chapter
said the same twice, in its introduction and again under G-code
commands.  All three predate motion reporting the pin as deprecated, and
they contradict it.

They now say the pin is deprecated and why, in the same words as the man
page.  The G-code chapter keeps the fact that the pin takes the same
numbering, which is what somebody migrating away from it needs to know.
The paragraph read as though the pin were an equal alternative that
happened to carry a caveat.  State the deprecation first, as a warning.
switchkins.c owned rtapi_app_main(), so a module could only use it by
having no main of its own.  That ruled out halcompile components, which
is why the switchable kinematics in hal/components each carry a private
copy of the dispatch, the kinstype pins and the switch statement.

Move rtapi_app_main(), rtapi_app_exit() and the coordinates= and sparm=
module parameters to switchkins_main.c, and give switchkins.c a single
entry point:

    int switchkinsInit(const int comp_id, kparms* kp, const char* coordinates);

It counts and validates the registered types, creates the pins and
starts on type 0.  The caller owns the hal component, doing hal_init()
before and hal_ready() after, so anything that already has a component
can use switchkins by calling this.

The types switchkinsSetup() supplies now reach the arrays through
switchkinsRegister() like any others, rather than being written
directly through its out parameters.  One registration path means the
checks apply to every type, so a module that both fills an argument and
registers the same type is refused rather than silently overwriting.

The eight existing modules gain switchkins_main.o in their -objs and
are otherwise untouched.
millturn, xyzab_tdr_kins, xyzacb_trsrn and xyzbca_trsrn each carried
their own copy of the switchkins dispatch: a private switchkins_type, a
kinematicsSwitch() with a hand-written case per type, and a setup
routine that had to hal_set_unready() the component again because it
ran from kinematicsType(), long after halcompile had called
hal_ready().  Four copies of the same thing, none of them sharing the
fixes made to switchkins.c.

They could not link switchkins.o before, because switchkins.c supplied
rtapi_app_main() and so does halcompile.  Now that the dispatch is
separate from the 'main' program, a component can link it and call
switchkinsInit() from EXTRA_SETUP(), which halcompile runs after
hal_init() and before hal_ready().

Two build changes make that possible:

 - the generated per-comp .mak takes a <component>-extra-objs list, so
   a .comp can name objects besides its own.
 - switchkins.h is copied to ../include and installed, so
   <switchkins.h> resolves from a generated component source.

Each of the four now registers its kinematics types and calls
switchkinsInit().  Their identity type comes from kins_util.c, which
gets them the coordinates= module parameter they never had, and a bad
motion.switchkins-type is now rejected and leaves the running
kinematics alone instead of stranding the module on a type that does
not exist.

Pin names are unchanged, except that millturn's in/out example pins are
gone: they were template scaffolding copied from userkins.comp, unused
by the sim config, and a kinematics-type setup routine is where
kinematics pins belong now.  millturn keeps its fpin pin and fdemo
function.

The xyzab-tdr, xyzacb-trsrn, xyzbca-trsrn and millturn sim configs give
the same positions through the same MDI sequence as before, to four
decimals, in every kinematics type.
Nothing stopped an out-of-tree kinematics module from using switchkins
except that there was no way to get at the implementation, so anyone
writing one reimplemented kinematicsSwitch() and the kinstype.is-N
pins, or did without switching entirely.

switchkinscomp.comp is the template for doing it properly.  It sets
TOPDIR to a source tree and includes switchkins.c and kins_util.c,
which is how tpcomp.comp and homecomp.comp already reach the
trajectory planning and homing sources.  The module then registers its
kinstypes and calls switchkinsInit() from EXTRA_SETUP(), the same
fifteen lines the in-tree components use.

That gets an out-of-tree module the kinematics switching, the
kinstype.is-N pins, the coordinates= identity mapping and the HAL and
G-code controls, all from the one implementation, and it costs no ABI:
the sources are compiled into the module, so it is built against one
tree and rebuilt when that tree changes.

Like tpcomp, the template is not built in tree because it has no
kinematics until TOPDIR is set, so it is filtered out of COMPS and its
manpage is named explicitly.

Renamed to user_switchkins, pointed at this tree and loaded as
[KINS]KINEMATICS, it homes, switches to its example kinstype and back,
and rejects a kinstype it does not have.
switchkins.h includes kinematics.h, so a module that includes
switchkins.h does not need to include kinematics.h itself.

switchkins.c had picked up the habit along with genhexkins, 5axiskins,
pumakins, scarakins and three21kins, which had it before any of this.
Modules that do not use switchkins.h still include kinematics.h
directly, as they must.
The kinematics modules are users of switchkins, not part of it, so they
take the header the way any other user would.  switchkins.c and
switchkins_main.c keep the quoted form, being the source itself.
A realtime module cannot link a library, so an out-of-tree kinematics
module has to compile the switchkins implementation itself.  Asking it
for the path to a source tree, as the template did, leaves anybody on a
deb install with nothing to point at.

Install switchkins.c and kins_util.c into share/linuxcnc, the way
mesa_modbus.c.tmpl already is, and put that directory on the realtime
include path.  The template then reads

    #include <switchkins.c>
    #include <kins_util.c>

and builds as it stands.
gcodemodule.cc got a raw printf when SELECT_KINS_TYPE was added, so the
preview printed a line for every G12.1 and G13.1 in the program.  It is the
only live printf in the file, every other one having been commented out, and
the neighbouring canon stubs are empty.  Make this one empty too.

saicanon.cc had the same printf.  There it should report, since saicanon
exists to echo the canonical commands, but through the same macro as the rest
of the file so it lands in the canon output with a line number and the
argument rather than beside it on stdout.
@grandixximo
grandixximo force-pushed the multiaxis-kins-limits branch from add4f5c to c62465b Compare August 25, 2026 01:23
@grandixximo

Copy link
Copy Markdown
Contributor Author

@BsAtHome a shape question before I build this into a PR, since you decided half of it once already in #3807 and I would rather not relitigate it by accident.

Item 4 needs a kinematics module to be callable outside RT, so a limit check or a preview can evaluate it at poses the machine has not reached. Two ways to give it its geometry, both built and measured.

Yours from #3807: the module publishes a fixed-layout struct through hal_struct_newf() and exports nonrt_attach(), and userspace reads the snapshot. Joining costs a module five things: parameters in a shared header, the struct registered, a refresh inside forward and inverse, an if (!haldata) branch, and its maths factored to take geometry as an argument.

The other binds a non-RT copy of the module's haldata to the value cells the running RT instance already owns, so its own forward and inverse work unmodified on live values. One nonrt_attach() calling hal_get_pin_value_by_name() per pin, and nothing else.

Same two modules, loader and Jacobian excluded since they are identical: +715/-67 with 252 HAL lines and a HAL_VER bump, against +158/-10 with one HAL line. Identical limits from both, to the digit.

That one line is EXPORT_SYMBOL(hal_get_pin_value_by_name). The function exists and returns the right pointer, it was just never exported. That is what I want your read on, since handing out an interior pointer into HAL's memory points the opposite way from #4458 and from the untangling you described.

So: pin binding with that export, or the struct interface on the grounds that HAL should hand out a declared blob rather than a pointer into itself? Either is fine by me. Note hal_struct_* is not in master, it lives in #3807, so choosing it means landing that first.

@grandixximo
grandixximo force-pushed the multiaxis-kins-limits branch from 288a0a4 to d5bc500 Compare August 25, 2026 02:45
@BsAtHome

Copy link
Copy Markdown
Contributor

@BsAtHome a shape question before I build this into a PR, since you decided half of it once already in #3807 and I would rather not relitigate it by accident.

If you want to "(re-)litigate", please, let me call a defense lawyer ;-)

[snip]
That one line is EXPORT_SYMBOL(hal_get_pin_value_by_name). The function exists and returns the right pointer, it was just never exported. That is what I want your read on, since handing out an interior pointer into HAL's memory points the opposite way from #4458 and from the untangling you described.

The function hal_get_pin_value_by_name() will be retired as it is replaced by the query API.

The trick is that you can get the reference of the pin and then use that reference. There is no need to look it up by name all the time, unless there is a chance that there will be of signal connect/disconnect events while running. You call hal_getref_p() and the pin reference will be there. If you want the value, you can call hal_get_p() with the callback set to NULL and it will simply get you the value. When there is no chance of signal connect/disconnect events, then you can use the reference returned by hal_getref_p() and use the appropriate getter.

If the pins are not supposed to be connected to, then you can make sure of that. You should then not make them pins but params. You simply get the param's value references with hal_getref_p() and use the getter. The param's value references will stay valid throughout the lifetime of the component.

So: pin binding with that export, or the struct interface on the grounds that HAL should hand out a declared blob rather than a pointer into itself? Either is fine by me. Note hal_struct_* is not in master, it lives in #3807, so choosing it means landing that first.

Essentially, if we can use existing infrastructure, then that is preferable.

@grandixximo

Copy link
Copy Markdown
Contributor Author

@BsAtHome thank you, that settles it, and it agrees with what is already on the branch.

The EXPORT_SYMBOL I asked about was against a revision I had already replaced and did not check before writing. The resolver calls hal_getref_p() once per pin and keeps the reference, and the module reads it through the typed getter. git diff switchkins-unify..kins-nonrt-bind touches nothing under src/hal. So: pin binding on existing infrastructure, no HAL change, no dependency on #3807. I will build it that way.

Params are not an option here: 5axiskins.pivot-length is netted from the tool offset in the bridgemill sim, which is the case the binding has to follow.

@BsAtHome

Copy link
Copy Markdown
Contributor

Just to be clear here... When you require pins and your code is reading, then your code should create a component and a set of input pins which must then be connected to the data output pins you want have the data from. Not doing so would create a situation where you must track pin connection on every read, which means that you have to do a hal_get_p every time for the pin (and that is slow). Using your own component with pins means that you are the owner of the reference and it will always point correctly, regardless whether it is connected or not.

grandixximo and others added 4 commits August 25, 2026 22:26
Alternative to publishing a parameter snapshot in shared memory.

A kinematics module's haldata is a struct of pin handles, and a handle
is an opaque pointer to the value cell hal_get_real() reads.  So a
second, non-RT copy of the module can point its haldata at cells
carrying the same values the RT instance reads, and its forward and
inverse then work unmodified, on live values, at any pose asked for.

Per module that comes to:

  - export nonrt_attach(), which asks a caller-supplied resolver for
    each of its input pins by name, runs the same coordinate parse
    setup runs, and returns the existing forward and inverse;
  - split that coordinate parse out of setup so nonrt_attach() can
    call it without creating pins.

The kinematics math is untouched, and there is no parameter struct to
declare, no shared header that grows once per module, no snapshot to
refresh inside the servo loop, no sequence counter to get right.
Out-of-tree modules can opt in without anyone editing a header they do
not own.

The cells bound to are pins of the caller's own component, not the RT
instance's: a reference into somebody else's pin has a lifetime that
belongs to that component, and rewiring the pin would strand it.  The
loader creates one input pin per value and connects it to the signal
the RT pin reads, or, where there is none, to one it makes and removes
again on teardown.  Such a pin's reference must live in HAL shared
memory, since that is where HAL rewrites it on connect and disconnect,
so the pins are made against hal_malloc() cells between hal_init() and
hal_ready().

Name lookup lives in the loader, userspace code linked against
liblinuxcnchal and free to call hal_getref_p().  The module is an RT
object, and walking the HAL name space from one is what the HAL
isolation work is removing; it would also risk binding against rtlib's
copy of the same symbols.  No HAL change is needed: this patch touches
no file under src/hal.

Bind input pins only, or the two copies write to each other's state.
5axiskins has no output pins and no scratch storage, so its non-RT
haldata is one static struct.  trivkins needs no binding at all, only
a statement that joints are axes.

Verified against the struct-snapshot version of this work: for the
same pivot length, kinslimits reports identical caps to the digit.
With 5axiskins.pivot-length set by setp and no motion thread running,
this version reads what was set while the snapshot version reads the
setup default, a snapshot refreshing only when RT calls forward or
inverse.  With the pin netted, the bound copy tracks the signal.
The remap carried the geometry of every supported machine inside itself, as
branches on the (primary, secondary) joint letter pair in kins_calc_secondary,
kins_calc_primary, kins_tool_transformation and kins_calc_tool_rot_c_for_horizontal_x.
Adding a machine meant adding a branch to each, and a machine whose maths did
not fit that shape could not be added at all.

The generic half now lives in remap.py and the machine half in a
remap_funcs_twp.py beside each config, pulled in with a plain import.  Eleven
functions form the interface, the ones the generic side needs to ask a machine:
which joint angles reach a tool orientation, how to build the transformation
matrix, what the default tool-x direction is, what to write on the module pins.
The two configs here supply their own, so the (C,B) and (C,A) branches that were
interleaved in one file are now one file each.

The generic remap.py and the machine files are David Mueller's, from
https://github.com/Sigma1912/LinuxCNC_Demo_Configs/tree/main/5axis-twp, where
this separation was worked out.  His snrtr modules carry exactly the two
branches this config pair needs.  Adapted here: the ini is read through
linuxcnc.ini rather than configparser, the kinematics switch is G12.1 rather
than a write to the deprecated motion.switchkins-type pin, kins_set_values
converts the two joint angles to degrees because the modules in tree read those
pins in degrees, and the pins keep their existing names, so neither module
changes and no config has to be rewired.

Three behaviour changes come with it, all of them the machine doing what was
asked where it previously did not.

kins_calc_primary appended its result outside the loop over candidate secondary
angles, so only the last candidate ever contributed a primary angle and the
solution set was half the size it should be.  With the full set, G53.1 P1 and
P2 find the positive-only and negative-only solutions they were asking for
instead of failing, and P0 sometimes picks a shorter move: on xyzacb-trsrn one
of the test orientations is now reached with the primary at -73.87 degrees
rather than 130.25, the same tool vector to nine decimal places.

Candidate angles were compared in radians against limits read in degrees, so
the limit test was meaningless for anything outside plus or minus 57 degrees.

On xyzbca-trsrn, G53.6, G68.3 and one G53.3 case reported success while leaving
the kinematics in identity with the module pins holding values from whatever ran
before.  They now activate the tilted work plane.

Verified by driving both configs through eight orientations under G53.1 P0, P1
and P2, G53.3, G53.6 and G68.3, and comparing against the same run before the
change.  Where a different joint solution is chosen the resulting tool vector is
identical to within 1e-9.  No case fails that used to work.
kins_calc_primary filtered its candidates against the secondary joint's
limits.  The function declares primary_min_limit and primary_max_limit as
globals and then does not use them, so the intent is not in doubt.

It is invisible on both configs in tree, where the primary C is the wider of
the two: the runs are byte-identical before and after over eight orientations
under G53.1 P0/P1/P2, G53.3, G53.6 and G68.3 on each machine, 36 commands and
no errors either way.  It bites the other way round, on a machine whose
primary is tighter than its secondary, where reachable orientations are
rejected for exceeding a limit belonging to the other joint.
Asking for a tool vector that lies in a principal plane raises "math domain
error" from asin() in kins_calc_primary.  The vector reaching that point is a
column of a product of rotation matrices, so it is a unit vector only to
within rounding, and where one of its components is zero the argument lands on
plus or minus one with a rounding error on top and falls outside the domain.

Round tripping every reachable orientation, primary and secondary swept in
five degree steps, gives 47 failures out of 5184 at the configured nutation of
45 degrees on both machines, and it does not need an unusual nutation angle to
appear: 15, 30, 45, 60, 75 and 90 degrees all fail, between 20 and 107 times.
Every failure is a tool vector with a component at zero, which is what a
G68.2 with I0 or J0 asks for.

Clamp the argument where it is within a rounding error of the limit, and leave
anything further out to raise, because that is an orientation the machine
cannot reach rather than an arithmetic artefact.  With the clamp all 5184
orientations solve at every nutation angle tried, on both machines.

kins_calc_possible_joint_angles logged such a failure and then fell through to
return a variable it had never assigned, so the domain error arrived as an
UnboundLocalError over the top of it.  Return no solution instead, which is
the answer the caller already handles.
@grandixximo
grandixximo force-pushed the multiaxis-kins-limits branch from d5bc500 to 78c5727 Compare August 25, 2026 13:25
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.

3 participants