Source code for ffc.quadrature.optimisedquadraturetransformer
# -*- coding: utf-8 -*-
"QuadratureTransformer (optimised) for quadrature code generation to translate UFL expressions."
# Copyright (C) 2009-2011 Kristian B. Oelgaard
#
# This file is part of FFC.
#
# FFC is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# FFC is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with FFC. If not, see <http://www.gnu.org/licenses/>.
#
# Modified by Anders Logg, 2009
from six import iterkeys
from six.moves import xrange as range
from six import advance_iterator as next
# UFL common.
from ufl.utils.sorting import sorted_by_key
from ufl.measure import custom_integral_types, point_integral_types
# UFL Classes.
from ufl.classes import IntValue
from ufl.classes import FloatValue
from ufl.classes import Coefficient
from ufl.classes import Operator
# FFC modules.
from ffc.log import error, ffc_assert
from ffc.quadrature.cpp import format
from ffc.quadrature.quadraturetransformerbase import QuadratureTransformerBase
from ffc.quadrature.quadratureutils import create_permutations
# Symbolics functions
from ffc.quadrature.symbolics import (create_float, create_symbol,
create_product, create_sum,
create_fraction, BASIS, IP, GEO)
[docs]class QuadratureTransformerOpt(QuadratureTransformerBase):
"Transform UFL representation to quadrature code."
def __init__(self, *args):
# Initialise base class.
QuadratureTransformerBase.__init__(self, *args)
# -------------------------------------------------------------------------
# Start handling UFL classes.
# -------------------------------------------------------------------------
# -------------------------------------------------------------------------
# AlgebraOperators (algebra.py).
# -------------------------------------------------------------------------
[docs] def sum(self, o, *operands):
code = {}
# Loop operands that has to be summend.
for op in operands:
# If entries does already exist we can add the code,
# otherwise just dump them in the element tensor.
for key, val in sorted(op.items()):
if key in code:
code[key].append(val)
else:
code[key] = [val]
# Add sums and group if necessary.
for key, val in sorted_by_key(code):
if len(val) > 1:
code[key] = create_sum(val)
elif val:
code[key] = val[0]
else:
error("Where did the values go?")
# If value is zero just ignore it.
if abs(code[key].val) < format["epsilon"]:
del code[key]
return code
[docs] def product(self, o, *operands):
permute = []
not_permute = []
# Sort operands in objects that needs permutation and objects
# that does not.
for op in operands:
# If we get an empty dict, something was zero and so is
# the product.
if not op:
return {}
if len(op) > 1 or (op and firstkey(op) != ()):
permute.append(op)
elif op and firstkey(op) == ():
not_permute.append(op[()])
# Create permutations.
# TODO: After all indices have been expanded I don't think
# that we'll ever get more than a list of entries and values.
permutations = create_permutations(permute)
# Create code.
code = {}
if permutations:
for key, val in permutations.items():
# Sort key in order to create a unique key.
l = sorted(key) # noqa: E741
# TODO: I think this check can be removed for speed
# since we just have a list of objects we should never
# get any conflicts here.
ffc_assert(tuple(l) not in code,
"This key should not be in the code.")
code[tuple(l)] = create_product(val + not_permute)
else:
return {(): create_product(not_permute)}
return code
[docs] def division(self, o, *operands):
ffc_assert(len(operands) == 2,
"Expected exactly two operands (numerator and denominator): " + repr(operands))
# Get the code from the operands.
numerator_code, denominator_code = operands
# TODO: Are these safety checks needed?
ffc_assert(() in denominator_code and len(denominator_code) == 1,
"Only support function type denominator: " + repr(denominator_code))
code = {}
# Get denominator and create new values for the numerator.
denominator = denominator_code[()]
for key, val in numerator_code.items():
code[key] = create_fraction(val, denominator)
return code
[docs] def power(self, o):
# Get base and exponent.
base, expo = o.ufl_operands
# Visit base to get base code.
base_code = self.visit(base)
# TODO: Are these safety checks needed?
ffc_assert(() in base_code and len(base_code) == 1,
"Only support function type base: " + repr(base_code))
# Get the base code and create power.
val = base_code[()]
# Handle different exponents
if isinstance(expo, IntValue):
return {(): create_product([val] * expo.value())}
elif isinstance(expo, FloatValue):
exp = format["floating point"](expo.value())
sym = create_symbol(format["std power"](str(val), exp), val.t,
val, 1)
return {(): sym}
elif isinstance(expo, (Coefficient, Operator)):
exp = self.visit(expo)[()]
sym = create_symbol(format["std power"](str(val), exp), val.t,
val, 1)
return {(): sym}
else:
error("power does not support this exponent: " + repr(expo))
[docs] def abs(self, o, *operands):
# TODO: Are these safety checks needed?
ffc_assert(len(operands) == 1 and () in operands[0] and len(operands[0]) == 1,
"Abs expects one operand of function type: " + repr(operands))
# Take absolute value of operand.
val = operands[0][()]
new_val = create_symbol(format["absolute value"](str(val)), val.t,
val, 1)
return {(): new_val}
[docs] def min_value(self, o, *operands):
# Take minimum value of operands.
val0 = operands[0][()]
val1 = operands[1][()]
t = min(val0.t, val1.t)
# FIXME: I don't know how to implement this the optimized
# way. Is this right?
new_val = create_symbol(format["min value"](str(val0), str(val1)), t)
return {(): new_val}
[docs] def max_value(self, o, *operands):
# Take maximum value of operands.
val0 = operands[0][()]
val1 = operands[1][()]
t = min(val0.t, val1.t)
# FIXME: I don't know how to implement this the optimized
# way. Is this right?
new_val = create_symbol(format["max value"](str(val0), str(val1)), t)
return {(): new_val}
# -------------------------------------------------------------------------
# Condition, Conditional (conditional.py).
# -------------------------------------------------------------------------
[docs] def not_condition(self, o, *operands):
# This is a Condition but not a BinaryCondition, and the
# operand will be another Condition
# Get condition expression and do safety checks.
# Might be a bit too strict?
c, = operands
ffc_assert(len(c) == 1 and firstkey(c) == (),
"Condition for NotCondition should only be one function: " + repr(c))
sym = create_symbol(format["not"](str(c[()])), c[()].t, base_op=c[()].ops() + 1)
return {(): sym}
[docs] def binary_condition(self, o, *operands):
# Get LHS and RHS expressions and do safety checks. Might be
# a bit too strict?
lhs, rhs = operands
ffc_assert(len(lhs) == 1 and firstkey(lhs) == (),
"LHS of Condtion should only be one function: " + repr(lhs))
ffc_assert(len(rhs) == 1 and firstkey(rhs) == (),
"RHS of Condtion should only be one function: " + repr(rhs))
# Map names from UFL to cpp.py.
name_map = {"==": "is equal", "!=": "not equal",
"<": "less than", ">": "greater than",
"<=": "less equal", ">=": "greater equal",
"&&": "and", "||": "or"}
# Get the minimum type
t = min(lhs[()].t, rhs[()].t)
ops = lhs[()].ops() + rhs[()].ops() + 1
cond = str(lhs[()]) + format[name_map[o._name]] + str(rhs[()])
sym = create_symbol(format["grouping"](cond), t, base_op=ops)
return {(): sym}
[docs] def conditional(self, o, *operands):
# Get condition and return values; and do safety check.
cond, true, false = operands
ffc_assert(len(cond) == 1 and firstkey(cond) == (),
"Condtion should only be one function: " + repr(cond))
ffc_assert(len(true) == 1 and firstkey(true) == (),
"True value of Condtional should only be one function: " + repr(true))
ffc_assert(len(false) == 1 and firstkey(false) == (),
"False value of Condtional should only be one function: " + repr(false))
# Get values and test for None
t_val = true[()]
f_val = false[()]
# Get the minimum type and number of operations
# TODO: conditionals are currently always located inside the
# ip loop, therefore the type has to be at least IP (fix bug
# #1082048). This can be optimised.
t = min([cond[()].t, t_val.t, f_val.t, IP])
ops = sum([cond[()].ops(), t_val.ops(), f_val.ops()])
# Create expression for conditional
# TODO: Handle this differently to expose the variables which
# are used to create the expressions.
expr = create_symbol(format["evaluate conditional"](cond[()], t_val,
f_val), t)
num = len(self.conditionals)
name = create_symbol(format["conditional"](num), t)
if expr not in self.conditionals:
self.conditionals[expr] = (t, ops, num)
else:
num = self.conditionals[expr][2]
name = create_symbol(format["conditional"](num), t)
return {(): name}
# -------------------------------------------------------------------------
# FacetNormal, CellVolume, Circumradius, FacetArea (geometry.py).
# -------------------------------------------------------------------------
[docs] def cell_coordinate(self, o): # FIXME
error("This object should be implemented by the child class.")
[docs] def facet_coordinate(self, o): # FIXME
error("This object should be implemented by the child class.")
[docs] def cell_origin(self, o): # FIXME
error("This object should be implemented by the child class.")
[docs] def facet_origin(self, o): # FIXME
error("This object should be implemented by the child class.")
[docs] def cell_facet_origin(self, o): # FIXME
error("This object should be implemented by the child class.")
[docs] def jacobian(self, o): # FIXME
error("This object should be implemented by the child class.")
[docs] def jacobian_determinant(self, o): # FIXME
error("This object should be implemented by the child class.")
[docs] def jacobian_inverse(self, o): # FIXME
error("This object should be implemented by the child class.")
[docs] def facet_jacobian(self, o): # FIXME
error("This object should be implemented by the child class.")
[docs] def facet_jacobian_determinant(self, o): # FIXME
error("This object should be implemented by the child class.")
[docs] def facet_jacobian_inverse(self, o): # FIXME
error("This object should be implemented by the child class.")
[docs] def cell_facet_jacobian(self, o): # FIXME
error("This object should be implemented by the child class.")
[docs] def cell_facet_jacobian_determinant(self, o): # FIXME
error("This object should be implemented by the child class.")
[docs] def cell_facet_jacobian_inverse(self, o): # FIXME
error("This object should be implemented by the child class.")
[docs] def facet_normal(self, o):
components = self.component()
# Safety check.
ffc_assert(len(components) == 1,
"FacetNormal expects 1 component index: " + repr(components))
# Handle 1D as a special case.
# FIXME: KBO: This has to change for mD elements in R^n : m <
# n
if self.gdim == 1: # FIXME: MSA UFL uses shape (1,) now, can we remove the special case here then?
normal_component = format["normal component"](self.restriction, "")
else:
normal_component = format["normal component"](self.restriction,
components[0])
self.trans_set.add(normal_component)
return {(): create_symbol(normal_component, GEO)}
[docs] def cell_normal(self, o): # FIXME
error("This object should be implemented by the child class.")
[docs] def cell_volume(self, o):
# FIXME: KBO: This has to change for higher order elements
# detJ = format["det(J)"](self.restriction)
# volume = format["absolute value"](detJ)
# self.trans_set.add(detJ)
volume = format["cell volume"](self.restriction)
self.trans_set.add(volume)
return {(): create_symbol(volume, GEO)}
[docs] def circumradius(self, o):
# FIXME: KBO: This has to change for higher order elements
circumradius = format["circumradius"](self.restriction)
self.trans_set.add(circumradius)
return {(): create_symbol(circumradius, GEO)}
[docs] def facet_area(self, o):
# FIXME: KBO: This has to change for higher order elements
# NOTE: Omitting restriction because the area of a facet is
# the same on both sides.
# FIXME: Since we use the scale factor, facet area has no
# meaning for cell integrals. (Need check in FFC or UFL).
area = format["facet area"]
self.trans_set.add(area)
return {(): create_symbol(area, GEO)}
[docs] def min_facet_edge_length(self, o):
# FIXME: this has no meaning for cell integrals. (Need check
# in FFC or UFL).
tdim = self.tdim
if tdim < 3:
return self.facet_area(o)
edgelen = format["min facet edge length"](self.restriction)
self.trans_set.add(edgelen)
return {(): create_symbol(edgelen, GEO)}
[docs] def max_facet_edge_length(self, o):
# FIXME: this has no meaning for cell integrals. (Need check
# in FFC or UFL).
tdim = self.tdim
if tdim < 3:
return self.facet_area(o)
edgelen = format["max facet edge length"](self.restriction)
self.trans_set.add(edgelen)
return {(): create_symbol(edgelen, GEO)}
[docs] def cell_orientation(self, o): # FIXME
error("This object should be implemented by the child class.")
[docs] def quadrature_weight(self, o): # FIXME
error("This object should be implemented by the child class.")
[docs] def create_argument(self, ufl_argument, derivatives, component, local_comp,
local_offset, ffc_element, transformation,
multiindices, tdim, gdim, avg):
"Create code for basis functions, and update relevant tables of used basis."
# Prefetch formats to speed up code generation.
f_transform = format["transform"]
f_detJ = format["det(J)"]
# Reset code
code = {}
# Affine mapping
if transformation == "affine":
# Loop derivatives and get multi indices.
for multi in multiindices:
deriv = [multi.count(i) for i in range(tdim)]
if not any(deriv):
deriv = []
# Create mapping and basis name.
mapping, basis = self._create_mapping_basis(component, deriv,
avg, ufl_argument,
ffc_element)
if mapping not in code:
code[mapping] = []
if basis is not None:
# Add transformation if needed.
code[mapping].append(self.__apply_transform(basis,
derivatives,
multi, tdim,
gdim))
# Handle non-affine mappings.
else:
ffc_assert(avg is None,
"Taking average is not supported for non-affine mappings.")
# Loop derivatives and get multi indices.
for multi in multiindices:
deriv = [multi.count(i) for i in range(tdim)]
if not any(deriv):
deriv = []
if transformation in ["covariant piola",
"contravariant piola"]:
for c in range(tdim):
# Create mapping and basis name.
mapping, basis = self._create_mapping_basis(c + local_offset, deriv, avg, ufl_argument, ffc_element)
if mapping not in code:
code[mapping] = []
if basis is not None:
# Multiply basis by appropriate transform.
if transformation == "covariant piola":
dxdX = create_symbol(f_transform("JINV", c,
local_comp, tdim,
gdim,
self.restriction),
GEO)
basis = create_product([dxdX, basis])
elif transformation == "contravariant piola":
detJ = create_fraction(create_float(1),
create_symbol(f_detJ(self.restriction), GEO))
dXdx = create_symbol(f_transform("J", local_comp,
c, gdim, tdim,
self.restriction),
GEO)
basis = create_product([detJ, dXdx, basis])
# Add transformation if needed.
code[mapping].append(self.__apply_transform(basis,
derivatives,
multi, tdim,
gdim))
elif transformation == "double covariant piola":
# g_ij = (Jinv)_ki G_kl (Jinv)lj
i = local_comp // tdim
j = local_comp % tdim
for k in range(tdim):
for l in range(tdim):
# Create mapping and basis name.
mapping, basis = self._create_mapping_basis(
k * tdim + l + local_offset,
deriv, avg, ufl_argument, ffc_element)
if mapping not in code:
code[mapping] = []
if basis is not None:
J1 = create_symbol(
f_transform("JINV", k, i, tdim, gdim,
self.restriction), GEO)
J2 = create_symbol(
f_transform("JINV", l, j, tdim, gdim,
self.restriction), GEO)
basis = create_product([J1, basis, J2])
# Add transformation if needed.
code[mapping].append(
self.__apply_transform(
basis, derivatives, multi,
tdim, gdim))
elif transformation == "double contravariant piola":
# g_ij = (detJ)^(-2) J_ik G_kl J_jl
i = local_comp // tdim
j = local_comp % tdim
for k in range(tdim):
for l in range(tdim):
# Create mapping and basis name.
mapping, basis = self._create_mapping_basis(
k * tdim + l + local_offset,
deriv, avg, ufl_argument, ffc_element)
if mapping not in code:
code[mapping] = []
if basis is not None:
J1 = create_symbol(
f_transform("J", i, k, gdim, tdim,
self.restriction), GEO)
J2 = create_symbol(
f_transform("J", j, l, gdim, tdim,
self.restriction), GEO)
invdetJ = create_fraction(
create_float(1),
create_symbol(f_detJ(self.restriction),
GEO))
basis = create_product([invdetJ, invdetJ, J1,
basis, J2])
# Add transformation if needed.
code[mapping].append(
self.__apply_transform(
basis, derivatives, multi,
tdim, gdim))
else:
error("Transformation is not supported: " + repr(transformation))
# Add sums and group if necessary.
for key, val in list(code.items()):
if len(val) > 1:
code[key] = create_sum(val)
else:
code[key] = val[0]
return code
[docs] def create_function(self, ufl_function, derivatives, component, local_comp,
local_offset, ffc_element, is_quad_element,
transformation, multiindices, tdim, gdim, avg):
"Create code for basis functions, and update relevant tables of used basis."
ffc_assert(ufl_function in self._function_replace_values,
"Expecting ufl_function to have been mapped prior to this call.")
# Prefetch formats to speed up code generation.
f_transform = format["transform"]
f_detJ = format["det(J)"]
# Reset code
code = []
# Handle affine mappings.
if transformation == "affine":
# Loop derivatives and get multi indices.
for multi in multiindices:
deriv = [multi.count(i) for i in range(tdim)]
if not any(deriv):
deriv = []
# Create function name.
function_name = self._create_function_name(component, deriv,
avg, is_quad_element,
ufl_function,
ffc_element)
if function_name:
# Add transformation if needed.
code.append(self.__apply_transform(function_name,
derivatives, multi, tdim, gdim))
# Handle non-affine mappings.
else:
ffc_assert(avg is None,
"Taking average is not supported for non-affine mappings.")
# Loop derivatives and get multi indices.
for multi in multiindices:
deriv = [multi.count(i) for i in range(tdim)]
if not any(deriv):
deriv = []
if transformation in ["covariant piola",
"contravariant piola"]:
for c in range(tdim):
function_name = self._create_function_name(c + local_offset, deriv, avg, is_quad_element, ufl_function, ffc_element)
if function_name:
# Multiply basis by appropriate transform.
if transformation == "covariant piola":
dxdX = create_symbol(f_transform("JINV", c,
local_comp, tdim,
gdim,
self.restriction),
GEO)
function_name = create_product([dxdX, function_name])
elif transformation == "contravariant piola":
detJ = create_fraction(create_float(1),
create_symbol(f_detJ(self.restriction),
GEO))
dXdx = create_symbol(f_transform("J", local_comp,
c, gdim, tdim,
self.restriction),
GEO)
function_name = create_product([detJ, dXdx,
function_name])
# Add transformation if needed.
code.append(self.__apply_transform(function_name,
derivatives, multi,
tdim, gdim))
elif transformation == "double covariant piola":
# g_ij = (Jinv)_ki G_kl (Jinv)lj
i = local_comp // tdim
j = local_comp % tdim
for k in range(tdim):
for l in range(tdim):
# Create mapping and basis name.
function_name = self._create_function_name(
k * tdim + l + local_offset, deriv, avg,
is_quad_element, ufl_function, ffc_element)
J1 = create_symbol(
f_transform("JINV", k, i, tdim, gdim,
self.restriction), GEO)
J2 = create_symbol(
f_transform("JINV", l, j, tdim, gdim,
self.restriction), GEO)
function_name = create_product([J1, function_name,
J2])
# Add transformation if needed.
code.append(self.__apply_transform(
function_name, derivatives, multi, tdim, gdim))
elif transformation == "double contravariant piola":
# g_ij = (detJ)^(-2) J_ik G_kl J_jl
i = local_comp // tdim
j = local_comp % tdim
for k in range(tdim):
for l in range(tdim):
# Create mapping and basis name.
function_name = self._create_function_name(
k * tdim + l + local_offset,
deriv, avg, is_quad_element,
ufl_function, ffc_element)
J1 = create_symbol(
f_transform("J", i, k, tdim, gdim,
self.restriction), GEO)
J2 = create_symbol(
f_transform("J", j, l, tdim, gdim,
self.restriction), GEO)
invdetJ = create_fraction(
create_float(1),
create_symbol(f_detJ(self.restriction), GEO))
function_name = create_product([invdetJ, invdetJ,
J1, function_name,
J2])
# Add transformation if needed.
code.append(self.__apply_transform(function_name,
derivatives,
multi, tdim,
gdim))
else:
error("Transformation is not supported: ",
repr(transformation))
if not code:
return create_float(0.0)
elif len(code) > 1:
code = create_sum(code)
else:
code = code[0]
return code
# -------------------------------------------------------------------------
# Helper functions for Argument and Coefficient
# -------------------------------------------------------------------------
def __apply_transform(self, function, derivatives, multi, tdim, gdim):
"Apply transformation (from derivatives) to basis or function."
f_transform = format["transform"]
# Add transformation if needed.
transforms = []
if self.integral_type not in custom_integral_types:
for i, direction in enumerate(derivatives):
ref = multi[i]
t = f_transform("JINV", ref, direction, tdim, gdim,
self.restriction)
transforms.append(create_symbol(t, GEO))
transforms.append(function)
return create_product(transforms)
# -------------------------------------------------------------------------
# Helper functions for transformation of UFL objects in base class
# -------------------------------------------------------------------------
def _create_symbol(self, symbol, domain):
return {(): create_symbol(symbol, domain)}
def _create_product(self, symbols):
return create_product(symbols)
def _format_scalar_value(self, value):
# print("format_scalar_value: %d" % value)
if value is None:
return {(): create_float(0.0)}
return {(): create_float(value)}
def _math_function(self, operands, format_function):
# TODO: Are these safety checks needed?
ffc_assert(len(operands) == 1 and () in operands[0] and len(operands[0]) == 1,
"MathFunctions expect one operand of function type: " + repr(operands))
# Use format function on value of operand.
operand = operands[0]
for key, val in list(operand.items()):
new_val = create_symbol(format_function(str(val)), val.t, val, 1)
operand[key] = new_val
# raise Exception("pause")
return operand
def _bessel_function(self, operands, format_function):
# TODO: Are these safety checks needed?
# TODO: work on reference instead of copies? (like
# math_function)
ffc_assert(len(operands) == 2,
"BesselFunctions expect two operands of function type: " + repr(operands))
nu, x = operands
ffc_assert(len(nu) == 1 and () in nu,
"Expecting one operand of function type as first argument to BesselFunction : " + repr(nu))
ffc_assert(len(x) == 1 and () in x,
"Expecting one operand of function type as second argument to BesselFunction : " + repr(x))
nu = nu[()]
x = x[()]
if nu is None:
nu = format["floating point"](0.0)
if x is None:
x = format["floating point"](0.0)
sym = create_symbol(format_function(x, nu), x.t, x, 1)
return {(): sym}
# -------------------------------------------------------------------------
# Helper functions for code_generation()
# -------------------------------------------------------------------------
def _count_operations(self, expression):
return expression.ops()
def _create_entry_data(self, val, integral_type):
# Multiply value by weight and determinant
ACCESS = GEO
weight = format["weight"](self.points)
if self.points > 1:
weight += format["component"]("", format["integration points"])
ACCESS = IP
weight = self._create_symbol(weight, ACCESS)[()]
# Create value.
if integral_type in (point_integral_types + custom_integral_types):
trans_set = set()
value = create_product([val, weight])
else:
f_scale_factor = format["scale factor"]
trans_set = set([f_scale_factor])
value = create_product([val, weight,
create_symbol(f_scale_factor, GEO)])
# Update sets of used variables (if they will not be used
# because of optimisations later, they will be reset).
trans_set.update([str(x) for x in value.get_unique_vars(GEO)])
used_points = set([self.points])
ops = self._count_operations(value)
used_psi_tables = set([self.psi_tables_map[b]
for b in value.get_unique_vars(BASIS)])
return (value, ops, [trans_set, used_points, used_psi_tables])