Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Commands.java Add looseSequence() method #6639

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

tom131313
Copy link

added Commands.looseSequence()
Comparable to Commands.sequence() to create a SequentialCommandGroup

Each command is run individually by proxy. The requirements of each command are only for the duration of that command and are not required for the entire group process. This allows the possibility of a subsystem default command to run within the sequential group.

Note that the name looseSequence refers to a loose coupling of commands or loosening of the subsystem requirements for the duration of the group. The sequence is still determined by the order of the parameters.

added looseSequence()
@tom131313 tom131313 requested a review from a team as a code owner May 20, 2024 18:33
@Oblarg
Copy link
Contributor

Oblarg commented May 20, 2024

Needs unit tests and ports to other languages, but I think this is clearly a good feature - maybe we should extend to the other command group types?

@Starlight220
Copy link
Member

I'm not a fan of the naming; "loose" is loosely defined (pun intended).
We already have proxyAll, so implementation should use that -- if the reduction of sequence(proxyAll(...)) to looseSequence (or whatever we name this) is significant enough to warrant another factory here.

@Oblarg
Copy link
Contributor

Oblarg commented May 20, 2024

I think this is a common enough composition to warrant it, especially because we want to be absolutely sure that if a user is attempting a loose sequence, they reliably proxy all of the commands in the sequence. The biggest proxy footgun is the potential self-cancel, and this cannot encounter that. Also, looseSequence(...) (while vague) is a better problem-space identifier than sequence(proxyAll(...)) imo.

I like "loose", personally, but am flexible on naming.

Agree on using proxyAll for implementation (forgot it existed).

@Starlight220
Copy link
Member

proxySequence?

@Oblarg
Copy link
Contributor

Oblarg commented May 20, 2024

I think I prefer loose to proxy; neither is perfect. I'll let others weigh in.

@KangarooKoala
Copy link
Contributor

Personally, I would be okay with loose, but I would prefer proxy since loose doesn't immediately come to mind and proxy is already used in commands.

@tom131313
Copy link
Author

We can play with other words to describe the loose proxy situation.

Root words or derivatives that may fit better such as independent releasing non-blocking unblocked separated unconstrained unwrap all with sequence

Copy link
Contributor

@spacey-sooty spacey-sooty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have an override for Set<Command> as well

@Oblarg
Copy link
Contributor

Oblarg commented May 21, 2024

@tom131313 I like separatedSequence a lot!

separatedSequence proxyAll
@tom131313
Copy link
Author

This should have an override for Set<Command> as well

It looks like in the current WPILib only the defer/DeferredCommand has Set. Is that parameter type being added to SequentialCommandGroup (and others) too?

@spacey-sooty
Copy link
Contributor

This should have an override for Set<Command> as well

It looks like in the current WPILib only the defer/DeferredCommand has Set. Is that parameter type being added to SequentialCommandGroup (and others) too?

Huh interesting I would say they should be or defer should have it removed for consistencies sake but thats a seperate pr problem

@Gold856
Copy link
Contributor

Gold856 commented May 21, 2024

DeferredCommand accepts a Set<Subsystem> to prevent teams from forgetting about requirements. No command in WPILib accepts a Set<Command>, so var-args is fine.

documentation
@KangarooKoala
Copy link
Contributor

Overall, good work!

Just don't want us to forget that this still needs unit tests and ports to other languages (including robotpy/robotpy-commands-v2 after this is merged). I'm also still a little iffy about the name- People might think there's some time gap between the commands- but it's still pretty good. The paragraph in the doc comment helps a ton, though- For the (few) people that do read it, there isn't any ambiguity. It might be nice though to add a brief note in the first sentence of the doc comment since some things only show the first sentence:
image
(This particular image is from a local javadoc build in the list of available commands- I don't remember what most IDEs display on their tooltips)

We already have proxyAll

That's in #5606, which isn't merged yet.

@Oblarg
Copy link
Contributor

Oblarg commented May 21, 2024

Re: naming, we could also try disjointSequence. A bit more formal/math-y, but also fewer timing connotations

@Starlight220
Copy link
Member

How about proxyingSequence (on the weight of repeatingSequence)?

I think it's better to not bring more terms into the mess when we have plenty of terms already. The relation to proxy and sequence needs to be clear.

@tom131313
Copy link
Author

How about proxyingSequence (on the weight of repeatingSequence)?

I think it's better to not bring more terms into the mess when we have plenty of terms already. The relation to proxy and sequence needs to be clear.

We do already have a good term that we can use but in a negative way. So another name suggestion and cautionary tale:

Commands can be executed in a sequence by a few ways such as a command triggers another command or timing paces a sequence such as press button "A" to schedule a command then press button "B" to schedule the next command or watch the clock to schedule a command then later on the clock schedule the next command.

The SequentialCommandGroup (sequence) was introduced to both pace commands one after another AND bestow special group properties regarding the default command blocking and the interrupt behavior of the group as a whole. This special sequencing was correctly name SequentialGroupCommand but the sequence name was incorrect and should have been called groupSequence. We may have to live with that misnomer but we can call the new, pure sequence ungroupedSequence.

The cautionary tale, of course, is the interrupt behavior of the ungrouped (being paced the same as if chained by triggers or clocking) is profoundly different than the group behavior. I've start testing the parallel version and that is difficult but seems to require the same caution - the group interrupt behavior is a lot different than not grouped.

new javadoc and new name for ungroupedSequence
@tom131313
Copy link
Author

add a brief note in the first sentence of the doc comment since some things only show the first sentence

I did this. I could have removed the <p> as that seems to do the same thing in VSCode but I assumed you meant more words were needed before the cr/lf. And you'll notice a major overhaul to the whole method.

@tom131313
Copy link
Author

tom131313 commented May 22, 2024

maybe we should extend to the other command group types

I tried an ungroupedParallel even though it seemed dangerous to me. It was worse than I thought and even my simple examples didn't work right. I might have misunderstood. What did you mean by other command group types?

@Oblarg
Copy link
Contributor

Oblarg commented May 22, 2024

@Starlight220 While I agree we shouldn't add too many new identifiers, I think a single new one here is appropriate because of the fundamentally abstract/complicated nature of proxy.

The ideal situation would be one where disjointSequence (or whatever we call it) will hopefully be usable by new users without needing to do a deep dive into proxyCommand et al. It could be that this is not a reasonable to expect, and we'd be better off just referring people to the deeper documentation and committing to a consistent (if very solution-space-oriented) name. But when possible I like to replace solution-space terms with "friendlier" problem-space terms, and I think this is a good candidate.

It's a matter of weighing discoverability against absolute consistency.

@tom131313 I did indeed mean parallel compositions - particularly the ones other than race where the commands do not all end at the same time.

@tom131313
Copy link
Author

parallel compositions

The problem was if the two (or more) parallel commands have a subsystem in common, then the results are bizarre, unexpected and seemingly wrong. I can add my ungroupedParallel (or disjointParallel) if we can live with expecting the user nearly perfectly keeping the same subsystem off more than one branch of a Parallel. I feel pretty sure my students will fail this test. I see no simple way to help the students succeed with this requirement.

@Oblarg
Copy link
Contributor

Oblarg commented May 22, 2024

@tom131313 You could enforce the disjoint requirements at construction-time, probably? This also suggests disjoint is the right naming paradigm.

We also should probably enforce that the proxied commands all have at least one requirement for all of these, or else people might double-compose them and that can break badly.

Copy link
Contributor

@KangarooKoala KangarooKoala left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a brief note in the first sentence of the doc comment since some things only show the first sentence

I did this. I could have removed the <p> as that seems to do the same thing in VSCode but I assumed you meant more words were needed before the cr/lf. And you'll notice a major overhaul to the whole method.

Thanks! Specifically, I meant a more detailed description in the first sentence (before the first period), but I'm glad to see you still added that detail!

You could enforce the disjoint requirements at construction-time, probably?

That's how a normal ParallelCommandGroup does it. (See ParallelCommandGroup.java#L51)

We also should probably enforce that the proxied commands all have at least one requirement for all of these, or else people might double-compose them and that can break badly.

Could you elaborate more on this? (Specifically what you mean by double-compose, and an example of bad usage would also be nice)

*
* @param commands the commands to include in the series
* @return the command to run the series of commands
* @see #sequence() use sequence() to invoke group sequence behavior
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @see #sequence() use sequence() to invoke group sequence behavior
* @see #sequence(Command...) use sequence() to invoke group sequence behavior

The parameter type(s) of a method must be specified. (See https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#see)

disjointParallel
@tom131313
Copy link
Author

enforce the disjoint requirements

disjointSequence() and disjointParallel() are committed. I'll do repeatedly and deadline next. At first glance it's not obvious to me why race couldn't or shouldn't have the same disjoint treatment with proxyAll.

disjoint commands
*
* @param commands the commands to include in the series
* @return the command to run the series of commands repeatedly
* @see #sequence(Command...) use sequenceRepeatedly() to invoke repeated group sequence behavior
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is sequence(Command...) this the right method to link to?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed it to a better reference.

* @see #parallel(Command...) use parallel() to invoke group parallel behavior
*/
public static Command disjointParallel(Command... commands) {
new ParallelCommandGroup(commands); // check parallel constraints
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should implement the check for disjoint requirements separately (probably in a private static convenience method)- It's self-documenting and (more importantly) will not register the commands as composed.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should implement the check for disjoint requirements separately (probably in a private static convenience method)- It's self-documenting and (more importantly) will not register the commands as composed.

I agree that my proposed validation that has a side-effect of registering the commands that then must be unregistered is a bit stinky but it appears to save duplicating a lot of code. Tracing the deadline validation, which may be the worst case, needs most of the following code pasted here from ParallelDeadlineGroup.java and CommandScheduler.java.

// from ParallelDeadlineGroup.java

  public final void setDeadline(Command deadline) {
    @SuppressWarnings("PMD.CompareObjectsWithEquals")
    boolean isAlreadyDeadline = deadline == m_deadline;
    if (isAlreadyDeadline) {
      return;
    }
    if (m_commands.containsKey(deadline)) {
      throw new IllegalArgumentException(
          "The deadline command cannot also be in the other commands!");
    }
    addCommands(deadline);
    m_deadline = deadline;
  }

  /**
   * Adds the given commands to the group.
   *
   * @param commands Commands to add to the group.
   */
  public final void addCommands(Command... commands) {
    if (!m_finished) {
      throw new IllegalStateException(
          "Commands cannot be added to a composition while it's running");
    }

    CommandScheduler.getInstance().registerComposedCommands(commands);

    for (Command command : commands) {
      if (!Collections.disjoint(command.getRequirements(), m_requirements)) {
        throw new IllegalArgumentException(
            "Multiple commands in a parallel group cannot require the same subsystems");
      }
      m_commands.put(command, false);
      m_requirements.addAll(command.getRequirements());
      m_runWhenDisabled &= command.runsWhenDisabled();
      if (command.getInterruptionBehavior() == InterruptionBehavior.kCancelSelf) {
        m_interruptBehavior = InterruptionBehavior.kCancelSelf;
      }
    }
  }


  // from CommandScheduler.java

  private final Map<Command, Exception> m_composedCommands = new WeakHashMap<>();

  /**
   * Register commands as composed. An exception will be thrown if these commands are scheduled
   * directly or added to a composition.
   *
   * @param commands the commands to register
   * @throws IllegalArgumentException if the given commands have already been composed, or the array
   *     of commands has duplicates.
   */
  public void registerComposedCommands(Command... commands) {
    Set<Command> commandSet;
    try {
      commandSet = Set.of(commands);
    } catch (IllegalArgumentException e) {
      throw new IllegalArgumentException(
          "Cannot compose a command twice in the same composition! (Original exception: "
              + e
              + ")");
    }
    requireNotComposedOrScheduled(commandSet);
    var exception = new Exception("Originally composed at:");
    exception.fillInStackTrace();
    for (var command : commands) {
      m_composedCommands.put(command, exception);
    }
  }

    /**
   * Requires that the specified command hasn't already been added to a composition, and is not
   * currently scheduled.
   *
   * @param command The command to check
   * @throws IllegalArgumentException if the given command has already been composed or scheduled.
   */
  public void requireNotComposedOrScheduled(Command command) {
    if (isScheduled(command)) {
      throw new IllegalArgumentException(
          "Commands that have been scheduled individually may not be added to a composition!");
    }
    requireNotComposed(command);
  }

  /**
   * Requires that the specified commands have not already been added to a composition, and are not
   * currently scheduled.
   *
   * @param commands The commands to check
   * @throws IllegalArgumentException if the given commands have already been composed or scheduled.
   */
  public void requireNotComposedOrScheduled(Collection<Command> commands) {
    for (var command : commands) {
      requireNotComposedOrScheduled(command);
    }
  }
  
  /**
   * Requires that the specified command hasn't already been added to a composition.
   *
   * @param commands The commands to check
   * @throws IllegalArgumentException if the given commands have already been composed.
   */
  public void requireNotComposed(Command... commands) {
    for (var command : commands) {
      var exception = m_composedCommands.getOrDefault(command, null);
      if (exception != null) {
        throw new IllegalArgumentException(
            "Commands that have been composed may not be added to another composition or scheduled "
                + "individually!",
            exception);
      }
    }
  }

  /**
   * Requires that the specified commands have not already been added to a composition.
   *
   * @param commands The commands to check
   * @throws IllegalArgumentException if the given commands have already been composed.
   */
  public void requireNotComposed(Collection<Command> commands) {
    requireNotComposed(commands.toArray(Command[]::new));
  }

    /**
   * Whether the given commands are running. Note that this only works on commands that are directly
   * scheduled by the scheduler; it will not work on commands inside compositions, as the scheduler
   * does not see them.
   *
   * @param commands the command to query
   * @return whether the command is currently scheduled
   */
  public boolean isScheduled(Command... commands) {
    return m_scheduledCommands.containsAll(Set.of(commands));
  }

* @throws IllegalArgumentException if the deadline command is also in the otherCommands argument
*/
public static Command disjointDeadline(Command deadline, Command... otherCommands) {
new ParallelDeadlineGroup(deadline, otherCommands); // check parallel deadline constraints
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as disjointParallel(): You should implement the check for disjoint requirement separately (probably in a private static convenience method)- It's self-documenting and (more importantly) will not register the commands as composed.

*/
public static Command disjointDeadline(Command deadline, Command... otherCommands) {
new ParallelDeadlineGroup(deadline, otherCommands); // check parallel deadline constraints
return deadline(deadline, proxyAll(otherCommands));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the deadline command need to be proxied?

Copy link
Author

@tom131313 tom131313 May 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the deadline command need to be proxied?

Maybe but doing that here doesn't help.

If the deadline is an individual command then "no" it doesn't need an asProxy() since it's going to end the group and release the requirements at that time with no possibility to release the requirements earlier.

If the deadline is a composite group command then "yes" it needs the Proxy treatment because the earlier part of the group could end and those requirements released early for the default command to run while the later part of the deadline runs with different requirements.

The Proxy for the deadline as a grouped command has to be added when that group is made (not when used or referenced later) because after its creation later additional wrapping groupings don't know about the inner groups.

This points out again that the Proxy has to be carefully considered minimally applying it starting from the inner commands to the outer commands.

Edit after more thought that I might not understand the objective of this disjoint work and I gave a somewhat wrong answer above.

I have mostly been thinking about the disjoint of a single group. The deadline portion doesn't need a proxy if an individual command and also race would not need proxies for individual commands but for groups would be useful. I now see that to assure disjointedness without much thought by the user, everything in sight needs to have proxy - groups and individual commands in all the various wrappers - race, parallel, sequence, deadline. This is because any of these constructs could be used in combination within any other and the only way to assure independence is make everything independent.

Likely this results in a lot of redundant and inefficient wrappings (of wrappings of wrappings). So we're back to "hand-tuning" the requirements by judicious addition of asProxy() if you want thoughtful efficiency.

So yes - proxy everything if that's the objective and I now assume it is.

@tom131313
Copy link
Author

tom131313 commented May 24, 2024

parallel compositions

Adding the proxyALL to the parallel construction including deadline adds surprisingly little value, I think. If the commands are the simplest possible - single command and not a composite - then the proxy works to allow the default command to activate before the end of the parallel.

Otherwise a composite command has to be wrapped in the disjointSequence because the proxy doesn't propagate and the default isn't going to activate without a proxy all the way done a nest.

And if the disjointSequence is used to warp the parallel commands, no further proxy is needed and parallel works exactly the same as disjointParallel.

We can cover the case of adding proxy to a simple command for parallel but it seems like a lot of complication for a possibly rarely used instance. And it may lead people to believe any command including any composite will be handled which it will NOT be. Composite commands have to be further wrapped by the user and that is far from obvious (took me a few hundred test cases to figure this out).

I see my comments on the parallel being of less utility than expecetd apply to sequence so I'm in favor of abandoning new code and then write an article describing how to get groups to honor the default command. I have test cases for good examples to share.

@tom131313
Copy link
Author

tom131313 commented May 27, 2024

The RepeatCommand has a fatal race (it always loses) with the ProxyCommand that is being used to disjointSequence so the repeated results are wrong. I added a delay to the RepeatCommand between the initialize and the execute to "prove" my conjecture. Seems to work but I have no idea what I'm doing so this is a case of testing-in quality which, of course, is frowned upon.

Next step?

  @Override
  public void execute() {
    if (m_ended) {
      m_ended = false;
      m_command.initialize();
      m_delayFirstExecuteIterations = 0; //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    }
    if (m_delayFirstExecuteIterations < 3) { //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            m_delayFirstExecuteIterations++; //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            return; //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
        }//<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    m_command.execute();
    if (m_command.isFinished()) {
      // restart command
      m_command.end(false);
      m_ended = true;
    }
  }

@KangarooKoala
Copy link
Contributor

The RepeatCommand has a fatal race (it always loses) with the ProxyCommand that is being used to disjointSequence so the repeated results are wrong.

Could you elaborate more on this? (What is the code being run (including any added or changed method definitions), what is the expected behavior, and what is the actual behavior?)

@tom131313
Copy link
Author

The RepeatCommand has a fatal race (it always loses) with the ProxyCommand that is being used to disjointSequence so the repeated results are wrong.

Could you elaborate more on this? (What is the code being run (including any added or changed method definitions), what is the expected behavior, and what is the actual behavior?)

This or equivalents fail: repeatingSequence(cmd1.asProxy(), cmd2.asProxy(), cmd3.asProxy());
Expected to run cmd1 until finished then run cmd2 until finished and then run cmd3. That is the result for the first time through but on the repeat the result is cmd1 and cmd2 run together and then when finished cmd3 runs.

Maybe it happens because the Proxied command (say cmd1 or cmd2) isn't quite ready to respond yet or it takes an extra iteration for that status to flow back through ProxyCommand to RepeatCommand for when the RepeatCommand checks its status. There isn't quite enough time for the repeat between the initialize() and the execute() for things to work right.

@KangarooKoala
Copy link
Contributor

Thanks for noticing that bug! It turns out to be a fairly involved bug, and I think the cleanest fix would be #6593.

This is what happens during the call to RepeatCommand.execute() as it repeats:

  • (1) SequentialCommandGroup.initialize() is called, during which:
    • (1a) ProxyCommand(cmd1).initialize() is called.
      • This schedules cmd1, but the schedule is deferred because we're in the command scheduler run loop.
  • (2) SequentialCommandGroup.execute() is called, during which:
    • (2a) ProxyCommand(cmd1).execute() is called, which does nothing.
    • (2b) ProxyCommand(cmd1).isFinished() is checked and returns true, causing steps 2c and 2d to occur.
      • isFinished() checks !cmd1.isScheduled(), which is true because the schedule was deferred (so the command wasn't scheduled yet).
    • (2c) ProxyCommand(cmd1).end(false) is called, which does nothing.
    • (2d) ProxyCommand(cmd2).initialize() is called.
      • This schedules cmd2, but this schedule is also deferred because we're in the command scheduler run loop.
  • (3) SequentialCommandGroup.isFinished() is checked and returns false.

Note that both cmd1 and cmd2 were queued to be scheduled, leading to the behavior you saw (cmd1 and cmd2 running simultaneously).

added more disjoint methods
updated javadocs
add proxyAll() restricted to commands with requirements
@tom131313
Copy link
Author

tom131313 commented May 31, 2024

We also should probably enforce that the proxied commands all have at least one requirement for all of these, or else people might double-compose them and that can break badly.

I added this check for restriction by changing the proxyAll() (one of which I had found in github). I included my modified version in this PR Commands.java. (deadline() had to have a tweak, also.) Possibly proxyAll() should always have the check for subsystem requirements. If not, then the version I put in Commands.java could be renamed to something like proxyAllWithRequirements() and, of course, change the new disjoint methods to reference proxyAllWithRequirements().

I believe this satisfies all the comments on this PR except possibly the validation scheme that registers then unregisters composed commands. Changing that is a daunting task for me to do elegantly so I'll leave that for others again to decide how to do it.

I think a complete set of Java disjoint methods is provided and they work as I have tested them. This should give you a good idea if you want to pursue including them in WPILib.

[The repeatingDisjointSequence is not well implemented pending correction of the underlying command class.]

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.

6 participants