-
Notifications
You must be signed in to change notification settings - Fork 93
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
Fix A* implementation #125
Conversation
Codecov Report
@@ Coverage Diff @@
## master #125 +/- ##
==========================================
- Coverage 97.54% 97.54% -0.01%
==========================================
Files 109 109
Lines 6318 6314 -4
==========================================
- Hits 6163 6159 -4
Misses 155 155 |
Why name it |
@etiennedeg it doesn't necessarily gather an existing edge, it recreates an edge from EDIT: I just realized this is breaking too, since the constructor (::Type{E})(src::Integer, dst::Integer) where {E <: AbstractEdge} = SimpleEdge(src, dst) |
I was not involved in that discussion but I still wonder, if we can't solve that issue without adding another method to the interface? |
@simonschoelly it was quite a long discussion (see this previous PR), but the conclusion I drew is that there is no clean way out of this mess. Please feel free to weigh in though! In my view it is a really important issue: if A* doesn't work on the standard library for weighted graphs, this kinda looks bad ^^ |
I think that switching to returning a sequence of vertices precludes support for multigraphs in the future. I would rather return a sequence of SimpleEdge for all graph types or have it give a copy of the actual edges in the input graph. I think the latter option would avoid needing a way to generically construct an edge of arbitrary type. We could add a flag to the function signature to enable this behavior. If avoiding a SemVer bump is important. |
Fair point, I hadn't though of multigraphs. |
Another option would be to introduce a new method with a different name, say |
I think a The way My proposal is to somehow fix this downstream, i.e. in function _make_edge(g, s, d)
return edgetype(g)(s, d)
# TODO maybe catch exception and output a friendly message to show what to implement
end then instead of exporting that function in the interface, we keep it somewhere near the astar function. We can then import and override that function in This is of course also not ideal, but I think we should avoid making the interface more complicated than it already is, as otherwise we will scare people away from creating their own graph types. Apart from that, I also like James idea of adding a keyword argument to the function to control behavior. |
This could also be in a submodule where we can stage v2.0 functions without respecting semver (although I am not sure about name collisions if we use the same name). |
Totally agree
Yeah, that is what I realized this afternoon. Sorry about that
Just to be clear, cause I'm new to this whole versioning stuff: if we require downstream users to add something to their packages for
To me this sounds like a good compromise waiting for 2.0, and it keeps the interface just as simple. To enable static dispatch I think the additional argument could be the type of the edges we want in the returned path: function a_star(g::AbstractGraph, s::Integer, d::Integer, ::Type{E}) where {E <: AbstractEdge}
# Returns a list of edges of type `E`. The constructor `E(s, d)` should exist.
end |
I think we have a broader problem here. When all graphs are SimpleGraphs then there is an equivalence between lists of vertices and paths in the graph. All of the algorithms that compute paths use this equivalence at some point. When the graphs have either metadata attached to the edges (weights, properties, etc.) or multiedges, this equivalence is no longer canonical. You need to know additional information about the graph in order to construct it. We could either say that Graphs.jl is really a SimpleGraph library and push the handling of this additional information to the downstream packages, or we could find generic ways to providing hooks for downstream libraries to tell us what we need to know. The approach used in this PR, introducing I think the core problem is that Graphs.jl implementations assume that there is an equivalence between sequences of vertices and paths in the graph. That core problem is much bigger than |
Indeed it seems like there are larger things at stake than A*, namely the fate of non-simple graphs. But that would require rethinking a large part of our package, most notably related to edge construction. For instance, the However, I think the most urgent task is is to reach a consensus on this PR so that A* finally works on (simple) weighted graphs. This is a pretty serious bug that has gone unfixed for a long long time, and it bothers me a lot ^^ |
Ok, so to get this fixed I think we need to just return E(u,v) or SimpleEdge(u,v) as the fallback. This delegates the handling of weights to the downstream library, which is fine because only the downstream consumer knows how to handle the additional structure. This doesn't fix or preclude multi graphs in the future. I think that so much of Graphs.jl needs to get fixed for multgraphs that we can keep the status quo until we have an interested person who wants to really dive into multigraphs. Questions about the PR specifically:
|
Yeah but the thing is, returning E(u, v) is the current (buggy) behavior, and returning SimpleEdge(u, v) would be breaking. My workaround with the additional type argument keeps the current behavior, but allows the user of eg SWG to specify that they want to return a vector of SimpleEdges. Of course this means users have to change their code to fix the bug, but it is better than us breaking the current behavior. What do you think?
As for your other remarks :
1) Good point, I’ll move the test over to SWG, it makes sense there anyway
2) I was trying to respect the typical order of arguments, as specified in the official style guide, but you think internal stability trumps that?
3) Ok I’ll revert that. But someday we need to put everything through JuliaFormatter during CI because the code base is ugly ^^
… Le 16 mai 2022 à 09:45, James ***@***.***> a écrit :
Ok, so to get this fixed I think we need to just return E(u,v) or SimpleEdge(u,v) as the fallback.
This delegates the handling of weights to the downstream library, which is fine because only the downstream consumer knows how to handle the additional structure. This doesn't fix or preclude multi graphs in the future. I think that so much of Graphs.jl needs to get fixed for multgraphs that we can keep the status quo until we have an interested person who wants to really dive into multigraphs.
Questions about the PR specifically:
We don't have any other reverse dependencies in the tests now and so I am hesitant to add one for this. It would be more consistent to have the test in SWG. There is some risk introduced by the possibility that a change in SWG would break the tests for Graphs.jl
We could avoid breaking the signature of astar_impl! just by making the edgetype_to_return an optional argument with a default of edgetype(g). While the _impl functions are generally considered internal to the API, I think that some people use them for performance critical tasks we should take the opportunity to avoid the breakage.
There appear to be some superficial formatting diffs that could be removed from the PR unless they are homogenizing style.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you authored the thread.
|
Yes, maybe we can default
If we decide to support multi-graphs, we will need to have a Trait to distinguish between simple graphs and general graphs, so the In the current PR implementation, the |
Yes, especially here where there is a good reason for a performance-based usage of the unexpected function.
It makes sense to me for SWG to define a method of |
Ok, but it requires downstream packages to implement it. If we ask users to implement a hack specifically to get |
@etiennedeg I don't think so, take a look at the default 5-argument method: function a_star(
g::AbstractGraph{U},
s::Integer,
t::Integer,
distmx::AbstractMatrix{T}=weights(g),
heuristic::Function=n -> zero(T),
) where {T,U}
return a_star(edgetype(g), g, s, t, distmx, heuristic)
end
With the default method we don't, which is why this patch is non-breaking in my opinion. |
@jpfairbanks with my suggestion they can do that by specifying |
Right, I think that we shouldn't restrict that argument to be a type because graphs with data on their edges need to have some enclosed variable to store that metadata. It will be easiest for SWG to pass the closure below: function edge_constructor(g)
newedge(u,v) = SimpleWeightedEdge(u, v, g.weights[u,v])
return newedge
end
a_star(g::SimpleWeightedGraph, ...) = a_star(g, ..., edge_constructor(g)) |
In terms of order of arguments, I put the |
This is the same if we default |
The current buggy behavior may actually be something we wish to keep, unless users explicitly declare that they want to output edges of a certain type (compatible with |
I don't understand, my proposition does not change the actual behavior until a user redefine something (either |
Oh, right, I though you wanted |
Well then it comes down to personal taste. I'm in favor of adding an argument to |
I think the solution here is good enough enough, we should definitiely refactor this function in the future though- @gdalle could you try, if you could change SimpleWeightedGraphs, so that it works correctly with the changed function? |
Yeah, I'll polish this PR during the week-end and transfer the test over to SWG. |
So I pushed a new version that adresses most of the previous concerns and provides a satisfying fix until 2.0 comes along:
I think this works but I have two problems left unsolved, for which I need your advice.
|
Note however that these problems are not bugs, they only impact generality / performance. As a result, I would be in favor of merging first and improving later |
I'd be glad to get feedback on the A* test over in SWG (see my fork branch). |
If |
Note to self: following the latest updates on #120, I should probably add back the |
I say we leave it as a function for now, this has waited long enough
… Le 22 mai 2022 à 21:02, Simon Schölly ***@***.***> a écrit :
@simonschoelly commented on this pull request.
In src/shortestpaths/astar.jl:
> """
function a_star(g::AbstractGraph{U}, # the g
s::Integer, # the start vertex
t::Integer, # the end vertex
distmx::AbstractMatrix{T}=weights(g),
- heuristic::Function=n -> zero(T)) where {T, U}
-
- E = Edge{eltype(g)}
-
+ heuristic=n -> zero(T),
Ah I didn't know that this would not for function-like objects. There is also Base.Callable but that is also too restrictive in that case.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you were assigned.
|
Yes because a fully reproducible docs build seemed like a good idea. I don’t know whether it is the default practice
… Le 22 mai 2022 à 21:05, Simon Schölly ***@***.***> a écrit :
@simonschoelly commented on this pull request.
In .gitignore:
> @@ -7,4 +7,4 @@ docs/site/
benchmark/.results/*
benchmark/.tune.jld
*.cov
-Manifest.toml
Is that on purpose? I see you also commited docs/Manifest.toml.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you were assigned.
|
This PR resolves two issues with the A* algorithm:
a_star
fails onSimpleWeightedDiGraph
#59 To make A* work with custom edge typesE
that do not define a constructorE(s,d)
, I created an undocumented methodmake_edge(g,s,d)
that falls back onSimpleEdge
. This follows a lengthy discussion in Change A* output to a vector of tuples #116 where several approaches were debated, some of them breaking.closed_set
variable which prevented it to be exponential in the worst-case scenario (see this Wikipedia paragraph from the article which inspired our implementation). However, I see no obvious way to test this pathological behavior so I'm submitting the PR as is.