Source code for ffc.quadrature.product

# -*- coding: utf-8 -*-
"This file implements a class to represent a product."

# Copyright (C) 2009-2010 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/>.
#
# First added:  2009-07-12
# Last changed: 2010-03-11

from functools import reduce

# FFC modules
from ffc.log import error
from ffc.quadrature.cpp import format

# FFC quadrature modules
from .symbolics import create_float
from .symbolics import create_product
from .symbolics import create_sum
from .symbolics import create_fraction
from .expr import Expr

# FFC quadrature modules
from .floatvalue import FloatValue


[docs]class Product(Expr): __slots__ = ("vrs", "_expanded") def __init__(self, variables): """Initialise a Product object, it derives from Expr and contains the additional variables: vrs - a list of variables _expanded - object, an expanded object of self, e.g., self = x*(2+y) -> self._expanded = (2*x + x*y) (a sum), or self = 2*x -> self._expanded = 2*x (self). NOTE: self._prec = 2.""" # Initialise value, list of variables, class. self.val = 1.0 self.vrs = [] self._prec = 2 # Initially set _expanded to True. self._expanded = True # Process variables if we have any. if variables: # Remove nested Products and test for expansion. float_val = 1.0 for var in variables: # If any value is zero the entire product is zero. if var.val == 0.0: self.val = 0.0 self.vrs = [create_float(0.0)] float_val = 0.0 break # Collect floats into one variable if var._prec == 0: # float float_val *= var.val continue # Take care of product such that we don't create # nested products. elif var._prec == 2: # prod # If expanded product is a float, just add it. if var._expanded and var._expanded._prec == 0: float_val *= var._expanded.val # If expanded product is symbol, this product is # still expanded and add symbol. elif var._expanded and var._expanded._prec == 1: self.vrs.append(var._expanded) # If expanded product is still a product, add the # variables. elif var._expanded and var._expanded._prec == 2: # Add copies of the variables of other product # (collect floats). if var._expanded.vrs[0]._prec == 0: float_val *= var._expanded.vrs[0].val self.vrs += var._expanded.vrs[1:] continue self.vrs += var._expanded.vrs # If expanded product is a sum or fraction, we # must expand this product later. elif var._expanded and var._expanded._prec in (3, 4): self._expanded = False self.vrs.append(var._expanded) # Else the product is not expanded, and we must # expand this one later else: self._expanded = False # Add copies of the variables of other product # (collect floats). if var.vrs[0]._prec == 0: float_val *= var.vrs[0].val self.vrs += var.vrs[1:] continue self.vrs += var.vrs continue # If we have sums or fractions in the variables the # product is not expanded. elif var._prec in (3, 4): # sum or frac self._expanded = False # Just add any variable at this point to list of new # vars. self.vrs.append(var) # If value is 1 there is no need to include it, unless it # is the only parameter left i.e., 2*0.5 = 1. if float_val and float_val != 1.0: self.val = float_val self.vrs.append(create_float(float_val)) # If we no longer have any variables add the float. elif not self.vrs: self.val = float_val self.vrs = [create_float(float_val)] # If 1.0 is the only value left, add it. elif abs(float_val - 1.0) < format["epsilon"] and not self.vrs: self.val = 1.0 self.vrs = [create_float(1)] # If we don't have any variables the product is zero. else: self.val = 0.0 self.vrs = [create_float(0)] # The type is equal to the lowest variable type. self.t = min([v.t for v in self.vrs]) # Sort the variables such that comparisons work. self.vrs.sort() # Compute the representation now, such that we can use it # directly in the __eq__ and __ne__ methods (improves # performance a bit, but only when objects are cached). self._repr = "Product([%s])" % ", ".join([v._repr for v in self.vrs]) # Use repr as hash value. self._hash = hash(self._repr) # Store self as expanded value, if we did not encounter any # sums or fractions. if self._expanded: self._expanded = self # Print functions. def __str__(self): "Simple string representation which will appear in the generated code." # If we have more than one variable and the first float is -1 # exlude the 1. if len(self.vrs) > 1 and self.vrs[0]._prec == 0 and self.vrs[0].val == -1.0: # Join string representation of members by multiplication return format["sub"](["", format["mul"]([str(v) for v in self.vrs[1:]])]) return format["mul"]([str(v) for v in self.vrs]) # Binary operators. def __add__(self, other): "Addition by other objects." # NOTE: Assuming expanded variables. # If two products are equal, add their float values. if other._prec == 2 and self.get_vrs() == other.get_vrs(): # Return expanded product, to get rid of 3*x + -2*x -> x, not 1*x. return create_product([create_float(self.val + other.val)] + list(self.get_vrs())).expand() elif other._prec == 1: # sym if self.get_vrs() == (other,): # Return expanded product, to get rid of -x + x -> 0, # not product(0). return create_product([create_float(self.val + 1.0), other]).expand() # Return sum return create_sum([self, other]) def __sub__(self, other): "Subtract other objects." if other._prec == 2 and self.get_vrs() == other.get_vrs(): # Return expanded product, to get rid of 3*x + -2*x -> x, # not 1*x. return create_product([create_float(self.val - other.val)] + list(self.get_vrs())).expand() elif other._prec == 1: # sym if self.get_vrs() == (other,): # Return expanded product, to get rid of -x + x -> 0, # not product(0). return create_product([create_float(self.val - 1.0), other]).expand() # Return sum return create_sum([self, create_product([FloatValue(-1), other])]) def __mul__(self, other): "Multiplication by other objects." # If product will be zero. if self.val == 0.0 or other.val == 0.0: return create_float(0) # If other is a Sum or Fraction let them handle it. if other._prec in (3, 4): # sum or frac return other.__mul__(self) # NOTE: We expect expanded sub-expressions with no nested # operators. Create new product adding float or symbol. if other._prec in (0, 1): # float or sym return create_product(self.vrs + [other]) # Create new product adding all variables from other Product. return create_product(self.vrs + other.vrs) def __truediv__(self, other): "Division by other objects." # If division is illegal (this should definitely not happen). if other.val == 0.0: error("Division by zero.") # If fraction will be zero. if self.val == 0.0: return self.vrs[0] # If other is a Sum we can only return a fraction. # NOTE: Expect that other is expanded i.e., x + x -> 2*x which # can be handled # TODO: Fix x / (x + x*y) -> 1 / (1 + y). # Or should this be handled when reducing a fraction? if other._prec == 3: # sum return create_fraction(self, other) # Handle division by FloatValue, Symbol, Product and Fraction. # NOTE: assuming that we get expanded variables. # Copy numerator, and create list for denominator. num = self.vrs[:] denom = [] # Add floatvalue, symbol and products to the list of denominators. if other._prec in (0, 1): # float or sym denom = [other] elif other._prec == 2: # prod # Get copy. denom = other.vrs[:] # fraction. else: error("Did not expected to divide by fraction.") # Loop entries in denominator and remove from numerator (and # denominator). for d in denom[:]: # Add the inverse of a float to the numerator and continue. if d._prec == 0: # float num.append(create_float(1.0 / d.val)) denom.remove(d) continue if d in num: num.remove(d) denom.remove(d) # Create appropriate return value depending on remaining data. if len(num) > 1: # TODO: Make this more efficient? # Create product and expand to reduce # Product([5, 0.2]) == Product([1]) -> Float(1). num = create_product(num).expand() elif num: num = num[0] # If all variables in the numerator has been eliminated we # need to add '1'. else: num = create_float(1) if len(denom) > 1: return create_fraction(num, create_product(denom)) elif denom: return create_fraction(num, denom[0]) # If we no longer have a denominater, just return the # numerator. return num __div__ = __truediv__ # Public functions.
[docs] def expand(self): "Expand all members of the product." # If we just have one variable, compute the expansion of it # (it is not a Product, so it should be safe). We need this to # get rid of Product([Symbol]) type expressions. if len(self.vrs) == 1: self._expanded = self.vrs[0].expand() return self._expanded # If product is already expanded, simply return the expansion. if self._expanded: return self._expanded # Sort variables such that we don't call the '*' operator more # than we have to. float_syms = [] sum_fracs = [] for v in self.vrs: if v._prec in (0, 1): # float or sym float_syms.append(v) continue exp = v.expand() # If the expanded expression is a float, sym or product, # we can add the variables. if exp._prec in (0, 1): # float or sym float_syms.append(exp) elif exp._prec == 2: # prod float_syms += exp.vrs else: sum_fracs.append(exp) # If we have floats or symbols add the symbols to the rest as a single # product (for speed). if len(float_syms) > 1: sum_fracs.append(create_product(float_syms)) elif float_syms: sum_fracs.append(float_syms[0]) # Use __mult__ to reduce list to one single variable. # TODO: Can this be done more efficiently without creating all # the intermediate variables? self._expanded = reduce(lambda x, y: x * y, sum_fracs)
return self._expanded
[docs] def get_unique_vars(self, var_type): "Get unique variables (Symbols) as a set." # Loop all members and update the set. var = set() for v in self.vrs: var.update(v.get_unique_vars(var_type))
return var
[docs] def get_var_occurrences(self): """Determine the number of times all variables occurs in the expression. Returns a dictionary of variables and the number of times they occur. """ # TODO: The product should be expanded at this stage, should # we check this? # Create dictionary and count number of occurrences of each # variable. d = {} for v in self.vrs: if v in d: d[v] += 1 continue d[v] = 1
return d
[docs] def get_vrs(self): "Return all 'real' variables." # A product should only have one float value after # initialisation. # TODO: Use this knowledge directly in other classes? if self.vrs[0]._prec == 0: # float return tuple(self.vrs[1:])
return tuple(self.vrs)
[docs] def ops(self): "Get the number of operations to compute product." # It takes n-1 operations ('*') for a product of n members. op = len(self.vrs) - 1 # Loop members and add their count. for v in self.vrs: op += v.ops() # Subtract 1, if the first member is -1 i.e., -1*x*y -> x*y is # only 1 op. if self.vrs[0]._prec == 0 and self.vrs[0].val == -1.0: op -= 1
return op
[docs] def reduce_ops(self): "Reduce the number of operations to evaluate the product." # It's not possible to reduce a product if it is already # expanded and it should be at this stage. # TODO: Is it safe to return self.expand().reduce_ops() if # product is not expanded? And do we want to? # TODO: This should crash if it goes wrong
return self._expanded
[docs] def reduce_vartype(self, var_type): """Reduce expression with given var_type. It returns a tuple (found, remain), where 'found' is an expression that only has variables of type == var_type. If no variables are found, found=(). The 'remain' part contains the leftover after division by 'found' such that: self = found*remain. """ # Sort variables according to type. found = [] remains = [] for v in self.vrs: if v.t == var_type: found.append(v) continue remains.append(v) # Create appropriate object for found. if len(found) > 1: found = create_product(found) elif found: found = found.pop() # We did not find any variables. else: return [((), self)] # Create appropriate object for remains. if len(remains) > 1: remains = create_product(remains) elif remains: remains = remains.pop() # We don't have anything left. else: return [(self, create_float(1))] # Return whatever we found.
return [(found, remains)]