From 66023ebfc82a87dde05181e4961bcb40289bbc47 Mon Sep 17 00:00:00 2001 From: Martin Bauer <martin.bauer@fau.de> Date: Fri, 21 Sep 2018 13:45:49 +0200 Subject: [PATCH] Phasefield & FD derivation - Added test for linear equation system and moved it to pystencils.utils --- fd/__init__.py | 12 ++--- utils.py | 116 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 6 deletions(-) diff --git a/fd/__init__.py b/fd/__init__.py index 39f7094d4..72b87094e 100644 --- a/fd/__init__.py +++ b/fd/__init__.py @@ -1,11 +1,11 @@ from .derivative import Diff, DiffOperator, \ - diff_terms, collect_diffs, create_nested_diff, replace_diff, zero_diffs, evaluate_diffs, normalize_diff_order, \ + diff_terms, collect_diffs, replace_diff, zero_diffs, evaluate_diffs, normalize_diff_order, \ expand_diff_full, expand_diff_linear, expand_diff_products, combine_diff_products, \ - functional_derivative + functional_derivative, diff from .finitedifferences import advection, diffusion, transient, Discretization2ndOrder +from .spatial import discretize_spatial - -__all__ = ['Diff', 'DiffOperator', 'diff_terms', 'collect_diffs', 'create_nested_diff', 'replace_diff', 'zero_diffs', - 'evaluate_diffs', 'normalize_diff_order', 'expand_diff_full', 'expand_diff_linear', +__all__ = ['Diff', 'diff', 'DiffOperator', 'diff_terms', 'collect_diffs', 'replace_diff', + 'zero_diffs', 'evaluate_diffs', 'normalize_diff_order', 'expand_diff_full', 'expand_diff_linear', 'expand_diff_products', 'combine_diff_products', 'functional_derivative', - 'advection', 'diffusion', 'transient', 'Discretization2ndOrder'] + 'advection', 'diffusion', 'transient', 'Discretization2ndOrder', 'discretize_spatial'] diff --git a/utils.py b/utils.py index da3fec7d7..a00b02185 100644 --- a/utils.py +++ b/utils.py @@ -4,6 +4,8 @@ from contextlib import contextmanager from typing import Mapping from collections import Counter +import sympy as sp + class DotDict(dict): """Normal dict with additional dot access for all keys""" @@ -83,3 +85,117 @@ def fully_contains(l1, l2): if l2_counter[element] < count: return False return True + + +class LinearEquationSystem: + """Symbolic linear system of equations - consisting of matrix and right hand side. + + Equations can be added incrementally. System is held in reduced row echelon form to quickly determine if + system has a single, multiple, or no solution. + + Example: + >>> x, y= sp.symbols("x, y") + >>> les = LinearEquationSystem([x, y]) + >>> les.add_equation(x - y - 3) + >>> les.solution_structure() + 'multiple' + >>> les.add_equation(x + y - 4) + >>> les.solution_structure() + 'single' + >>> les.solution() + {x: 7/2, y: 1/2} + + """ + def __init__(self, unknowns): + size = len(unknowns) + self._matrix = sp.zeros(size, size + 1) + self.unknowns = unknowns + self.next_zero_row = 0 + + def copy(self): + """Returns a copy of the equation system.""" + new = LinearEquationSystem(self.unknowns) + new._matrix = self._matrix.copy() + new.next_zero_row = self.next_zero_row + return new + + def add_equation(self, linear_equation): + """Add a linear equation as sympy expression. Implicit "-0" is assumed. Equation has to be linear and contain + only unknowns passed to the constructor otherwise a ValueError is raised. """ + self._resize_if_necessary() + linear_equation = linear_equation.expand() + zero_row_idx = self.next_zero_row + self.next_zero_row += 1 + + control = 0 + for i, unknown in enumerate(self.unknowns): + self._matrix[zero_row_idx, i] = linear_equation.coeff(unknown) + control += unknown * self._matrix[zero_row_idx, i] + rest = linear_equation - control + if rest.atoms(sp.Symbol): + raise ValueError("Not a linear equation in the unknowns") + self._matrix[zero_row_idx, -1] = -rest + + def add_equations(self, linear_equations): + """Add a sequence of equations. For details see `add_equation`. """ + self._resize_if_necessary(len(linear_equations)) + for eq in linear_equations: + self.add_equation(eq) + + def set_unknown_zero(self, unknown_idx): + """Sets an unknown to zero - pass the index not the variable itself!""" + assert unknown_idx < len(self.unknowns) + self._resize_if_necessary() + self._matrix[self.next_zero_row, unknown_idx] = 1 + self.next_zero_row += 1 + + def reduce(self): + """Brings the system in reduced row echelon form.""" + self._matrix = self._matrix.rref()[0] + self._update_next_zero_row() + + @property + def matrix(self): + """Return a matrix that represents the equation system. + Has one column more than unknowns for the affine part.""" + return self._matrix + + def solution_structure(self): + """Returns either 'multiple', 'none' or 'single' to indicate how many solutions the system has.""" + self.reduce() + non_zero_rows = self.next_zero_row + num_unknowns = len(self.unknowns) + if non_zero_rows == 0: + return 'multiple' + + *row_begin, left, right = self._matrix.row(non_zero_rows - 1) + if non_zero_rows > num_unknowns: + return 'none' + elif non_zero_rows == num_unknowns: + if left == 0 and right != 0: + return 'none' + else: + return 'single' + elif non_zero_rows < num_unknowns: + if right != 0 and left == 0 and all(e == 0 for e in row_begin): + return 'none' + else: + return 'multiple' + + def solution(self): + """Solves the system if it has a single solution. Returns a dictionary mapping symbol to solution value.""" + return sp.solve_linear_system(self._matrix, *self.unknowns) + + def _resize_if_necessary(self, new_rows=1): + if self.next_zero_row + new_rows > self._matrix.shape[0]: + self._matrix = self._matrix.row_insert(self._matrix.shape[0] + 1, + sp.zeros(new_rows, self._matrix.shape[1])) + + def _update_next_zero_row(self): + result = self._matrix.shape[0] + while result >= 0: + row_to_check = result - 1 + if any(e != 0 for e in self._matrix.row(row_to_check)): + break + result -= 1 + self.next_zero_row = result \ No newline at end of file -- GitLab