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

86 Improve documentation MotionCheck #87

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

chenkins
Copy link
Contributor

Changes

Improve documentation of MotionCheck Behaviour #86.

Related issues

Resolves #86

Checklist

  • Tests are included for relevant behavior changes.
  • Documentation is added in the docs folder for relevant behavior changes. If you made important user-facing
    changes, describe them under the [Unreleased] tag in CHANGELOG.md.
  • New package dependencies are declared in the pyproject.toml file.
    Requirement files have been updated by running tox -e requirements.
  • Code works with all supported Python versions (3.10, 3.11 and 3.12). Checks run with all three version and are
    required to run successfully.
  • Code is formatted according to PEP 8 (an IDE like PyCharm can do this for you).
  • Technical guidelines listed in CONTRIBUTING.md are followed.

@chenkins chenkins changed the title 86 improve documentation motion check 86 Improve documentation MotionCheck Nov 20, 2024
tests/test_agent_chains.py Outdated Show resolved Hide resolved
dPred: Set[Cell] = dPred

# if in blocked, it will not also be in a swap pred tree, so no need to worry about overwriting (outdegree always <= 1!)
# TODO why not mark outside of the loop? The loop would then only need to go over nodes with indegree >2 not marked purple or red yet
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@hagrid67 @aiAdrian is there a reason why svBlocked are not all marked red outsie of the loop?

Copy link
Contributor

Choose a reason for hiding this comment

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

yes I can't see any reason why we don't use some sort of bulk action to mark svBlocked as "red"; though it may not save much time as it looks like an O(n) operation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Focus is on readability and not on performance, agreed.

svBlocked = self.find_stop_preds(svStops)
# Just look for the tree of preds for each voluntarily stopped agent (i.e. not wanting to move)
# TODO why not re-use mark_preds(swStops, color="red")?
# TODO refactoring suggestion: only one "blocked" red = 1. all deadlocked and their predecessor, 2. all predecessors of self-loopers, 3.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@hagrid67 @aiAdrian if the loop only contained conflict resolution for the remaining nodes of indegree > 1, that would greatly reduce reading complexity - do you see a problem with that? And also drop the red-purple distinction?

Copy link
Contributor

@hagrid67 hagrid67 Dec 4, 2024

Choose a reason for hiding this comment

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

Yes I think the suggestion that we only need to process indegree>1 in a conflict resolution loop sounds correct.

I think the intention of the red / purple distinction is that

  • red is "temporary" ie blocked by a voluntarily stopped agent, or a slow one which is stepping in the same cell
  • purple is the result of a deadlock, which here means a "swap" where two agents are adjacent and facing, ie they are trying to "swap" cells. Purple is therefore "permanent" for the rest of the episode. I don't think we make any use of this but it could be useful.

Purple should take precedence over red, but I don't think it does. eg in the following chain:
1East -><- 2West <-3West <-4West
1 and 2 are deadlocked, so 3 and 4 should also be deadlocked: there is nothing they can do now to escape the deadlock.

But if 2 or 3 also has a failure, or maybe even chooses a voluntary "pause/stop" action, I think this in effect "breaks" the purple chain, in the implementation as I remember it. I remember partial chains flicking from purple to red and back. I think this is visually misleading but harmless (because they still don't move).

What happens is that if say agent 3 above has a malfunction, then it's "desired motion" becomes a self-loop which breaks the chain in our representation. (When I wrote this, I think all agent speeds were 1)

We could somehow preserve purple chains from one timestep to the next: once an agent is purple, it remains purple for the episode. We could then prevent those agents stepping, and reduce the processing in the motion check. It may also be useful in the observation (if it isn't already there).

I suspect that the very poor performance of motioncheck in the profiling test case may be related to random actions which result in huge jams and this provokes a lot of traversing of predecessor trees. If these were marked purple and preserved and then skipped, there could be performance benefit.

# Get all the chains of agents - weakly connected components.
# Weakly connected because it's a directed graph and you can traverse a chain of agents
# in only one direction
# TODO why do we need weakly connected components at all? Just use reverse traversal of directed edges?
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@hagrid67 @aiAdrian why do we need weakly connected components at all? We could just reverse traverse the directed graph?

Copy link
Contributor

@hagrid67 hagrid67 Dec 4, 2024

Choose a reason for hiding this comment

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

Yes I think you are right once again! We could just traverse the reverse* graph, starting from the "swaps and stops" (ie immediate deadlocks, and voluntary / failed / slow agents). There doesn't seem to be any need to break the graph down into weakly connected components first. The traversal will naturally terminate as it reaches the "boundaries" of WCCs.

When I wrote the motioncheck I tried to look for algos which did clever stuff, assuming they would be faster than my own implementation. Processing the WCCs separately seemed reasonable and my small-case performance checks did not expose the O(n^2) performance problems we seem to have.

See the comment below - I think we still need the reverse graph because the dfs traversal to read the tree of blocked agents can only progress in a forward direction.
(*You said reverse-traverse the graph, I'm saying traverse the reverse graph ;)

@@ -12,31 +41,24 @@ class MotionCheck(object):
"""

def __init__(self):
self.G = nx.DiGraph()
self.G = nx.DiGraph() # nodes of type `Cell`
# TODO do we need the reversed graph at all?
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@hagrid67 @aiAdrian why do we need the reversed graph at all? Can we not just use the predecessors of directed graphs? Or does this have performance drawbacks?

Copy link
Contributor

@hagrid67 hagrid67 Dec 4, 2024

Choose a reason for hiding this comment

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

We need to traverse the desired motion graph backwards, to find agents which are trying to move into a blocked cell.

I don't think there is a NetworkX call to do this. dfs_predessors does the wrong thing: it traverses the tree in the forward direction, and simply returns the predecessor of each node.

For example, if cell 4 is blocked, we want all the (branched) predecessors 0,1,2,3,5,6. But dfs_predecessors returns an empty set because 4 has no successors.

G = nx.DiGraph([ (0,1), (1,2), (2,3), (3,4), (5,6), (6,2) ])
nx.dfs_predecessors(G, source=4)
returns: {}

However we can get what we need using the reverse graph and dfs_postoder_nodes (as in the code):

H = G.reverse()
list(nx.dfs_postorder_nodes(H, source=4))
returns:  [0, 1, 5, 6, 2, 3, 4]

However this maybe raises the question, do we really need the forward graph? Maybe we should build only the reverse graph, because we only really need to follow the motion in reverse? We only seem to use G.succ in check_motion() and it doesn't seem to be doing much.

So if we built only the reversed graph Grev, we could use Grev.succ instead of G.pred.

Generally trying to navigate "upstream" in a Directed Graph seems like hard work in NetworkX.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@hagrid67 thought of:

g = nx.DiGraph([ (0,1), (1,2), (2,3), (3,4), (5,6), (6,2) ])
print(list(g.predecessors(4)))
# returns: [3]

# Just look for the tree of preds for each voluntarily stopped agent
svBlocked = self.find_stop_preds(svStops)
# Just look for the tree of preds for each voluntarily stopped agent (i.e. not wanting to move)
# TODO why not re-use mark_preds(swStops, color="red")?
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@hagrid67 @aiAdrian why do we need find_stop_preds - couldn't we not just re-use mark_preds(swStops, color="red")?

Copy link
Contributor

@hagrid67 hagrid67 Dec 4, 2024

Choose a reason for hiding this comment

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

Gosh yes, well spotted, this does seem to be pointless; I'm embarrassed for my younger self!

FWIW, the logic in mark_preds (called block_preds in my older version) does not match the comment "only color those not already marked" but it looks like it overwrites the color. In my older version it seems to count the ones it has to change, which would justify the check; but then the count is discarded anyway.

@aiAdrian
Copy link
Contributor

aiAdrian commented Dec 2, 2024

The documentation i can understand is looks good. but the detailed questions i can not answer - > please @hagrid67

Copy link
Contributor

@hagrid67 hagrid67 left a comment

Choose a reason for hiding this comment

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

I think the changes look good.
The questions also seem to suggest further improvements which I have commented on.

def find_swaps(self) -> Set[Cell]:
"""
Find loops of size 2 in the graph, i.e. swaps leading to head-on collisions.
:return: set of all cells in swaps.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.

Improve documentation of MotionCheck Behaviour.
3 participants