"""Routines and class for Projected Bogoliubov MBPT diagrams."""
from __future__ import division
from builtins import str
from builtins import range
from past.utils import old_div
from operator import xor
import copy
import itertools
import networkx as nx
import adg.bmbpt
import adg.tsd
import adg.diag
[docs]def generate_anomalous_diags(diag, nbody_max):
"""Generate PBMBPT graphs with anomalous lines, with some redundancy.
Args:
diag (BmbptFeynmanDiagram): The diagram to generate children from.
nbody_max (int): The maximal n-body character of a graph vertex.
Returns:
(list): The anomalous graphs generated.
"""
graph = diag.graph
anom_graphs = [graph]
vertices = [vert for vert in graph if not graph.nodes[vert]['operator']]
nx.set_edge_attributes(graph, False, 'anomalous')
# Turn normal propagators in anomalous ones
tweakable_edges = []
for vert1 in vertices:
for vert2 in vertices[vert1:]:
tweakable_edges += [(vert1, vert2) for _
in range(graph.number_of_edges(vert1, vert2))]
for comb in unique_edge_combinations(tweakable_edges,
diag.equivalent_permutations()):
new_graph = copy.deepcopy(graph)
for edge in comb:
key = sum(1 for prop
in new_graph.out_edges(edge[0], keys=True, data=True)
if edge[1] == prop[1] and not prop[3]['anomalous']) - 1
new_graph[edge[0]][edge[1]][key]['anomalous'] = True
anom_graphs.append(new_graph)
# Loop to generate self-contractions
for iter_graph in reversed(anom_graphs):
test_vertices = []
for v in vertices:
test_vertices += [v for _
in range(nbody_max
- (old_div(iter_graph.degree(v), 2)))]
if test_vertices:
for comb in unique_vertex_combinations(
test_vertices, equiv_generating_permutations(iter_graph)):
new_graph = copy.deepcopy(iter_graph)
new_graph.add_edges_from(((vert, vert) for vert in comb),
anomalous=True)
anom_graphs.append(new_graph)
return anom_graphs
[docs]def generate_combinations(iter_list):
"""Generate all possible combinations of length 1 to total.
Attributes:
iter_list (list): A list of iterable objects.
Returns:
(list): A list with all the possible combinations of all lengths.
>>> print(generate_combinations([1, 2, 3]))
[(1,), (1, 2), (1, 2, 3), (1, 3), (2,), (2, 3), (3,)]
"""
combinations = []
for i in range(1, len(iter_list) + 1):
combinations += [k for k in itertools.combinations(iter_list, i)]
# Remove duplicates
return sorted(set(combinations))
[docs]def unique_edge_combinations(edges, permutations):
"""Return all edge combinations not producing equivalent anomalous graphs.
Attributes:
edges (list): The edges that can be modified.
permutations (list): The permutation generating equivalent diagrams.
Returns:
(list): The list of edges producing unique anomalous diagrams.
>>> edges = [(1, 3), (2, 3)]
>>> permutations = [{1: 1, 2: 2}, {1: 2, 2: 1}]
>>> print(unique_edge_combinations(edges, permutations))
[((1, 3), (2, 3)), ((2, 3),)]
"""
edge_combs = generate_combinations(edges)
if len(permutations) <= 1:
return edge_combs
unique_edge_combs = copy.deepcopy(edge_combs)
for idx, comb1 in enumerate(edge_combs):
for comb2 in edge_combs[idx+1:]:
is_same_perm = False
if len(comb1) == len(comb2):
for permutation in permutations[1:]:
new_comb = []
for edge in comb2:
new_0 = permutation[edge[0]] \
if edge[0] in permutation.keys() else edge[0]
new_1 = permutation[edge[1]] \
if edge[1] in permutation.keys() else edge[1]
new_comb.append(tuple((new_0, new_1)))
if sorted(comb1) == sorted(new_comb):
is_same_perm = True
break
if is_same_perm:
unique_edge_combs.remove(comb1)
break
return unique_edge_combs
[docs]def unique_vertex_combinations(vertices, permutations):
"""Return vertex combinations generating unique anomalous diagrams.
Return combinations of vertices on which self-contractions can be added
without producing topologically equivalent PBMBPT diagrams.
Attributes:
vertices (list): Vertices that can be self-contracted.
permutations (list): The permutations that generate equivalent diags.
Returns:
(list): Vertex combinations that do not produce equivalent diags.
>>> vertices = [1, 3]
>>> permutations = [{1: 1, 3: 3}, {1: 3, 3: 1}]
>>> print(unique_vertex_combinations(vertices, permutations))
[(1, 3), (3,)]
"""
combinations = generate_combinations(vertices)
if len(combinations) <= 1:
return combinations
unique_combs = copy.deepcopy(combinations)
for idx, comb1 in enumerate(combinations):
for comb2 in combinations[idx+1:]:
is_same_perm = False
if len(comb1) == len(comb2):
# Skip the identity
for permutation in permutations[1:]:
new_comb2 = [permutation[item]
if item in permutation.keys() else item
for item in comb2]
if sorted(comb1) == sorted(new_comb2):
is_same_perm = True
break
if is_same_perm:
unique_combs.remove(comb1)
break
return unique_combs
[docs]def equiv_generating_permutations(graph):
"""Return the list of permutations generating equivalent PBMBPT diags.
Attributes:
graph (Networkx MultiDiGraph): The graph to be checked.
Returns:
(list): The mappings giving equivalent graphs, inc. identity.
"""
op_nm = nx.algorithms.isomorphism.categorical_node_match('operator', False)
anom_em = nx.algorithms.isomorphism.categorical_multiedge_match('anomalous',
False)
unsort_io_degrees = []
for node in graph:
# Edges going out that are anomalous are annihilators going in
nb_anom_out_edges = sum(1 for edge
in graph.out_edges(node, data=True, keys=True)
if edge[3]['anomalous'])
# And thus should be decounted to NetworkX's out_degree...
out_degree = graph.out_degree(node) - nb_anom_out_edges
# ...and added to NetworkX's in_degree
in_degree = graph.in_degree(node) + nb_anom_out_edges
unsort_io_degrees.append((in_degree, out_degree))
unsort_io_degrees = tuple(unsort_io_degrees)
perm_vertices = [vertex for vertex, degrees in enumerate(unsort_io_degrees)
if not graph.nodes[vertex]['operator']
and unsort_io_degrees.count(degrees) >= 2]
permutations = []
doubled_graph = adg.diag.create_checkable_diagram(graph)
for permutation in itertools.permutations(perm_vertices):
permuted_graph = nx.relabel_nodes(doubled_graph,
dict(list(zip(perm_vertices,
permutation))),
copy=True)
intersection = copy.deepcopy(doubled_graph)
intersection.remove_edges_from(e for e in doubled_graph.edges()
if e not in permuted_graph.edges())
matcher = nx.algorithms.isomorphism.DiGraphMatcher(doubled_graph,
intersection,
node_match=op_nm,
edge_match=anom_em)
if matcher.is_isomorphic():
permutations.append(dict(list(zip(perm_vertices, permutation))))
return permutations
[docs]def filter_new_diagrams(new_diags, old_diags):
"""Eliminate diagrams having a topologically equivalent diag.
Attibutes:
new_diags (list): The list of newly created PBMBPT diagrams.
old_diags (list): The list of already checked PBMBPT diagrams.
"""
iso = nx.algorithms.isomorphism
op_nm = iso.categorical_node_match('operator', False)
anom_em = iso.categorical_multiedge_match('anomalous', False)
for ind, new_diag in adg.tools.reversed_enumerate(new_diags):
# Only anomalous props that are not self-contractions can produce
# topologically equivalent diagrams through our generation process
if new_diag.has_anom_non_selfcontracted_props():
for new_diag_2 in new_diags[:ind]:
if new_diag_2.io_degrees == new_diag.io_degrees \
and new_diag_2.has_anom_non_selfcontracted_props():
matcher = iso.DiGraphMatcher(new_diag.check_graph,
new_diag_2.check_graph,
node_match=op_nm,
edge_match=anom_em)
if matcher.is_isomorphic():
del new_diags[ind]
break
else:
# Go through the list backwards: first items are BMBPT diags
for old_diag in reversed(old_diags):
if not isinstance(old_diag, ProjectedBmbptDiagram):
# All PBMBPT diags have been parsed
break
if old_diag.io_degrees == new_diag.io_degrees \
and old_diag.has_anom_non_selfcontracted_props():
matcher = iso.DiGraphMatcher(old_diag.check_graph,
new_diag.check_graph,
node_match=op_nm,
edge_match=anom_em)
if matcher.is_isomorphic():
del new_diags[ind]
break
[docs]class ProjectedBmbptDiagram(adg.bmbpt.BmbptFeynmanDiagram):
"""Describes a PBMBPT diagram with its related properties.
Attributes:
two_or_three_body (int): The 2 or 3-body characted of the vertices.
time_tag (int): The tag number associated to the diagram's
associated TSD.
tsd_is_tree (bool): The tree or non-tree character of the
associated TSD.
feynman_exp (str): The Feynman expression associated to the diagram.
diag_exp (str): The Goldstone expression associated to the diagram.
vert_exp (list): The expression associated to the vertices.
hf_type (str): The Hartree-Fock, non-Hartree-Fock or Hartree-Fock for
the energy operator only character of the graph.
unique_id (int): A unique number associated to the diagram.
vertex_exchange_sym_factor (int): Lazy-initialized symmetry factor
associated to the vertex exchange, stored to avoid being computed
several times.
check_graph (NetworkX MultiDiGraph): A copy of the graph that can be
used for topological equivalence checks (lazy-initialized).
"""
__slots__ = ('_check_graph',)
def __init__(self, graph, unique_id, tag, child_tag):
"""Generate a PBMBPT diagram by copying a BMBPT one.
Args:
graph (NetworkX MultiDiGraph)): The graph of interest.
"""
adg.bmbpt.BmbptFeynmanDiagram.__init__(self, graph, unique_id)
self.tags = [tag, child_tag]
self.set_io_degrees()
[docs] def set_io_degrees(self):
"""Attribute the correct in- and out-degrees to a PBMBPT diagram."""
unsort_io_degrees = []
for node in self.graph:
# Edges going out that are anomalous are annihilators going in
nb_anom_out_edges = sum(1 for edge
in self.graph.out_edges(nbunch=node,
data=True,
keys=True)
if edge[3]['anomalous'])
# And thus should be decounted to NetworkX's out_degree...
out_degree = self.graph.out_degree(node) - nb_anom_out_edges
# ...and added to NetworkX's in_degree
in_degree = self.graph.in_degree(node) + nb_anom_out_edges
unsort_io_degrees.append((in_degree, out_degree))
self.unsort_io_degrees = tuple(unsort_io_degrees)
self.io_degrees = tuple(sorted(self.unsort_io_degrees))
self._check_graph = None
[docs] def attribute_qp_labels(self):
"""Attribute the appropriate qp labels to the graph's propagators."""
idx_counter = 1
for prop in self.graph.edges(keys=True, data=True):
if prop[3]['anomalous']:
prop[3]['qp_state'] = "k_{%i}k_{%i}" % (idx_counter + 1,
idx_counter)
idx_counter += 2
else:
prop[3]['qp_state'] = "k_{%i}" % idx_counter
idx_counter += 1
[docs] def vertex_expression(self, vertex):
"""Return the expression associated to a given vertex.
Args:
vertex (int): The vertex of interest in the graph.
Returns:
(str): The LaTeX expression associated to the vertex.
"""
expression = r"\epsilon^{" \
+ "".join("%s"
% prop[3]['qp_state']
for prop
in self.graph.out_edges(vertex, keys=True, data=True)
if not prop[3]['anomalous']) \
+ "}_{" \
+ "".join("%s"
% (prop[3]['qp_state'].split("}")[0] + "}"
if prop[3]['anomalous'] else prop[3]['qp_state'])
for prop
in self.graph.in_edges(vertex, keys=True, data=True)) \
+ "".join("%s"
% (prop[3]['qp_state'].split("}")[1] + "}")
for prop
in self.graph.out_edges(vertex, keys=True, data=True)
if prop[3]['anomalous']) \
+ "}"
return expression
[docs] def has_sign_factor(self):
"""Return True if a sign factor is associated to the diagram.
Wrapper allowing for easy refactoring of expression code.
Returns:
(boolean): The presence of a sign factor.
"""
# Use exclusive or for the sign factor
return xor(self.has_crossing_sign(), self.has_anom_props_linked_sign())
[docs] def symmetry_factor(self):
"""Return the overall symmetry factor of the diagram.
Returns:
(str): The combination of all symmetry factors.
"""
sym_factor = ""
vertex_sym = 1
anom_factor = self.anomalous_contractions_factor()
for vertex_degrees in self.unsort_io_degrees:
if self.unsort_io_degrees.count(vertex_degrees) >= 2:
vertex_sym = self.vertex_exchange_sym_factor
break
sym_factor += "%i" % (vertex_sym*anom_factor) \
if (vertex_sym*anom_factor) > 1 else ""
sym_factor += self.multiplicity_symmetry_factor()
return sym_factor
[docs] def multiplicity_symmetry_factor(self):
"""Return the symmetry factor associated with propagators multiplicity.
Returns:
(str): The symmetry factor associated with equivalent lines.
"""
factor = ""
# Account for up to three-body operators
prop_multiplicity = [0 for _ in range(6)]
for vertex_i in self.graph:
for vertex_j in self.graph:
nb_normal_props = sum(1 for prop
in self.graph.edges(vertex_i,
keys=True,
data='anomalous')
if prop[1] == vertex_j and not prop[3])
nb_anomalous_props = sum(1 for prop
in self.graph.edges(vertex_i,
keys=True,
data='anomalous')
if prop[1] == vertex_j and prop[3])
if nb_anomalous_props >= 2:
prop_multiplicity[nb_anomalous_props - 1] += 1
if nb_normal_props >= 2:
prop_multiplicity[nb_normal_props - 1] += 1
for prop_id, multiplicity in enumerate(prop_multiplicity):
if multiplicity == 1:
factor += "(%i!)" % (prop_id+1)
elif multiplicity >= 2:
factor += "(%i!)" % (prop_id+1) + "^%i" % multiplicity
return factor
[docs] def anomalous_contractions_factor(self):
"""Return the factor associated with anomalous self-contractions.
Returns:
(int): The anomalous self-contractions factor.
"""
nb_self_contractions = len(list(nx.selfloop_edges(self.graph)))
return pow(2, nb_self_contractions)
[docs] def equivalent_permutations(self):
"""Return the permutations generating equivalent diagrams.
Returns:
(list): Vertices permutations as dictionnaries.
"""
op_nm = nx.algorithms.isomorphism.categorical_node_match('operator',
False)
anom_em = nx.algorithms.isomorphism.categorical_multiedge_match(
'anomalous', False)
perm_vertices = [vertex for vertex, degrees
in enumerate(self.unsort_io_degrees)
if not self.graph.nodes[vertex]['operator']
and self.unsort_io_degrees.count(degrees) >= 2]
permutations = []
for perm in itertools.permutations(perm_vertices):
permuted_graph = nx.relabel_nodes(self.check_graph,
dict(list(zip(perm_vertices,
perm))),
copy=True)
# Check for a permutation that leaves the graph unchanged
# (only way to keep the edge list of the same length)
intersection = copy.deepcopy(self.check_graph)
intersection.remove_edges_from(e for e in self.check_graph.edges()
if e not in permuted_graph.edges())
check = nx.algorithms.isomorphism.DiGraphMatcher(self.check_graph,
intersection,
node_match=op_nm,
edge_match=anom_em)
if check.is_isomorphic():
permutations.append(dict(list(zip(perm_vertices, perm))))
return permutations
[docs] def has_anom_props_linked_sign(self):
"""Return True if there is a minus sign associated to anom props.
Anomalous propagators departing to higher vertices introduce a sign
factor if a normal propagator is going to an even higher vertex, as it
departs from the canonical representation used for numerator
extraction.
Returns:
(bool): The presence of the sign factor.
"""
counter = 0
for vertex in self.graph:
for prop_a in self.graph.out_edges(vertex,
keys=True,
data='anomalous'):
# Check that prop_a is anomalous and not a self-contraction
if prop_a[3] and prop_a[1] != prop_a[0]:
for prop_b in self.graph.out_edges(vertex,
keys=True,
data='anomalous'):
# Check for a normal prop going to a higher vertex
if prop_b[1] > prop_a[1] and not prop_b[3]:
counter += 1
# True if overall factor is odd
return counter % 2 == 1
[docs] def has_anom_non_selfcontracted_props(self):
"""Return True if the diagram has anomalous propagators.
Returns:
(bool): The presence of anomalous propagators.
"""
for prop in self.graph.edges(keys=True, data='anomalous'):
if prop[3] and prop[1] != prop[0]:
return True
return False
[docs] def write_diag_exps(self, latex_file, norder):
"""Write the expressions associated to a diagram in the LaTeX file.
Args:
latex_file (file): The LaTeX outputfile of the program.
norder (int): The order in BMBPT formalism.
"""
latex_file.write(
"\\begin{align}\n\\text{PO}%i.%i.%i\n" % (norder,
(self.tags[0] + 1),
(self.tags[1] + 1))
+ "&= %s" % self.feynman_exp
+ r" \nonumber \\" + "\n"
+ "&= %s\\end{align}\n" % self.diag_exp)
@property
def check_graph(self):
"""Return a graph that can be used for topological equivalence checks.
Lazy-initialized to reduce memory and CPU costs as this operation
requires a deep copy.
Returns:
(NetworkX MultiDiGraph): The graph with doubled anomalous props.
"""
if self._check_graph is None:
self._check_graph = adg.diag.create_checkable_diagram(self.graph)
return self._check_graph
[docs] def write_graph(self, latex_file, directory, write_time):
"""Write the PBMBPT graph and its associated TSD to the LaTeX file.
Args:
latex_file (file): The LaTeX output file of the program.
directory (str): The path to the result folder.
write_time (bool): ``True`` if we want informations on the
associated TSDs.
"""
latex_file.write('\n\\begin{center}\n')
adg.diag.draw_diagram(directory, latex_file,
"%i_%i" % (self.tags[0], self.tags[1]), 'diag')
if write_time:
latex_file.write(
'\\hspace{10pt} $\\rightarrow$ \\hspace{10pt} T%i:'
% (self.time_tag + 1))
adg.diag.draw_diagram(directory, latex_file,
str(self.time_tag), 'time')
latex_file.write('\n\\end{center}\n\n')