Select Git revision

Martin Bauer authored
moments.py 25.59 KiB
# -*- coding: utf-8 -*-
r"""
Module Overview
~~~~~~~~~~~~~~~
This module provides functions to
- generate moments according to certain patterns
- compute moments of discrete probability distribution functions
- create transformation rules into moment space
- orthogonalize moment bases
Moment Representation
~~~~~~~~~~~~~~~~~~~~~
Moments can be represented in two ways:
- by an index :math:`i,j`: defined as :math:`m_{ij} := \sum_{\mathbf{d} \in stencil} <\mathbf{d}, \mathbf{x}> f_i`
- or by a polynomial in the variables x,y and z. For example the polynomial :math:`x^2 y^1 z^3 + x + 1` is
describing the linear combination of moments: :math:`m_{213} + m_{100} + m_{000}`
The polynomial description is more powerful, since one polynomial can express a linear combination of single moments.
All moment polynomials have to use ``MOMENT_SYMBOLS`` (which is a module variable) as degrees of freedom.
Example ::
>>> from lbmpy.moments import MOMENT_SYMBOLS
>>> x, y, z = MOMENT_SYMBOLS
>>> second_order_moment = x*y + y*z
Functions
~~~~~~~~~
"""
import itertools
import math
from copy import copy
from collections import Counter, defaultdict
from typing import Iterable, List, Sequence, Tuple, TypeVar, Optional
import sympy as sp
from pystencils.cache import memorycache
from pystencils.sympyextensions import remove_higher_order_terms
MOMENT_SYMBOLS = sp.symbols('x y z')
T = TypeVar('T')
# ------------------------------ Discrete (Exponent Tuples) ------------------------------------------------------------
def moment_multiplicity(exponent_tuple):
"""
Returns number of permutations of the given moment tuple
Example:
>>> moment_multiplicity((2,0,0))
3
>>> list(moment_permutations((2,0,0)))
[(0, 0, 2), (0, 2, 0), (2, 0, 0)]
"""
c = Counter(exponent_tuple)
result = math.factorial(len(exponent_tuple))
for d in c.values():
result //= math.factorial(d)
return result
def pick_representative_moments(moments):
"""Picks the representative i.e. of each permutation group only one is kept"""
to_remove = []
for m in moments:
permutations = list(moment_permutations(m))
to_remove += permutations[1:]
return set(moments) - set(to_remove)
def moment_permutations(exponent_tuple):
"""Returns all (unique) permutations of the given tuple"""
return __unique_permutations(exponent_tuple)
def moments_of_order(order, dim=3, include_permutations=True):
"""All tuples of length 'dim' which sum equals 'order'"""
for item in __fixed_sum_tuples(dim, order, ordered=not include_permutations):
assert(len(item) == dim)
assert(sum(item) == order)
yield item
def moments_up_to_order(order, dim=3, include_permutations=True):
"""All tuples of length 'dim' which sum is smaller than 'order' """
single_moment_iterators = [moments_of_order(o, dim, include_permutations) for o in range(order + 1)]
return tuple(itertools.chain(*single_moment_iterators))
def moments_up_to_component_order(order, dim=3):
"""All tuples of length 'dim' where each entry is smaller or equal to 'order' """
return tuple(itertools.product(*[range(order + 1)] * dim))
def extend_moments_with_permutations(exponent_tuples):
"""Returns all permutations of the given exponent tuples"""
all_moments = []
for i in exponent_tuples:
all_moments += list(moment_permutations(i))
return __unique(all_moments)
# ------------------------------ Representation Conversions ------------------------------------------------------------
def exponent_to_polynomial_representation(exponent_tuple):
"""
Converts an exponent tuple to corresponding polynomial representation
Example:
>>> exponent_to_polynomial_representation( (2,1,3) )
x**2*y*z**3
"""
poly = 1
for sym, tuple_entry in zip(MOMENT_SYMBOLS[:len(exponent_tuple)], exponent_tuple):
poly *= sym ** tuple_entry
return poly
def exponents_to_polynomial_representations(sequence_of_exponent_tuples):
"""Applies :func:`exponent_to_polynomial_representation` to given sequence"""
return tuple([exponent_to_polynomial_representation(t) for t in sequence_of_exponent_tuples])
def polynomial_to_exponent_representation(polynomial, dim=3):
"""
Converts a linear combination of moments in polynomial representation into exponent representation
:returns list of tuples where the first element is the coefficient and the second element is the exponent tuple
Example:
>>> x , y, z = MOMENT_SYMBOLS
>>> set(polynomial_to_exponent_representation(1 + (42 * x**2 * y**2 * z) )) == {(42, (2, 2, 1)), (1, (0, 0, 0))}
True
"""
assert dim <= 3
x, y, z = MOMENT_SYMBOLS
polynomial = polynomial.expand()
coeff_exp_tuple_representation = []
summands = [polynomial] if polynomial.func != sp.Add else polynomial.args
for expr in summands:
if len(expr.atoms(sp.Symbol) - set(MOMENT_SYMBOLS)) > 0:
raise ValueError("Invalid moment polynomial: " + str(expr))
c, x_exp, y_exp, z_exp = sp.Wild('c'), sp.Wild('xexp'), sp.Wild('yexp'), sp.Wild('zc')
match_res = expr.match(c * x**x_exp * y**y_exp * z**z_exp)
assert match_res[x_exp].is_integer and match_res[y_exp].is_integer and match_res[z_exp].is_integer
exp_tuple = (int(match_res[x_exp]), int(match_res[y_exp]), int(match_res[z_exp]),)
if dim < 3:
for i in range(dim, 3):
assert exp_tuple[i] == 0, "Used symbols in polynomial are not representable in that dimension"
exp_tuple = exp_tuple[:dim]
coeff_exp_tuple_representation.append((match_res[c], exp_tuple))
return coeff_exp_tuple_representation
def moment_sort_key(moment):
"""Sort key function for moments to sort them by (in decreasing priority)
order, number of occuring symbols, length of string representation, string representation"""
mom_str = str(moment)
return get_order(moment), len(moment.atoms(sp.Symbol)), len(mom_str), mom_str
def sort_moments_into_groups_of_same_order(moments):
"""Returns a dictionary mapping the order (int) to a list of moments with that order."""
result = defaultdict(list)
for i, moment in enumerate(moments):
order = get_order(moment)
result[order].append(moment)
return result
# -------------------- Common Function working with exponent tuples and polynomial moments -----------------------------
def is_even(moment):
"""
A moment is considered even when under sign reversal nothing changes i.e. :math:`m(-x,-y,-z) = m(x,y,z)`
For the exponent tuple representation that means that the exponent sum is even e.g.
>>> x , y, z = MOMENT_SYMBOLS
>>> is_even(x**2 * y**2)
True
>>> is_even(x**2 * y)
False
>>> is_even((1,0,0))
False
>>> is_even(1)
True
"""
if type(moment) is tuple:
return sum(moment) % 2 == 0
else:
moment = sp.sympify(moment)
opposite = moment
for s in MOMENT_SYMBOLS:
opposite = opposite.subs(s, -s)
return sp.expand(moment - opposite) == 0
def get_moment_indices(moment_exponent_tuple):
"""Returns indices for a given exponent tuple:
Example:
>>> get_moment_indices((2,1,0))
[0, 0, 1]
>>> get_moment_indices((0,0,3))
[2, 2, 2]
"""
result = []
for i, element in enumerate(moment_exponent_tuple):
result += [i] * element
return result
def get_exponent_tuple_from_indices(moment_indices, dim):
result = [0] * dim
for i in moment_indices:
result[i] += 1
return tuple(result)
def get_order(moment):
"""Computes polynomial order of given moment.
Examples:
>>> x , y, z = MOMENT_SYMBOLS
>>> get_order(x**2 * y + x)
3
>>> get_order(z**4 * x**2)
6
>>> get_order((2,1,0))
3
"""
if isinstance(moment, tuple):
return sum(moment)
if len(moment.atoms(sp.Symbol)) == 0:
return 0
leading_coefficient = sp.polys.polytools.LM(moment)
symbols_in_leading_coefficient = leading_coefficient.atoms(sp.Symbol)
return sum([sp.degree(leading_coefficient, gen=m) for m in symbols_in_leading_coefficient])
def non_aliased_moment(moment_tuple: Sequence[int]) -> Tuple[int, ...]:
"""Takes a moment exponent tuple and returns the non-aliased version of it.
For first neighborhood stencils, all moments with exponents 3, 5, 7... are equal to exponent 1,
and exponents 4, 6, 8... are equal to exponent 2. This is because first neighborhood stencils only have values
d ∈ {+1, 0, -1}. So for example d**5 is always the same as d**3 and d, and d**6 == d**4 == d**2
Example:
>>> non_aliased_moment((5, 4, 2))
(1, 2, 2)
>>> non_aliased_moment((9, 1, 2))
(1, 1, 2)
"""
moment = list(moment_tuple)
result = []
for element in moment:
if element > 2:
result.append(2 - (element % 2))
else:
result.append(element)
return tuple(result)
def is_shear_moment(moment):
"""Shear moments in 3D are: x*y, x*z and y*z - in 2D its only x*y"""
if type(moment) is tuple:
moment = exponent_to_polynomial_representation(moment)
return moment in is_shear_moment.shear_moments
is_shear_moment.shear_moments = set([c[0] * c[1] for c in itertools.combinations(MOMENT_SYMBOLS, 2)])
@memorycache(maxsize=512)
def discrete_moment(func, moment, stencil):
r"""
Computes discrete moment of given distribution function
.. math ::
\sum_{d \in stencil} p(d) f_i
where :math:`p(d)` is the moment polynomial where :math:`x, y, z` have been replaced with the components of the
stencil direction, and :math:`f_i` is the i'th entry in the passed function sequence
Args:
func: list of distribution functions for each direction
moment: can either be a exponent tuple, or a sympy polynomial expression
stencil: sequence of directions
"""
assert len(stencil) == len(func)
res = 0
for factor, e in zip(func, stencil):
if type(moment) is tuple:
for vel, exponent in zip(e, moment):
factor *= vel ** exponent
res += factor
else:
weight = moment
for variable, e_i in zip(MOMENT_SYMBOLS, e):
weight = weight.subs(variable, e_i)
res += weight * factor
return res
def moment_matrix(moments, stencil):
"""
Returns transformation matrix to moment space
each row corresponds to a moment, each column to a direction of the stencil
The entry i,j is the i'th moment polynomial evaluated at direction j
"""
if type(moments[0]) is tuple:
def generator(row, column):
result = sp.Rational(1, 1)
for exponent, stencil_entry in zip(moments[row], stencil[column]):
result *= int(stencil_entry ** exponent)
return result
else:
def generator(row, column):
evaluated = moments[row]
for var, stencil_entry in zip(MOMENT_SYMBOLS, stencil[column]):
evaluated = evaluated.subs(var, stencil_entry)
return evaluated
return sp.Matrix(len(moments), len(stencil), generator)
def gram_schmidt(moments, stencil, weights=None):
r"""
Computes orthogonal set of moments using the method by Gram-Schmidt
Args:
moments: sequence of moments, either in tuple or polynomial form
stencil: stencil as sequence of directions
weights: optional weights, that define the scalar product which is used for normalization.
Scalar product :math:`< a,b > = \sum a_i b_i w_i` with weights :math:`w_i`.
Passing no weights sets all weights to 1.
Returns:
set of orthogonal moments in polynomial form
"""
if weights is None:
weights = sp.eye(len(stencil))
if type(weights) is list:
assert len(weights) == len(stencil)
weights = sp.diag(*weights)
if type(moments[0]) is tuple:
moments = list(exponents_to_polynomial_representations(moments))
else:
moments = list(copy(moments))
pdfs_to_moments = moment_matrix(moments, stencil).transpose()
moment_matrix_columns = [pdfs_to_moments.col(i) for i in range(pdfs_to_moments.cols)]
orthogonalized_vectors = []
for i in range(len(moment_matrix_columns)):
current_element = moment_matrix_columns[i]
for j in range(i):
prev_element = orthogonalized_vectors[j]
denominator = prev_element.dot(weights * prev_element)
if denominator == 0:
raise ValueError("Not an independent set of vectors given: "
"vector %d is dependent on previous vectors" % (i,))
overlap = current_element.dot(weights * prev_element) / denominator
current_element -= overlap * prev_element
moments[i] -= overlap * moments[j]
orthogonalized_vectors.append(current_element)
return moments
def get_default_moment_set_for_stencil(stencil):
"""
Returns a sequence of moments that are commonly used to construct a LBM equilibrium for the given stencil
"""
from lbmpy.stencils import get_stencil
from pystencils.stencils import stencils_have_same_entries
to_poly = exponents_to_polynomial_representations
if stencils_have_same_entries(stencil, get_stencil("D2Q9")):
return sorted(to_poly(moments_up_to_component_order(2, dim=2)), key=moment_sort_key)
all27_moments = moments_up_to_component_order(2, dim=3)
if stencils_have_same_entries(stencil, get_stencil("D3Q27")):
return to_poly(all27_moments)
if stencils_have_same_entries(stencil, get_stencil("D3Q19")):
non_matched_moments = [(1, 2, 2), (1, 1, 2), (2, 2, 2), (1, 1, 1)]
moments19 = set(all27_moments) - set(extend_moments_with_permutations(non_matched_moments))
return sorted(to_poly(moments19), key=moment_sort_key)
if stencils_have_same_entries(stencil, get_stencil("D3Q15")):
x, y, z = MOMENT_SYMBOLS
non_matched_moments = [(1, 2, 0), (2, 2, 0), (1, 1, 2), (1, 2, 2), (2, 2, 2)]
additional_moments = (6 * (x ** 2 * y ** 2 + x ** 2 * z ** 2 + y ** 2 * z ** 2),
3 * (x * (y ** 2 + z ** 2)),
3 * (y * (x ** 2 + z ** 2)),
3 * (z * (x ** 2 + y ** 2)),
)
to_remove = set(extend_moments_with_permutations(non_matched_moments))
return sorted(to_poly(set(all27_moments) - to_remove) + additional_moments, key=moment_sort_key)
raise NotImplementedError("No default moment system available for this stencil - define matched moments yourself")
def extract_monomials(sequence_of_polynomials, dim=3):
"""
Returns a set of exponent tuples of all monomials contained in the given set of polynomials
:param sequence_of_polynomials: sequence of polynomials in the MOMENT_SYMBOLS
:param dim: length of returned exponent tuples
>>> x, y, z = MOMENT_SYMBOLS
>>> extract_monomials([x**2 + y**2 + y, y + y**2])
{(0, 2, 0), (0, 1, 0), (2, 0, 0)}
>>> extract_monomials([x**2 + y**2 + y, y + y**2], dim=2)
{(0, 1), (2, 0), (0, 2)}
"""
monomials = set()
for polynomial in sequence_of_polynomials:
for factor, exponent_tuple in polynomial_to_exponent_representation(polynomial):
monomials.add(exponent_tuple[:dim])
return monomials
def monomial_to_polynomial_transformation_matrix(monomials, polynomials):
"""
Returns a transformation matrix from a monomial to a polynomial representation
:param monomials: sequence of exponent tuples
:param polynomials: sequence of polynomials in the MOMENT_SYMBOLS
>>> x, y, z = MOMENT_SYMBOLS
>>> polys = [7 * x**2 + 3 * x + 2 * y **2, \
9 * x**2 - 5 * x]
>>> mons = list(extract_monomials(polys, dim=2))
>>> monomial_to_polynomial_transformation_matrix(mons, polys)
Matrix([
[7, 3, 2],
[9, -5, 0]])
"""
dim = len(monomials[0])
result = sp.zeros(len(polynomials), len(monomials))
for polynomial_idx, polynomial in enumerate(polynomials):
for factor, exponent_tuple in polynomial_to_exponent_representation(polynomial):
exponent_tuple = exponent_tuple[:dim]
result[polynomial_idx, monomials.index(exponent_tuple)] = factor
return result
# ---------------------------------- Visualization ---------------------------------------------------------------------
def moment_equality_table(stencil, discrete_equilibrium=None, continuous_equilibrium=None,
max_order=4, truncate_order=3):
"""
Creates a table showing which moments of a discrete stencil/equilibrium coincide with the
corresponding continuous moments
Args:
stencil: list of stencil velocities
discrete_equilibrium: list of sympy expr to compute discrete equilibrium for each direction, if left
to default the standard discrete Maxwellian equilibrium is used
continuous_equilibrium: continuous equilibrium, if left to default, the continuous Maxwellian is used
max_order: compare moments up to this order (number of rows in table)
truncate_order: moments are considered equal if they match up to this order
Returns:
Object to display in an Jupyter notebook
"""
import ipy_table
from lbmpy.continuous_distribution_measures import continuous_moment
dim = len(stencil[0])
u = sp.symbols("u_:{dim}".format(dim=dim))
if discrete_equilibrium is None:
from lbmpy.maxwellian_equilibrium import discrete_maxwellian_equilibrium
discrete_equilibrium = discrete_maxwellian_equilibrium(stencil, c_s_sq=sp.Rational(1, 3), compressible=True,
u=u, order=truncate_order)
if continuous_equilibrium is None:
from lbmpy.maxwellian_equilibrium import continuous_maxwellian_equilibrium
continuous_equilibrium = continuous_maxwellian_equilibrium(dim=dim, u=u, c_s_sq=sp.Rational(1, 3))
table = []
matched_moments = 0
non_matched_moments = 0
moments_list = [list(moments_of_order(o, dim, include_permutations=False)) for o in range(max_order + 1)]
colors = dict()
nr_of_columns = max([len(v) for v in moments_list]) + 1
header_row = [' '] * nr_of_columns
header_row[0] = 'order'
table.append(header_row)
for order, moments in enumerate(moments_list):
row = [' '] * nr_of_columns
row[0] = '%d' % (order,)
for moment, col_idx in zip(moments, range(1, len(row))):
multiplicity = moment_multiplicity(moment)
dm = discrete_moment(discrete_equilibrium, moment, stencil)
cm = continuous_moment(continuous_equilibrium, moment, symbols=sp.symbols("v_0 v_1 v_2")[:dim])
difference = sp.simplify(dm - cm)
if truncate_order:
difference = sp.simplify(remove_higher_order_terms(difference, symbols=u, order=truncate_order))
if difference != 0:
colors[(order + 1, col_idx)] = 'Orange'
non_matched_moments += multiplicity
else:
colors[(order + 1, col_idx)] = 'lightGreen'
matched_moments += multiplicity
row[col_idx] = '%s x %d' % (moment, moment_multiplicity(moment))
table.append(row)
table_display = ipy_table.make_table(table)
ipy_table.set_row_style(0, color='#ddd')
for cell_idx, color in colors.items():
ipy_table.set_cell_style(cell_idx[0], cell_idx[1], color=color)
print("Matched moments %d - non matched moments %d - total %d" %
(matched_moments, non_matched_moments, matched_moments + non_matched_moments))
return table_display
def moment_equality_table_by_stencil(name_to_stencil_dict, moments, truncate_order=3):
"""
Creates a table for display in IPython notebooks that shows which moments agree between continuous and
discrete equilibrium, group by stencils
Args:
name_to_stencil_dict: dict from stencil name to stencil
moments: sequence of moments to compare - assumes that permutations have similar properties
so just one representative is shown labeled with its multiplicity
truncate_order: compare up to this order
"""
import ipy_table
from lbmpy.maxwellian_equilibrium import discrete_maxwellian_equilibrium
from lbmpy.maxwellian_equilibrium import continuous_maxwellian_equilibrium
from lbmpy.continuous_distribution_measures import continuous_moment
stencil_names = []
stencils = []
for key, value in name_to_stencil_dict.items():
stencil_names.append(key)
stencils.append(value)
moments = list(pick_representative_moments(moments))
colors = {}
for stencil_idx, stencil in enumerate(stencils):
dim = len(stencil[0])
u = sp.symbols("u_:{dim}".format(dim=dim))
discrete_equilibrium = discrete_maxwellian_equilibrium(stencil, c_s_sq=sp.Rational(1, 3), compressible=True,
u=u, order=truncate_order)
continuous_equilibrium = continuous_maxwellian_equilibrium(dim=dim, u=u, c_s_sq=sp.Rational(1, 3))
for moment_idx, moment in enumerate(moments):
moment = moment[:dim]
dm = discrete_moment(discrete_equilibrium, moment, stencil)
cm = continuous_moment(continuous_equilibrium, moment, symbols=sp.symbols("v_0 v_1 v_2")[:dim])
difference = sp.simplify(dm - cm)
if truncate_order:
difference = sp.simplify(remove_higher_order_terms(difference, symbols=u, order=truncate_order))
colors[(moment_idx + 1, stencil_idx + 2)] = 'Orange' if difference != 0 else 'lightGreen'
table = []
header_row = [' ', '#'] + stencil_names
table.append(header_row)
for moment in moments:
row = [str(moment), str(moment_multiplicity(moment))] + [' '] * len(stencils)
table.append(row)
table_display = ipy_table.make_table(table)
ipy_table.set_row_style(0, color='#ddd')
for cell_idx, color in colors.items():
ipy_table.set_cell_style(cell_idx[0], cell_idx[1], color=color)
return table_display
# --------------------------------------- Internal Functions -----------------------------------------------------------
def __unique(seq: Sequence[T]) -> List[T]:
"""Removes duplicates from a sequence in an order preserving way.
Example:
>>> __unique((1, 2, 3, 1, 4, 6, 3))
[1, 2, 3, 4, 6]
"""
seen = {}
result = []
for item in seq:
if item in seen:
continue
seen[item] = 1
result.append(item)
return result
def __unique_permutations(elements: Sequence[T]) -> Iterable[T]:
"""Generates all unique permutations of the passed sequence.
Example:
>>> list(__unique_permutations([1, 1, 2]))
[(1, 1, 2), (1, 2, 1), (2, 1, 1)]
"""
if len(elements) == 1:
yield (elements[0],)
else:
unique_elements = set(elements)
for first_element in unique_elements:
remaining_elements = list(elements)
remaining_elements.remove(first_element)
for sub_permutation in __unique_permutations(remaining_elements):
yield (first_element,) + sub_permutation
def __fixed_sum_tuples(tuple_length: int, tuple_sum: int,
allowed_values: Optional[Sequence[int]] = None,
ordered: bool = False) -> Iterable[Tuple[int, ...]]:
"""Generates all possible tuples of positive integers with a fixed sum of all entries.
Args:
tuple_length: length of the returned tuples
tuple_sum: summing over the entries of a yielded tuple should give this number
allowed_values: optional sequence of positive integers that are considered as tuple entries
zero has to be in the set of allowed values
if None, all possible positive integers are allowed
ordered: if True, only tuples are returned where the entries are descending, i.e. where the entries are ordered
Examples:
Generate all 2-tuples where the sum of entries is 3
>>> list(__fixed_sum_tuples(tuple_length=2, tuple_sum=3))
[(0, 3), (1, 2), (2, 1), (3, 0)]
Same with ordered tuples only
>>> list(__fixed_sum_tuples(tuple_length=2, tuple_sum=3, ordered=True))
[(2, 1), (3, 0)]
Restricting the allowed values, note that zero has to be in the allowed values!
>>> list(__fixed_sum_tuples(tuple_length=3, tuple_sum=4, allowed_values=(0, 1, 3)))
[(0, 1, 3), (0, 3, 1), (1, 0, 3), (1, 3, 0), (3, 0, 1), (3, 1, 0)]
"""
if not allowed_values:
allowed_values = set(range(0, tuple_sum + 1))
assert 0 in allowed_values and all(i >= 0 for i in allowed_values)
def recursive_helper(current_list, position, total_sum):
new_position = position + 1
if new_position < len(current_list):
for i in allowed_values:
current_list[position] = i
new_sum = total_sum - i
if new_sum < 0:
continue
for item in recursive_helper(current_list, new_position, new_sum):
yield item
else:
if total_sum in allowed_values:
current_list[-1] = total_sum
if not ordered:
yield tuple(current_list)
if ordered and current_list == sorted(current_list, reverse=True):
yield tuple(current_list)
return recursive_helper([0] * tuple_length, 0, tuple_sum)