-
Notifications
You must be signed in to change notification settings - Fork 269
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
Implemented the Moran process on graphs #799
Changes from all commits
8b9459a
96c73e0
af69177
88989f7
0985230
d4bb462
496fe41
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
""" | ||
Weighted undirected sparse graphs. | ||
|
||
Original source: | ||
/~https://github.com/marcharper/stationary/blob/master/stationary/utils/graph.py | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not against this. A couple of initial thoughts:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To be clear: If we go with my suggestion of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I figured this would come up which is why I didn't add tests yet. We can switch this out for another library, I just used what I had laying around. It's a bit overkill since we don't need the weights (yet?), but it optimized in the right ways -- sparse implementation, precomputed neighbors. So we could also just simplify it to parts needed (relatively easy). I'm not opposed to networkX but I'm not familiar with its API, what internal representation it uses (adjacency matrix? sparse matrix?) etc. Thoughts? I agree re: consistent API with spatial tournaments -- we could also use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That's the killer point right there! I guess that also includes the possibility of graphs that change over time etc which also rules out networkX (which really is a data representation library coupled with graph theoretic algorithms as opposed to the flexibility we might need here). I suggest going for flexibility on the input (so we can open an issue for my suggestion of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In order to get the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, we can save the input edges, or output a sorted list. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are cases in the literature where the graph changes, it's called "active linking". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Turns out we need the graph to be weighted for some applications in the literature. If the reproduction graph is weighted then it changes the death probabilities in the birth-death case. |
||
""" | ||
|
||
from collections import defaultdict | ||
|
||
|
||
class Graph(object): | ||
"""Weighted and directed graph object intended for the graph associated to a | ||
Markov process. Gives easy access to the neighbors of a particular state | ||
needed for various calculations. | ||
|
||
Vertices can be any hashable / immutable python object. Initialize with a | ||
list of edges: | ||
[[node1, node2, weights], ...] | ||
Weights can be omitted for an undirected graph. | ||
|
||
For efficiency, neighbors are cached in dictionaries. Undirected graphs | ||
are implemented as directed graphs in which every edge (s, t) has the | ||
opposite edge (t, s). | ||
""" | ||
|
||
def __init__(self, edges=None, directed=False): | ||
self.directed = directed | ||
self.original_edges = edges | ||
self.out_mapping = defaultdict(lambda: defaultdict(float)) | ||
self.in_mapping = defaultdict(lambda: defaultdict(float)) | ||
self._edges = [] | ||
if edges: | ||
self.add_edges(edges) | ||
|
||
def add_edge(self, source, target, weight=None): | ||
if (source, target) not in self._edges: | ||
self._edges.append((source, target)) | ||
self.out_mapping[source][target] = weight | ||
self.in_mapping[target][source] = weight | ||
if not self.directed and (source != target) and \ | ||
(target, source) not in self._edges: | ||
self._edges.append((target, source)) | ||
self.out_mapping[target][source] = weight | ||
self.in_mapping[source][target] = weight | ||
|
||
def add_edges(self, edges): | ||
for edge in edges: | ||
self.add_edge(*edge) | ||
|
||
def edges(self): | ||
return self._edges | ||
|
||
def vertices(self): | ||
"""Returns the set of vertices of the graph.""" | ||
return list(self.out_mapping.keys()) | ||
|
||
def out_dict(self, source): | ||
"""Returns a dictionary of the outgoing edges of source with weights.""" | ||
return self.out_mapping[source] | ||
|
||
def out_vertices(self, source): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If undirected, why not just have a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We may want to allow the graphs to be directed, see e.g. here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might some of these methods be better as properties? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Getters would make sense for e.g There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's leave it for now. If we fancy refactoring the graph, we can do so later. |
||
"""Returns a list of the outgoing vertices.""" | ||
return list(self.out_mapping[source].keys()) | ||
|
||
def in_dict(self, target): | ||
"""Returns a dictionary of the incoming edges of source with weights.""" | ||
return self.in_mapping[target] | ||
|
||
def in_vertices(self, source): | ||
"""Returns a list of the outgoing vertices.""" | ||
return list(self.in_mapping[source].keys()) | ||
|
||
def __repr__(self): | ||
s = "<Graph: {}>".format(repr(self.original_edges)) | ||
return s | ||
|
||
|
||
## Example Graphs | ||
|
||
|
||
def cycle(length, directed=False): | ||
""" | ||
Produces a cycle of length `length`. | ||
Parameters | ||
---------- | ||
length: int | ||
Number of vertices in the cycle | ||
directed: bool, False | ||
Is the cycle directed? | ||
Returns | ||
------- | ||
a Graph object | ||
""" | ||
|
||
graph = Graph(directed=directed) | ||
edges = [] | ||
for i in range(length - 1): | ||
edges.append((i, i+1)) | ||
edges.append((length - 1, 0)) | ||
graph.add_edges(edges) | ||
return graph | ||
|
||
|
||
def complete_graph(length, loops=True): | ||
""" | ||
Produces a complete graph of size `length`, with loops. | ||
https://en.wikipedia.org/wiki/Complete_graph | ||
|
||
Parameters | ||
---------- | ||
length: int | ||
Number of vertices in the cycle | ||
directed: bool, False | ||
Is the graph directed? | ||
Returns | ||
------- | ||
a Graph object | ||
""" | ||
offset = 1 | ||
if loops: | ||
offset = 0 | ||
graph = Graph(directed=False) | ||
edges = [] | ||
for i in range(length): | ||
for j in range(i + offset, length): | ||
edges.append((i, j)) | ||
graph.add_edges(edges) | ||
return graph |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would have thought this line wasn't needed? Am I wrong?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It depends on how you access the graph class in code,
axelrod.graph.Graph
will fail without this line.We could use
from .graph import Graph, complete_graph, ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought it wouldn't fail but happy as it is :)