"""Attaches particles to a NetworkX graph, when EDGES represent particles.
Note about convention:
A particle's "out" node is the one from which it is outgoing. Similarly, its
"in" node is the one into which it is incoming.
e.g. a -->-- b : a is the "out" node, b is the "in" node for this edge.
An "incoming edge" to a node is an edge whose destination node is the node
in question. Similarly, an "outgoing edge" from a node is any edge whose source
is the node in question.
e.g. c ---p->-- d: here p is an incoming edge to node d, whilst it is also an
outgoing edge for node c.
"""
from __future__ import absolute_import
from pythiaplotter.utils.logging_config import get_logger
import networkx as nx
log = get_logger(__name__)
[docs]def assign_particles_edges(edge_particles):
"""Attach particles to directed graph edges when EDGES represent particles.
The graph itself is a networkx MultiDiGraph: a directed graph, that can have
multiple edges between 2 nodes. We distinguish these via the edge['barcode']
attribute, where the barcode value = particle barcode. We can use
MultiDiGraph.edges(data=True) to correctly iterate over *all* edges.
Additionally marks particles as initial/final state as necessary, based on
whether they have any parents/children, respectively.
Parameters
----------
edge_particles: list[EdgeParticle]
The Particle in each EdgeParticle will be assigned to a graph edge,
using the vertex information in the EdgeParticle object.
Returns
-------
NetworkX.MultiDiGraph
Directed graph with particles assigned to edges, and nodes to represent relationships.
"""
gr = nx.MultiDiGraph(attr=None) # placeholder attr for later in printer
# assign an edge for each Particle object, preserving direction
# note that NetworkX auto adds nodes when edges are added
for ep in edge_particles:
gr.add_edge(ep.vtx_out_barcode, ep.vtx_in_barcode,
barcode=ep.barcode, particle=ep.particle)
log.debug("Add edge %s > %s for %s", ep.vtx_out_barcode, ep.vtx_in_barcode, ep.particle)
# Get in-degree for nodes so we can mark the initial state ones
# (those with no incoming edges) and their particles
for node, degree in gr.in_degree_iter(gr.nodes()):
if degree == 0:
for _, _, edge_data in gr.out_edges_iter(node, data=True):
edge_data['particle'].initial_state = True
# Do same for final-state nodes/particles (nodes which have no outgoing edges)
for node, degree in gr.out_degree_iter(gr.nodes()):
if degree == 0:
for _, _, edge_data in gr.in_edges_iter(node, data=True):
edge_data['particle'].final_state = True
log.debug("Edges after assigning: %s", gr.edges())
log.debug("Nodes after assigning: %s", gr.nodes())
return gr
[docs]def remove_particle_edge(graph, edge):
"""Remove a particle edge from the graph.
Rewires the other particles such that the nodes at either end of the edge
essentially merge together into one:
- any children of the edge, are now children of the edge's parents
- any incoming edges into the edge's in node are now
incoming to the out node (ie where the parents are incoming to)
Parameters
----------
graph: NetworkX.MultiDiGraph
edge : (int, int)
Outgoing node, incoming node
"""
# rewire: ensure all incoming parents and all outgoing children use same vtx
out_node, in_node = edge
parents = graph.predecessors(out_node)
if (len(parents) == 0 and len(graph.successors(out_node)) == 0):
graph.remove_node(out_node)
return
children = graph.successors(in_node)
if (len(children) == 0 and len(graph.predecessors(in_node)) == 1):
graph.remove_node(in_node)
return
for child in children:
these = graph[in_node][child]
for x in these: # since Multi DiGraph
log.debug("Adding %d %d %s", out_node, child, these[x])
graph.add_edge(out_node, child, **these[x])
# incoming edges to the in_node
for out_e, in_e, edge_data in graph.in_edges(in_node, data=True):
if (out_e, in_e) == (out_node, in_node):
continue # ignore the original edge itself!
log.debug("Adding %d %d %s", out_e, out_node, **edge_data)
graph.add_edge(out_e, out_node, **edge_data)
graph.remove_node(in_node)
[docs]def remove_redundant_edges(graph):
"""Remove redundant particle edges from a graph.
A particle is redundant when:
1) > 0 'child' particles (those outgoing from the particle's incoming node
- this ensures we don't remove final-state particles)
2) 1 'parent' particle with same PDGID (incoming into the particle's
outgoing node - also ensures we don't remove initial-state particles)
3) 0 'sibling' particles (those outgoing from the particle's outgoing node)
Note that NetworkX includes an edge as its own sibling, so actually we
require len(sibling_edges) == 1
e.g.::
--q-->-g->-g->-g->--u---->
| |
--q-> --ubar->
Remove the middle gluon and last gluon, since they add no information.
These redundants are useful to keep if considering MC internal workings,
but otherwise are just confusing and a waste of space.
Since it loops over the list of graph edges, we can only remove one edge in
a loop, otherwise adding extra/replacement edges ruins the sibling counting
and doesn't remove redundants. So the method loops through the graph edges
until there are no more redundant edges.
Since we are dealing with MultiDiGraph, we have to be careful about siblings
that actually span the same set of nodes - these shouldn't be removed.
There is probably a more sensible way to do this, currently brute
force and slow.
Parameters
----------
graph : NetworkX.MultiDiGraph
Graph to remove redundant nodes from.
"""
done_removing = False
while not done_removing:
done_removing = True
for out_node, in_node, edge_data in graph.edges_iter(data=True):
# get all incoming edges to this particle's out node (parents)
parent_edges = graph.in_edges(out_node, data=True)
# get all outgoing edges from this particle's out node (siblings)
sibling_edges = graph.out_edges(out_node)
# get all outgoing edges from this particle's in node (children)
child_edges = graph.out_edges(in_node)
if len(parent_edges) == 1 and len(child_edges) != 0 and len(sibling_edges) == 1:
parent_out, parent_in, parent_data = parent_edges[0]
# Do removal if parent PDGID matches
if parent_data["particle"].pdgid == edge_data["particle"].pdgid:
log.debug("Doing edge: %d %d", out_node, in_node)
log.debug("Parent edges: %s", parent_edges)
log.debug("Child edges: %s", child_edges)
done_removing = False
log.debug("Removing redundant edge (%d, %d) %s", out_node, in_node, edge_data)
remove_particle_edge(graph, (out_node, in_node))
break