Nozzle pre-rotation/parallel motion on all axis - implementation details

106 views
Skip to first unread message

Jan

unread,
Apr 5, 2024, 9:12:15 AMApr 5
to OpenPnP
Hi Mark!
As suggested, this is a separate continuation of
https://groups.google.com/d/msgid/openpnp/a4fef4f6-f005-4de2-a5a0-5ab9d645d750%40makr.zone
in a new thread.

On 05.04.2024 13:21, 'mark maker' via OpenPnP wrote:
> Maybe you should open a new thread for this. 😉
>
> Regarding the merging:
>
> Good question. I haven't thought this out.
>
> Maybe we need to reverse it like so:
>
> Usage in JobProcessor would be the other way around:
>
> for each (upcomingPlacement : upcomingPlacements) {
>
upcomingPlacement.nozzle.moveTo(upcomingPlacement.targetLocationOfStep,*MotionOption.Subordinate*);
> }
>
>
currentPlacement.nozzle.moveTo(currentPlacement.targetLocationOfStep);
>
Thats what I have. It provide a convenient way to merge multiple
subordinations and let the next non-subordinate motion take precedence.

> Then in the AbstractMotionPlanner.moveTo()
>
<https://github.com/openpnp/openpnp/blob/dd58cf65b0465b426bef8ebd7b87cc7b767ddf7f/src/main/java/org/openpnp/machine/reference/driver/AbstractMotionPlanner.java#L149>
if the MotionOpen.Subordinate is given, you only record the subordinate
location temporarily. Only record the subordinate axis-coordinates that
have changed vs. currentLocation. Do not really plan the motion yet,
i.e., stop after simply storing the subordinate location in the
AbstractMotionPlanner.
>
That's an idea I had too. However, it has the disadvantage, that
uncommitted subordinations might starve while the motion queue is
executed. If you agree, I'll implement it anyhow and we'll see how this
starvation can be handled gracefully. Likely queued subordinations can
be dropped as part of the execution.

> Multiple calls with MotionOpen.Subordinate are combined by
> AxesLocation.put() into the temporary subordinate location. NOTE:
> machines with shared Z or C axes might overwrite the same physical axis
> multiple times. You have to decide which one wins: the first or last
> caller i.e. which is to be the left or right argument of put().
>
Ok, we'll see how that can be handled. I guess either way is neither
just good nor bad. An option to disable pre-rotation is likely a
valuable way out.

> Then when the next _non_-subordinate moveTo() comes, you take the
> subordinate location and put() it into the newLocation and then put()
> the new _non_-subordinate axesLocation on top.
>
Ok, thats easy.

> Then also reset the recorded subordinate location, so it starts empty
> for the next round.
>
> Note this also solves the backlash compensation problem, as it comes
> before it is even determined.
>
> Much cleaner like that! 😎
>
Indeed. The code to handle that is already very lengthy and hence rather
error proven. You suggestion to queue them before it much easier to
document and understand.

Jan

PS: I'll keep implementing this, however, I can't push a PR for further
review and discussions yet because my code is based on PR #1614 (sort
placements rewrite).

mark maker

unread,
Apr 5, 2024, 9:50:12 AMApr 5
to ope...@googlegroups.com

> However, it has the disadvantage, that uncommitted subordinations might starve while the motion queue is executed.

What do you mean by "starve"?

Do you mean where there is no non-subordinate moveTo() before the next waitForCompletion()?

I would say we should not allow for this to happen. You should always record the subordinate motion before doing a non-subordinate motion, which is like a "commit" for the combo. IMHO, it only works that way, because we need to know which coordinates/axes are non-subordinate, i.e. those four (X Y Z C) coming from the "commit" moveTo().

In fact, now that I think about it, you should even actively reset the recorded subordinate location in waitForCompletion(), so it isn't executed on the next moveTo() if it wasn't properly committed.   

Or worse, so it isn't even inherited to the next machine task if a an exception happened! Otherwise this could lead to completely unexpected and potentially dangerous moves! 

Note that org.openpnp.spi.base.AbstractMachine.submit() handler always calls waitForCompletion(), even in case of exception, so this is granted.

_Mark

Jan

unread,
Apr 5, 2024, 11:14:31 AMApr 5
to ope...@googlegroups.com
On 05.04.2024 15:50, 'mark maker' via OpenPnP wrote:
> /> However, it has the disadvantage, that uncommitted subordinations
> might starve while the motion queue is executed./
>
> What do you mean by "starve"?
>
> Do you mean where there is no _non_-subordinate moveTo() before the next
> waitForCompletion()?
>
Yes.

> I would say we should not allow for this to happen. You should always
> record the subordinate motion before doing a non-subordinate motion,
> which is like a "commit" for the combo. IMHO, it only works that way,
> because we need to know /which/ coordinates/axes are _non_-subordinate,
> i.e. those four (X Y Z C) coming from the "commit" moveTo().
>
> In fact, now that I think about it, you should even actively reset the
> recorded subordinate location in waitForCompletion(), so it isn't
> executed on the next moveTo() if it wasn't properly committed.
>
That's the fear I have and I'm intending to implement active reset as
you suggested.

Jan
> --
> You received this message because you are subscribed to the Google
> Groups "OpenPnP" group.
> To unsubscribe from this group and stop receiving emails from it, send
> an email to openpnp+u...@googlegroups.com
> <mailto:openpnp+u...@googlegroups.com>.
> To view this discussion on the web visit
> https://groups.google.com/d/msgid/openpnp/b3de9134-8f5f-4700-b124-f3eec86d6c5e%40makr.zone <https://groups.google.com/d/msgid/openpnp/b3de9134-8f5f-4700-b124-f3eec86d6c5e%40makr.zone?utm_medium=email&utm_source=footer>.

mark maker

unread,
Apr 5, 2024, 11:36:00 AMApr 5
to ope...@googlegroups.com

Getting back to this:

> Ok, we'll see how that can be handled. I guess either way is neither just good nor bad. An option to disable pre-rotation is likely a valuable way out.

We'll just have to document how it behaves. I propose the following:

"Multiple moveTo(location, MotionOption.Subordinate) must be executed in the order in which they are anticipated to happen"

Then when you record the coordinates do it as follows:

    subordinateLocation = newLocation.put(subordinateLocation);

so existing subordinate coordinates overwrite new ones of the same axis, i.e., the first one recorded wins.

For machines with shared rotation axis (as an example) it means that it will prerotate to the first moveTo(location, MotionOption.Subordinate) but not the second, etc. which makes most sense.

For the JobProcessor optimizer, the need to predict the order of the moveTo()s means you need to know the nozzle sort order of the next Step. This will likely complicate things.

Or you could ignore it for now and instruct users of machines with shared rotation axes to disable the feature.

_Mark

mark maker

unread,
Apr 14, 2024, 10:53:49 AMApr 14
to ope...@googlegroups.com

Hi Jan,

Moved over from the other thread:

General remark: do not call this feature "pre-rotate". This term is already used for "pre-rotate" bottom vision (confuses me each time I read it). 

Your new feature is at least in theory completely neutral for all axes. For instance, AbstractHead.moveToSafeZ(double) could equally move all Z axes at the same time (e.g. quad nozzle machine, or dedicated Z axes machine).

>    I'm digging deeper into the mud and found some new issues, I'd like to discuss: at present I've implemented pre-rotation as separate steps in the job processor located between optimization and actual execution. For picking the pre-rotation is therefore executed even before the feed, which is nice, because under the assumption, that the move from place to pick or feed is much longer then from feed to pick, this is efficient. However, for ReferencePushPullFeeder used as drag feeders, the feed operation resets the additional rotation axis in line 422 (I've configured my peeler as such, with additive rotation). This trigger - in my current implementation - a "subordinated motion queue not empty" warning, which voids the pre-rotation. Could we move this to the end of the feed operation or handle the reset asynchronously as this axis is never used elsewhere and it's safe to reset it at any time?

The scenario you describe should not happen in real world, as your push pull actuator should either have no rotation axis mapped:

https://github.com/openpnp/openpnp/wiki/ReferencePushPullFeeder#feeder-actuator

Or an axis that is entirely different than the one of the nozzle:

https://github.com/openpnp/openpnp/wiki/ReferencePushPullFeeder#peeler-axis

I guess this happens because you just reused your regular nozzle rotation axis in simulation  🙂. Create a new axis, as you would for a real peeler, and this resetting code should be irrelevant, i.e. those two axes should not bother each other. Report back if my assumption are wrong.


Nevertheless, I still see cases where this "resetting" could happen. Often the "current location" of a HeadMountable is taken and then only certain axes altered (and therefore ultimately moved). This use case should preserve the subordinate coordinates.

See callers of AbstractHeadMountable.getLocation().

See callers of Location.derive(Double, Double, Double, Double) and Location.deriveLengths(Length, Length, Length, Double). There are other ways to do the same thing.

See also AbstractHeadMountable.substituteUnchangedCoordinates(Location, Location) where the old Double.NaN method is implemented.


The first idea is to discuss what AbstractCoordinateAxis.coordinate reflects where getLocation() ultimately gets its coordinates. Is it the current planned coordinate with or without subordinate motion? I'm quite reluctant to change this, it is used everywhere!

Should it be handled in AbstractHeadMountable.getLocation() where the ramifications can be better judged? More likely.

But it should actually be implemented in AbstractMotionPlanner with a new method MotionPlanner.getLocation() and then merely called from AbstractHeadMountable.getLocation(). Separation of concerns.

These my thoughts so far.

_Mark

To unsubscribe from this group and stop receiving emails from it, send an email to openpnp+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/openpnp/794a1f67-9468-448f-9581-99d2cc76c9d1%40makr.zone.

Jan

unread,
Apr 15, 2024, 5:14:11 PMApr 15
to ope...@googlegroups.com
Hi Mark!

On 14.04.2024 16:53, 'mark maker' via OpenPnP wrote:
> Hi Jan,
>
> Moved over from the other thread:
>
> General remark: do not call this feature "pre-rotate". This term is
> already used for "pre-rotate" bottom vision (confuses me each time I
> read it).
>
Understand. Do you have any suggestions? Especially the new step I
currently inserted into the JobProcessor to request the rotation on all
nozzles shall have a meaningful name and "pre-rotation" sounded like a
good catch...

> Your new feature is at least in theory completely neutral for all axes.
> For instance, AbstractHead.moveToSafeZ(double) could equally move all Z
> axes at the same time (e.g. quad nozzle machine, or dedicated Z axes
> machine).
>
Yes, its likely a good idea to make use of the feature there too.

> />    I'm digging deeper into the mud and found some new issues, I'd
> like to discuss: at present I've implemented pre-rotation as separate
> steps in the job processor located between optimization and actual
> execution. For picking the pre-rotation is therefore executed even
> before the feed, which is nice, because under the assumption, that the
> move from place to pick or feed is much longer then from feed to pick,
> this is efficient. However, for ReferencePushPullFeeder used as drag
> feeders, the feed operation resets the additional rotation axis in line
> 422 (I've configured my peeler as such, with additive rotation). This
> trigger - in my current implementation - a "subordinated motion queue
> not empty" warning, which voids the pre-rotation. Could we move this to
> the end of the feed operation or handle the reset asynchronously as this
> axis is never used elsewhere and it's safe to reset it at any time? /
>
> The scenario you describe should not happen in real world, as your push
> pull actuator should either have *no* rotation axis mapped:
>
> https://github.com/openpnp/openpnp/wiki/ReferencePushPullFeeder#feeder-actuator
>
> Or an axis that is *entirely different* than the one of the nozzle:
>
> https://github.com/openpnp/openpnp/wiki/ReferencePushPullFeeder#peeler-axis
>
> I guess this happens because you just reused your regular nozzle
> rotation axis in simulation  🙂. Create a new axis, as you would for a
> real peeler, and this resetting code should be irrelevant, i.e. those
> two axes should not bother each other. Report back if my assumption are
> wrong.
>
I'm afraid this is either not that easy or my understanding of the
setup/code is to limited: I do have a separate axis that's only used for
that purpose (peeler). If it's configured as "additive", at the
beginning of feed() motionPlanner.setGlobalOffsets(rotation); is called.
The first thing, that's happening there is
executeMotionPlan(CompletionType.CommandStillstand);. IIRC we discussed
earlier, that pending subordinations shall be removed if the motion
planner queue is executed. Therefore I currently have the draining in
executeMotionPlan().
Anyhow, I expect, that this is just one detail making the subordination
difficult. Therefore I'd suggest to push this fix into the future until
subordination actually works.

> Nevertheless, I still see cases where this "resetting" could happen.
> Often the "current location" of a HeadMountable is taken and then only
> certain axes altered (and therefore ultimately moved). This use case
> should preserve the subordinate coordinates.
>
> See callers of AbstractHeadMountable.getLocation().
>
> See callers of Location.derive(Double, Double, Double, Double) and
> Location.deriveLengths(Length, Length, Length, Double). There are other
> ways to do the same thing.
>
> See also AbstractHeadMountable.substituteUnchangedCoordinates(Location,
> Location) where the old Double.NaN method is implemented.
>
>
> The first idea is to discuss what AbstractCoordinateAxis.coordinate
> reflects where getLocation() ultimately gets its coordinates. Is it the
> current planned coordinate /with/ or /without/ subordinate motion? I'm
> quite reluctant to change this, it is used /everywhere/!
>
> Should it be handled in AbstractHeadMountable.getLocation() where the
> ramifications can be better judged? More likely.
>
> But it should actually be implemented in AbstractMotionPlanner with a
> new method MotionPlanner.getLocation() and then merely called from
> AbstractHeadMountable.getLocation(). Separation of concerns.
>
Hm... seems it's getting even more complicated... Initially I thought,
that subordinations could be used to request/register motion, that shall
be executed if possible. This way I thought I could just request a
movement to the pick location and when the actual feed or pick movement
is executed, the subordinations are packed on top moving only axis that
are not part of the movement that triggered the motion. With respect to
the motion planner, this would imply, that subordinate motions have to
be requested and will be added/merged to the next motion. If no such
triggering motion is coming, I think it's safe to remove all queued
subordinations.
How you said, that there are situations in which subordinations may
stay intact. If that's the case in a limited amount of situations, we
could trigger executeMotionPlan() with a different or additional
argument. Shall be easy to implement if the places where that has to
happen are known. Anyhow, if one is missing, it would be just a missed
optimization, nothing spectacular.
You also says, that the current location may reflect the subordination.
I don't understand under which conditions this could be useful. If
anyone wont's to move, it shall say where. And than that move shall be
executed based on the current location. Axis that have changed
locations, shall move to the new location.
I've implemented - as you suggested - the detection, if an axes is
moving or not by checking its current and new location. That's ok to
queue subordinations. However, for the final move, I'm not sure if I
have to handle situations, where an axes is actually requested to /not/
move. That would be encoded by currentLocation == newLocation and will -
at present - be interpreted as this-axes-is-unused. If AxisLocation
would only carry axis, that are requested to be moved, this and the rest
could be simplified.
Anyhow, I'm still fighting to setup a static use case where
"pre-rotation" is regular in place generating repeatable G-Code. It
seems there are more areas where unexpected motion is requested then I
thought...

Jan

> On 05.04.2024 17:35, 'mark maker' via OpenPnP wrote:
>>
>> Getting back to this:/
>> /
>>
>> /> Ok, we'll see how that can be handled. I guess either way is
>> neither just good nor bad. An option to disable pre-rotation is likely
>> a valuable way out.
>> /
>>
>> We'll just have to document /how/ it behaves. I propose the following:
>>
>> *"Multiple moveTo(location, MotionOption.Subordinate) must be executed
>> in the order in which they are anticipated to happen"*
>>
>> Then when you record the coordinates do it as follows:
>>
>> subordinateLocation = newLocation.put(subordinateLocation);
>>
>> so existing subordinate coordinates overwrite new ones of the same
>> axis, i.e., the first one recorded wins.
>>
>> For machines with shared rotation axis (as an example) it means that
>> it will prerotate to the first *moveTo(location,
>> MotionOption.Subordinate)* but not the second, etc. which makes most
>> sense.
>>
>> For the JobProcessor optimizer, the need to predict the order of the
>> moveTo()s means you need to know the nozzle sort order of the /next/
>> https://groups.google.com/d/msgid/openpnp/794a1f67-9468-448f-9581-99d2cc76c9d1%40makr.zone <https://groups.google.com/d/msgid/openpnp/794a1f67-9468-448f-9581-99d2cc76c9d1%40makr.zone?utm_medium=email&utm_source=footer>.
>
> --
> You received this message because you are subscribed to the Google
> Groups "OpenPnP" group.
> To unsubscribe from this group and stop receiving emails from it, send
> an email to openpnp+u...@googlegroups.com
> <mailto:openpnp+u...@googlegroups.com>.
> To view this discussion on the web visit
> https://groups.google.com/d/msgid/openpnp/ac2e2c38-6269-40c8-9750-031fa5d3607e%40makr.zone <https://groups.google.com/d/msgid/openpnp/ac2e2c38-6269-40c8-9750-031fa5d3607e%40makr.zone?utm_medium=email&utm_source=footer>.

mark maker

unread,
Apr 16, 2024, 9:15:46 AMApr 16
to ope...@googlegroups.com

> Therefore I currently have the draining in executeMotionPlan(). ... IIRC we discussed earlier, that pending subordinations shall be removed if the motion planner queue is executed.

Ah, I misunderstood your earlier use of the term "reset". I thought it is was referring to the G92 command, which should really only affect the axes that are actively reset. I understand what you mean, now. 

But is there any reason you moved the "subordinate coordinate reset" into executeMotionPlan() and not into waitForCompletion() as I proposed? That's not the same thing, semantically.

> If that's the case in a limited amount of situations, we could trigger executeMotionPlan() with a different or additional argument.

As far as I understand, that is not the case. See above, instead only reset in waitForCompletion().

> Hm... seems it's getting even more complicated...

Yes but things are clearing up, as we sharpen our understanding of what exactly is happening when. Like with the careful distinction between "execute" and "wait for completion" above. And it is an interesting discussion 😎.

> Initially I thought...

What you describe is still mostly accurate... but with a complication or two... see below.

> If anyone wont's to move, it shall say where. And than that move shall be executed based on the current location. Axis that have changed locations, shall move to the new location...

Yes, and just to state the obvious, the HeadMountable (hm) that is actually moved, gives us the mapped axes that are affected, like a "bitmask" over all the machine axes. Those masked axes would override subordinate axes if they were previously registered (this happens in case of shared axes). But all the non-masked axes can potentially move in parallel as subordinate axes.

> You also says, that the current location may reflect the subordination. I don't understand under which conditions this could be useful.

Short answer: you are kind of right for now. 😇

Long answer: Sometimes, even when you move a particular hm, you don't care to move all axes. I first thought there are existing examples, but turns out I haven't found them where expected. 

For instance, the BlindsFeeder may open a cover using the nozzle tip. It should not care about the nozzle rotation (assuming run-out is compensated), and it should not unnecessarily cause rotation and spoil optimized (subordinate) rotation. But turns out, it currently has its own "rotation optimization" hard-coded by rotating the nozzle to the feeder's "pick rotation in tape", see BlindsFeeder.actuateCover(Nozzle, boolean, boolean, boolean)'s use of BlindsFeeder.getPickRotationInTape(). Needless to say, this current "optimization" easily becomes counter-productive when you open all covers at job start (many unnecessary rotations) and in case the pushing nozzle tip is not the one actually used to pick (nozzle tips can be configured to allow pushing or not). Also not sure if all RotationModes are properly supported.

For this example to be valid as candidate for your subordinate motion optimization, and to solve the mentioned counter-productiveness, you would actually have to modify the BlindsFeeder to use the rotation from nozzle.getLocation() instead. For the sake of the argument, let's temporarily assume this to be the case. I hope it then becomes clear that for this use case of hm.getLocation() it should return the planned location with the subordinate coordinates applied on top. Otherwise the optimization gets lost.

A similar use case is ReferenceAutoFeeder.isMoveBeforeFeed() and cousins in other feeder classes. But again, if enabled, the feeder currently performs its own hard-coded rotation optimization, and again it can be counter-productive e.g. in case of shared rotation axes.

There is another prominent case I thought was relevant, but as as it turns out it is even a counter-example, and it adds complexity 😭... read on.

Take hm.moveToSafeZ() for the most common example of not caring about other axes. The intent is  to make sure the hm is at least at safe Z. But you don't care about all the other axes. More specifically, you want to preserve all other axes of the hm at their planned position to not generate unnecessary motion. Then you take these coordinates from hm.getLocation() and substitute only the coordinates you want to change, in this case only Z.

It is then that previously registered subordinate coordinates become more complex. 

Obviously, we do not want to rotate on the up-going Safe Z move, on large rotations it would kinda "screw off" the part while still half in its pocket, and it would likely slow down the Z move as the rotation may take longer than the solo Z move.

But we also do not want to lose the registered subordinate C coordinate (rotation), it must remain registered and only be executed on the (long) X/Y move while at Safe Z.

Conversely, in case of multiple Z axes (quad or dedicated Z nozzles), we absolutely want the subordinate Z axes to move at the same time, on the up-going Safe Z move of the principal hm. So there is a difference in wanted behavior between types of axes!

I guess we need an enum AxisSubordination { Never, InSafeZone, Always } or similar on the ReferenceControllerAxis. With such a setting subordination can then be configured by axis and it can be decided which type is needed. I&S should suggest the obvious behavior according to axis type.

Then in AbstractMotionPlanner.moveTo(HeadMountable, AxesLocation, double, MotionOption...) when you compose the move out of the subordinate and non-subordinate coordinates, you need to use AxesLocation.isInSafeZone() on both currentLocation and newLocation to determine which axes can be superimposed*. After the move, the axes that were not superimposed must remain in the registered subordinate AxesLocation, only the moved ones (including the non-subordinate ones of the hm) must be removed from it. 

With this logic the situation around hm.getLocation() actually becomes simpler (for now). Conclusion: 

  1. We see that for hm.moveToSafeZ(), the needed hm.getLocation() should actually not return subordinate coordinates, or more specifically the hm masking is already enough to preserve the subordinate Z coordinates.
  2. We see that BlindsFeeder and other code I checked, does not currently use the hm.getLocation(). We can leave that optimization to the future.
    We would likely expand AbstractHeadMountable.getApproximativeLocation(Location, Location, LocationOption...) with a new LocationOption that includes subordinate coordinates from the planner. Perhaps we need such a LocationOption per axis type (X, Y, Z, Rotation).

So in the end it is as said in the "short answer" above: you are kind of right for now. No need to change getLocation() or anything. but in exchange you need to add enum AxisSubordination distinction, or equivalent.

_Mark

P.S.

* Side note: strictly speaking this is a overcautious in case of a multi-head machine (not to be confused with multi-nozzle machines). Strictly speaking you should group the axes by their heads, and only check isInSafeZone() for the group to which the subordinate axis belongs. But such a multi-head machine is not currently supported anyways, and for purposed such as conveyors etc. they are always in their safe zone.

Reply all
Reply to author
Forward
0 new messages