# -*- coding: utf-8 -*-
"""Algorithms for working with linearized computational graphs."""
# Copyright (C) 2008-2016 Martin Sandve Alnæs
#
# This file is part of UFL (https://www.fenicsproject.org)
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from collections import defaultdict
from heapq import heapify, heappop
from ufl.corealg.traversal import unique_pre_traversal
from ufl.corealg.multifunction import MultiFunction
# O(n) = O(|V|) = O(|E|), since |E| < c|V| for a fairly small c.
# --- Basic utility functions ---
[docs]def lists(n):
return [[] for i in range(n)]
[docs]def len_items(sequence):
return list(map(len, sequence))
# --- Graph building functions ---
[docs]def build_graph(expr): # O(n)
"""Build a linearized graph from an UFL Expr.
Returns G = (V, E), with V being a list of
graph nodes (Expr objects) in post traversal
ordering and E being a list of edges. Each edge
is represented as a (i, j) tuple where i and j
are vertex indices into V.
"""
V = []
E = []
handled = {}
for v in reversed(list(unique_pre_traversal(expr))):
i = handled.get(v)
if i is None:
i = len(V)
handled[v] = i
V.append(v)
for o in v.ufl_operands:
j = handled[o]
e = (i, j)
E.append(e)
G = V, E
return G
# --- Graph class ---
[docs]class Graph:
"Graph class which computes connectivity on demand."
def __init__(self, expression):
self._V, self._E = build_graph(expression)
self._Ein = None
self._Eout = None
self._Vin = None
self._Vout = None
[docs] def V(self):
return self._V
[docs] def E(self):
return self._E
[docs] def Ein(self):
if self._Ein is None:
self._Ein = extract_incoming_edges((self._V, self._E))
return self._Ein
[docs] def Eout(self):
if self._Eout is None:
self._Eout = extract_outgoing_edges((self._V, self._E))
return self._Eout
[docs] def Vin(self):
if self._Vin is None:
self._Vin = extract_incoming_vertex_connections((self._V, self._E))
return self._Vin
[docs] def Vout(self):
if self._Vout is None:
self._Vout = extract_outgoing_vertex_connections((self._V, self._E))
return self._Vout
def __iter__(self):
return iter((self._V, self._E))
# --- Scheduling algorithms ---
[docs]class HeapItem(object):
def __init__(self, incoming, outgoing, i):
self.incoming = incoming
self.outgoing = outgoing
self.i = i
def __lt__(self, other):
a = (self.outgoing[self.i], self.incoming[self.i])
b = (other.outgoing[other.i], other.incoming[other.i])
return a < b
def __le__(self, other):
a = (self.outgoing[self.i], self.incoming[self.i])
b = (other.outgoing[other.i], other.incoming[other.i])
return a <= b
def __eq__(self, other):
a = (self.outgoing[self.i], self.incoming[self.i])
b = (other.outgoing[other.i], other.incoming[other.i])
return a == b
[docs]def depth_first_ordering(G):
V, E = G
Vin = G.Vin()
Vout = G.Vout()
Ein_count = len_items(Vin)
Eout_count = len_items(Vout)
# Make a list and a heap of the same items
n = len(V)
q = [HeapItem(Ein_count, Eout_count, i) for i in range(n)]
heapify(q)
ordering = []
while q:
item = heappop(q)
iv = item.i
ordering.append(iv)
for i in Vin[iv]:
Eout_count[i] -= 1
# Resort heap, worst case linear time, makes this algorithm
# O(n^2)... TODO: Not good!
heapify(q)
# TODO: Can later accumulate dependencies as well during dft-like
# algorithm.
return ordering
# --- Graph partitoning ---
[docs]class StringDependencyDefiner(MultiFunction):
"""Given an expr, returns a frozenset of its dependencies.
Possible dependency values are:
"c" - depends on runtime information like the cell, local<->global coordinate mappings, facet normals, or coefficients
"x" - depends on local coordinates
"v%d" % i - depends on argument i, for i in [0,rank)
"""
def __init__(self, argument_deps=None, coefficient_deps=None):
MultiFunction.__init__(self)
if argument_deps is None:
argument_deps = {}
if coefficient_deps is None:
coefficient_deps = {}
self.argument_deps = argument_deps
self.coefficient_deps = coefficient_deps
[docs] def expr(self, o):
return frozenset()
[docs] def argument(self, x):
default = frozenset(("v%d" % x.number(), "x")) # TODO: This is missing the part, but this code is ready for deletion anyway?
return self.argument_deps.get(x, default)
[docs] def coefficient(self, x):
default = frozenset(("c", "x"))
return self.coefficient_deps.get(x, default)
[docs] def geometric_quantity(self, x):
deps = frozenset(("c", "x",))
return deps
[docs] def facet_normal(self, o):
deps = frozenset(("c",))
# Enabling coordinate dependency for higher order geometries
# (not handled anywhere else though, so consider this experimental)
# if o.has_higher_degree_cell_geometry():
# deps = deps | frozenset(("x",))
return deps
[docs] def spatial_derivative(self, o): # TODO: What about (basis) functions of which derivatives are constant? Should special-case spatial_derivative in partition().
deps = frozenset(("c",))
# Enabling coordinate dependency for higher order geometries
# (not handled anywhere else though).
# if o.has_higher_degree_cell_geometry():
# deps = deps | frozenset(("x",))
return deps
dd = StringDependencyDefiner()
[docs]def string_set_criteria(v, keys):
# Using sets of ufl objects
key = dd(v)
for k in keys:
key |= k
return frozenset(key)
[docs]def partition(G, criteria=string_set_criteria):
V, E = G
n = len(V)
Vout = G.Vout()
partitions = defaultdict(list)
keys = [None] * n
for iv, v in enumerate(V):
# Get keys from all outgoing edges
edge_keys = [keys[ii] for ii in Vout[iv]]
# Calculate key for this vertex
key = criteria(v, edge_keys)
# Store mappings from key to vertex and back
partitions[key].append(iv)
keys[iv] = key
return partitions, keys