From 6448afefda47b1f986b62aab16a247d131dd7e99 Mon Sep 17 00:00:00 2001 From: David Ham Date: Thu, 19 Jun 2014 11:55:33 +0100 Subject: [PATCH 001/749] remaining renaming requirements --- finat/__init__.py | 1 + finat/element/__init__.py | 4 + finat/element/derivatives.py | 14 +++ finat/element/finiteelementbase.py | 158 +++++++++++++++++++++++++++++ finat/element/indices.py | 64 ++++++++++++ finat/element/lagrange.py | 153 ++++++++++++++++++++++++++++ finat/element/utils.py | 90 ++++++++++++++++ 7 files changed, 484 insertions(+) create mode 100644 finat/__init__.py create mode 100644 finat/element/__init__.py create mode 100644 finat/element/derivatives.py create mode 100644 finat/element/finiteelementbase.py create mode 100644 finat/element/indices.py create mode 100644 finat/element/lagrange.py create mode 100644 finat/element/utils.py diff --git a/finat/__init__.py b/finat/__init__.py new file mode 100644 index 000000000..95f6f869a --- /dev/null +++ b/finat/__init__.py @@ -0,0 +1 @@ +import element diff --git a/finat/element/__init__.py b/finat/element/__init__.py new file mode 100644 index 000000000..311300c61 --- /dev/null +++ b/finat/element/__init__.py @@ -0,0 +1,4 @@ + +from lagrange import Lagrange +from finiteelementbase import PointSet +from utils import KernelData diff --git a/finat/element/derivatives.py b/finat/element/derivatives.py new file mode 100644 index 000000000..14c8e700b --- /dev/null +++ b/finat/element/derivatives.py @@ -0,0 +1,14 @@ + +class Derivative(object): + "Abstract symbolic object for a derivative." + + def __init__(self, name, doc): + self.__doc__ = doc + self.name = name + + def __str__(self): + return self.name + +grad = Derivative("grad", "Symbol for the gradient operation") +div = Derivative("div", "Symbol for the divergence operation") +curl = Derivative("curl", "Symbol for the curl operation") diff --git a/finat/element/finiteelementbase.py b/finat/element/finiteelementbase.py new file mode 100644 index 000000000..593417b7f --- /dev/null +++ b/finat/element/finiteelementbase.py @@ -0,0 +1,158 @@ +import numpy + + +class UndefinedError(Exception): + def __init__(self, *args, **kwargs): + Exception.__init__(self, *args, **kwargs) + + +class PointSetBase(object): + """A way of specifying a known set of points, perhaps with some + (tensor) structure.""" + + def __init__(self): + pass + + @property + def points(self): + """Return a flattened numpy array of points. + + The array has shape (num_points, topological_dim). + """ + + raise NotImplementedError + + +class PointSet(PointSetBase): + """A basic pointset with no internal structure.""" + + def __init__(self, points): + self._points = numpy.array(points) + + @property + def points(self): + """Return a flattened numpy array of points. + + The array has shape (num_points, topological_dim). + """ + + return self._points + + +class Recipe(object): + """AST snippets and data corresponding to some form of finite element evaluation.""" + def __init__(self, indices, instructions, depends): + self._indices = indices + self._instructions = instructions + self._depends = depends + + @property + def indices(self): + '''The free indices in this :class:`Recipe`.''' + + return self._indices + + @property + def instructions(self): + '''The actual instructions making up this :class:`Recipe`.''' + + return self._instructions + + @property + def depends(self): + '''The input fields of this :class:`Recipe`.''' + + return self._depends + + +class FiniteElementBase(object): + + def __init__(self): + + self._id = FiniteElementBase._count + FiniteElementBase._count += 1 + + _count = 0 + + @property + def cell(self): + '''Return the reference cell on which we are defined. + ''' + + return self._cell + + @property + def degree(self): + '''Return the degree of the embedding polynomial space. + + In the tensor case this is a tuple. + ''' + + return self._degree + + @property + def entity_dofs(self): + '''Return the map of topological entities to degrees of + freedom for the finite element. + + Note that entity numbering needs to take into account the tensor case. + ''' + + raise NotImplementedError + + @property + def entity_closure_dofs(self): + '''Return the map of topological entities to degrees of + freedom on the closure of those entities for the finite element.''' + + raise NotImplementedError + + @property + def facet_support_dofs(self): + '''Return the map of facet id to the degrees of freedom for which the + corresponding basis functions take non-zero values.''' + + raise NotImplementedError + + def field_evaluation(self, points, static_data, derivative=None): + '''Return code for evaluating a known field at known points on the + reference element. + + Derivative is an atomic derivative operation. + + Points is some description of the points to evaluate at + + ''' + + raise NotImplementedError + + def basis_evaluation(self, points, static_data, derivative=None): + '''Return code for evaluating a known field at known points on the + reference element. + + Points is some description of the points to evaluate at + + ''' + + raise NotImplementedError + + def pullback(self, derivative): + '''Return symbolic information about how this element pulls back + under this derivative.''' + + raise NotImplementedError + + def moment_evaluation(self, value, weights, points, static_data, derivative=None): + '''Return code for evaluating value * v * dx where + v is a test function. + ''' + + raise NotImplementedError + + def dual_evaluation(self, static_data): + '''Return code for evaluating an expression at the dual set. + + Note: what does the expression need to look like? + ''' + + raise NotImplementedError diff --git a/finat/element/indices.py b/finat/element/indices.py new file mode 100644 index 000000000..9ebc7d097 --- /dev/null +++ b/finat/element/indices.py @@ -0,0 +1,64 @@ +import pymbolic.primitives as p + + +class IndexBase(p.Variable): + '''Base class for symbolic index objects.''' + def __init__(self, extent, name): + super(IndexBase, self).__init__(name) + if isinstance(extent, slice): + self._extent = extent + elif isinstance(extent, int): + self._extent = slice(extent) + else: + raise TypeError("Extent must be a slice or an int") + + @property + def extent(self): + '''A slice indicating the values this index can take.''' + return self._extent + + @property + def _str_extent(self): + + return "%s=(%s:%s:%s)" % (str(self), + self._extent.start or 0, + self._extent.stop, + self._extent.step or 1) + + +class PointIndex(IndexBase): + '''An index running over a set of points, for example quadrature points.''' + def __init__(self, extent): + + name = 'q_' + str(PointIndex._count) + PointIndex._count += 1 + + super(PointIndex, self).__init__(extent, name) + + _count = 0 + + +class BasisFunctionIndex(IndexBase): + '''An index over a local set of basis functions. + E.g. test functions on an element.''' + def __init__(self, extent): + + name = 'i_' + str(BasisFunctionIndex._count) + BasisFunctionIndex._count += 1 + + super(BasisFunctionIndex, self).__init__(extent, name) + + _count = 0 + + +class DimensionIndex(IndexBase): + '''An index over data dimension. For example over topological, + geometric or vector components.''' + def __init__(self, extent): + + name = 'alpha_' + str(DimensionIndex._count) + DimensionIndex._count += 1 + + super(DimensionIndex, self).__init__(extent, name) + + _count = 0 diff --git a/finat/element/lagrange.py b/finat/element/lagrange.py new file mode 100644 index 000000000..13ba84118 --- /dev/null +++ b/finat/element/lagrange.py @@ -0,0 +1,153 @@ +import pymbolic.primitives as p +from finiteelementbase import FiniteElementBase, Recipe +from utils import doc_inherit, IndexSum +import FIAT +import indices +from derivatives import div, grad, curl +import numpy as np + + +class Lagrange(FiniteElementBase): + def __init__(self, cell, degree): + super(Lagrange, self).__init__() + + self._cell = cell + self._degree = degree + + self._fiat_element = FIAT.Lagrange(cell, degree) + + @property + def entity_dofs(self): + '''Return the map of topological entities to degrees of + freedom for the finite element. + + Note that entity numbering needs to take into account the tensor case. + ''' + + return self._fiat_element.entity_dofs() + + @property + def entity_closure_dofs(self): + '''Return the map of topological entities to degrees of + freedom on the closure of those entities for the finite element.''' + + return self._fiat_element.entity_dofs() + + @property + def facet_support_dofs(self): + '''Return the map of facet id to the degrees of freedom for which the + corresponding basis functions take non-zero values.''' + + return self._fiat_element.entity_support_dofs() + + def _tabulate(self, points, derivative): + + if derivative is None: + return self._fiat_element.tabulate(0, points.points)[ + tuple([0]*points.points.shape[1])] + elif derivative is grad: + tab = fiat_element.tabulate(1, points.points) + + indices = np.eye(points.points.shape[1], dtype=int) + + return np.array([tab[tuple(i)] for i in indices]) + + else: + raise ValueError( + "Lagrange elements do not have a %s operation") % derivative + + def _tabulation_variable(self, points, kernel_data, derivative): + # Produce the variable for the tabulation of the basis + # functions or their derivative. Also return the relevant indices. + + # updates the requisite static data, which in this case + # is just the matrix. + static_key = (id(self), id(points), id(derivative)) + + static_data = kernel_data.static + fiat_element = self._fiat_element + + if static_key in static_data: + phi = static_data[static_key][0] + else: + phi = p.Variable('phi_e' if derivative is None else "dphi_e" + + str(self._id)) + data = self._tabulate(points, derivative) + static_data[static_key] = (phi, lambda: data) + + i = indices.BasisFunctionIndex(fiat_element.space_dimension()) + q = indices.PointIndex(points.points.shape[0]) + + ind = [i, q] + + if derivative is grad: + alpha = indices.DimensionIndex(points.points.shape[1]) + ind = [alpha] + ind + + return phi, ind + + def _weights_variable(self, weights, kernel_data): + # Produce a variable for the quadrature weights. + static_key = (id(weights), ) + + static_data = kernel_data.static + + if static_key in static_data: + w = static_data[static_key][0] + else: + w = p.Variable('w') + data = weights.points + static_data[static_key] = (w, lambda: data) + + return w + + @doc_inherit + def basis_evaluation(self, points, kernel_data, derivative=None): + + phi, ind = self._tabulation_variable(points, kernel_data, derivative) + + instructions = [phi[ind]] + + depends = [] + + return Recipe(ind, instructions, depends) + + @doc_inherit + def field_evaluation(self, field_var, points, + kernel_data, derivative=None): + + phi, ind = self._tabulation_variable(points, kernel_data, derivative) + + if derivative is None: + free_ind = [ind[-1]] + else: + free_ind = [ind[0], ind[-1]] + + i = ind[-2] + + instructions = [IndexSum([i], field_var[i] * phi[ind])] + + depends = [field_var] + + return Recipe(free_ind, instructions, depends) + + @doc_inherit + def moment_evaluation(self, value, weights, points, + kernel_data, derivative=None): + + phi, ind = self._tabulation_variable(points, kernel_data, derivative) + w = self._weights_variable(weights, kernel_data) + + q = ind[-1] + if derivative is None: + sum_ind = [q] + else: + sum_ind = [ind[0], q] + + i = ind[-2] + + instructions = [IndexSum(sum_ind, value[sum_ind] * w[q] * phi[ind])] + + depends = [value] + + return Recipe([i], instructions, depends) diff --git a/finat/element/utils.py b/finat/element/utils.py new file mode 100644 index 000000000..4c6593320 --- /dev/null +++ b/finat/element/utils.py @@ -0,0 +1,90 @@ +""" +from http://stackoverflow.com/questions/2025562/inherit-docstrings-in-python-class-inheritance + +doc_inherit decorator + +Usage: + +class Foo(object): + def foo(self): + "Frobber" + pass + +class Bar(Foo): + @doc_inherit + def foo(self): + pass + +Now, Bar.foo.__doc__ == Bar().foo.__doc__ == Foo.foo.__doc__ == "Frobber" +""" +import pymbolic.primitives as p +from functools import wraps + + +class DocInherit(object): + """ + Docstring inheriting method descriptor + + The class itself is also used as a decorator + """ + + def __init__(self, mthd): + self.mthd = mthd + self.name = mthd.__name__ + + def __get__(self, obj, cls): + if obj: + return self.get_with_inst(obj, cls) + else: + return self.get_no_inst(cls) + + def get_with_inst(self, obj, cls): + + overridden = getattr(super(cls, obj), self.name, None) + + @wraps(self.mthd, assigned=('__name__', '__module__')) + def f(*args, **kwargs): + return self.mthd(obj, *args, **kwargs) + + return self.use_parent_doc(f, overridden) + + def get_no_inst(self, cls): + + for parent in cls.__mro__[1:]: + overridden = getattr(parent, self.name, None) + if overridden: + break + + @wraps(self.mthd, assigned=('__name__', '__module__')) + def f(*args, **kwargs): + return self.mthd(*args, **kwargs) + + return self.use_parent_doc(f, overridden) + + def use_parent_doc(self, func, source): + if source is None: + raise NameError("Can't find '%s' in parents" % self.name) + func.__doc__ = source.__doc__ + return func + +doc_inherit = DocInherit + + +class KernelData(object): + def __init__(self): + + self.static = {} + self.params = {} + + +class IndexSum(p._MultiChildExpression): + def __init__(self, indices, body): + + self.children = (indices, body) + + def __getinitargs__(self): + return self.children + + def __str__(self): + return "IndexSum(%s, %s)" % (str([x._str_extent for x in self.children[0]]), + self.children[1]) From 70c3585da820ed1ff206d954fdd91320cccd2681 Mon Sep 17 00:00:00 2001 From: David Ham Date: Thu, 19 Jun 2014 13:07:09 +0100 Subject: [PATCH 002/749] Logo based on Rob's Texan pun --- docs/images/logo.png | Bin 0 -> 61843 bytes docs/images/logo.svg | 194 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 docs/images/logo.png create mode 100644 docs/images/logo.svg diff --git a/docs/images/logo.png b/docs/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0eab2ed5c452647077e7ae4a9101cca75150f6fc GIT binary patch literal 61843 zcmXtg2Rzl^|NphJiL9*3CuQ%-OhoCDm3wjRajk1*Wm6IvCAl)Lk+LqA8!p)^WD~BL zk#Vy{+|2%`@9**Vc(~@{;eF0|jpyt6d>$_i_3tw?@G?Lk5atIuTE-9vg#>t)q&o+` zQ*p&=2z;RS)qG$=2i`*I9Am-P^xit=z7U9j=f8IfS@yj+@Xd>U+Gc)_yqx@=*!dtK zPo6xHa`i;{I@oz5rM!HcGq+TEA&|?E2U;2?ftZcP>DQJI0=CFFPPUNkzMfL?c>aoR zr{b9J^4~{!c&eyfK3IL6URHVbj^X@~D1G3%_ z%^KGqMIm2Y^&d6B#&1;0`PvAzv8uow9pUgd8>_=EUm7D58#vdk)x+nw#oU1FG%p-!*>U3H{NC;wP zJ$g7f7=F*^Z=w3B=L*f48EH98~{{xqbG>K{fby?AB~!eAnws7a+fb)u!g= z3Nvqc{g|JRtdRH*^0gvC-@w4t*_pPmu&}DJF>%;M%fi9}5(z#+@U^u;3#8!C(1ZnV z9`)e@*U;d40%6&vDfH#$riaZqA>v@;Vn9GZsClX&q+q1DIqW!~;nM9g+Q<;{)}ev= zDFMRg&&CWQjvhfibHJlo=lP2&BtCG! zJ9)k#-a_0Ic!yV3u5~WTdbqpWstR6!h~(3~8?}|$xqkgRBbhepDR)vnq-1@H@iQbB zi}js9+Nd1dnGM-Kq-7PHk7B*O1Hm^pZ$IsQ&)MW5P$gB&VrCV#PGB$tf74s>bLKix#EgN7zXw*l@jqa=Bba3 zIIym~W9$1SBMMMK6_Q^z30hWH2D2I$3)7#~Kg*JR5uaxWgB4@3CP%;G^5#K2eHe}n zX%ysh5b2fc*~3&3N7wg%{#xu}t7~8IURXc37fkNxMjWdv(sbJ zZfSf|p;iCC!qEH%kzcko*KCjzaTermYF7PpH|BPhzS5vCh$8U7d=84ea%JnvFBXe; z&Efh86mttC7EfuLAbU?VA8kCPK){vvG##zu8EhWePNewD4PBr20IBM1w6@_JnXyof?lngSv<=zDIyiufz!@#&HOoB}fz+vcZCGlbQL z+%78p>^3p>e=9dGH#Ys&A*dPPf_v=keW#Nr86SKRjzAsEisy6C%82H3fM|n!8UJi0 zXhK8;47?#`WEL!_4n6=O1zn)15|0wC3O!~KfLv->vaGlKnPQw8%g<(xl61x42&r*- zk-jW_4UoGK&nHjb9gxXEiu}sC9syQoO^NsZ`wPnHo$76g2!pro_`TSl5_YC@K9VIR zqYQDv!MjWJY9R*+EG#Vc5g_)Kk!EB50u-EW0m4KBsOImahZn2h(j$njZ=P!G%m$)`PoR{|zxRFu>yB;V~=* z3LLshgj#2n+E_VKcm%=sv|sFwXuh1&EAGh9i?-&e7oVCkNFAC%MZYT>=ck#2UysyH znRmbGu*y9>Ie8_YLqI^FBOs=+QSBp+rFX8rOsZ^vJz$@&MA+YdfB|}}nTk}+(yw*p z{p7QOiN}9Kh*t%ncV|&gqb;>Q#_A}MQi#e<2#U*&IHcR@)gdQ~JI5?)@*xmG+`7+f z$U9^@Bcx(BJoD(3zg|BGA%T1?`T+@-MJMtx;)noUrdy=EI8d>9tlqDdh_ zTrcX_<);|CqMk-Vw=Nh_zw^~+W4pjcm1MzSK7{RvOtYfLL%xRf_7Ow8vV2q1`L-Jg z#G3<3B`I#D1i0v!BFTM3fmkg!=PlGpVLA5{}8WM8n;vCGuh6Bg@9%vAOyu+qeF_ zJ*xSKYK{nQ=2rOxO)HJk`9M`rA}0`hz9m?!S=}U?gl&dh^ob5F5Z8as*SEc*rsU7{pcU0nDq=!XfOrfBc ztW-+uz3qNpQ&SV>Qj%NXdV;g48B$=*C}^EK>7P3fNx1i4Ze`RvICtHXeH{AvrmOtY z_xdNl&W^Y5K09b{Z&&o!5Y_JFVUeVexDxWY+{7$d*RG=v(YABGA@h7*ug5*Dbn_ZHEX~RY+Xu z70bC!%hx0Kz}~|{^59wikX+{w*oCBOMTo~%4Yu+9wCMx@DrMoD$?Bz5XpIc5xro1E zU**m!>N4~ThZ$Zt8H{a{)6;837*f5hsu$=)sD&uSgyL`vS{&1y>@=J(Sg>fm(9lv? zY#qBdo6!oUw}DJ4eqp1qn&?JeaktroVwTrB8SxOJPAyTwYE36wuDgG&IA&gH*j=rkOSIWt$TTHkx z6^|85Eq}-X|JmQx+N!}c_ivSumMB`*+2G5K*t(&{5-*e902!F6>%`eY%%y)InT3_L zczj*5NPP0T2*4K!S-$SLhsR|f!>(l2_zpOmuapw{`g%gP*XmYO2eXJ7Jr#})2^Um8 zfBJMKgtwdL?s%D1{9Wq$V1Ivxl9F3k7+;pDzjM4mitXXrSjoiXWNSqo77N|YPJ@wx z1UGYKudSH&mA=$bA$`+(@a&hKsPOB`T&$L)wl><2a9^9o_KN#;p$oI74O&*+ZjM;| z)%N5wX`vr=Wwm9ff{)Y)0R4Z3{Q1$r_^*Y8{T{ymE99N$$g0~XWn2IKPJg2bsQ?+~ zX|L~f(L)ZLlEa|2c;_6S-Y%AT{N9y!H4Dn8LBoWQyE=I{4I;eqa8-CnvtC{1cItSF zG09f=rpz7P5B^tpBU_9wyg7F{AnENiPaU_mhPKg=4}ic^Gc#IKZwWOuF*YYZxPKK@ zMm)W=ubJ8X}@CC7<}VqTvOmLAw;Vm+WltcC)pPt$#LlyAr>fyYbkz& zuA!{hR67FxSqxF=UM?mJy?wfG6R}?i2_enD{CeRHzbXYuwISxoKnO|z6IDZe@7mzS zyS_g4-W5}hTJQDQC>;4uJxlPgg8z7O<4eR&>yxwczdM)Cj{h>hFR{+Bcdks=GG$zg zOLtY^?TC`~oca8;LV}{|F7xodofQJ)Z19I&ah5bQq)pp|1CPVcpFd;c@^Zl#X79wy z5@P~`F->Ws1GkZv-b2>;ORxUc$Jqr_4E0H1b;9=TwR)ey&zES*6|_sf%VaD#2r@O84>bQ8aBOE!&-? z%OM{v_F-+cvMCu+&|-F|=wDLA87V?TH?wqkU4yc?LIRm520)2M!7IuZ0&0Bc=kR|n zI50}-!y5CHqsPX?QRq78=KQYZZdGNNk~@ImVo8p|iBUo?FT#B~m_29jSjaNk}#dWMHicXS2JgCCzIt{mc zX4h*2FOjjS`aJMZD}kge|Cy%7Xp5S2pMA9LWG*k&n%#LpWPk>V+@R+&G=VWYy)b6z zOHeTj!fzF~Nz%Z}g4jywm4+dn2unkT2K137cu7h4GZR+Akj#?k8d^%o&9=QzPIKiU}HLqYTo4L!J$#`?OyJly|C;3?YC7=`jkP0=jz zu&_N3&VXu9u?VY78JDgx`r?3(kIx1`kZs1vwg0eUm>#O20Uj!&^b$8;-@O@M04&)A z)q+P`H@4DCw zTPek^?uhAz5U}po@NzF~6A%ziw$_mOv@zbk{ck2!nTFJoIQp9P&;8S9CQmEmDJ z*#2HmK2Ko+6)ie!dj+6iK$c69#GxvdKOG^_;8og0bM>HY54)}W%7d+DMy zr0(u`2i$Cr=XTzieQF*#_Pod{Yh4_L1neTD`4T;6sYgkF%WrT?5U+s=MoMeTvJ>h3r1G+ z{~j!~$CMv_4-fgrCzM;s9lO=mht-<@&KuLn3pj&ilQ<4$B$rHG-_^UlK0)Ba%-no+ z%5$ei)&K6f)X>1EvAO-mle*}PX5i3MrU*AQ6 zH>)y=y?WfgA3*vRt5Fywp2k};X2*<@bBalKj4&;wi<-JO$Yo;Q`IGGHnErw+*wu9* zMVMBHzVt-NF#mP)@%QbshvnlXwQ5{U5L+C2kM5E8VS#I;jB6{;A{YuFNu)!i9d}R9 z0GZ(bC35~gHuVL_yX9%r))dl3(i>>Dp z|CQpFFAJHi%kcKkV+?GO(kFH8BFjhj)sKefD3cDTSzDx|pd6_p;t=amGT}DshjYF>49JFq+8p^eo?= zkn)_{0#*;~@;MAS&Ygy{KophIpKm}rYkEb<@#_bh)9TygV`0i7NT5E8Ez;@f*lx5$ z?@|(+LwjLxVBm-B2q-9L^YfR(F$J#5-i`PEZ8t$I>wA+s?VhR)KB~7Q>RSYiBU<52 zHr2Z-h0AxunP=bJ5wlX#g7C@vj(oa4m;u;*}>-8^jh>T+yMbzY5C zMEr?~_*0XX!_o*5m(Xy7MUzQ@fI2!2fBtQFm<=-D5zqZ+t)!gm_E}>HSvk<^Uw9vN zqO;8~Q?1Kmtsg&^p)UGx^J(WNYH)B)&Gq^|Jxcx-Hyr}m86MtnD=AyC3rS^5aOJz# zNEYe67WtLJ1u{X?E1sK70zmB7VP|LOMR}E4nRiHgW`R_QQ^Kz_3r!o2Xai27x8iRI zzOiu=aQ>(J>*H#WP=IX(imZ+Pb=J+gqWV)I6%o4-h7w^F9;zqioR+-em5^fL^{=ao z&V!vQy#e^~UK#%C%8{+gTq+}s?o6{5IP8?%e1o5_--mLH|*wnet^gX{fE7m(S^hE(1 za;)XJ+5pf&$eb-$mmLQ7TVI~M7!V9JHOpjFrhfO-Hgy&R+uO1X|5p}P8-1Kdo1 z;LRkdSed~WpNh+!XrzhO-fS&Oc6})@&?3l#yXi$C&DS}dkPAeDm{cMOSKTyy|QcQnMi(0sD1t9R|KX;|-Euo*>G z+uGU<)Yw`s@dzWp|GLY_{AFFw-U&unZ~9^>f$Y-5y9!aI?Y#6)Ie=%)k?mR6Np4=-$9 zUMYE2ni6+R$2R|57#6MFLGT9EKBJ$vT_O|BwlP1Wnu4SvsUi%=c4n=MN-@5lfeJF` zK(C|Y{@z$@^JS7mFX+kd>*YH?>Q9$yaq%dv*#5krS$*9! zece8%+X=T**HufGz}z7X2DW`lTtx*0gHpN2og1yxfLSs_pn@c)?i5#6$^-pF`e}XW zrTo88I34J`B=y6k3x6wtSTp@smH}kh$a8!D;t~Xbv`W zr|Q`d6wV5cV626IZUA$ux#(x0KP_PPKx}#s9NJ16!O#&q^Av#E{wuwA_e`J+JVr+z zr(JhNT{2E_b>`q#U>`U0nr^1nTjl}Migi9;*2uMC6Dtax_MkFUJuG$Lt63V6MQ92z zb=ThV?&*aRI8#dIwlDo_?*=lari?`CFeyUxYzUyrVnt0;^9L*X^6}8U5)(Uspd=@n z;^B1lZvTz}vVyPEv)T_<>#5k7nF+|}09sV(Mk7?(p-=OEy_u$wdC9!+O+&;*csYyl zw;q|AD7kwDFu4C`t}E!D}cNBH}<1jolGvv*KPyn6HI?zNBEszI6v6vAHz`+O7NS++ON*L_;F zG9F6$5emuLuzYw@?9ao!U;0xa`Tq6FA^~^_lo`s@$yBro2uy>4Id7H6u{6y?cnz8Y$Clyo$}Tld@+gEEp>S5_ zKWs@wscib6iOH?7u(GgLCGM^-4Pf(G(Z1qs@z80W zk8@(Za>>PcpmwTy4aO+uSUvVx$z@vAt)E_o*3SsngU83>Sy-Lx?WwfwE{=#vKzI@h ziq=i6vSQGdi1k(4OQAbob)8o&VI8_D=sS$!aOM*1F5u*-H+AGBT%El=UekPGf#Q0~ z;a}ps`~kiD_nsTfmc{b+AoMa#pT_=bVU9iE=|Q-%nsf3qo%<)d4P>b}CcIZ23{n;J zO+{izLGBKCq7>7Q-n~#e##gd0@{Lm7bqe&NSIk~&#t-fO<56?eFKt(8sA#K9tY7>c zyt#4E{gJ`Y(>}oejX*=#@qxM<4h4ZryeH2V^guVRJll24aC(1ih1%??@H0_-{qHy&P?X45i??jq?2bk4A>!3G_gUMIbb&FJ(@&6kIE>o~CD z=ug813*<)A^dFf`qH}!)o~D<)zJ2tQ@v0lt7JZvEHAW#GnS6{uS-{X#=k5I8wD}AK zxjIiwPw%l_y8W9~P_@NHp$$NRJb{!B7NOwf!QUPwEN0FB?R!u%q}7O*QgQ;Fx{<~V zWaBU>kU;hLpD?&KdH z?exu;KJHvMUKnwh&-`@9G4f&7(eNPzBZ+@Dh?FH=MMP7t6lbveN9SUV(a}T(DMzz~ zU2UV!mGOmwHUHlWP+5tVuaK&YS|t{i!A)sPBRU97#a`DS zgF)>L(){_@)D$mcX~2^w4^R&)Clju}rAOgDV1dA_nyzP3KE7Ds%29J&J|~KEkW~}b zlvQ^gjb5?}pKZfK#32m;Z1(4M{**aa`+_WoZY}G>wS8V(X=hbGvD_8gK2=($z3gPF z)ABKey_d(s&O8+;S=Zja1j?%?AfzOfRtJ~lR3^rjL5kRz=O+a}xLt8R~r}Ejs868iQlP6&xTIdGNi$YuGZ>zsZABK8KO`NaZJHB(IFxlYZix zhtfu)jhVn`hC^i$ac``3WQ2Z=cbLVRTAaiWSd8tH3X!;CuR?)tb+A@sdOF$ ziZ!64T@>tbHUsT zeT-vjH+A5&7r`LCJvKbdG0=UD6yF}O-FOTP0B5oyS9QU}K}|qw>W#iL?z#D}G8agQ z12LPbp!9P@^S$m8fA7Dy@-X6!z{YNgDLCReM&I(a=zGIRBofzw7B>wiG&SA6=VIqQAX!uj3poG2 z-b$2n;o8t{cLAx}k)2|>@gHtW`t;Q%{7`snrnXWh%WrJW*0iLAirDO(-)Q)5OiV|T z3L2ch^lKdg>Y-}Tt|JAI(K{UKs)&Amep-M{{?DlZbbJ;yGu6!gz_2j2HY`>QiGy|W z2+zA^+>`fxjwZn6D~k$?%90Oga8rA;|BV{wqp7!N6v8VdaKi|Va|g(aL)$fLZpyf4 z!O16UZRw_G{~Ii|OE6M|sNULC;uO(e;C_#-toM^f>upROG-jVhC3e*>cz~r~Y_fIi zUvL&#)0(^!6|K$D{P(zoaf(rZH+NNSB1ab%{?WXg71DSS2KHm_%bgXxMJe`AVHxV2 z+V3fgIOI`d0SJcDA83T86LI$Vk7Zsd*NPo|mA<}XN`iZUC#!D1Z&bl2p4XYtcr}>m z>M0I~D+FUtp*`(s+?a+?9G9WyL4m8K)p$wu4=sQHfPib?H@5&&tEuwmSJ$QEWAgTs zfE{MAr(U0DsZHjnjVrLUkf_#3Yg)rQD(wuA6k|}@U6ivr6Z;yiUprNrTQd6%e3HEu zP%-W=aE#T0iRSy^eU*#E6LX7_M8vln?;VE}KJrJG3H7YKFJ+Hw3qA{NP@CIe5~1Pw z(4wcPq;wb7&6D1-0ksvzF>N=48J!cxxiPP~4d!4!8@sQ#yc?xhG0#A*YmzncrRC+v z1+ERg12)a!-=!xtX6m1mS4MdOiZJ}{{`#{uIUN8~C=s0d?U&BZ->DZhr`&@@vUB=_ z(>E=%Yz6anU-7Q$>$SLc)e<1;wl5@=5=hnGMeb%AEEGv69D@s++RDfsUx`O2Ux?$^eoJg8NE-rkV}0s##c1LL467!ln+J}tsi z8)Nz^@!we5ba%{zX}j`e+0m%I)d^oW0_+mCS&W73@y+b!f~tJZ?9L?)Nos{Mrvo)l;%RT z_OFt=7G-hepLJ*N$)M!hhZ~n!1yY$Eklc$ z?&xcQPX=mL8$4JpQzgk(QrJRxgudm39qh8MP4VHBuqB(DlqxFg6LadVXJ;J5?ABU& zMQ31hC?yoFD_aL;Upia5u3pQUOKljut?QJnM1pRgGPU%D*aUAo_o-GAQx_h0_xUvY z42->3X$lw^8>sD68ea;UrAE23va)`+mBAAc01~4zI=44BYk`gGk4(Lq+Joz7ZPd2@ z5h>gzh4`lE;75iO@r6UNdC-~;i0=)w<{H?;e=^?7WOfVMDjBi3XW)#i>I0a8>YYjf z((InLk>QBKmp+9o{g>(8AqLt;=h=*4FbpzXq*PJtp60%{%kzOfOLNo*oUWa=7BC`g zi{~%FuMeUi%2mo*d}(Fua`G1IH+#O(sE9n$o^~?YR(thYSJCu?ny>eMPp&UZGdZN0 ztZfK?tZ1|2^^JHQ4YQ}gk!OkijrhsQLDO@lTYNJU6G>EXimLCM0D`3C+}iqHf4!Lx zFvd}9k^3m(dW#!p5nL3Dys-~dzi)CMf#`R|`9$E-rGEy7v~)0`IM}j4KwAen6e-tf ztDI%5=p#O*p3Z@Qao&1(S4(RliH;pPrjSV4(#dnPv#7w;Bo+Bh9zE4cCyFB!efa%t zKhQvX9kmhfE1(sclH>9&Ll|k(3nJYzUI4@0q&pWUCn4e! zo3YmBdp4-3TgJ74oMim~68|w?jz@Xu-89!12411K7D`*nqP+gS*-V98)@}8GhT|bA zib$I#j5|R*iXU-t>Le~*MsU!6g5@(~WyzDdFfuot?_~Z16F3Eqz&M|LC5-ass(bnR zlnSZAxCd#z0w~CyO~tORE*_?HkHuMJ)F+iT-!e8|5_mtGTgSA{H=0?5^`ZoN)X&aD zzKOXxo3zYuA0PH@#;O58XwCyX@lcq$K#Afs&$s)Q`HZ=!oPDUm>g!I0~90Z8*Y;G4Xa*$=8g zxN_Ffu*=oTX{0{@O0&7j(fcoe9S>fNg_VR~ia3!KydA1jat8>bv`z(4QBjJo%pxo6 z>vb1@1=_)+2Yszyq|89M(6_G(=ek|IyMg%+cf$fDD$sU+p(+!2%~s0A^R+l%_Q*AP z+?RnQv_LDwuevd6*me8%N%Vi{4WU0=`e^wWTL~nf`}Z9b5{pXW4ZhU=8#RPW!m_6p zmjzbL(PnD#2aY5czGeSUG4?cQ<0gq-8YE2>myBOE=tfH(Tx;wq6Rm5UptUov!MJu{ z*tOOu+MoNuzu!e1zWWUv6gG5X`5Z`duJVXbfm6*M&oEbGgR8a_04`-lW7KR+!>M+wEak!U`zAnw>`~#$EznoKY()M?jr}i zw^D7f?j5Rnm$77T5)IK6h0QjF9gAN*d<%)!{38{R6pJC;WL|!U%Qa{Vco*42K0cke z8XOoHi^a-h>3^!Mq{`(J-t&ObmX4Rk+wP0O2t8*H(!$q?k{e*?5HkewX-DPRGsy&1 z@W=$B_!Z9i4ydS*Pi8R4 zd02c=HMJS3dRZaSLRuG3kpHNh0I{yeL&Jy5oI_m%O% zFgSWj9d`qaN=6w*l2L9UA(VmU!qp@;f+4oV0!RQS4HW`3j^RTW=P4h_zKI(j6(DrGd z>OB}tB(CieKYy--uDh4lVy|@bm$4Gl#ce%&6lu4l%(@&94tY{hW7=GX6<8Bk{((V( zkc6`d@axc+fO-o=ZqiA94Gi`vvPY?l^2(+xAQxs%UffkJ-#R3x!EE%lp@f9_S>gJ~`;SGjz_D?=vY# z1?H#hp)zfXG^NC$#>>LcNOOVe9xazElYg(jUavcNpVb;OcGt-AF`<0sH$iQEG2GI8TQIodJw7b#ck8%%l@gP0g#7Y%G1$u5>USmsN zDbe2D{ouqhTRq~{g7=@gW>op1#U#iNUAK$ycgwIlD2frz78n+aU~iM~0~4A=F2eVF~0mq@6AMo#l+gWLIwwvf~215n4~o%lCxjNR!aPb@=(=e ze2Gnth=;t{3Ep_iz@TeamXs&Sq02LS8wMBsoL7gah+5kyolSvhta=XmCaT_YVJ)r= zotp=PQZ}!HST~Luq2R^!uyuK)=52hey`dCv*Ef)xBy@pCFLfC(3&02ni3C&uPyouY z3^t*^Xn=_v7>FBvn{5`km7oj9-`0OQ(yMt7q&~D`<6GTwt!Yk?v*xXFW`O`}RVg zlRPl2F~!^kV;*%>D+B_NGgatLEA6i*wB2W|W!BBgwvuBdwgil+q{W>{av{N5u}c~T zM!oEQr-o0T*noEpgS^;lv@Kd>60((}_rMmiN|DbGr-3oZ^`VWaIT1XcAkY*9wruJ2 zihKN<7ORcR!)-=ZQ#E!CRsVhIW@K;sPeh{6lDm zC=A_2I_z6Klb&ycukO%8+~kVKY2X!{g>!=xkdf6AKJ9;n`D!G+S%qKm{(pH$!>6(5 zJ1pAV2O31e2Zpt-JfBbe!fvWK(s=7ruC`Pg!tV80T*SYfO~FQcgUkS;R?=Jq{F z?v{-WI&oB_ALzaIsrCHbotIKDJJppl`E4gG%8PdlHptSIy!D*=ODX;53MdsX40 zDi{?jldcC8(CB7o5dR;Cw5G;xiTt}+@|Sn*p)Cz&yF9r8Dyn_|{(T8NL*)?KSWGbS zMrMBs5dnPLXk#}IkB6f9ReD=h#GY!d4=HjLJ)M0Qkm{4;KiIj+8p3RF zG-2)CA`Fk%Dn|^IB*;qih^e()Aw-~1C~!y&%sFzNW*lfJ2Mn_%dTK~_-~K&Js~BqM zRzKvB9i98x*gyKJ+5J4Mj42Y+#FaACBhfopDE8Gs>F^pttGjk7$jD9S4yWLrAA)tH zI4`Iy*B@2Wxp)y_hTCcc^k89Bt1ZR}9?$q}kGaI{?by79H~r(@uUd=7yQ4pBF6 z-puQP+6fm9jQUGTQZdpdF?^i&Xmeu~srEoE-E_;K>h==AgHTb_0f` z`#8E z83pEsa3X@;_JI?p30L&-66QmJtBzi%9@i#kv zHp9*|7hGEa_hutE&nhy|t~)P3>`~g?2c@P=GZ$>Xpzl64XK#`&z4?M9biRF^G>@LT z{poEt3ZvT9n))xK9QpK9WnZ*gKOb9NrC#t}Mo*3bad}Vcf%g8@JJ1vub#enyf!Q4O zfWz!FaIU=debqK?WHk;BCB<0QyBr%wZ8@L>Xm!xSUw~^I^Cjg?c{`12w0Xb=@y$6W zta|69LUO}|co;1H)Zg5bM&f2Pv?csvDQK!*US4k*y6k{e-&jmc%;=G5(U-Hwpw~>O z^>4Zt1>&O!@WR|=rr6jY>ETV^pYxNCqa!X^2qu}ea+vhmvU`68qsRpTY!6RQL1C-4g`5t4afFsq zrlAx{@+BuVgoG&)g(Qvtm!gI227tg7=c+3BmXJyuA77L>1wKrm#=qX*_g7X`{nFY> z0c-=0i+grwkEMF+KF>mJDdKC#e7=So<)9tmz5nhIEjTWDi{_g!eWNk7KczNVAVe3_ zzGNQlO#73O^O}_iEe-@suyvXRj_&2h4{8X_xoRat23ZRDQN~2)P5`5r1kCGY541G} zTjzVg>U%yJ_~iZ7gt`#&!HxWzg=&Mv87n_N)R$Q|9vN%?uKLC=et64g1FmWbv%U}r z6qr_%@f&PxH1#~J+uj3j2&q@6lOL-CFbp8)13+sIuda&jWQd_^mC*z z;ivbm^G%dIDXRB1(q(J*X1T4o^MxV0((2nO(hc{A6{_(>(nE*Jx_hY&g0pa=_l6c1IgESB!qdACSVtJp9dul5x(5cOmy%By z?qJ?TYaPn8TP5CeBR z4`==4yY&tCNfx0tpmme-4Q|Rvk0*2f{^l&4D&262-KD$7mf<}XJ3(*=6``MXWOqN#M{7_MIOc^J{^gt*g^QLJw(L96#l-UxzuqkagOVGlMrh1hKcA}7 zQ}(XI_0_SJM+{Ixq#MQfs|1Rbo_EQMWdj3FMcCh=tsT?dj^7+s?3(A)&z+C@&0&&< zPepSMm7UPLQ=9B!m^AdzT=I`#u#`4kQSHh}i8i1COJu{r!cQFK-j|q`1MEYe3L^sg z_5_p)aBH@rw*o$H!7O3x@3{|bwY*H{*x-PF0xDk2!so~NOEJ4)WdLs0T!eu-c2(>lk8Y$j=9KPPY5I{0bV z=%25&WZVjtlP3~=eg_BPLp>z_vK?JDvuVPupSyk?3DKk4mc&$gvn&Yl3ue1FhX#HA zt}wtH)7}p3k(I5so3-T40A3@6J*{v(`|&$Riz;{8D7iR1c0KqC6;nFrg*S}yokdtI zjqb!Yxp;2-7$7}U#^#qjYwmUyUhA?pRXq#^4)%aLJ3S3cHHQaf`%ZE2)=hxgQ89Kc zn)7;0!iyQPSHP{snQg-pe%&kPu&Lf}$cky+brzFe%_31$q6O0yj)q_lKG~?;&JHK7 zj#PdhF4~~*co)PdMopl0G-oWj8Dd}6SV+gkeA$gP9wJh8O+@Cx(J2!O)e)UFQxZk)gAfzSOZaf^B=yv>3 zAyJ7m?-5lS2-)sj{N$C<32G3q(aj7DyyCX~4jP{BpKQu?aQ%4oSr6bPQpHM<;(khRmhJ9@JotK>#ajAyrFParP#Y7&T*z3wh~vp@iC$c5StE+}CSd zpJ*_IgaW(svIURg+uP-)zNlXR9elB(Piudf#Fr?W2~$)OHG**sjCq&QJF^hI^&w7n zxOiwUq84d_?iIX!a@1H5bQrSTAb&!7GH3O-miYUs`3+U>y22i@)n*}A6kQF#fe9Pn z5*q0HaA>VeNFTVuQG(4=9i)cFy+~Ep+9Gw&mZQ-(e()il^JoIt29rns7lidoeAvQxZ|j? zq_Omj_wIAvH_s&Q6x0VajN{>U$}~=u>{>`XI?|X)NE3FW2uoyPC|VhUN}GQNws~(B zeCj+uZETPN^Uu9OR67Ygck*!zEt=O=7;_!7c_68PdNz1C z;%%PlKJC!@Z6+S%0Wjor^4M@_&vY(g_JC?qMIc0jTUP(>Q2kyVVXTy}{ghr+8SPar zyfn%nMYed!xdBvh-bBU0y@@767#gTN7%}Wn8(!wIhNeGDS9KvdDzAsySSK>tajUjS zLL$-a4d;D{M5Kls;@awaA&6?ai77Bd+Nj^o#=Y!y!DYqBQajK!X5VYJnO|~zwmOC>{?!F8$^F&Mo$ApiP#+^lpG%(#%U`?VrK#yZti zA+cl9JbH0acC9X&!!W8=GeoN0p6}U=LC!lNp>l#}f-JLqh|@BGm~DPV_1qm37!i{- z7Bo@Lw9OWbo2oo#!DohBOJ^{>z`@muU_~jJ-DN3>O|itS9UV7E{A!*1ds;bn{6W3$ z;IDP9@kwEBg8ik`iB@mCgav96(fe=&??qFL1lVTYCx_?wq*J_@S}5KUk= z%=hlO+MGfT)^DyEl&Uw8K0X>gPMS^b1GQoGDtc5~y1EL!ZFRbsRO94C_?> zEVz!gvAsPK&n*o+8^hpoijMLm_?G~+Ff%rjh1$=L&F7Ps^skAO)8z@9A?fh!IxmIp z`WW7wdL4a8_rn)1Gvi}5odxcFc2bQ)6(j9WQ_jm7CKd5KUK6|gdp6B^5lV9}KIUfB| z8H4KiUHo{o*cE{?#kzBJg)F3q7!q;e<{l!cw>N_+Q$8`&|bss)gyAs6nn>#EXD>R*SBR7&|Q48lxSH3!cyGazp5N z0g;UE$pCLL=xWvF^yl>ON5i0r^E#^9Il2bJp{;0X<>7Jr+-zI}lDhHm+99rr&0?IV zA`03@8O7~~apuf7f9vdv1dIjfrH1olKr?mS#+0~Z5$G>B!;hD zN0B8IDyuy1zr!|#SF+Z`Tx(*jz$sfK zJ8n$?NGt%a0oCB4CSDF;Hpi`>_gdRz_gHMrnlbr|{FGoR(L}g?8PDf#;2g#-vHd-Ia z?tTC23}P62X||PLh_RD}ruRFDM+@T8_b*Q{<2fF*Azoufn>i;7^Ye{v!8IKne~&>x z<#yD?fGi8&0aWaNI&(+G%7>O^7CB_!?r5bmr=~}pX%^OpOic}*nqT)V-Xz`bF8;d6 zd)JE+YCGdPHDr8DdDKH|;QiL3vJPE~o~tBYP_>QJ9?B3;_C~cV7)Jtw*VfJ1;**b? z#~}R*?qncb$9HmDcgEA6%x&a9>oc;uw0b1bcg$(_*ns>Yw_9#N13wmpLQi!lR9Q#`LI!G)=Yav1%!;20kKDnmMG9>UM zHieSX7kS@6_rqpn8u-O34FrIN8T?w*{aq=#me}rIv431~_;D?W?ecLyX+(;fQCO%J z_Q@#l7k`V3JN)%Rq5T|x$uo=p6Q8B9R_5p%Szq^v_I44-(u!5+y%!t8C6GztpWJ@) znWJ|0i|73k^1tTiTa|^cTpZRDA(HUyH_zqqkH)KNh90LRv0VWOLEYwNUz2OX{?do{ zZ19-;i4$2_*}#4alq;iKw)o=3SmWF4rS8dl3-uL!FT3ZUv~jfKc>$Ww_*A+MQ`pt) zO&fcIkKF!V2K(JyUUOtek%HjcDRH^hWGDk|H_86mluC>Our>g@WPZ1WA^`ybZ){kf zPlT}%<$Glf+K?)vrXuM8`yDO|S8GEZ2SfEq&??3U3=#>Jgan#e*eXBiBy;^Qu`F_T zS1g2UJb#qf(@B=DrNNY8w@%+0N%R~E21Ldf9hq4Z-2>a`j`lmwSFq{mxI=7(u_T1v zYz`C80C!ff=h^NrI(5EY_6SzV0s`4L7NhwvjDDM|8@;AAzU>buJLM2EN3j!wE**8~ z)zww?TtmW=O{Ht^T?L8d+pM$2;B))E&fra%@Y>vy{xNipGAF;Lru#1GgHm(KmufQ*;1B z2h04xb8NJ@_jK#I9$Q5{+Bl3hJ8Y!tlN$YTY3BY8dYO3AWcC3THI}h<8^L>eYx~aY z^$VZ$Ved>X8aUKa_PM*KShtNR&45>Ec{vWkNqS(!I0Z!u_I0|wj;r0$ltG(N);KCf z8~S`~Ytdw>gS+E@k6fpWovW)6pY09hH)6jUnmEjhhR_;X?w2fsCdX9vY#Kyhp#g1t zw7)#~qVwiYt3nzFg*Nnv^s3(HebH}&V*!k)bw5@4XsTuD!lP0;@)F=B+`im2( zRsRh4#KGF;ux-#Dc=mvqY}SsS&(bMR&)!K&X7+1E3=eaImCdL~WPz7IU-!cIU<8Ca z+1P*zVM!gGX;ecuCcR1e!Z$y*n#uaI0-<}Q7D&)Z_jiG8s21&t`JtcAmueec!uN^B76-Q7*S`OL*sH7c$iLIXT0`9#6KwnAI2O4= z3^sDI0p6#e%1Pj@`t8H_>6(}oXVl${&5A_##!Q59sg=LG!}eDEgGSW{ zjsxr2Q!i_O4>B?IQ}!O|dUjC+O0|KNM~`<&BroFu8*KaT#F1>@2`W@1duK>$=AZL} z1i!q&H9@3*zX(^@*;U{y?QGQ8+_cGYl$k-S&DmUJw@wC>v%=uk6WFqT^^`&Od z!0)O!s)qC9sVBF;rb@^6^0RF^3alx$k7-|78-0V>EC)5ee0%@29@BI69GkXI0P>!P zBRfo;?;$gJc-8hp*chM9q-kap6N{T9(t`v($U`I2EY((pwS_<&cBkAk^l4TwPo{`^ zl(4!g7ysDL#(<8Fq|F}{Mo?bfTS8w(U-8fRKYk=D#1+?*_3*?wU7sWhEe zF8c4^`sF8AOqFt?DyvJ=N^ozf9fhZ?qSl9hOG?@wiXvs@7$O0v~HW^)$MZS`6}tlv;(rcb3p0& z8yl(~mTIFLlDVfp1i`{w%<2ufTV3=mu2!yh~71?99W|5mnhKykeYEz#{H zZ@zNb5@oMHjgPR(<3Q%3ZVEoTPW)z@a+>sPOla_PpTn*0_B%m zU=QkG&U|`un%MA*RL$EQrC}w$cOc?%b4Eu;Hw7DBK~2q6!+TdS8vZ2NB|r`+gFQ<= zfY<$SDed0E#DnjZui?mAUw@LB6uX?0vv2esSF1Abk=3JlL1))@4=taSGDZ6v95(By z8|nvKZ%wY_5E+Dr_D}vtAVBO@MjMBnklAdTLY!Yj*5B1$z|4X*^Hp^d`^v)93z+3o6Yz<#{uo*));<1@K}$ z@C)v5m=iSlW}iKT7+v4~y*nmtsBje!ytm~RsGhj1U*hgyvWuG<$_oVIbnZA>?6Kw> zw%hZ8Tg^pgjX=XhvXVYy+R|-sp;+^K4jd_<3XznS#(pO`vgvWT-+O1#_hz>?5tS0c z-RF%4>mn;0c;*AXsd@-!hEmq^iV3b$j!1ug;_3_OiywVaiJBT$=nN#3Txl@!t3q}v zgxzz0``s(9u8wNVb*91>8wuh;zBD6>I9dvWl`zH7ASl{`J{eW zbq`}Lz0W6?6FvO?qYBV>=on*LCB9 z3yo;L#JVAm9vjgv_e>xDzz*MdjYC>U#9Z7LQ2n85(Sh|tHgre*1>MYp7+q`biLlnw3C z`x8Qgi?GVK!k=AagEUmd*|WX%J@>tWDmgITu~X-xDAsa<6d!g~y>zHA$WIOPcy$SS zGo`oC3$~}fCoGw|J$U=hI$~zU$Us-y#^|xF5!I56iV!V{w47Yu#f8*AgbctW;5=o) z{F&A6+RhHY>E;1J)>;AQ%IC>k+i-}dFy7r>s|n>K?Pjx5yT4bZFE-f>(gvIO9J8=f z23sarmryC-&p_KfEA3ag5refm23=_nN<=!sr5=mBaNU>ZR22;9o$mZNoRA`@y#Kg}rrMkit73@U0yG(dL>LpQaKe*!KKE9zhdsS`(3VGGp{|mS zmijL-`sd>9RUO+2Cip-zwCHCFS0p;Tt6nVd{w1z@aV|cSka@>-5L?w|ckB=StP<^r zj4p0G-{(=l5e3}GAR-e1+Vg;G$iMsZXmkAcKX6U|N$c>xvnXARz>R7i523SG1hla~ zOT|WCNrx{U(;Ng{NDs-`4>ap>Xt7&;O1zOH%7@hxsM9V7^k-&Ovf>kD0L`bk4nV{ zS}L-!mUMfb6wHJL;>SZiZ*+5+IO_xiOGo_CHlm?oB(^ZLI19!#sDcF#=16{+x&z`u z#Ko^yZTFF5MC-Kk>2LJ6IhKV6AAIkf(QGX4a6t@_SvQXk;vDND#Fy z4%eTkoST}OdO|^xYos5HpFVwTp0bf)Bd`ylk90Zw#V5`$m2Vh}AL?iNGPSYtWC67- z+_%2YZ}4KtTKJ!Tz5`scGL~=KULXGb8kA!9Sy=x*CAj?;X_*X2&Ai=|x$3mZPo5%3 zFNeG|5+*r<7?(fzU<+!(>rRcSdK?2-$EM$NqjaxBQd)Nks-dJ)tr8cerHE|t-wU~JX+W6H=#?Yq_eNw*=2>-wL_e|mT`Z|a+mJ=`r zN~W;dY?t2&@yzu2)H*JfnE>ym?DMJm7NW9Gd}XOlm(?+yaTfQw3xv!bg`{TE7ADe$ z5&c`O(S%<;7Ij?YeURRi&J9Pha&k8xY&_hsVV_NaXYGntQ;GA9%37)pWj(7ra}c(> zf(q3fP2K%SG0g(6ZJ_LRtaw3fOh$~dUA3X?;}t%Of4io5SL-{=_c znCwU>GcDJSG9zQXce;G^#`GnJTFA0+udnP@D*xY=xZ$qyK%-Q7IMJ`o6HwYpg}oEX z)RS=1o7d?1Nc4m9d$BkkNDW)nbacI(h3yE#HglXATRV*(GFbe#$v88EmsPSLOaPWS zrl_cBhA@86>+Y7sw|8HhZ$9miI~0(VyiGx@$g%&`ptc~vIQPrarxi^-C{;ml9 zRVv$6GR6=B3Du`9Quf^=b4zQ{55Oo4>?kJHQ@~i`|Myy|9l9_BN(X^d<&pzmQgH&n z0|VJri1>&S)kkGqw}O)dJ$+#U89O!=58YzD?>+Ia13E9Kr(MyNfklJ~H#e^dDmt#; zBsM%`(M1Z?lIZRma)l{!C{y>ie8K!@T0r-Wy`#w5PU(B^%3xOn5P3npHP54`n-niD}oPk#@)0 zv&F)I>Wj1`iOK2xZ*rF8*pPbkYk6N`zh-=X@gsQNAqS6I@*@mP!;SN_Tb(ZwgTX9X zZM16!`iy~yB$5(q`~VKtC?pX37(fRh5I2$wS2h_UcwAp$iVg>XKPib>Z>&Y+1il$I zG#rZ7)O<*(8l|dI&YRVmkug2H*e-|p>XGo<(=;eFdVhPG-xr7JTs|mU(KcQwtK^@u z+Gc@N&X7{gr;^rRWd2Er?`5jgdiRqx+0!N0!Izi4^vtQY+1y0Jx#Y-~;z)12O#_yO ze>?00s`bnt6)8)&Y#FHO>y#07XV+Z7hlScpRzJ^r4FNgs?q{6%0>l>Uy^N`x@sA^5 zQ_JnRIg8DsFwe0+3U9B^RW9fW(rg(hDMcJ27b0@cE4b7Ew}wMg*D4D2NA~Sm>*9@! z*?Q*(uz;UkSOqaDs;=RkX$^+bmrXotWH%4h6X9*vGS9%$41>zfs{?xXN&%gZ)JCjy z3*hz~z9BMyTb#f_8uVXoCZ zka$n87Ub~3hjRSPkyzBhFGZ`{`1XQJjkEQzr*%OZu)Z(eBfi)A(W}#YWu`|DE1S*$ zT(3y(b1VSPp0cW8Pwd+!+3T90h{|<2!UHxxdRrqMikZ*gZtF-}0QGZO=31@IG&<-T z)QBHdIqo70$r>M?sugq0;yCkq%i%# z>>X7LH&)1i`>O-V2tTFJ-Nx224_&iZ^N6=E+iMBS_AiL?d4v~I4@_K zyuYVcZATn+@)t|7A(<=Cdgh)(JFoq`Xc|Y-BLVy9<9iBp<8K3s5GZZuL3+g<8LbZI z2XSYV;80i33cZ%9d?@iefxRyUOQ`L+t8-F$`RKRraev11G9a-Oh09N}O8S7`{Y(n? zeXS#}(%_{`C?z|zJ-KaEk^eL91^xz6BPHO}8o>5qG2nyZV|1_IKOl3gb!44{u~u`?B{9LMZC_4K-pR$Civ;j?p! zZ)0;cp)mcY%;z(scPqS<93(=GTxY733A@%878aN>4#(B;R`)2aBVT>~@YQ1WI2=;` z*Dr-p3tms^nF|?-i@W@Yj#z$g5x=MNNbk$u>#^mvwY5!kKI)OiLBnC@=smwZu3YP@$VT_EOQ*AF4V=OBZ zN;u3(+U|6<&@w$~0cpsvDYcpZ_X1cpzx>uPR0JfzdrPh`T4WKavve*%e0Q!ymsL6M z-hJ}wmpFGVtXL1wsB;Y~)XL0U-7KZ~i}enF*k_!50xrg0_sqT^4o=zNx8%=LjtWTU zaa3?xFQGVh+BtS(RlfL+x4r54f|Ct%!4@SH_Q2*Wie19JRkETUk$M7HY>z|j$I~0X z(PWNBNy@C2mm$uBj4_u0ZgK?^zDs*7!y`~9aPc+o;D=VurFG$3+RTL2T$_fzPQ=es zX|(&pJj2{$>`KbuQkVzQJEY{Zg)6>wU^~n%Ya}se&pk*2WRG-7B&0?ARdgvJGhwA- zn%XpY2?O%bI012HR#r{`Lj*5XxZ~>A-pZ!JtrT76;_sRiQfHL$q=OhX<)no$1XuAu zxMN*XA;E*eg*Sw0yjZke31GU%?4eYE`m^3<8k`SnjRo@Z3%7Z%N*$Xl`h z%^3*FvNgi#a;&_vq+%)Q+ldQJRiImlBNY(-yhUb%zj_zF>g}>zf-)eK8-)TLS!+~;93rF%vochnm@WcqAreTgu6&rm*5>W?ArU!&AOcfY#{oAYK+hRyha=KR#dcI~S zPGEArK{)?o8n|Kd-j{VPsr-a?;#1Z_EH8g}k@)%XqoF$y_bC42dWvz#-`fgprNLC9 zJYQ=sYEtz$d_uzY3QlI#Yf+~_q`I{|XQRw#=luPF68~ldU#kuvp-~$pl|gRBBv)bV z4)3lN2Rg|D+%9~I4kQuH8}zqa@|iOOiO|SL$xZLg%PdLhPB6xH73-A-2+*w7W*Jec zMe{pX0;X*R3T)}kG5k2PP*o-7gWa4WP<+AKo zcTMA0X*#tNALBn`FB$0yn@c{AQQa<6Ww7SW>}65hjOpT9KaHb9n8-`Ki3UrY-8i%? zVbSmE^be>m%W64u!M$KmDZ;|os?&7;yk>B_0u{)7klFLAXz zv#r&&m(df3NbyJ%Yek@J&2DD6OOH^-@Bz4qZJ_7qd%Cb?ALy(Pk0a~Cz@z}kgr_u_ zzHN}}SzH_m(A?a*%O$|h4GhkJ;R*;3yMYwI3HOM3h+?GEx8NW*D|Gncu<1V|qEl7K zR-8~3tFW;4)03|rzdmzb9v0ghn%TI4;*`i3oD4##I%F6f?0n8vV!J$C!B_tLC{D=I zs`m|^5JQ^8fs5D;J!A>hLTJ&(n_VL3G%wCXPNX`SU(TG$zx=X-!&c#ZDcn55;~N$) zp>7fbXz2h#yv`7#HFE%N%vT0$cG=KRfz5QpU0>PDo073(-Y7Yz>gdXl{EtLvqtG|-A==^ zYF!}04NP6-HG<9U>u&fj^32#SHz)-cnhC;DOiYlfY7@ZH zv%B0WA4h^QBUc)=0s>B^lj+IATWEcI8R6Ke6{(u9jXA)wB_|ePY{y6{}F89H6zbwds9caaI9V^M#84_tF7BQ{( zpn8<_Az%1nrQztcL0jhmea{((uUzpxVSkLT%uYbXv&WzGvUs#RlSSZa*r~<0zmip+ z=)TV&4TtW%fK-ZoT7Hib7v&#Rj)_(h*bO#H2iccg>lTkq@{J?E&6Xf0*_I-+ovRMhSs?K%F4`hc_bXg`R!4WX z!E0ea{$P6l0U7`?ZM>gBQ@u(m13lsI491-Ei+dSWTi-De_OgOe;xJ%m8pS=bcFEf0 z8~R4j$*-?^@OOWI|H4JwXS>6#_f<4OY#LPigIrIt-UL5ArRB-}k<{ah6AoygxkO{UkSj?}U^E>cwcT$J7sMKIt8Id zyV(=)F3Zc6jHJ&!73vC1Pwr*N8>oB5?&8rxL*rGvaD27q_et?bV|`7c2PQq2;haS3 zvS~i6eXh9aUVhCjnE4d$%yCU&y!ms5j)MfZK*m`Eg%8qE0{b3z20>H*HGIN9ct4_^ z89@x0S+VzqVp5=yEIcuZjOjWZ6ZvkYd*P3#XcQgH_f7=b6O6j)!+*FK@TgIpg?`{a zyIYSgta2vzh`t*879oa*1(+BFW#vx)w*`0SD4EWDsfaE`GpfihjpLn3`oj=DGx6XT zB?i}f)4vL1&FpL5qum#QM2oe*Qn;O&BpX>OYGEev1FOzVmwyUoEU?2~ixh?Jlg9S3 z8CgK0$=vN8B;^wKTw7brLHNyb?Ctv|E%E_&+SR#0kEknr+C@Ad$kS8ojvdoGCL|yI zsIui>Rd$atFP*S>k^d;wb<@wdg)lQmH?#K=lLUx~g0$DFhfkEhex-Kxr}a(AEf02ta2~iw2KdO&RnCn_5~pD<6W4 zKiGdHyJo(u)aRPAf&-}QvZ{Zfsl?Gin0}u#>U+&v&z7c{*g-%ZxVqv9!)q?;#*941 zEaj`Q>sGa*vZ-_b(eJ1vJgxfEODHM$B%D|r2E(y^UrRoEXT%Yr1D}{YB3d?G&E$ED ztLXKMS=#PpM%{CiumPRYUd{oEm1c~ z8|*l^I4~kN3&X}0bw>XzF6nD=LMRhDtkvF((U?iHl(Q9F0Y@VgWMyGL#9Zgad8B0W z=A(h~Cq>9!Tj@Hp87N=6?7jtADoT={+vnQdo;B*S>Na&wfDKzzsy2N+{|`ZnJab%? z31i^QyJOY{-|=wv6QYq>YwovLibTi7;gy44=8Q~Jf+c`FV5IyPI{(tx<-4Fzl+CQO zPzBq%mAe1gMK&jafsvx4MDfXj-av4@bSC%bn6(l zlz%fW*rOXRNuROCHGNhw=(UK{Lc}vbv%_7e8t(IaG_4%%=ECZ?wr~?7>&$zc%$}3= zuy-5BfUw5lU2g#tg8FYGt>n5Lv-Gp}m@E*0PY6;0%NmOFbTRP+?5)JwyO0p@{CtGx z`pmS-FTrdNVJs_$7V|v(Y{nw5NM8X zB?5_{D?5VJ7}8bw1*@|w*MsHkK{$QV{T*T<^Qu=@e+09a{xm$~9j0|{1q~x)Lk*(6 zy?sW>VGqf-*>@@*qUzoESzx3}5~BCTTrIR6+yX1CM3nJyD(?0bT{y(cJ=IlzTtr%J z24&}0`W_N3%$Pv?Pe*{xrhKoGUTGD(<7mns^%}My+vC#&slPT>Sbxm7*!NWQzF=(` zs_~hZKI*|BGO&v`?CUO0(3D=0V-r2WQqCNt;{7q0_jO#13+pI3lzlSakP9SReq_&> zS2IXVJRThd^^}Z2BT){!ZO!t(l;|wnAH< z+2JdxvrH?oS?NqrO>Vb9h}cGPJZ9S+@`6(at}7L=p~l?1BQmfaC7q=E}`h(CW<|$)B7Ac1LBI?9ghU^H7)L0j*UZm{t7etZlY781?r%F3 zrzGinBQNKeM!OqaUwe8U6Dz~*4e&hM!bCQPyfDTG`3vGgJlwR&Ht^(JmF~Ib=lk3W zI;Ij=1O(K92Ypf@#3~P?3H?5104TS)XzPkc-)6#x6dN7c*@(6G(H4&%Q|F$|M`h^( zFY|M5&91-Bpx*^kOXdAJqS3q2rB7=Ghz!*EfT9}2FoyVE4PiGk>B=%4 zXP)bGtP5NI7$tDqLW%`7~hOk+sm*$6FS` zT~gxvsU#6b+pHY2-y;Rt{ZY66Unj|f-b25zj&`Yna?BIw z`(b~uwbZrQoMxd@?c5GCCJ=v>`|U38=zU9KeooFxM|?=NnJFIf(AMT()eTdlBIzs& zlrQ2VWDk?WOUnOFbVV9hN7=Jj!&8r?N2;ZRNDk>s(zmr6sj4r@e3Z!*QSNk6q4ZhY z0gk&#Q}GLzf1QK?4aCBZ(!p&{3GVugzy@`b>;RuO+ zgu^*SRYE4D0+?nZQ^wuKbn5+(keR33Eui^jSjI1KjPR2B(BTc0jo4Ib-L7}`DZqY2 zfo@*axTl~cO~77w{6SL}0>c`6cP5MbwCe?81no<^Q>Fc@SYu;Vmhr@U zQU_(yJN#dtP<{u@MB>Ot%E3s?>mjl_lfacEzl5ud#KJjAneO%zBWQw^2tSRA#FzAFv1 z1Zv0%78vz?+CWxTpypyaV&Qe=^jxuK&kJHdgYH27cQIOZKac&Tr6ri=6z?qI{(+KS zk6(ASS_pKcxR-GK?b)aT6#t&|gWeTashFpExzKm=zkl2lFa2EoUOGEL_yg$@5rP=? zE9mQ^Jbe*3B_$Y}lNWIl;k$z@|NHfbxjCX#d3!*Szq5(i3e>@F(=ICMER~{+jHG>_{o;9o!;9RqK#WSI~5u` zs2&~T8#~{v(`ipPr3i^cNzv13XR>(rk(g(NP~=0Ev8>+n-nyD*xAt0={4-5r?Xet( zpaAF^%hOAMwZa_BSu3p~w!KCicblq@HD*%gYZv#3-iNzQlpXnj8tFDhKLU60e8?T2 z>P9YWSxb&s++X^UL1rbZ`XmmQe}RYd-z(?aE1jq;Tie!4cK%_iF)3hgvyOs>U2_gAL-@d8FD|%5E!x=UwKlVF{_CYD!8QI}<>j=t zd~%^)nWo--^Nv|&l-f9yEnOo+^weI6B@R#5`Bz_(e^Bu9L&~W;AfB4aOpISd=jZUP zU|@>k-B$lo7rahH2zri{g=I8S>T43qn$?3E`xbB6u#wuPHZsc36te(m9$R6Xd z|ECeBU>z^Rg?-@GD?ziza9s07v+lX^S=YKaNCv_euQw#}kj=bFpY4ajhF2i0^$^8V z=0KSa{~eeGmSVasc{HiOHt+ItdZRd^b-xdj!tltU%+sBUoGO&Sy9Sg4V_dC^dyr|w zwNiMAh{L=cf{1t1LbwTuu4QnTYe2CI%qYw3B|%Y>I5P9;pGHZ7OnAei_6ehAWb*rh zH*<6~tzRV!H2Z{8c4zX`{;NKQwHU<6@e%S4C-T&+oP;i9uOeqAX_PlzaVMzQr~Oht zDlM1^&f`?Mt&hqr#<>eWebjMpq5+q)6e(pM4F;wX4e7-xN z$;($K_sLR{*-Y7&P@j;VY`z91BkQX^&K>eR*~5MkV@BsJeu0eibN!&AQvWjjtahaF z@ap0QoWl$2!vh9 zLxOn!@%-x0G)_&anja*)V;qFL^B;iD0{Hn!7x0S{`FXsy-z1RUW#O!Fqx5F?@HaY% zp!pZVtOYS=m5dO3`sbb~ly=cpYH?qRPVp}GKpY(j7M!@lHKTJsuJEx@1Mf3@ zRrB6lXA4)H3fXaBhD8};$hA1Wm`Dlni%iW&w-OJ~UGZA!O74a02fud1t1J-#c+aT_ zEW2h;;PU%VMpiZl=Rc9Ys0z5D^_CvLMLV%cm~o_|stQdgsfnimJ{OijOkg6smt_z$ zgZLmc)`(wmAlbdJKnSs{DZ^=+qu6iRJ->S|38?b}T`d}QKhefd+{(UJyv;oTq?j`w zBEcI?7I2{o?`9V~Y^kD5&RI7VY+0CZ@aldUAHuQnu>vg(OyJ)eCC^0UTro%6@L?y$ z@DioyZsQsE&BCCdiJ_yw<(vnu^ZjoF#XY5Y%%wBWILJ9Jy+`;?k;ZF7t><7uXtVWq7)X=;%O!k~Wy3X3uZzoXX>f9MX!YjxeAdk% z*Z9&c(c2S*REV^J%s&G|yW}VGA{|bX5Q6YD6FPN0toIy=M1io1X0^xMp#sdh-|Wp~ zH;)dZN0suPYzfm-^sh2IX8kjoY#EXo*a@_*fI9mY475snm8Y zw8-t>r8i{e&QPsNx^40s8B0#-N))sU+V{$m3M{JLhKKtSW{FaVHEM{oKFrJvTu`pp zDrKTs6bbXWYr)}*&w!**q%!&D<{Rpv{uF*LBJmz2VEiQNnRdJGV|Z2~yzYKW6Z}`Z zhdu3WX?5*H2xO3Wl!~_*EuxhD@=k9Hj5nvy7@ypqp0nS!CRR<4!UUp zX83S0|AG{svFN|*YBL(Lij$ooW1K?Gu9X{t5WrFm?%y=WuERun>|2xotkyjnd`SQ5 z>n8*LG7bL>RnCDX5fSYvj006B>=h45 zet<;CIlCU~0Ou`h*HD*~g&5=gDX|+c*VQP)Mq3u?P4QLy_~v?m5Z+y%rs16J4Lv0I zV`e(9hnB@UaYK=$5XQuVnc)D zrcr$%#i%NZZ8IYG_%zb60e-XVf_-*&PXRl_Z#ZA)NeyN>8_v1~GT(Xn8U6e<&(wb; zp4Z6kWaYlIuwYgi`cVvTeK^RnT|yuDDl~?-Sn3NKWYHGfa$zI&(iYsCZ;aSLvET;` za)c>^5To>^!#7xYDKISO)(LVzI69sl92~@>bbW}UdUH93am@N_+;sxbB`pB{HxIA; zx78USpcq5J@guVI>4wT@Q+odEusQ+4sSzpr?YR;csy9Sj!2=CV9Xy{LsjZNKe+0%p zfgWjo-%6{*1CzmWP6rWW%PUvUL1f|A#D_~ymtsld7M92&2!Zn_F}s8zaUcLfF~lCg zk{Id;ny0aiS4qt6fARP7UBnn&)hSE5tDhkgx(h7TNBNZq86zito3-EryLUp zzu?q}2u6s^T97v|DROsXfERT0n)Oy(0&8-{(r|b^xnX+9U#7+S9?p{^**#p-p{3yx z#~k#WxaMkB$!phnY3IuIB~7}yFAs3|haWSo7MAVG0nT4d8GqbG+m|=LJ%}%WeVMV| zHLk>Q#XB0Fv^YvSY|Qis%>Mp@z2$jAw9xf@JJ#d*JIEwi)Oy1j)sWw7?VoA)hd#=$ z50NI0+kV*eBlE2PEEz$L=VEV=n`JaDtu>71E{vfQ&w4 z81?m=k$>R(>22UyWIIZ#q~2@P}lsf(#+^Zy?t5Zw&cB?l};PJGT+RMFY>XBK$rMSZJZ6 zD5OVm^W9a}zKNmGQ*?;bS``}Z1Md@)BQ#45B{%o&UdCmA-A7l*DndgL;krgn! z)hM09$k!cOveZ=hiBX1*Zd5J@_ahxp=x1ZdPCgRzxq#N_dr!moj50yIDVxEL7joI)n$6%jA%^%cmb@j z2mj{I9Ru{-@;ilVj*0uB?Yh?Y7}7=hp{iKj#?pXT@YZ!o>_5kcn%0%5l5&2qp2Z^A zdpGjMNHBBxy#F?!cN6q(0m%IHC6oTDo>~C84?~BLj>@7SH_RA_>q+fUjr80?DR)cD^Nt? z)Y)P10b_2gE-KF01H-@yh^J}eJG6A)NNk@sxvg-2A^0EkdDj(~3=Y7Chuq#CWp{=x z9#nR9t6o9eZz_D>)`DPiguXtQgiF&Amp;sGYHmKzDJLb>4J=PY`=fe`i7dA8;dLq? z-}Tn=;>Ot-(c*}GFlDfIN#-hXH6dFhRmQ7S<4dEF5Bn8ap4!1e6Ow7-Ksq6Yj}{$X zS<-G4L6i?^mI)8-2-#dbuo$E@%uO7n&`_xrAYLYgXKQ|YG<(2vm@^(HW0rZ!6E}YK zO6fcHeubmxhnHs$fzO!FMp_lw9 z`ME2AcBkVB2sBIQ24fez558H|CLEoC%bmJ$fIt8CZ}w3x^p{VvWen|7;t3## z^jVFz>4}4z|KJFVpPE*tWZS2c3lET6l6drlI%N{IVtBhrwHLB?8g*d-H!A!sI z6vYX^$I35fOf43}TVxLK0#T;{;Nf1gwtd{!TZK^9l6-?oEc;-1#!Ob+KtM)FYx zoOD!3HHO?BAtcmw72)ve#U7pfCiId+=oVk zD2H2!Gx14|HSZkO=4>>Z|Ag!xa@Prv;8%dX6Su`teAdQD2i~|JLXWYoF%k$FB7*2G z%Sh*wSW+cNzq+F#@V1{`!nAU(JIj;f zN9HCZ`YZk2?rk3Sdv?YE!<26ODYft-@cBPSJ1wnhXrEOmn9;JAaCMn23rYa6bWLEi zuVh(etmDDJA~8*{ENgIUQ0cvNE?Fi0^hGbDUlS*YLLp9B+t9ju=Fc-QmVW)~Ob?!R z9TryvGN(u!aKZn<>VB;4@4K@yLzqY+K-<@y1Li_*z9n+U=!24X#(!kYG!PX0dn4qD zUq@p5p;dr@4;Dbb#-V>TQ9P2(v|Zwuq$PX%&h^}-v?!7KVGVl@{+m}g0$3B7;=a@Xv380>Q(#{kvJX72lhzvZd8sqs2{<@^a;M<$hYw7H;S1gpQ!}xc% zw2U!{8Je~Hjft(>4)CgsXInYLV`Bz&V%$4>NlI%ne<39}a1|SX^2TyoaJAM8N)arP zIqCZFC4dD#hL!#iL?=nkyNfvFmC1wVj#rO^y!*8Oj5T*viK7IzKZGv({`yyznhOgL zGDSm`S|ykjz~SKLIWh)V87{O2fbXE=Ez7Dq-|$JNU4Un{0FYtMMW?t?;gY{i!NJ!%z*T>AXO`>uqY|;)ALisQn^62` zG|p0`zZfwd-rnBx7g)y{cks)*%&eB~h12@S(V0Hx9PJ^i32Jsp!~)8@iqIU`X34xC z(v!wQ3Qcg789s#t*$3qJJqpv`(HmVP`En@Eyu|oWGMfF{-vg@uK$h8UP;nB5Q!=Gw zdMDi2pa~%b}`CeZS`8OX#Tg|Cirl9IkBqt67XN{R=@7M-2mYn@I`H9+G-Oz68@It*95;xrGCfLkKd7`5AaLQ zu@v3ki27g71m9fZxDmgZl!%25&v{uYD27q9ufFvS1wKHT{gI@mJ&wBad_&dK?ykFZ z)yEX-bP6OpBQ1K3zUKbk4O!eVJvPY$eSe0(f|CcJ`Eao-hR_i@I~I;j<2zWAv?l9@(NTV0ua);6 z`z)qUpMcNK`4S9@pK`acl4u6u@|97IyQ2KY&fAGf>{}5LS|Jm$ZBIXv8o)7z-{zai zx@baHoYHji7}~f<^{MY256OIJkl+&sWti!d*z?H0 z;jZ&7>u>&trn8QUs(b(T(9&Jf2uMjcNC`?w$-pp#bVw+j%FsxN0z)GuC^0lB9nzgc zNl6R{(g=vayPxm7erxGk@`o_Rf=I3AZ%K9P*&>%G}uQ$7UZ>Waf>Gh(HGMP!t;+zCdK0g7jja$UhaD>e#LWkZ#CEygD-@o8+o)vA zJ3L-wL>0(`_E;7J{hK2jD;wJ_cdejzCr7VYq(`A|>>#rax^$deYE`tm77>6GJI;$EwU&tiicIo_csa-cw@>1vg3|6?PZ}_ zg!vCaAY2NMK29~|{k%I7w##84m~B=vap~UoR|;j)))R>E{%n;gz-b6~nUNq3PZjIk zUX6dZl&<C-O!opND@eA!cnsfoz55iAH` z#9kE}{t}L4SbA$`qBYQ9ah=p*_I1LZC;KqF<&DMOm7N>uEwZD`ggD|04rJ9|fF%06 zSo#-)$jaY5THShTD{dbFqGAub=b#&whY*7GW z)1(4v@818+_i&-KiV7D{0xv7&uPHBf)}NIG{dNgbKMycCK&!r%?dAURy4QH5r-kA8uYcExn^W&Z0C?{1pFsqwst317;mH(Ven$5q!ZZk-2$`Q^6e;4 zc89elB{7PwQuq|kAJKy6egWC`jn{Gcti8fYBv%-x`6mTQ`}T=K<+vmP)|%ua`f`5} z;@Zw2o+14!a@;DzU564T!Ls(1fAGbo0nd4oM%`w;c1>0Pw~moR_Ov(TpVgPj&G?F5 zfsVegYM7UVo~g!)4ZeFe0P|t9(GJh>eAChAWMzpSJCqgel_^TmPEsp-&r_V@>6~e! zz}h_XqNpVqGg@iFd?W3&$7M*bzBW2GW&lNkl@UzM&5s9nSz1^$Hn>@srZ$xFo6Xjf z>I}{KEc)A`Bz-Khik)?)B)0bUhSGqg5!EiyYyI%`ZlEo?$T8laDpi#$7~h3j<}*h> zlsV+gB;;(7A}%dLd67WiN1lZ>sl$YV^@pa?1)PIB+$7PKyBr0p>SlgoTe5c(N~?rP zcqVM${_fy>I;MydkEG>IN$}Ve6BCVPG3$W2j9}Q?zwydhs#KcUQX7c3wHr8 ze?ZOzfz>~G`x{^}0FyNCGR$%gral((oUwTzB~OfS%Y2OP7W1)S!enz4q{b{1I}a<} z^!44wh7RhHn=5`D*LyCXy=qS6;$vmPm^~zarJ?2z9HRfth9X*=wAYS$<>he@;C)|3 zrTjTYBR4nbH^1hcsS^6d#(9?zkNcW7s%7Gq-NSMPjj<1!s3W9b4k`A*O@xdbSD(Cj z?y8Mni1i7uLPRM)ly{5V-J~p!xl;zc*YRtl3E<6Vz);L61+LFWfAOjHEae}Vjw-ns zo-j>@uWvOzJKuGAGq3_k+lvh|T6{>Zx0CQPijH#QG9*|8n+`$6hE94 zSfazOC?AjioD}mEdnM{`di5`Q%Gr0b^KIy0&yIF?UA0517hg6LgaM*~#g&6+hZ+g_ zJC%EbgRWp#a7gkwRKJ1h5O<}Nx`^8@-5*PKiXFv3oj4clO~O0#9?yo)VhOpv(5;Ex z!H2m8?TMDyEgp%;XMwft!06tm5+cqZ){z&SU1vVD>6}6KGYM9-sswGHtUhWvaF4Z4 zmvk$tR}n#4eBdt6@P&$ua77*9B<605FHrTmNot9Y@Adqbq&GC60YtVO^RFH6u<0Oz zbi`lbD09u`AI4yZkrdXWAsk44-b zznH+sICZ?r&c;eR5YO-n4i2VMWmu-EIV@%hwTqSxOSLlPqvY;Q=XyHTUju3`i!I=C zrAX`1rGnbX;*y1xl@-(ESNGM=FN601+!yj~<^^q}7tzPEL#MZQ)D6a8$;5NVZ{F!F ztZ=^T?pnGM@cAYU{`J;vae)RR&g~y+L4YpGH^N`RgX36r=uZS2uezh zes3^-H*)FTW)H)^1$34{j3;VA5)v)JbF^ejGielkVu(BCd@pa3N7F`4gtxtKjWo8U zfaN=5_qtSmYvmlJI8*t2ma25?6G}>?eQmor8i#KF^?8;=l&uU#pXTX>;ZQUCt-%im zy){><=E0Me&V_)gPS&*%#j+GJ<}QjkBUi&Pcj9kzeAxt%o{wp21ooP0P(wBGIm+fL zxNw)ckNC~S&fZfJV0^~jbl>&GsIFM~yzAD-he1(2HfxUyKY;zJuBG?!gsfv+K@}iN zKpNZ@VY4{|aYq2K^7m{=IOO1*^(GhnH>!kn`kTiKoE5d14mdSJ2d7^d478IcGR9Ex zB9-|f^%O5Rasg!}nPQ0p@izCRC~+Ynj&%XZf_?a{hP%=k_$=MQcZPr#T{UhCw_%e$ z6?C=cf4$X{OI+A0b=}hygj;Vcn)Fc9`=8rTO=x`uPK&~&%;w32QyoY%A{B&A zVRX6DzvP4pV)oU1jQop{$94|fx#!OvYej}?`s?cGAIw;Z=o^le?q-^v>AuUcgCXdb zfh3YH4ll*9f|tk%wZ5cRB{cMxOgav<_JK{~Kx;~(<^km5@X=qT$M?>j)2mBjK{#of zYA%eJF$(FI^&dK@Fl+tb{Ce6{6Pl4*Yx_kn`05!ZVS zvGbpTbrl;GYXolChAUKMp4yqO{gdUa0ef)$N%^h2@A}riT9Y&f9(4GW4ESLkWgKJLH+J zio_l9XhYc;LtdonE)5)W@$cmR?IoDJa_2x$?#f=oR^cCg#Llaz_?6*kvdoODunQB) zLhbQ)Y5ZtNG%%Jl*E9*B@<5jk^ydFF8g=Wlq?sUeg`%2;P`QyeFke&54 zxV*Xs75xXv-+E$nD3>UrNa=W0MGJf*JE|a`R4S8P1r!bSA}}HZ9K9|M@LmSGpM`yg zO8c=FdH^FAsuXy72lC3tXA^vzK{W+*RCv3!$Kir>bO;NSG0XUi*CfgWb-brSKURcq zHl^D{FU`o|ZFfErR1hKbI_zy!Q^I%&tt3ejad?p`eB|b8a+ec{H>ZB6dd7&$zAm}A z!-vV$->DYRI%TC#k=S*Q1w)5PJQr0Ds>{IQBSJ2}++`^fnRLbfe79Bw64+QyZc-)* zv(M=DSBS}^g#h6@jx?Tch^qy)p9q7iQ8oqZxHQrJHATD?UGZAI5g&+Y`9Blo=3HKW z!TLubIy1%_3f0uuF#xUOkJ1mw-}LqBvR(jK|TH)`f}1*Y+}pzn zA)dpAV^jRa_W4XxHQuyLGc}#NGXwR3L3z$dxoD0Rc0Q3~b-b9t9dq!z-P{g`rrJwN zVSW8hc6GI`%4~MP?AP>H9j@_~vLauDyHMRu=SZ7O68>LWIh9Kx5t1FkUY>$;r-Gh5YJvKIkjYK+Ueg zo>@??wk3aAQ&VYEbD+?Y({SWA2Uap^Qh_q1zKCia8}r&7ms1|~@x5W&s%>Mb`ZMHjODXd|*+^_E7bV72g*nQaA=rRT=d({k%mckaUT$OV_5V{9bh#Pve3YC@^o3cw&C?5bghw zWXCW6GH+0)d$#0(6|^?|@FNcFU%Z;ybc4IMXU!b8@?@rf5sAepIisy;j+TYM3RYR% zV~bfgy5)~ou9ukacena5BL$7rN|Dj@!}z}G)4#klm-1x-I00Cs2$X8Dji*tQ9(;X8 z*FORA^xwt+_04u+B9#+>m*mav9)XK&sxW0f>Z-{0veXJ;u=OXk8X(a-UZwbO+bivU ztM}F2?b_e8?;*hhoT3HSBd{TI-N8;Z%mg*t^%dqsN9k9hWs}(WqeuGjsrDrz%t8qT z+Nqv547BP;^R1O#+aiJ)XVj~3ysEf#2Jy6Y4=*1%x2wsW$ET$gZu#nqMEWTENDL3@ zuO|1Innz=ex2?y3p|6f)lnV%l1y)lHsq(ZCPj%fb`~J%d3neHxJP*Y#yD)nWU*9gm zEhksJK!ifzfGX)6+X}7*aN&uaKnHw z*2@TNJ*%i1kku7r`kTAxznudEKV$1H;~2d~z9d<4 zuZrhn&ILHhT7L*ho7si&ZI+e3;5(7AlZri+U2niOhG~iqCO$~zuajkHLKQc09-e|DKNEU|rks`GDt?s!30SbJL52rrs0gu-)zmuuUYp^WF&_9fO zL>wR#8y3`RiH!mtG)f>tYlWntGoKXR3@BX?JimJRwV^Z|Vpw4D;OZxyul>)ThPt^3 zV=EKwW}FX(O8?f@h1>l_gYM~8=VmdUekAC{^@YxI|7uDhmW&J^LfZ}zD~8dY#D>Hc zVXt{^jGot%xniN>EwDa7b8;S3e9UN!0^Ys5 zQFB;>J{;vvrWvSwTUMx+PpMwNpqozauZXw(2-(Zvl_G>ae!pozHv)fB=hb%R&!E9YP+QjjtaT>t5^VeVCLLhI-D+pG5T#0pE6 zU@C@`xZy#2D_U48x~oYBJMCS3nT8c@0|;@_Go2qOwaaH^xZLpz1WmnoQK;!J-7s-P zdIV{<-Y#(^(w=W0-To&@T-4Oj!GA1J@uf65xuc0=Z39{6-KuH>TbVtd^eO+Aj)4Ke zz&Tu4bFRl3aQ#UUW1Y;(dAs8EqhFY6J48Nkxh8d45`g%o2Ye4w!WF4%V9d$Z@nuz1 z2)urx6!;ejsIuQz(sf5rP7J?|JSixKRC}2}8Jj{&37)6g6X6s|!k|!L$?_o?&vBsQ z(8as;8jpQQ(pa%uX9Vr3Zjd5gtU-H%&!MZ%a}}tJ%xUm`HEk8}9u5jz3E!-e-|XJr zY>kYB)Cz|Dw1sWUu^0QmaO)yG@T&O5D4rd%{=z6JHUHV3xFGJmKn-+Tey9;Bqt>o9 zO}02KaKy8s6lO#tD5GWgTYV?qT>3g}&zCO+9@*50vwnTZSzKz!KqlboPG)#P?_G*V zM(!xWbAdkAD!)u3=RAf&>$UjO+}4rTlj=f{RHfK=k^MC-81QL)6!jgKc%0bh1xV>0 zz6~9AlU9o4feWu-*0zNmy-{5@)_-reRun=`uFs$UBXwAy+kI3g=>02Y)0Jd9b$-Ah zFgZsR_k3uy>h!c=?ak}|P$gI$uVlIbr5Lz#$3a!L3MTl)%`EWfC*FTP z-2ePj2??a2gAB-7-OFBpl8^TewH{ZHS0l*h`wQx00m`zU>gl3ugkn{Z_9gY`CPD3o z%w*Ru|Mi;OoNjMl_D)RfFd0sj2ucnb=9fyOE)p3*-_{8JG%OT!p;4bRrkUj9+0(ib|F^YfuFvWg?vN-3&=`MZlP;jSbiv7I9-!XV^U6at2Lt?4hZz~1&CytIIQKD^k z5`<{}UQ>mu zJyvfv>B=qMV$~#}VX=#J)*f{Oe}_Ndg{XHsh;vFTBjk2`B7e~G9r&;nv9ycNz}syC z=tU;oF2Q67p#1b*zhMLOnQ=W~znUCSl7Loq8u$>u1Nc&slYYqKBT9(^bt-3Vi)dmw zjh=tmrT7Rc<+nF0w?Vf<=k2%Qi8mX1H+$_IKRd+~%!NX&8F^=vcR8bCe>l_KMI`9Y zMl_M-RW;5(F`&7OBxZ-_ho#yx372|7@Umx7RW0nqE{&OEi_5MfG3ev} z%K|v?L)eof@`|E(a-%aubA&{4+}eLeR&w^|dZx((FYqEOJs&eXbN145B1$}nx5%P>CAW}RpXD`wSeWl<+On5q zR>s=EK=8?4Im@Vnn_K);zHnYI+L*l1fDH=WvV73uFWP%_g#C1?cf7Kbe`-kSYkZC1 zh?Pisn!WR^NJ+h*)*sW?!uxX~>qg$)d3*E1uec?;gDsk|t zmCGJ6f9_&549?u&Wyd=^C(d?fpXkM@3csHLD_=R}Vw?JenaP;_;zEH^7L#4|Ku^t* zuLt*KFk>QKE-LvuCnJ>Gnfvs4-n%L4fN?(BQdE)h($fc#b z(l0HM2eSdVs?~BHG4g5AVHPep*tT`~YhV7T5e>5+sEkHZd~?pWq}_X@^!Oc;rqqj5 z2?EwW5!Pt9)sVSXT8G_IfsLtcNd-s*lro17N3mne2|tAHU`CZ5y=f!%I{oOvslRQ( zdp5-U_xeR~z57KlN`j!{@=%>wT)sr>>Tqo$E$Vv>KzoWavzgWXCNVSHJW6Z-w{ZLR zz1o7O4nKDR@QiyZ@8Wl;qFc{NDF*_z)j&jKv@tbi7*sxQ{{0mHNAG8je*Ye89IUAi zf_KeUwAR*^msyEH6-?=F0t6RD3<2d$ugFxO%*r$o#H%797^=}}WK%cD<0S$$&oEJ= z&%}BzPlwCf{?u2thJchu^#1_W=+C)t{z8k4^6OQ%z4|E`C}tV+<3!g_r}IlktD2hw z_J^n_w@&b%uCJQ&hPT+6Xs6OjGS{Gyh%oPRGVYwBzMn}J6j)GNs$^m8lJ#{$U0sqE zdUE6j)K`1HA47wP-8F~K*x1_I7T5owjO_Mh51D$)5!pFl@`Qvy7@*Mc%9-N{`&9YB z-W~&YNsc>$VQCR&tt(kM{JFX^%T);UKJCfLQS4nOET55)%XC$G{r4xawEN!c4%1)c zjwR3L8+3bBy?-Qo>4Ikyy`cS2+usyWG_f8q7C2fb#86orO&^?J4k866;R>zG2j|I; zx#f@$69XE6mf9!~hz&!$cVY?^@adhOn-dAa9Q?~s%Fy8nIr`3abNCXBleHGnIMWXC zXeXn&o$yG1ZFF~}+XAlpv)Og_Dkh19M_+ETYD*6pAg;3742S_os~*BS#Nlmhm;nuu znb~V6lZPl-USg0C7%N2?VOD67y5)HQfQ-GL->x(3=3yPE%#-~ck) z$R>@T!6dr)3cQVJA%ILG zO!Q0MB7tf#Tq$h9GFn~<(zOf!(4MY>in!^tmzO>`R(QYtfhax=0?Uss7*y6)3yfZ0 zmx|Kouy-iYT)YD_JShuGKoMU1%P{K#YK5H)vyes`h2d-d-pQzY`Kz+4aHNcrtXDte z{k3o8Ka0g;8z$bNF13ezt&v1N=wx|I)_Ze=Q^#E&8@}!qV#f5O|C~#E1O{RkNn{rP zei(dlLHyOd^=}%s7XNe^J@#@51BX>=u6)hA_7{(T()*kHc3?wpKvY@1ESX&Tsg9MSs4I5<@uT>?JetcI!Gwrj?Svs>ZEZ-5&?|44aiz&eozMHgi1OEH z_P&!D9%rPU(raHp2bp~{IFvNZk{TKRv?mLWEpYkV-jp*8Q^YKcCBW2}8Y$^lJu>pt z@}a3`f`Fs32gtkAToWBdr07!!a7W;MtFoj2$ zeaIuVLq#tAW`@v6;-_88^r7|-YvFhT0d0T%CpZs+CV(Z3g)DjsUE`r8RiI-jprY@Yg1K=Xx z-dxMYi_R@CM~UKt{Sb)bj31m&5eiyyYm;0X%J2r_A!z8fRwGG1ah||YbDCOw@^{ak zySkC;#e9)d$Zib`TyTdVBpfInSJ^33RalT1m9Zm*cjspVtCIS_dIh|sP1V(+^UKSM zjAYSqGjRqT{5U8w*b|Q?+gg~t5hb!pcb00LaQDmRS@DPWlAq4>(XTdYm8FBmPO~rw ze1w!#>5jJ?lVxjDXf!$Ij*|2`n1n+vE#-W^JWnJM!g_Y2cuATV(=U5}OnB3u5GG)o zlghn5WAePmrdngHaFGS>5?gGjcH$(rqvkZ$2|C@nA7luzxVci^bC9#uCQCoR4*{;| z1Wq`8P!am_1+PD){%dp%krSgnc<o@?|Ce z3^TI?LqedaIk|l>7{sd}R34C3Rm(Zl<3ldXW79x7;M$^G3g@OmL*QC&$!z`n=)q2P zRJL^Y?x-x~(z5^yS&ns$S_Ka+IA0OhpX67S&$P`gANd{~d-9Zc5q#8LWWm5R=@}== zAzhPM{)#j-hgkoWO{U?CSiGE`HkP=nG5wSurr?V;g7mGHFDG`-=zJ`sX$&=!@&(#E z#9wK00}FL}?ZH#Zyh%}I+_H4NK5Y2WHP`f`KOd9&zRAUD47bs2IS+d_80~ghc2k%< z8Rao)@arfO^r1EkWG;=ZhT*CLELq+w5RzZdK-z z^A}sv?E@fM_*H&!f2kQb55rIYU0kHQue2>$n^ue0dGqe;U(^UDyY~zaG`zp@U z)wRNx?qlc2x+bI>CM{l%&tNh*(M|xF)y)-=z6ZW? z{obMkJ{1pUPfM{qIQq_33)v^3JK<^D8b1aDD<~+iKtz2}+Wkj5=fCaCmnsmsJtBzb z?yx;un$+%9Z7;~DXQ9E~ff(}b^_7(Sa?9JPsb>rG^C|{3b&ZXLn(^=A`TjFcs~*@F z!YwH31d8hSr)}uknUTO$4*fL9;ipKgf(?HS+c}=G5NDn8TyKukqP*1{}0< zbK-P})Pnx=qlC27j49R5$R#R+dy5oHiCzoreF(i1P7aR!6)+?!CEB@;Vb!~u^T3ajr>->r$_1?21u$T<%s2jw* z=ND8(Wl>JU{j{R3=8ZUWMfW0N17z+f1FvKyQ5iPG`3HqJ#nBT@oY{IfZQ|oXYPBm_ z%{HC*d$QVp5*8x@xSyGr>s7pDpC^b=$x+qXKn56V*C zE_;gJXG=32xS^;oqhKY5*;^$?zh`+rcy3y2Vdy2}2p>Ag?XrO=FE+IPJDaI?UmT+B zEy5F&%`o}8(i)ToS>1hpE2ns|iUa8YRmGj(UsphfMz?9&N(B6sn1UcjBH(GXaTvJ$ z6%h*G)pcaD3$QPXGVp%Xg=^%oeP549o&)z&e>ox{f%We-f9m@q3iF_#Wi~nrzVU z{_ht96$s19*+C3)iZf^rV%YXnF}fvnjLTt#X*l2g%|**vp{V#m!|y&#QiyV!&?Cgn z=Jq>nVpy3i^c6wa_RZeB_`>4it}9z&pZD(tNvfs6Aie81xtuwAI!-wUL`T>UVmU=o zBwr+6RcOai;ipwgTAP3v#}_j!`;v(RlOt8y--DK4o}0ROYz`tH37&C8S6FU9V3xjN zKAeM&KmUpGd60g`PPHaWEgqlI*SDU}p0=GI4mFvhg=|BJoQi#sb zPrwKYHI1Cydo5c_T5b<3+Y<;GRvq}3z`pf*U|ym(#iLiex+Jaq*GU4ngxxu0JQ!h{ z0uooZmoKM!d+{N8rToBC19)Hn{4rps3QoxpKAfDO*%D>pUThx@>J1yswAkQfs8n90<9aRm?7ltGg zv_o)>yC>_{fq_usZo(^YV-()}lcC+Gj-ox-wu?6acW9yEX+aZ0*N!TuO0S_ux2^Hr zRJ9!Yi4|XwlazRn+ip#bczyw<_c7aW{Cs(+DC;tl$YMQELqtzFaqaPsbgNg@47=i0 z6;QMbDYWnkro88AgcVALrWkHyYcDJf{=C-=dZHa&YB3r@tKP#OL`%Ok`aoQ&o&Ks* zdiRZwa}KBCx9VlZvue%-T5Irj)Dv0FulPU`9wi%zP?!5q=%k$wKYB`extYG1%PsvD z?xLbr_2=aD>va#aaQKF6@JZGRF2$(;Be)XF*Jn_GESv;RCvYDDT6o+^Le<^&y)_Ct zvD8+<UUAG%f!ke4%%2#gyvty+q^qPJia1(kDl#JOxX{-avdf(c zJn+hS0PtC%pX)2IQ%RpBtoiFOsBx|3Ax71c@m;>R&8W|1MTA+)(<(!RGcsuM3d()q zZ^QxZc~n-TS>}>;_{UDK_k3@WBkb}5G|iH`x&iSpf8}6(zSdDjLE+Qv^AHvv3tPq9 zDKr|mlP7?t0#qOLrzA=NwKOt!fWWQF5d9S9-_hoez)L{Ed5ASK9`iu8O zkf-0@J~i*I7_7-Ai~cT}nrHP^qs650mQS?e+n8+GBYe6Yo5Ve$mc3?AYvtzsoqDPk#5$NL}YL&mLK@hCbN!4iJ`H4{fS23kccYVDwcx^@7t1_YukD zP;bu5nh*u}35{*yrWii%>bqmuXh6%KQmdKh$#9eT`9ydF?3rM~-$MQ!_0?5ZtAHvW zKtTcOe!-h|yHd}VqBLCTW`{8B3ns{vE2fJEvjbrFt3}`T}q-9YP zcZ_$iU;p#>OeiR`;6u*QQV4z9@y2p_O@jAyQd_N^%D29``HIApfguCQQ2=$Y6|)ei zzDxhJX{PEu)+>DD97ZFxsO{|JaaiDCQHBaUutFv(ls^GmCmvw=oL*`i4 zn|8L-zNslXM5H0>u(AP7&U~l2_F#jbx1hDP>=2xkzV_NWUid9Tt7T+uEzgp&%r+E% z8?Ll*J2lzvJIcwCJHk-gbUhiLhNu_&^XJcYIJsgTaN9wEb_5VGDlEPOI#j0>V#hnP zoOoBY%d#>p9aml8(SGeRKRB6bAHCo_bky?BowIh_ZrjDP7I9B9 zBW&!q8S2K!MLAbR9431D=jXMi^6`5lZZ2!t>KAhebdv>Ex)+HHQr14P11}F4;TkHq zu#CvA=73=Dm@kbl=K;_Q<+eLRvu(Gv_xPm3C|d@PbSL2^sJoC#}=H_&9+gJWtXJVa3!8kQB3Xl)N;;J{g6C#0llxo=YJi+GS#O+-sI~ zG=ueh*|NEs4C>2jcjJJiP|^&##6+t9{ub(o$1%C2}Y zWjns3!hNg47bPpngJe4L7aV?3Vu8xy__4Sc6lU-57^BhEmJ%#j>y6E$RYLZS9imfv z++x3lYp7MCAR5S`!HCKB7-e)w^#LKW(MGa#IgEKWi%w#Uv+8otPWoQ!`3o`AX?w=7 z==tIMRmaN!Q>l(`lv7GwhBMEl+V&vmqcWa=l~hLELll+^OLpy@R(c#OP&(99_$>$J zh_LQqd@`!$M_1J}EFRIpO-`6`nZ0@wa3+8;FYUKNL6yI-zCa;~NE%nR&10WdIHUH% zk*})UmS&QZi&uQ=1zg=CGheGk+}C?&Jsjv5LF13bztNon-fXW5h+&ZLyWFP^RL`EC+*ycyq{%{v!mFae{;ow$^VC)v zJK50bZa5;S^WNR1-*g!jJQytD!b+~D%37)NzZJ)`wdV#;Z3ibb?)%badPdAjOH237 z2Ys9>-)JO~+%M#Dp#DCnxL(ARmSp{*Ti>Jn4&&|*(p`2 z=16y$FJjiRdKATaN-XWF5k{C&Uc_3}FDK#Tfh3Da`_>$5FRzml!@<9+Mh9T&aG+qE zQl|e9{Tr&(_Wrtr7H?jU(5d zLxR?)cx~3$i1Hq5XQIPu_J{YSDt6E^o!C@b&bgp=bQV&0+M`bR0hwC&=p`1#DR8S` zZCS12L5fm_ATBIrIGl@vA<`%xcaLv%>xS5Lh=1=E7#rWkty$!VuLJoWGRe#WR{2m}1`u?)9Cyk{oVj?hYL9?+V^MhcupEb(}Hx z*dSj~2h^gar5*lzHZo4??Q*2&MMBSpcm(7)@)@4s)fppxPbO%yP+3YyNlD$gvsdbT z7o2GVZrCB9sqNkE8<3*zKvcWoC@QyQZT;eaJB0pR^W`<@m`XxQZu8I_juaYgEX=IB z>*~&9)n&}(khv`Bmx?MnFN)JWsj-Hwb_TRq%aKpY>ZQ;N3VaJ78y+tTY8zkC#jrj{ zpF!CuZ}fsu+C%b4BS97Fy*rsU+m5o^!BV;!G3PEbEt$jhpFf}6Y!w?h9^QFAG_d!B zdE#?VzL=WaJF%KGT;*+Sm?KlqI|Xd>h|_TU~*uO2sgaq$0h~i4DkuMc;=j+rfQrl2(o{wXKMAm zI%Q#8uTTh4WYNS1jKv56lPKjm#^eF)lSUY$&J!z3+C@-b6-W8RF&J-(M|x-$__dL0 ze{LRLorh@;GTH_&Hn1Ydars54Y)O#zgdj=pY8(GQ=kVmT!V?xg*84-l{83ZJ%G%y# z%-_D4Ia=ZzE?>Qwg7uV(WDHxQREF2_DLtT2V%hzer?Bb*$DT1;nH@D(#wX_*B| zJkGNH&GmBEefwq4_HrOv*J_qsg{5NpJS42y{~1e=Jd z&V8@`escLrgh1e+HRSqimQCHaX0TjN8z0UR#cfqXB!sO`SvUIN>6AV8p%gZumYte7ieTWS`;o7{I(mZ~bYB=Mp{J3ZPm zmQfM8q6ObuYu#j+ZK`4V<%!%5-5bM?tzaeU)HPb4`n$+!i5EEbs+!(Klk2^VzU`mj z8r(kDc6Ap*k&J!J$#av%h9-A!&ghFog^!h$!Z)ny2P+`iK_79Z27R%pO=u!tW=0Lw zoVqP?S|Vt6r9X&1)~VggUT<(yq4_n+WUIkfWTU}r#a)zXL1JO?jX;JF??lAl&$GV1 zT2dF5`08|BmU6eBeQ4fTn^1^OPHfRpFGtQNLM()_4~nd+`&=WQ7e`o-?u`*N;!S&3 zXD4&#^C8cDW`jD9yrBvNZ837k#1N%2Hv@O@Gws#xfQx@re60nVoSI{O>($;~yw1?HdIo+O^pD7rJs1jz>7%?RL?HB%*oQ#jo z{9=(qP@=%%2v~8}(+ES}`ghJKvHPg{gxY|vPa6}WwJw=F)u}x;AH;DXc@1uwd_8W9 zL~#f-ypGYv!9LzVi_aeA17hATs4cLY=!h(r<`kDl)t@XA$COM>HDFr9(GfjeomX^g z+$+67a5pM{VV(Aw-L_Ij{nO7 zXz_82M+{(6l$u0Hk;*zQZrt&49l$jLAS8n|b-0ads|!i3O_G{V>cz7xpS}^u z-ATr*AKB>B-0kZV7yRwA6D$XN-Ts4_zvF&lhpZcM+S2cvBx@8Sm}1o3b#!#BYj4jL zB&}W-M{s}VA4*Eks6$tw4J~GO_P1Np#x)#hW^eS^az@a9C&CFn2dQ%;XoNQLfSv(W zEz#O4W4uSst3yuTnS*n3p!??8WdMy*g+$1Qr0jbdn%j;U^Yw1KHZ$SVDvmwhaUn60 zBqeTULPJS!H9XgNqXN1+e+#?&($6DJvOjLqy@xpu?NZ|---{IZ-i$`fd;%Au7dsBb}&y$iP z^S_PcDe0UOn!CyB6q%-EdJS%G~vfl@`r^yvip&Q(*B&L8)$J z1BR9PxmhqB^)SI;DAZjs*UMs-6*7kL0x=XzLAsQHnRM9;unc~x_w8kvgkLeBxjTLe zR?kP)NY?Df^f&2iYpbd-g+2}#^MUZ=*}Bqv4*zb>gG({y*O<{?J)B+G{uUp$C`~5<)^H zjEpq5P}Adm|34q2*@9nv8xy3bEfkd9BpF-@KAphI?CmkFs@i}cmEbNOpmP=?u&1?$ zeMvQ+lB}<s9!kg)RRLc>`v z!^`-i5bSzA4`LCej?YuIO9m&&PTMnZ&8=IZ|$8?jl4ucL&uY1 z<)9u%Uw<|jZRC8>@(7|Dm41uR6%Y0H8`*EzYsDlgDgI6JQu$}^z}v$><^tAWM}Eu+ zI6mc>nF>su?=!<>G5XM%9kAY0qUY%S)tjm4xUHZx>ZL(Q;pHIu&i;UdC2zSiAB*Ou z3Tm(bY-W442@dh91h8TCg1g!d6Qr`bV>_rK@cg(_yZMpLo8=&v_*wMdJ1{czU4|*G z^{B@XJw+uk$mnmflua6&r&l!$lRgV`Pew{y7mM5LB|H3Gs|25uJE49HRJI)B3b4~) z33DSbfCkP_s}4f6y$wQ}{)|)(_k`pA-8%FWZqH+Y`)y+ zi=t9u2ms6dZZukq6w?|0Sw>Mxr1))AKR#MWj-6ZCUgu6Uph=&J zvt{Y9i%0%!`8czqW*aX{PXyM~vQkfQ13{6b3$&3yTKHyfFaGMf?JN4PWhDm8aQiE; z)_RS~WBz%|*&nn%7oQ>c&o)9dK)+cG&zX`GUwe25-W z>=$2yK*l^v(VgKT^rn4@K`LH4quxj6^;N6b`C)L3A=?VDF;SG#7@#+CR zg0zePb2>^@Je|fs2B&O}v^bh{*=#*;o89HEA|#nOu@YGY{VNFK{NvTNB5*wN4;hYN zyediu^*%rI9KHPF3J69o7t!(<1;}gzN+`Bq>Q-NoE&4^dSkPR_gRSfnw_SZ2k^?M0 zSvJ#YPHZNM8whh!sr=i%?a{l%n#tcjCS`3((^4ES3)t>iiF`Ty*~59GsPd|EH_-QF zYs67Dd2pnVifU5p#kgFDU~Jgim;_N!657184xH2f_U3De>&q-F`%W&IZMg8>2aBp3 zZ1{Dk9Uu(}^EO0lh{lHZ9hKxSwWeSe(7X8^q1YduOfj_hd&CwN5`KJ`Wvh7KU+D>? z%o(@*L~=3GdZ{Jg#I{>Zl(L+wvC)4bF)6YZ`KF!o2*dCU*+Ay%kbU8hpJRb_754=B3XJOc6C2I@)$7Nonwe@ zu*ZEa{7AKnw_yH9>HFq+lhVhpf?p0?r!<@&%N?&UK!oZ3{I$}ba7EixA42FfT@%IY zWX+N`UbYW@sd39Gh!OH?cDy||e3ynm0^8PWd}T1f-m$;$zVOD=GpR!vp|zi`c*3@M z-@^`;FFVB6`hQ1#`=ccyPcHP>{!2jMg#=D%P+ox z@BuJRaz10q5r$OChr|L723W<=Yod9<%!rWXD=(|fRt&Jr$-0);7lQ``bJCbO;$RM? z4LX>I?}DF7OTl41QQTpfVcj+`F%hbnT+3pr$;g>R7+Q{IfZHmB;crtkAtkMN`Zo}U z{66nf&o0acy|4t>waw2hZp|HiDIke`ab;laAfxWE4eP{wxnl{4qOrt`X}=GU@!GTd zxcgEl%PAJPSZtYgX_LeL>mGZ6foHEpKVRRAf#mz3zdFzhT%0)jXMqYCI#u=cJ&Mx= zjt399xIn$;iEhU`<=?OUUfg~s*L&k8C9X~p%h>BbjOp++{Ghx6nCMPq1p&AfCr=Ge zUQu-XOpso_;F_)U93T9yYB@@>VBQQTC9tRT?HRt=*ob&i_yJ(~?I4&1eIR(ubG{2~ zQ-1)U8%zj-ojI5@6&3H0$KaM{54~8sO7|_SZ%Bc<**nnQ8uzHcr=}SQiXaEH*o?$8 z@3}MN2>1z`#V2wiX;C%hlI|8Gd*p;`Q80k*NxM zz50p4;;B~2wA6!rVfH?_3tP=6%NH==C(I!7dgIt8!~rJQTVR3|2fo?lE)V*Cy?>h# z(ceSpbx4yGFcfkmtausg6>1SFf_KO&fbN9Xp)eJAvHqW#czbU%*ZVX_gQW z)C+dH`Z(D3#XYpGyUNaiA|n9{;g9+etayN!+1cTf$56p~nw5Y6M_bc5LK=*+EJxzA zEJwmG6jRqfxY@Yr_cG|^bAD#N2Tm-5p~RFrymchc)&h;6*)n%`?>}Tmm-bh=N*wWw zQ$5w{Qkd_}-ynYvoq~0icSrCf_uTY8CQoE#0g&P0+Ku>j-~k$oFz9)9k3c|}BM)Kh z;IWN>8qkzP!F(UJq^wV3qovy~pITyi;L_lK)b4+TjfR6Q6(NB<9ACUv^fB`a%j2+q zPA+Sit>cnJMY-#z}#P`pyK4x6^_CBWiQE1FfQnucmdIn~US}ha*Uq{y+ z5B2}Y&&n1;LPV0i_bwwlyL0wFE+Zo&M9BP-y|NSGIOD8{O9*9z6SB!TviJJE{raPa z9`)!xpZDi;@7MG7dOt@+`7R)K2DVvwddZdo+V1t^(ECmIcvyvb9CvpIC};%580I z$b)>DTl{jchr)eOtjy5vBOQAu2JcT9zIdpN&;u`@Zt&nvhKry0tPI?}+#h)=!bl;^ zLe)IGRfkSr>`K?mtK|3hjwRjs)LsKlnx5n%MG+N^D(Vwx>i}3Wt&Lukp2~Cr&-*Ap+-(! z@5*3)owr`lWB=VH&12dZBsN`(U-wSlw;er?kKv5{5E1D&Z~}z6{k1-+65IDUauhy_ z_TgLL8J~+eZB3?205zfN5(9{I^c*ll9>|~oJuc$d*h*a-8Sl0n5ZwyRobt6CN_lw8 zpDm=0d*(ABGVPUmZK&2IdvlBkjYuYug0K2$Wo*$ZN`U_WsVyUjzKOdw!LdH+^l*3I zg^Sx=JL-52>^zqYkN&|@W#?}0!u?)weQ9n0s_lgUM@LH@ENnuj7`Y`IfzC|Q_<-1k z;P{4Me0!?`x51%?%@tVoN6^*+Abo1aU-~O>>pyE#d|eZjNL$USd!ecKzw=Gf*Tzvo zfFb;cEdd)eV4HU{&P+_qH@J=1k3uFGJClNIQZ*A^gFREm91*M&{Ze;?Ut69(Dy>V1 zx$tJIE)!##6G_XqE|`L4q>xdi_Ti`A=5biOAX9$4j=fi_*QVNT$9737{}#w+aeoZ^mu&CyJp zI~wf4U;sqgG%4wy2-szw;8UY@5Z>Omz*~{?(H0>fw*rtAXn?-VZW)b;TSay=e^viu zSEEnMYKV>DW+14C!l*t(v0J@z+ut58-Vsbn=!wN+)Zt3Oi}GY-|0EALL_Cn{cE-L~XIW30JNcXA zHNh_dM4az}^WEuY?}sNSaFO7GN?jamiNH76vEd`XHb8@JQ!gK5{GXVx1gVnIRAx?u z><>?j0|b%K_lw=X+zH~e^FeATy8!*x(6XbiM~%JEd*Jm!h4qCF#q9r7_wA5| zNP=`^Dj6N=_u4;uCt@MceaAJp_4lhplKn{U*0a@NTc3ufb}?*;jk6(VJ>c?aPn{`r zAAeaAxRoBs7!sF9ZstUP7)bg@Q1GFpQjwX9-K6yCbBjes{F_udt(LxN8Yko-q5P2 zpOd#Cg}b@AT|7r1aK?`nIYfalj_hBwZQ*EBa@?mX9Now0p}g7oWoZo!5G_tv8QP(r zuk<p&+VZwqxvf0rdQf>M1xuEjIp%t>Q*N30WeTC(P&7Zs&A`E70<>wU7H>sf4l2eb-Tj?V?mr>o(06%tU<3_17tU4>H#ZDp zXYnxiNlhg>#?shi`B^9Cy<_i>^jIV&PcgSuhDjhnQqohE(C&taoZaCE-$V{*5|(L) zcQg8H2W=&F%BTCD9Wp&0tvlWM_fn03v;TNX8(vn)BZPoWT1JZI?4$UiR#EU`CJP<_ z_wClx^@{WPD7KIGC*VQgcYqNUwb-W(YZLl@%6kK>r{BBfIXpoZ8ZSZ@mfu(V0U8${ z{kYBlcFLznJf5-h^V0*&Ug^_^uh`ej!ZmCa^C|NHnXU!!<}x$Te~d&_160%9JJ~6t z32LDS+V2|*} zk82;#tI8^z%)mGmZgems=S@WT?J$#ps@5aWV0$2!Yx&hyKBrz&(GrrPL}?kA?1L!G zG!V!LM+se^>Y{$BM%#TnWE^_#CEJ!lad&H#DfFco;><#+_bn>a$1qNkBN2Ld=ZBGJ zy!2$!PwGB3PP@-bhp)_fm#qDKJ*eKw@sU|GvsY|5YO9C!(x? zBy}(L41Q5oReDD)N={m`XJD7O`&r*-j9eHN_lhm#xJ+K&ho*91t_`Stwj=X5Ha8WX z+|D#SH5x!Uu-RIj=!mxT&J1+P+Buq?o&@D-(O5dY^>uNhY>vLz^mr|YY(K!70~3@V z;C%rl5?oi-+QKkA)v4K3bwyR_oA+|vUlLx)5bpk8$8fRjVOM2U0phbqxHgZNvZTnY zkwt5?^a{*LFYf4zBRFcz0fWjgmnS}n3Irq0y5hVu%Tb;4&D|6gHbfB=VIVqY8;I6fQCMH zJb8t?IgEry?d}l14N%WqTq#Ij>Q??|VpB7;+k zF(hbzOzCQGWmLR}rqb#6pFct8i1tIomMy6v=`mKAKPNmLHEA&tD@D20mO;3`FPG}L z=BR%9ptI$h%8%l$*1dtqhnRr@DuNEcponlJg?I5Bdl%5VQNFB!l1$Yqa)AVn=L7XR zsNhBjZE)%l(d|<`oGP3iCzgOkM zVaRSuRQpLdTdV%wiAbeF1lpPLKlS`etj7iR2=%t6_rB;of`Rej+o)uQEUfqR=?U;c z6;r`n10*o#i2}5-Ymb4(nLs`PFZr$6_C#hW_wBKYXD47-Ryx8%&;fXkTbr9DS*Z8m zfoQ_HDOA6z*g|W6+r)|mz>nqH_ad*FXG=#q>K`Y5ZE!nKx>|XCXf41HXBgV$XOu2W z(a(5=J^FGqJRWj7@?uy#O875PybJ~iBNR_IXD>ImcuPJcK7&O(y07Q8)bSurSTKRf zp32G~T>ja-N=Q`Zc!0|t?4)~RhV8o__F|=P>HM~49zcFiO%-383jl#nNw?gXukPXw z&?Kwvomd>41p~Lz@^USpm5mpfgjK{9={uUO7jZ8&n*v)zH2AF~^<|++8u)>X>Yu!a zp3I7|0bTPrcURRf1tJa0&^^wH)QoO3e6gsvw7FcgKmNFIV%V)w1Tc}-16s0wTU({s zx<^~HJwb{q*Q>{NwFBfd-E2YIcz7KkN7S+{Fbx8N1%@By2Ni*BneqmK*MMdh(Q@0f z&Ut+z9{shJDta0hj$eQ+dVkbgXelW=7N`L6=+q48v_2UkEa6B{EN@bQvaEF3GkeB{ zcCt*5udUQiS9tD`sCW5q_G0tuU=p1h-sC~^o;LEY4O!bsN(hmImxQKG$l zY_CHRz_wud!To_r;u!A07!VE1d4Y_GH}+Fsi)Dz!YMEXaWxi9F5Fw zt+2#Qh%ocl!DMWZkPLIYYC;=FvyUtJM5ZA!g&hMjAqQj6+9KZII@SR(l>!15{V4=Q z63yZv0p^(e^XCKLs{@yyrM723Dgo`!=^rFrp0~uuEWLaj=YntpW_N=9qG1d)KwOD{ z+yyRZMdv$-uTRlRr^QNVQH3fDYD4Av<0@BOLSGJhkswMCaQja!KGiBJNJxQB!0K#& zWRZEe;Re#2TSDD(WCIx8J!?NI5eiif;L{G0yrFCEP$!L<4C2*}1?ec`-JVFc|-$kkwfZFzaoj(=reEhnCUt(n^t zR|oC3p%xEbl~pwei8M4ZDe>hnbTmswn*sqAO;c#)bX$Du%ODC$=swjaI`&>*g>*`y z2M#4#cU;zany4x>vvR#d{QSn*SXe9=$r5_(Z-jaTEi#|yZUvQk2Pp}`@$>=t)jx}} z0B}lDkE{;PE(k3yLX=KMqcxtHl%yG|nWbk(#{#GZ0YBlJMq*na`XSqx9_!0urPCQD zaiITN#{kB0Hh1{pyWRysS*f%sCeUEU!mtd0b==BP)7drz@C}od&MfudK z_=Fb9&TXzl(#h-N1Ke&CJ{WK~zxPH=m{gB@gM*L0`Sx-01<;KAit@!A|MP2)tA2ff z0|Sw5z>X;01l&Ml6MBOFWF8VfsIf^Tpjm%(@mI{Z7ia1^+%uJ3ErUlG^d2e?ME}-BjRAC#2*OiXlc=E*Jt33%Ao9=tcWV4 zX8gnPIH%VWuZ|PP-_TQgi4oOPQBgsaajUD*)mCj_s(k;syZcv}o{=>=NYgkj0Ma|b zwnsDKJ@9@_S@<=RJ&?z)NHp?~$Cj6Gmk70;ERI{ZpCn$cDP1Vd1RrnD;kq+JP^~{P z76fG&i*B#J!eeX8imv~dtIwA`F8_L5&VfeX8QQW)S~ws(TI*p`zOGy-hZaUAxMuoLtK6g<*pBAS!gQr*buCrBjKvYQF)}sf<#bdq#3zco z^s@KmZ11qk>uA>0{P<6(-z(P>him!wLpEUR!c!XL6}p?&qi@01<2JE zLasR>mwnaeeGfq&Pea)n%{6u`RNrVNOqU4#skhm^3qIsd33a)U3F!JyB1Yn?b3H%+D@+;8H`g-Qwvx>J%;QYW)Sl{3Ld(*u?x#z8I+2z9Hb>LX(zZ13&um1{l0 z&m5_fwxgM1P)r`geFyO8gTQZ3>4>8|fLS5iSZq3RIzL~wB26NF_D4<1N11m-bpEmM z<&0%imut=Voxf^7dUcgyxc>Bkg4Vagv3>6LO7?t~qdT42J&}*oNLSi23`NsBm)<%s zEu}}Lj%Z?xUG3?|MukM$vowe|!Q9_+b?bZa@anHgCP4ukQtjTX^lO>J9zdi#f$(E8iMbI3p{A8phFVcVZ$7h)JlQN%gn{N#yF80Mid%W3G2( z*_`<+S1wx{=_3s-!sqT6kjEqx2`RFRSx&OLH{W;8D0^^>ryK@)-D)O1`;eF(w*MWsPmr;p7&~oP-^}!BwiUNl(JcNBIH9AEzYAB=m~%{@^E!kV`%m{ zsXz*+<~z=q-aUB~c2cj@{&;AJF}#SIMEUY1o!Y@;C#7Poy7I>)u>W#o#q<$>zhC@5 zX1RljwY+7EJQxr6g1WyQdQyogx72ZrbOSzP%>wu^1G>qRBXK0d=$g;IU5#)zV>`FN zme1#lhZp_7o~?Dl9?pcNeu)OdpF_}Zq8ws5Js6}EoXnj5??0Ww8N(C%^;L8c_pgkM z46X!w-QZH==DF~N96|GAt?WBwZ4lR0ebZP$UzEn%SrL0HZ#kI^5~8k`ig*)n zlO^=eg>F{_U#3)oWS!`$p&I_l>iGs+4&_qkqnm;er! z)JGr#%2r4=pfh2``N)=!td&V8q=~3!%#Im$GmdN>9h{srzhDbwvdhfPy=(8c0Fcl{w%c11qu zds+QOweRQTbSQnO1<&&QQaQQ&hgA`bPOF2hXAT<`Z6#bv3HZh$|6g**H2= zq`fS-dG1`v0F{tXGXoVWT?G`2)f@F7nDi#@T=P;}Er~(RJRlDzKj>RJG7LZwWmIQi zY6<-WwQ>B5h*-EO>hB2+fSoRFW#ApnwJe%t#+f*3p-_HzU*8WsS>-a`3MXU_HG1mz1pzo=vUQ}5NGo;J?is7Z` z{K|MAjr}6u$7lpc1|NjQ6?rYY%2M(r)>f@rRPxfcvZqRRYN5Dm2BN}^gZ_mC)yE|S z2hd%VZR7SOoUw-@|H z+_VOCF%!?ZERv@^9#Kd0_uq@4?q4<}d~^@s!Sw!;Rj^vUI@;~t!^91UCq1zzLy8UBM*;cQ&A@HX9&!xRElHJ1WpcU}jc~`prOPVKKSUJSpJG6a zh`83y2^N{6rAZ4BA%dCP=6XLNPxMfRcM8 z-|T9r&C=)B*R?*lkU^k=8JCiuz%8EyQ9(3DMh8~BKe)I$*WHn>1cf2^=-*xGO6f8H z3O9ixn!mAcP*A)t3g5h?KQ{;XKbD8b75D}Q@`jwQJ%r#2OVCunF7ckdOXC#1S$crC zz@s=`%1U%0S`t_^qp*QCH1%*UlohXIJZXO)t939nWP91*-+jYR(sSLz1s2);$(qkT z>lM8mOTrAYh_F*GZ_GUtG7vz$msB(B2!vmN zIHrQ#yO07nIfMi{vStjP`L(habYLfNa&Eltqk+9}{(cB#Ab$zi)_NpJF;eiOVw;Zh ziar5k4hH&Kh|G^=WTAu4@F0zX;*@xeVJ0I1gzoCFYU_3eh$nM>GMLz=6SY_bnQ&@8 zZg}@jg)g?KL(Q;o??nJSiNx6WP~|ESPkV@;-6BFQpN7@emfrEsr1^NimX=EyK{$_j zw?CAotjtFXp^F-sb{~IYO32SsGAQvkz@1eg(5G$lw8x9 zVAy!SWEAvld?h*tbTA%Akc2X;&GR32vNiY&`)^}|5m@{q-SSr6KE%3^Kr1NgaW)4) z?fB|8bZqdxzT2|5zdxK!F$j(~2afZ=dC>UfDXzNUAC)a*HaPYNCpH^i&Gj!>jEa>$ zTL^w4BH#M^DU!N{n{0Z`htAh8wBI>tj&^%slm|n*1w-^H&_iOmW4*E!2;VVdb;B<{ zdj<&eL~zFzZI?*G5c$vj>M*fhroPbTAQK`}!IoVW|CzWAgb_LaL&d#XDi|n+=~H*P zX_Rt)ZurdxJ7@HlNF_ODYv7gDreO3OnHbvB~#^?~MuLTc6K%Ci7y=PRmIZ9etE5n{aVge&yM}u%H-M z@cLU85~U6^+|kMDP}_vAZvzS20VGFF^7JpOM|G}r^isiUP z7zViZieCY4xUE)*$L)Fv&gzd^!vg9e5K)!r^cO6*VF-`oh&@*sgi2v5Z#_2oIXMiW zd8Sutr0=jiw}c?bdX)O3tz~L+9o#PtWvG<}07JY;h>S#6i{qb{3SW%Tnnw~aDQ}X> zr`-e<5%N6!qIeJ?>o0VWZD3&uSvG_wvMJpFjzaH`ul81li{%iFuWno$5%oDUb4tk zQO_e{vvg9P5+T)6JD-wNA&t`4exIlNct=3g$)hyBlY0q(B^|t$mP(6-S%OIugA_~VSH=)@T3cwb}Vp@OuF^Lp;?`@aJR73p>J36)Wf-dr5 zONF9pdFQ)gK@wuV{X+v7cb$oZmf5Z9NczO8-#S$-FGSn%(H$*nIR!+|iK|MV<}Y-` z-A+4Ohb)#qvG*M-{ANN-hq`03X zcMH>W*hhVL#qQ#s5SesdS)N&~ttm3pFGwHG#TIF3@Pq?3aBL9?Sfm#$#G~x@b&Y76 zx?FSf`#a00HXo;synZwJ+w+CZaDc5r+HK-A0R@ULT<yd;0a9GI z3t&e7<85{TJ(Y(R=x>1aF}BpFQv+NIHEO~l?$~=I_!ub$%SR;a!1QcazWv3YY4=7u ztYheIK~Wz*ei|AJ+_JYy%d|d^Wix8R#w-~7d#W6(Q^ zlN54q&>KkaczY%{Uv7fnsg~&~OTml+D-+FRe~lM5ckIu%6Q7vuLxV|3=DO|JB*eC! zO?;~+!V<%C3hgX$_teWfpU16C?OXV@G+l9iV6s989OG7^TQaJ`vEWrr1l~W#^YbSs z0gln%i;=<1kJW(z~YXEJ(kQeO#ZuOzCh@9xsJJa_p+Qr_04 zFeOgtQ{7H#s!U#K%BDTVnRP{utm8P(2+Y?Y54D!7osl5mwgvELT@F5qk;u~Nn7WKt zzkgHM#w405Rc-YE-=_c;q1Q-i44kKjU{fG-h(~&*An@W;!r$(0$0Syf#k6vLE~P2t zssUzmq1{rUj^7O#1mN0#;Yts0vTsm2gopgs$HLiH`;j4?r*R}1R)d& zL=MA^WxjxCI4eL9x>Ykv$WsuXCmBZ8@n2fKR0kOxtc}rd`eljG`d7C;Vs~Lx&TV8w35=%9DXPO(A|qW*d9fu|28Hj?ts~ed|4>1p>lJ` zayaqLjr`Y_F#sTFkCCz%LnI}2o|mnh7gb#~xPnhg8Fouy#Ay|gz#vS;0yacdfY|xM z(-T4^1i2TA2Z>@F2PTOS2$-t4ruonO&4Q>U(M8d{4!Tzk)vxstcfCXxD>@G9uFoQ=}p@`ug9EITv3M5L?smxs4C6LclLgH9gg; I$2M>N2cEA882|tP literal 0 HcmV?d00001 diff --git a/docs/images/logo.svg b/docs/images/logo.svg new file mode 100644 index 000000000..2151002a4 --- /dev/null +++ b/docs/images/logo.svg @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + φ + 0 + + + + From 3eded7199560014ef75bd14c07d4715a33f04d58 Mon Sep 17 00:00:00 2001 From: David Ham Date: Thu, 19 Jun 2014 15:23:11 +0100 Subject: [PATCH 003/749] Flatten the package a bit. Now that we don't have ambitions to build a compiler, element is all of finat. --- finat/__init__.py | 5 ++++- finat/{element => }/derivatives.py | 0 finat/element/__init__.py | 4 ---- finat/{element => }/finiteelementbase.py | 0 finat/{element => }/indices.py | 0 finat/{element => }/lagrange.py | 2 +- finat/{element => }/utils.py | 0 7 files changed, 5 insertions(+), 6 deletions(-) rename finat/{element => }/derivatives.py (100%) delete mode 100644 finat/element/__init__.py rename finat/{element => }/finiteelementbase.py (100%) rename finat/{element => }/indices.py (100%) rename finat/{element => }/lagrange.py (98%) rename finat/{element => }/utils.py (100%) diff --git a/finat/__init__.py b/finat/__init__.py index 95f6f869a..311300c61 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -1 +1,4 @@ -import element + +from lagrange import Lagrange +from finiteelementbase import PointSet +from utils import KernelData diff --git a/finat/element/derivatives.py b/finat/derivatives.py similarity index 100% rename from finat/element/derivatives.py rename to finat/derivatives.py diff --git a/finat/element/__init__.py b/finat/element/__init__.py deleted file mode 100644 index 311300c61..000000000 --- a/finat/element/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ - -from lagrange import Lagrange -from finiteelementbase import PointSet -from utils import KernelData diff --git a/finat/element/finiteelementbase.py b/finat/finiteelementbase.py similarity index 100% rename from finat/element/finiteelementbase.py rename to finat/finiteelementbase.py diff --git a/finat/element/indices.py b/finat/indices.py similarity index 100% rename from finat/element/indices.py rename to finat/indices.py diff --git a/finat/element/lagrange.py b/finat/lagrange.py similarity index 98% rename from finat/element/lagrange.py rename to finat/lagrange.py index 13ba84118..8759dbe4a 100644 --- a/finat/element/lagrange.py +++ b/finat/lagrange.py @@ -44,7 +44,7 @@ def _tabulate(self, points, derivative): if derivative is None: return self._fiat_element.tabulate(0, points.points)[ - tuple([0]*points.points.shape[1])] + tuple([0] * points.points.shape[1])] elif derivative is grad: tab = fiat_element.tabulate(1, points.points) diff --git a/finat/element/utils.py b/finat/utils.py similarity index 100% rename from finat/element/utils.py rename to finat/utils.py From d71cf7e56684708d98410a0418a40e988b4a67ed Mon Sep 17 00:00:00 2001 From: David Ham Date: Thu, 19 Jun 2014 17:39:42 +0100 Subject: [PATCH 004/749] plausibly working gradient tabulation --- finat/finiteelementbase.py | 9 ++++++++- finat/lagrange.py | 17 +++++++++-------- finat/utils.py | 6 ++++-- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 593417b7f..0ada5483c 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -38,9 +38,16 @@ def points(self): return self._points + def __getitem__(self, i): + if isinstance(i, int): + return PointSet([self.points[i]]) + else: + return PointSet(self.points[i]) + class Recipe(object): - """AST snippets and data corresponding to some form of finite element evaluation.""" + """AST snippets and data corresponding to some form of finite element + evaluation.""" def __init__(self, indices, instructions, depends): self._indices = indices self._instructions = instructions diff --git a/finat/lagrange.py b/finat/lagrange.py index 8759dbe4a..9beef14c6 100644 --- a/finat/lagrange.py +++ b/finat/lagrange.py @@ -46,22 +46,23 @@ def _tabulate(self, points, derivative): return self._fiat_element.tabulate(0, points.points)[ tuple([0] * points.points.shape[1])] elif derivative is grad: - tab = fiat_element.tabulate(1, points.points) + tab = self._fiat_element.tabulate(1, points.points) - indices = np.eye(points.points.shape[1], dtype=int) + ind = np.eye(points.points.shape[1], dtype=int) - return np.array([tab[tuple(i)] for i in indices]) + return np.array([tab[tuple(i)] for i in ind]) else: raise ValueError( "Lagrange elements do not have a %s operation") % derivative def _tabulation_variable(self, points, kernel_data, derivative): - # Produce the variable for the tabulation of the basis - # functions or their derivative. Also return the relevant indices. + '''Produce the variable for the tabulation of the basis + functions or their derivative. Also return the relevant indices. - # updates the requisite static data, which in this case - # is just the matrix. + updates the requisite static data, which in this case + is just the matrix. + ''' static_key = (id(self), id(points), id(derivative)) static_data = kernel_data.static @@ -87,7 +88,7 @@ def _tabulation_variable(self, points, kernel_data, derivative): return phi, ind def _weights_variable(self, weights, kernel_data): - # Produce a variable for the quadrature weights. + '''Produce a variable for the quadrature weights.''' static_key = (id(weights), ) static_data = kernel_data.static diff --git a/finat/utils.py b/finat/utils.py index 4c6593320..68494b959 100644 --- a/finat/utils.py +++ b/finat/utils.py @@ -1,3 +1,7 @@ +import pymbolic.primitives as p +from functools import wraps + + """ from http://stackoverflow.com/questions/2025562/inherit-docstrings-in-python-class-inheritance @@ -17,8 +21,6 @@ def foo(self): Now, Bar.foo.__doc__ == Bar().foo.__doc__ == Foo.foo.__doc__ == "Frobber" """ -import pymbolic.primitives as p -from functools import wraps class DocInherit(object): From 519bafac1fdd08220bb4a42949708bdd77499ad1 Mon Sep 17 00:00:00 2001 From: David Ham Date: Thu, 19 Jun 2014 17:44:33 +0100 Subject: [PATCH 005/749] A little logical refactoring --- finat/__init__.py | 2 +- finat/finiteelementbase.py | 69 -------------------------------------- finat/lagrange.py | 3 +- 3 files changed, 3 insertions(+), 71 deletions(-) diff --git a/finat/__init__.py b/finat/__init__.py index 311300c61..c4193395d 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -1,4 +1,4 @@ from lagrange import Lagrange -from finiteelementbase import PointSet +from points import PointSet from utils import KernelData diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 0ada5483c..785427ed9 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -1,77 +1,8 @@ -import numpy - - class UndefinedError(Exception): def __init__(self, *args, **kwargs): Exception.__init__(self, *args, **kwargs) -class PointSetBase(object): - """A way of specifying a known set of points, perhaps with some - (tensor) structure.""" - - def __init__(self): - pass - - @property - def points(self): - """Return a flattened numpy array of points. - - The array has shape (num_points, topological_dim). - """ - - raise NotImplementedError - - -class PointSet(PointSetBase): - """A basic pointset with no internal structure.""" - - def __init__(self, points): - self._points = numpy.array(points) - - @property - def points(self): - """Return a flattened numpy array of points. - - The array has shape (num_points, topological_dim). - """ - - return self._points - - def __getitem__(self, i): - if isinstance(i, int): - return PointSet([self.points[i]]) - else: - return PointSet(self.points[i]) - - -class Recipe(object): - """AST snippets and data corresponding to some form of finite element - evaluation.""" - def __init__(self, indices, instructions, depends): - self._indices = indices - self._instructions = instructions - self._depends = depends - - @property - def indices(self): - '''The free indices in this :class:`Recipe`.''' - - return self._indices - - @property - def instructions(self): - '''The actual instructions making up this :class:`Recipe`.''' - - return self._instructions - - @property - def depends(self): - '''The input fields of this :class:`Recipe`.''' - - return self._depends - - class FiniteElementBase(object): def __init__(self): diff --git a/finat/lagrange.py b/finat/lagrange.py index 9beef14c6..897a06e0a 100644 --- a/finat/lagrange.py +++ b/finat/lagrange.py @@ -1,5 +1,6 @@ import pymbolic.primitives as p -from finiteelementbase import FiniteElementBase, Recipe +from finiteelementbase import FiniteElementBase +from ast import Recipe from utils import doc_inherit, IndexSum import FIAT import indices From 48d07629cb8e808d64999d7e11ea36486b36703c Mon Sep 17 00:00:00 2001 From: David Ham Date: Thu, 19 Jun 2014 18:28:20 +0100 Subject: [PATCH 006/749] Now test with grad. --- finat/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/finat/__init__.py b/finat/__init__.py index c4193395d..79c367b97 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -2,3 +2,4 @@ from lagrange import Lagrange from points import PointSet from utils import KernelData +from derivatives import div, grad, curl From 8a371afbcf77b935d58cb1709277a76abf30d4f0 Mon Sep 17 00:00:00 2001 From: David Ham Date: Thu, 19 Jun 2014 18:38:41 +0100 Subject: [PATCH 007/749] missing dependencies --- finat/ast.py | 25 +++++++++++++++++++++++++ finat/points.py | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 finat/ast.py create mode 100644 finat/points.py diff --git a/finat/ast.py b/finat/ast.py new file mode 100644 index 000000000..3eac3701a --- /dev/null +++ b/finat/ast.py @@ -0,0 +1,25 @@ +class Recipe(object): + """AST snippets and data corresponding to some form of finite element + evaluation.""" + def __init__(self, indices, instructions, depends): + self._indices = indices + self._instructions = instructions + self._depends = depends + + @property + def indices(self): + '''The free indices in this :class:`Recipe`.''' + + return self._indices + + @property + def instructions(self): + '''The actual instructions making up this :class:`Recipe`.''' + + return self._instructions + + @property + def depends(self): + '''The input fields of this :class:`Recipe`.''' + + return self._depends diff --git a/finat/points.py b/finat/points.py new file mode 100644 index 000000000..6a303ba9c --- /dev/null +++ b/finat/points.py @@ -0,0 +1,40 @@ +import numpy + + +class PointSetBase(object): + """A way of specifying a known set of points, perhaps with some + (tensor) structure.""" + + def __init__(self): + pass + + @property + def points(self): + """Return a flattened numpy array of points. + + The array has shape (num_points, topological_dim). + """ + + raise NotImplementedError + + +class PointSet(PointSetBase): + """A basic pointset with no internal structure.""" + + def __init__(self, points): + self._points = numpy.array(points) + + @property + def points(self): + """Return a flattened numpy array of points. + + The array has shape (num_points, topological_dim). + """ + + return self._points + + def __getitem__(self, i): + if isinstance(i, int): + return PointSet([self.points[i]]) + else: + return PointSet(self.points[i]) From efe0ed2d76ef986081e7d1463d8080f9a967a1ca Mon Sep 17 00:00:00 2001 From: David A Ham Date: Thu, 7 Aug 2014 18:53:14 +0100 Subject: [PATCH 008/749] Sketch of pullback. Needs more thought --- finat/lagrange.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/finat/lagrange.py b/finat/lagrange.py index 897a06e0a..2f87706b0 100644 --- a/finat/lagrange.py +++ b/finat/lagrange.py @@ -143,8 +143,11 @@ def moment_evaluation(self, value, weights, points, q = ind[-1] if derivative is None: sum_ind = [q] - else: + elif derivative == grad: sum_ind = [ind[0], q] + else: + raise ValueError( + "Lagrange elements do not have a %s operation") % derivative i = ind[-2] @@ -153,3 +156,11 @@ def moment_evaluation(self, value, weights, points, depends = [value] return Recipe([i], instructions, depends) + + @doc_inherit + def pullback(self, phi, kernel_data, derivative=None): + + if derivative is None: + return phi + if derivative == grad: + return None # dot(Jinv, grad(phi)) From 9e1181a3609e15aad293edf7172f295a402b1009 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Thu, 7 Aug 2014 18:53:40 +0100 Subject: [PATCH 009/749] Beginnings of vector finite element. --- finat/vectorfiniteelement.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 finat/vectorfiniteelement.py diff --git a/finat/vectorfiniteelement.py b/finat/vectorfiniteelement.py new file mode 100644 index 000000000..e8914778f --- /dev/null +++ b/finat/vectorfiniteelement.py @@ -0,0 +1,27 @@ +from finiteelementbase import FiniteElementBase +from derivatives import div, grad, curl +from utils import doc_inherit, IndexSum +from ast import Recipe +import indices + +class VectorFiniteElement(FiniteElementBase): + def __init__(self, element, dimension): + super(VectorFiniteElement, self).__init__() + + self._cell = element._cell + self._degree = element._degree + + self._dimension = dimension + + self._base_element = element + + @doc_inherit + def basis_evaluation(self, points, kernel_data, derivative=None): + + # Produce the base scalar recipe + sr = self._base_element.basis_evaluation(points, kernel_data, derivative) + + # Additional dimension index along the vector dimension. + alpha = indices.DimensionIndex(points.points.shape[1]) + + return Recipe([alpha] + sr.indices, sr.instructions, sr.depends) From 48aff4b6f4405a9cf5f088b352703c1a7fd6f1b2 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Fri, 8 Aug 2014 16:38:00 +0100 Subject: [PATCH 010/749] Sketch implementation of VectorFunctionSpace Vector function spaces require much more symbolic manipulation, so this sketch implementation is useful in raising some of the issues we have. --- finat/ast.py | 48 ++++++++++++++++++++++++++ finat/indices.py | 9 +++++ finat/lagrange.py | 22 +++--------- finat/points.py | 17 ++++++++++ finat/vectorfiniteelement.py | 66 +++++++++++++++++++++++++++++++++++- 5 files changed, 144 insertions(+), 18 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index 3eac3701a..97887857f 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -1,3 +1,28 @@ +from pymbolic.mapper import IdentityMapper + + +class _IndexMapper(IdentityMapper): + def __init__(self, replacements): + super(_IndexMapper, self).__init__() + + self.replacements = replacements + + def map_index(self, expr): + '''Replace indices if they are in the replacements list''' + try: + return(self.replacements[expr]) + except IndexError: + return expr + + def map_foreign(self, expr, *args): + + if isinstance(expr, Recipe): + return Recipe.replace_indices(self.replacements) + else: + return super(_IndexMapper, self).map_foreign(expr, *args) + + +# Probably Recipe should be a pymbolic.Expression class Recipe(object): """AST snippets and data corresponding to some form of finite element evaluation.""" @@ -23,3 +48,26 @@ def depends(self): '''The input fields of this :class:`Recipe`.''' return self._depends + + def __getitem__(self, index): + + replacements = {} + + try: + for i in range(len(index)): + if index[i] == slice(None): + # Don't touch colon indices. + pass + else: + replacements[self.indices[i]] = index[i] + except TypeError: + # Index wasn't iterable. + replacements[self.indices[0]] = index + + return self.replace_indices(replacements) + + def replace_indices(self, replacements): + """Return a copy of this :class:`Recipe` with some of the indices + substituted.""" + + return _IndexMapper(replacements)(self) diff --git a/finat/indices.py b/finat/indices.py index 9ebc7d097..36a2eda30 100644 --- a/finat/indices.py +++ b/finat/indices.py @@ -1,4 +1,5 @@ import pymbolic.primitives as p +from pymbolic.mapper.stringifier import StringifyMapper class IndexBase(p.Variable): @@ -25,6 +26,14 @@ def _str_extent(self): self._extent.stop, self._extent.step or 1) + mapper_method = intern("map_index") + + def get_mapper_method(self, mapper): + + if isinstance(mapper, StringifyMapper): + return mapper.map_variable + else: + raise AttributeError() class PointIndex(IndexBase): '''An index running over a set of points, for example quadrature points.''' diff --git a/finat/lagrange.py b/finat/lagrange.py index 2f87706b0..93c68761f 100644 --- a/finat/lagrange.py +++ b/finat/lagrange.py @@ -88,21 +88,6 @@ def _tabulation_variable(self, points, kernel_data, derivative): return phi, ind - def _weights_variable(self, weights, kernel_data): - '''Produce a variable for the quadrature weights.''' - static_key = (id(weights), ) - - static_data = kernel_data.static - - if static_key in static_data: - w = static_data[static_key][0] - else: - w = p.Variable('w') - data = weights.points - static_data[static_key] = (w, lambda: data) - - return w - @doc_inherit def basis_evaluation(self, points, kernel_data, derivative=None): @@ -138,7 +123,7 @@ def moment_evaluation(self, value, weights, points, kernel_data, derivative=None): phi, ind = self._tabulation_variable(points, kernel_data, derivative) - w = self._weights_variable(weights, kernel_data) + w = weights.kernel_variable("w", kernel_data) q = ind[-1] if derivative is None: @@ -162,5 +147,8 @@ def pullback(self, phi, kernel_data, derivative=None): if derivative is None: return phi - if derivative == grad: + elif derivative == grad: return None # dot(Jinv, grad(phi)) + else: + raise ValueError( + "Lagrange elements do not have a %s operation") % derivative diff --git a/finat/points.py b/finat/points.py index 6a303ba9c..bfd664351 100644 --- a/finat/points.py +++ b/finat/points.py @@ -1,4 +1,5 @@ import numpy +import pymbolic.primitives as p class PointSetBase(object): @@ -33,6 +34,22 @@ def points(self): return self._points + def kernel_variable(self, name, kernel_data): + '''Produce a variable in the kernel data for this point set.''' + static_key = (id(self), ) + + static_data = kernel_data.static + + if static_key in static_data: + w = static_data[static_key][0] + else: + w = p.Variable(name) + data = self._points + static_data[static_key] = (w, lambda: data) + + return w + + def __getitem__(self, i): if isinstance(i, int): return PointSet([self.points[i]]) diff --git a/finat/vectorfiniteelement.py b/finat/vectorfiniteelement.py index e8914778f..4f172a3fb 100644 --- a/finat/vectorfiniteelement.py +++ b/finat/vectorfiniteelement.py @@ -21,7 +21,71 @@ def basis_evaluation(self, points, kernel_data, derivative=None): # Produce the base scalar recipe sr = self._base_element.basis_evaluation(points, kernel_data, derivative) - # Additional dimension index along the vector dimension. + # Additional dimension index along the vector dimension. Note + # to self: is this the right order or does this index come + # after any derivative index? alpha = indices.DimensionIndex(points.points.shape[1]) return Recipe([alpha] + sr.indices, sr.instructions, sr.depends) + + @doc_inherit + def field_evaluation(self, field_var, points, + kernel_data, derivative=None): + + basis = self._base_element.basis_evaluation(self, points, + kernel_data, derivative) + + alpha = indices.DimensionIndex(points.points.shape[1]) + ind = basis.indices + + if derivative is None: + free_ind = [alpha, ind[-1]] + else: + free_ind = [alpha, ind[0], ind[-1]] + + i = ind[-2] + + instructions = [IndexSum(i, field_var[i, alpha] * basis[ind])] + + depends = [field_var] + + return Recipe(free_ind, instructions, depends) + + @doc_inherit + def moment_evaluation(self, value, weights, points, + kernel_data, derivative=None): + + basis = self._base_element.basis_evaluation(self, points, + kernel_data, derivative) + w = weights.kernel_variable("w", kernel_data) + ind = basis.indices + + q = ind[-1] + alpha = indices.DimensionIndex(points.points.shape[1]) + + if derivative is None: + sum_ind = [q] + elif derivative == grad: + sum_ind = [ind[0], q] + else: + raise NotImplementedError() + + value_ind = [alpha] + sum_ind + + instructions = [IndexSum(sum_ind, value[value_ind] * w[q] * basis[ind])] + + depends = [value] + + free_ind = [alpha, ind[-2]] + + return Recipe(free_ind, instructions, depends) + + @doc_inherit(self, phi, kernel_data, derivative=None): + + if derivative is None: + return phi + elif derivative == grad: + return None # IndexSum(alpha, Jinv[:, alpha] * grad(phi)[:,alpha]) + else: + raise ValueError( + "Lagrange elements do not have a %s operation") % derivative From b3054d9d7ca431a57188de98c6d77407c70369ae Mon Sep 17 00:00:00 2001 From: David A Ham Date: Fri, 8 Aug 2014 16:47:21 +0100 Subject: [PATCH 011/749] Fecking pep8 --- finat/indices.py | 1 + finat/points.py | 1 - finat/vectorfiniteelement.py | 6 ++++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/finat/indices.py b/finat/indices.py index 36a2eda30..1ca5272a7 100644 --- a/finat/indices.py +++ b/finat/indices.py @@ -35,6 +35,7 @@ def get_mapper_method(self, mapper): else: raise AttributeError() + class PointIndex(IndexBase): '''An index running over a set of points, for example quadrature points.''' def __init__(self, extent): diff --git a/finat/points.py b/finat/points.py index bfd664351..3044352de 100644 --- a/finat/points.py +++ b/finat/points.py @@ -49,7 +49,6 @@ def kernel_variable(self, name, kernel_data): return w - def __getitem__(self, i): if isinstance(i, int): return PointSet([self.points[i]]) diff --git a/finat/vectorfiniteelement.py b/finat/vectorfiniteelement.py index 4f172a3fb..46c8692af 100644 --- a/finat/vectorfiniteelement.py +++ b/finat/vectorfiniteelement.py @@ -4,6 +4,7 @@ from ast import Recipe import indices + class VectorFiniteElement(FiniteElementBase): def __init__(self, element, dimension): super(VectorFiniteElement, self).__init__() @@ -80,12 +81,13 @@ def moment_evaluation(self, value, weights, points, return Recipe(free_ind, instructions, depends) - @doc_inherit(self, phi, kernel_data, derivative=None): + @doc_inherit + def pullback(self, phi, kernel_data, derivative=None): if derivative is None: return phi elif derivative == grad: - return None # IndexSum(alpha, Jinv[:, alpha] * grad(phi)[:,alpha]) + return None # IndexSum(alpha, Jinv[:, alpha] * grad(phi)[:,alpha]) else: raise ValueError( "Lagrange elements do not have a %s operation") % derivative From 8a899fcbe35b76d5efebb5ba1c7d2bde483d86c1 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Fri, 8 Aug 2014 17:22:21 +0100 Subject: [PATCH 012/749] fess up to some bugs --- finat/__init__.py | 1 + finat/vectorfiniteelement.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/finat/__init__.py b/finat/__init__.py index 79c367b97..3edeaf152 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -1,5 +1,6 @@ from lagrange import Lagrange +from vectorfiniteelement import VectorFiniteElement from points import PointSet from utils import KernelData from derivatives import div, grad, curl diff --git a/finat/vectorfiniteelement.py b/finat/vectorfiniteelement.py index 46c8692af..96d006607 100644 --- a/finat/vectorfiniteelement.py +++ b/finat/vectorfiniteelement.py @@ -18,6 +18,8 @@ def __init__(self, element, dimension): @doc_inherit def basis_evaluation(self, points, kernel_data, derivative=None): + # This is incorrect. We only get the scalar value. We need to + # bring in some sort of delta in order to get the right rank. # Produce the base scalar recipe sr = self._base_element.basis_evaluation(points, kernel_data, derivative) From ad6614edc2b073f69af4eecf9e4deeb93557295e Mon Sep 17 00:00:00 2001 From: David A Ham Date: Thu, 14 Aug 2014 11:53:36 +0100 Subject: [PATCH 013/749] Initial commit of Sphinxified documentation. --- docs/Makefile | 202 +++++++++++++++++++++++++++++ docs/source/conf.py | 286 ++++++++++++++++++++++++++++++++++++++++++ docs/source/index.rst | 22 ++++ 3 files changed, 510 insertions(+) create mode 100644 docs/Makefile create mode 100644 docs/source/conf.py create mode 100644 docs/source/index.rst diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 000000000..74a982dc5 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,202 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp \ +devhelp epub latex latexpdf text man changes linkcheck doctest gettext \ +serve + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " serve to launch a local web server to serve up documentation" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +TARGETS = html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + + +livehtml: html + python server.py + +serve: html + cd $(BUILDDIR)/html; python -m SimpleHTTPServer $(PORT) + +source/teamgrid.rst: source/team.py + cd source; python team.py + +.PHONY: source/obtaining_pyop2.rst + +source/obtaining_pyop2.rst: + wget https://raw.github.com/OP2/PyOP2/master/README.rst -O $@ + +apidoc: $(GENERATED_FILES) + sphinx-apidoc ../finat -o source/ -f -T + +clean: + rm -rf $(BUILDDIR)/* + +buildhtml: apidoc + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + +html: apidoc buildhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: apidoc + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: apidoc + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: apidoc + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: apidoc + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: apidoc + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: apidoc + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/FInAT.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/FInAT.qhc" + +devhelp: apidoc + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/FInAT" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/FInAT" + @echo "# devhelp" + +epub: apidoc + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: apidoc + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: apidoc + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: apidoc + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: apidoc + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: apidoc + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: apidoc + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: apidoc + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: apidoc + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: apidoc + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: apidoc + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 000000000..1f5c608dc --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,286 @@ +# -*- coding: utf-8 -*- +# +# FInAT documentation build configuration file, created by +# sphinx-quickstart on Thu Aug 14 11:38:06 2014. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.mathjax', + 'sphinx.ext.viewcode', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'FInAT' +copyright = u'2014, David A. Ham and Robert C. Kirby' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.1' +# The full version, including alpha/beta/rc tags. +release = '0.1' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' +#html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +#html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +#html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'FInATdoc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', + +# Latex figure (float) alignment +#'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'FInAT.tex', u'FInAT Documentation', + u'David A. Ham and Robert C. Kirby', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'finat', u'FInAT Documentation', + [u'David A. Ham and Robert C. Kirby'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'FInAT', u'FInAT Documentation', + u'David A. Ham and Robert C. Kirby', 'FInAT', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'http://docs.python.org/': None} diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 000000000..ab78c49d9 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,22 @@ +.. FInAT documentation master file, created by + sphinx-quickstart on Thu Aug 14 11:38:06 2014. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to FInAT's documentation! +================================= + +Contents: + +.. toctree:: + :maxdepth: 2 + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + From ce8595857c41f183a9cbae40108f3f8e89cb3b2b Mon Sep 17 00:00:00 2001 From: David A Ham Date: Thu, 14 Aug 2014 17:51:34 +0100 Subject: [PATCH 014/749] Initial version of Sphinx documentation --- docs/source/_themes/finat/README | 4 + docs/source/_themes/finat/layout.html | 41 + docs/source/_themes/finat/static/banner.png | Bin 0 -> 155781 bytes docs/source/_themes/finat/static/banner.svg | 1396 +++++++++++++++++ .../_themes/finat/static/dialog-note.png | Bin 0 -> 1582 bytes .../_themes/finat/static/dialog-seealso.png | Bin 0 -> 1502 bytes .../_themes/finat/static/dialog-topic.png | Bin 0 -> 1910 bytes .../_themes/finat/static/dialog-warning.png | Bin 0 -> 1391 bytes docs/source/_themes/finat/static/epub.css | 310 ++++ .../_themes/finat/static/feature-item-1.png | Bin 0 -> 8126 bytes docs/source/_themes/finat/static/featured.css | 74 + .../_themes/finat/static/feed-icon-14x14.gif | Bin 0 -> 606 bytes .../_themes/finat/static/fenics-book-icon.png | Bin 0 -> 3112 bytes .../_themes/finat/static/fenics-web.png | Bin 0 -> 3466 bytes docs/source/_themes/finat/static/fenics.css_t | 672 ++++++++ .../source/_themes/finat/static/fenics.css_t~ | 672 ++++++++ docs/source/_themes/finat/static/footerbg.png | Bin 0 -> 333 bytes docs/source/_themes/finat/static/headerbg.png | Bin 0 -> 193 bytes docs/source/_themes/finat/static/icon.ico | Bin 0 -> 1150 bytes docs/source/_themes/finat/static/icon.png | Bin 0 -> 676 bytes docs/source/_themes/finat/static/ie6.css | 7 + .../finat/static/jquery.latest-commit.js | 38 + docs/source/_themes/finat/static/middlebg.png | Bin 0 -> 2797 bytes .../Apache License Version 2.txt | 53 + .../neuton-fontfacekit/Neuton-webfont.eot | Bin 0 -> 28730 bytes .../neuton-fontfacekit/Neuton-webfont.svg | 145 ++ .../neuton-fontfacekit/Neuton-webfont.ttf | Bin 0 -> 28552 bytes .../neuton-fontfacekit/Neuton-webfont.woff | Bin 0 -> 18860 bytes .../finat/static/neuton-fontfacekit/demo.html | 33 + .../static/neuton-fontfacekit/stylesheet.css | 16 + .../SIL Open Font License 1.1.txt | 91 ++ .../finat/static/nobile-fontfacekit/demo.html | 48 + .../nobile-fontfacekit/nobile-webfont.eot | Bin 0 -> 29592 bytes .../nobile-fontfacekit/nobile-webfont.svg | 149 ++ .../nobile-fontfacekit/nobile-webfont.ttf | Bin 0 -> 29360 bytes .../nobile-fontfacekit/nobile-webfont.woff | Bin 0 -> 19800 bytes .../nobile_bold-webfont.eot | Bin 0 -> 26880 bytes .../nobile_bold-webfont.svg | 148 ++ .../nobile_bold-webfont.ttf | Bin 0 -> 26660 bytes .../nobile_bold-webfont.woff | Bin 0 -> 18244 bytes .../nobile_bold_italic-webfont.eot | Bin 0 -> 29632 bytes .../nobile_bold_italic-webfont.svg | 148 ++ .../nobile_bold_italic-webfont.ttf | Bin 0 -> 29384 bytes .../nobile_bold_italic-webfont.woff | Bin 0 -> 19584 bytes .../nobile_italic-webfont.eot | Bin 0 -> 34264 bytes .../nobile_italic-webfont.svg | 148 ++ .../nobile_italic-webfont.ttf | Bin 0 -> 34036 bytes .../nobile_italic-webfont.woff | Bin 0 -> 21824 bytes .../static/nobile-fontfacekit/stylesheet.css | 52 + .../finat/static/sample-news-image.png | Bin 0 -> 3219 bytes .../_themes/finat/static/social-buttons.html | 44 + .../_themes/finat/static/transparent.gif | Bin 0 -> 49 bytes docs/source/_themes/finat/static/unknown.png | Bin 0 -> 2802 bytes docs/source/_themes/finat/theme.conf | 10 + docs/source/conf.py | 13 +- 55 files changed, 4307 insertions(+), 5 deletions(-) create mode 100644 docs/source/_themes/finat/README create mode 100644 docs/source/_themes/finat/layout.html create mode 100644 docs/source/_themes/finat/static/banner.png create mode 100644 docs/source/_themes/finat/static/banner.svg create mode 100644 docs/source/_themes/finat/static/dialog-note.png create mode 100644 docs/source/_themes/finat/static/dialog-seealso.png create mode 100644 docs/source/_themes/finat/static/dialog-topic.png create mode 100644 docs/source/_themes/finat/static/dialog-warning.png create mode 100644 docs/source/_themes/finat/static/epub.css create mode 100644 docs/source/_themes/finat/static/feature-item-1.png create mode 100644 docs/source/_themes/finat/static/featured.css create mode 100644 docs/source/_themes/finat/static/feed-icon-14x14.gif create mode 100644 docs/source/_themes/finat/static/fenics-book-icon.png create mode 100644 docs/source/_themes/finat/static/fenics-web.png create mode 100644 docs/source/_themes/finat/static/fenics.css_t create mode 100644 docs/source/_themes/finat/static/fenics.css_t~ create mode 100644 docs/source/_themes/finat/static/footerbg.png create mode 100644 docs/source/_themes/finat/static/headerbg.png create mode 100644 docs/source/_themes/finat/static/icon.ico create mode 100644 docs/source/_themes/finat/static/icon.png create mode 100644 docs/source/_themes/finat/static/ie6.css create mode 100644 docs/source/_themes/finat/static/jquery.latest-commit.js create mode 100644 docs/source/_themes/finat/static/middlebg.png create mode 100755 docs/source/_themes/finat/static/neuton-fontfacekit/Apache License Version 2.txt create mode 100755 docs/source/_themes/finat/static/neuton-fontfacekit/Neuton-webfont.eot create mode 100755 docs/source/_themes/finat/static/neuton-fontfacekit/Neuton-webfont.svg create mode 100755 docs/source/_themes/finat/static/neuton-fontfacekit/Neuton-webfont.ttf create mode 100755 docs/source/_themes/finat/static/neuton-fontfacekit/Neuton-webfont.woff create mode 100755 docs/source/_themes/finat/static/neuton-fontfacekit/demo.html create mode 100755 docs/source/_themes/finat/static/neuton-fontfacekit/stylesheet.css create mode 100755 docs/source/_themes/finat/static/nobile-fontfacekit/SIL Open Font License 1.1.txt create mode 100755 docs/source/_themes/finat/static/nobile-fontfacekit/demo.html create mode 100755 docs/source/_themes/finat/static/nobile-fontfacekit/nobile-webfont.eot create mode 100755 docs/source/_themes/finat/static/nobile-fontfacekit/nobile-webfont.svg create mode 100755 docs/source/_themes/finat/static/nobile-fontfacekit/nobile-webfont.ttf create mode 100755 docs/source/_themes/finat/static/nobile-fontfacekit/nobile-webfont.woff create mode 100755 docs/source/_themes/finat/static/nobile-fontfacekit/nobile_bold-webfont.eot create mode 100755 docs/source/_themes/finat/static/nobile-fontfacekit/nobile_bold-webfont.svg create mode 100755 docs/source/_themes/finat/static/nobile-fontfacekit/nobile_bold-webfont.ttf create mode 100755 docs/source/_themes/finat/static/nobile-fontfacekit/nobile_bold-webfont.woff create mode 100755 docs/source/_themes/finat/static/nobile-fontfacekit/nobile_bold_italic-webfont.eot create mode 100755 docs/source/_themes/finat/static/nobile-fontfacekit/nobile_bold_italic-webfont.svg create mode 100755 docs/source/_themes/finat/static/nobile-fontfacekit/nobile_bold_italic-webfont.ttf create mode 100755 docs/source/_themes/finat/static/nobile-fontfacekit/nobile_bold_italic-webfont.woff create mode 100755 docs/source/_themes/finat/static/nobile-fontfacekit/nobile_italic-webfont.eot create mode 100755 docs/source/_themes/finat/static/nobile-fontfacekit/nobile_italic-webfont.svg create mode 100755 docs/source/_themes/finat/static/nobile-fontfacekit/nobile_italic-webfont.ttf create mode 100755 docs/source/_themes/finat/static/nobile-fontfacekit/nobile_italic-webfont.woff create mode 100755 docs/source/_themes/finat/static/nobile-fontfacekit/stylesheet.css create mode 100644 docs/source/_themes/finat/static/sample-news-image.png create mode 100644 docs/source/_themes/finat/static/social-buttons.html create mode 100644 docs/source/_themes/finat/static/transparent.gif create mode 100644 docs/source/_themes/finat/static/unknown.png create mode 100644 docs/source/_themes/finat/theme.conf diff --git a/docs/source/_themes/finat/README b/docs/source/_themes/finat/README new file mode 100644 index 000000000..3e4bdedc9 --- /dev/null +++ b/docs/source/_themes/finat/README @@ -0,0 +1,4 @@ +This is the Sphinx theme for the FInAT web page +It was originally based on the Firedrake web page, +which was based on the dolfin-adjoint theme, +which was in turn based oh the FEniCS project theme. diff --git a/docs/source/_themes/finat/layout.html b/docs/source/_themes/finat/layout.html new file mode 100644 index 000000000..9b22d0f09 --- /dev/null +++ b/docs/source/_themes/finat/layout.html @@ -0,0 +1,41 @@ +{% extends "basic/layout.html" %} + +{% block extrahead %} + + + + +{% if theme_favicon %} + +{% endif %} +{% endblock %} + +{# override upper relbar to show our navigation menu #} +{% block relbar1 %} +
+ FInAT Project Banner +
+ +
+
+{% endblock %} + +{# do not display lower relbar #} +{% block relbar2 %}{% endblock %} + +{# do not display sidebars #} +{% block sidebar1 %}{% endblock %} +{% block sidebar2 %}{% endblock %} diff --git a/docs/source/_themes/finat/static/banner.png b/docs/source/_themes/finat/static/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..46d02b19e2a64b8e29ea1f8a249d299ed74b6e82 GIT binary patch literal 155781 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

J2QBqb%uW)p%ts|cKznj(9SOE0wD$-UH&dW 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 M4Lw`|*80x>0auBhng9R* literal 0 HcmV?d00001 diff --git a/docs/source/_themes/finat/static/banner.svg b/docs/source/_themes/finat/static/banner.svg new file mode 100644 index 000000000..076bacc2f --- /dev/null +++ b/docs/source/_themes/finat/static/banner.svg @@ -0,0 +1,1396 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + φ + 0 + + + + + + + FInAT + + + diff --git a/docs/source/_themes/finat/static/dialog-note.png b/docs/source/_themes/finat/static/dialog-note.png new file mode 100644 index 0000000000000000000000000000000000000000..263fbd5866aef1ce3645d768fbf55def9a091905 GIT binary patch literal 1582 zcmV+}2GRM6P)k@WH7pQF=3=faW zkFSqR=8AIgXBSA;Hc`EO&K^N)42Y`_9UkcY+JOPZzW)_SXY=A^lj%j;c6NwBgJ_tV zm_2r=oma1nQ4OegJG6y4*V7pt+<)-viajx2cgoi@dv1pQz5%6oPs>nSm~*w?TqQVv zob8dII{c&Oe%S1c$Jb4}_O0hohn`**8Ge(}Coiu}Aq2|y9sKoAe_L3{Is>OKFbSRv z1pSn5A0ELYKmLQ6BwuOzH&f_kC zpw7uXeVu-psxJ`U5$EUY>gJ?Py7zS&L7kH>03>~lEiK4GzOF#Bu=x@I$Wn<&a}!AV z0JO@7l0!?lsTn6_uHD-u=x*nv?jh4tOM8aU5Jm$S4XgGI1^n1jw1Qj)a0G^`sW^46 z`zk{v@<5WOf#ro&h@# znjak7C0?~s9=SM_n9NYjv(wK7=2M5YtS{PKY$ zvq{93_SMB}%_$-0$B^l1M4*|BZ#$YIXIuaXZY|uu(-Vrempj=|${=$lGCK?Q%G0#? z`m+#hhFpCs04oQ|EN0RVf?FVz z;UBke#-&y%!v?064Rok zOzr0t(XWi*Ji3QBXXf%JRoe>fIM6Q~a~|x{=CxG~y=Q>o+w;yR!_$*li|?MfjH_0w z4(~z?Yj=n@ipBh~`I%&-^WdPS`V7dV>N~21K#jH|clB`PgWJ~4*uv-kzRU4neaIZ( zs_AL~J|J*?3i;mzZ}f)kj*(jv-9BB_qC@+I5-iVl$9ByRp&N*X5L#OY%C2rk literal 0 HcmV?d00001 diff --git a/docs/source/_themes/finat/static/dialog-seealso.png b/docs/source/_themes/finat/static/dialog-seealso.png new file mode 100644 index 0000000000000000000000000000000000000000..3eb7b05c84809454c095bcb61b27d83585540bb2 GIT binary patch literal 1502 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 literal 0 HcmV?d00001 diff --git a/docs/source/_themes/finat/static/dialog-warning.png b/docs/source/_themes/finat/static/dialog-warning.png new file mode 100644 index 0000000000000000000000000000000000000000..7233d45d8e6e41ef8fcb318c76303a9b6f23997e GIT binary patch literal 1391 zcmV-#1(5oQP)}piCG~<@Tj_&{ee%V|SZW{gB8?y>5~HGEkRU`Q5xgR16NK8$#%7av&gnyv zB_>9ji%*39&LbK3qFu zI7(%ZZ%;}2Gqh=wf?-g1;X+qD8r}8{{PEcV%0X!tqH%6yq(DC>&`hf#DjvXW3(1702fiEY= zF5vsm0&oGwD!P6+uzb1JdigTStXXLJ`Pjl?)VXtsUA;>7;>9+ps^67D`lthR!lCrM zJiGbuAzhIO`u_b0*{$vea+<+#n+u)TG0)=^W&Y8i0fonJZU{&O3K7Vlni6`w*i02KV;P{rfRu zG0etBGUm-QC7c=+fU_{mF@F3e|CA|e+x6?%3JOrCPscC}T3cI*Mx&UfiIS0lW6Kua z*4E+=1XR`QT~P`vMhu|BsqFlGD_&EBWm!0O?*~ty__PKSBsxE>~io%jH4{flNukRa8X# z{rjXQCrg{pH?0i59u|NFLJ0B8)HQ2(TUCW{yU};B)sYgKfwF=i#fQ*|T%prYYO&>#^_Jg-l9HIC3}~D2jrjDCoMLxDnFn#I<)X9Z#O% zl~TASP4c~kT|)vm3aV6;lT*TB*>>d$((gwvEKE2_WWLjh)9HL~UI>A6!v?geQ)#cO zBP%CIQ>6UickujYfDG8-Td-iF{q<|n@$@N&XN+4a)1MXii!#xt%)zy89UYGz zG12e0NnI~2gPDT@C~$aEUY=^+y^GP-hO?w((CCB^XqtxXDMlUN#ETcC z1p+4hp6CQXDP-AvzM{13Y_09qE$nO8qUGca8odyL^73*nUc5M{b0Gw_%uGDnx6}FT z856u-Rdc(;WsuwJSM3x1@yun*tghR)G2?Oc_3NRv6|2V>LcBLlv~F)$efs`;+#5F% zJ#z;0`E&f~>DJ4JhLgbjz5vpKd|!698oPE4fasPj{qtqSV!U}nQhK`b970DyKML_) z0Ovbsj@-OC=8MgnrPMUAEG)wS)5J22?y+HDnPzw2Z#-bF|D0)J#^W@

' + // Needs to be wrapped. + // ADD DESIRED CLASSES TO HTML TAGS BELOW! + '' + // Commit author image + '
' + + '
' + // First line of commit message + '' + // Link to commit author + ' authored at ' + // Outputs the commit date + '
' + + '
' + ); + var repo = results.data[i]; + var commitUrl = repo.html_url; // Grabs URL of the commit + $commit.find('.commit-author-img').attr('src', repo.author.avatar_url); // Add commit author avatar image + $commit.find('.commit-link').attr('href',commitUrl).text(repo.commit.message.split("\n")[0]); // Adds link to commit and commit SHA + $commit.find('.commit-author').attr('href', repo.author.html_url).text(repo.commit.author.name); // Outputs commit author name + $commit.find('.commit-date').text(new Date(repo.commit.author.date).toLocaleString()); // Outputs commit date + $commit.appendTo($container); + } + $('.commit:even').addClass('even'); + } + }); + }); +}); diff --git a/docs/source/_themes/finat/static/middlebg.png b/docs/source/_themes/finat/static/middlebg.png new file mode 100644 index 0000000000000000000000000000000000000000..2369cfb7da3e5052c2ad4932a6d56240c92654c7 GIT binary patch literal 2797 zcmV4Tx0C)kNmUmQBSrfqTdoR7v5<-y@dJRoV0Fe@UkzPe5BmqJR7!t5oLbpCc6HqI?@={d8%D5al;0(=!Cz zYydD6nO!2_rJ!tuGDRE_#zA==00c_%EKZ!o62USwPXIWXSj`sE}8w<4jU*%sHzk2;U z$a?$5<7MdQoaVsz{O95^ejS$UX;36cb2fe1Y+3Y{{cC>d?Hh%b}~Geu0H=$|_LAH!zl zAj2Q0Uf9TEuaUC0Snjw2jC3cfEVxw!5{*}g2jLb zQa}a}gIur*tOxm^5bOYZKsl%aHJ}bOfD@nvoCX)bWpEwb1byH>7z88W8JGmG!3+dJ zc!&zoAT>xEGJwn=8;A|fhrFObC=7~)5};&A1WBP)&_<{bDu&9TgHRpxBXkP709}Q8 zpu5lzG!Fdvqf1r2MCzX|yZIz>xmnl~$ zpHUuUAPhr>A0wSn#5lp|XS`F#WweHcflJworSw_BrjROl77!Go4w=>|jpnXz2LrNOcbC zbnDFM8tF#rZqRMieW*v$W9ud9?bd78o7C6V57J+yU$1}9fM~!rNHN%J&}lGjXk-{| zxY@A9aLh>6$j@knQN7UvW2&*M@lxYz|7l}t!?UTdxjmOU*L&{Txvg_w*qYf2Z1>yVv7^}q*=@FKxBFo4U@x|B zupf8OcSvxkbQoaM*&*z0>?@8~M-Rufj;9^pI@vo(oK86X;mmSQb3W=kHqU6DU|!9< zVHaH&uFFA}!THSj3G)xkA9U4m<+@h8K6cY{%Av^?0i=GocG202Kesu9q`liwb@Yi zqU=@)9sQZ=k{U}lNr!Ug=Tzjp$&JcAxlD1HXj#{C)8$*2kFM}u@%>87O5V!$RXVHI zuNqqIzWU%AXiegp_O*Iz^VW{6^I3OfJ!yT~`d>C!Z7AOGYGd@qwmi+eb$P>^d^XkR z%jJvn2R1uzuG)gxBHYrwb?(-(tse{c1=k9#3QG##Z{uyd_MP>2rQdzpp0vHY$i8U* z4%`mWj{cplJC77A7OyBC-W9Z~c{g)+!R}Xkmh8D&Vp~$Rm$X;9cd#_Dw6#pXY)9Gq z@|5zv3Xh7$N{z~`mDBt9`+E1g?Qf{ktSYQ}cR+aH&Ox7p&DDn0C5Lc_at=MIiK^-R zp8b7Yt$J-??T5pn!-Ge{j&#&H)YTo;I9gN>*GucikHsIm`Ge;VtqrV(gN=;F!sFn$ z^!U>s6MpPJ5pbgYB>QB;PX<3#Hqn|2nxW?9&66!DErYGGtv#pwPqnu>w>AB2@$=!+ zI;ShnD4!`hOFEl(_S3l)=cdkQou9and||kKN&EeaF&A%lgm!da3b=ITviIeSo$j6I zuDDz|ebwpescYO08?84TZ?^T!>p9!&+I!)a=dH`P z{cd0HThQ0jAK8CrAbw!*4*$;B-SoRJ?&aK@xxelK_Cdizg@+}NG#*v|YVvF2p#9*P zAK5^Wa`oDjMp>M1#i^e9C^!r+xaf~-RMm2 zd;I&-4<;YlJ_dYz@G0Zdr@sILoAdna&gY5%000SaNLh0L01FcU01FcV0GgZ_0000` zNkl3M)`0?X^CI%pY5dZ)Ghq6c#BU2mL4m7>^xj0=#gtkGV1kD*#^bxk;#3qK# z1w@EpQ$noku{i^$7!@T*ax+fPAS4hh!X^U%5(tH;2fehL00000NkvXXu0mjf?v`T0 literal 0 HcmV?d00001 diff --git a/docs/source/_themes/finat/static/neuton-fontfacekit/Apache License Version 2.txt b/docs/source/_themes/finat/static/neuton-fontfacekit/Apache License Version 2.txt new file mode 100755 index 000000000..4df74b8ce --- /dev/null +++ b/docs/source/_themes/finat/static/neuton-fontfacekit/Apache License Version 2.txt @@ -0,0 +1,53 @@ +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and + +You must cause any modified files to carry prominent notices stating that You changed the files; and + +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. \ No newline at end of file diff --git a/docs/source/_themes/finat/static/neuton-fontfacekit/Neuton-webfont.eot b/docs/source/_themes/finat/static/neuton-fontfacekit/Neuton-webfont.eot new file mode 100755 index 0000000000000000000000000000000000000000..b964bbe02a5711dd2185b41a2626a85b3c06b26b GIT binary patch literal 28730 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 + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 2010 Brian Zick http21326infowith Reserved Font Name NeutonLicensed under the Apache License Version 20 the Licenseyou may not use this file except in compliance with the LicenseYou may obtain a copy of the License at httpwwwapacheorglicensesLICENSE20Unless required by applicable law or agreed to in writing softwaredistributed under the License is distributed on an AS IS BASISWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND either express or impliedSee the License for the specific language governing permissions andlimitations under the License + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/source/_themes/finat/static/neuton-fontfacekit/Neuton-webfont.ttf b/docs/source/_themes/finat/static/neuton-fontfacekit/Neuton-webfont.ttf new file mode 100755 index 0000000000000000000000000000000000000000..c3481949614cdd4d4d0f93f8b6508c57ebee6a05 GIT binary patch literal 28552 zcmd75eSB2aoj-ogojVVi%uMDfnM@|b%w(7h!!Q{pLl}k-0|8?UF{Cl2A*L}zh=?K} zQluitrIglEmr~a9l`gT??+Yu2TAB*SfCTx+>)Idwut-15AP&9BIV=KIPU+rycZ(9CNt92b?qcib+=bGhrt>gbk!uxNY zkNd;7FJH6rTfh9)y*M6b%=p2&wHxlNuZ(}2u{8w4zpcAt<+`C~f3TUcwH>&ho*`_o zr!?g?+CM$#-SX1jF0MFmRL#QJN;u~1Z}5}Fk^Uq-%?&@}e`H1`8#BiJI1K4~!)Z1u zx#>Zp0e9iwUo)0=>R%1hanI=0E6|7VdD)cIlI%p;Iq4F|w_y4Trx`Qk;~9mZV`io5B|pAHVq6#qo>(aPj2DM_yn3`pVZwULStF|Mi~NBd^^U{=D(? zrJmq*|NTF1Dxns)aS0pxUHK1i7-XZ#Y$>yr+w6`Cr_1f}R#y3{{WXEw;FM4}QWu>X ztB+4hOi#{8HKb=|8nd&SX6NSQ=Qhu4xvF*kg0_X=)@wSh?Ofb-UH6inPxM~j_sON7 z>R&c+!{DcfZX8~I<=6f~`=%9iVb$pNTPANc|*9b{i-e_|Kezp>x2{p>8emz`$! zv&YyY?0L4EF@?=gm}jvP?CDoJKeJ3>(bj6klgFL>*0F_2LlSjYs@p$sp8c9OQ$R|+^%pwol?qX zgtyX1euVdj6+`UWXjqZ!I~CT`ziZ{LX3-#FtvmR7WC#@R?me10K2-efwmoZ|EOxtC$fDpDBGj8tz|weITOn zW&OJV(6!NB(eSQocSV;|PY4{^Qh%I81~yEA&H^vkq;wXRpi3m^3In8d5kz){u@7xC zo5u=}`(#lzvVfc{fP<4ovuJb0l2+K6qHJPLIjP7_)yR`YgE&&lc#_S`WL*(gEaHkR zlEPY)KcI~o`#yMRk^B>UW6#CY{83YgIiS%#$&~VR(O_f_Imwlvc&ph&M@4-%R%K?E zWU<1;qWDrpx>zMzV>MM3IPKRzIfW-xe~rtnmK*YUd^|U6cDB)1Rp~XGqIL0l4;0Ak zTw|rvP#JHM_6%IrnoOk<3tDHSBBnWQ;qJEE=C5z=O(dIJ8k4C;<0D32xr1l4vlpJX zRLh1eX|WY@PHHhFft3n2jTOuiODc|RksDbVx@BqPiWAH_i4oN$aNt2F3?_4Qa=#+g zzbu((tUi_M?@uN&)uyCZOFKCKqu7BM66@kEa!kI!0xZNEm=Z`AyuRveHmgd;Hwv!W zU}IyWVn`R9Q$lpogp*}fJ5F*XoK_<|sm!i9*?R4}ce2W))uGfl74x|2H@~k~#!t2` ze7A>AD^*UVd|a)xysy~Cjksq_sxf<#V+OG`(N^(0$IAQ`G^(6q)~a$eDxG8YN*iu- zimgj*y|kr!eBvHFRV|*vI|GyNshxaJknS0qGI?i+&KubG%$9OnWtG3Cq4bZ>uaXwUa^V#w@@gL67Xxwsfi)=Ct zuq10_dG;e#m=4Jh_@8dFCdcMakC!D2^*C2>%Mg={ku8D9xHELk?Zvf9oF1!=do5t| zxoO35Mx9}LL*d~wuA-z%(F~_*0s*Hr7PI<^`bK3Yt}E_raf*J%+o)unY7Te#8;f-& zmV=m7>mtw$5tt#l+=?Nu)>UKg%QLmwR(i|sTRl59`E2B-(u*d7G^0`c$)kPsQha0we3m%8{Y@N^)fvXX!FEYx^s!gzy9!9FPtr;8d&2 z-Z!GD)|9<(0IK~B4IJ!W6CmycN2+c+6q2!WoZo-5;|%IH$G?#fG!uI_z%?m48r)!FgI-o<-)V%l|k zCBwo8FFe7&_0SjZzWdPbyTAIS>w0h@*|o>8YRkYNe`wq8zaLoom4Qts`{(A)EAqqE z^SwN|{?tQv-TU{&U4t7=3^&4V0k5CCc+U8Q@jd9dIcx#gU*K8^XVnzYZ2=#vt%T4v zfzX7mj^zZjw3-J?C!A^v&{wW2>~g14MLg~{v8lkcTj+1J)P*`;pMCTdyRNGYPG#n} zT48-(agHknC?&&8sEdr)Hi~VQwgsH0p)YE3xZHUqn^zja)_HbZs&JCASkevrBgY^J zSMv$TMJ%thxC$0WDxc3QZnsjGSIS*V4QP{!;c_sqw7QNnx!hY**DU13H=%!D*PuK}fQVvvYAnoGzL?UeD~DGg@al$9oQM-|^tV6DPJ5 z19ESl6bz>KeJA1Rxb~p~-AC_x;I>=eTG%$y{6%--Kjn67#8-XrTlew99SeAtn~uG6 z^xL5wx8f6&0H_JJ`RE%3;ebg!qSu}#H3_8PLdZRRAPmWTX_0h>+8lA{S;H+88OHt$7 z%nQG$Ab|(CQc3hz%kX@aKH53CK#IxiX{Fgs z&S2ldV0&8wKA^2?>5k*0#$)Y^f+K^s;LS%Sc4|j@M!S|idGa0p4!$EZlffjtYJ84a z0aKJUu%}odPBu=1kyS%^yU;EKpjgHN0q#f^%ZzLuPMheAv0)l1@^o4;of&hQoVW~z zCtU(-#;LbVP8iV1Vn`en>OwRW)cMPj>P(&WYrpl6kL#>&qG8&o8q42zDDpT{Wixb1 zK{jGXnuM#)a&V?MuZHSe?y-PpY6^_W)Ib@|`txes=yE^e@lJ)yq%&+YZ*1j0uwc~F zl&dsVdcmed4jch#mF5;ctCi2jElFNyj+$iqgRA6fdH9rk{A*G~>XxFlp?kZf9{>KB zwBiRQr^$MFg)H|)rFN;in!jQ4n56PUtK@(ox%39@yK6_ST^Xc@Iy$cK_ZrYk&GCpCWV<4CmwW zTFCJ=C65JW`3%TnStBS^G#c4ekXxfn0%$as0p;asMLHw8X`}9v*kiotSJ-V3a(VZsb4RtwFcn9{`xBWot& z0yl;;sz*l%(IXZ9=WhynRD&MAag`b0SGb^uuS5@D^&~x1j{zN`p~(b`u3)u!FlNDU z<3JY71MqQu{3<@_=~VEDIkK4#J~-6ZHNPd1vZP+U?cQYrot^)5P^zk#$Zq8W_ZL>| zNF}rJMAv8g2X-G=F*>$~R4#b5OMA&M09ab!^(oD1)e8E?)1(Ta^SE-Auvl{-@l;~| z4B%-cA&Xm~dYF`$J~Jm&k8f74q?dJ)k`-f)=dR+Up-Vfx@1(RLk6x3xathZSwfe5T zKC9Cgm=a5^n$y(Txh^wQDa$TvUBEUs-dP*+I5+if_}uK|hRv&YsylApeQ4vx;$z=k zI_ht&j7*7DI6FJmZR{U#ZQ$G_m04xKvn3SCCg=BjW_M3}OB+A>#_7B#{-Z5Nwr<-2 zm?d^Vds5ym?*!l2*)j&IDP>ubN}H`vH?ORczE#vtIyP_k$tmqWDt$AX+KCP?Ui`J3FRVn^4$`yZ`&?z3Af4-^iZ*m<|IcjZVdHhy^E#oKp; zI=iPDxpwmK!y7lbhn>eB{+A+u;pmaw_wnwojq5fYdyZG!w{J=$)ef4T(#{#4k_VZI zEoC%pKt)I;dYaRU5z0u=QmMyiFB==4W~xzcc$ySO^RD0RBL(kz?!Lx#gAww#ydfxpS7-*oXc!*;}V@~oQ;@eWp)O;~OL zmK}uU8-$MJ%8dfAbAi{nPGt!h?pED!x2B6r^wwOb`YD(-Fz6egV&~I6Geuh+*4gpp zPd>+}!w$vbRNBYY>rL+~)5pi^ub&QkJ}$P?#5O@&+%$HLr5%?##P(XT?G)R^V%sIQ z*U`3smt0}|$MedxypqTlaFLvWuDsHjR~F}$YxBxAd8H%I=2uwir?+3*b=~!dC|}dj zxtLVz7617Ebb~OzKD7iaR5!Z?EX33sT8N+AGlkTY_-J;nJ_q^9LQqNuhN*%(B>W5& ze2P@ERE6oGI|}3{Ko6$5aK0OOn!{VbZ6#BMbo-_%@62S-(fl+YU3KWb8wO3La*Nkp z9=BG=h!9y#vLrcEA!{U)@O#@Aefq}R?z?%#$sIkt+nm0rJL>gYzfjE&}*fjY?5WpZ4tjE(3Hw&+BOgN z$6~`b9vM6^dP{IdFy*UpR+=QgH+`eDWL0lpU+1*kO)IZyPmr-BvC|jRat3xn4V%tJ z7}RIcX=Ka6B-QES6d}0Ew6tm@|+u7aZ) z@jT$X8osjP%B$0+0QV+VRl+^XHo7PAjiHc+nAthl6eUOb5;9GvxvB8V)9Gwa@Al7q z*=gUieq+9+?7;DZ%2OlTJ6k&gQb+rpqqj;=9^r27^zZMxd+)|gy*;K#hjn;#+r7Jr zr+>ZoP&6^sHsi>;wR-?6VBNqZzFoFpG*XxWnp>%y zY^jJ>el$v1@A`1eazl5%DH)F^a{2B{t$2J-t}`CXHOCXZhU{cBnLvLW+6R0O`r~3D zJxM3PD}LWbJOcjW)`-!rk0j3;;H1-D{otq zlkdyL@b8XZbQk(Cc=3Qik`F^4UdI%$zXB~tCfQ{z$R*N(64$lh^ndv2r*);mov>dO zNhm>;!FJ_hOM*(~H}{osGfdcT&7U|c#d;>zNS)HaM$JF*le?t*blNTA+oiV*F=$0I zcop7>a81Z$Cf0;stVgh+QOz8`MGoofj)^r6X@3RZzI{87Zr@Jt;Vc0d90G(tZ;1 z9H#*w(If4XCYHBJ2$yf3*chES@)`cN4)^+trwwhUC`+@ekY$3ESImmIyD~jZG5+gd zBu^Jc^7IBWl3UYC@Qms*y)l+@1rciptw`gubTOs3f{54TkvdYs{8CG(S=UVPpFgkUTw`)omVz%#ot}lq%PfBK zc=bTpq`8X$+X2JTHyMKwF&enFC1k{eUSikv_wM=fy*)kg_?({FTz5?< zX)@*KcI9&Mcy?_r7N3)A-ET8J^phJ}kRRX;B0*rx543j)Eq$ugOf$VzA8frt?{TH|I$OqvxH*;Yl;pj zX6EveFF=37#WN!w;j4;db&|xqzQ$aWgLm>l?&tFU`}xB5J^iyYHml>yJ)PO~(*ARM z<6f^>wr(CCS-F06I2{Putg`&Ui?43mx_L7ISM7(De@;z6b^ZRO@-GhtLnQixJk8cb`^4U3U*{8Yt;DNJm{p|xI(vhRv zR`HvPepDSmpIT9?fInn=+^on76`&nr7)6I5h-xZpY%Iu#Ocia;3Iv-KS5~#bN-u}k z;7O}qo&B4)|85mZQIT=pIIg-~?<>}E<5;=d3VX=z z9JAS-XtKFb5#(|&DVex_6|N`ZX}C{q0K>-N{R$q1HBYGxDX&Cb zYK04IDZ5oe9Q)}mWrhwl9QUa?a3=}98R-l(SR@`15eB%UktW^r0+$AD&pvPETVLYV z9Xt8F{LY70t>W6Zw1;+c>(`epUD~%_d*{Nc4G%q=j}PSJqp`spUpadJ>fxbAtM%qp z+aA^4*f`j~WAos^&AYenIAI}~N1XKHSz{YQgc0@$RtOQA{KBCuSb-+hMFUKo5avn~ z1sRc6qgW3Gu1Gqg0M=0nV6A>Kdt82(yJ%zfK=6|G(*-FT_8H;^@~uqCOieJj3d*2qhb{q=VK;Dc|TD<)k(*1n>>tSyzE-MD)FZ5z6;pOxLB zt#xwi)}!Ci&S}SwJ#@l9O?#8`uMq$H!2f>ZyQ~V3tYC#IqP!23r^c0(S3_X?(1*w_ zAOxX(Iw^g6as>%oz)KJ!&f(?tMUObEfo$4>|(GF=p9GpAQCVw+F*v#j(+%mdti+1|H z-5WP7Y~S|KG29g6+lfesA%d^{h?n_L7o&Td@UTUrSgG?g1)W85j~Yd!pop8z4lhSu z$xJk?PAgSsC@D>0CQ~B?z7R~oft53Iba}kgliFC-ZIgY)@XF%H(vdP8Rs?5aWe0kHl`dboO zK^oY!GIl;|dDggdP}}C>^2DQGacT+Y>4^gO+O!>D;wL8ImtO_^m8drsu0mnLDZ{5Y5m zM$%#gaUruhG4Tu^oV|L=t#Pd=@rZMxNe6hVi<>5n?{jG}&npuzrA7s`B#$o}FMy95 z*cX|eyMheD4JihVYzU-^q>FVW^@LzP23b@TkX$0KhnND5t5kMiZ5%7F4?#WIbOPIi zdLoHMEME;lOvhZ2x@pADHkV=tY!O5q)L1=$u5&3<^9qg_LJ*3@rC3Xbj1XlAGC6@l zl!@Ve5NGHrS1cE);4V`{#3ZEVMT>4 zoc2qH^q}MZr?s`p?p=L-OQlnf{@sHsMsqp7QQPeP9sGqzWWl0sTOQWV{p8sk-;!E6 z(5NlldsWM-)s)`WWx{j>JP|;3Qo#wHP<&Z(WJtq1s{E)h5UfEFGF==B21_~47L8sf z@rE#+5$vovQ-o$CoXL%(+>m93e2{*_IS~|gL%%&DS<0MMKH!U~UKhB)?yey&V8~>s zwt$Ed3j{=<%?S}=9CH~<`;blAXM0Y1?^TEN)rud#VwOrcf24MFK^8!kAXE%`vch_%XcX*zDNS*? zN1`Xs|EqmsL+Q)<7wJR%;>+45=}~;g!(zz2ktQ+e-zme3^@If!20knj7Kpu*iR2Lg zlW7I>7EbNnj&|u4g7&vx#b#F|2ZTHsAUy?@&zzbWRxO3Z$qdSJk2lQS1wehro zE5<&rk3Js-kBSo%eu=Wo;1#ta^=aiYlyTj9$^83E|D_x66;7mN)hZb8l`!5boRw8% zZ}3Yk;ee=R7SOIX2p)I2lqm7|6_n$sz~KV2G7jxtN}JYW4D&gq2kbBWu43XQ+wpQ{9s~-U?jUSvxCOL1W@j(I}SdTz!RV7nDY{*Cr@UjEYb%JTY)%!E8bP z;u18Mw68-tVE?I9eQ;I!e6N)>F1iN9xwQ3Qv+t8kNeo4w=2y^%Ae%xqB%~~0V5W)fi*&DHLTO1_4N4_It_%ss|MWfD>dUMn>YM!v2H z-9GV{kpJ_y$W{`|501rU<-8_z4l#p-ap*M@{=u7=4u4Si6KVh)X#i|$;!;Sg=+Q$( z9zAzlrW;KZ6|18JS_PH2sfhrZWqRcZEF)nt>0z)a>PI-m5$Uf{&$m$Kvw=s$&HNZ& z`Lk~yy1sW{P&xUw_S_Rs@`_sM+T|l#b`A~SSmoux+PTe-ZPVsTZ}Aa+{Ps0P<>eRE z;;Oa3JjeI!+_Cw#)u=MaHvRqR(AMocK6l%i&w+065#$$;tFL8$_2JxGpix(ONQo^0 zg{nVfA{Om#2G2mX6W}QNgSCh_6uAY6<->a_0&qVlNp*c*g!9Mjt^gT}3@R9~OBgIf zw5nhzBkMtUJG$NPD*2>M*{}FxMTirD6Ru>0 z^$-*ZXeH=WrvVBixCx3zKrs#Wz?kfx#Siq2`v)Zv5u2wF&7pKJu%xXMe4o)ZW&vYaN)A9||^ylAG^(%EXWJ z2m40`SB`vrcE-AjzkTx1;q_~{hFCuFZxCb46UMh-AJmg2ML8Ow&`Q1l>aKKh$8>VX zK1}ZFS}&0st|LcQk;n}*i^yHuNR}3YkCPM!+SCZyQ$AZwKv0}C5{zV}+C_d}ty{6d zfOdiMk&jZ|WcEzq`p{C&qp%eWmy(br9$4{|G<4#@mpAXYnTl`z^9N77_#6AnPp|#t z(t-YOeih7cn4COq`<7p}XfGVu5kiFx@8bPh#jF+5djtL3_pcZ>f#;hpo;CayKHePm zudL7nOo*|?2c{^7gdaRs)8wP_kUZ%qg*U2E(;Jgjeo-fd2GvFieUqyUz9&0!uCB9Q zJMiP@bcOG9B2if!!amDEzb2zEgGVUBp#AiUmV|!V{7jO~z5B7-7;fyC{kcx+X z`_Pflb5XrM5HX#C(boj@sg0B?^Mey>$ns(wqj$(v@Oz>Hw^SjhXDW!{gdWj@I(m&D zxt|ov!QW)!u#_fja&1j@es@hVo9`IvTy|ae=GI&(U5z~{1x+B;)&+oT(CFU z-P|%Q&b7y0-nn)&iUgg%*|B3wXY-<1COKpSwz@ur|N z;ZJ6>ogKqZYg#r3S|Cniq;aEyeTEgvKntn}p@G!gSU`!7P%$t?MR{(8@LR}1gVRzD zo`Y-?6QWRuMh}#6;2MIeR3{*D6zZUgS1pJCf^h(;HXz810tVdlx_hP@ zsAyY_ZHWyTZSU)@i4E50vG-?{ohA#-ty?L~P$?|5SXzlrS|>;Zs!ntm4mJ&U6m)fhd}ET^;};_1CR40x>SeQq zN(%KH{G@#;>7E5qwaI05NqcP1o_%t3e=q9VOwub>sl)NCMV|QDqEB>nEp5tAkCTOy zx}d%HhnAuCj%=>YAnfZ2d16O}EuL&{UB70ZAt>NFYcc^Y2WF=f?4%{g&QdIw-3GH& z0927C7-*b+;?>{Rh;iBuW5zMA8Y|yN71YV*m*2RaP9q&^0mKgEK2;lvS1^J>9e+H`DVWJHLDc0?w`1DLX!nh=UN3=PMFP=rM+fP}+0~lbn>0p#!2BTDUU74opHv;4F+ur)J0K)??ya1z0fYB2C zgUE(n35FYBxW-RDUjFaz(i27m2oVId;G9FX_})h`*vWaXzu79FH(M;Q>l{iMY9p)P z2Y%T6a(w~~h5EcY4#bwvw@+Sx1>?E$(v?}_3J!Db5@IM>Vzo+Z9M2(lFm#$nc{9AK zh~ukT`zh_8v;$9S8+9zbBM*kof8Fq2303mJ^LJmu7i3WP9sgaggHgQMVvQat^;E&e zwG#`NvPGND(?&~$V1e2+IfTMg+xv@uBxpr)8$~f?{`VF4_{mudU#ZYfTSdvbtA>h0 zPd@ATg$<|6kvbVO$UaX}wU%3yGE}OUxsgY9(e^_(Rntu{wS1RVrc#aMu+<=A8WNDD zm)w;2>Elo9PEHL>FN9ag%a?SsqlC#zJD~{55O}q^_VQEO$_E|NHpdG*#XHVAq-gm! zv{8QWsB>bU<5}$q?HjM4st5&lw>HCn(3ZLbxhu6i9vt7{PljqrmReIDB{i#wA{|ur|ai!;9SfMfWWCnGmK@^IMape z5;Jl5DW-CrwZxEOBeT>XK`oXXzDdq1x#~RbkZ!Jc6g69|uUe&m?S5_ODci(1?8mh} z!B-QC)Fpt9YgPgDixRR`A}ArBf(+Wp@*+K=T3{`cCqT)%F@xPi1FQqZF**ljwVyvs zKqwqAuyNHKKykKZ9EI78iYZa%X0{N0(P^d{NIzOB9K&6T3_3P(Xl_rWk_-RNuybP5 z&>EY(^1_I8@_`i#&+pwPzoVV5;yVXFc$Y_9+D5X8IC~3qs!7#Nt->b6D1(A$q^)UOFg3_eX;r6(mm9{XM?#EsH=1145|oua=Szte}_s z{dwIVa)-e~Qs!@sXHoQrQ5Ep*)bjkvN%)A-;1Qz`DovAsL{%8zvxq4#1fNBu4lO3Y z$8`8)LbGHf_{{V@h9ODN<^ek%k&g#nVvMx-1gE^!T~tKi6T3w2(W~> z&upSptI4e2_tQW8>@eMj7={It0dSKMMgdYYU>^>);RS9a?jhzOHm3}@#~e2we<2m| zn*X%=MiW`o{MKu%&`dbRc5oVu|k4Iu~4>{HnN0_g64Dr!OU7=tQK4}cLLFoqBJYvLb?WZ zNzfWx$r1O8St_WboZ+P4HkI%rb%~iOS>Rl1RI8BeMRrl0K?TYQl;gWp*EGmgm0OL9 z`3|$vFicogY$^;B)d>5uP*$!7d2`NKlQRwzQqL6XWd%qUis(?C$k(>;C_%Xze%s$QEsaE`aewA!%z}IS;yJm~ z_(#-HMA+@D-~ir=UL$KE!w?csumD+P<-_n2c@F0p)eTeL(1>)MsC_L5MktNr#;{V5 zj3yX~iqEB593%!_GA2>l0vp>@4#CK)63K`G(Q$e8bvy zI(bjX<2dq&a#}n5!DG9h>V0_qKD#yOpOxvp|90cHfk048Z9A#`@;BO>+Npk{bbed! zlRxabx~01CX?UqkfY8y6+?YX(`kEhTD+}e z@zM3`Pj_~0XkXkJUVU)$4t{Ry#!cNlsnnbU4=fl6h0eztYC{gEWR>lszD#zB1r?W} z$UR@+SG{oi`e-C6Wgh+w;jR-jKP0zemO})U2s+iLfa+DEOd0y1l#^7!yO>x;=_^c! z5``SZ$+eJewE{I)dh&v)Q($<4Cp=#AHgulwi8^&N!WLjzBsPc#m?uUxq>%8=Z#?km zeV-omP8_%PEj`$k&kXtxzI{@A=M}!iaF|ynHfyH&FznH- ze&3vPysrm*n?@1i{U#rBch`S7&NQV_)L3U#KQx%0cSczgMP^`a3fNEph03BJV5@GV zP=gwrtoL>!(11!&J$w`uwIk>|#nZfI!Pppn%@IV9(vI&J9Y=lFb&U?+uqvD3JzLgx zBomx3c4|*@OL>3Rkn9iczhiAPg50rri&{4}=RUuB&4pKzqf~nt(2}x4Zb9w*C)kac z?_CIz5*ZR|b}Sqmf+|K13Xvj|+!U|Y^HN>u;%reLic-1a(vn6VPAjf62(_rh%e{?Y zXJsK`2r9!Ns+bmH2M!+qh;dZyTpJ(2l&og3`Es0Dh%^hFS*V-=F#Pwbg2~F+tyf=1 zJb$fA&4obTxSKLn&8|XSnEZdHc3lktC~rojF|W*aA&H57(yhAE*e`VzDyzgCsycTe z-aA0|47t>)(E3Ukv)vG#DHRnXt3jwmp?XtkmLb%JK0`O>Ge_fb^dSsa7F7(PGwJ2UQ*O7@ z<2QNjH5QN6ZjIlOL>Ya#V{l;o{l8f34&I${uV~-9W|ghAA?S2E%%*H+&w;Jm+7~_Z zLbhpbcg7;eLWbnVPxQ35-MMyfK(}E~>?Xg7{9~Bn6X4?{)%{>-rx^+E$`EA$$fzyY zh$T=01TNr6N)pFT0XfZ$$`pNEq3UK#CsL=-7-}j*0+q&|Qo077r9=!@52MOB`o^bFRr+6LTIkmgHi>nnAXDbfN+xoVyDL96c za-nS?7}C<)VZ(R7Fi}hl52U4(W#g@DCI%8T5dyJoZ0^GxGb<{%mtB@lfQT6wg(|8q z#&ihO8w$gQW8MgXhdMDzSX5X%`s699N5teQ1i;Z!0|1rgWfV-LdC0pc&B^eg%e+*C zC}u^*>zgZ1oN)5K(6~cenoKmePERhqsyUe$PD+1l8?33lur4uTJe*6!F=?jdQ&fJ4 zasGKN&7U?UFefK~3d91I|Im9!wW>&airMH$XI}27!%~TUB9Dnnn6g7zPP9ZuQ}3s% zTJNV}Y7;`yK)BOruAeNIEcG;VRvq&Jz?d+Mcf{pVLHmd}A3{9o53xx$KGDtC5B#r9h`HB;dI{08{+_s<3JM2IOb5VPFm3Lme zYr(9hrnI%Wd(*8e4oos#`v)H+MkJe`-<=s8$Y}f5`>#n4bVi~UQ)hsGqWcB1XFyG9 zi@;0z&#*#*C8nE|Fo0QR>Lfgu`H6a*uzmlB4*uyWPh4Vn@ol_6haBE|*wIbwi2i;x zFfBWo!z*bjzeX)L2gHtg4H!9?z$uA~GmALQ)EpRn_FKA*jHWa!qB_Ri{xmf^uQJap7G zalv|jQ|`viw-;>_zjhqVH!s^a_^=&H;5W^KnW_Wb`5d2_8STk6X%9~vP7L>_rM7|Q z7QSg>M5v0kp?ovHWuh-J3_ce4#F9CSV2L0;h~p&1il7g{uLVz4nDimxlA=X0=4ebr z9YO77Jfe6C)WqZ$QjH;9HIgMqhDzuP!qFd~nU4&aQ`T6R+8h!HK-V*9b zP&I*EG!4aRqcpG1AeU$(#ml{MsUU0CvkX ztXrek{-M~bP*EZes`}sr5qYA5JfwIXhA^H_p0qF==%pLXR9+yr!_=w+Q&jlk3+ZG>OvwF{-f4OFsMb6FQ@!##Z?UX}XG@O!;HVq}c2U`Nc zXoT;+_Z{uqN8ItaGic*md13AOLrDyF@SlK_C*%(BFJ>xD0X3<W zra6dGKXQ_>Mg~Qy#0+L))ruezJ${R9kiM?Mq+}x5l^pY!>=*&z z40})vXOzW!EZyK1(;Pu+(qc64(+|obq$NCF%5X|$77{+B?GXD>jKT#r(d?|tTG$}8 zl$f7O;)fDApE>FH{*z9AL$$X$jVwfK??_)NcU80JT^p3o@AHGH8tu0Sm-hAMh8mlB z`=V`HD!y94(~LTICv?mltg}?`qwB?(h;%4TX^J9>yjmVTfz zzDB*iJwjKI(WN9yDL?o6-~9L*J)-1tDi!1E41`b{s3^vzXWdW~Q)-FPGcpZ`ut0}1 zH3moj5hhm#BaMQtG+!uJa+hF>>CUvMpe}K;8RyJ(lLzLOYvZ7Aalf^voOs< zr#jEN`WEhq?BCbQms+)gymW9{L&oQ;X;wo6nX*f|FdVw0|mm{QE2uGl&L}4S#E0P;) zB-wQhE#d(XQQZ{$*vJ5ellChKtj(wQm8)>pN643EJk zoLKSRko1+z+HmcK-^LI-ne_P6J>@4kUoJHR2khdj(A80lGxvxYEe7)?D+M!HG15}- znb~Px#}%2k;_tsGV#Je~w}40p%=^Uezahe*S7zR@TG0Ph=1mXrQjMdFYGwkLGH^C2 zWL3`ncHF^VaO}{IAJ85)JT*9wow!~)HVgX;Lq5ZCu$~T!@vIz!H}MOKSA$}xn$oKd zRRdB-lh9NXG?;`AUqK|`cE1%B(9q0kKN4_3Jpp(2-~YERvSOJ9)mQdDCdrgvIlG*TYy7X5J9X&wnlnG3uO2`7lc_)7rk}?AQ%cbRyGYSB{`BQ4 z{d6fsXQvdMit@5c*}0Eo=IFdg$g%l0Ok|i$zflFjj@A3l4;!Jl?*(|*t6_gW{;IUjiHm4|Ho8Gbka zQg)y}tG&lfV*fR*FFkj45sDQ^vbL=n}QtV|?-pC4~)xa*} z^GC{W|DOK*s|QEH&Zf~qGyy^Z{)o`0iNxr{X?PqLJNyHvYZ z%64Bm*3nS~Q&GVLSmweiAquDRwU5Xj&CsFf3zdbT6w@J9Xl7|y76S*a?evFv#j@fO|d z!rEgP&s!yEJPC1NcStfv;*ufYIApb0%j*pp0T8%g1S= zTpO)xhjJ7YTG9bBmxLG(9Diickp@>Kk=Sq&0|FsNP*L$*ndd|KsM~|}4^YciAweA7 z!j~i5Q3~x?VNRg%wnpr7+2jB(57p%fu9%Ft`2OK`6hn!^s@(^p=sY5i5ZgH|fjXJJlW(p5QNNe>+`^R8~xz>?!8s(@A$gEb5(xcmTi>X30fRq_U-8E`u)=3zD$O{_a@Ix z`D*ns+epWH+s5pL=Y30&-H}H9hg9^ zjLWF|4S)`2=a|J@1I+qqi|p@MEuELXM{Cv6+O(5c;$tTB70YS(!SJe;!eXd;w`0)> zw-Sb;aE4$UqQ;7hL!mnqyIZLM_aTZo+mkc5N^X(x#MB-b`IyC38LN;avk{YfQr2e^ zCg~-|bF%es?tb~``q(Ymru4qXw2516nq*ldrBQS~klW4!x7xSZj&T3(`_kHW7ng$k z_SM?sdFK{qTHCi<<{6&`O%2pd){oS(3wq^dX_^xPgdbm>Rjl@lsZOF^Q#bCb0+{Oo zWl1)K&586GN^=EP*Nppwj`zdpa*WHW(}Kd0it&?|t4(OdyjqxJaFzatY7?2n!R~i} z3^lZs=G&;;M645h1qp~09Gq<`cU1T<5r)`TwZrky%Td&7=xWtPX)8F@!jmR@bh139 zGB-J^q{-aM5wQHTG8HU1rB|x_(eaxbu{zZFa1pniT_b?ys5h$l;g6 zYigz_E!88UH17>%PFl@kHzQ4+D1~PIr2xS^3jSPC#ZL1f)4KPGOb%8vAup9G7U7vv zT{LP*NbR>k2PcrmdBot4m1SvCuIilzj*HKl4US{#3}i09$4uU!ZQ88axT?flN*vth zLAiLC(R4Z#6l125ktWy8YQVxv$gGewEa4nBP9i@7JzE z@$f0_roG3GatoIpIo`SNodXxY`|v|Se@p(;Bb$y4ds>5;y&J!J;~<}MtIfCP>B2ob zP*L8`pV1mM1*OW<_!E3Udu{v_|Ncqtz5AUfH$3@-_RHRZo+VwW^tEqV4z!Fv^yG?> zK5N6hTQ-4z|KG2{C9}Bns-fK!5jlz+TfqJRuNI1gW>^Q{)h-~fHlnwuP#Y(HQo zyfO=e9oB77=D5bpp17zqpW;?pKzFy3$Vw4}u;GdX8o~<*qrpX`q$dVwvkzegy?{E+ zqm@2$oPvs!DKf|3`9^X5+IKW%-{8Q#cP}4Vy7a)#Q=c)m%r|amJdU9}vncLxN9<;| zzayV-T_`PRj#dYnGD*Ypy#Ir(TON8)%r=;HCdxy~A)!tW6grpPzzts30!wt|&D^tySf)p_8Be3a0kzI57WT zKWD!DT*z>ipD=kbCs<#54;9N|?LD7RVi+7~?Y(NUo2g{qBT52GN<~mb)QTh26R!`s zeeu-Q-6Qd(2`tI!>F#Pt#N~T)4Y{V4hHUK;6nMAhrX?Jm89_6z;T8U{$)SH2MGKQ_ z{^7e=*#uKjOW#FI1_rynz7sfMa?L+IFigo~vlriUyFFO;FOl&2qsh+h5jja;%O+Fy z4xY&_%4S;9%`>x@)*>;$E4?Y;O(AoSiT%p-ER|^spc}3LnwFErT-V}My({U?js&p1(RuwESti%F~HkG}EV8D0@!BjJkKua-p8Ab<+6;&T<07(CYppXdt1q-GA z3BHdfLG0sCe+*@Kh0Yya-L3I>iDBf_$5N|Da7R{{swV=Oz;DgguFsNUpQTb*M^LX?M5u zZ?xV!Sp67&3Vw^(9_jC}*7-Wtg?MUS)3|>Feh$Ml7BsZ5 zMc60sp7nCw#p5Ovni;QWLE{3}Y`UKLj9wO!Uu7YjZ#EV1y_fL&2`KtcxMpD?^J^gF z^|+4v0Z`@^TUR}VsYx10T~)oKTOQhUaAF)ehojH{$oGJejx~nd+52zpUl`WxMi4Vg`XG9 z9Kz2F9Ic@H=^b{|ZGg|2@f$2{{5nfp{tCa(QoR+{+1CHQ$j12>rA~un_?>K%+vLv} zry6^V_ZW{Ee{Qmx22A&xE|@=Ke%vzG@{r|C>lEu(t-mOrReneL{pD}jj@ZB8FgXrY z$Q5@~yyINuyx_XSb;R|r?iKDMo_U^!yw%>nskBvorK-QG=nMNkUtLrEjDL~;e*c*o zQ%!r#gEgmWeiiuZz}vN5wZ9K`1&>WR8mbB18TtwS4+{50W=4+Ind-h=_d#@IsyuaI z>Mvq#v0v7IDejIB#eWq4)wG^z_e{GdF+cJ9=?9aY$=hah%vkpq{=PD!IOEKWcT$d2 zXX|oi6#AQrm#`zm(cCtCfFKY9q~JluB(Avl(kj?H{w-jrD9HTZ`z{ zW_Aa=h26|nu{&89qw!6sg9w9KGg$_|8EChOJC>t{HH`k=f;(;%chVDg;tg{_;WVDe zuq?K-QCE2j?j6P36L{XrHsSbAyg!VJ!IfC;Y$e|J8EjYJOgrwnU3|HlEyvoYD{-zH z+l@F!R~qoVm34`)uEa4tP0wvadj;BepiS?l=N6zZ%h5ai=)=zt981Oh8}KfAGr`yZ z2npuNH`IOPOLc&t8Q~F_0VQZu4kKTh@&<+FYTi9))H!Im)=*vp< ziS8tfgmJtU?^uUBZo!*w7uUmA;AgTQAMQKhVA)4{uokQ0-HCVT?_FMcH&HW;J4dno z_;-cz%$)+KVcZiIRKHBaP3X%eU}gCgm~6oN?f`$JMXw1WoeCRprVH!CEo4`NV%KAv zDq)l;_DS4Dc-SDYatB)LfvsEcCY^pGqNns79UC{{nCX{Y zOk#5r&J$-54sI5lxQh%R@EJ4euiJ4^5RIR5!C*6Axm(ZBQtoriR( zr~~g^k9)7hwhsF?aZE?V|DVG7KGwmO04sgC`YBxP#oq$ptq<3(7SArh8T`Lia91}* zQhMTA(WdtjdOF-idMq$0W}Y4vCwt$DJfG ze~Cv(cInB<>l^UamEhJ}1b^RzD?}s0`bI$?de_Z(hNSuqoYyH57Ej%QuM;LG<&JPl z&#pi}NDk;KaWc`7a7x$zXX-Qn1DCY~=@se=_392#e94}KGO#onUrn)jU=(lML%!Gmu|=J4$=QBIeQXwm-XBAmn49=`8Isznh!lq7;k{4 zFWFq$AMoan`*$}^(s*IO$YI2?V)$R$V5Wx^ks_?_gq&>!){1apZ2%AE99Cjf@gaxd z$Lx^+B1yD*LunD&o-fi0Hv;!b$>3@KXvsNDIy12wgr4m1?sQ8=r%fUgts& z&clk%SFu($ADq|*PF)1fZ3h$`;KxpIWf!deZt!XkJe*$eSReR(DY(5KTsy#SV1t;S zI)tj_VYVEZ*e|f1STkrhq9VK4eO%^7ZbG`!f~cAmc{Urja|f^BPVPeNb`1YVx$j}! zi$~c1;co6>U*uk1$*ZtPVYPYV?YGRFxnO2#zlipYGiQop>@%g~#?n4p+RrNOn@aoH zrG2inpHtfBOZx@-K2v&cCSx7F<>rldtX#2t!z%q!W4pd@68o!5U1)F9yMVpu0`{T{ z*w11o_{E(HzmF|eEIs`y=U-Z;w$uMxOL5|V%;oJxE>}QHGe6?aoF5-$w KZ!A;1$^QodDA0KT literal 0 HcmV?d00001 diff --git a/docs/source/_themes/finat/static/neuton-fontfacekit/Neuton-webfont.woff b/docs/source/_themes/finat/static/neuton-fontfacekit/Neuton-webfont.woff new file mode 100755 index 0000000000000000000000000000000000000000..d3d61a7c4fac39d807cd2619004ae88645d15ac7 GIT binary patch literal 18860 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 literal 0 HcmV?d00001 diff --git a/docs/source/_themes/finat/static/neuton-fontfacekit/demo.html b/docs/source/_themes/finat/static/neuton-fontfacekit/demo.html new file mode 100755 index 000000000..b207dc284 --- /dev/null +++ b/docs/source/_themes/finat/static/neuton-fontfacekit/demo.html @@ -0,0 +1,33 @@ + + + + + + + Font Face Demo + + + + + +
+

Font-face Demo for the Neuton Font

+ + + +

Neuton Regular - Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

+ +
+ + diff --git a/docs/source/_themes/finat/static/neuton-fontfacekit/stylesheet.css b/docs/source/_themes/finat/static/neuton-fontfacekit/stylesheet.css new file mode 100755 index 000000000..ef4859e68 --- /dev/null +++ b/docs/source/_themes/finat/static/neuton-fontfacekit/stylesheet.css @@ -0,0 +1,16 @@ +/* Generated by Font Squirrel (http://www.fontsquirrel.com) on June 12, 2011 05:33:46 AM America/New_York */ + + + +@font-face { + font-family: 'NeutonRegular'; + src: url('Neuton-webfont.eot'); + src: url('Neuton-webfont.eot?#iefix') format('embedded-opentype'), + url('Neuton-webfont.woff') format('woff'), + url('Neuton-webfont.ttf') format('truetype'), + url('Neuton-webfont.svg#NeutonRegular') format('svg'); + font-weight: normal; + font-style: normal; + +} + diff --git a/docs/source/_themes/finat/static/nobile-fontfacekit/SIL Open Font License 1.1.txt b/docs/source/_themes/finat/static/nobile-fontfacekit/SIL Open Font License 1.1.txt new file mode 100755 index 000000000..e4b0c4ff5 --- /dev/null +++ b/docs/source/_themes/finat/static/nobile-fontfacekit/SIL Open Font License 1.1.txt @@ -0,0 +1,91 @@ +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/docs/source/_themes/finat/static/nobile-fontfacekit/demo.html b/docs/source/_themes/finat/static/nobile-fontfacekit/demo.html new file mode 100755 index 000000000..983298770 --- /dev/null +++ b/docs/source/_themes/finat/static/nobile-fontfacekit/demo.html @@ -0,0 +1,48 @@ + + + + + + + Font Face Demo + + + + + +
+

Font-face Demo for the Nobile Font

+ + + +

Nobile Regular - Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

+ + + +

Nobile Italic - Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

+ + + +

Nobile Bold - Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

+ + + +

Nobile Bold Italic - Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

+ +
+ + diff --git a/docs/source/_themes/finat/static/nobile-fontfacekit/nobile-webfont.eot b/docs/source/_themes/finat/static/nobile-fontfacekit/nobile-webfont.eot new file mode 100755 index 0000000000000000000000000000000000000000..4c06b605e25b1d8539a2e652d2c7b14caafd1d43 GIT binary patch literal 29592 zcmc${4R~ACl`eet(b17C%kq&dTb5;6l4V&@WLc3FMUfrHKXDx6IL0`}b&N5IaZE@_ zoDf0?o{+j}nuatDffh0uhB7phVYm!)B)gRHP-+MiD5dldxJ)kt+z#!e(3bY=o!dYx z-*@dJJ4t_@JKsFt^L&o12dJDuA#3*ExFFe2P6+#+nnIg8MX&zpq3INl=QQMlfZGq(zR z&}LlNDR_mLP%A9suLhw<*oZT9botqBXnC7(BX58E;vKa@Tv&;3EWhHJ-amRGnoy?| zY$QQwZ|~bMxa(_;IBBMv*R`!}7oMamYzdCNS8t5go$4|raomC9%{T7Yvg@n8^36DY z6z?D2c>8T$<*kA*3xe)1aJ_kK*G)UvboB3V{4^lA^`NVfN@jbZy;jZyLw|)9E z|LrP4*oFSF-|f0}>#pHf{&~M3+)C}6@g`^pFJ{$u==0^37b3aOW&DZ-N2PQIx^SHx{mXGk3|A3Z)SMip5%m1Jyo1dTm>HKr^ z&(6=ze{+6j{_F2O!?*lOQB-571@hR9Hq`OSGl{w`YhtNcd9~-Dip0( z{XGL}=k1$R!QWJ>%He^=P5j(}P2L}?tf;m$qOuzAzo_PLL=|h+_YSP}Z}LS{UCqs< zUbVSrz^68Eil|Zz-RkrE?i_gA_39=U+6~OPe!0oz_o@2ufZBfhCVpYlCVZd1reJ6= zqRKVtO7;aj-TQ^1A(txPYYa8%Dt@YY@szQ~X7#3`5jC&Idw?GDJigzn>H=5!y{c5* zsR}&18%wseY-Z-H}nt93Tyn{H*yd6dk5BUboo@aY2a=Q=qmr+e(&8^-R<8( zJ)z;yminU*8f-v=l?FgNwm%pEI_hutZ`r>2@{!Q3MKu`2-A4#)oumG{4XU?iV3lhY zSL`)YLNjY_ZDs49QXmQVmKtA+o_p`^9q^B!_x@HFKA681kZ4T$W>;BWI8$8W#2Hl=%_tQf zYAI_i^GpTQQr1yR*^n1jjp(H}n)b1D2GPBNT^CYFVvntX0dDw9{}wH7aR? z;lfPQU;X}P{9;M1Ma`epr;802EUI}{k9JdHnV~2=rQ=(fwyIN^%5xgAQ=&|nO7gK& ztV|UYo6$_+TPxq%X-jP!ybbOu<#*vbT?^k+zVJP6YBN=_(9%QabwWDdTwBYUOGVuR z?^mps^ZnXC^QShKfuw+fq(2caitG5h*l&mz`*r>zdM9z|{dCzAPkWA4JncU2dH8Wx z`kS8Pz8T+_e5apueM5{*ynTQ#!_s_u9?s6VeRugiFYga)q%Xoy8sTRbh}d>+sfTR;y}8#o2k$iga6P>qsa!onHZ zvkX|AuIF@|G1>_GYQ2(9g1Sm&Atr^>t2jQ@>N1wD@<(45(*COG%Y@+%4xJk&BwAFz zqE^nTRf-yzo%wa^a}9J>QkMX?OJ}9@vOM56I;$>Krb0`X6ooU`5@+zs7OO2Ke7!QT zBotky0hUcw20~HV2Si!AG-k=Fs%%yDm`_E^lPmB7$(J}P z#9~8r(CR30loZ89;^-?42FXPua2GBt2j-; zp_hHxCVxrr)qP`UT2IEt`tSa9_SHxIzOk+cF=a<4T0(ntkMyP*-R-7Rr=_4|l641t zj_m&Y!P{E<`%+_{Ha`52xg>T^&}C*%y1Q7*M3p~S%x-ll&` za0-4QD6~Q?en!aD5e|d#>GgV{ie`CKT^pbFX$R%e=~8VkfN6C#M_rjLxX=z$sfvZu zZlK7now0LdPOs69)+p&}TwjOltN598&m|NFt7?J5s$lKQEFGw({pvt1p;KfW;_fo5 zE#oLPr3mJx^?>=3RcqD(=IL@D4(vixGY$ZH091R$>o^jChy)<(l^u%LYB$K0!RjLB z$YYD`l2^2=GN56+j#~QBbiinI*aMaI$*a4PjlO{0VU+v&v#&pXnC*|nz8;JHLVRp; zuAhB1FR$2MS`ujis^h`81I24OY(Ns86p9n8;xm@n*a3WRfTl3D1 zW3PUjg|n}JZC!i&x{IIFPp|oSBl}{iCYDTuf^HWpa|c6-WGr0&&d3=RObzgsNnn$1#4OtqOFa?Brz}(hcTSEwwX$wHpj{3q=`fHrb_qi@VZutdhGS*s&Fj8*k2c9_6xBGZ zh81=F>`Y1K`H*&YqoQ6roA%{jP(8CVZ*KhF12~-zHajI5Yrx+Ov`zawMs>Y15d4$qls^uX`8{NOA|GNbC|DcQ%a4bR6|OYQbKcqWc2td zmqwbJ*KfSma0PQ@&1Smzr>=2IyS^2aW=%A5O54(i1Hn>-gWR;P6){cmN-SRyuhS^a zV8^tU@nS0yaS%=+a~OhjjF;aVtWJW~;@%{ai8wt>qt6OP`@|-+7iiR9DI4zV>wQ$T zm_q?)d9nTb-x}!ai6$IQR$d>iUEbEfKC+xCuo%tn_5aA-*xKE?rLQOZ`_uP-V{p^= z*zm~Q^#8ieU^F`IMti{h z`=4^Tmc~}5IvaZ1-Q~qDTXy2pUHD%|?ysG10co%TZWzT5uwvChs1+3U;Mw@QlL z=77^+&r=MNNs=YWR%9x(Ih-zkQXymF+D(|#a z8n%Mf%!Sz$jzyBO(700Xj)%OyV!Pex^i+rZL0_MzGm`MfTz&^|P{bH~BMJCC&WcXS7%B|A!5S2nc`bq(z!=L8dm=Hp_!+ypxl{j~9ZhN5cT z8|e!4b1CnqV>Q#zoLHXgQ8LG+h=L#&7CJxV?+yj3eBe2a!LTpr?R8f8!_lxW7A$eN z+@W|R8TB=exJTADZt35%<1-V#czj(?DC)k&=JM4hI+NoA2PY3ca^%GIyIXpqKCYuO z!KsVtqOwP@3eO5^K~yb_tJbKR&vh$OPDwwqMOEdEv>7wL=+cZYRA|O4g~XF(1BqCJ z%tb60x{{90=%gsF7MQ7}xq$NqJ=DM{QCEQ3Tc}JK3rSOwl&O3PTF^}L{&zb#E5IcS zT}l@kjC6yAZqTa=R8~bMl}EMIvT4znZ?W>}yP(PVbV(O;=zPV#B11k47CBk+p_`5! zyXhg8%)av2O@DcM>@jKo5L4>2zhp)A*^h>@zfQ5D>@QQy3S6BPp3t4u{Zz;ou7z&^ zHc~vzz$^82P}fN#5lm1mnLxWtmj`_s>aFPwRjebOmV}&Cm&r5H&!8Gow)ARslW$o9TS*+4)g=N;=;FwOvkWrb4k@HUvTci9M`z-_CePg_{LF9{(%P zuruIq0(s(jhuKA4SUe4hQAuN_0n?koB1j7m0eGAN6A^|8R~3E5hsCq(;zJMdd*?r5 zTZGqz0-;gJ@}=h{ri{k01@ziPkLXPfyT2Q8c)VW0<#uG z&>Bq}2`pXwSyeEUs+9CfJko}FyaAsptDyC{jHH3I)?(KtEJ_7nU_lxz=y}BaE4ir9 zB*352N;Kx=uUwe_djf$q9ou&wIXt?3RnwAyqLkQgZ0Z~s+I{%t#-=4p3N15E=aO(N zu`C`PXm48@^tep;bGn1kmd^gcR-AE{=b6OrBjdZT84O25;ik6Uo{jyTBU^?ZPBgkb z{mpIH^!If3-LYvPo^X3irtfjQp3Jax+ULM&@FsMy2qp^!2kcdW=*G;0853KkkPb*5Cv(VyrEj_ zxXl^T6d50Eix5pp8IzJMT?h&?3YMJy-Vy2x*Zzah{#gOEe{q%*(j`VmP2KOjIHq-5 zQPiSYFfrAF&U4OG1QzC7{c;bnuqm5@!wj~Ulo8=cCR&PM66914_H{a3k~<{*LP!@b z0qw{>qMdkxGyu|1i3SQ;5OQL*cm*YqWYGJHC)fk`)y3AeA4zny`KwmQ1>V5wLk*+t z9np~AWih$R+uQHVo)`O0vhKS!bPbHHOj<3rqP?=wYJ-@wS9#*WWU#Tfwe9Mj;rC8) zzOa2hrTdZeq_9Ewj-Xz}i63)K72~Qqs+LC6RRrU1j_L}aIv?*QObWaA4eN9**Ag@K zS^~Ab0Q~RjOH|;lpt)G#)~KM|`0affI4cy@JFB`CwOj)`)2Kz8vok;4^6lrecA%CG zvno?m-K>5}WIB=tQ{~j2h3q7LA^ z47{W$tZ?MiHkzn1S!A+*K_$CP5<~e9ZeL;aoBYE=J;(3Ks_jdc4=2`ke7WPQepfK& zE@^1pd`;W3NJrzcNQEcj30xHnITFrrIN=UNT&~*4>W*YrYV4-*C1ZyrtE4!c?BMOc z51hz1*KKOs6HgS`Ek%bdc@B$%k;)eIH#J_oQ^iAxM;fATVU2 z?&a_PLGY70d{#K2ck5Xx^lr7tI%O^Khk;%pU19~YS*bIK zBh_zJD=>*1Ne`=F^v-G5ac|LbnovP5hzaF1qX@TT(gmxJ#0`pU&a0B#MvJe`60-SO zBYW`p?wNM+S94!;x>9XTuTC5|H2Lqt17o%EJ@<-vqAXcN6ToPeBnYAV|8A&FyyJoF zUK_mc69>NY+JOm&%Vr0D_Rfz=Bl@6F1=)VBkXb>pMW;WXap|yArawPz3F}-1;k1Qd z(ah&o0CNh~RxM32jJ^e*wE?Sb+M-ijWqi=#6=Z}cwVDy42n&{NkdKnnG}kVJ3tFV? z;CgYqZc()i@`2Of_cHPK`!^3aHB|c8$8Wv8&sGy?N_O<5n%cuVc5WLSZf*(%Gjd?_ zw!IUNDt2F>wP~PdRg*d@vAvFvZNF9n0`t(%6nypp|VsIR@PAsj1qC?{RIk|?)Q-myQIE76xZa*$@w11BE1TEvQZ$RE=SWMfdUdm|HkE# zMA8!v(MuW+slW66c_Lc9s#jEb)&hMJ%mLa#ElcaeP^uj$syUs7sr1!cmvL7RZW)fE`x=&LGfdBn94XhF+jvV@=aJwcVDka1gI2 zD^TN18pkKCExDR$+)0Ab!WkD(NXT0tSW*n2MOYW@94;Fwm`2Ri4+Z>d8?WoW<+VeT z?X7n;_`<<(OH5_bRo9XYLJkZ-)VD`8hQUhm?*lcchDB^cDv@VH5 zmFOGTyMOxp=FawIVV8q#zTuxw?HCI=Y|iTNr@PhR&9jepG^DOBwK+m#J0_103=Rz* zYYj*Jew*!xt|Ji+l{pnM1kDcTNX!1N?uUs-?VTUS9PmTiT#dkO#zIgo1eC+X63PK( zT|;3x<^bjeQ=9l4@B^HFC7l2`otOj7_)PdQ2Sll`+@j5a226ax>6dUsUY-(fa*^5% zj>S2UPz0_`S(r8juH^O_G%*K0W|;|FO|UWEH4x*3KRP_J3WT4{{(kT1=EjCr6{PQP zBhqiJ;G{46N4D)f(A%d=zB+eusIRjnS(~sqlzUzAXm8)p$mb^?U?#RI6l`mKYPc=l z7KsLk^aGsq8~X!+XsqXIz-Aiy)H&T|P@_Re@E((vszIYMO$QKF`lYj*?U6A`;g#+7QhG$i? zF;vUq-W6ts6$r>D<^7M$3>_Z(ICJeAhpFM3WX?rv!`HId1=#N0LW9&Q{T4oey98CQ z>F}`r#Wyf2GWi!&_$$=*nedkH|5j50ApC_{Ris(YH8f%bLYkNY$*>K> zqY00G|99G`xtdG^lC)9lL^82O7}|W)Gv9w}ant|}n}uzNrb3!YG-}%BMjgmw$vj{| z49>kZD&8}<>oN8#_QCkv*}1P#A4OsJ{F}P-@X@RmI)uLwGV3r$r(5-c9hMz=TBeul zg=Q%{wR)Y&2ztCKss`UklSR~4M@A8;fta}113TpAQKzO{6i=UfMH>q6AApC>!T!J|HU3Al z`LY*Ol*MKVE*W!Ii;I@8AW0}!R_~V0mdKLYKv$xxucQ4g_=C(Ai><<45pqW>?5hId zHuLo6!5vktp|IU?-&dazDC9-UsL9rQ%gCZfrEX%0_+O0#x8rKFd`fa8mu=T(9%-%=XlGHCCBywyv-D10-uAWs_ zD^ty@Ti~@hrc50@_668DIJ{)zWN?lLNLDwueBtOZ!YireILaMx+5Eow&r`(ijCL0>>FS@~!z z-rc=tY;xk@u|0c#u&y&4^0=3UTbq_Q#5|=A1sY*qvC|V=THDgL_K6!dM?ziQW8=4m znqnX!eIEAFdp%9AQ9O#DT5DEvcH2 zMKPItc9ZYl#aJx4Bnn;4!M1Jx`q0==Pj5JCRE#_3Mqd5!;MnHYhETw2iO0Tq^u%M? z^AA4EWRqyhx0sYXlSR&xWTP}FZjQ%TFx1*UII!!MJ-=h#Ky&-f@dtV(*&vON-bA#E zTpayr@D2m4kZ))*ETfYQh{0;{VG*%dBaDf#n#ZHE6hqR(Jc0WP4o2c+tKRs~LxiUa zpgWU-d4TY1;RWobS>c75b4zoVrCL?2r69Sk=tA0HGD0AvO+^>T&ZjH#u`y9%HKO$F#9Z6NgW{dp zb8`5?>+B%r@%y^hNN;=r;|*ZEK3Fy_!nT|*q7oX72v(VHfkqQsCWz2zVl+Wpxsa^{ zi;UH&m0B3Zfunk|o6TD6at+vKeI+JEF($(@?4%uG&INHoZLz6lFg>^tuP{_Kf|n>V zrM>*Y1w(*{-QY|3a^$Viewe+$wmmknr?+cK$Z9bc+5BLt*RfN9Kx4!8y|;|q_1=5; z_m9@MmAiNE$!0%h;$;6oC{n0|LOZwr@G&`TQ{Xg+HN<;D?O`~}T~13hduZ#1u2|IL zvY1a4D!yQ2!`4iD2b(Oi1;e3O^KdNga&Z|bOwPY4y(xDJpB58B<_6+!4d8Aqbr}nR zY^nbI)W#buBwB9a(25bXlyo1!vO^;g^9Rpfqe+rRMeUePUvIylUZ+XLjeVpG^lj91 z0q2b`G;0kJa)R5SPf<5%?MfXaJsqXFsBrnYNv&O}qWWf4hoahNRl71(Vt0Uee9Dy5 zR|;Ax<69Tsmh-KfZ!7rL!?#}A>eWVNs->|59wGA&p6$YS=TEiL;nW(wZR6W^zFkM# z8Ato`Bh*aYpiFgd*oc-_^KB2`Uc_2h0rnsb+Zd{P~i$Z`b zTW<7VGI*FF$`F97W&VZ04zzvZ?vBNn56y|XWZh~J|HS2W@g`AY@_gDXx@3vFPdZ(d z!Bo7tt+6xJ9cfsZa7#@gQ-5SV;!Y;n

cR{O6;}4res&!pwldu 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 GRs07veul9C literal 0 HcmV?d00001 diff --git a/docs/source/_themes/finat/static/nobile-fontfacekit/nobile-webfont.svg b/docs/source/_themes/finat/static/nobile-fontfacekit/nobile-webfont.svg new file mode 100755 index 000000000..2ef6265ad --- /dev/null +++ b/docs/source/_themes/finat/static/nobile-fontfacekit/nobile-webfont.svg @@ -0,0 +1,149 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 20072010 by vernon adams All rights reserved +Foundry : vernon adams + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/source/_themes/finat/static/nobile-fontfacekit/nobile-webfont.ttf b/docs/source/_themes/finat/static/nobile-fontfacekit/nobile-webfont.ttf new file mode 100755 index 0000000000000000000000000000000000000000..1a607e95af6fca159f8f20282619f0f9a4d71d51 GIT binary patch literal 29360 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 literal 0 HcmV?d00001 diff --git a/docs/source/_themes/finat/static/nobile-fontfacekit/nobile-webfont.woff b/docs/source/_themes/finat/static/nobile-fontfacekit/nobile-webfont.woff new file mode 100755 index 0000000000000000000000000000000000000000..cf6959b4dcbb57837166bc775a2827fdc2267a21 GIT binary patch literal 19800 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 T11m%Uum&JSumdYZS+EorvVg60hHP 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 FzW^$a0A&CG literal 0 HcmV?d00001 diff --git a/docs/source/_themes/finat/static/nobile-fontfacekit/nobile_bold-webfont.svg b/docs/source/_themes/finat/static/nobile-fontfacekit/nobile_bold-webfont.svg new file mode 100755 index 000000000..cf5bab40c --- /dev/null +++ b/docs/source/_themes/finat/static/nobile-fontfacekit/nobile_bold-webfont.svg @@ -0,0 +1,148 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 200710 by vernon adams All rights reserved + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/source/_themes/finat/static/nobile-fontfacekit/nobile_bold-webfont.ttf b/docs/source/_themes/finat/static/nobile-fontfacekit/nobile_bold-webfont.ttf new file mode 100755 index 0000000000000000000000000000000000000000..07aa902a4437c2b1dbd08658b70458043b7d53e2 GIT binary patch literal 26660 zcmbV#4`5r>dG9$_S68wu%U7~wS(ar-Rh7 z%1L0|_TIB1-T(KT^L^j>{(mQg5<+DBh=g=@4qQHb)AuR~p;nx&THdj&lRU<*(D^tH zTya&R;oh0OTX5WkW4ofWc**yzH@5LZ8y>H3nPSF`@e91 zV8fcJwe$Y9{p*BGm~s8LH*K80<%Tc(?FvHH{vO94-gNWYn^yn)2U`ib?oHg^hA)E# z`LU+F%C@H#o~bHqtN9fhjw;w0BH);kuiy*l;0Qw$|3)}Yj6^i1jQ8P1etm0rkxU8> zc8l4-9^`+#mAQkPw%$BV0wi!&lqR)FLW(vDkKr1>N8BaFupi(@?0NRB(yx)9L= zW&c$JD+fO}v}*Y3k!wa*kFB}5wf|)Q+6lmttee~k3fKSE&h1>h-9FBbZrl17w|{;c z-~A#XcYgWeTmJmMwjR@NmoX;Aq~p{=N6G87o|cohG59~y4x#}v&(k`(kf!MMw2QXV z5ps@7w3{v^?~*5J1+5_}d4d4>bqXoFOsVM`Qo3(lrI1i-g(Ag!*O~y^S?CQ0S9$N^NB#N{YtW+i zPWH+{G*Vm__yr%1wJwlQOtF!nKwx>ObItm|P+($Qe+9d*gtd&O19t_M-_^M$bXVZ6 z5N|G|kp3a14R>OEtXZXPEjz$7Hr`Up>55>`8#sLzz<`HVpl>S+eMA3nTWMV=aJtZV zC@^%{Ro<+2b;~mhfM8^GftQa996O zXdJx{wR^Flp>{yNt$!#-0_4(b+jBHPu~7ooDxS4fyr+1ht4_gLzy&{XorDE&7y-P) zfEn9?X**#Y#J-6vB00?eSYDKfRg5X7L>^NddsHze6mmK*nn)4Oi?V8@vAlsFsYNtK z79>;lpgkP42Rnp-7NYlQlg7bMj&z7WM}q>{Kl`E3Zu|so*hv$~*;pGUi4?OpI}(a` zTD6$oIcl(o=2%|P-&#gvs$j4?RFf^6#rw;GhLEALDb-N!a+;)2O=RAF`gc>$ciqrG zus)q0xT>+q_{rH9wYU1Fr~CS|nM{_o+iM7m1iV)yMXX(HV~R0F6$@*#(0|1svmxZo zdb`DFw}`b%jfHlLX@~~#(@P&(-24iCj6T%7}Z>0 zDvIUI#a5i5N+6*IX-sLVJl1;sp9Y94W>G5Z6?45(DXSjy8O1ECCi9tNty6!xm0v8c zSFH6)xvaWy(JHIO*i_0#g-TOd?5KhNO6*th$VZDjX6%&9M=j;W*m226OI%jmCi7oA z|8=roc8{CigQqI^Q+TI$?mbm=@A0vFj#kgz>1XE+Z54uHP7=uWi={yQfAXiaRWih< zQ<+dYRhBaFuPbCqxk83e8T)2%=}sm1+*^xZ&pwlWD|_Gf1JC@X<;}LAwZ7T*@l*b% z>BIN^>0bJX*3Z88{^>q#fIiH=_x*|C%s~2Pr^Ht!g)AXgl41HalCv-hDM_g-q<56c zhMZ*uLxiQcD5lgU)Mn|OTwOCeQy0S-M}jI>vuUBWn%+@bWpy!)Nk(E^^)#XKJlJp zHd`qqxx2zwy|`t0*Q()9qmZ^)^vl&P%U4~v1cXwxmDn9QNck+Lm)cy1gZa@$94HR^ zQIc-$uXSgY6%M5*3)DAen)FX9or2m>?lvTZc?mi%GB09xS5urQH-Q_NT%j~OH!mTS zIoZjKJzPuqp1a&n?KQZ?Moki3Leq`7i}v}v4au&)j?vx?dpB+A7%Zu72$y$wFG<<& z@7fhl_`LMaT_0@dn(A5U?oA8rm$lwDwN!gs`-t|4g3aP}E*Lg1ao76777Jb0J6i4Q z3yOP3d%99df5>9nw8iQV)@Yh&k*y|&RAd&k!;8EtlgY03zK`zMQFhY?(QWga{pA8R z3)SABpVDKSH&DqYTU@f;>Il)aB$QaHeBN`Nj^UoA^>M!q8tI|g1E!ay-6TPlkrm`N z@>TL{lDmO%((MxQim_ZL<2W`DaQ;$>IB}wj5%Y>fe$UViwvt$W4?nByk(HXjX?uPl ze@a;nD&EVP`vMaw1#@2@tDWG1Am)69ex^cJyK%IY)3mZxR=aS0C(exX>+08ZGF~+J z*7rH@>6FzK#b=Z*SzTUy#-J`=QC|@yljH88}o599y(Cju#O;gcyKJk^kC0-Eb-WL~I7Dx>ZqdY+E{ z%$pctFmZC>lW~@VxY%nVpceiC1s$?l-OgapADox5$Gu}+{k3tQTee!veor#KtY^40 zm5BQzA=y!4HcM|8Tg#n3Z>T30Ew;wJ{+iz2Lk9B3xsT~T>aVE|h0-p(W_f2K?e^F# zc#U^>dCw4gOE~0nI!dgP_?D;KlMLe>)M|CPEAf^{R9xTR-qH}O4Y{0F*&urTwef}V zWtp~Ad$Kc=jC+05X7xE@;ZQi{P1i2$jl?TG7E3z0vnvu$n=O-{ye%E>KeJk}^(}2} zz->;qMHa+LZ!DQiFG+NzmnJ%!udejrCGJRYUN{yHdM@o;<@L$wBQ!f)i93cnTSh_skegCRxSofloWwlHrkK6Q#(a#+aDukzS!lH8*w9NB1udrh|`^IN+hGpMXO{Bhn6_q+g{kY`-XLk zo2$GX?W^fh8njtUt<-YhcdqeMoA-@QkM~`5OHGhw`+6Vi$;KBaJbtSv34*q?BM^y& z%3ZsRlG$n&OGS^(>=G=3)NG2|{7kJ#4*r1V`j>R9jD`K4YQbi-Mk0gRmZo(F`=gP* z-iguSf$px5_8uLRroO#*apxP4{iJ+6=5ffi;SGJgw{L7|rbUv&R#I9dh=PIcbd<_L zZ#*iz=(Rb@JQm4`CMCOo&NvNDv%}?X3HL<8ux=>Rujofyzw+wZ6;+HEsaC4QRl~9i z+LNv}LH1IzLwimB-HWz5(UyX? z+`O%dgyK4_7NISV-oMOeIZLF;AxGwQerNdV?&Yna&|H%{5A_cikK?sNyS9Dv{s#`u zHF!JyHvRBvrs|@tjPQ;jC#{2~`zBFJ5{f;c6m!K7ELkx(C=wVhbY9bG#oC}k%Y|U) zqP|p4K-9~niBnXSOib*XBrf*(Rnfq%8bo%r#LBK(OZZhI^w*<;p#-8mv-Z_LFww5a z@=>EKmBmz9GDBHEY80Wo8I@A`sJWEsd^lCiP94kK{#q4(0+(#;l3HpqvlrUf3ythW zcJ?9zQH{3xditnfG#A_Kxf}vndD3;TT(2y_Uyg?Bgazz$)xriysmLj%mAw@B@M8_%&lDW?`bu`Hq=A_UKwV)2;N6lMZB3k<>(0YgYXNFR*)J=s*u*V{4{iPD4GJss=A{+gg{b>AF{#=Fq| z{X&c3F+&G9s++-q0fBv~A|{xX#WY~N_kKgA&_YcbymEL3t_u1#-3hBEO>$l+p34c?e(Y+`nlR5NdUjvH`$d*K!VE<5JE2vefLmL-h0<2-Tl>L zojuLzR705@s;Q4>I+Lwk%?*jP(;0}&OV=d<&j}3rn7E%vBusK5)6w7%0y&!!it)56 zU~py(PB4OsC^vM&M&R2tL5t~S+M#cWksF1x8x6O^Os8ZIY|Nne5M=HllJhdid0>-R z@IJMX!ESLvS#VmZYfzndZHum!l^=g9&eSp+NLP_gs#)t9W#LIS$wdRE_E97I&0+r{ zHQ$&ej~b+fS<$FZb^56zuEsl|&8d}Mh7FGwefMRRMtjZ~Xl81i#i0-;dQ(jqUG{!j zdn=`wm}w?5TPgd;Q3RRHki;-l3NgD!2)o2(-pL?N_ExuBR)#mnvBdyK-TUW2!vR(V|C0b^1 z+3Zzrt6z5cMUPdqS*5OUs8n_YWq7jz->tKKq9o?XRb)L;LJ2hmx?G%4Td-N4P*$W= z3xo1NLdl#~y`azzrqDP|BnWN0!^s#>$6_#`l|ZO71E9D8om2=rby;PxU5RIv7Isou zZdb0l0Hp$qF4l3~<&p_Pi5oW{SFY$nM@>*&R59H&(+GiRO0zu%xdPyrHd`uem8D3r zzdqcN>KIwk)z%V^c-;nJ!^pb!4jP%?*)=>e-QE&fCU$wt%LW{wY+X-kBAudFw{~3C z5>1Aq@!=jh?5=6D_$7<4qFC&)h_cZpTT3lgtJfQkENl7P$RVd^Xn5Vm-o9kg2f5r8L4kq9^oWMDQT-Yq^sETjbULn*>k zNU6nuvH`9RTS6&04N1v3E_6UeYRDN%7;Ft9U|XtFb>qjsax$tGm6Cd+Qhd^;(vmX@ zImsMYB_LXgZ85e5P6R8!p0bDRVS5m?Hz4djm(Odj&OA-Sw492De{4T@LesSO1nNLQ z;6Y9cJ7&8@kGPLqLN=3}lT9%A^4tWKzLjC@f-wXsRi!r3&u7)n@HGZ-B>wz>q{40Q6Gt=h^4Q*Kv_ zENq;M;utz9$0?f~!cr-6C>E}pJNVph%9M$w^CsS$ffuUbDqr|K7z+2ox?|M7yT7wz zVr+WT7yGmqceF2?8Xa5Q8VrSM+FDnSP2c{~(ymaLc8V6O*At7jB>X|A+X|a%czEXo zJ++To){OP_rc)v9W?SCVTYFUPiJ;#mE@34QiNT zI0+eBPgR3-fmH25H^_2_VPpSHUVD+gZP-hN{MpQx49sa|AbVzahxPr+y9 z1a{P`(1&C~ZN+AJLhUWEDfp(EfuUwu>1LeDs#{`Ea=P`sW?8)yD2w2pE7=WI{50oZ z%~nvRIjdgTZFl68t)0uAu%^_iOK~;;gf4H@3Afj-2FyU~RSvZZD8I;O1KgzxcK00j z=ZL+?ZNmle1>2hm?Sk<>4^n{HSm18y#=str`;us5I=$(#?nwC3-kw!!*L|+{vW{@1 zyXWTYWHjdX&?;Xd+1}dQIWV}aZ*gZ`!snyy$=JkXT_TzM$7B-D`G?~%?GSaew`3N? zD}83?HN#tndwb$(r^n-Ni1+m3aM*20#}f-%+WVJv#o~USY_rL}U@X?P?E1k-C=?zV zTb#`<7VLvj{0;8c=gMa7196{ti1?t3T}5&dqlg7SgBKoKhK43KQS#L~z^i3-J^;>C z8a8LDp*yNh7vMMFu6jLerUaQV^<=7H@8l0+(+fF?@uOs`zNL*HjVY}-!#?Myz&*ev9hB*a<98vjL0^t*PVzq zw{*K5F5%ez8#cAH(68^;KDlrAuCG`DwA0D(gDIYA%PM(Kl8 z(fWi}*)MAYI{G+o!$pP(SD^BkfOs$C#P)*5Ud~yPXovsx(>l8ZSG$~d>1w+xp}GNj z7gO710G!x?1Ea)g7tOOSd|HWT9a=-b8?E-H6Aix0nIf-uY$jMx=z{^Db55b>JYwhU z%Z6vfQ|RIr0)sDalt>NwYDlmE0QNYqfOSC|nDWP5yTP_oS2E_H2=$^uPa0K2=^1c{ zlgEC2QomqSBwQi5Vk|wQiUt^VT-b?B*yRNlX63rtoWM&{dl9woG<46rB82G6?+ZkG zUV99m=K;@NY88`)DUP!MvpQIuCxjk~ysy|}_$nZ#EFE-Fe0vRVobwsppw@TarCZ*8 zm+4CZ3d_*NbQwbxr4}V}h8oDSNKivyjO-v7ql_R&rifP3L47j18z{uArTHkDv?d|F}HovsG7j z9hl<{@FwBjh^?jO-k)wA9U15w6a))=AE9V0^^Gy@gr(8aYe|>JTKu8fQmf7CDh~!j z&c&JjP1TF0(~XB4Gjt(-jgR>)DW|hO_O0tfvNIlxMWcyeq_aw+e&grHgT!DhTfhuikqZKiJHImrK8X$i@t*<8MbyO7n5n-J7k znaKC*F62-mzgqV#cP3Ob<9S;+&x4R*`dhcG#-Rv}=+kdRR;O^(qC1ljleOpD^!*kt zjPj-YKJ_+zUbLD%d_&jsra&{-pHy4zAepw4$Cf<$hcqq}E7!|MZ@7L6CJcTrm@p*Y z+IIa7Q+!I$TLb>NaXHHIS5 zYyuPKkBa;q=g#J5k)_$q|Z|&{BJ@TCgsrQb%(p`h2>yK+s zO|5Th5q5XC#G-Mxufk_7l|={C!!ju$XWcu#A?&!D3Y(?}`p1*WYlgMoKD2pkva353 zT9_TNg(uTluit9+xl{3uo~yU58EtK;4Z`=#M0JD1tR>ugZ^vY`J(km6(LVgkJqw?W z+8rC#j(=nCLtPsjPPZ?X3dS<>KzyMfm<91+5S>5}v^Bm8f1(MN!L%+*;PXnQasmr$ zNM<6G#FV1Ba4CF6R4GoVR-IIyZ2GseoV$qiU?VI@ZLooZ2(oIioiQFYT0~g8MeNr= z6bo}^foT?5IEgTR!aO+e5(N!G1A@(2p@`lin6> z1N-e%fZ%;DeiPGOZI}*u)|M$$kA-*iNBX?kb<0H0XXw~pLsbyfJN;$IujuMOAg~`M?XA#pH3tk*QH6XkfsuApRxFJ)<=2C$4 z?s@jPZfB8IJ7<;qv`@5GW?rW^ zYUjGEt(5*Prz?i5=YT6Sse~NhUNW@^J>{6HWE{)OdkUYhQY5P~@F1eoRjkvMMd-9x zz<^ygNq~5OSt4`#7KihDhUkx<)YfbppyK#MsfaM4tt@%nm@**6&b^`S`uQnsV)BXJ z>E6`R*7>7}IADi9iOhFiOu7q{&U!={>>!r%Ml%dqxa|tG1Lr4{dDcJX_T*mCB6vj= zOM~wJEI2)x3lbm5o1p!;xL{_fFfnt#a0+hs541bq)Oxg!0FMi87m-W#w*S+1kty-# z+ATDydlN4-dR$yIlM$w8Kt^bA=Wk}<@nh|6nSI%KSZXEp+^)dX7Ao!$jha4NWu-Xz57w08H*^HOYP zi?I953&ImKtqdQ?#WXm0o0sct7V>(DS0pgVGjBV{E+=q@ zy8xezLK^0pPs7OqRWW2wxq6Qr5uQ47U}RF`Yd2^Dq38>Sv2&#c~pn88b3&a0MP+6O$mcu;tpfDlwK}&{>8ZGNlM& zB?QA}Q$Kxz-Zt}|(4%eFEIq)=uyFX?2j|`p4$X`(yfFXnVT{8_>Iyu;P~Zs^W&|67 z0W-kBr@z678DQct<5+j?p_>E{(kIU|#g`I5+?3!j`3mw{0FxY%ijZr>nVur*FrlaO z#oT>ape7b3fHPAqK;y~F{_O``q*`EmFejVAat1p3$zzLediUSi1raGoMy2SaDmu>~ zT??@zfW%;Co;Plk6vS)6C_z5~3ZNG7upgm$x{KaRbK0}o1KLB{UrDjEQ{w(JFNiH? zpJZQ*;Q;Uo=7l)ND$om?8|0$?gEt{&2*Js+pA0%eS%8DtQ$P71ufD>2ZbHw^Csjik z?>GWk1~d8wzdLuhFqsdRAe%4Let_;4#%Y(fb7mj<^0F94Uy^5^gzah5J>zJ6-|Jtqc-2A6gw zQ^+5v@}`oV?JK(`*RANkv@4m&W)sP-jw@FI4xmqCGw|UCeYO=O2UpE>6(?U_!8Ejd zd4XNUIZ)g{P(_2{memm8fhnlg&r$99oDS2R$sMB}o_SGl_07rH&>s4;JEynyk2kl> z3(*yA?cGs- z!z~?8wD&J<4h3aoAToV(QXAoT1vW@O@)8MV3D;iW_@Bcm>puF{W!8V zD1KRO!d_ECZ3q0CVweNvfKuHhkjpOS4sJWI-JYj~IeQ8)<8uJIF6L)}5(@O8N0s=b zVB$MQ_s|EAf91gCL)nbW?RLhZi(1<|R&H57(B4@S()L$+Qpt|i;VrR|wvNeKzsq5j zz5e0$mccq2FVa7cWXDx|*|cquGm@ z?QlCRX6g_2YC}7BiMs~-x-zM7#3s{HhwQgVB^J@9yvYW!pYYlx;2<`F0-aw2BsA6>g9PNiv(uY(95wdn{i4cbOMBE@wZ4{M`%M=b6wF zlbKu}C%`0&Od$yxx{47Z*eEeo$OsKQCmGqU#1aqmOlSj>CiqyzZ)qDfxaujHC7--t zjOp~U2fFZ+Xd}#G_w4nGFQwEZ)KbQB=5a!SkcY#PnI9_V5f6e_AthO<;qhU-`aBkN zhdIztm=L(eJPVO|d+4-imQ7Zh)9$HkUeR3>_j;~xremf+V)c?b=Z7d;QbQW1}`_0#l{rRsLoMl zqJzp!@c!sD{i9cYXXnzFIr;?Is1RAD>@RMC9~IRs`X zU8>+dLtwwt$GE$%;MF`I9WgKsf(FfWi1s3jYD9BsOjti7rq1X%glrj3RFMzQWZS# zD;Q25dm{dapRz}x0oAL;7BjBfRAc2C=C@sP&j;VquSq;lTS9`i4UStQ(;SgabB1uI zt?quHZ6$@#>ZLMl-k<*~C{JZUFJ=E&E;=Ga6E8hWcR%|QozNc9Zg}?{Z9SlHKpZ)H zNL&xKC4aUDylM6va5e&16q9EQ(z*Z(CbV+~qr_psZA+F|162^IA|Q394T6~AU}0(T z?3~0^e5P47E&DZ(ydFDs^w$A4!nvJdp!X;hijZK$lM9ga4Q0y=+#eN*5!ptCCyVj+ zqo!h(b%5W%g;Zlad*O)94d`glto5y~Sqn_^vdCcwWH9$nq_1m!x}7HR*^a%}wXNEV z+6xAs!KOi=F$J5xuQ>pZCjk#9;9+JlU8Ya+37A8X5(fn0&}KfrRf|p={}g-ko&s$! zEz+n`FY_p!JeJMB$=&tvUPJ#C3uFN|6Uf4Vu%Ho2CNyAC^F+hqSqbMO2tYBQQ>3NzAFb=^ejf z8Qm;&z`W|JRE2Js&<&&!G4VkKq?6%K??NigG;kMw(!vjCmWl&2-NNJN5WXa@pM6z( zQE&e(*oQD0!3VQ?`v{O+@J-BBgP2Uj#Jv3tj3wp^M}~$18=Nym-xA(u@f>&rJ~{ik z_6Z-~VdUM1q!uv>d@KcF# z8?+;&3~!4MAnXy+W5LXD<_|+!=$evk!<7CdA0vVlsOPqwi)dwB^izMe}v!Dt;Y7IbSXVFV5JTYRl$_`B)!$IyF zRiMek)T#!I5W$4slJNr;@P{T3*B%rOTPYNdT6u~+U;E9NnMq{dzF!KflL zfoN8-+K~pXV80YVX@chuj&!cxLH&p{1z`_C;p5Ji8WsbFnvrhO6N5(g>^(cU)Z(rV zWu8_Bj!`e|T-U69kVy7)-}vP2NVH?>a>IOYU`Kl4BXsM~uywk!#qQ{TZu+^+PtA-T z7_5)egG15S$Zrh>AruBIjw2V%CLJd-s&;NH=(@$JTnWIDFXP%RrZV8e1DT$Jx?9C% zG@P2u31dfv4Pd~MNqyMzWWds_;C$SQ&z5MV}U*v^JsZjg=8 zx+11B43p`&0Ksv(X3JB%*Pf!0gqOg3M%1*-=EHO?NfIR_K&ywuNdk%@Oc{B z_<39VpljQ{1LG6Dy|I|kyyx19j;`dm$KCiFpD+0RpK81N%&*4GNg9dtbl<#*@f?Ay z2VI_!PQWu1A~%yMh1#k$e)cs}$z9Vst1bq@vi7=CB}%gkzo*4#en=zrqM3VHpHyM-;{& zt#3*_v**-$I`*d?cmKr3Q8>plA-{w6{`Br8TW-Er7U`=rwENEc?-6cIo@qbalWGt8 zGtFJ?<3HAR4+@F(R6BzR2gW*vv2F*hDoK!RBsmwGeln&XOCBgB7UjKGmxLLJq4OD7 zlTbXm1IydM3c8>i<}EzH#?9&~q-*3P8A(NKxT>R!O+DF#0hjR!=g2BvmRA7Ga-);e zo+}7N5~D->DPTahMmXqT@yIbD5$!wp(SaLZ7a<&ay8hzfZ5_u?Y1{7YKmqGti9c^o zjR@((Th{j-Y~Q8*{h=NGy~FQ}QR@5BcTKlaUld%1@x~U&kgd`|;zZOnSB4Stu)ShS zvB*8EE|j5WBv|qcRa`8GiU|@2FllSZId~eCLxykB+>k4CFb%tmY1rkAXp6yc5H(OF z9djrU(|L#svqwUL6jQ!iemF7t%;Xn3`a70U4@!3)G<=MlpD*w7Yo0Cl?>lEUK%F&^ z-kIl&67bN7`mZr)={YwLpmvp!orqmJ-pd3t5*XT z0sUwhyOPkIXydvPakk}TApx^nR#u!;FY%pGVkeJDUpl#xiEW!QC@Y^ksYa{MD3Oy# z=SL$@jPQGrD)jfPK6*v>;ODq1fgk!G&M@^zWz0sESUOJH4qubf&sdLgwOviRac&vU zD+$<@YLLb?V2npeSt8ANwcE}q!_CY?W(smMv6WecEP4r2X=1 zD`f>R@wLab=gN?u?YwK(%yzANk(HKFN-|4M~Z~YeHQyQOC4of z(}{$bA5JEo8R+g!+8(JYuXBYR*`Qk*&t7q2YJGF|_0dJiRAg}~I_U6kaXMFzoY2PK zN^JI2$Sy~*o0=`fmdfy=v@hc<_4z7-&YqxRsnJ0mgKTOsXSGBZ|M6|yC^YU1zs4zBc=@c1TD;RdtX6(;S)PLEa#% z2P$S-M39v|F#fE7hgoE`40~k>6rHn-D3Gq3nhNGDnw!gbVx9{QBqyZhJac1B zC<`AR9Sw(CTdo`&>~CTf!0Gk<12=4{s`9PKB)bg{o$FY)W~6I*ebQ+<$SZ>TQ5&rD zCg9XK3|Ui*%BAx$m-CXIJID2a8?(eM4ctOSC!yxDKnJse;@^7S!)C9jXYuiHfpiky zWu|f?Rfo?KYUm}>C|&~rCL5PKG5HFbc;%|9Pecluxr zvm|u2s04c^hNaR}KE#DAT(wk*a$=s3!Oxdj=vMV%7nNB|)nU2=bB4}mV}eFgZD&#x z^;pGB?E%Aq-4OyZ4aLmCowBFg1p+_61zsDG%?dKt-FkfY_{Y7`p8i{=d-`K7ef>Lg zBg0ht?yd3qP>c3pho>T3F&Y(j<)%`ZNNi*{8k^pDdd6z7<)~X;eC|PmXIF)p7RP$d zJ;C9Hl{duaFlHyq$O8;86Z8@5np|l?8G_hBNCg8@oZQ~!L3oH5)ma!DbOW$as6wNO&18R!7GF9{A9-4Pu)AB5+7qb?ao7IG-}zuBA-uQw8iUUt zKZn&-Fi$%n`yWT&ir}djC{%CQbp6J_7>cXVIaDbXVtp{i9q1IJJ`^me#f-}f;PR@` z!QgCSq8^FuPP)j4)OEbUHUZSH&r<_iw)0s^h zGeRdy+%H8i{ny&7+DY0*-`2Ja4G#~|-9sbv8ua;~wqE=QeRd!ldc7Y1DD*RjF%={` zK+N@*`2~)M#ApnFN%XP?#lh7x?)_(}(2fFteRfrX<$(S(;M!RpB-79+qb#T@NQIrN z)Wd+FCwnL#5aXHfIkF6R{;4q?q?Qi?rCt&K>iT7!2=kgOJ0 zxNrM9cu9Qs42{R4gNKoo^0M~aH)j4__~mrAWr=6VzoMf;Tq4K(zN$2x!mu#zNsRj# zr=vLYg|oO%UROPHRyJ-X0TdyX6B*(n1;o|!al0^Xm&|ihY8qI`R!v~sR;H1Gq>7p9 zLeu~oHR93{J4*sbQZ&;<%j7WUb?yvPWF%eHMm004Qu#+$N13j}WW^+1&_6!Wok}K= z^mcITHz%(dP9C7EQ4=9qZKdJfEw{u&@$QyEr`v26()6uqe>%ASTW8)7O83!Fs3lJq z4Pe-gSSadh-b~MQdV@>S*+m&Peu}w643pw+UK^$Jonkg(nqui4Ia@K)%WPIwoW-l9 zR2W4UYN8AnAd1CwRKkd6^PhcEOqn(z;XNL4gneOOCO(;J-7+QadMq9GIc#M%?F9!N zIQItH-v*n;0r)4$*L9mloriWAwDtUwhJ;ebD>2lR&X(Vip6bw*ss*xAe-if18O41P zHO)s83+ka);rAlF>S(#UKEZSpK~g#c8D`~jN)1TmL6FWU%2G7Mb}YyzemnU8(jw%cFXe9NY+r@d!%Q@T44^@(Lp zbUf2IMgQm4uWRo=)ZVjhvb)%_+?Sa8_D>cGr)Xk$*zc<_zv6RxsKx5*bvRwqj&Se8 z@2yPH>)r@edXc=>pNcKHyp7|F%>LeRK)M??;A)bq2DPdsfEQ!+ESaF7cbmZAVFrfJ zr;~(vBtH!OnkB#Jj*yFj{ko-CQVl?$_Mg~cQ7lN>u_6PCDWGe!NCDUV(kzWl|L|0} zdYZPsY(X++r*_QiTUs;USMBZ>OQcBB<6GL2?C)+*CX$Aa_tMvSqv1cg~F4LL9vJ= z{MfK((E=E!gn|pmUB#s99U{gIRRJX!bB)hAkUjL$`@T&ZzjctdffIh`p8eYQ#9cF1 z;lr6y;or}0752<**8RugPV}c36s2pF68z|M!?PxM+J}lLN1@{gz@Q%Q9&(y|~g#g6Ftv>rN19Z%44ZFA?Y9j}g$Z3{;OZh-dp2KnsJOB>c1 z-^nYn#!+=&z z-+M>6mzv+2nb2TvqV0XNaUpMfnJgon1V(S(SupN96H41@c-z$|@ae_cFu7cMt}V)r+L#5r9N3Pw0T4*Q13~fnFYuQ_0EqKfmu^HLh&MMMP347x_41T!&ac5G zr=C`@FY0!dSgqEEKq%}_`LnTv-{~l|Nn(l32DyW%=VMfYI6;wPB9`^XBH>`v;YJEF zRwWQ3$Gz^QEgJ_qx~>U_-446OEFCX42mBqe-CdbhZ@AoPw-t40A51Upu0Z_hhY~7? z%boM$(R5qyicKx;wk>63uza&n!n>*bg0$;eHKd|KbDP2|G`NO$of;)(_}i%GNqE4*YGWG%}9=h_B! z1%R7nRL1y36VtPJbRX6<9G124Xg08kbbdtu2(z@zEoNN};Z96lB?+bH;@Pa;#f2&j z7km>J5CTRSI_Nb!Eb#wHWUD>)oQP38E}1QInL8XFUEVQp zXxs5^hr2ItZHh;O9(ReQ2&{_|g{H-@@F2WymlAMKR{zXa6DcWYfGM#T#g6(*%CRmJ zT*2(TUu5U~AVuzdgH0JSu|=WSstt@F3x*ojMJULH6b8-YfgWWBX}Xlfi*8QE(%xk? zHU0qdGw91pTBc&@aH=P3^?OitL;DU54I;i9D=)QCtT(WK)v(>_jRlQ1>hmrG|LH~i zz+oK2S|}-U4ar5BAgIu}1}lbTMmhI(HgZ+0kd=dM!F)>ZYZ`b?p@21Ki6S`=xUOKX z=@3@7ftWAlVjd-|HpVo$FN9AkhEQG7OWYHxt1|_EC>tWnM4uynhG*pA7ko?xA$!wx$aBzCv`ftSJg8#dM&^JE9yb2rlM>ELm|vpX7=sM~-DrTx`YXZ5YFGk777qNb zWdk)xmWI2nOLw#{NwcU=Y4w{>A= zsDl7kJF5pnUVN8bSq6YzIe>$3d}A`6h0gC_DM;Si+x6{p;7J6 zW><}@k9Ef~DPOwS?2{^s?<{uOg066F;_7h7fi+r;V#Y%cXn%p&zt<;OMWOH9Hz_8W z%i^%b7lBf|A?U-;9N?#q)%NARaY_>6_q}1kexIOY! zJ-!1SlQn*?Ps9p8Jg;^ZK_O#QiXs{_uCH>&(i^e*gI=M8>`KMS7CKgK?&Kb(JMP3tzL-^Rv_IO|L9V`T2XjTPaVV3Yns&P1 zr$8&`3!>))8!wD5(AJzMa*jRgvJI5Q<|Wkgf^N#aoK378!%tnD?#^E-WW7M2ZKW=q zYw3i!D->MTx@vIU#OCSgd+*-4Zg{Xip7O+yMdJ3Bqdup#dF9~PLyv9Syls5r6~l{L zkf}wIiEBo-?cBfji`$0>GAR_J`$Lgz^RCq+>nB!^4fJ=k*M?te%B-K-+cD5r>*@9_ z&2}};Pq>_AGDzDN^2ef?sp%C}dp1uE4xo%9Vkt4$_HLP;n&|J#ri1=yxU+d=Xke^& z&*9X3e}hVWB)MFu^!`zMQjjwon9OlFT8`Rwry~P|`BXz0Ak;6LIc)V0@0rsih#q zS}Y|2m$%xSMsce>Af*;C+KYpSLBKO#RuU|(5!DK6lAUO>AGH!+IlHITfhztvv7qxX z6uL87LHQ0_feNJo=|;FJu-cfNUuU&olBvvO2-(YYTM$t`+tA>B_x|Yd2kspi&fjP@B_63On7A8h}Jh}6&x-7GRKKK<}F_gy_S zIB@TQzsqU=GBBj>zxT+C-`KZ*|G&OOBM0`+ye`~dWOh2b7xoNRXy@8!RQrH#ce>LF z_)jRjJSjuuzls;rG9&K#8O>_<(e1*-zM0p#toeV(_x}%#{Qo~QX!vi}PZ5q?w0;W4 zWBR{%M!NW!Erub%gPaIETP2gN6PPdNR9WzSuvKB$GBJfsp^A+!5CbQVYJpY8)FXSN zt~NCBFsdtM-x96%WTQ>VY}>@V=uyKEy@9U~<8?ai)1mqCE^Q3+tr_QE;ca&ma>hQr zPz}~gAfGLI{f`UPFuRv8RKpzk=Ors&sHPbU)$o4!E@%A^_aN8dK<5p${*V`yt!(vB{R383SR+<}xctd~|^Hc|lQru9e?qgwd zm~>2a1kCUtTP6XE&oL!ZwKJ!f1;radY^(k})KdQej^k{hok%vhh}GvRH?Yz0qls`d zcOI%97$XI!7T|o*8H^8bHoQ|9->n5mmvP4l8;=8l*Mw4?Dl8GC_~*uVVU-|63N9=U z#KHy`CGN|k_LvRSC9@&3RravB2p_9%x2qv|a#>uCQOIX;!@Pk1CAR1={qB4|dMq=9 z@%}mNoyVuA{5(D>d6Bjtf5i#EZaq;-dF?NjJ$<6{VuC+LDC^o()%H`09FA`d7C z-tTgzx0-@j0TnKA#E4X(3dS!xdp>ZbQ;@!V5z7xmYF6H%C#AuujMc*OetuLiKrSq8 z6h0p_(nWdrvx^=HzHH{IzGjrY_Vq8!3dv6|g*4EIg4p!HU{fZusDEyGB+SJmYMx(0 zEz=_M4fuOlK2|x>L$P{MEjB3j52o@N-R$%?Ac;u9{Gj@ACFrF&p`=eM3mft^dL-G` zkgG{E$yZaw)a{xeC>fGEGK^9%gv5^j@aI@E8TuM2YE&4km_Lsa|!;wWA112AeX;T=N4DVol2S zmewo6euu3;Z1!K?*R`&*?yj+GyFelXp)K4;_rht#@uW7z~3a zsQC2*Sf~*8Td76+*lmzKp03V2PlUUVYfo4Dyp>uy5(x(POmF(m@$N9Q`30=G`j+V> z;|m0{gZvQIJ6mVOz~Ke;ShZm3TX>xrFi^S@J8FjNTk8f85x0@}!d93`e?AqL5el4t zY_UP65|r_pzI=(U0U*M|LU>PJ{%=@?3VCx5l!%lebF(-I|BeO4Ew{WoqsR7SB=^}* zs@9q_VBls{z#uP0E@KYkJQg&^vaIZnxs2JwMcotJh-?9YGGy0<>{zeZJ}i7NOWqUu zsp}K?!<;{Q^W`^;$wP3%pFTgU6_x4tOQ&#ugsk}VbwbQ9 z=+FJf>%1@M&%%Ewet&i8;y(ubSbiUWSWmxtZae$r%yVd=2lt{lmtW>%kgn6N!y5SC znSFBhL{XVOs=`&a*)9AV!GcyG0aPgo`|y2<>=$LyEB=ApX&fbe=AGn_^ha_;7{VHZ z&145+37v*Nkc>EwObh=^9u!BRgJvO*_K`tk7vF8zOD4p7$R6>p$W|jE1K8go{yW(% zPLLhO3Y`DnWHZ`4!M@_(ke%Z9(AEa*XNb-8*JQg?O|p`c92fscdZZQPIM%~Hj`tjw zE+bo{rQ|-tDEe^+@f+X5Gk;C?n0`TyNbAV~X_!nJA0d1A_9WSZ=SIYxvmZ*AV?R0j zvFWG8Z5*6EC;npgN%4P@w78SJC4P$>=G&iRe;w&CjF2PZt7OzTjP@tVgmHwd7ylWn zDR-<9Ms^s&=;DLH7of=n82LN(eBvd!=? zs9_m7g?1!qh3Wm82Q#<%}EN z!S)1sQQAeu*)!}t$Y)KO=aXU3JCT}63)X39Df$k+x3c^p(opzpAcyE3!oP@B#%kkY ztOkp%OA%&lk42?SKM0hQ;)^7 z$g|$_fafXCuRR}E-ck9cx5Im^s;=q|-`9L^R?n;ceDyD?fA3%Ke>~s_d^Knb_6C1j zGf;D|=AS~gP+RD>&||e7wPRrt9>(Wa^A1OZ`IYk<=YJG+{3oBfXlHaR`uXTLqQAJL z{E{zUqQ#nG_p(pjlDePNov!;$-TQS~+{S0pZn75@f(wz)&i?-j0p12|=-0q8822y( z7uXZwN+-ACtM6096mBi-<3F*0jjx|K5F5FxurK0GEGL&eYebFcBZYkl=g$@Pe?m4; z2Rwxv$xUP{^7O8U*L5ATC<5@_UrVCcHjoAQWJnxQy9GEN$9(~`y%u+Gz;hdLv<9nw ztszs$NT|orQsiFVi0y^9vHwZqW;`*?pJLBW;Lct=J&qP{#Bn?JZ^YRx*dND;*?VvK z)E$BI_YCs4Oyfzt^(5ZK`nUk=)$}1lAi(!8KD!Qg-h%P4`)|c{{aQWzBMU#(M*Msm z1AxNy=x^YBUjpdc8h++Fw7-G(VGZ8O#xe($TkzCc9Ca4@A3$$5AvgVcjGm33L1j6% z7oOgT^Yy@Lf!At30o?!G-)@{_v|#|17!jL7QZOPO7DRHa$Pcoi0#}9>@4$MfNahEA z-N?K2ARp0-T7Dm?CVsRWBsI{|YEh{&k3`T;6w&_}sRL%~;dxyEs5Af~jj-mMfbWIK z_*n#qF2>AV0{gxVcx?xzbns}$a?%A^)s4)g9>iUG$rXV7m56d(1ubwT86=-0L#Xi_ z#%d!Y@aT>remO?gAgK2xtgC;3?4brw>D>^aM*PniCafz_g#T%y7@1-v)J99GOzqGf zj^cm0`FoNlkCI*FZ?TeF8FiAs#Og5R)J-d>$FzCF^$Qj(T~OF|ux-PF1^gJ>WZ}4> zuuT=VjfHKxux%=AGllKK!ZusjF4ecm!h4fR%jEUfZN7Q!#G2`K`lW_WeVgtSmu_RWQT z`}p4=0yG1_<&_v&zPYk*|632xOZ$j*HMG{Z`{tg%LGq3HTt!H8Ll-9^000c{Tl1X* zAcn1k^I~FWYW>X_eQW9f08rzvM#fT8eaG)u2&``n{Qn0S0Ls+L-Q=4i0|0on0RRm= z5FlTPnX&$NoKVoWhWQ)pI9|M}X5ZvD*ZS=feS-{15L(X6+R5#kTm7!l{au61MV=kW z%GU5(CzAHffqesgBj%ysTHo!vu4vZ(#u9=21=X_Aw>JLfI=?kA-#DUJ6;80Tb#wv% zevN+nT;F)G^sAJ#b1?pn6$k&075opnlO67EKtnw}JtIKC{ulcO)Ep(f&$ysD7eIpv zDhTl3n30~|4wwN9B(k1?9(X?@kqO8>766t6(D9w%zi|`wvBpsUWdGo$H0K{+V1>Lv zCf(C29@ym;=4J%7)4Jt}%jH=4&3@AvbkEqCK4=G7$j;YS+_Ro*63olP^ z3ocG>ivFA36)Q-rzwQIiGU;m8eS*l&`52PF1X({iwZNi6m4MCQ5O~G5)a2P4B-X%MP@PUGbNaj zgx+Y5?BG90d4NX3+z>(~)zHAobDYK1P=r_Rk*eyM!+q?Je#LIqeCjG(p$+avq`hrs zRD{nvFU(rVQN8}O&9oX-vSAKJb?q+gK57PpSuFUt%AKsyYn}OG?g5$ zQgOUbl|qzCZ)nZLZ2Tcf5nxNXpd!1(vd;tcG&UU&C|kh>MWBbId5G=C$(8e4V!ME< zflYKZ@-Hf>sEh;EL6RfoTO#NosU+4tKlNo4?lKORxDHq#kawzY!iv4g+YWjk(;j?# zlOx6HCLW=QnPkvqP!WI@78F%+hwwAUQxbl(_%YX?3S^?8&^H^p@oH7HGpWg0j!6~T z!)JZtZleAz-ys__`0BH&B44VMjnIwjjUd+DgAN`l8p4+U`VAN2?7X&hE{_Wt{&fFj zp>jEbwE9lxc{B$EIj|1$`}_DdPyjdp^y>=%<_7HQ^z}kpNoVw$o-`wiHg*>ipr5Fx zo2dUC#4Axx7lgt-jJ3~My^pnGkGJ27NKO`l8u~jWvp&g$`mi`@{x`d<()iu()P!sU zYyG$jwRAQ`8~ysxR3-<yzV`8*_rmw1nfd zd(JWM%+rfKBy$-Uu}ME$n3|DTdVBjJI!tE1{)k_yv&kf;Vyb4xqDw*h zzfy9_Rq@1W6U?c}ehDj<1(q-MKPy;An&0d@;&05LY!Kk3lwo6B6d|$3 zj*n*J*Y|5HK-}{zRQ0Ow{Sxb`L>o~FNye*K%bDo=$R=i=WdB*JM9?xZ8G2rTFmk&s z*S+X$`_kx}WTS}7i-BkvNtG+b=+Uxsr`;_Z{q69h=uD6%Sy8QxXc=6Rr=~?S*ujkPrV2UN zW`AngtxwS;@JH3+xCQh`@=*oc-=}wbK-+$R;!>F!PBM+cKXns~$g&Lj222gguVG;O}jaY3ga6EayeEN6H~8@;%i>_KVEx{OQ!x{v7Gd7<2$|da;Y8GHxTjN z(hr-zw#HmLA9!vmrCw=UOM9Gh&*r^B4|o=S7TZEf@~o?l)po8KV2oQG792Qub|!bz z_&VoNtUmWVmo}=PKpM@j*->1|G&~$4X6_D!WFI64OC!Ci;Mugn1 z{1zfYRm@T4xVxrODG#WPXA-7r7*0N{2ln?Usci{sq>SVb5gQ&N*3&Jj-t$ORmQ@em z1|VO9`e1~XGhtZyW9{D8mPv8)TPKq|wzk7`cS@>~9=R@}WEa|A`#f?4nY$QAmu>JD z+SE?d{A869!BE9l+&3MYH<)3rZqm4kjqw9#C zyDZJ#>CJXh3c)#dS{#_q`%T|di>vIT)!m~>+GD1sfIyq7r{){-lUZEuxu>`#s5kM|iJAN#ZftnC$LtR4zTep;_5o(BJZ_c2bJNv#wo()wz&HN)exKvx zW=9>*S=0xn)&9k25uqr?3t)_U2UY8@u_Nex_?aZQC6F(zQo~kLYeNzri(JP7O3ByDg1c6qx3C)QJ+Oba2wj6!QhI75Ja)Pe_2(f}BIKkOf z?IVl1uM%kM9eb7|r?9EUUg;Em7E-USx>+0VprhR->_GLH<)Mdk<8`v-9dx{(aQ6#2 zaNtdF*CqDY;jBH`E{MjF!!B1!P|CD4_00E!Q3KPu^)vM+R*sx;rvFqlClI-MKYP`3 z=xaGUOfN{lo)ifhvrV>f%nS^M+pxi1JO4M+Vq7xnXI=H@t7tDR5L|jTRi^41`$}da zT#t>8dz}8q=Os(#2*ypQ=UyF)Wn=Mk_hHnEDdrCQ z=Pm-D$N@vVUGE`P%k73He=H-mzo%a2)aBoR4pGC6=Fdkj8XZEH4l9z0m%xAF$>IOj znw>oBSu|b=^}UO);3U~o%YQ< zoB%6nH&jy|$OGWjvZ% z5p!DF(GG_koLWE@k%bBM#l_CMh6OOMWIMV$PC2kN`#0P^O1MQ&bUYn`g==Q>LoXzXhqb)5MclEz;bd%Wg@@pcy0g?%_I_Es5> zc#E(I?)G|puFqYhn==W;vOamAzQNzb)fm`SOof_$KuTliw+=2 zU_$M1YVab{-Qw|7C5HTudW;r*OMGRsgU1s1vvY?2L|>!nwa*9AU?Xh-r!}g(sTPJi zoZmllG^m}aBIU4!6xe{Jyz(eE?NR;*`K1R;yD>S2atsr=w*2gkSy3CZc-%UPpKE

XEP-ICXSt1t!?j}}u2#+~B#2A6B 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 literal 0 HcmV?d00001 diff --git a/docs/source/_themes/finat/static/nobile-fontfacekit/nobile_bold_italic-webfont.eot b/docs/source/_themes/finat/static/nobile-fontfacekit/nobile_bold_italic-webfont.eot new file mode 100755 index 0000000000000000000000000000000000000000..f05b579dd4b86184d220153b8edea7106f9fc2d6 GIT binary patch literal 29632 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 + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 2007 by vernon adams All rights reserved + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/source/_themes/finat/static/nobile-fontfacekit/nobile_bold_italic-webfont.ttf b/docs/source/_themes/finat/static/nobile-fontfacekit/nobile_bold_italic-webfont.ttf new file mode 100755 index 0000000000000000000000000000000000000000..54dedd83b6a187db6c026fa31397e7e4a1bf7249 GIT binary patch literal 29384 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 literal 0 HcmV?d00001 diff --git a/docs/source/_themes/finat/static/nobile-fontfacekit/nobile_bold_italic-webfont.woff b/docs/source/_themes/finat/static/nobile-fontfacekit/nobile_bold_italic-webfont.woff new file mode 100755 index 0000000000000000000000000000000000000000..d88da7512d8a1e608548a42b499f752a313ca267 GIT binary patch literal 19584 zcmY&;V{k4_wDl9)wrwXTwr$(CZJqGMwyhJ}wr%Ic{_@`M{<&SfYG$uqYwxO_n(mse zahDep0{{VjnoIxy@xQCn=>L!ZyZ^sUOiV=<006T2VR3$dtL0GeCN3f>_QOT|#CSg- z1hfD^<&_zjf4H)rSn>x3Y53t|#+ zVqmZfVgv<_XkcUj+7C}?26T@JfF=fX{3Q8r+$_>?eV})|xBpl4Qd(f3BEArdzKMjf zk%0lgkkdfq7*z!wjfIk#^25wf`aN~Hw^wtjiJ&BGC{7kdf{Kw57ywxd{q?`O3=GTy z46wk(z!5n?4w-^pX<=~!n!tmB20N!N0FdB-=x~vs#2W~y10JYhXmGe8C@?q> z7zla-^5W|v%F^0GijtdRsW;W*W`2u<|ougmZ$#>E&kISTb4qOfEx;mf7$| z%BZTSsjRIEXU5LN*wli=j0dnFlA{0szT5!-P&qIRa4K*Uz!?x1pax(HSOR?Iz;n@sJ~oym1VYp7Evtf13K*e zgWw(&Rx88O%PKqc0TK9qs%@z6ImT@3v7+0^wGA5Ou8caH>O{;9?uVQkI_&E2eZ$jfP8E($&UVdI1U;rop`1|{3aa!-{Lh!&@N@w6P zH)F+)HL-kfr=4i{=~a~TBKQ+g1)OrYBbdf9);b)R#zIy~DA+Q$C3ef1Ue3;qYpiut zw}M2nRML&K9GT(S32>&$Ew0j3M`sPR9Ar8fvK?C~^P5U`b}cYWks~|a(-JMX7CkqA zckX?P@Tb1HPJDOnyL+BzxQZ<*f(uR+7YK(G%&hQK^AP`}nR1O!ldClHM33Std1@9< zPdAuJ1>9Gar7IYv5Q%4ik(IGo4q>^84NN;g=vIo)GWmvE-WDd|UGgj5dY$*~cRax8 z#8l*EsYFq0MDLXK+PfuiM4Ls0yZ==bLZK?HG+^6s{)^kmv5{!2m_bTleoX8N2ZSmH z@#2~;Yg>u3-!3D*v3qFdqVRoc(2*rG4zWxcK6|oyM)SUxc{BfuduT|qherjdUPXwF zs*Pd@eV`+)j(f5bN7Q>dUk6n-r_`0)@lp0eJi(1W!pLSBwFp=Ra!Ulw2KEnSnrCGJ zB{kWMN71-IR}rcut1x$E@S_rnf`Y`PO;KlxNZ(1Vm6er}re_$Ewa|-Rk|TpT67{ZL z(ek(!CfQ<2#X@y721qgoQJE^$ANg|Cg5#4DRlUW@kB>9B%OTdkr_$vu$>%6+3_4Ab z8EAeLgIMk3mvWPgCKG$r=NRU+e)w)a`!TzX{lC?-%5T?q!4qtl4j5S?rOjTA=67?%s-|{8jUQX`o1b27WKbstWkI ztBr{@L}?q2=5iT@@ssZl`@hTwZ;!0tr^?w<+bC`1So5EHvU+cNLqEy6PPXK7muD3j zi|hL$s-AuaR=xT2y`0a9#_N2zKd4S}hpqRtO^`a&Dz+*gETXQ9pt`j%1Kn6cWJ#8|W~cmj(Ti+c@fnc1BUGfp*QLb3ooP2u8zXQ8u&&*IL@Iwj`Hc@vpH(+7-jqD_r6<@&P`j3MS!`rxJtF|!QH!*Q9)jD4E0%e7@4A%N;}&nh?`3oGs+cXlfI7fHzulP+_p`zj$p_5Ghf;DuXNg_)8|B)kQ}#% zfmX0MzvQk6=a4b9jFmX#QC7`;mWuhd9r$=9hamMdOdQN}VQ@u!<(~fa zkTd<8KiAJnKobLjWAPQalP(54NH8~7RBxKRo_=ofZqryl-+Ewe^J)O4UTX;XA0&|4 zgA;z()2k)MMV@I_msXSt%bkUE^^jQO+XYdoz7w>1Pe`{On$&pq*mjX@Zv$Mr`<>#w z+BG9uY2qTe6~v?Hxqf;772(eM)@Eow^(xhBolWgYnOOW)5fAsAcSkib6SrYj8sbv> zdgJsO`ERKKLts#U2E_p?mZK09Dn)HmSqp2yG4B`_d`0W-jRQ`|X!fR-IK2ZnH`H>L5Na3%qrRay(U=kBEMDwbvOhk?JRQxAbm&N;@thsHiRT587Utcp z*5;)n=6gG4W<9RW-vk5sI;B}Ij?MXx*_9}EIo+8UBI>>xaymOqoXPgpmCmbIEeZk& z2)1}V z9tK-UmC9Asc{k^-?hh_WeCHU)Dc_Y{yXp!@-?eL}%`^X5JHJk5h`16WY9#4Zx*WZA z)Yxxi-N1q~pSiw%16dwJ%HvV zkH3`tq|AP8zjA)+lVhi)=$g+h%%WA^J%|{^`Fu;Rx*bM(+aq=15tl3!dE*AW7<{>0 z#-IVN{qH#lxXOi zoT~%1D!RC_ytCgKh{(pBYiKIl9zVx7_=HTJb$;Cs&*(jV*GlG1$e#Dnce&3Lxq7Wb zyp)`eV}4@+QJhI7=|UkRQPoc&?v3Ql-fzTFZJRtpGr`TN`iad^y_cSy{MW)<+Jv&9 zxkeLj2BzJ$2H6PmS6$r6Sv-j**0|}4%qFwX`<-yTM*&?A-$vs7#ZA8L=HBPXe0Gn| zmv~lobZ7->E`(%G)8>NrK3brAG<;?(M?Q$IOL*xxNhCc#MElgVS6jdi3Ut_lZ69L8 zng|nO6NKOs;;`j|U(fJzaTOD&xK#vm~&xIUw_)So{-&ZVgX9VY| zkGYfQ_BQ67pG+7WX-F z&wE}cShQD)DBR+*UfJ96T?@QxyFUtj&E0rDPVe8iJ`=xyJfBe>z>b+)LAF0RR07$6 z$oCjC7C}pe@=PFyk#<6QqWIOZ#o-mh97%s2swWJm-*u<646BqJ>EDp$38R|J5QonN z31{u$0k-ALvAT2k#_#v@A~c8@yru>JNkOIN3F^gB=bL%r$!zV~aQZ}~T84i=e?;f|S2 zi|z9B{3|r)5KmDSkOj~Ige1^6#KXWktz*=}Ih<$ny2=b6R)sjc8(L5|2d43^cXd3_a9M!;^DI8(HP24%DjrL?wT;Ilkp>~AN zUW9O_`)g1}P7Hc^iHZW%L}Muv3~^?7j3EOt+7eD=9T_r} zIOFb8Bb9Y2ZO%N!vAqhb9py9A(-zI!hZn$ZSEK?yN{j(P7#|YToe0T{s&<5$1dJrY zx?PHdB#4bBDhhmYVJ_F>=;|EcZ8Y<7H5mS@=>^_9#~1&QSI8BM(r0-<(K=vE^&hSH zQ@oaZ_bKq^6>_uG3nTux;!Cv)kp3m^@|-IEYO?~DFmFc#onb+QCAChtYW4gfxGA=# zQgL|5i!w#P`VEZZC)npdhG#kAqa$hP?}HW@XI%*=sGmK}8X@ZAF*43?ZN2ha$KUWx z`aX5`y6NBh`MKF&-i4LWvqB+?ZKK>G&I>UsG;pQsdcnjF{?aKH;PC@G>EMW#SP4G~ z*x?WUS`s;ZdQafG+4w#fqL33lG}~mUE4CyKA&MoD3-3JQx+Rp z;^V}AzwO~Q%m8|h(N;IxY04nkhz2C0>s^$VCYG~{)utL_)f)Os$F;%V)8hET!u72Q znbk2NqEzP?pJ;;K=CsJSA;1`xIj%ux4(SHQ<{lZ1YsCtr?d8?(|9)S+mptyEWQ1Y7 zra;lmfJQA2PD={WPtej{Id~GsK9Hm;;n2B@M(@Xr&gd3s~TIU-rYyvHG&yU zUk6nhZo=aGy;Yu{q9fh7|3sL$DJBcuYCxZvm^F@HtL%G{{6{}-=Xeh*jwy}iwv9QBkGDm|FtNccq z2JOk=p_{uE5BuU2=CZvgqJ}WO!!7SSIAPg_@)X$qV+EKdL!@JZ*u~wy(cF2X!XY;I z`QgFtkG4}a9(MuK+|i)1)6TwLOZ1jJlnU6(NCjaq+rqVz?l)J))22~Azu6yds-q8Wb!mWdH6xe>g1-@=YKLgB|r zZcG;BDSxH+adY6-uY-cxFTE88eKAxo%(#RhyA*SRqGg4xWa?m~5Hr;=Es~GLaV<#e znWn0@X)CB*T(L|j0!G+Q-6=YptV$n z;@TN-Bd$M_bh$@<*G|09QsN&5B}ruLzp+q$oF+$l%lS7`dFA2oO$J-oqxZkPkIqI8L^~dn>~e^2$&}-^@MA}h8L-oC zHhk{EzAK0Dt`U4}9E9mhMp9=qR%VrD98)eO17DbECGtT53B}Jc;?3!jlULKiJ~E#m z@p>0Syz?GJzpr(XM{2>f+$=2mRqPt{hId1W(2-j+e;gjJ(SarsxyD<>%l4+o7Gw-! zhXnaDo=ZAKIZ#7(aO_L#8LXM_4wjHc6mgZBWz7uXI@wLg_Wq*tAjx_Fxo{E<+}dCK zFrzJ5J5C$`q8~{tJ!fhPUu(R)8dhRu_s}3?@UAqg$+{avHDZrzElIjyNzb=|-||1r zX4q!V8q4qNE4RhjNI12OXf|uL&pf@jN#6{b*N?N02stmTzgQl#R1ZKIE|`-|S5=G~ z=_gRdE{sM0itVdqC$kO?Itk0cD}9y;5z5{w)PMb`k*aU~&T%WY)#b~E4ehMi1_?)q z^4n2>D67tJ;K4~b>%LHODo&ON>xc6eanvn?9tnMvzGUAy`(IJvq3a*warm`*3O?uP?98hY=|g^L;oMjBv@z3H#Lz19jzpvfg|TcH3q7 zBLC04LHDFn$(J^L-2}pm2MXFKg;&aGIXp(Aa~!c25WZ!JD+!Jp8$9}wW=%bG#6{v| z$As##p|7JB5mX`)TMuG}2(&gcFb7*8v#Z6j4?6|0>os4U$710d09YAlB>m-)i|%6# z>Y=zlHK_ao-zHYQDfIu{I(uv!P^llr{$5615voMy*^QF(UM$vT<05Eu%dw^S6vajjbvnlF;>+j=CXpD{r7n;ejf+$^>Lu4 zLz1p(WH*Sl^*-caX0cR<*aHtya0nP+3QQmoIotK3mJ zbG(M18x582>e{p@k#8RbmGIyeHY+d(czytbNPO-0I8N1w65&0A zn)f`2vy(sAb4(Y+dS)EexzgJM+oOL;qP065x{$c%_tw`X?a@!Zy|=eT4OZozeyzQ6 zk1FztzcTW=B{H>cLwZ(YG!sfqZd-tg*`qdZ>{|RupwK2WVxC$b&E}LV*#fWl_MASc z-RT?(j%;zIq% zHBJ`(nQ(;Ho85H?gA-H-kF0KV$KdGy|3EB$a+&1aX)ETc63_OYZv< zVGPAas#uv*e7x=Wiki+U$aLyazMx^&FsQ1fs97LiaKmVAPuhZbjxUDm*q$Ig-#7ZNqMKr43zmj% zuytF8*%Ca9gHoeKuXD$W-I4Z4r6y`jfatl!g&{2K@P_~qY-hqcb!9?h#!}1y$E+nC zoOgga2P)h&9>H>QL~{;eXJhm~o zn$`{I_aDqqt!0`FKKzR~I$>PE?b=bHQVUh!Q;v9tB8Jif0WKD<5PIK9oXEPCcQ_w6 z8{3D*jI8ZNLtw9Zw`0ZA1W6|MfGd2$!Bvg`amoc}?~wkB_@@s2;>L5R4e>&j^tg2* z8&c_sZwDRjv}x;g+ZkSJWeq)UY$)}>b)M=zD9DH$Lk@K)lH9Z=>yjZKowA%g&O-l|@QgQi~c;5!rI)Q|0J zs1O@VGJOn99>W-0~7JHAP(vDoYX+D?BnYhhgP6*QXJm%({IGh~lDW#U5%<|Ig z9b3em|0=%dKB+Rhgl=#31ZM_~_HI*t?-=ri60e_A=bUE$(*1 z>0@NFdaFzs<%=>Li)I5pN0|ZE9tg6$s-MnwhZ;0z)W20f#%cB!IjXo$FThWu^In64 z@TES2g&bxXnrC}Ip)bQ@f^Bs|C&oS_e1=h896e`pwwVp)Wc`exc5naY0_Eo16$AEl z1F8vL=$&?POSIEaQR2Q%)E+>iwk;&MTs4Gl_da|7T^$-T*PqRAyd6XDqgNtsh4JMT z6Md2TAWlzvD6#R>x4H^qOd@@i4{cc$t9=WwWCWK`T2=zQTSHz_L>SXy;kxFToJT;Z zqR2K{$A}oiS6Q^fJBZ6NF9pe-VwbjKH;D^p4B9Uiq3}>wlY@z0XHj#iu0Zn(SP>&5 z3;mqJCbkoTl%=uSfT*Oxu^?qlL@r)OQ?z#si&0R(yL@kD*wEUh4qM_}RHPQ3`KW&_ zjdfW_kMx<`ESC{qoD8@)?clT=KG}AlPcK9|Q^?86!at1|mn~o2z0v)K;hEadRWt7V z_`IC*g+z=AN#4=p^($gYB67(9#n&TaEhjI8ePJ}#m?Ga2K$(C3&u^>!2n?8D$E z%3H;&K5HtCAq}A6*&#pCf=@_mq3BqWOMf6DV>9iO+}5J-EQg7RCf`Hq1r~aui1P=r z^MM*6HbD`o+q06mAHH?O-2vYqh7w*lQScD)o&WJv>vkGj#>s+y)h-NHWpeL5E{HyO zu(im|ZkHiiBC71y%sPb(vWcciw;0KXd&k9!L~}09?7rtmzq3?B>(#BBuFnt!4lvB& z{w%!qe|J6rx`kWiiml9o*vT8TuZ-&56Gj8@MAg;-$LH=%p>rzx#lh_;2XIdWjkV|i z+0hP=?b(Q{;Qu5oh=C2g5FKhKxapC_n@#M(2V;6y_zeX4{pliZ+9qm z)%d3OH8M<7YmHl&p^cdzk^c1?4&+K^br>0Mk~R~$$f?1MKJm5%q5RK5fF?G?s^9D+ z+s{7mYHzU+?cyI2?a%5CMh;YaAezJhRhF>y5brA!#md7jf~t+UiAX?iSo((?~zpu?CGKdeyq=!8|= z0AHiWgkQ!=RLXhPlhb_OChcmOYal;7-_z^*G~8b_4bqQiBEA!_oik1FI%VjT1M;17 zFmdA&%|hS05X<7=qDhXJLvP$or5=!Z>*>+1170<{Tf4RARJ+?{4R+JV+`*fx@hw_J z7|;(yYKk;mW9XnVp!PPZtb5v9!dH#~MKRa6Y5ew=;=$bQ%zms;zF}ea27#96Gt<8m zAwL)1G3AETT0H-vxBAY*1< zU6hrQin#hr?pGhe&L{j|+M*_CoZ9sOL-H|$)%n6aQ97zB!Kp9LSf)DzMm7|O4v%2h zbW#2^@8{f9O_P3-S{DRN&F!GZ<{|mSe@vCKFQCV1LEHo*}v2QPgpP#*bX4#O4p%jG@^eqY%0PUDMEJWych*xdY~^2Lmu`@SD<47hp< zmi14zE)GhL5jRF#-fsq6eMmg#fMJ2h;=AC*xZmjRi?N1>q43 z$j0?LgzsQ`d(!DS;Bkn!qJiNxUJKxmI~7=cS2aahRRIWArK_xI#D?gC_1mO`h+u9A z7DK>9oS>gpSp{JcY z^KAm&MYw*DpHq{-N8fGy;NMjG&@qgzXTVd&fCnQETmbo*u^cWd4}>or1mTfJ&8je) zo5f?z&`|ZKjuwnoIw4>t2JvwXMGIZdL~u`VhMhM22&aHk3C)&~SoM3~`<`e3l`X=` zMlLqW>JztkX+!{bEG2p8rJ)$o1Eg+Syx94#wY8l9ASOuY2Kr>?lAYvoZFiOq%C$qA z*{CRGuLO)9HG+ zDx$ZC&5ZDQ;9M^UBV2G+RSENAZS*>YLRUNOwm^Z@7-+&D6Q;sh z;>!F+Ei#2>B^A#sb`6ksYm>y~8NbcuX~Z>6IH4q>2IuyxV}!3johooNbkGSz#$Z## zW3viiVEShbx@l6igMxX|ZS>vCC_-V?cuFXmP*9z^OpWYGol5*B&>C7uLyc-x&R&M8 zHda4b`c~3EWKXo*rT8oYy(wme_zX%TWT%Ov86*QIbDRzGT?@qjY9inLoOBx#e}@Or zBkXgWG8}Qq&)z__HngC>uj6sUKjbfDZ}D$V41JFEOjju<`?ex%V8Yy>3HOCmHMLx5 z&t;xn%P8z>{?Qz;BEJS_i1LgLa{1^)~rgG39AG$ZdA*0MLX)uMtx!h zp2HE1!Hua4D+4odZxsC2-})H9f%QZeSz<0FvpQZ%Mk=U=H|Rr9!3pxWeDm&z#i_9TZVOkXuvDnT+HKnu!@ZM*&PCYX%7K(LuH z&POWOb3*;K9{C1RZ8LR<-Yz;M~q5g1$$T{6|wdto1~lx61Ghm;LJEZ$2mBlgUNr3$+Xibe z3`2_EmO<^{VS=2S4ZIV{giM*GxM|vVM4yEe!wl_Q$HIbVSX;U9?5pj6)J#iCl)$81K{j0=Vy=Ye4mJ6znx6%4^)4LYlPi^btX|9gP1>W@P9 zDc`w!k$g_$2yq-G# zVMS%|Sd}CsZb}7MX@?|DdNR7>ul<4V?vP9i5RQB;;F`bduL~D?UyuGIsp-j9Qb3z? zt5Kebt&Bi*6=|h_xWbd@HWM;rNAWGrj8`5hzK|~rt=2kK{{Ep;nxxCU*H+k!9=Nm* z+@=LFJPxZu)=PYhNpN9DZW_obq{ys|;dg7NQ=} zu2=)e-?^ycist6kgXV*M8ax=`EXqq_<+#^oET0Qlo4E@N&!2D6MLn$PD!@EO=c7f) z(pw*A#~Haf+G6Dal5Eg~v~UotZg3L}jnb)5enT}PcQ|UeM2y;fTeDTT_9n=+U9_~i zVW}`LM~kpVQ{bXKp`t(_i`FC?=t9CUpO+t z^UTnAP_o&vr_;|-6RrQX%ktwrcWkswlmr_b#@e}OEXaD=F{J^ryh@!Zl$SnCTciu2 z0$up*?!5&=yYpTk{bw;feRo|ZpV!exzc{Nn787z(_Tc?nRCf)Qk8YKY#Adi1b=*Fc z|GjeYsCh@!F>4Jz586Ghuw?{`l@q5kRsrY;&LR$UCJGXHcU@`5kyUNCBtK&3q+R>V z^&jP%gPR^8FXzlN>eH#rsK$O7=~sm&Vgju}s=z;nBFbGzVpSCI9M*3dlkuJhbZ?<$ z-8VokgjVep{w}JE`~`?!hK?A{e-=sX^fcolD%m{UL?wxtrGyaaB~4-yMa)bkiKB>7 zynrMr%fAyZl#hd@n5KuFitJm_6}vR4ioIiLxeJrqtxeYWm*q1CXAK0>FC<}O`#uN9 z4Mq94NU_mHA@FCPMb9v{g>apb90}?Pah02l&dp%*ocTqqgzE$L+0!I(#@Vi)E5BCG zteK@1Ng7R}rjw#lv1qMKiOmF~xQ#;X^QL;djtie4EJAbiR^yh>_*ow;F7{DqMY93e z5EIN>(l)vka)0t3s}+wUPoIpmaB_IV{<$Om{U zX^gMCvrpz0f>%;e#@QT9twL->)JzD;DjzJcvX}NiUC<6?{ZK_jpMQ z(`Z_Oj*8W}eN+yq9Gph*)62;!yU|2Jeewx1-$c~FyXxy%caziC ziM`w0%9MH1M?&_YSfG$%Rb&LY`vH-RlMOj1^PZi!Lo1F6n~h4%2ww^d zq4D2&C}S)Yd8QK6Y(r|x-Z;fhy51I80%k6SDjCyc%H@+faONjz){6d;@l<6Jxg{Y+ zpX9$K%-|q!jUCC@)B9Fwf%g^%vo-0)AfISC4zwNDDG80X)ysRqb7#M={@{lPxZSJ` zn;wSxE<9iPl^-ZWwETX0B1=q{nOUHmcYTh%8nDf!hsA1e20bJkwJSU?SF1oA22BVc zG?RfrS|FAZ0i#=Kt->qW%SwT#b!^lBQeHJgCEi1Tw?UvI(2Dr(I1mr{6;;Z`kEEm|izb=Yvx0;tsmA23l0#w3I~`Tw{`iBceNzTzi1;p{l6c z_Lm`;p}A<33~m)-Gj&yVpToxaiK9cW_@xP2+AuXw4|6~(*lG%s8ekZDxgwxX19zev zI+>*fS$@*_aB+7egh0b5c9UU5ZgO*5p^mP@*Q8*UDPVp3)01a&f%~Z+Dw&t3>$<&$ zKN5nZV5BdSmGSko?`&+V@XA*FG=K8ruGIU_B!l0V8AbcA!$);d<9TwsvXQ)%!F{r0 z;@h?ir84iBOXeDDTF5jJs@tG!!5)BKT_a~>TRP$-R0Q2>(JxV+7jqie*)ZvIWH|>l zP=wp={_wI$6svYsS}n|I!~&OvVSoWEKwqnBt&o(l%pGPbw^n-&KMQX zmT$ zx>PthPui1DR9;>#z`{u*qppQay;Ky%Og|`Ek-M07kR`e_q8y1_U-t*@z5*1zB~U|S zV2*@JF##xO)wyCxa+LTliRMt*Hh3T-4&F$TEkgDx!xe_Gt1-tDygk8h6Yj>lvKQ{1 zA(S2i0w1G~678=FCj7%11_XQ1VF5j3SXen-*FL9!%S zOx>%nE+)+`n7UBcV*2+CqzdiJ$Tg@!Vl#{f8~cSS6PZ^?4$JUV*>~Ng z)hzWL)rXp$ukDCE-5s(D?vuJWT{aGsvYLL9;`R#Ks(diFeFcZ+nEPP(GRv!_U7T}nw+)yW%alwU)#+6ZGOMLQB*OYWX`{B|k|3;Yu86K*95RCIr z`Hf*lw9q&lNt)Y6sg6ARAvk4mW~^_NyyvivB)p3nF-k1)@l1iWyS9`&duQG2{Zz6% zELv0NfuT)@9(k=b7=$ct^phU}6eM0pT`HRd>sUZSrg zjQBXpsE1b^47k!K8^{<0v?lP&wJUJOzsW$O1jedf<<(RpGS^ijVx3el0Lo!+CY_9>YsLm6xZGs4M&v-fAc#C+7Ptd_(Y4y^hUzJI z7xMm?_^F*-NRpPDC@ZgghHOE)yQaUV%6%OoFbzqc&mp)p((o4t0bt88J4H zo104H-4AA!2>}efEiyAx#9=gVlNN;mp6<3r-N`#ZYpk0VEG$0owmboD1w>tVv8Ro1 zu2VYXzK`%O8VGbe5qcN16mVH?h$feoy*#i>%H)P6XwEFDy7ddTBm@o8U?bB;C)v9h zggP-ZRTkrY`gyTEKy94@m(&5yP;(BJPx0ur^Y8<)CP& z@fbsxOPs$`{))8>2+aM;v!Jt+u;20ArnU;rqH*d0F@j?zq2Fz1re^#<#l>_+bAWDZDh`LAU7;|DHQp^_V>V z6JpgdVEixQz$SNnDbQn%QwzsnXYM}*4)i|=yJ*ZtaU%Mxhx4WoI_QCL zZj)iqx9ik{QbH1myT~-3G?13AXwd-Qv;sBUbb9J#vVAl*E5~+_+0##rhT4+_c~@8M z@JIC)$ECoQE4+@iZV0V~vF&bq6K@Q$v!qA~ZO&ESN7-4?!hi#Dfn}?5)+IX;n|ctn zvR*M`wrgwUSCe25Cgt+o9nNy{f~F-T9?4{CX*De6uA&m_d6jzNb$aoPLiFha=+H3s zLvVB44xJk8vDn60i2r`upTu{V~ktIT!sM5rEuu6wa6 z{;kpn^9>fKegXlE=`5^MZ#PE1r?u{FW!^!0i8oSiuutb8_eYMfTp5M;CeYmgvT&q5dI-a@Mz^Mr0mDt(ze?cz`Lm*^fXOm&0E1c)vvMZ z#8jmdqGB&<<}@4u(PQ8{i>z(aP1A*xsO{1R1`Ok~?k~N^#fRcr0_&^w?Y|saIRE-G zX+5ImKE3T`DP-;W9gJzeDhawOdU!ah?;p;p6IDPr;p6Enr2kD zYu~qSJMHYkuZRui&iQXPU#%`*Prm_??sT{p4dh7F__JGz%Qx3-c<*TF+Y`_2eEa*N z3a6G{^j{$G>#Rok;eoKad(p)BI?SXC=EX+8=_u}Wzy>OM78?w(IvQlOnNHn9_3yKW z3>^)c^lAm<2S`8yLKm73lQ=2&i~e{Z+zM%BkdMV9@w(tb6)c|JQxq-bG~Y@1UCPZ` zBMi2zUaI{retaIfuDro|Du?iybk6H&SjW^ccLX#xiLv-o(EOh+ey!yUlVKKH#(~V@3OJ6|@GG|=Qd?mm4fh6j+6uN)+~((HYJQ%|N$9ijiinE}-b$aFK3^?b zMT;l-!x!$mlRB-l`PL%;Aaz!gXW?$%xX_x5iTCLFF|S>fbpKa@2MIFDlHLZ!tA8n0 z(bUbSlQZ1T=8iaNcV8v#mtxgm86izayaSVAkys|GX$teE9N#ABa+92zHS7}p->`C8 zATn&@;XC#uEt=`s<``~ASuRRGg5eXaTWdN}(q}r{@I$)hy?^rp)dk}blZtXvy238z z=p;ZoW9qQ$CfY_yiuQ@Hh2E1L`hqyW5XtBs97yJ^R;>7~(1Y{8`mTk$x|Zo=_BemB z=84BKbfUt^Xlc&%XQA4-)rQ!e`?Gtaylo-tu0pk7_}tVgfl)Anw_{91qm9hpMVMd0 zCt}{FCb|;bw_;_T^eg(6CJ<=Uj5U<1Fcq(hP9cR$7nG?Eo;ZT%8AYP6E>_EwX=jI> z^!}Q-_Zt)@Rrs0j66YAr)`Cq&j7U>AyH1E|ZZ|$Go~DDr(dsgi6=?#E)trbXF(cNU zvB4VDoU?B83&8aaP~DmB><*eIPZa%xfAZ1YlYf~%ZJfH{;T`p{{E7DV=Q5io&xjNL z%$@kXhuHN60QxbXy6E)cF??+s4CC_)txCJZL6);=P7F^oVW3d5Wp30VffgU0dL^~^ zTJP)1e&Qt`s?q_`_WI$i=KF7tO3V<>kOtH)ID=IQ=w6t@VOTkjqj@iUQ+ zD(7%vdYe(nw1b1xbkM)bT)%Wy^bf#zT9Z|d_IMALWef0SZQ68Pswh)Z1c&J*X6%W~ ztOx##Mrjns`mpTPI0%H!uV{>zS6y$hy#RtBpyNjWQ5Ho^N8wlSlIa)#*cUY2BI zHN6pEqd-{yJ!X#3YdEbz$)%UZ0FnX#ET;gIlTn$P=F~lfKfKqE(_!!5(|6o@o(n=n( z1>dUU?ZkGfx&>~0Z$A1lqS+o9=*ZLI7r3b9mti#-D(Ohe0x&FQ&wI5d9ZE8AbMC@i z*P|lhjcT;1iO3(BR>R4}OsG=hqDkuTBH21s{=kXyJPm6Q7dDk@ue=_NwZ%PqZ7QiY za}e@kxz5qfa9`yM5K>h`iG4KZ=WUUD*s%Y4+7Fwqy~4+qGXpWy^I-Bi)DZ7x!+dOg;>g`HpOXEApr8_K3LN$JZG@GC+$+ZSM6KqaQhB zNX~W6C`w+*9XodJDr=>qLgJBO9$L?i8e>$;jKc(9AnO6o ztyqP_KZ*sPzLNHY2~yo?29HtR&c!gwZ!^)wIDHjc&D2?kHEnyD2!vS62o69eORRM; zjjbRFu31Y#jWWrUiXKlmqIT%ctHCXc12b04rHSV}d-h-~I0Vi7gcDnKdmu=vmUXNhM zJbr#QZ^+7mBTjTmo0KH8QS_hrk9;M^nX!j-8RwE;xUu0R?A+hRmd+<-iBqgal?8!$ z(oF??iwu&gf^mnBCj>Z~=g;b*qEx3n)4f~KwlQ|+AU4RQU)lX=8t8wMFKNI}gBY_w zaWC}YzttcdUpO>kSvt^xZeY+!P&@a$zw4tpi3J_Q5VQc{D$y*RyzW)z~gf2f=dtJGE491!V$VJ?YDJmN3ayKmbG9 z)t?E`7#szBQ?kZ{tSF`-OHB_;MBoE5j%BzGlj7DJeYcS#Nlyz08iKfz=kOx|+u`qC z6&GfmHgAgY8}7PyzDVI2ex}d`6^yQ)NAsBt(y{isa5osE!_C;={6 zQ<3N7JRx{ui`zsqcFabP9Kim#$dIvECq_?2Hmbo-yY3T`(1}V#clulfdH#`zF<0HX)M9R~;d~v+Gj?y>wj3FA^Rcf2O zSuNBM~;ew#*K`qd!And*8G*-ZkMzo6T8qTyL@pFZOlx35?5 zUa~J32doqGz*jX7&Wfq}usf=3?DVxWrvG9iD+nVRD4|-MgQjSp_4FJ%knd6~*pX_s zmwsVIs)O60-K3nC$N^tJDY|3gb5ODO4S^Ia$YCu=iv}MrmuSYNX0cFl0<0M1>=PW| z3eK(Psbe^+)qs?^q)EajWJ{Ek^$6*+Y4WK%Nq=&ZuE~kd|CvXRb`+;-Rn!Jru zP`{}qU+*)wpcXW4CRPwt!dAbiYmF8VZ3qRL7?f-9#gg@Tw7>yrM9`@Y@#0}({o;H) zG}hl}^ws|P;WH`Xl`MLKB*KU_Bbj?W9Q>O1>Hh`f3mf$Gi9?599{kAPqU`_D-~YF- z9v$80=RpQIDd_U2lFi8*+in^E{KU}qjhha!wg?hl?>rRogxaLM(vgY8f}ViK&xtNq zDt_JeiSA(akzIXV?W<#9u)0j(g_BR+H`dXH0)bR=eWs^lOIz20;~kl7U+qJIs6A&%;M`t4Ni2|*?Pgisa28L7ot+bB*5=wl6R*SD0_>M*;&mYIYuDi*Nvv^gZe4r1j%~*}wz(y1=-3r2 zd4gE5hP`spCAlJ&D}W0Gu>gJXpY)9W(UFJm z-Yut}Xu4zjvxWT!{r=D5-^k75t*evQJ48;1#=EB`?h8efcw5Y z*L6-!e)ZA+{gv+CJ^N6b{@CQ+z5AYh?u&c(E)?hij*EsPa%X+q`%VrCzej`7blff? zg1-);9P=9U4$0+|h1ShWr8EHL;jWj^L47~U(We#$3|#tuvaa0!pU)inKEAw&U{z@mL1$(0<7JDQfaY3SMUY>Q9x=Y}4b>FCA5F$N_Aol74;tUE zc2v<;^8IjLkqM64!Su2n&~FKRonE}oidw16`izxeR)Zmt2_>ZBYAa+BJ-nPRZrU1X znuO_M_kzN%bQ1cgDVG*qeJ!0@lrl0Y?y~dCr*~`++=P0SEo2QuDHrEX=O>qG8l`PO z(p-WwNZKYBNs8A-te37+WPPxV(DpJiLqCDe%sYh6GOm0i>$X>s&=u~fzlqE!S|U3R zR{-T_14d8Nu!Es<{_ilO*ZT#6JeFI!eD6Gzcp>+Rdd56w<& z9~nQgb%>rAzio7FZuY?Vi}@V7Wr(cz+rRjG_A*&xG6Ymk72_~?!b)+0NzGH=)U2FClk%Dsv(~O%p8;bwvT-TbuG2um029@J$#Jw={oQ!J zePHY4)Y0oU_J(u5kXzhE+ltFHBz6xCsugps6XRc- z8XkEp7N-+03gIT#cc$;y(cKe^Bb(jkvDzKN@_G$j9f!Wtw&q3s<)tMXlsEvHBNLO4 zO=*9pKf#oL#e58Ns}zsjyZ&*Hy$RL`@3JlWgr$*wvfTBp(mwOqvHzg>}#2 zN$VDX>17jKt>TQ$Y6qlIfw#+3VeR6K!!DNJ84zcx0yz5%Zz!9215cM>lZspcZo=o` z=@nC9EqJ1<==MpaioDWP*y8+XxP~iA@c$>mCr^d_gh?mp6Q;(NO+=w*|L`=~(j=9S zUisq_W|!iZpJG1+8i8_#oniJuCm%_lnJ@fy=e?Pgy7*ZH` z7?43H0IVhlrT_qV+C7m!Xp=!0#(&?v?|olDCx==~h6pN3D8-?OgNW3&ixw>op_Gsz zND0(RYYjD)QbPEU&>;keSn&`3sgw@l5FDh43PLB_MLLLNDk9=kaVhZ)iU$wRargf2 zo_o)I=R7vtDg-O$4a%BfN6%98eJmzYK{_l@bX)95$fk7DuSp(g7xQ&wAZNk%or zA>U0bDPC&Gh_pDY)6DBdM)U(q@{@6WX7zQ-lH`#boCPA<4SsyGj7>3mARx3xgwva>O1D-8#A)sm|VN1 zZ^^rBOt~g#mT?zT30D}>ifP*JTjHRuF)2Na8F#_B31cSg?PbeUl0C5XP=1h=KHf`L za6-;m%`H|ty_YQ}HOH)FY({mScY5nnypc*!kvxPOR`bu@fc@7iQ|hnyY(8e5wQR=y z7t|6adt1r=FrhWBT84z?f}{R4qaCJmjz7uFz6J|=+2*&2t)Fq?j>-D<2N#)q?*IUJ z+GAi~FoMDyhCap|rdupbtPZR(tX-@-*mT%R*w(R~U}s=AV4uLgjU$2M7pDj34lW0- zI&M4eV>}8xc|0vVYk2i;z2H2O6DX-a7B&@uqR7uxf*f9c%Q z`JuZ{_ZANLMUPKUL(f4kLT`%R6@3r=L;AlA;y}=l!|M;7Q}@M*Mqpw zMMM1o1y?F4Zi>*-ePUx9h^aQQBKQQ(`#afkVB= z40O(fVlfOW67g~=azznS-6sk{MVDkcrctMGQKHwXtad8qQ0iEmO!1X)rmW#k{75*J z^P7HRO>SMv7T^4%m^g<*xa#akJ_C0yxM!q{#m($o%u-6rU)73`6V6+X{o1kVHm z3@anbXhvP#)pf_=$x-V3U_W_roLL!(jG=@%o;pAK8{#TQB9_UK)=&nN!C&`eR*e9- z0sn=t0gV7lun16Cu>k>&S+Pb2D_9}05LrvG6jE3ruz*<|um&q!SOBmFAVshPD?|aX O2OveT11m&XuoNp_HoX-9 literal 0 HcmV?d00001 diff --git a/docs/source/_themes/finat/static/nobile-fontfacekit/nobile_italic-webfont.eot b/docs/source/_themes/finat/static/nobile-fontfacekit/nobile_italic-webfont.eot new file mode 100755 index 0000000000000000000000000000000000000000..c8f700921b4ae7bc5cafa3192c77740254d730a6 GIT binary patch literal 34264 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 YkzHLua5gv^BciJT+y$zmGh%i98 + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 20072010 by vernon adams All rights reserved + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/source/_themes/finat/static/nobile-fontfacekit/nobile_italic-webfont.ttf b/docs/source/_themes/finat/static/nobile-fontfacekit/nobile_italic-webfont.ttf new file mode 100755 index 0000000000000000000000000000000000000000..ff56b0e0aee70be62a09a252fe26ea0faacbec6f GIT binary patch literal 34036 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 N5!?l;rz>i6{TpJ8$W8zN literal 0 HcmV?d00001 diff --git a/docs/source/_themes/finat/static/nobile-fontfacekit/nobile_italic-webfont.woff b/docs/source/_themes/finat/static/nobile-fontfacekit/nobile_italic-webfont.woff new file mode 100755 index 0000000000000000000000000000000000000000..af1582bbfaccf3fb6a5c9e29835d415d12faf8c5 GIT binary patch literal 21824 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 O2OveT11m&XuoNqLtN#lC literal 0 HcmV?d00001 diff --git a/docs/source/_themes/finat/static/nobile-fontfacekit/stylesheet.css b/docs/source/_themes/finat/static/nobile-fontfacekit/stylesheet.css new file mode 100755 index 000000000..10d0a7c52 --- /dev/null +++ b/docs/source/_themes/finat/static/nobile-fontfacekit/stylesheet.css @@ -0,0 +1,52 @@ +/* Generated by Font Squirrel (http://www.fontsquirrel.com) on June 12, 2011 05:30:25 AM America/New_York */ + + + +@font-face { + font-family: 'NobileRegular'; + src: url('nobile-webfont.eot'); + src: url('nobile-webfont.eot?#iefix') format('embedded-opentype'), + url('nobile-webfont.woff') format('woff'), + url('nobile-webfont.ttf') format('truetype'), + url('nobile-webfont.svg#NobileRegular') format('svg'); + font-weight: normal; + font-style: normal; + +} + +@font-face { + font-family: 'NobileItalic'; + src: url('nobile_italic-webfont.eot'); + src: url('nobile_italic-webfont.eot?#iefix') format('embedded-opentype'), + url('nobile_italic-webfont.woff') format('woff'), + url('nobile_italic-webfont.ttf') format('truetype'), + url('nobile_italic-webfont.svg#NobileItalic') format('svg'); + font-weight: normal; + font-style: normal; + +} + +@font-face { + font-family: 'NobileBold'; + src: url('nobile_bold-webfont.eot'); + src: url('nobile_bold-webfont.eot?#iefix') format('embedded-opentype'), + url('nobile_bold-webfont.woff') format('woff'), + url('nobile_bold-webfont.ttf') format('truetype'), + url('nobile_bold-webfont.svg#NobileBold') format('svg'); + font-weight: normal; + font-style: normal; + +} + +@font-face { + font-family: 'NobileBoldItalic'; + src: url('nobile_bold_italic-webfont.eot'); + src: url('nobile_bold_italic-webfont.eot?#iefix') format('embedded-opentype'), + url('nobile_bold_italic-webfont.woff') format('woff'), + url('nobile_bold_italic-webfont.ttf') format('truetype'), + url('nobile_bold_italic-webfont.svg#NobileBoldItalic') format('svg'); + font-weight: normal; + font-style: normal; + +} + diff --git a/docs/source/_themes/finat/static/sample-news-image.png b/docs/source/_themes/finat/static/sample-news-image.png new file mode 100644 index 0000000000000000000000000000000000000000..0534438d35554cfb5537934f31e1ed88616916db GIT binary patch literal 3219 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 literal 0 HcmV?d00001 diff --git a/docs/source/_themes/finat/static/social-buttons.html b/docs/source/_themes/finat/static/social-buttons.html new file mode 100644 index 000000000..c2cdef39e --- /dev/null +++ b/docs/source/_themes/finat/static/social-buttons.html @@ -0,0 +1,44 @@ + + + + + + A test of social buttons + + + +

+ + + + +
+
+

Let’s be social!

+
+
+
+
+
+

Please test these buttons by logging into your social profiles.

+
+
+

© 2011

+
+
+ + diff --git a/docs/source/_themes/finat/static/transparent.gif b/docs/source/_themes/finat/static/transparent.gif new file mode 100644 index 0000000000000000000000000000000000000000..0341802e5111ad58a05ef6e93e59bfa6e5d3d1a6 GIT binary patch literal 49 tcmZ?wbhEHbWMp7un8*ME|G@yrQ2fcl$j-pTpaT*G$ulr9g)lN$0{}SU3LpRg literal 0 HcmV?d00001 diff --git a/docs/source/_themes/finat/static/unknown.png b/docs/source/_themes/finat/static/unknown.png new file mode 100644 index 0000000000000000000000000000000000000000..f97413690af2d74fdb3908aff2ea8255119a44f8 GIT binary patch literal 2802 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 v documentation". @@ -148,7 +151,7 @@ # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} @@ -203,8 +206,8 @@ # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + 'papersize': 'a4paper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', From 969bcbfc10727873e0c25604a4447934cae4da29 Mon Sep 17 00:00:00 2001 From: David Ham Date: Thu, 14 Aug 2014 20:57:49 +0100 Subject: [PATCH 015/749] Documentation builder fixes --- docs/Makefile | 2 ++ docs/source/_themes/finat/static/fenics.css_t | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index 74a982dc5..d866dab1c 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -7,6 +7,8 @@ SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build +PYTHONPATH := $(PWD)/..:$(PYTHONPATH) + # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) diff --git a/docs/source/_themes/finat/static/fenics.css_t b/docs/source/_themes/finat/static/fenics.css_t index 6fe485ade..e562584cd 100644 --- a/docs/source/_themes/finat/static/fenics.css_t +++ b/docs/source/_themes/finat/static/fenics.css_t @@ -244,11 +244,11 @@ div.note { background: #e1ecfe url(dialog-note.png) no-repeat 10px 8px; } -#firedrake-package #id1 { +#finat-package #id1 { display: none; } -#firedrake-package h2 { +#finat-package h2 { background-color: #fafafa; border: 2px solid #000080; border-right-style: none; @@ -256,7 +256,7 @@ div.note { padding: 10px 20px 10px 60px; } -#firedrake-package div.section>dl { +#finat-package div.section>dl { border: 2px solid #ddd; border-right-style: none; border-bottom-style: none; @@ -630,7 +630,7 @@ display: none; margin-right: 1em; } -#the-firedrake-team td { +#the-finat-team td { text-align: center; } @@ -661,12 +661,12 @@ img[alt="build status"] { } /* Equispace the logos. */ -#firedrake-is-supported-by tr { +#finat-is-supported-by tr { width: 100%; } /* Equispace the logos. */ -#firedrake-is-supported-by td { +#finat-is-supported-by td { width: 25%; text-align: center; } From b17757356b41c28cc2a287c0e3d60ee7b819d4ce Mon Sep 17 00:00:00 2001 From: David Ham Date: Thu, 14 Aug 2014 21:01:04 +0100 Subject: [PATCH 016/749] fecking lint --- docs/source/conf.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 44f917c8c..ae90613e2 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -12,9 +12,6 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys -import os - # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. @@ -209,22 +206,22 @@ # The paper size ('letterpaper' or 'a4paper'). 'papersize': 'a4paper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + #'preamble': '', -# Latex figure (float) alignment -#'figure_align': 'htbp', + # Latex figure (float) alignment + #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ('index', 'FInAT.tex', u'FInAT Documentation', - u'David A. Ham and Robert C. Kirby', 'manual'), + ('index', 'FInAT.tex', u'FInAT Documentation', + u'David A. Ham and Robert C. Kirby', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -267,9 +264,9 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'FInAT', u'FInAT Documentation', - u'David A. Ham and Robert C. Kirby', 'FInAT', 'One line description of project.', - 'Miscellaneous'), + ('index', 'FInAT', u'FInAT Documentation', + u'David A. Ham and Robert C. Kirby', 'FInAT', 'One line description of project.', + 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. From 1f4e86accaf82cda6f3338cb9a66fba5d1b890ef Mon Sep 17 00:00:00 2001 From: David A Ham Date: Fri, 15 Aug 2014 14:42:22 +0100 Subject: [PATCH 017/749] Some steps towards working vectorfiniteelement Introduce ForAll and the beginnings of Wave. Put some mathematical documentation in. Use the proper delta form for vector basis evaluation. --- finat/ast.py | 96 ++++++++++++++++++++++++++++++++++-- finat/finiteelementbase.py | 15 +++++- finat/lagrange.py | 4 +- finat/utils.py | 14 ------ finat/vectorfiniteelement.py | 44 ++++++++++++++--- 5 files changed, 144 insertions(+), 29 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index 97887857f..8188f4ad1 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -1,5 +1,6 @@ +import pymbolic.primitives as p from pymbolic.mapper import IdentityMapper - +from indices import DimensionIndex, BasisFunctionIndex, PointIndex class _IndexMapper(IdentityMapper): def __init__(self, replacements): @@ -27,9 +28,9 @@ class Recipe(object): """AST snippets and data corresponding to some form of finite element evaluation.""" def __init__(self, indices, instructions, depends): - self._indices = indices - self._instructions = instructions - self._depends = depends + self._indices = tuple(indices) + self._instructions = tuple(instructions) + self._depends = tuple(depends) @property def indices(self): @@ -37,6 +38,25 @@ def indices(self): return self._indices + @property + def split_indices(self): + '''The free indices in this :class:`Recipe` split into dimension + indices, basis function indices, and point indices.''' + + d = [] + b = [] + p = [] + + for i in self._indices: + if isinstance(i, DimensionIndex): + d.append(i) + elif isinstance(i, BasisFunctionIndex): + b.append(i) + if isinstance(i, PointIndex): + p.append(i) + + return map(tuple, (d, b, p)) + @property def instructions(self): '''The actual instructions making up this :class:`Recipe`.''' @@ -71,3 +91,71 @@ def replace_indices(self, replacements): substituted.""" return _IndexMapper(replacements)(self) + + +class IndexSum(p._MultiChildExpression): + """A symbolic expression for a sum over one or more indices. + + :param indices: a sequence of indices over which to sum. + :param body: the expression to sum. + """ + def __init__(self, indices, body): + + self.children = (indices, body) + + def __getinitargs__(self): + return self.children + + def __str__(self): + return "IndexSum(%s, %s)" % (str([x._str_extent for x in self.children[0]]), + self.children[1]) + + +class ForAll(p._MultiChildExpression): + """A symbolic expression to indicate that the body will actually be + evaluated for all of the values of its free indices. This enables + index simplification to take place. + + :param indices: a sequence of indices to bind. + :param body: the expression to evaluate. + + """ + def __init__(self, indices, body): + + self.children = (indices, body) + + def __getinitargs__(self): + return self.children + + def __str__(self): + return "Forall(%s, %s)" % (str([x._str_extent for x in self.children[0]]), + self.children[1]) + + +class Wave(p._MultiChildExpression): + """A symbolic expression with loop-carried dependencies.""" + + def __init__(self, index, variables, base, expr): + pass + +class Delta(p._MultiChildExpression): + """The Kronecker delta expressed as a ternary operator: + +.. math:: + + \delta[i_0,i_1]*\mathrm{body}. + +:params indices: a sequence of indices. +:params body: an expression. + +The body expression will be returned if the values of the indices match. Otherwise 0 will be returned. + """ + def __init__(self, indices, body): + + self.children = (indices, body) + + def __getinitargs__(self): + return self.children + + def __str__(self): + return "Delta(%s, %s)" % self.children diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 785427ed9..531773d2e 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -81,8 +81,19 @@ def pullback(self, derivative): raise NotImplementedError def moment_evaluation(self, value, weights, points, static_data, derivative=None): - '''Return code for evaluating value * v * dx where - v is a test function. + '''Return code for evaluating: + + .. math:: + + \int \mathrm{value} : v\, \mathrm{d}x + + where :math:`v` is a test function or the derivative of a test + function, and : is the inner product operator. + + :param value: an expression. The free indices in value must match those in v. + :param weights: a point set of quadrature weights. + :param static_data: the :class:`.KernelData` object corresponding to the current kernel. + :param derivative: the derivative to take of the test function. ''' raise NotImplementedError diff --git a/finat/lagrange.py b/finat/lagrange.py index 93c68761f..a0e94251f 100644 --- a/finat/lagrange.py +++ b/finat/lagrange.py @@ -1,7 +1,7 @@ import pymbolic.primitives as p from finiteelementbase import FiniteElementBase -from ast import Recipe -from utils import doc_inherit, IndexSum +from ast import Recipe, IndexSum +from utils import doc_inherit import FIAT import indices from derivatives import div, grad, curl diff --git a/finat/utils.py b/finat/utils.py index 68494b959..5b051b91c 100644 --- a/finat/utils.py +++ b/finat/utils.py @@ -1,4 +1,3 @@ -import pymbolic.primitives as p from functools import wraps @@ -77,16 +76,3 @@ def __init__(self): self.static = {} self.params = {} - - -class IndexSum(p._MultiChildExpression): - def __init__(self, indices, body): - - self.children = (indices, body) - - def __getinitargs__(self): - return self.children - - def __str__(self): - return "IndexSum(%s, %s)" % (str([x._str_extent for x in self.children[0]]), - self.children[1]) diff --git a/finat/vectorfiniteelement.py b/finat/vectorfiniteelement.py index 96d006607..10ee6a12f 100644 --- a/finat/vectorfiniteelement.py +++ b/finat/vectorfiniteelement.py @@ -1,12 +1,35 @@ from finiteelementbase import FiniteElementBase from derivatives import div, grad, curl -from utils import doc_inherit, IndexSum -from ast import Recipe +from utils import doc_inherit +from ast import Recipe, IndexSum, Delta import indices class VectorFiniteElement(FiniteElementBase): + def __init__(self, element, dimension): + r"""A Finite element whose basis functions have the form: + + .. math:: + + \boldsymbol\phi_{\alpha,i} = \mathbf{e}_{\alpha}\phi_i + + Where :math:`\{\mathbf{e}_\alpha,\, \alpha=0\ldots\mathrm{dim}\}` is + the basis for :math:`\mathbb{R}^{\mathrm{dim}}` and + :math:`\{\phi_i\}` is the basis for the corresponding scalar + finite element space. + + :param element: The scalar finite element. + :param dimension: The geometric dimension of the vector element. + + :math:`\boldsymbol\phi_{\alpha,i}` is, of course, vector-valued. If + we subscript the vector-value with :math:`\beta` then we can write: + + .. math:: + \boldsymbol\phi_{\beta,(\alpha,i)} = \delta_{\beta,\alpha}\phi_i + + This form enables the simplification of the loop nests which + will eventually be created, so it is the form we employ here. """ super(VectorFiniteElement, self).__init__() self._cell = element._cell @@ -24,12 +47,18 @@ def basis_evaluation(self, points, kernel_data, derivative=None): # Produce the base scalar recipe sr = self._base_element.basis_evaluation(points, kernel_data, derivative) + # Additional basis function index along the vector dimension. + alpha = indices.BasisFunctionIndex(points.points.shape[1]) # Additional dimension index along the vector dimension. Note # to self: is this the right order or does this index come # after any derivative index? - alpha = indices.DimensionIndex(points.points.shape[1]) + beta = indices.DimensionIndex(points.points.shape[1]) - return Recipe([alpha] + sr.indices, sr.instructions, sr.depends) + d, b, p = sr.split_indices + + return Recipe((beta,) + d + (alpha,) + b + p, + Delta((beta, alpha), sr), + sr.depends) @doc_inherit def field_evaluation(self, field_var, points, @@ -38,13 +67,14 @@ def field_evaluation(self, field_var, points, basis = self._base_element.basis_evaluation(self, points, kernel_data, derivative) - alpha = indices.DimensionIndex(points.points.shape[1]) + alpha = indices.BasisFunctionIndex(points.points.shape[1]) + beta = indices.DimensionIndex(points.points.shape[1]) ind = basis.indices if derivative is None: - free_ind = [alpha, ind[-1]] + free_ind = [beta, alpha, ind[-1]] else: - free_ind = [alpha, ind[0], ind[-1]] + free_ind = [beta, alpha, ind[0], ind[-1]] i = ind[-2] From b6efcebba8d544d5ab3a73388b7536ff68545a33 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Fri, 15 Aug 2014 14:55:24 +0100 Subject: [PATCH 018/749] fecking lint --- finat/ast.py | 4 +++- finat/vectorfiniteelement.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index 8188f4ad1..4f20382e4 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -2,6 +2,7 @@ from pymbolic.mapper import IdentityMapper from indices import DimensionIndex, BasisFunctionIndex, PointIndex + class _IndexMapper(IdentityMapper): def __init__(self, replacements): super(_IndexMapper, self).__init__() @@ -135,9 +136,10 @@ def __str__(self): class Wave(p._MultiChildExpression): """A symbolic expression with loop-carried dependencies.""" - def __init__(self, index, variables, base, expr): + def __init__(self, index, base, expr): pass + class Delta(p._MultiChildExpression): """The Kronecker delta expressed as a ternary operator: diff --git a/finat/vectorfiniteelement.py b/finat/vectorfiniteelement.py index 10ee6a12f..374e57ef4 100644 --- a/finat/vectorfiniteelement.py +++ b/finat/vectorfiniteelement.py @@ -24,7 +24,7 @@ def __init__(self, element, dimension): :math:`\boldsymbol\phi_{\alpha,i}` is, of course, vector-valued. If we subscript the vector-value with :math:`\beta` then we can write: - + .. math:: \boldsymbol\phi_{\beta,(\alpha,i)} = \delta_{\beta,\alpha}\phi_i From e3461ca04df02d39d185078c6863a51b064d0030 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Fri, 15 Aug 2014 15:23:00 +0100 Subject: [PATCH 019/749] Remove unfortunate cast to tuple --- finat/ast.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index 4f20382e4..e4d0e3ec9 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -30,7 +30,7 @@ class Recipe(object): evaluation.""" def __init__(self, indices, instructions, depends): self._indices = tuple(indices) - self._instructions = tuple(instructions) + self._instructions = instructions self._depends = tuple(depends) @property @@ -87,6 +87,9 @@ def __getitem__(self, index): return self.replace_indices(replacements) + def __str__(self): + return "Recipe(%s, %s)" % (self._indices, self._instructions) + def replace_indices(self, replacements): """Return a copy of this :class:`Recipe` with some of the indices substituted.""" @@ -129,7 +132,7 @@ def __getinitargs__(self): return self.children def __str__(self): - return "Forall(%s, %s)" % (str([x._str_extent for x in self.children[0]]), + return "ForAll(%s, %s)" % (str([x._str_extent for x in self.children[0]]), self.children[1]) From 6ab527c8516e33c23aec11146b9bbb3f3e445578 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Fri, 15 Aug 2014 18:25:43 +0100 Subject: [PATCH 020/749] Make Recipe an expression --- finat/ast.py | 28 +++++++++++++++++++++++++--- finat/lagrange.py | 10 +++++----- finat/vectorfiniteelement.py | 17 +++++++---------- 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index e4d0e3ec9..35f04b2d1 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -1,6 +1,7 @@ import pymbolic.primitives as p from pymbolic.mapper import IdentityMapper -from indices import DimensionIndex, BasisFunctionIndex, PointIndex +from pymbolic.mapper.stringifier import StringifyMapper, PREC_NONE +from indices import IndexBase, DimensionIndex, BasisFunctionIndex, PointIndex class _IndexMapper(IdentityMapper): @@ -24,14 +25,24 @@ def map_foreign(self, expr, *args): return super(_IndexMapper, self).map_foreign(expr, *args) -# Probably Recipe should be a pymbolic.Expression -class Recipe(object): +class _StringifyMapper(StringifyMapper): + + def map_recipe(self, expr, enclosing_prec): + return self.format("Recipe(%s, %s)", + self.rec(expr.indices, PREC_NONE), + self.rec(expr.instructions, PREC_NONE)) + + +class Recipe(p.Expression): """AST snippets and data corresponding to some form of finite element evaluation.""" def __init__(self, indices, instructions, depends): self._indices = tuple(indices) self._instructions = instructions self._depends = tuple(depends) + self.children = instructions + + mapper_method = "map_recipe" @property def indices(self): @@ -70,6 +81,9 @@ def depends(self): return self._depends + def __getinitargs__(self): + return self._indices, self._instructions, self._depends + def __getitem__(self, index): replacements = {} @@ -87,6 +101,9 @@ def __getitem__(self, index): return self.replace_indices(replacements) + def stringifier(self): + return _StringifyMapper + def __str__(self): return "Recipe(%s, %s)" % (self._indices, self._instructions) @@ -105,6 +122,11 @@ class IndexSum(p._MultiChildExpression): """ def __init__(self, indices, body): + if isinstance(indices[0], IndexBase): + indices = tuple(indices) + else: + indices = (indices,) + self.children = (indices, body) def __getinitargs__(self): diff --git a/finat/lagrange.py b/finat/lagrange.py index a0e94251f..7e4cc9e80 100644 --- a/finat/lagrange.py +++ b/finat/lagrange.py @@ -80,11 +80,11 @@ def _tabulation_variable(self, points, kernel_data, derivative): i = indices.BasisFunctionIndex(fiat_element.space_dimension()) q = indices.PointIndex(points.points.shape[0]) - ind = [i, q] + ind = (i, q) if derivative is grad: alpha = indices.DimensionIndex(points.points.shape[1]) - ind = [alpha] + ind + ind = (alpha,) + ind return phi, ind @@ -106,13 +106,13 @@ def field_evaluation(self, field_var, points, phi, ind = self._tabulation_variable(points, kernel_data, derivative) if derivative is None: - free_ind = [ind[-1]] + free_ind = (ind[-1],) else: - free_ind = [ind[0], ind[-1]] + free_ind = (ind[0], ind[-1]) i = ind[-2] - instructions = [IndexSum([i], field_var[i] * phi[ind])] + instructions = [IndexSum((i,), field_var[i] * phi[ind])] depends = [field_var] diff --git a/finat/vectorfiniteelement.py b/finat/vectorfiniteelement.py index 374e57ef4..58d74c75e 100644 --- a/finat/vectorfiniteelement.py +++ b/finat/vectorfiniteelement.py @@ -64,21 +64,18 @@ def basis_evaluation(self, points, kernel_data, derivative=None): def field_evaluation(self, field_var, points, kernel_data, derivative=None): - basis = self._base_element.basis_evaluation(self, points, + basis = self._base_element.basis_evaluation(points, kernel_data, derivative) - alpha = indices.BasisFunctionIndex(points.points.shape[1]) - beta = indices.DimensionIndex(points.points.shape[1]) - ind = basis.indices + alpha = indices.DimensionIndex(points.points.shape[1]) - if derivative is None: - free_ind = [beta, alpha, ind[-1]] - else: - free_ind = [beta, alpha, ind[0], ind[-1]] + d, b, p = basis.split_indices + + free_ind = (alpha,) + d + p - i = ind[-2] + i = b[0] - instructions = [IndexSum(i, field_var[i, alpha] * basis[ind])] + instructions = IndexSum(i, field_var[i, alpha] * basis) depends = [field_var] From d7b21275d06b635ba8ad65faf5c24e977ab15629 Mon Sep 17 00:00:00 2001 From: David Ham Date: Sun, 17 Aug 2014 12:46:11 +0100 Subject: [PATCH 021/749] Add Let expressions --- finat/ast.py | 47 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index 35f04b2d1..b2ee2766a 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -1,3 +1,6 @@ +"""This module defines the additional Pymbolic nodes which are +required to define Finite Element expressions in FInAT. +""" import pymbolic.primitives as p from pymbolic.mapper import IdentityMapper from pymbolic.mapper.stringifier import StringifyMapper, PREC_NONE @@ -165,6 +168,32 @@ def __init__(self, index, base, expr): pass +class Let(p._MultiChildExpression): + """A Let expression enables local variable bindings in an +expression. This feature is lifted more or less directly from +Scheme. + +:param bindings: A tuple of pairs. The first entry in each pair is a + :class:`pymbolic.Variable` to be defined as the second entry, which + must be an expression. +:param body: The expression making up the body of the expression. The + value of the Let expression is the value of this expression. + + """ + + def __init__(self, bindings, body): + try: + for b in bindings: + assert len(b) == 2 + except: + raise FInATSyntaxError("Let bindings must be a tuple of pairs") + + super(Wave, self).__init__((bindings, body)) + + def __str__(self): + return "Let(%s)" % self.children + + class Delta(p._MultiChildExpression): """The Kronecker delta expressed as a ternary operator: @@ -172,17 +201,27 @@ class Delta(p._MultiChildExpression): \delta[i_0,i_1]*\mathrm{body}. -:params indices: a sequence of indices. -:params body: an expression. +:param indices: a sequence of indices. +:param body: an expression. + +The body expression will be returned if the values of the indices +match. Otherwise 0 will be returned. -The body expression will be returned if the values of the indices match. Otherwise 0 will be returned. """ def __init__(self, indices, body): + if len(indices != 2): + raise FInATSyntaxError( + "Delta statement requires exactly two indices") - self.children = (indices, body) + super(Delta, self).__init__((indices, body)) def __getinitargs__(self): return self.children def __str__(self): return "Delta(%s, %s)" % self.children + + +class FInATSyntaxError(Exception): + """Exception raised when the syntax rules of the FInAT ast are violated.""" + pass From e4e7c7a10c9f5892daae5680c60625118e8bb8c7 Mon Sep 17 00:00:00 2001 From: David Ham Date: Sun, 17 Aug 2014 18:22:59 +0100 Subject: [PATCH 022/749] Refactor the indices of Recipe Recipe now takes a triple of tuples of indices. This significantly simplifies the implementation of finite elements. Consequently, the lagrange and vectorfiniteelement modules have been updated. The latter now appears to correctly handle cancellation of zeros. --- finat/ast.py | 87 +++++++++++------------------ finat/lagrange.py | 63 +++++++-------------- finat/vectorfiniteelement.py | 104 ++++++++++++++++++----------------- 3 files changed, 106 insertions(+), 148 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index b2ee2766a..ba787f107 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -13,19 +13,17 @@ def __init__(self, replacements): self.replacements = replacements - def map_index(self, expr): + def map_index(self, expr, *args): '''Replace indices if they are in the replacements list''' + print expr, self.replacements try: return(self.replacements[expr]) - except IndexError: + except KeyError: return expr - def map_foreign(self, expr, *args): - - if isinstance(expr, Recipe): - return Recipe.replace_indices(self.replacements) - else: - return super(_IndexMapper, self).map_foreign(expr, *args) + def map_recipe(self, expr, *args): + return Recipe(self.rec(expr.indices), + self.rec(expr.expression)) class _StringifyMapper(StringifyMapper): @@ -33,59 +31,35 @@ class _StringifyMapper(StringifyMapper): def map_recipe(self, expr, enclosing_prec): return self.format("Recipe(%s, %s)", self.rec(expr.indices, PREC_NONE), - self.rec(expr.instructions, PREC_NONE)) + self.rec(expr.expression, PREC_NONE)) class Recipe(p.Expression): """AST snippets and data corresponding to some form of finite element - evaluation.""" - def __init__(self, indices, instructions, depends): - self._indices = tuple(indices) - self._instructions = instructions - self._depends = tuple(depends) - self.children = instructions - - mapper_method = "map_recipe" - - @property - def indices(self): - '''The free indices in this :class:`Recipe`.''' - - return self._indices - - @property - def split_indices(self): - '''The free indices in this :class:`Recipe` split into dimension - indices, basis function indices, and point indices.''' - - d = [] - b = [] - p = [] + evaluation. - for i in self._indices: - if isinstance(i, DimensionIndex): - d.append(i) - elif isinstance(i, BasisFunctionIndex): - b.append(i) - if isinstance(i, PointIndex): - p.append(i) + A :class:`Recipe` associates an ordered set of indices with an + expression. - return map(tuple, (d, b, p)) - - @property - def instructions(self): - '''The actual instructions making up this :class:`Recipe`.''' - - return self._instructions - - @property - def depends(self): - '''The input fields of this :class:`Recipe`.''' + :param indices: A 3-tuple containing the ordered free indices in the + expression. The first entry is a tuple of :class:`DimensionIndex`, + the second is a tuple of :class:`BasisFunctionIndex`, and + the third is a tuple of :class:`PointIndex`. + Any of the tuples may be empty. + :param expression: The expression returned by this :class:`Recipe`. + """ + def __init__(self, indices, expression): + try: + assert len(indices) == 3 + except: + raise FInATSyntaxError("Indices must be a triple of tuples") + self.indices = tuple(indices) + self.expression = expression - return self._depends + mapper_method = "map_recipe" def __getinitargs__(self): - return self._indices, self._instructions, self._depends + return self.indices, self.expression def __getitem__(self, index): @@ -108,12 +82,15 @@ def stringifier(self): return _StringifyMapper def __str__(self): - return "Recipe(%s, %s)" % (self._indices, self._instructions) + return "Recipe(%s, %s)" % (self.indices, self.expression) def replace_indices(self, replacements): """Return a copy of this :class:`Recipe` with some of the indices substituted.""" + if not isinstance(replacements, dict): + replacements = {a: b for (a, b) in replacements} + return _IndexMapper(replacements)(self) @@ -136,7 +113,7 @@ def __getinitargs__(self): return self.children def __str__(self): - return "IndexSum(%s, %s)" % (str([x._str_extent for x in self.children[0]]), + return "IndexSum(%s, %s)" % (tuple(map(str, self.children[0])), self.children[1]) @@ -164,7 +141,7 @@ def __str__(self): class Wave(p._MultiChildExpression): """A symbolic expression with loop-carried dependencies.""" - def __init__(self, index, base, expr): + def __init__(self, var, index, base, expr): pass diff --git a/finat/lagrange.py b/finat/lagrange.py index 7e4cc9e80..a76fd8172 100644 --- a/finat/lagrange.py +++ b/finat/lagrange.py @@ -57,11 +57,11 @@ def _tabulate(self, points, derivative): raise ValueError( "Lagrange elements do not have a %s operation") % derivative - def _tabulation_variable(self, points, kernel_data, derivative): + def basis_evaluation(self, points, kernel_data, derivative=None): '''Produce the variable for the tabulation of the basis functions or their derivative. Also return the relevant indices. - updates the requisite static data, which in this case + updates the requisite static kernel data, which in this case is just the matrix. ''' static_key = (id(self), id(points), id(derivative)) @@ -80,67 +80,42 @@ def _tabulation_variable(self, points, kernel_data, derivative): i = indices.BasisFunctionIndex(fiat_element.space_dimension()) q = indices.PointIndex(points.points.shape[0]) - ind = (i, q) - if derivative is grad: alpha = indices.DimensionIndex(points.points.shape[1]) - ind = (alpha,) + ind - - return phi, ind - - @doc_inherit - def basis_evaluation(self, points, kernel_data, derivative=None): - - phi, ind = self._tabulation_variable(points, kernel_data, derivative) - - instructions = [phi[ind]] - - depends = [] + ind = ((alpha,), (i,), (q,)) + else: + ind = ((), (i,), (q,)) - return Recipe(ind, instructions, depends) + return Recipe(indices=ind, expression=phi[ind[0] + ind[1] + ind[2]]) @doc_inherit def field_evaluation(self, field_var, points, kernel_data, derivative=None): - phi, ind = self._tabulation_variable(points, kernel_data, derivative) + basis = self.basis_evaluation(points, kernel_data, derivative) + (d, b, p) = basis.indices + phi = basis.expression - if derivative is None: - free_ind = (ind[-1],) - else: - free_ind = (ind[0], ind[-1]) + expr = IndexSum(b, field_var[b[0]] * phi) - i = ind[-2] - - instructions = [IndexSum((i,), field_var[i] * phi[ind])] - - depends = [field_var] - - return Recipe(free_ind, instructions, depends) + return Recipe((d, (), p), expr) @doc_inherit def moment_evaluation(self, value, weights, points, kernel_data, derivative=None): - phi, ind = self._tabulation_variable(points, kernel_data, derivative) - w = weights.kernel_variable("w", kernel_data) + basis = self.basis_evaluation(points, kernel_data, derivative) + (d, b, p) = basis.indices + phi = basis.expression - q = ind[-1] - if derivative is None: - sum_ind = [q] - elif derivative == grad: - sum_ind = [ind[0], q] - else: - raise ValueError( - "Lagrange elements do not have a %s operation") % derivative + (d_, b_, p_) = value.indices + psi = value.replace_indices(zip(d_ + p_, d + p)).expression - i = ind[-2] - - instructions = [IndexSum(sum_ind, value[sum_ind] * w[q] * phi[ind])] + w = weights.kernel_variable("w", kernel_data) - depends = [value] + expr = IndexSum(d + p, psi * phi * w[p]) - return Recipe([i], instructions, depends) + return Recipe(((), b + b_, ()), expr) @doc_inherit def pullback(self, phi, kernel_data, derivative=None): diff --git a/finat/vectorfiniteelement.py b/finat/vectorfiniteelement.py index 58d74c75e..663fe3487 100644 --- a/finat/vectorfiniteelement.py +++ b/finat/vectorfiniteelement.py @@ -12,9 +12,9 @@ def __init__(self, element, dimension): .. math:: - \boldsymbol\phi_{\alpha,i} = \mathbf{e}_{\alpha}\phi_i + \boldsymbol\phi_{\beta,i} = \mathbf{e}_{\beta}\phi_i - Where :math:`\{\mathbf{e}_\alpha,\, \alpha=0\ldots\mathrm{dim}\}` is + Where :math:`\{\mathbf{e}_\beta,\, \beta=0\ldots\mathrm{dim}\}` is the basis for :math:`\mathbb{R}^{\mathrm{dim}}` and :math:`\{\phi_i\}` is the basis for the corresponding scalar finite element space. @@ -22,14 +22,14 @@ def __init__(self, element, dimension): :param element: The scalar finite element. :param dimension: The geometric dimension of the vector element. - :math:`\boldsymbol\phi_{\alpha,i}` is, of course, vector-valued. If - we subscript the vector-value with :math:`\beta` then we can write: + :math:`\boldsymbol\phi_{i,\beta}` is, of course, vector-valued. If + we subscript the vector-value with :math:`\alpha` then we can write: .. math:: - \boldsymbol\phi_{\beta,(\alpha,i)} = \delta_{\beta,\alpha}\phi_i + \boldsymbol\phi_{\alpha,(i,\beta)} = \delta_{\alpha,\beta}\phi_i This form enables the simplification of the loop nests which - will eventually be created, so it is the form we employ here. """ + will eventually be created, so it is the form we employ here.""" super(VectorFiniteElement, self).__init__() self._cell = element._cell @@ -39,76 +39,82 @@ def __init__(self, element, dimension): self._base_element = element - @doc_inherit def basis_evaluation(self, points, kernel_data, derivative=None): - # This is incorrect. We only get the scalar value. We need to - # bring in some sort of delta in order to get the right rank. + r"""Produce the recipe for basis function evaluation at a set of points +:math:`q`: + + .. math:: + \boldsymbol\phi_{\alpha,(i,\beta),q} = \delta_{\alpha,\beta}\phi_{i,q} + + """ # Produce the base scalar recipe - sr = self._base_element.basis_evaluation(points, kernel_data, derivative) + sr = self._base_element.basis_evaluation(points, kernel_data, + derivative) + phi = sr.expression + d, b, p = sr.indices - # Additional basis function index along the vector dimension. - alpha = indices.BasisFunctionIndex(points.points.shape[1]) # Additional dimension index along the vector dimension. Note # to self: is this the right order or does this index come # after any derivative index? - beta = indices.DimensionIndex(points.points.shape[1]) - - d, b, p = sr.split_indices + alpha = (indices.DimensionIndex(self._dimension),) + # Additional basis function index along the vector dimension. + beta = (indices.BasisFunctionIndex(self._dimension),) - return Recipe((beta,) + d + (alpha,) + b + p, - Delta((beta, alpha), sr), - sr.depends) + return Recipe((alpha + d, b + beta, p), Delta(alpha + beta, phi)) - @doc_inherit def field_evaluation(self, field_var, points, kernel_data, derivative=None): + r"""Produce the recipe for the evaluation of a field f at a set of +points :math:`q`: - basis = self._base_element.basis_evaluation(points, - kernel_data, derivative) - - alpha = indices.DimensionIndex(points.points.shape[1]) - - d, b, p = basis.split_indices - - free_ind = (alpha,) + d + p + .. math:: + \boldsymbol{f}_{\alpha,q} = \sum_i f_{i,\alpha}\phi_{i,q} - i = b[0] + """ + # Produce the base scalar recipe + sr = self._base_element.basis_evaluation(points, kernel_data, + derivative) + phi = sr.expression + d, b, p = sr.indices - instructions = IndexSum(i, field_var[i, alpha] * basis) + # Additional basis function index along the vector dimension. + alpha = (indices.DimensionIndex(self._dimension),) - depends = [field_var] + expression = IndexSum(b, field_var[b + alpha] * phi) - return Recipe(free_ind, instructions, depends) + return Recipe((alpha + d, (), p), expression) - @doc_inherit def moment_evaluation(self, value, weights, points, kernel_data, derivative=None): + r"""Produce the recipe for the evaluation of the moment of + :math:`u_{\alpha,q}` against a test function :math:`v_{\beta,q}`. - basis = self._base_element.basis_evaluation(self, points, - kernel_data, derivative) - w = weights.kernel_variable("w", kernel_data) - ind = basis.indices + .. math:: + \int u_{\alpha,q} : \phi_{\alpha,(i,\beta),q}\, \mathrm{d}x = + \sum_q u_{\alpha}\phi_{i,q}w_q - q = ind[-1] - alpha = indices.DimensionIndex(points.points.shape[1]) + Appropriate code is also generated in the more general cases + where derivatives are involved and where the value contains + test functions. + """ - if derivative is None: - sum_ind = [q] - elif derivative == grad: - sum_ind = [ind[0], q] - else: - raise NotImplementedError() + # Produce the base scalar recipe + sr = self._base_element.basis_evaluation(points, kernel_data, + derivative) + phi = sr.expression + d, b, p = sr.indices - value_ind = [alpha] + sum_ind + beta = (indices.BasisFunctionIndex(self._dimension),) - instructions = [IndexSum(sum_ind, value[value_ind] * w[q] * basis[ind])] + (d_, b_, p_) = value.indices + psi = value.replace_indices(zip(d_ + p_, beta + d + p)).expression - depends = [value] + w = weights.kernel_variable("w", kernel_data) - free_ind = [alpha, ind[-2]] + expression = IndexSum(d + p, psi * phi * w[p]) - return Recipe(free_ind, instructions, depends) + return Recipe(((), b + beta + b_, ()), expression) @doc_inherit def pullback(self, phi, kernel_data, derivative=None): From e1656359f71f6b676e883f9929b94d824895d21f Mon Sep 17 00:00:00 2001 From: David Ham Date: Sun, 17 Aug 2014 18:57:11 +0100 Subject: [PATCH 023/749] more complete ast support for Delta --- finat/ast.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index ba787f107..40a00020c 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -15,15 +15,22 @@ def __init__(self, replacements): def map_index(self, expr, *args): '''Replace indices if they are in the replacements list''' - print expr, self.replacements + try: return(self.replacements[expr]) except KeyError: return expr def map_recipe(self, expr, *args): - return Recipe(self.rec(expr.indices), - self.rec(expr.expression)) + return expr.__class__(self.rec(expr.indices, *args), + self.rec(expr.expression, *args)) + + def map_delta(self, expr, *args): + return expr.__class__(*(self.rec(c, *args) for c in expr.children)) + + map_let = map_delta + map_for_all = map_delta + map_wave = map_delta class _StringifyMapper(StringifyMapper): @@ -33,6 +40,10 @@ def map_recipe(self, expr, enclosing_prec): self.rec(expr.indices, PREC_NONE), self.rec(expr.expression, PREC_NONE)) + def map_delta(self, expr, *args): + return self.format("Delta(%s, %s)", + *(self.rec(c, *args) for c in expr.children)) + class Recipe(p.Expression): """AST snippets and data corresponding to some form of finite element @@ -116,6 +127,8 @@ def __str__(self): return "IndexSum(%s, %s)" % (tuple(map(str, self.children[0])), self.children[1]) + mapper_method = "map_index_sum" + class ForAll(p._MultiChildExpression): """A symbolic expression to indicate that the body will actually be @@ -137,6 +150,8 @@ def __str__(self): return "ForAll(%s, %s)" % (str([x._str_extent for x in self.children[0]]), self.children[1]) + mapper_method = "map_for_all" + class Wave(p._MultiChildExpression): """A symbolic expression with loop-carried dependencies.""" @@ -170,6 +185,8 @@ def __init__(self, bindings, body): def __str__(self): return "Let(%s)" % self.children + mapper_method = "map_let" + class Delta(p._MultiChildExpression): """The Kronecker delta expressed as a ternary operator: @@ -186,7 +203,7 @@ class Delta(p._MultiChildExpression): """ def __init__(self, indices, body): - if len(indices != 2): + if len(indices) != 2: raise FInATSyntaxError( "Delta statement requires exactly two indices") @@ -198,6 +215,11 @@ def __getinitargs__(self): def __str__(self): return "Delta(%s, %s)" % self.children + mapper_method = "map_delta" + + def stringifier(self): + return _StringifyMapper + class FInATSyntaxError(Exception): """Exception raised when the syntax rules of the FInAT ast are violated.""" From 671aa2e5d58c9bd50fab8cb3864582d3333cb1a4 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Mon, 18 Aug 2014 13:49:10 +0100 Subject: [PATCH 024/749] Experimental pullback configuration. Rather than having a separate symbolic pullback operation, apply the pullback when tabulating. This seems to be very straightforward and still allows for Jacobian cancellation magic. --- finat/lagrange.py | 23 +++++++++++------- finat/utils.py | 61 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 74 insertions(+), 10 deletions(-) diff --git a/finat/lagrange.py b/finat/lagrange.py index a76fd8172..4eae9f070 100644 --- a/finat/lagrange.py +++ b/finat/lagrange.py @@ -57,7 +57,7 @@ def _tabulate(self, points, derivative): raise ValueError( "Lagrange elements do not have a %s operation") % derivative - def basis_evaluation(self, points, kernel_data, derivative=None): + def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): '''Produce the variable for the tabulation of the basis functions or their derivative. Also return the relevant indices. @@ -72,7 +72,7 @@ def basis_evaluation(self, points, kernel_data, derivative=None): if static_key in static_data: phi = static_data[static_key][0] else: - phi = p.Variable('phi_e' if derivative is None else "dphi_e" + phi = p.Variable(('phi_e' if derivative is None else "dphi_e") + str(self._id)) data = self._tabulate(points, derivative) static_data[static_key] = (phi, lambda: data) @@ -83,16 +83,23 @@ def basis_evaluation(self, points, kernel_data, derivative=None): if derivative is grad: alpha = indices.DimensionIndex(points.points.shape[1]) ind = ((alpha,), (i,), (q,)) + if pullback: + beta = indices.DimensionIndex(points.points.shape[1]) + invJ = kernel_data.invJ[(beta, alpha)] + expr = IndexSum((beta,), invJ * phi[(beta, i, q)]) + else: + expr = phi[(alpha, i, q)] else: ind = ((), (i,), (q,)) + expr = phi[(i, q)] - return Recipe(indices=ind, expression=phi[ind[0] + ind[1] + ind[2]]) + return Recipe(indices=ind, expression=expr) @doc_inherit def field_evaluation(self, field_var, points, - kernel_data, derivative=None): + kernel_data, derivative=None, pullback=True): - basis = self.basis_evaluation(points, kernel_data, derivative) + basis = self.basis_evaluation(points, kernel_data, derivative, pullback) (d, b, p) = basis.indices phi = basis.expression @@ -102,9 +109,9 @@ def field_evaluation(self, field_var, points, @doc_inherit def moment_evaluation(self, value, weights, points, - kernel_data, derivative=None): + kernel_data, derivative=None, pullback=True): - basis = self.basis_evaluation(points, kernel_data, derivative) + basis = self.basis_evaluation(points, kernel_data, derivative, pullback) (d, b, p) = basis.indices phi = basis.expression @@ -123,7 +130,7 @@ def pullback(self, phi, kernel_data, derivative=None): if derivative is None: return phi elif derivative == grad: - return None # dot(Jinv, grad(phi)) + return None # dot(invJ, grad(phi)) else: raise ValueError( "Lagrange elements do not have a %s operation") % derivative diff --git a/finat/utils.py b/finat/utils.py index 5b051b91c..5289ad750 100644 --- a/finat/utils.py +++ b/finat/utils.py @@ -1,5 +1,7 @@ +import pymbolic.primitives as p +import inspect from functools import wraps - +import FIAT """ from http://stackoverflow.com/questions/2025562/inherit-docstrings-in-python-class-inheritance @@ -72,7 +74,62 @@ def use_parent_doc(self, func, source): class KernelData(object): - def __init__(self): + def __init__(self, coordinate_element, affine=None): + """ + :param coordinate_element: the (vector-valued) finite element for + the coordinate field. + :param affine: Specifies whether the pullback is affine (and therefore + whether the Jacobian must be evaluated at every quadrature point). + If not specified, this is inferred from coordinate_element. + """ + + self.coordinate_element = coordinate_element + if affine is None: + self.affine = coordinate_element.degree <= 1 \ + and isinstance(coordinate_element.cell, _simplex) + else: + self.affine = True self.static = {} self.params = {} + self.geometry = {} + + @property + def J(self): + + try: + return self.geometry["J"] + except KeyError: + self.geometry["J"] = p.Variable("J") + return self.geometry["J"] + + @property + def invJ(self): + + # ensure J exists + self.J + + try: + return self.geometry["invJ"] + except KeyError: + self.geometry["invJ"] = p.Variable("invJ") + return self.geometry["invJ"] + + @property + def detJ(self): + + # ensure J exists + self.J + + try: + return self.geometry["detJ"] + except KeyError: + self.geometry["detJ"] = p.Variable("detJ") + return self.geometry["detJ"] + +# Tuple of simplex cells. This relies on the fact that FIAT only +# defines simplex elements. +_simplex = tuple(e for e in FIAT.reference_element.__dict__.values() + if (inspect.isclass(e) + and issubclass(e, FIAT.reference_element.ReferenceElement) + and e is not FIAT.reference_element.ReferenceElement)) From 4c433ec453e24cd7fe7b971c6acde933e9af5ee2 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Mon, 18 Aug 2014 13:56:15 +0100 Subject: [PATCH 025/749] Turns out doc_inherit was a PITA after all --- finat/lagrange.py | 4 --- finat/utils.py | 70 ------------------------------------ finat/vectorfiniteelement.py | 2 -- 3 files changed, 76 deletions(-) diff --git a/finat/lagrange.py b/finat/lagrange.py index 4eae9f070..c20eb69eb 100644 --- a/finat/lagrange.py +++ b/finat/lagrange.py @@ -1,7 +1,6 @@ import pymbolic.primitives as p from finiteelementbase import FiniteElementBase from ast import Recipe, IndexSum -from utils import doc_inherit import FIAT import indices from derivatives import div, grad, curl @@ -95,7 +94,6 @@ def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): return Recipe(indices=ind, expression=expr) - @doc_inherit def field_evaluation(self, field_var, points, kernel_data, derivative=None, pullback=True): @@ -107,7 +105,6 @@ def field_evaluation(self, field_var, points, return Recipe((d, (), p), expr) - @doc_inherit def moment_evaluation(self, value, weights, points, kernel_data, derivative=None, pullback=True): @@ -124,7 +121,6 @@ def moment_evaluation(self, value, weights, points, return Recipe(((), b + b_, ()), expr) - @doc_inherit def pullback(self, phi, kernel_data, derivative=None): if derivative is None: diff --git a/finat/utils.py b/finat/utils.py index 5289ad750..1c7c35a3c 100644 --- a/finat/utils.py +++ b/finat/utils.py @@ -1,77 +1,7 @@ import pymbolic.primitives as p import inspect -from functools import wraps import FIAT -""" -from http://stackoverflow.com/questions/2025562/inherit-docstrings-in-python-class-inheritance - -doc_inherit decorator - -Usage: - -class Foo(object): - def foo(self): - "Frobber" - pass - -class Bar(Foo): - @doc_inherit - def foo(self): - pass - -Now, Bar.foo.__doc__ == Bar().foo.__doc__ == Foo.foo.__doc__ == "Frobber" -""" - - -class DocInherit(object): - """ - Docstring inheriting method descriptor - - The class itself is also used as a decorator - """ - - def __init__(self, mthd): - self.mthd = mthd - self.name = mthd.__name__ - - def __get__(self, obj, cls): - if obj: - return self.get_with_inst(obj, cls) - else: - return self.get_no_inst(cls) - - def get_with_inst(self, obj, cls): - - overridden = getattr(super(cls, obj), self.name, None) - - @wraps(self.mthd, assigned=('__name__', '__module__')) - def f(*args, **kwargs): - return self.mthd(obj, *args, **kwargs) - - return self.use_parent_doc(f, overridden) - - def get_no_inst(self, cls): - - for parent in cls.__mro__[1:]: - overridden = getattr(parent, self.name, None) - if overridden: - break - - @wraps(self.mthd, assigned=('__name__', '__module__')) - def f(*args, **kwargs): - return self.mthd(*args, **kwargs) - - return self.use_parent_doc(f, overridden) - - def use_parent_doc(self, func, source): - if source is None: - raise NameError("Can't find '%s' in parents" % self.name) - func.__doc__ = source.__doc__ - return func - -doc_inherit = DocInherit - class KernelData(object): def __init__(self, coordinate_element, affine=None): diff --git a/finat/vectorfiniteelement.py b/finat/vectorfiniteelement.py index 663fe3487..8e8161531 100644 --- a/finat/vectorfiniteelement.py +++ b/finat/vectorfiniteelement.py @@ -1,6 +1,5 @@ from finiteelementbase import FiniteElementBase from derivatives import div, grad, curl -from utils import doc_inherit from ast import Recipe, IndexSum, Delta import indices @@ -116,7 +115,6 @@ def moment_evaluation(self, value, weights, points, return Recipe(((), b + beta + b_, ()), expression) - @doc_inherit def pullback(self, phi, kernel_data, derivative=None): if derivative is None: From 1a32eeba7c87cfeae1d0ffedbf59686c86ef22db Mon Sep 17 00:00:00 2001 From: David A Ham Date: Mon, 18 Aug 2014 14:18:13 +0100 Subject: [PATCH 026/749] Clean up string output. Get rid of a lot of the index clutter from the string output. --- finat/ast.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index 40a00020c..09fd28093 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -42,7 +42,12 @@ def map_recipe(self, expr, enclosing_prec): def map_delta(self, expr, *args): return self.format("Delta(%s, %s)", - *(self.rec(c, *args) for c in expr.children)) + *[self.rec(c, *args) for c in expr.children]) + + def map_index_sum(self, expr, *args): + return self.format("IndexSum((%s), %s)", + " ".join(self.rec(c, *args) + "," for c in expr.children[0]), + self.rec(expr.children[1], *args)) class Recipe(p.Expression): @@ -92,9 +97,6 @@ def __getitem__(self, index): def stringifier(self): return _StringifyMapper - def __str__(self): - return "Recipe(%s, %s)" % (self.indices, self.expression) - def replace_indices(self, replacements): """Return a copy of this :class:`Recipe` with some of the indices substituted.""" From a00db377cca46055a265952f3344e1459496bacd Mon Sep 17 00:00:00 2001 From: David A Ham Date: Mon, 18 Aug 2014 14:44:22 +0100 Subject: [PATCH 027/749] Fix printing of greek --- finat/indices.py | 2 +- finat/lagrange.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/finat/indices.py b/finat/indices.py index 1ca5272a7..4eee1fc0b 100644 --- a/finat/indices.py +++ b/finat/indices.py @@ -66,7 +66,7 @@ class DimensionIndex(IndexBase): geometric or vector components.''' def __init__(self, extent): - name = 'alpha_' + str(DimensionIndex._count) + name = u'\u03B1_'.encode("utf-8") + str(DimensionIndex._count) DimensionIndex._count += 1 super(DimensionIndex, self).__init__(extent, name) diff --git a/finat/lagrange.py b/finat/lagrange.py index c20eb69eb..fe6ab0364 100644 --- a/finat/lagrange.py +++ b/finat/lagrange.py @@ -71,8 +71,8 @@ def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): if static_key in static_data: phi = static_data[static_key][0] else: - phi = p.Variable(('phi_e' if derivative is None else "dphi_e") - + str(self._id)) + phi = p.Variable((u'\u03C6_e'.encode("utf-8") if derivative is None + else u"d\u03C6_e".encode("utf-8")) + str(self._id)) data = self._tabulate(points, derivative) static_data[static_key] = (phi, lambda: data) From 053f8327ef690383ad9223168b0fed546e3085b0 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Tue, 19 Aug 2014 14:18:08 +0100 Subject: [PATCH 028/749] Add div to VectorFinitElement VectorFiniteElements recipes now handle Div. --- finat/vectorfiniteelement.py | 96 +++++++++++++++++++++++++++--------- 1 file changed, 73 insertions(+), 23 deletions(-) diff --git a/finat/vectorfiniteelement.py b/finat/vectorfiniteelement.py index 8e8161531..9e05c8efc 100644 --- a/finat/vectorfiniteelement.py +++ b/finat/vectorfiniteelement.py @@ -38,80 +38,130 @@ def __init__(self, element, dimension): self._base_element = element - def basis_evaluation(self, points, kernel_data, derivative=None): + def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): r"""Produce the recipe for basis function evaluation at a set of points :math:`q`: .. math:: - \boldsymbol\phi_{\alpha,(i,\beta),q} = \delta_{\alpha,\beta}\phi_{i,q} + \boldsymbol\phi_{\alpha,(i,\beta),q} = \delta_{\alpha,\beta}\phi_{i,q} + \nabla\boldsymbol\phi_{(\alpha,\gamma),(i,\beta),q} = \delta_{\alpha,\beta}\phi_{\gamma,i,q} + + \nabla\cdot\boldsymbol\phi_{(i,\beta),q} = \phi_{\beta,i,q} """ - # Produce the base scalar recipe + # Produce the base scalar recipe. The scalar basis can only + # take a grad. For other derivatives, we need to do the + # transform here. sr = self._base_element.basis_evaluation(points, kernel_data, - derivative) + derivative and grad, pullback) phi = sr.expression d, b, p = sr.indices - # Additional dimension index along the vector dimension. Note - # to self: is this the right order or does this index come - # after any derivative index? - alpha = (indices.DimensionIndex(self._dimension),) # Additional basis function index along the vector dimension. beta = (indices.BasisFunctionIndex(self._dimension),) - return Recipe((alpha + d, b + beta, p), Delta(alpha + beta, phi)) + if derivative is div: + + return Recipe((d[:-1], b+beta, p), + sr.replace_indices({d[-1]: beta[0]}).expression) + + elif derivative is curl: + raise NotImplementedError + else: + # Additional dimension index along the vector dimension. Note + # to self: is this the right order or does this index come + # after any derivative index? + alpha = (indices.DimensionIndex(self._dimension),) + + return Recipe((alpha + d, b + beta, p), Delta(alpha + beta, phi)) def field_evaluation(self, field_var, points, - kernel_data, derivative=None): + kernel_data, derivative=None, pullback=True): r"""Produce the recipe for the evaluation of a field f at a set of points :math:`q`: .. math:: \boldsymbol{f}_{\alpha,q} = \sum_i f_{i,\alpha}\phi_{i,q} + \nabla\boldsymbol{f}_{\alpha,\beta,q} = \sum_i f_{i,\alpha}\nabla\phi_{\beta,i,q} + + \nabla\cdot\boldsymbol{f}_{q} = \sum_{i,\alpha} f_{i,\alpha}\nabla\phi_{\alpha,i,q} """ - # Produce the base scalar recipe + # Produce the base scalar recipe. The scalar basis can only + # take a grad. For other derivatives, we need to do the + # transform here. sr = self._base_element.basis_evaluation(points, kernel_data, - derivative) + derivative and grad, pullback) phi = sr.expression d, b, p = sr.indices - # Additional basis function index along the vector dimension. - alpha = (indices.DimensionIndex(self._dimension),) + if derivative is div: + + expression = IndexSum(b+d[-1:], field_var[b+d[-1:]] * phi) + + return Recipe((d[:-1], (), p), expression) + + elif derivative is curl: + raise NotImplementedError + else: + # Additional basis function index along the vector dimension. + alpha = (indices.DimensionIndex(self._dimension),) - expression = IndexSum(b, field_var[b + alpha] * phi) + expression = IndexSum(b, field_var[b + alpha] * phi) - return Recipe((alpha + d, (), p), expression) + return Recipe((alpha + d, (), p), expression) def moment_evaluation(self, value, weights, points, - kernel_data, derivative=None): + kernel_data, derivative=None, pullback=True): r"""Produce the recipe for the evaluation of the moment of :math:`u_{\alpha,q}` against a test function :math:`v_{\beta,q}`. .. math:: - \int u_{\alpha,q} : \phi_{\alpha,(i,\beta),q}\, \mathrm{d}x = - \sum_q u_{\alpha}\phi_{i,q}w_q + \int \boldsymbol{u} \cdot \boldsymbol\phi_{(i,\beta)}\, \mathrm{d}x = + \sum_q \boldsymbol{u}_{\beta,q}\phi_{i,q}w_q + + \int \boldsymbol{u}_{(\alpha,\gamma)} : \nabla \boldsymbol\phi_{(\alpha,\gamma),(i,\beta)}\, \mathrm{d}x = + \sum_{\gamma,q} \boldsymbol{u}_{(\beta,\gamma),q}\nabla\phi_{\gamma,i,q}w_q + + \int u_{q} \nabla \cdot \boldsymbol\phi_{(i,\beta)}\, \mathrm{d}x = + \sum_{q} u_q\nabla\phi_{\beta,i,q}w_q Appropriate code is also generated in the more general cases where derivatives are involved and where the value contains test functions. """ - # Produce the base scalar recipe + # Produce the base scalar recipe. The scalar basis can only + # take a grad. For other derivatives, we need to do the + # transform here. sr = self._base_element.basis_evaluation(points, kernel_data, - derivative) + derivative and grad, pullback) + phi = sr.expression d, b, p = sr.indices beta = (indices.BasisFunctionIndex(self._dimension),) (d_, b_, p_) = value.indices - psi = value.replace_indices(zip(d_ + p_, beta + d + p)).expression w = weights.kernel_variable("w", kernel_data) - expression = IndexSum(d + p, psi * phi * w[p]) + if derivative is div: + beta = d[-1:] + + psi = value.replace_indices(zip(d_ + p_, d[:-1] + p)).expression + + expression = IndexSum(d[:-1] + p, psi * phi * w[p]) + + elif derivative is curl: + raise NotImplementedError + else: + beta = (indices.BasisFunctionIndex(self._dimension),) + + psi = value.replace_indices(zip(d_ + p_, beta + d + p)).expression + + expression = IndexSum(d + p, psi * phi * w[p]) return Recipe(((), b + beta + b_, ()), expression) From a22fc04e5bbb1aa79bdb5a97e799f85c86a3388e Mon Sep 17 00:00:00 2001 From: David A Ham Date: Tue, 19 Aug 2014 14:19:36 +0100 Subject: [PATCH 029/749] LeviCivita symbol. Add a LeviCivita symbol to the language in order to support Curl. --- finat/ast.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/finat/ast.py b/finat/ast.py index 09fd28093..0cde2ed37 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -31,6 +31,8 @@ def map_delta(self, expr, *args): map_let = map_delta map_for_all = map_delta map_wave = map_delta + map_index_sum = map_delta + map_levi_civita = map_delta class _StringifyMapper(StringifyMapper): @@ -49,6 +51,10 @@ def map_index_sum(self, expr, *args): " ".join(self.rec(c, *args) + "," for c in expr.children[0]), self.rec(expr.children[1], *args)) + def map_levi_civita(self, expr, *args): + return self.format("LeviCivita(%s)", + self.join_rec(", ", expr.children, *args)) + class Recipe(p.Expression): """AST snippets and data corresponding to some form of finite element @@ -132,6 +138,32 @@ def __str__(self): mapper_method = "map_index_sum" +class LeviCivita(p._MultiChildExpression): + r"""The Levi-Civita symbol expressed as an operator. + + :param free: A tuple of free indices. + :param bound: A tuple of indices over which to sum. + :param body: The summand. + + The length of free + bound must be exactly 3. The Levi-Civita + operator then represents the summation over the bound indices of + the Levi-Civita symbol times the body. For example in the case of + two bound indices: + + .. math:: + \mathrm{LeviCivita((\alpha,), (\beta, \gamma), body)} = \sum_{\beta,\gamma}\epsilon_{\alpha,\beta,\gamma} \mathrm{body} + + """ + def __init__(self, free, bound, body): + + self.children = (free, bound, body) + + def __getinitargs__(self): + return self.children + + mapper_method = "map_index_sum" + + class ForAll(p._MultiChildExpression): """A symbolic expression to indicate that the body will actually be evaluated for all of the values of its free indices. This enables From 71d98e5d2411b3f9bc01e6b4a8356b9094eaa273 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Tue, 19 Aug 2014 16:52:11 +0100 Subject: [PATCH 030/749] Implement Curl for VectorFunctionSpace --- finat/ast.py | 8 +-- finat/vectorfiniteelement.py | 96 ++++++++++++++++++++++++++++-------- 2 files changed, 81 insertions(+), 23 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index 0cde2ed37..0be768519 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -131,9 +131,8 @@ def __init__(self, indices, body): def __getinitargs__(self): return self.children - def __str__(self): - return "IndexSum(%s, %s)" % (tuple(map(str, self.children[0])), - self.children[1]) + def stringifier(self): + return _StringifyMapper mapper_method = "map_index_sum" @@ -161,6 +160,9 @@ def __init__(self, free, bound, body): def __getinitargs__(self): return self.children + def stringifier(self): + return _StringifyMapper + mapper_method = "map_index_sum" diff --git a/finat/vectorfiniteelement.py b/finat/vectorfiniteelement.py index 9e05c8efc..af134a723 100644 --- a/finat/vectorfiniteelement.py +++ b/finat/vectorfiniteelement.py @@ -1,6 +1,6 @@ from finiteelementbase import FiniteElementBase from derivatives import div, grad, curl -from ast import Recipe, IndexSum, Delta +from ast import Recipe, IndexSum, Delta, LeviCivita import indices @@ -43,11 +43,15 @@ def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): :math:`q`: .. math:: - \boldsymbol\phi_{\alpha,(i,\beta),q} = \delta_{\alpha,\beta}\phi_{i,q} + \boldsymbol\phi_{\alpha (i \beta) q} = \delta_{\alpha \beta}\phi_{i q} - \nabla\boldsymbol\phi_{(\alpha,\gamma),(i,\beta),q} = \delta_{\alpha,\beta}\phi_{\gamma,i,q} + \nabla\boldsymbol\phi_{(\alpha \gamma) (i \beta) q} = \delta_{\alpha \beta}\nabla\phi_{\gamma i q} - \nabla\cdot\boldsymbol\phi_{(i,\beta),q} = \phi_{\beta,i,q} + \nabla\times\boldsymbol\phi_{(i \beta) q} = \epsilon_{2 \beta \gamma}\nabla\phi_{\gamma i q} \qquad\textrm{(2D)} + + \nabla\times\boldsymbol\phi_{\alpha (i \beta) q} = \epsilon_{\alpha \beta \gamma}\nabla\phi_{\gamma i q} \qquad\textrm{(3D)} + + \nabla\cdot\boldsymbol\phi_{(i \beta) q} = \nabla\phi_{\beta i q} """ # Produce the base scalar recipe. The scalar basis can only @@ -63,11 +67,20 @@ def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): if derivative is div: - return Recipe((d[:-1], b+beta, p), + return Recipe((d[:-1], b + beta, p), sr.replace_indices({d[-1]: beta[0]}).expression) elif derivative is curl: - raise NotImplementedError + if self.dimension == 2: + + return Recipe((d[:-1], b + beta, p), LeviCivita((2,) + beta, d[-1:], phi)) + elif self.dimension == 3: + alpha = (indices.DimensionIndex(self._dimension),) + + return Recipe((d[:-1] + alpha, b + beta, p), LeviCivita(alpha + beta, d[-1:], phi)) + else: + raise NotImplementedError + else: # Additional dimension index along the vector dimension. Note # to self: is this the right order or does this index come @@ -82,11 +95,15 @@ def field_evaluation(self, field_var, points, points :math:`q`: .. math:: - \boldsymbol{f}_{\alpha,q} = \sum_i f_{i,\alpha}\phi_{i,q} + \boldsymbol{f}_{\alpha q} = \sum_i f_{i \alpha}\phi_{i q} - \nabla\boldsymbol{f}_{\alpha,\beta,q} = \sum_i f_{i,\alpha}\nabla\phi_{\beta,i,q} + \nabla\boldsymbol{f}_{\alpha \beta q} = \sum_i f_{i \alpha}\nabla\phi_{\beta i q} - \nabla\cdot\boldsymbol{f}_{q} = \sum_{i,\alpha} f_{i,\alpha}\nabla\phi_{\alpha,i,q} + \nabla\times\boldsymbol{f}_{q} = \epsilon_{2 \beta \gamma}\sum_{i} f_{i \beta}\nabla\phi_{\gamma i q} \qquad\textrm{(2D)} + + \nabla\times\boldsymbol{f}_{\alpha q} = \epsilon_{\alpha \beta \gamma}\sum_{i} f_{i \beta}\nabla\phi_{\gamma i q} \qquad\textrm{(3D)} + + \nabla\cdot\boldsymbol{f}_{q} = \sum_{i \alpha} f_{i \alpha}\nabla\phi_{\alpha i q} """ # Produce the base scalar recipe. The scalar basis can only # take a grad. For other derivatives, we need to do the @@ -98,12 +115,28 @@ def field_evaluation(self, field_var, points, if derivative is div: - expression = IndexSum(b+d[-1:], field_var[b+d[-1:]] * phi) + expression = IndexSum(b + d[-1:], field_var[b + d[-1:]] * phi) return Recipe((d[:-1], (), p), expression) elif derivative is curl: - raise NotImplementedError + if self.dimension == 2: + beta = (indices.BasisFunctionIndex(self._dimension),) + + expression = LeviCivita((2,), beta + d[-1:], + IndexSum(b, field_var[b + beta] * phi)) + + return Recipe((d[:-1], (), p), expression) + elif self.dimension == 3: + # Additional basis function index along the vector dimension. + alpha = (indices.DimensionIndex(self._dimension),) + beta = (indices.BasisFunctionIndex(self._dimension),) + + expression = LeviCivita(alpha, beta + d[-1:], IndexSum(b, field_var[b + beta] * phi)) + + return Recipe((d[:-1] + alpha, (), p), expression) + else: + raise NotImplementedError else: # Additional basis function index along the vector dimension. alpha = (indices.DimensionIndex(self._dimension),) @@ -118,17 +151,22 @@ def moment_evaluation(self, value, weights, points, :math:`u_{\alpha,q}` against a test function :math:`v_{\beta,q}`. .. math:: - \int \boldsymbol{u} \cdot \boldsymbol\phi_{(i,\beta)}\, \mathrm{d}x = - \sum_q \boldsymbol{u}_{\beta,q}\phi_{i,q}w_q + \int \boldsymbol{u} \cdot \boldsymbol\phi_{(i \beta)}\ \mathrm{d}x = + \sum_q \boldsymbol{u}_{\beta q}\phi_{i q}w_q + + \int \boldsymbol{u}_{(\alpha \gamma)} \nabla \boldsymbol\phi_{(\alpha \gamma) (i \beta)}\ \mathrm{d}x = + \sum_{\gamma q} \boldsymbol{u}_{(\beta \gamma) q}\nabla\phi_{\gamma i q}w_q + + \int u \nabla \times \boldsymbol\phi_{(i \beta)}\ \mathrm{d}x = + \sum_{q} u_{q}\epsilon_{2\beta\gamma}\nabla\phi_{\gamma i q}w_q \qquad\textrm{(2D)} - \int \boldsymbol{u}_{(\alpha,\gamma)} : \nabla \boldsymbol\phi_{(\alpha,\gamma),(i,\beta)}\, \mathrm{d}x = - \sum_{\gamma,q} \boldsymbol{u}_{(\beta,\gamma),q}\nabla\phi_{\gamma,i,q}w_q + \int u_{\alpha} \nabla \times \boldsymbol\phi_{\alpha (i \beta)}\ \mathrm{d}x = + \sum_{\alpha q} u_{\alpha q}\epsilon_{\alpha\beta\gamma}\nabla\phi_{\gamma i q}w_q \qquad\textrm{(3D)} - \int u_{q} \nabla \cdot \boldsymbol\phi_{(i,\beta)}\, \mathrm{d}x = - \sum_{q} u_q\nabla\phi_{\beta,i,q}w_q + \int u \nabla \cdot \boldsymbol\phi_{(i \beta)}\ \mathrm{d}x = + \sum_{q} u_q\nabla\phi_{\beta i q}w_q - Appropriate code is also generated in the more general cases - where derivatives are involved and where the value contains + Appropriate code is also generated where the value contains test functions. """ @@ -155,7 +193,25 @@ def moment_evaluation(self, value, weights, points, expression = IndexSum(d[:-1] + p, psi * phi * w[p]) elif derivative is curl: - raise NotImplementedError + if self.dimension == 2: + + beta = (indices.BasisFunctionIndex(self._dimension),) + gamma = d[-1:] + + psi = value.replace_indices((d_ + p_, d[:-1] + p)).expression + + expression = IndexSum(p, psi * LeviCivita((2,) + beta, gamma, phi) * w[p]) + elif self.dimension == 3: + + alpha = d_[-1:] + beta = (indices.BasisFunctionIndex(self._dimension),) + gamma = d[-1:] + + psi = value.replace_indices((d_[:-1] + p_, d[:-1] + p)).expression + + expression = IndexSum(alpha + p, psi * LeviCivita(alpha + beta, gamma, phi) * w[p]) + else: + raise NotImplementedError else: beta = (indices.BasisFunctionIndex(self._dimension),) From 7ac81efe833aa279dad85619bab2e15dd2a2a32a Mon Sep 17 00:00:00 2001 From: David A Ham Date: Tue, 19 Aug 2014 17:42:58 +0100 Subject: [PATCH 031/749] Trial, not test --- finat/vectorfiniteelement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/vectorfiniteelement.py b/finat/vectorfiniteelement.py index af134a723..55cf7b480 100644 --- a/finat/vectorfiniteelement.py +++ b/finat/vectorfiniteelement.py @@ -167,7 +167,7 @@ def moment_evaluation(self, value, weights, points, \sum_{q} u_q\nabla\phi_{\beta i q}w_q Appropriate code is also generated where the value contains - test functions. + trial functions. """ # Produce the base scalar recipe. The scalar basis can only From b4ad84f7d00f9e70dab776e92c31f14f267989a2 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Tue, 19 Aug 2014 18:27:34 +0100 Subject: [PATCH 032/749] factor common features of different finite elements --- finat/finiteelementbase.py | 48 +++++++++++++++++++++++ finat/lagrange.py | 80 ++++++++++++++------------------------ 2 files changed, 77 insertions(+), 51 deletions(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 531773d2e..496615b77 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -1,3 +1,6 @@ +import numpy as np + + class UndefinedError(Exception): def __init__(self, *args, **kwargs): Exception.__init__(self, *args, **kwargs) @@ -105,3 +108,48 @@ def dual_evaluation(self, static_data): ''' raise NotImplementedError + + +class FiatElement(FiniteElementBase): + """A finite element for which the tabulation is provided by FIAT.""" + def __init__(self, cell, degree): + super(FiatElement, self).__init__() + + self._cell = cell + self._degree = degree + + @property + def entity_dofs(self): + '''Return the map of topological entities to degrees of + freedom for the finite element. + + Note that entity numbering needs to take into account the tensor case. + ''' + + return self._fiat_element.entity_dofs() + + @property + def entity_closure_dofs(self): + '''Return the map of topological entities to degrees of + freedom on the closure of those entities for the finite element.''' + + return self._fiat_element.entity_dofs() + + @property + def facet_support_dofs(self): + '''Return the map of facet id to the degrees of freedom for which the + corresponding basis functions take non-zero values.''' + + return self._fiat_element.entity_support_dofs() + + def _tabulate(self, points, derivative): + + if derivative: + tab = self._fiat_element.tabulate(1, points.points) + + ind = np.eye(points.points.shape[1], dtype=int) + + return np.array([tab[tuple(i)] for i in ind]) + else: + return self._fiat_element.tabulate(0, points.points)[ + tuple([0] * points.points.shape[1])] diff --git a/finat/lagrange.py b/finat/lagrange.py index fe6ab0364..49979d12a 100644 --- a/finat/lagrange.py +++ b/finat/lagrange.py @@ -1,60 +1,14 @@ import pymbolic.primitives as p -from finiteelementbase import FiniteElementBase +from finiteelementbase import FiatElement from ast import Recipe, IndexSum import FIAT import indices -from derivatives import div, grad, curl -import numpy as np +from derivatives import grad -class Lagrange(FiniteElementBase): +class ScalarElement(FiatElement): def __init__(self, cell, degree): - super(Lagrange, self).__init__() - - self._cell = cell - self._degree = degree - - self._fiat_element = FIAT.Lagrange(cell, degree) - - @property - def entity_dofs(self): - '''Return the map of topological entities to degrees of - freedom for the finite element. - - Note that entity numbering needs to take into account the tensor case. - ''' - - return self._fiat_element.entity_dofs() - - @property - def entity_closure_dofs(self): - '''Return the map of topological entities to degrees of - freedom on the closure of those entities for the finite element.''' - - return self._fiat_element.entity_dofs() - - @property - def facet_support_dofs(self): - '''Return the map of facet id to the degrees of freedom for which the - corresponding basis functions take non-zero values.''' - - return self._fiat_element.entity_support_dofs() - - def _tabulate(self, points, derivative): - - if derivative is None: - return self._fiat_element.tabulate(0, points.points)[ - tuple([0] * points.points.shape[1])] - elif derivative is grad: - tab = self._fiat_element.tabulate(1, points.points) - - ind = np.eye(points.points.shape[1], dtype=int) - - return np.array([tab[tuple(i)] for i in ind]) - - else: - raise ValueError( - "Lagrange elements do not have a %s operation") % derivative + super(ScalarElement, self).__init__(cell, degree) def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): '''Produce the variable for the tabulation of the basis @@ -63,6 +17,10 @@ def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): updates the requisite static kernel data, which in this case is just the matrix. ''' + if derivative not in (None, grad): + raise ValueError( + "Scalar elements do not have a %s operation") % derivative + static_key = (id(self), id(points), id(derivative)) static_data = kernel_data.static @@ -96,6 +54,9 @@ def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): def field_evaluation(self, field_var, points, kernel_data, derivative=None, pullback=True): + if derivative not in (None, grad): + raise ValueError( + "Scalar elements do not have a %s operation") % derivative basis = self.basis_evaluation(points, kernel_data, derivative, pullback) (d, b, p) = basis.indices @@ -107,6 +68,9 @@ def field_evaluation(self, field_var, points, def moment_evaluation(self, value, weights, points, kernel_data, derivative=None, pullback=True): + if derivative not in (None, grad): + raise ValueError( + "Scalar elements do not have a %s operation") % derivative basis = self.basis_evaluation(points, kernel_data, derivative, pullback) (d, b, p) = basis.indices @@ -129,4 +93,18 @@ def pullback(self, phi, kernel_data, derivative=None): return None # dot(invJ, grad(phi)) else: raise ValueError( - "Lagrange elements do not have a %s operation") % derivative + "Scalar elements do not have a %s operation") % derivative + + +class Lagrange(ScalarElement): + def __init__(self, cell, degree): + super(Lagrange, self).__init__(cell, degree) + + self._fiat_element = FIAT.Lagrange(cell, degree) + + +class DiscontinuousLagrange(ScalarElement): + def __init__(self, cell, degree): + super(Lagrange, self).__init__(cell, degree) + + self._fiat_element = FIAT.DiscontinuousLagrange(cell, degree) From 3c32b23a45c8be895838413e1158edc407ebc47a Mon Sep 17 00:00:00 2001 From: David A Ham Date: Tue, 19 Aug 2014 18:37:30 +0100 Subject: [PATCH 033/749] Beginnings of hdiv --- finat/hdiv.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 finat/hdiv.py diff --git a/finat/hdiv.py b/finat/hdiv.py new file mode 100644 index 000000000..d3ab17cc4 --- /dev/null +++ b/finat/hdiv.py @@ -0,0 +1,20 @@ +import pymbolic.primitives as p +from finiteelementbase import FiatElement +from ast import Recipe, IndexSum +import FIAT +import indices +from derivatives import div, grad, curl +import numpy as np + + +class HDivElement(FiatElement): + def __init__(self, cell, degree): + super(HDivElement, self).__init__(cell, degree) + + + +class RaviartThomas(HDivElement): + def __init__(self, cell, degree): + super(RaviartThomas, self).__init__(cell, degree) + + self._fiat_element = FIAT.RaviartThomas(cell, degree) From c6b196d3270dc1c54acd6fe65e80ed66976945c9 Mon Sep 17 00:00:00 2001 From: David Ham Date: Tue, 19 Aug 2014 23:37:24 +0100 Subject: [PATCH 034/749] plausible sketch of H(div) --- finat/finiteelementbase.py | 28 ++++++++++++++ finat/hdiv.py | 77 ++++++++++++++++++++++++++++++++++++-- finat/lagrange.py | 34 ++++------------- 3 files changed, 110 insertions(+), 29 deletions(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 496615b77..4974d164a 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -1,4 +1,5 @@ import numpy as np +from ast import Recipe, IndexSum class UndefinedError(Exception): @@ -153,3 +154,30 @@ def _tabulate(self, points, derivative): else: return self._fiat_element.tabulate(0, points.points)[ tuple([0] * points.points.shape[1])] + + def field_evaluation(self, field_var, points, + kernel_data, derivative=None, pullback=True): + + basis = self.basis_evaluation(points, kernel_data, derivative, pullback) + (d, b, p) = basis.indices + phi = basis.expression + + expr = IndexSum(b, field_var[b[0]] * phi) + + return Recipe((d, (), p), expr) + + def moment_evaluation(self, value, weights, points, + kernel_data, derivative=None, pullback=True): + + basis = self.basis_evaluation(points, kernel_data, derivative, pullback) + (d, b, p) = basis.indices + phi = basis.expression + + (d_, b_, p_) = value.indices + psi = value.replace_indices(zip(d_ + p_, d + p)).expression + + w = weights.kernel_variable("w", kernel_data) + + expr = IndexSum(d + p, psi * phi * w[p]) + + return Recipe(((), b + b_, ()), expr) diff --git a/finat/hdiv.py b/finat/hdiv.py index d3ab17cc4..24c3e60d4 100644 --- a/finat/hdiv.py +++ b/finat/hdiv.py @@ -1,17 +1,88 @@ import pymbolic.primitives as p from finiteelementbase import FiatElement -from ast import Recipe, IndexSum +from ast import Recipe, IndexSum, LeviCivita import FIAT import indices from derivatives import div, grad, curl -import numpy as np class HDivElement(FiatElement): def __init__(self, cell, degree): super(HDivElement, self).__init__(cell, degree) - + def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): + + static_key = (id(self), id(points), id(derivative)) + + if static_key in kernel_data.static: + phi = kernel_data.static[static_key][0] + else: + phi = p.Variable((u'\u03C6_e'.encode("utf-8") if derivative is None + else u"d\u03C6_e".encode("utf-8")) + str(self._id)) + data = self._tabulate(points, derivative) + kernel_data.static[static_key] = (phi, lambda: data) + + i = indices.BasisFunctionIndex(self.fiat_element.space_dimension()) + q = indices.PointIndex(points.points.shape[0]) + alpha = indices.DimensionIndex(kernel_data.tdim) + + if derivative is None: + if pullback: + beta = alpha + alpha = indices.DimensionIndex(kernel_data.gdim) + expr = IndexSum((beta,), kernel_data.J(alpha, beta) * phi[(beta, i, q)] + / kernel_data.detJ) + else: + expr = phi[(alpha, i, q)] + ind = ((alpha,), (i,), (q,)) + elif derivative is div: + if pullback: + expr = IndexSum((alpha,), phi[(alpha, alpha, i, q)] / kernel_data.detJ) + else: + expr = IndexSum((alpha,), phi[(alpha, alpha, i, q)]) + ind = ((), (i,), (q,)) + elif derivative is grad: + if pullback: + beta = indices.DimensionIndex(kernel_data.tdim) + gamma = indices.DimensionIndex(kernel_data.gdim) + delta = indices.DimensionIndex(kernel_data.gdim) + expr = IndexSum((alpha, beta), kernel_data.J(gamma, alpha) + * kernel_data.invJ(beta, delta) + * phi[(alpha, beta, i, q)]) \ + / kernel_data.detJ + ind = ((gamma, delta), (i,), (q,)) + else: + beta = indices.DimensionIndex(kernel_data.tdim) + expr = phi[(alpha, beta, i, q)] + ind = ((alpha, beta), (i,), (q,)) + elif derivative is curl: + beta = indices.DimensionIndex(kernel_data.tdim) + if pullback: + d = kernel_data.gdim + gamma = indices.DimensionIndex(d) + delta = indices.DimensionIndex(d) + zeta = indices.DimensionIndex(d) + expr = LeviCivita((zeta,), (gamma, delta), + IndexSum((alpha, beta), kernel_data.J(gamma, alpha) + * kernel_data.invJ(beta, delta) + * phi[(alpha, beta, i, q)])) \ + / kernel_data.detJ + else: + d = kernel_data.tdim + zeta = indices.DimensionIndex(d) + expr = LeviCivita((zeta,), (alpha, beta), phi[(alpha, beta, i, q)]) + if d == 2: + expr = expr.replace_indices((zeta, 2)) + ind = ((), (i,), (q,)) + elif d == 3: + ind = ((zeta,), (i,), (q,)) + else: + raise NotImplementedError + else: + raise NotImplementedError + + return Recipe(ind, expr) + class RaviartThomas(HDivElement): def __init__(self, cell, degree): diff --git a/finat/lagrange.py b/finat/lagrange.py index 49979d12a..534bc5d79 100644 --- a/finat/lagrange.py +++ b/finat/lagrange.py @@ -23,18 +23,15 @@ def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): static_key = (id(self), id(points), id(derivative)) - static_data = kernel_data.static - fiat_element = self._fiat_element - - if static_key in static_data: - phi = static_data[static_key][0] + if static_key in kernel_data.static: + phi = kernel_data.static[static_key][0] else: phi = p.Variable((u'\u03C6_e'.encode("utf-8") if derivative is None else u"d\u03C6_e".encode("utf-8")) + str(self._id)) data = self._tabulate(points, derivative) - static_data[static_key] = (phi, lambda: data) + kernel_data.static[static_key] = (phi, lambda: data) - i = indices.BasisFunctionIndex(fiat_element.space_dimension()) + i = indices.BasisFunctionIndex(self._fiat_element.space_dimension()) q = indices.PointIndex(points.points.shape[0]) if derivative is grad: @@ -58,13 +55,8 @@ def field_evaluation(self, field_var, points, raise ValueError( "Scalar elements do not have a %s operation") % derivative - basis = self.basis_evaluation(points, kernel_data, derivative, pullback) - (d, b, p) = basis.indices - phi = basis.expression - - expr = IndexSum(b, field_var[b[0]] * phi) - - return Recipe((d, (), p), expr) + return super(ScalarElement, self).field_evaluation( + field_var, points, kernel_data, derivative=None, pullback=True) def moment_evaluation(self, value, weights, points, kernel_data, derivative=None, pullback=True): @@ -72,18 +64,8 @@ def moment_evaluation(self, value, weights, points, raise ValueError( "Scalar elements do not have a %s operation") % derivative - basis = self.basis_evaluation(points, kernel_data, derivative, pullback) - (d, b, p) = basis.indices - phi = basis.expression - - (d_, b_, p_) = value.indices - psi = value.replace_indices(zip(d_ + p_, d + p)).expression - - w = weights.kernel_variable("w", kernel_data) - - expr = IndexSum(d + p, psi * phi * w[p]) - - return Recipe(((), b + b_, ()), expr) + return super(ScalarElement, self).moment_evaluation( + value, weights, points, kernel_data, derivative=None, pullback=True) def pullback(self, phi, kernel_data, derivative=None): From a73924444dcb33780d81173471ebc4545cd584ec Mon Sep 17 00:00:00 2001 From: David A Ham Date: Wed, 20 Aug 2014 10:37:12 +0100 Subject: [PATCH 035/749] Suppress instance attribute values. --- docs/source/_themes/finat/static/fenics.css_t | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/source/_themes/finat/static/fenics.css_t b/docs/source/_themes/finat/static/fenics.css_t index e562584cd..6c6e55de8 100644 --- a/docs/source/_themes/finat/static/fenics.css_t +++ b/docs/source/_themes/finat/static/fenics.css_t @@ -138,6 +138,10 @@ img.commit-author-img { display: none; } +em.property { + display: none; +} + a.commit-link { font-weight: bold; padding-bottom: 4px; From aee2feb94206f21b760e4c77a8fbd57ff5d45883 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Wed, 20 Aug 2014 10:48:39 +0100 Subject: [PATCH 036/749] Introduce more HDiv elements. Also minor rename to clarify which finite element classes are bases. --- finat/finiteelementbase.py | 7 ++++--- finat/hdiv.py | 18 ++++++++++++++++-- finat/lagrange.py | 11 ++++++----- finat/utils.py | 6 ++++++ 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 4974d164a..695584de6 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -111,10 +111,11 @@ def dual_evaluation(self, static_data): raise NotImplementedError -class FiatElement(FiniteElementBase): - """A finite element for which the tabulation is provided by FIAT.""" +class FiatElementBase(FiniteElementBase): + """Base class for finite elements for which the tabulation is provided + by FIAT.""" def __init__(self, cell, degree): - super(FiatElement, self).__init__() + super(FiatElementBase, self).__init__() self._cell = cell self._degree = degree diff --git a/finat/hdiv.py b/finat/hdiv.py index 24c3e60d4..9236a1722 100644 --- a/finat/hdiv.py +++ b/finat/hdiv.py @@ -1,12 +1,12 @@ import pymbolic.primitives as p -from finiteelementbase import FiatElement +from finiteelementbase import FiatElementBase from ast import Recipe, IndexSum, LeviCivita import FIAT import indices from derivatives import div, grad, curl -class HDivElement(FiatElement): +class HDivElement(FiatElementBase): def __init__(self, cell, degree): super(HDivElement, self).__init__(cell, degree) @@ -89,3 +89,17 @@ def __init__(self, cell, degree): super(RaviartThomas, self).__init__(cell, degree) self._fiat_element = FIAT.RaviartThomas(cell, degree) + + +class BrezziDouglasMarini(HDivElement): + def __init__(self, cell, degree): + super(RaviartThomas, self).__init__(cell, degree) + + self._fiat_element = FIAT.BrezziDouglasMarini(cell, degree) + + +class BrezziDouglasFortinMarini(HDivElement): + def __init__(self, cell, degree): + super(RaviartThomas, self).__init__(cell, degree) + + self._fiat_element = FIAT.BrezziDouglasFortinMarini(cell, degree) diff --git a/finat/lagrange.py b/finat/lagrange.py index 534bc5d79..f610a3db1 100644 --- a/finat/lagrange.py +++ b/finat/lagrange.py @@ -1,12 +1,12 @@ import pymbolic.primitives as p -from finiteelementbase import FiatElement +from finiteelementbase import FiatElementBase from ast import Recipe, IndexSum import FIAT import indices from derivatives import grad -class ScalarElement(FiatElement): +class ScalarElement(FiatElementBase): def __init__(self, cell, degree): super(ScalarElement, self).__init__(cell, degree) @@ -35,14 +35,15 @@ def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): q = indices.PointIndex(points.points.shape[0]) if derivative is grad: - alpha = indices.DimensionIndex(points.points.shape[1]) - ind = ((alpha,), (i,), (q,)) + alpha = indices.DimensionIndex(kernel_data.tdim) if pullback: - beta = indices.DimensionIndex(points.points.shape[1]) + beta = alpha + alpha = indices.DimensionIndex(kernel_data.gdim) invJ = kernel_data.invJ[(beta, alpha)] expr = IndexSum((beta,), invJ * phi[(beta, i, q)]) else: expr = phi[(alpha, i, q)] + ind = ((alpha,), (i,), (q,)) else: ind = ((), (i,), (q,)) expr = phi[(i, q)] diff --git a/finat/utils.py b/finat/utils.py index 1c7c35a3c..42f52e4ab 100644 --- a/finat/utils.py +++ b/finat/utils.py @@ -24,6 +24,12 @@ def __init__(self, coordinate_element, affine=None): self.params = {} self.geometry = {} + #: The geometric dimension of the physical space. + self.gdim = coordinate_element._dimension + + #: The topological dimension of the reference element + self.tdim = coordinate_element._cell.get_spatial_dimension() + @property def J(self): From 0b37bb944decd07b77be9247e532362cacb89109 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Wed, 20 Aug 2014 12:05:25 +0100 Subject: [PATCH 037/749] refactor out basis tabulation --- finat/finiteelementbase.py | 21 ++++++++++++++++++++- finat/hdiv.py | 10 +--------- finat/lagrange.py | 10 +--------- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 695584de6..316d086ff 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -1,3 +1,4 @@ +import pymbolic.primitives as p import numpy as np from ast import Recipe, IndexSum @@ -156,6 +157,20 @@ def _tabulate(self, points, derivative): return self._fiat_element.tabulate(0, points.points)[ tuple([0] * points.points.shape[1])] + def _tabulated_basis(self, points, kernel_data, derivative): + + static_key = (id(self), id(points), id(derivative)) + + if static_key in kernel_data.static: + phi = kernel_data.static[static_key][0] + else: + phi = p.Variable((u'\u03C6_e'.encode("utf-8") if derivative is None + else u"d\u03C6_e".encode("utf-8")) + str(self._id)) + data = self._tabulate(points, derivative) + kernel_data.static[static_key] = (phi, lambda: data) + + return phi + def field_evaluation(self, field_var, points, kernel_data, derivative=None, pullback=True): @@ -179,6 +194,10 @@ def moment_evaluation(self, value, weights, points, w = weights.kernel_variable("w", kernel_data) - expr = IndexSum(d + p, psi * phi * w[p]) + if pullback: + # Note. Do detJ cancellation here. + expr = IndexSum(d + p, psi * phi * w[p] * kernel_data.detJ) + else: + expr = IndexSum(d + p, psi * phi * w[p]) return Recipe(((), b + b_, ()), expr) diff --git a/finat/hdiv.py b/finat/hdiv.py index 9236a1722..ccb18cd3f 100644 --- a/finat/hdiv.py +++ b/finat/hdiv.py @@ -12,15 +12,7 @@ def __init__(self, cell, degree): def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): - static_key = (id(self), id(points), id(derivative)) - - if static_key in kernel_data.static: - phi = kernel_data.static[static_key][0] - else: - phi = p.Variable((u'\u03C6_e'.encode("utf-8") if derivative is None - else u"d\u03C6_e".encode("utf-8")) + str(self._id)) - data = self._tabulate(points, derivative) - kernel_data.static[static_key] = (phi, lambda: data) + phi = self._tabulated_basis(points, kernel_data, derivative) i = indices.BasisFunctionIndex(self.fiat_element.space_dimension()) q = indices.PointIndex(points.points.shape[0]) diff --git a/finat/lagrange.py b/finat/lagrange.py index f610a3db1..fd82b0076 100644 --- a/finat/lagrange.py +++ b/finat/lagrange.py @@ -21,15 +21,7 @@ def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): raise ValueError( "Scalar elements do not have a %s operation") % derivative - static_key = (id(self), id(points), id(derivative)) - - if static_key in kernel_data.static: - phi = kernel_data.static[static_key][0] - else: - phi = p.Variable((u'\u03C6_e'.encode("utf-8") if derivative is None - else u"d\u03C6_e".encode("utf-8")) + str(self._id)) - data = self._tabulate(points, derivative) - kernel_data.static[static_key] = (phi, lambda: data) + phi = self._tabulated_basis(points, kernel_data, derivative) i = indices.BasisFunctionIndex(self._fiat_element.space_dimension()) q = indices.PointIndex(points.points.shape[0]) From eedf6f51595739988d868d8fb398126e3e700e25 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Wed, 20 Aug 2014 12:05:53 +0100 Subject: [PATCH 038/749] initial h curl implementation --- finat/hcurl.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 finat/hcurl.py diff --git a/finat/hcurl.py b/finat/hcurl.py new file mode 100644 index 000000000..2ffa180f9 --- /dev/null +++ b/finat/hcurl.py @@ -0,0 +1,78 @@ +from finiteelementbase import FiatElementBase +from ast import Recipe, IndexSum, LeviCivita +import FIAT +import indices +from derivatives import div, grad, curl + + +class HCurlElement(FiatElementBase): + def __init__(self, cell, degree): + super(HCurlElement, self).__init__(cell, degree) + + def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): + + phi = self._tabulated_basis(points, kernel_data, derivative) + + i = indices.BasisFunctionIndex(self.fiat_element.space_dimension()) + q = indices.PointIndex(points.points.shape[0]) + alpha = indices.DimensionIndex(kernel_data.tdim) + + if derivative is None: + if pullback: + beta = alpha + alpha = indices.DimensionIndex(kernel_data.gdim) + expr = IndexSum((beta,), kernel_data.invJ(beta, alpha) * phi[(beta, i, q)]) + else: + expr = phi[(alpha, i, q)] + ind = ((alpha,), (i,), (q,)) + elif derivative is div: + if pullback: + beta = indices.DimensionIndex(kernel_data.tdim) + gamma = indices.DimensionIndex(kernel_data.gdim) + expr = IndexSum((gamma,), kernel_data.invJ(alpha, gamma) + * kernel_data.invJ(beta, gamma) + * phi[(alpha, beta, i, q)]) + else: + expr = IndexSum((alpha,), phi[(alpha, alpha, i, q)]) + ind = ((), (i,), (q,)) + elif derivative is grad: + if pullback: + beta = indices.DimensionIndex(kernel_data.tdim) + gamma = indices.DimensionIndex(kernel_data.gdim) + delta = indices.DimensionIndex(kernel_data.gdim) + expr = IndexSum((alpha, beta), kernel_data.invJ(alpha, gamma) + * kernel_data.invJ(beta, delta) + * phi[(alpha, beta, i, q)]) + ind = ((gamma, delta), (i,), (q,)) + else: + beta = indices.DimensionIndex(kernel_data.tdim) + expr = phi[(alpha, beta, i, q)] + ind = ((alpha, beta), (i,), (q,)) + elif derivative is curl: + beta = indices.DimensionIndex(kernel_data.tdim) + d = kernel_data.tdim + zeta = indices.DimensionIndex(d) + if pullback: + if d == 3: + gamma = indices.DimensionIndex(kernel_data.tdim) + expr = IndexSum((gamma,), kernel_data.J(zeta, gamma) * + LeviCivita((gamma,), (alpha, beta), phi[(alpha, beta, i, q)])) \ + / kernel_data.detJ + elif d == 2: + expr = LeviCivita((2,), (alpha, beta), phi[(alpha, beta, i, q)]) \ + / kernel_data.detJ + else: + if d == 3: + expr = LeviCivita((zeta,), (alpha, beta), phi[(alpha, beta, i, q)]) + if d == 2: + expr = LeviCivita((2,), (alpha, beta), phi[(alpha, beta, i, q)]) + if d == 2: + ind = ((), (i,), (q,)) + elif d == 3: + ind = ((zeta,), (i,), (q,)) + else: + raise NotImplementedError + else: + raise NotImplementedError + + return Recipe(ind, expr) From e0f7e7d7213608a765bb6bc2759766158ece259e Mon Sep 17 00:00:00 2001 From: David A Ham Date: Wed, 20 Aug 2014 12:13:11 +0100 Subject: [PATCH 039/749] import all the elementnames --- finat/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/finat/__init__.py b/finat/__init__.py index 3edeaf152..a13074719 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -1,6 +1,7 @@ - -from lagrange import Lagrange +from lagrange import Lagrange, DiscontinuousLagrange +from hdiv import RaviartThomas, BrezziDouglasMarini, BrezziDouglasFortinMarini from vectorfiniteelement import VectorFiniteElement from points import PointSet from utils import KernelData from derivatives import div, grad, curl + From b46707fa531ee60a0e38c9e8849c6f01f3cf332c Mon Sep 17 00:00:00 2001 From: David A Ham Date: Wed, 20 Aug 2014 16:42:08 +0100 Subject: [PATCH 040/749] remove unused import --- finat/hdiv.py | 1 - 1 file changed, 1 deletion(-) diff --git a/finat/hdiv.py b/finat/hdiv.py index ccb18cd3f..6f8019154 100644 --- a/finat/hdiv.py +++ b/finat/hdiv.py @@ -1,4 +1,3 @@ -import pymbolic.primitives as p from finiteelementbase import FiatElementBase from ast import Recipe, IndexSum, LeviCivita import FIAT From fe986ec212c5ac94efb3e3b55616e67ebbfc40f5 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Wed, 20 Aug 2014 16:42:42 +0100 Subject: [PATCH 041/749] draft FInAT interpreter --- finat/interpreter.py | 147 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 finat/interpreter.py diff --git a/finat/interpreter.py b/finat/interpreter.py new file mode 100644 index 000000000..bd5a9760a --- /dev/null +++ b/finat/interpreter.py @@ -0,0 +1,147 @@ +"""An interpreter for FInAT recipes. This is not expected to be +performant, but rather to provide a test facility for FInAT code.""" +import pymbolic.primitives as p +from pymbolic.mapper.evaluator import FloatEvaluationMapper, UnknownVariableError +from ast import IndexSum, ForAll, LeviCivita +import numpy as np + + +def _as_range(e): + """Convert a slice to a range.""" + + return range(e.start or 0, e.stop, e.step or 1) + + +class FinatEvaluationMapper(FloatEvaluationMapper): + + def __init__(self, context={}): + """ + :arg context: a mapping from variable names to values + """ + super(FinatEvaluationMapper, self).__init__(context) + + self.indices = {} + + def map_variable(self, expr): + try: + var = self.context[expr.name] + if isinstance(var, p.Expression): + return self.rec(var) + else: + return var + except KeyError: + raise UnknownVariableError(expr.name) + + def map_recipe(self, expr): + """Evaluate expr for all values of free indices""" + + d, b, p = expr.indices + body = expr.expression + + return self.rec(ForAll(d+b+p, body)) + + def map_index_sum(self, expr): + + indices, body = expr.children + + # Sum over multiple indices recursively. + if len(indices) > 1: + expr = IndexSum(indices[1:], body) + + idx = indices[0] + + e = idx.extent + + total = 0.0 + for i in _as_range(e): + self.indices[idx] = i + total += self.rec(expr) + + self.indices.pop(idx) + + return total + + def map_index(self, expr): + + return self.indices[expr] + + def map_for_all(self, expr): + + indices, body = expr.children + + # Execute over multiple indices recursively. + if len(indices) > 1: + expr = IndexSum(indices[1:], body) + + idx = indices[0] + + e = idx.extent + + total = [] + for i in _as_range(e): + self.indices[idx] = i + total.append(self.rec(expr)) + + self.indices.pop(idx) + + return np.array(total) + + def map_levi_civita(self, expr): + + free, bound, body = expr.children + + if len(bound) == 3: + return self.rec(IndexSum(bound[:1], + LeviCivita(bound[:1], bound[1:], body))) + elif len(bound) == 2: + + self.indices[bound[0]] = (self.indices[free[0]] + 1) % 3 + self.indices[bound[1]] = (self.indices[free[0]] + 2) % 3 + tmp = self.rec(body) + self.indices[bound[1]] = (self.indices[free[0]] + 1) % 3 + self.indices[bound[0]] = (self.indices[free[0]] + 2) % 3 + tmp -= self.rec(body) + + self.indices.pop(bound[0]) + self.indices.pop(bound[1]) + + return tmp + + elif len(bound) == 1: + + i = self.indices[free[0]] + j = self.indices[free[1]] + + if i == j: + return 0 + elif j == (i + 1) % 3: + k = i + 2 % 3 + sign = 1 + elif j == (i + 2) % 3: + k = i + 1 % 3 + sign = -1 + + self.indices[bound[0]] = k + return sign * self.rec(body) + + elif len(bound) == 0: + + eijk = np.zeros((3, 3, 3)) + eijk[0, 1, 2] = eijk[1, 2, 0] = eijk[2, 0, 1] = 1 + eijk[0, 2, 1] = eijk[2, 1, 0] = eijk[1, 0, 2] = -1 + + i = self.indices[free[0]] + j = self.indices[free[1]] + k = self.indices[free[1]] + sign = eijk[i, j, k] + + if sign != 0: + return sign * self.rec(body) + else: + return 0 + + raise NotImplementedError + + +def evaluate(expression, context={}): + return FinatEvaluationMapper(context)(expression) From 61c5e6b548f0631cf21b2295a308a695fbb9d415 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Wed, 20 Aug 2014 17:08:44 +0100 Subject: [PATCH 042/749] include kernel_data in evaluate --- finat/interpreter.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/finat/interpreter.py b/finat/interpreter.py index bd5a9760a..06be85a7b 100644 --- a/finat/interpreter.py +++ b/finat/interpreter.py @@ -4,7 +4,7 @@ from pymbolic.mapper.evaluator import FloatEvaluationMapper, UnknownVariableError from ast import IndexSum, ForAll, LeviCivita import numpy as np - +import copy def _as_range(e): """Convert a slice to a range.""" @@ -143,5 +143,16 @@ def map_levi_civita(self, expr): raise NotImplementedError -def evaluate(expression, context={}): +def evaluate(expression, context={}, kernel_data=None): + """Take a FInAT expression and a set of definitions for undefined + variables in the expression, and optionally the current kernel + data. Evaluate the expression returning a Float or numpy array + according to the size of the expression. + """ + + if kernel_data: + context = copy.copy(context) + for var in kernel_data.static.values(): + context[var[0]] = var[1] + return FinatEvaluationMapper(context)(expression) From 327b531a33d731d01cea1d5d4af68d649325d8c9 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Wed, 20 Aug 2014 17:18:25 +0100 Subject: [PATCH 043/749] whitespace! --- finat/__init__.py | 1 - finat/interpreter.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/finat/__init__.py b/finat/__init__.py index a13074719..a0144017b 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -4,4 +4,3 @@ from points import PointSet from utils import KernelData from derivatives import div, grad, curl - diff --git a/finat/interpreter.py b/finat/interpreter.py index 06be85a7b..4d616845e 100644 --- a/finat/interpreter.py +++ b/finat/interpreter.py @@ -6,6 +6,7 @@ import numpy as np import copy + def _as_range(e): """Convert a slice to a range.""" @@ -38,7 +39,7 @@ def map_recipe(self, expr): d, b, p = expr.indices body = expr.expression - return self.rec(ForAll(d+b+p, body)) + return self.rec(ForAll(d + b + p, body)) def map_index_sum(self, expr): From a2bc465aef4698ee022c586c4e981bf54c8ad414 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Wed, 20 Aug 2014 17:36:39 +0100 Subject: [PATCH 044/749] So much more fun without the infinite recursion --- finat/__init__.py | 1 + finat/interpreter.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/finat/__init__.py b/finat/__init__.py index a0144017b..b1cd329ed 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -4,3 +4,4 @@ from points import PointSet from utils import KernelData from derivatives import div, grad, curl +import interpreter diff --git a/finat/interpreter.py b/finat/interpreter.py index 4d616845e..2aa698942 100644 --- a/finat/interpreter.py +++ b/finat/interpreter.py @@ -48,6 +48,8 @@ def map_index_sum(self, expr): # Sum over multiple indices recursively. if len(indices) > 1: expr = IndexSum(indices[1:], body) + else: + expr = body idx = indices[0] @@ -73,6 +75,8 @@ def map_for_all(self, expr): # Execute over multiple indices recursively. if len(indices) > 1: expr = IndexSum(indices[1:], body) + else: + expr = body idx = indices[0] @@ -154,6 +158,6 @@ def evaluate(expression, context={}, kernel_data=None): if kernel_data: context = copy.copy(context) for var in kernel_data.static.values(): - context[var[0]] = var[1] + context[var[0].name] = var[1]() return FinatEvaluationMapper(context)(expression) From 597feb8898f6a073b0e7cd2bf154a148ad4d92cc Mon Sep 17 00:00:00 2001 From: David Ham Date: Wed, 20 Aug 2014 23:08:22 +0100 Subject: [PATCH 045/749] IndexSum != ForAll --- finat/ast.py | 2 +- finat/interpreter.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index 0be768519..ab6e941cc 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -229,7 +229,7 @@ class Delta(p._MultiChildExpression): .. math:: - \delta[i_0,i_1]*\mathrm{body}. + \mathrm{Delta((i, j), body)} = \delta_{ij}*\mathrm{body}. :param indices: a sequence of indices. :param body: an expression. diff --git a/finat/interpreter.py b/finat/interpreter.py index 2aa698942..afea000ce 100644 --- a/finat/interpreter.py +++ b/finat/interpreter.py @@ -74,7 +74,7 @@ def map_for_all(self, expr): # Execute over multiple indices recursively. if len(indices) > 1: - expr = IndexSum(indices[1:], body) + expr = ForAll(indices[1:], body) else: expr = body From daba4024abd461764189421b8111e5637be35222 Mon Sep 17 00:00:00 2001 From: David Ham Date: Wed, 20 Aug 2014 23:20:02 +0100 Subject: [PATCH 046/749] some silly copy and paste errors --- finat/lagrange.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/finat/lagrange.py b/finat/lagrange.py index fd82b0076..8f514f6fd 100644 --- a/finat/lagrange.py +++ b/finat/lagrange.py @@ -49,7 +49,7 @@ def field_evaluation(self, field_var, points, "Scalar elements do not have a %s operation") % derivative return super(ScalarElement, self).field_evaluation( - field_var, points, kernel_data, derivative=None, pullback=True) + field_var, points, kernel_data, derivative, pullback) def moment_evaluation(self, value, weights, points, kernel_data, derivative=None, pullback=True): @@ -58,7 +58,7 @@ def moment_evaluation(self, value, weights, points, "Scalar elements do not have a %s operation") % derivative return super(ScalarElement, self).moment_evaluation( - value, weights, points, kernel_data, derivative=None, pullback=True) + value, weights, points, kernel_data, derivative, pullback) def pullback(self, phi, kernel_data, derivative=None): From 2bf71ec66521b6245a1af1ca5a4fedc9c675ed5c Mon Sep 17 00:00:00 2001 From: David Ham Date: Wed, 20 Aug 2014 23:39:31 +0100 Subject: [PATCH 047/749] fix prettyprinting of indices --- finat/ast.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/finat/ast.py b/finat/ast.py index ab6e941cc..a3593f9d4 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -3,7 +3,7 @@ """ import pymbolic.primitives as p from pymbolic.mapper import IdentityMapper -from pymbolic.mapper.stringifier import StringifyMapper, PREC_NONE +from pymbolic.mapper.stringifier import StringifyMapper, PREC_NONE, PREC_CALL from indices import IndexBase, DimensionIndex, BasisFunctionIndex, PointIndex @@ -37,6 +37,15 @@ def map_delta(self, expr, *args): class _StringifyMapper(StringifyMapper): + def map_subscript(self, expr, enclosing_prec): + return self.parenthesize_if_needed( + self.format("%s[%s]", + self.rec(expr.aggregate, PREC_CALL), + self.join_rec(", ", expr.index, PREC_NONE) if + isinstance(expr.index, tuple) else + self.rec(expr.index, PREC_NONE)), + enclosing_prec, PREC_CALL) + def map_recipe(self, expr, enclosing_prec): return self.format("Recipe(%s, %s)", self.rec(expr.indices, PREC_NONE), From 827dd178a03426affbcc0ac5902895b4bd356262 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Thu, 21 Aug 2014 09:12:02 +0100 Subject: [PATCH 048/749] whitespace! --- finat/ast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/ast.py b/finat/ast.py index a3593f9d4..3cad68713 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -41,7 +41,7 @@ def map_subscript(self, expr, enclosing_prec): return self.parenthesize_if_needed( self.format("%s[%s]", self.rec(expr.aggregate, PREC_CALL), - self.join_rec(", ", expr.index, PREC_NONE) if + self.join_rec(", ", expr.index, PREC_NONE) if isinstance(expr.index, tuple) else self.rec(expr.index, PREC_NONE)), enclosing_prec, PREC_CALL) From 876be2f12d65bf996c436e7bdb073519bede40ad Mon Sep 17 00:00:00 2001 From: David A Ham Date: Thu, 21 Aug 2014 11:48:04 +0100 Subject: [PATCH 049/749] Make variable name generation a bit safer --- finat/finiteelementbase.py | 10 +++------- finat/utils.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 316d086ff..419779a3d 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -11,11 +11,7 @@ def __init__(self, *args, **kwargs): class FiniteElementBase(object): def __init__(self): - - self._id = FiniteElementBase._count - FiniteElementBase._count += 1 - - _count = 0 + pass @property def cell(self): @@ -164,8 +160,8 @@ def _tabulated_basis(self, points, kernel_data, derivative): if static_key in kernel_data.static: phi = kernel_data.static[static_key][0] else: - phi = p.Variable((u'\u03C6_e'.encode("utf-8") if derivative is None - else u"d\u03C6_e".encode("utf-8")) + str(self._id)) + phi = p.Variable(("d" if derivative else "") + + kernel_data.tabulation_variable_name(self, points)) data = self._tabulate(points, derivative) kernel_data.static[static_key] = (phi, lambda: data) diff --git a/finat/utils.py b/finat/utils.py index 42f52e4ab..60f31eb6f 100644 --- a/finat/utils.py +++ b/finat/utils.py @@ -30,6 +30,24 @@ def __init__(self, coordinate_element, affine=None): #: The topological dimension of the reference element self.tdim = coordinate_element._cell.get_spatial_dimension() + self._variable_count = 0 + self._variable_cache = {} + + def tabulation_variable_name(self, element, points): + """Given a finite element and a point set, return a variable name phi_n + where n is guaranteed to be unique to that combination of element and + points.""" + + key = (id(element), id(points)) + + try: + return self._variable_cache[key] + except KeyError: + self._variable_cache[key] = u'\u03C6_'.encode("utf-8") \ + + str(self._variable_count) + self._variable_count += 1 + return self._variable_cache[key] + @property def J(self): From d818bd7465b8cdb895246e85c8c3f430b05b3870 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Tue, 26 Aug 2014 14:50:05 +0100 Subject: [PATCH 050/749] Subscript stringifier is now in pymbolic --- finat/ast.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index 3cad68713..19cd1bd63 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -37,15 +37,6 @@ def map_delta(self, expr, *args): class _StringifyMapper(StringifyMapper): - def map_subscript(self, expr, enclosing_prec): - return self.parenthesize_if_needed( - self.format("%s[%s]", - self.rec(expr.aggregate, PREC_CALL), - self.join_rec(", ", expr.index, PREC_NONE) if - isinstance(expr.index, tuple) else - self.rec(expr.index, PREC_NONE)), - enclosing_prec, PREC_CALL) - def map_recipe(self, expr, enclosing_prec): return self.format("Recipe(%s, %s)", self.rec(expr.indices, PREC_NONE), From 6a2ae4e87a8c8a0390dba14100749a7d09e4e8b7 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Tue, 26 Aug 2014 15:13:54 +0100 Subject: [PATCH 051/749] Put in some jacobians on the way to pullback implementation. --- finat/finiteelementbase.py | 2 +- finat/hcurl.py | 42 ++++++++++++++++++++--------------- finat/hdiv.py | 45 +++++++++++++++++++++----------------- finat/lagrange.py | 2 +- finat/utils.py | 11 ++++------ 5 files changed, 55 insertions(+), 47 deletions(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 419779a3d..aa916b5a5 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -192,7 +192,7 @@ def moment_evaluation(self, value, weights, points, if pullback: # Note. Do detJ cancellation here. - expr = IndexSum(d + p, psi * phi * w[p] * kernel_data.detJ) + expr = IndexSum(d + p, psi * phi * w[p] * kernel_data.detJ(points)) else: expr = IndexSum(d + p, psi * phi * w[p]) diff --git a/finat/hcurl.py b/finat/hcurl.py index 2ffa180f9..a43d7024b 100644 --- a/finat/hcurl.py +++ b/finat/hcurl.py @@ -15,52 +15,58 @@ def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): i = indices.BasisFunctionIndex(self.fiat_element.space_dimension()) q = indices.PointIndex(points.points.shape[0]) - alpha = indices.DimensionIndex(kernel_data.tdim) + + tIndex = lambda: indices.DimensionIndex(kernel_data.tdim) + gIndex = lambda: indices.DimensionIndex(kernel_data.gdim) + + alpha = tIndex() + + # The lambda functions here prevent spurious instantiations of invJ and detJ + invJ = lambda: kernel_data.invJ(points) + detJ = lambda: kernel_data.detJ(points) if derivative is None: if pullback: beta = alpha - alpha = indices.DimensionIndex(kernel_data.gdim) - expr = IndexSum((beta,), kernel_data.invJ(beta, alpha) * phi[(beta, i, q)]) + alpha = gIndex() + expr = IndexSum((beta,), invJ()[beta, alpha] * phi[(beta, i, q)]) else: expr = phi[(alpha, i, q)] ind = ((alpha,), (i,), (q,)) elif derivative is div: if pullback: - beta = indices.DimensionIndex(kernel_data.tdim) - gamma = indices.DimensionIndex(kernel_data.gdim) - expr = IndexSum((gamma,), kernel_data.invJ(alpha, gamma) - * kernel_data.invJ(beta, gamma) + beta = tIndex() + gamma = gIndex() + expr = IndexSum((gamma,), invJ()[alpha, gamma] * invJ()[beta, gamma] * phi[(alpha, beta, i, q)]) else: expr = IndexSum((alpha,), phi[(alpha, alpha, i, q)]) ind = ((), (i,), (q,)) elif derivative is grad: if pullback: - beta = indices.DimensionIndex(kernel_data.tdim) - gamma = indices.DimensionIndex(kernel_data.gdim) - delta = indices.DimensionIndex(kernel_data.gdim) - expr = IndexSum((alpha, beta), kernel_data.invJ(alpha, gamma) - * kernel_data.invJ(beta, delta) + beta = tIndex() + gamma = gIndex() + delta = gIndex() + expr = IndexSum((alpha, beta), invJ()[alpha, gamma] * invJ(beta, delta) * phi[(alpha, beta, i, q)]) ind = ((gamma, delta), (i,), (q,)) else: - beta = indices.DimensionIndex(kernel_data.tdim) + beta = tIndex() expr = phi[(alpha, beta, i, q)] ind = ((alpha, beta), (i,), (q,)) elif derivative is curl: - beta = indices.DimensionIndex(kernel_data.tdim) + beta = tIndex() d = kernel_data.tdim - zeta = indices.DimensionIndex(d) + zeta = tIndex() if pullback: if d == 3: - gamma = indices.DimensionIndex(kernel_data.tdim) + gamma = tIndex() expr = IndexSum((gamma,), kernel_data.J(zeta, gamma) * LeviCivita((gamma,), (alpha, beta), phi[(alpha, beta, i, q)])) \ - / kernel_data.detJ + / detJ() elif d == 2: expr = LeviCivita((2,), (alpha, beta), phi[(alpha, beta, i, q)]) \ - / kernel_data.detJ + / detJ() else: if d == 3: expr = LeviCivita((zeta,), (alpha, beta), phi[(alpha, beta, i, q)]) diff --git a/finat/hdiv.py b/finat/hdiv.py index 6f8019154..42497c8f0 100644 --- a/finat/hdiv.py +++ b/finat/hdiv.py @@ -15,32 +15,39 @@ def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): i = indices.BasisFunctionIndex(self.fiat_element.space_dimension()) q = indices.PointIndex(points.points.shape[0]) - alpha = indices.DimensionIndex(kernel_data.tdim) + + tIndex = lambda: indices.DimensionIndex(kernel_data.tdim) + gIndex = lambda: indices.DimensionIndex(kernel_data.gdim) + + alpha = tIndex() + + # The lambda functions here prevent spurious instantiations of invJ and detJ + J = lambda: kernel_data.J(points) + invJ = lambda: kernel_data.invJ(points) + detJ = lambda: kernel_data.detJ(points) if derivative is None: if pullback: beta = alpha - alpha = indices.DimensionIndex(kernel_data.gdim) - expr = IndexSum((beta,), kernel_data.J(alpha, beta) * phi[(beta, i, q)] - / kernel_data.detJ) + alpha = gIndex() + expr = IndexSum((beta,), J()[alpha, beta] * phi[(beta, i, q)] + / detJ()) else: expr = phi[(alpha, i, q)] ind = ((alpha,), (i,), (q,)) elif derivative is div: if pullback: - expr = IndexSum((alpha,), phi[(alpha, alpha, i, q)] / kernel_data.detJ) + expr = IndexSum((alpha,), phi[(alpha, alpha, i, q)] / detJ()) else: expr = IndexSum((alpha,), phi[(alpha, alpha, i, q)]) ind = ((), (i,), (q,)) elif derivative is grad: if pullback: - beta = indices.DimensionIndex(kernel_data.tdim) - gamma = indices.DimensionIndex(kernel_data.gdim) - delta = indices.DimensionIndex(kernel_data.gdim) - expr = IndexSum((alpha, beta), kernel_data.J(gamma, alpha) - * kernel_data.invJ(beta, delta) - * phi[(alpha, beta, i, q)]) \ - / kernel_data.detJ + beta = tIndex() + gamma = gIndex() + delta = gIndex() + expr = IndexSum((alpha, beta), J()[gamma, alpha] * invJ()[beta, delta] + * phi[(alpha, beta, i, q)]) / detJ() ind = ((gamma, delta), (i,), (q,)) else: beta = indices.DimensionIndex(kernel_data.tdim) @@ -50,17 +57,15 @@ def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): beta = indices.DimensionIndex(kernel_data.tdim) if pullback: d = kernel_data.gdim - gamma = indices.DimensionIndex(d) - delta = indices.DimensionIndex(d) - zeta = indices.DimensionIndex(d) + gamma = gIndex() + delta = gIndex() + zeta = gIndex() expr = LeviCivita((zeta,), (gamma, delta), - IndexSum((alpha, beta), kernel_data.J(gamma, alpha) - * kernel_data.invJ(beta, delta) - * phi[(alpha, beta, i, q)])) \ - / kernel_data.detJ + IndexSum((alpha, beta), J()[gamma, alpha] * invJ()[beta, delta] + * phi[(alpha, beta, i, q)])) / detJ() else: d = kernel_data.tdim - zeta = indices.DimensionIndex(d) + zeta = tIndex() expr = LeviCivita((zeta,), (alpha, beta), phi[(alpha, beta, i, q)]) if d == 2: expr = expr.replace_indices((zeta, 2)) diff --git a/finat/lagrange.py b/finat/lagrange.py index 8f514f6fd..e9c81a400 100644 --- a/finat/lagrange.py +++ b/finat/lagrange.py @@ -31,7 +31,7 @@ def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): if pullback: beta = alpha alpha = indices.DimensionIndex(kernel_data.gdim) - invJ = kernel_data.invJ[(beta, alpha)] + invJ = kernel_data.invJ(points)[(beta, alpha)] expr = IndexSum((beta,), invJ * phi[(beta, i, q)]) else: expr = phi[(alpha, i, q)] diff --git a/finat/utils.py b/finat/utils.py index 60f31eb6f..28778ea44 100644 --- a/finat/utils.py +++ b/finat/utils.py @@ -48,8 +48,7 @@ def tabulation_variable_name(self, element, points): self._variable_count += 1 return self._variable_cache[key] - @property - def J(self): + def J(self, points): try: return self.geometry["J"] @@ -57,11 +56,10 @@ def J(self): self.geometry["J"] = p.Variable("J") return self.geometry["J"] - @property - def invJ(self): + def invJ(self, points): # ensure J exists - self.J + self.J(points) try: return self.geometry["invJ"] @@ -69,8 +67,7 @@ def invJ(self): self.geometry["invJ"] = p.Variable("invJ") return self.geometry["invJ"] - @property - def detJ(self): + def detJ(self, points): # ensure J exists self.J From f92f56b9089ceeb6c9bdbb27a20bf5851163a4a5 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Tue, 26 Aug 2014 15:52:55 +0100 Subject: [PATCH 052/749] inconsistent commas --- finat/vectorfiniteelement.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/finat/vectorfiniteelement.py b/finat/vectorfiniteelement.py index 55cf7b480..a2233624c 100644 --- a/finat/vectorfiniteelement.py +++ b/finat/vectorfiniteelement.py @@ -21,11 +21,11 @@ def __init__(self, element, dimension): :param element: The scalar finite element. :param dimension: The geometric dimension of the vector element. - :math:`\boldsymbol\phi_{i,\beta}` is, of course, vector-valued. If + :math:`\boldsymbol\phi_{i\beta}` is, of course, vector-valued. If we subscript the vector-value with :math:`\alpha` then we can write: .. math:: - \boldsymbol\phi_{\alpha,(i,\beta)} = \delta_{\alpha,\beta}\phi_i + \boldsymbol\phi_{\alpha(i\beta)} = \delta_{\alpha\beta}\phi_i This form enables the simplification of the loop nests which will eventually be created, so it is the form we employ here.""" From 8f99f8bb3b8cacc38d238bd1f57189765ef0006c Mon Sep 17 00:00:00 2001 From: David A Ham Date: Tue, 26 Aug 2014 15:58:52 +0100 Subject: [PATCH 053/749] one more comma --- finat/vectorfiniteelement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/vectorfiniteelement.py b/finat/vectorfiniteelement.py index a2233624c..043d950b3 100644 --- a/finat/vectorfiniteelement.py +++ b/finat/vectorfiniteelement.py @@ -11,7 +11,7 @@ def __init__(self, element, dimension): .. math:: - \boldsymbol\phi_{\beta,i} = \mathbf{e}_{\beta}\phi_i + \boldsymbol\phi_{\betai} = \mathbf{e}_{\beta}\phi_i Where :math:`\{\mathbf{e}_\beta,\, \beta=0\ldots\mathrm{dim}\}` is the basis for :math:`\mathbb{R}^{\mathrm{dim}}` and From 864e0dde88ec9735dc127f62babc4e4bdad3398d Mon Sep 17 00:00:00 2001 From: David A Ham Date: Tue, 26 Aug 2014 15:59:27 +0100 Subject: [PATCH 054/749] one more comma --- finat/vectorfiniteelement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/vectorfiniteelement.py b/finat/vectorfiniteelement.py index 043d950b3..9728b0416 100644 --- a/finat/vectorfiniteelement.py +++ b/finat/vectorfiniteelement.py @@ -11,7 +11,7 @@ def __init__(self, element, dimension): .. math:: - \boldsymbol\phi_{\betai} = \mathbf{e}_{\beta}\phi_i + \boldsymbol\phi_{\beta i} = \mathbf{e}_{\beta}\phi_i Where :math:`\{\mathbf{e}_\beta,\, \beta=0\ldots\mathrm{dim}\}` is the basis for :math:`\mathbb{R}^{\mathrm{dim}}` and From 5c1701623965aa1e541e18aeb896580daa46f515 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Tue, 26 Aug 2014 18:18:22 +0100 Subject: [PATCH 055/749] Symbolic definitions for geometry operations. --- finat/ast.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++++ finat/utils.py | 52 +++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index 19cd1bd63..6aa39d66f 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -33,6 +33,8 @@ def map_delta(self, expr, *args): map_wave = map_delta map_index_sum = map_delta map_levi_civita = map_delta + map_inverse = map_delta + map_det = map_delta class _StringifyMapper(StringifyMapper): @@ -55,6 +57,22 @@ def map_levi_civita(self, expr, *args): return self.format("LeviCivita(%s)", self.join_rec(", ", expr.children, *args)) + def map_inverse(self, expr, *args): + return self.format("Inverse(%s)", + self.rec(expr.expression, *args)) + + def map_det(self, expr, *args): + return self.format("Det(%s)", + self.rec(expr.expression, *args)) + + +class Array(p.Variable): + """A pymbolic variable of known extent.""" + def __init__(self, name, shape): + super(Array, self).__init__(name) + + self.shape = shape + class Recipe(p.Expression): """AST snippets and data corresponding to some form of finite element @@ -257,6 +275,38 @@ def stringifier(self): return _StringifyMapper +class Inverse(p.Expression): + """The inverse of a matrix-valued expression. Where the expression is + not square, this is the Moore-Penrose pseudo-inverse. + + Where the expression is evaluated at a number of points, the + inverse will be evaluated pointwise. + """ + def __init__(self, expression): + self.expression = expression + + mapper_method = "map_inverse" + + def stringifier(self): + return _StringifyMapper + + +class Det(p.Expression): + """The determinant of a matrix-valued expression. + + Where the expression is evaluated at a number of points, the + inverse will be evaluated pointwise. + """ + def __init__(self, expression): + + self.expression = expression + + mapper_method = "map_det" + + def stringifier(self): + return _StringifyMapper + + class FInATSyntaxError(Exception): """Exception raised when the syntax rules of the FInAT ast are violated.""" pass diff --git a/finat/utils.py b/finat/utils.py index 28778ea44..275f1e270 100644 --- a/finat/utils.py +++ b/finat/utils.py @@ -1,13 +1,18 @@ import pymbolic.primitives as p import inspect import FIAT +from derivatives import grad +from ast import Let, Array, Inverse, Det class KernelData(object): - def __init__(self, coordinate_element, affine=None): + def __init__(self, coordinate_element, coordinate_var=None, affine=None): """ :param coordinate_element: the (vector-valued) finite element for the coordinate field. + :param coorfinate_var: the symbolic variable for the + coordinate values. If no pullbacks are present in the kernel, + this may be omitted. :param affine: Specifies whether the pullback is affine (and therefore whether the Jacobian must be evaluated at every quadrature point). If not specified, this is inferred from coordinate_element. @@ -49,14 +54,31 @@ def tabulation_variable_name(self, element, points): return self._variable_cache[key] def J(self, points): + '''The Jacobian of the coordinate transformation. + .. math:: + + J_{\gamma,\tau} = \frac{\partial x_\gamma}{\partial X_\tau} + + Where :math:`x` is the physical coordinate and :math:`X` is the + local coordinate. + ''' try: return self.geometry["J"] except KeyError: - self.geometry["J"] = p.Variable("J") + self.geometry["J"] = Array("J", (self.gdim, self.tdim)) return self.geometry["J"] def invJ(self, points): + '''The Moore-Penrose pseudo-inverse of the coordinate transformation. + + .. math:: + + J^{-1}_{\tau,\gamma} = \frac{\partial X_\tau}{\partial x_\gamma} + + Where :math:`x` is the physical coordinate and :math:`X` is the + local coordinate. + ''' # ensure J exists self.J(points) @@ -64,10 +86,11 @@ def invJ(self, points): try: return self.geometry["invJ"] except KeyError: - self.geometry["invJ"] = p.Variable("invJ") + self.geometry["invJ"] = Array("invJ", (self.tdim, self.gdim)) return self.geometry["invJ"] def detJ(self, points): + '''The determinant of the coordinate transformation.''' # ensure J exists self.J @@ -78,6 +101,29 @@ def detJ(self, points): self.geometry["detJ"] = p.Variable("detJ") return self.geometry["detJ"] + def bind_geometry(self, affine, expression, points): + """If self.affine != affine, return expression. Else return a Let + statement defining the geometry.""" + + if self.affine != affine or len(self.geometry) == 0: + return expression + + g = self.geometry + + inner_lets = [] + if "invJ" in g: + inner_lets += (g["invJ"], Inverse(g["J"])) + if "detJ" in g: + inner_lets += (g["detJ"], Det(g["J"])) + + J_expr = self.coordinate_element.evaluate_field( + self.coordinate_var, points, self, derivative=grad, pullback=False) + J_expr = J_expr.replace_indices(zip(J_expr.indices[-1], expression.indices[-1])) + + return Let((g["J"], J_expr), + Let(inner_lets, expression) if inner_lets else expression) + + # Tuple of simplex cells. This relies on the fact that FIAT only # defines simplex elements. _simplex = tuple(e for e in FIAT.reference_element.__dict__.values() From 6a5fc684eae967d23fb41769c0188c78c367e461 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Fri, 5 Sep 2014 17:19:00 +0100 Subject: [PATCH 056/749] random junk --- finat/finiteelementbase.py | 8 +++++++- finat/hdiv.py | 4 ++-- finat/utils.py | 15 ++++++++++----- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index aa916b5a5..43165db41 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -192,7 +192,13 @@ def moment_evaluation(self, value, weights, points, if pullback: # Note. Do detJ cancellation here. - expr = IndexSum(d + p, psi * phi * w[p] * kernel_data.detJ(points)) + expr = psi * phi * w[p] * kernel_data.detJ(points) + + if kernel_data.affine: + expr = kernel_data.bind_geometry( + IndexSum(d + p, expr), points) + else: + expr = IndexSum(p, kernel_data.bind_geometry(IndexSum(d, expr))) else: expr = IndexSum(d + p, psi * phi * w[p]) diff --git a/finat/hdiv.py b/finat/hdiv.py index 42497c8f0..3ae0b83d2 100644 --- a/finat/hdiv.py +++ b/finat/hdiv.py @@ -37,9 +37,9 @@ def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): ind = ((alpha,), (i,), (q,)) elif derivative is div: if pullback: - expr = IndexSum((alpha,), phi[(alpha, alpha, i, q)] / detJ()) + expr = IndexSum((alpha,), phi[alpha, alpha, i, q] / detJ()) else: - expr = IndexSum((alpha,), phi[(alpha, alpha, i, q)]) + expr = IndexSum((alpha,), phi[alpha, alpha, i, q]) ind = ((), (i,), (q,)) elif derivative is grad: if pullback: diff --git a/finat/utils.py b/finat/utils.py index 275f1e270..d5d480078 100644 --- a/finat/utils.py +++ b/finat/utils.py @@ -101,15 +101,18 @@ def detJ(self, points): self.geometry["detJ"] = p.Variable("detJ") return self.geometry["detJ"] - def bind_geometry(self, affine, expression, points): - """If self.affine != affine, return expression. Else return a Let - statement defining the geometry.""" + def bind_geometry(self, expression, points=None): + """Let statement defining the geometry for expression. If no geometry + is required, return expression.""" - if self.affine != affine or len(self.geometry) == 0: + if len(self.geometry) == 0: return expression g = self.geometry + if points is None: + points = self._origin + inner_lets = [] if "invJ" in g: inner_lets += (g["invJ"], Inverse(g["J"])) @@ -118,7 +121,9 @@ def bind_geometry(self, affine, expression, points): J_expr = self.coordinate_element.evaluate_field( self.coordinate_var, points, self, derivative=grad, pullback=False) - J_expr = J_expr.replace_indices(zip(J_expr.indices[-1], expression.indices[-1])) + if points: + J_expr = J_expr.replace_indices( + zip(J_expr.indices[-1], expression.indices[-1])) return Let((g["J"], J_expr), Let(inner_lets, expression) if inner_lets else expression) From 2a6b96e5db23901fac000c64fa4fb171da7160b4 Mon Sep 17 00:00:00 2001 From: David Ham Date: Sun, 28 Sep 2014 13:43:55 +0100 Subject: [PATCH 057/749] remove bind_geometry from finiteelementbase --- finat/ast.py | 10 ++++++++-- finat/finiteelementbase.py | 15 ++++----------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index 6aa39d66f..e9ba7f0df 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -3,8 +3,8 @@ """ import pymbolic.primitives as p from pymbolic.mapper import IdentityMapper -from pymbolic.mapper.stringifier import StringifyMapper, PREC_NONE, PREC_CALL -from indices import IndexBase, DimensionIndex, BasisFunctionIndex, PointIndex +from pymbolic.mapper.stringifier import StringifyMapper, PREC_NONE +from indices import IndexBase class _IndexMapper(IdentityMapper): @@ -144,6 +144,12 @@ def __init__(self, indices, body): else: indices = (indices,) + print type(body) + # Perform trivial simplification of repeated indexsum. + if isinstance(body, IndexSum): + indices += body.children[0] + body = body.children[1] + self.children = (indices, body) def __getinitargs__(self): diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 43165db41..7c4a46814 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -190,16 +190,9 @@ def moment_evaluation(self, value, weights, points, w = weights.kernel_variable("w", kernel_data) + expr = psi * phi * w[p] + if pullback: - # Note. Do detJ cancellation here. - expr = psi * phi * w[p] * kernel_data.detJ(points) - - if kernel_data.affine: - expr = kernel_data.bind_geometry( - IndexSum(d + p, expr), points) - else: - expr = IndexSum(p, kernel_data.bind_geometry(IndexSum(d, expr))) - else: - expr = IndexSum(d + p, psi * phi * w[p]) + expr *= kernel_data.detJ(points) - return Recipe(((), b + b_, ()), expr) + return Recipe(((), b + b_, ()), IndexSum(p, expr)) From 60e226de8e9721b40b1e446fce773dcc7be61a08 Mon Sep 17 00:00:00 2001 From: David Ham Date: Sun, 28 Sep 2014 15:00:43 +0100 Subject: [PATCH 058/749] connect PointIndex to PointSet --- finat/ast.py | 1 - finat/hcurl.py | 2 +- finat/hdiv.py | 2 +- finat/indices.py | 6 ++++-- finat/lagrange.py | 2 +- finat/points.py | 6 +++++- finat/utils.py | 2 +- 7 files changed, 13 insertions(+), 8 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index e9ba7f0df..ec0bb58b1 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -144,7 +144,6 @@ def __init__(self, indices, body): else: indices = (indices,) - print type(body) # Perform trivial simplification of repeated indexsum. if isinstance(body, IndexSum): indices += body.children[0] diff --git a/finat/hcurl.py b/finat/hcurl.py index a43d7024b..2ad82e1c1 100644 --- a/finat/hcurl.py +++ b/finat/hcurl.py @@ -14,7 +14,7 @@ def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): phi = self._tabulated_basis(points, kernel_data, derivative) i = indices.BasisFunctionIndex(self.fiat_element.space_dimension()) - q = indices.PointIndex(points.points.shape[0]) + q = indices.PointIndex(points) tIndex = lambda: indices.DimensionIndex(kernel_data.tdim) gIndex = lambda: indices.DimensionIndex(kernel_data.gdim) diff --git a/finat/hdiv.py b/finat/hdiv.py index 3ae0b83d2..67abc83b7 100644 --- a/finat/hdiv.py +++ b/finat/hdiv.py @@ -14,7 +14,7 @@ def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): phi = self._tabulated_basis(points, kernel_data, derivative) i = indices.BasisFunctionIndex(self.fiat_element.space_dimension()) - q = indices.PointIndex(points.points.shape[0]) + q = indices.PointIndex(points) tIndex = lambda: indices.DimensionIndex(kernel_data.tdim) gIndex = lambda: indices.DimensionIndex(kernel_data.gdim) diff --git a/finat/indices.py b/finat/indices.py index 4eee1fc0b..36144d35a 100644 --- a/finat/indices.py +++ b/finat/indices.py @@ -38,12 +38,14 @@ def get_mapper_method(self, mapper): class PointIndex(IndexBase): '''An index running over a set of points, for example quadrature points.''' - def __init__(self, extent): + def __init__(self, pointset): + + self.pointset = pointset name = 'q_' + str(PointIndex._count) PointIndex._count += 1 - super(PointIndex, self).__init__(extent, name) + super(PointIndex, self).__init__(pointset.extent, name) _count = 0 diff --git a/finat/lagrange.py b/finat/lagrange.py index e9c81a400..07b0d9f33 100644 --- a/finat/lagrange.py +++ b/finat/lagrange.py @@ -24,7 +24,7 @@ def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): phi = self._tabulated_basis(points, kernel_data, derivative) i = indices.BasisFunctionIndex(self._fiat_element.space_dimension()) - q = indices.PointIndex(points.points.shape[0]) + q = indices.PointIndex(points) if derivative is grad: alpha = indices.DimensionIndex(kernel_data.tdim) diff --git a/finat/points.py b/finat/points.py index 3044352de..f84b8da3c 100644 --- a/finat/points.py +++ b/finat/points.py @@ -25,9 +25,13 @@ class PointSet(PointSetBase): def __init__(self, points): self._points = numpy.array(points) + self.extent = slice(self._points.shape[0]) + """A slice which describes how to iterate over this + :class:`PointSet`""" + @property def points(self): - """Return a flattened numpy array of points. + """A flattened numpy array of points. The array has shape (num_points, topological_dim). """ diff --git a/finat/utils.py b/finat/utils.py index d5d480078..487a8fc3c 100644 --- a/finat/utils.py +++ b/finat/utils.py @@ -23,7 +23,7 @@ def __init__(self, coordinate_element, coordinate_var=None, affine=None): self.affine = coordinate_element.degree <= 1 \ and isinstance(coordinate_element.cell, _simplex) else: - self.affine = True + self.affine = affine self.static = {} self.params = {} From 3edceaf416aa148b6609503244798d7aa68f53fc Mon Sep 17 00:00:00 2001 From: David Ham Date: Mon, 29 Sep 2014 09:11:59 +0100 Subject: [PATCH 059/749] Pass a PointIndex instead of a PointSet to lots of places --- finat/ast.py | 3 +++ finat/finiteelementbase.py | 38 +++++++++++++++++++++++------------- finat/hcurl.py | 11 +++++------ finat/hdiv.py | 23 +++++++++++----------- finat/indices.py | 2 +- finat/lagrange.py | 15 +++++++------- finat/utils.py | 19 ++++++++---------- finat/vectorfiniteelement.py | 21 +++++++++++--------- 8 files changed, 71 insertions(+), 61 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index ec0bb58b1..03a96887a 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -151,6 +151,9 @@ def __init__(self, indices, body): self.children = (indices, body) + self.indices = self.children[0] + self.body = self.children[1] + def __getinitargs__(self): return self.children diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 7c4a46814..320382486 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -53,23 +53,31 @@ def facet_support_dofs(self): raise NotImplementedError - def field_evaluation(self, points, static_data, derivative=None): + def field_evaluation(self, field_var, q, kernel_data, derivative=None): '''Return code for evaluating a known field at known points on the reference element. - Derivative is an atomic derivative operation. - - Points is some description of the points to evaluate at + :param field_var: the coefficient of the field at each basis function + :param q: a :class:`.PointIndex` corresponding to the points + at which to evaluate. + :param kernel_data: the :class:`.KernelData` object corresponding + to the current kernel. + :param derivative: the derivative to take of the test function. ''' raise NotImplementedError - def basis_evaluation(self, points, static_data, derivative=None): + def basis_evaluation(self, q, kernel_data, derivative=None): '''Return code for evaluating a known field at known points on the reference element. - Points is some description of the points to evaluate at + :param field_var: the coefficient of the field at each basis function + :param q: a :class:`.PointIndex` corresponding to the points + at which to evaluate. + :param kernel_data: the :class:`.KernelData` object corresponding + to the current kernel. + :param derivative: the derivative to take of the test function. ''' @@ -81,7 +89,7 @@ def pullback(self, derivative): raise NotImplementedError - def moment_evaluation(self, value, weights, points, static_data, derivative=None): + def moment_evaluation(self, value, weights, q, kernel_data, derivative=None): '''Return code for evaluating: .. math:: @@ -93,13 +101,15 @@ def moment_evaluation(self, value, weights, points, static_data, derivative=None :param value: an expression. The free indices in value must match those in v. :param weights: a point set of quadrature weights. - :param static_data: the :class:`.KernelData` object corresponding to the current kernel. + :param q: a :class:`.PointIndex` corresponding to the points + at which to evaluate. + :param kernel_data: the :class:`.KernelData` object corresponding to the current kernel. :param derivative: the derivative to take of the test function. ''' raise NotImplementedError - def dual_evaluation(self, static_data): + def dual_evaluation(self, kernel_data): '''Return code for evaluating an expression at the dual set. Note: what does the expression need to look like? @@ -167,10 +177,10 @@ def _tabulated_basis(self, points, kernel_data, derivative): return phi - def field_evaluation(self, field_var, points, + def field_evaluation(self, field_var, q, kernel_data, derivative=None, pullback=True): - basis = self.basis_evaluation(points, kernel_data, derivative, pullback) + basis = self.basis_evaluation(q, kernel_data, derivative, pullback) (d, b, p) = basis.indices phi = basis.expression @@ -178,10 +188,10 @@ def field_evaluation(self, field_var, points, return Recipe((d, (), p), expr) - def moment_evaluation(self, value, weights, points, + def moment_evaluation(self, value, weights, q, kernel_data, derivative=None, pullback=True): - basis = self.basis_evaluation(points, kernel_data, derivative, pullback) + basis = self.basis_evaluation(q, kernel_data, derivative, pullback) (d, b, p) = basis.indices phi = basis.expression @@ -193,6 +203,6 @@ def moment_evaluation(self, value, weights, points, expr = psi * phi * w[p] if pullback: - expr *= kernel_data.detJ(points) + expr *= kernel_data.detJ return Recipe(((), b + b_, ()), IndexSum(p, expr)) diff --git a/finat/hcurl.py b/finat/hcurl.py index 2ad82e1c1..47d5d43ed 100644 --- a/finat/hcurl.py +++ b/finat/hcurl.py @@ -9,12 +9,11 @@ class HCurlElement(FiatElementBase): def __init__(self, cell, degree): super(HCurlElement, self).__init__(cell, degree) - def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): + def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): - phi = self._tabulated_basis(points, kernel_data, derivative) + phi = self._tabulated_basis(q.points, kernel_data, derivative) i = indices.BasisFunctionIndex(self.fiat_element.space_dimension()) - q = indices.PointIndex(points) tIndex = lambda: indices.DimensionIndex(kernel_data.tdim) gIndex = lambda: indices.DimensionIndex(kernel_data.gdim) @@ -22,8 +21,8 @@ def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): alpha = tIndex() # The lambda functions here prevent spurious instantiations of invJ and detJ - invJ = lambda: kernel_data.invJ(points) - detJ = lambda: kernel_data.detJ(points) + invJ = lambda: kernel_data.invJ + detJ = lambda: kernel_data.detJ if derivative is None: if pullback: @@ -47,7 +46,7 @@ def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): beta = tIndex() gamma = gIndex() delta = gIndex() - expr = IndexSum((alpha, beta), invJ()[alpha, gamma] * invJ(beta, delta) + expr = IndexSum((alpha, beta), invJ()[alpha, gamma] * invJ()[beta, delta] * phi[(alpha, beta, i, q)]) ind = ((gamma, delta), (i,), (q,)) else: diff --git a/finat/hdiv.py b/finat/hdiv.py index 67abc83b7..a3ca1fced 100644 --- a/finat/hdiv.py +++ b/finat/hdiv.py @@ -9,12 +9,11 @@ class HDivElement(FiatElementBase): def __init__(self, cell, degree): super(HDivElement, self).__init__(cell, degree) - def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): + def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): - phi = self._tabulated_basis(points, kernel_data, derivative) + phi = self._tabulated_basis(q.points, kernel_data, derivative) i = indices.BasisFunctionIndex(self.fiat_element.space_dimension()) - q = indices.PointIndex(points) tIndex = lambda: indices.DimensionIndex(kernel_data.tdim) gIndex = lambda: indices.DimensionIndex(kernel_data.gdim) @@ -22,18 +21,18 @@ def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): alpha = tIndex() # The lambda functions here prevent spurious instantiations of invJ and detJ - J = lambda: kernel_data.J(points) - invJ = lambda: kernel_data.invJ(points) - detJ = lambda: kernel_data.detJ(points) + J = lambda: kernel_data.J + invJ = lambda: kernel_data.invJ + detJ = lambda: kernel_data.detJ if derivative is None: if pullback: beta = alpha alpha = gIndex() - expr = IndexSum((beta,), J()[alpha, beta] * phi[(beta, i, q)] + expr = IndexSum((beta,), J()[alpha, beta] * phi[beta, i, q] / detJ()) else: - expr = phi[(alpha, i, q)] + expr = phi[alpha, i, q] ind = ((alpha,), (i,), (q,)) elif derivative is div: if pullback: @@ -47,11 +46,11 @@ def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): gamma = gIndex() delta = gIndex() expr = IndexSum((alpha, beta), J()[gamma, alpha] * invJ()[beta, delta] - * phi[(alpha, beta, i, q)]) / detJ() + * phi[alpha, beta, i, q]) / detJ() ind = ((gamma, delta), (i,), (q,)) else: beta = indices.DimensionIndex(kernel_data.tdim) - expr = phi[(alpha, beta, i, q)] + expr = phi[alpha, beta, i, q] ind = ((alpha, beta), (i,), (q,)) elif derivative is curl: beta = indices.DimensionIndex(kernel_data.tdim) @@ -62,11 +61,11 @@ def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): zeta = gIndex() expr = LeviCivita((zeta,), (gamma, delta), IndexSum((alpha, beta), J()[gamma, alpha] * invJ()[beta, delta] - * phi[(alpha, beta, i, q)])) / detJ() + * phi[alpha, beta, i, q])) / detJ() else: d = kernel_data.tdim zeta = tIndex() - expr = LeviCivita((zeta,), (alpha, beta), phi[(alpha, beta, i, q)]) + expr = LeviCivita((zeta,), (alpha, beta), phi[alpha, beta, i, q]) if d == 2: expr = expr.replace_indices((zeta, 2)) ind = ((), (i,), (q,)) diff --git a/finat/indices.py b/finat/indices.py index 36144d35a..b600f6124 100644 --- a/finat/indices.py +++ b/finat/indices.py @@ -40,7 +40,7 @@ class PointIndex(IndexBase): '''An index running over a set of points, for example quadrature points.''' def __init__(self, pointset): - self.pointset = pointset + self.points = pointset name = 'q_' + str(PointIndex._count) PointIndex._count += 1 diff --git a/finat/lagrange.py b/finat/lagrange.py index 07b0d9f33..8bab6ffa4 100644 --- a/finat/lagrange.py +++ b/finat/lagrange.py @@ -10,7 +10,7 @@ class ScalarElement(FiatElementBase): def __init__(self, cell, degree): super(ScalarElement, self).__init__(cell, degree) - def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): + def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): '''Produce the variable for the tabulation of the basis functions or their derivative. Also return the relevant indices. @@ -21,17 +21,16 @@ def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): raise ValueError( "Scalar elements do not have a %s operation") % derivative - phi = self._tabulated_basis(points, kernel_data, derivative) + phi = self._tabulated_basis(q.points, kernel_data, derivative) i = indices.BasisFunctionIndex(self._fiat_element.space_dimension()) - q = indices.PointIndex(points) if derivative is grad: alpha = indices.DimensionIndex(kernel_data.tdim) if pullback: beta = alpha alpha = indices.DimensionIndex(kernel_data.gdim) - invJ = kernel_data.invJ(points)[(beta, alpha)] + invJ = kernel_data.invJ[(beta, alpha)] expr = IndexSum((beta,), invJ * phi[(beta, i, q)]) else: expr = phi[(alpha, i, q)] @@ -42,23 +41,23 @@ def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): return Recipe(indices=ind, expression=expr) - def field_evaluation(self, field_var, points, + def field_evaluation(self, field_var, q, kernel_data, derivative=None, pullback=True): if derivative not in (None, grad): raise ValueError( "Scalar elements do not have a %s operation") % derivative return super(ScalarElement, self).field_evaluation( - field_var, points, kernel_data, derivative, pullback) + field_var, q, kernel_data, derivative, pullback) - def moment_evaluation(self, value, weights, points, + def moment_evaluation(self, value, weights, q, kernel_data, derivative=None, pullback=True): if derivative not in (None, grad): raise ValueError( "Scalar elements do not have a %s operation") % derivative return super(ScalarElement, self).moment_evaluation( - value, weights, points, kernel_data, derivative, pullback) + value, weights, q, kernel_data, derivative, pullback) def pullback(self, phi, kernel_data, derivative=None): diff --git a/finat/utils.py b/finat/utils.py index 487a8fc3c..c56ac287d 100644 --- a/finat/utils.py +++ b/finat/utils.py @@ -10,7 +10,7 @@ def __init__(self, coordinate_element, coordinate_var=None, affine=None): """ :param coordinate_element: the (vector-valued) finite element for the coordinate field. - :param coorfinate_var: the symbolic variable for the + :param coordinate_var: the symbolic variable for the coordinate values. If no pullbacks are present in the kernel, this may be omitted. :param affine: Specifies whether the pullback is affine (and therefore @@ -19,6 +19,7 @@ def __init__(self, coordinate_element, coordinate_var=None, affine=None): """ self.coordinate_element = coordinate_element + self.coordinate_var = coordinate_var if affine is None: self.affine = coordinate_element.degree <= 1 \ and isinstance(coordinate_element.cell, _simplex) @@ -31,7 +32,6 @@ def __init__(self, coordinate_element, coordinate_var=None, affine=None): #: The geometric dimension of the physical space. self.gdim = coordinate_element._dimension - #: The topological dimension of the reference element self.tdim = coordinate_element._cell.get_spatial_dimension() @@ -53,7 +53,8 @@ def tabulation_variable_name(self, element, points): self._variable_count += 1 return self._variable_cache[key] - def J(self, points): + @property + def J(self): '''The Jacobian of the coordinate transformation. .. math:: @@ -69,7 +70,8 @@ def J(self, points): self.geometry["J"] = Array("J", (self.gdim, self.tdim)) return self.geometry["J"] - def invJ(self, points): + @property + def invJ(self): '''The Moore-Penrose pseudo-inverse of the coordinate transformation. .. math:: @@ -80,21 +82,16 @@ def invJ(self, points): local coordinate. ''' - # ensure J exists - self.J(points) - try: return self.geometry["invJ"] except KeyError: self.geometry["invJ"] = Array("invJ", (self.tdim, self.gdim)) return self.geometry["invJ"] - def detJ(self, points): + @property + def detJ(self): '''The determinant of the coordinate transformation.''' - # ensure J exists - self.J - try: return self.geometry["detJ"] except KeyError: diff --git a/finat/vectorfiniteelement.py b/finat/vectorfiniteelement.py index 9728b0416..c0939feb2 100644 --- a/finat/vectorfiniteelement.py +++ b/finat/vectorfiniteelement.py @@ -38,7 +38,7 @@ def __init__(self, element, dimension): self._base_element = element - def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): + def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): r"""Produce the recipe for basis function evaluation at a set of points :math:`q`: @@ -57,8 +57,9 @@ def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): # Produce the base scalar recipe. The scalar basis can only # take a grad. For other derivatives, we need to do the # transform here. - sr = self._base_element.basis_evaluation(points, kernel_data, - derivative and grad, pullback) + sr = self._base_element.basis_evaluation(q, kernel_data, + derivative and grad, + pullback) phi = sr.expression d, b, p = sr.indices @@ -89,7 +90,7 @@ def basis_evaluation(self, points, kernel_data, derivative=None, pullback=True): return Recipe((alpha + d, b + beta, p), Delta(alpha + beta, phi)) - def field_evaluation(self, field_var, points, + def field_evaluation(self, field_var, q, kernel_data, derivative=None, pullback=True): r"""Produce the recipe for the evaluation of a field f at a set of points :math:`q`: @@ -108,8 +109,9 @@ def field_evaluation(self, field_var, points, # Produce the base scalar recipe. The scalar basis can only # take a grad. For other derivatives, we need to do the # transform here. - sr = self._base_element.basis_evaluation(points, kernel_data, - derivative and grad, pullback) + sr = self._base_element.basis_evaluation(q, kernel_data, + derivative and grad, + pullback) phi = sr.expression d, b, p = sr.indices @@ -145,7 +147,7 @@ def field_evaluation(self, field_var, points, return Recipe((alpha + d, (), p), expression) - def moment_evaluation(self, value, weights, points, + def moment_evaluation(self, value, weights, q, kernel_data, derivative=None, pullback=True): r"""Produce the recipe for the evaluation of the moment of :math:`u_{\alpha,q}` against a test function :math:`v_{\beta,q}`. @@ -173,8 +175,9 @@ def moment_evaluation(self, value, weights, points, # Produce the base scalar recipe. The scalar basis can only # take a grad. For other derivatives, we need to do the # transform here. - sr = self._base_element.basis_evaluation(points, kernel_data, - derivative and grad, pullback) + sr = self._base_element.basis_evaluation(q, kernel_data, + derivative and grad, + pullback) phi = sr.expression d, b, p = sr.indices From 3f2059d77427e7917b18c0b0316435b0c8eeca0d Mon Sep 17 00:00:00 2001 From: David Ham Date: Mon, 29 Sep 2014 12:30:04 +0100 Subject: [PATCH 060/749] Draft geometry mapper. Tree visitor which purports to put in the Jacobian terms at the right points in the expression tree. --- finat/geometry_mapper.py | 88 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 finat/geometry_mapper.py diff --git a/finat/geometry_mapper.py b/finat/geometry_mapper.py new file mode 100644 index 000000000..0c3d969e7 --- /dev/null +++ b/finat/geometry_mapper.py @@ -0,0 +1,88 @@ +from pymbolic.mapper import IdentityMapper +from indices import PointIndex +from points import PointSet +from ast import Let, Det, Inverse, Recipe +from derivatives import grad + + +class GeometryMapper(IdentityMapper): + """A mapper which identifies Jacobians, inverse Jacobians and their + determinants in expressions, and inserts the code to define them at + the correct point in the tree.""" + + def __init__(self, kernel_data): + """ + :arg context: a mapping from variable names to values + """ + super(GeometryMapper, self).__init__() + + self.local_geometry = set() + + self.kernel_data = kernel_data + + def __call__(self, expr, *args, **kwargs): + """ + In the affine case we need to do geometry insertion at the top + level. + """ + + if not isinstance(expr, Recipe): + raise TypeError("Can only map geometry on a Recipe") + + body = self.rec(expr.body) + + if self.kernel_data.affine and self.local_geometry: + # This is the bottom corner. We actually want the + # circumcentre. + q = PointIndex([[0] * self.kernel_data.tdim]) + + body = self._bind_geometry(q, body) + + elif self.local_geometry: + raise ValueError("Unbound local geometry in tree") + + # Reconstruct the recipe + return expr.__class__(self.rec(expr.indices), + body) + + def map_variable(self, var): + + if var.name in ("J", "invJ", "detJ"): + self.local_geometry.add(var) + + return self + + def map_index_sum(self, expr): + + body = self.rec(expr.body) + + if not self.kernel_data.affine \ + and self.local_geometry \ + and isinstance(self.indices[-1], PointIndex): + q = self.indices[-1] + + body = self._bind_geometry(q, body) + + # Reconstruct the index_sum + return expr.__class__(self.rec(expr.indices), + body) + + def _bind_geometry(self, q, body): + + kd = self.kernel_data + + # Note that this may not be safe for tensor product elements. + phi_x = kd.coordinate_var + element = kd.coordinate_element + J = element.field_evaluation(phi_x, q, kd, grad) + + inner_lets = (((kd.detJ, Det(kd.J)),) + if kd.detJ in self.local_geometry else () + + ((kd.invJ, Inverse(kd.J)),) + if kd.invJ in self.local_geometry else ()) + + # The local geometry goes out of scope at this point. + self.local_geometry = set() + + return Let(((kd.J, J)), + Let(inner_lets, body) if inner_lets else body) From 41547e7a95ae9aec00f4d8066af25b1cb43a551e Mon Sep 17 00:00:00 2001 From: David Ham Date: Tue, 30 Sep 2014 10:03:43 +0100 Subject: [PATCH 061/749] indented printing of expressions --- finat/__init__.py | 1 + finat/ast.py | 118 ++++++++++++++++++++++++----------- finat/finiteelementbase.py | 6 +- finat/geometry_mapper.py | 17 +++-- finat/lagrange.py | 2 +- finat/vectorfiniteelement.py | 16 ++--- 6 files changed, 104 insertions(+), 56 deletions(-) diff --git a/finat/__init__.py b/finat/__init__.py index b1cd329ed..8e8626d7f 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -5,3 +5,4 @@ from utils import KernelData from derivatives import div, grad, curl import interpreter +from geometry_mapper import GeometryMapper diff --git a/finat/ast.py b/finat/ast.py index 03a96887a..500399b4b 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -2,28 +2,21 @@ required to define Finite Element expressions in FInAT. """ import pymbolic.primitives as p -from pymbolic.mapper import IdentityMapper +from pymbolic.mapper import IdentityMapper as IM from pymbolic.mapper.stringifier import StringifyMapper, PREC_NONE from indices import IndexBase -class _IndexMapper(IdentityMapper): - def __init__(self, replacements): - super(_IndexMapper, self).__init__() - - self.replacements = replacements - - def map_index(self, expr, *args): - '''Replace indices if they are in the replacements list''' - - try: - return(self.replacements[expr]) - except KeyError: - return expr +class IdentityMapper(IM): + def __init__(self): + super(IdentityMapper, self).__init__() def map_recipe(self, expr, *args): return expr.__class__(self.rec(expr.indices, *args), - self.rec(expr.expression, *args)) + self.rec(expr.body, *args)) + + def map_index(self, expr, *args): + return expr def map_delta(self, expr, *args): return expr.__class__(*(self.rec(c, *args) for c in expr.children)) @@ -37,33 +30,77 @@ def map_delta(self, expr, *args): map_det = map_delta +class _IndexMapper(IdentityMapper): + def __init__(self, replacements): + super(_IndexMapper, self).__init__() + + self.replacements = replacements + + def map_index(self, expr, *args): + '''Replace indices if they are in the replacements list''' + + try: + return(self.replacements[expr]) + except KeyError: + return expr + + class _StringifyMapper(StringifyMapper): - def map_recipe(self, expr, enclosing_prec): - return self.format("Recipe(%s, %s)", - self.rec(expr.indices, PREC_NONE), - self.rec(expr.expression, PREC_NONE)) + def map_recipe(self, expr, enclosing_prec, indent=None, *args, **kwargs): + if indent is None: + fmt = "Recipe(%s, %s)" + else: + oldidt = " "*indent + indent += 4 + idt = " "*indent + fmt = "Recipe(%s,\n" + idt + "%s\n" + oldidt + ")" + + return self.format(fmt, + self.rec(expr.indices, PREC_NONE, indent=indent, *args, **kwargs), + self.rec(expr.body, PREC_NONE, indent=indent, *args, **kwargs)) + + def map_let(self, expr, enclosing_prec, indent=None, *args, **kwargs): + if indent is None: + fmt = "Let(%s, %s)" + else: + oldidt = " "*indent + indent += 4 + idt = " "*indent + fmt = "Let(\n" + idt + "%s,\n" + idt + "%s\n" + oldidt + ")" - def map_delta(self, expr, *args): + return self.format(fmt, + self.rec(expr.bindings, PREC_NONE, indent=None, *args, **kwargs), + self.rec(expr.body, PREC_NONE, indent=indent, *args, **kwargs)) + + def map_delta(self, expr, *args, **kwargs): return self.format("Delta(%s, %s)", - *[self.rec(c, *args) for c in expr.children]) + *[self.rec(c, *args, **kwargs) for c in expr.children]) - def map_index_sum(self, expr, *args): - return self.format("IndexSum((%s), %s)", - " ".join(self.rec(c, *args) + "," for c in expr.children[0]), - self.rec(expr.children[1], *args)) + def map_index_sum(self, expr, enclosing_prec, indent=None, *args, **kwargs): + if indent is None or enclosing_prec is not PREC_NONE: + fmt = "IndexSum((%s), %s) " + else: + oldidt = " "*indent + indent += 4 + idt = " "*indent + fmt = "IndexSum((%s),\n" + idt + "%s\n" + oldidt + ")" - def map_levi_civita(self, expr, *args): + return self.format(fmt, + " ".join(self.rec(c, PREC_NONE, *args, **kwargs) + "," for c in expr.children[0]), + self.rec(expr.children[1], PREC_NONE, indent=indent, *args, **kwargs)) + + def map_levi_civita(self, expr, *args, **kwargs): return self.format("LeviCivita(%s)", - self.join_rec(", ", expr.children, *args)) + self.join_rec(", ", expr.children, *args, **kwargs)) - def map_inverse(self, expr, *args): + def map_inverse(self, expr, *args, **kwargs): return self.format("Inverse(%s)", - self.rec(expr.expression, *args)) + self.rec(expr.expression, *args, **kwargs)) - def map_det(self, expr, *args): + def map_det(self, expr, *args, **kwargs): return self.format("Det(%s)", - self.rec(expr.expression, *args)) + self.rec(expr.expression, *args, **kwargs)) class Array(p.Variable): @@ -88,18 +125,18 @@ class Recipe(p.Expression): Any of the tuples may be empty. :param expression: The expression returned by this :class:`Recipe`. """ - def __init__(self, indices, expression): + def __init__(self, indices, body): try: assert len(indices) == 3 except: raise FInATSyntaxError("Indices must be a triple of tuples") self.indices = tuple(indices) - self.expression = expression + self.body = body mapper_method = "map_recipe" def __getinitargs__(self): - return self.indices, self.expression + return self.indices, self.body def __getitem__(self, index): @@ -118,6 +155,14 @@ def __getitem__(self, index): return self.replace_indices(replacements) + def __str__(self): + """Use the :meth:`stringifier` to return a human-readable + string representation of *self*. + """ + + from pymbolic.mapper.stringifier import PREC_NONE + return self.stringifier()()(self, PREC_NONE, indent=0) + def stringifier(self): return _StringifyMapper @@ -242,7 +287,10 @@ def __init__(self, bindings, body): except: raise FInATSyntaxError("Let bindings must be a tuple of pairs") - super(Wave, self).__init__((bindings, body)) + super(Let, self).__init__((bindings, body)) + + self.bindings, self.body = self.children + def __str__(self): return "Let(%s)" % self.children diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 320382486..4893777fb 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -182,7 +182,7 @@ def field_evaluation(self, field_var, q, basis = self.basis_evaluation(q, kernel_data, derivative, pullback) (d, b, p) = basis.indices - phi = basis.expression + phi = basis.body expr = IndexSum(b, field_var[b[0]] * phi) @@ -193,10 +193,10 @@ def moment_evaluation(self, value, weights, q, basis = self.basis_evaluation(q, kernel_data, derivative, pullback) (d, b, p) = basis.indices - phi = basis.expression + phi = basis.body (d_, b_, p_) = value.indices - psi = value.replace_indices(zip(d_ + p_, d + p)).expression + psi = value.replace_indices(zip(d_ + p_, d + p)).body w = weights.kernel_variable("w", kernel_data) diff --git a/finat/geometry_mapper.py b/finat/geometry_mapper.py index 0c3d969e7..afaa6947a 100644 --- a/finat/geometry_mapper.py +++ b/finat/geometry_mapper.py @@ -1,7 +1,6 @@ -from pymbolic.mapper import IdentityMapper -from indices import PointIndex from points import PointSet -from ast import Let, Det, Inverse, Recipe +from indices import PointIndex +from ast import Let, Det, Inverse, Recipe, IdentityMapper from derivatives import grad @@ -34,7 +33,7 @@ def __call__(self, expr, *args, **kwargs): if self.kernel_data.affine and self.local_geometry: # This is the bottom corner. We actually want the # circumcentre. - q = PointIndex([[0] * self.kernel_data.tdim]) + q = PointIndex(PointSet([[0] * self.kernel_data.tdim])) body = self._bind_geometry(q, body) @@ -50,7 +49,7 @@ def map_variable(self, var): if var.name in ("J", "invJ", "detJ"): self.local_geometry.add(var) - return self + return var def map_index_sum(self, expr): @@ -58,8 +57,8 @@ def map_index_sum(self, expr): if not self.kernel_data.affine \ and self.local_geometry \ - and isinstance(self.indices[-1], PointIndex): - q = self.indices[-1] + and isinstance(expr.indices[-1], PointIndex): + q = expr.indices[-1] body = self._bind_geometry(q, body) @@ -74,7 +73,7 @@ def _bind_geometry(self, q, body): # Note that this may not be safe for tensor product elements. phi_x = kd.coordinate_var element = kd.coordinate_element - J = element.field_evaluation(phi_x, q, kd, grad) + J = element.field_evaluation(phi_x, q, kd, grad, pullback=False) inner_lets = (((kd.detJ, Det(kd.J)),) if kd.detJ in self.local_geometry else () @@ -84,5 +83,5 @@ def _bind_geometry(self, q, body): # The local geometry goes out of scope at this point. self.local_geometry = set() - return Let(((kd.J, J)), + return Let(((kd.J, J),), Let(inner_lets, body) if inner_lets else body) diff --git a/finat/lagrange.py b/finat/lagrange.py index 8bab6ffa4..ab412e97b 100644 --- a/finat/lagrange.py +++ b/finat/lagrange.py @@ -39,7 +39,7 @@ def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): ind = ((), (i,), (q,)) expr = phi[(i, q)] - return Recipe(indices=ind, expression=expr) + return Recipe(indices=ind, body=expr) def field_evaluation(self, field_var, q, kernel_data, derivative=None, pullback=True): diff --git a/finat/vectorfiniteelement.py b/finat/vectorfiniteelement.py index c0939feb2..286bf8bad 100644 --- a/finat/vectorfiniteelement.py +++ b/finat/vectorfiniteelement.py @@ -60,7 +60,7 @@ def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): sr = self._base_element.basis_evaluation(q, kernel_data, derivative and grad, pullback) - phi = sr.expression + phi = sr.body d, b, p = sr.indices # Additional basis function index along the vector dimension. @@ -69,7 +69,7 @@ def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): if derivative is div: return Recipe((d[:-1], b + beta, p), - sr.replace_indices({d[-1]: beta[0]}).expression) + sr.replace_indices({d[-1]: beta[0]}).body) elif derivative is curl: if self.dimension == 2: @@ -112,7 +112,7 @@ def field_evaluation(self, field_var, q, sr = self._base_element.basis_evaluation(q, kernel_data, derivative and grad, pullback) - phi = sr.expression + phi = sr.body d, b, p = sr.indices if derivative is div: @@ -179,7 +179,7 @@ def moment_evaluation(self, value, weights, q, derivative and grad, pullback) - phi = sr.expression + phi = sr.body d, b, p = sr.indices beta = (indices.BasisFunctionIndex(self._dimension),) @@ -191,7 +191,7 @@ def moment_evaluation(self, value, weights, q, if derivative is div: beta = d[-1:] - psi = value.replace_indices(zip(d_ + p_, d[:-1] + p)).expression + psi = value.replace_indices(zip(d_ + p_, d[:-1] + p)).body expression = IndexSum(d[:-1] + p, psi * phi * w[p]) @@ -201,7 +201,7 @@ def moment_evaluation(self, value, weights, q, beta = (indices.BasisFunctionIndex(self._dimension),) gamma = d[-1:] - psi = value.replace_indices((d_ + p_, d[:-1] + p)).expression + psi = value.replace_indices((d_ + p_, d[:-1] + p)).body expression = IndexSum(p, psi * LeviCivita((2,) + beta, gamma, phi) * w[p]) elif self.dimension == 3: @@ -210,7 +210,7 @@ def moment_evaluation(self, value, weights, q, beta = (indices.BasisFunctionIndex(self._dimension),) gamma = d[-1:] - psi = value.replace_indices((d_[:-1] + p_, d[:-1] + p)).expression + psi = value.replace_indices((d_[:-1] + p_, d[:-1] + p)).body expression = IndexSum(alpha + p, psi * LeviCivita(alpha + beta, gamma, phi) * w[p]) else: @@ -218,7 +218,7 @@ def moment_evaluation(self, value, weights, q, else: beta = (indices.BasisFunctionIndex(self._dimension),) - psi = value.replace_indices(zip(d_ + p_, beta + d + p)).expression + psi = value.replace_indices(zip(d_ + p_, beta + d + p)).body expression = IndexSum(d + p, psi * phi * w[p]) From 1cfc70822f86bf15db98fbcd5a0c8a12c32cc9ef Mon Sep 17 00:00:00 2001 From: David Ham Date: Tue, 11 Nov 2014 10:07:00 +0000 Subject: [PATCH 062/749] typo in interpretter --- finat/interpreter.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/finat/interpreter.py b/finat/interpreter.py index afea000ce..2637c27e3 100644 --- a/finat/interpreter.py +++ b/finat/interpreter.py @@ -37,9 +37,8 @@ def map_recipe(self, expr): """Evaluate expr for all values of free indices""" d, b, p = expr.indices - body = expr.expression - return self.rec(ForAll(d + b + p, body)) + return self.rec(ForAll(d + b + p, expr.body)) def map_index_sum(self, expr): From 36008eb2a134929e2286f163bd910147df3760d9 Mon Sep 17 00:00:00 2001 From: David Ham Date: Mon, 17 Nov 2014 16:24:44 +0000 Subject: [PATCH 063/749] typo --- finat/finiteelementbase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 4893777fb..314d6f2c0 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -142,7 +142,7 @@ def entity_closure_dofs(self): '''Return the map of topological entities to degrees of freedom on the closure of those entities for the finite element.''' - return self._fiat_element.entity_dofs() + return self._fiat_element.entity_closure_dofs() @property def facet_support_dofs(self): From 37f7d7a153cf9e539be22f925573ef767195f4ef Mon Sep 17 00:00:00 2001 From: David Ham Date: Mon, 17 Nov 2014 18:37:00 +0000 Subject: [PATCH 064/749] beginnings of bernstein --- finat/bernstein.py | 17 +++++++++++++++++ finat/points.py | 14 ++++++++++++++ finat/quadrature.py | 7 +++++++ 3 files changed, 38 insertions(+) create mode 100644 finat/bernstein.py create mode 100644 finat/quadrature.py diff --git a/finat/bernstein.py b/finat/bernstein.py new file mode 100644 index 000000000..001dd99c6 --- /dev/null +++ b/finat/bernstein.py @@ -0,0 +1,17 @@ +from finiteelementbase import FiniteElementBase + + +class Bernstein(FiniteElementBase): + """Scalar-valued Bernstein element. Note: need to work out the + correct heirarchy for different Bernstein elements.""" + + def __init__(self, cell, degree): + super(Bernstein, self).__init__() + + self._cell = cell + self._degree = degree + + def field_evaluation(self, field_var, q, kernel_data, derivative=None): + + + diff --git a/finat/points.py b/finat/points.py index f84b8da3c..27c9cefe6 100644 --- a/finat/points.py +++ b/finat/points.py @@ -58,3 +58,17 @@ def __getitem__(self, i): return PointSet([self.points[i]]) else: return PointSet(self.points[i]) + + +class TensorPointSet(PointSetBase): + def __init__(self, factor_sets): + super(TensorPointSet, self).__init__() + + self.factor_sets = factor_sets + + +class StroudPointSet(): + """A set of points with the structure required for Stroud quadrature.""" + + def __init__(self, factor_sets): + super(TensorPointSet, self).__init__(factor_sets) diff --git a/finat/quadrature.py b/finat/quadrature.py new file mode 100644 index 000000000..f7dbb65c4 --- /dev/null +++ b/finat/quadrature.py @@ -0,0 +1,7 @@ +import numpy +import pymbolic.primitives as p + + +class QuadratureRule(object): + """Object representing + From db11c69a23b623160d9f7ee120f7f6b48af9d4c1 Mon Sep 17 00:00:00 2001 From: David Ham Date: Mon, 17 Nov 2014 21:11:01 +0000 Subject: [PATCH 065/749] Gauss-Jacobi implementation --- finat/gauss_jacobi.py | 111 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 finat/gauss_jacobi.py diff --git a/finat/gauss_jacobi.py b/finat/gauss_jacobi.py new file mode 100644 index 000000000..0ed8aeaa0 --- /dev/null +++ b/finat/gauss_jacobi.py @@ -0,0 +1,111 @@ +"""Basic tools for getting Gauss points and quadrature rules.""" +__copyright__ = "Copyright (C) 2014 Robert C. Kirby" +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +import math +from numpy.math import factorial + + +def compute_gauss_jacobi_points(a, b, m): + """Computes the m roots of P_{m}^{a,b} on [-1,1] by Newton's method. + The initial guesses are the Chebyshev points. Algorithm + implemented in Python from the pseudocode given by Karniadakis and + Sherwin""" + x = [] + eps = 1.e-8 + max_iter = 100 + for k in range(0, m): + r = -math.cos((2.0*k + 1.0) * math.pi / (2.0 * m)) + if k > 0: + r = 0.5 * (r + x[k-1]) + j = 0 + delta = 2 * eps + while j < max_iter: + s = 0 + for i in range(0, k): + s = s + 1.0 / (r - x[i]) + f = eval_jacobi(a, b, m, r) + fp = eval_jacobi_deriv(a, b, m, r) + delta = f / (fp - f * s) + + r = r - delta + if math.fabs(delta) < eps: + break + else: + j = j + 1 + + x.append(r) + return x + + +def gauss_jacobi_rule(a, b, m): + xs = compute_gauss_jacobi_points(a, b, m) + + a1 = math.pow(2, a+b+1) + a2 = math.gamma(a + m + 1) + a3 = math.gamma(b + m + 1) + a4 = math.gamma(a + b + m + 1) + a5 = factorial(m) + a6 = a1 * a2 * a3 / a4 / a5 + + ws = [a6 / (1.0 - x**2.0) / eval_jacobi_deriv(a, b, m, x)**2.0 + for x in xs] + + return xs, ws + + +def eval_jacobi(a, b, n, x): + """Evaluates the nth jacobi polynomial with weight parameters a,b at a + point x. Recurrence relations implemented from the pseudocode + given in Karniadakis and Sherwin, Appendix B""" + + if 0 == n: + return 1.0 + elif 1 == n: + return 0.5 * (a - b + (a + b + 2.0) * x) + else: # 2 <= n + apb = a + b + pn2 = 1.0 + pn1 = 0.5 * (a - b + (apb + 2.0) * x) + p = 0 + for k in range(2, n+1): + a1 = 2.0 * k * (k + apb) * (2.0 * k + apb - 2.0) + a2 = (2.0 * k + apb - 1.0) * (a * a - b * b) + a3 = (2.0 * k + apb - 2.0) \ + * (2.0 * k + apb - 1.0) \ + * (2.0 * k + apb) + a4 = 2.0 * (k + a - 1.0) * (k + b - 1.0) \ + * (2.0 * k + apb) + a2 = a2 / a1 + a3 = a3 / a1 + a4 = a4 / a1 + p = (a2 + a3 * x) * pn1 - a4 * pn2 + pn2 = pn1 + pn1 = p + return p + + +def eval_jacobi_deriv(a, b, n, x): + """Evaluates the first derivative of P_{n}^{a,b} at a point x.""" + if n == 0: + return 0.0 + else: + return 0.5 * (a + b + n + 1) * eval_jacobi(a+1, b+1, n-1, x) From b72268ef2421252aff66f4d5c0b8986bef77df97 Mon Sep 17 00:00:00 2001 From: David Ham Date: Mon, 17 Nov 2014 21:38:24 +0000 Subject: [PATCH 066/749] draft stroud quadrature --- finat/quadrature.py | 51 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/finat/quadrature.py b/finat/quadrature.py index f7dbb65c4..e87708474 100644 --- a/finat/quadrature.py +++ b/finat/quadrature.py @@ -1,7 +1,50 @@ -import numpy -import pymbolic.primitives as p +import numpy as np +from gauss_jacobi import gauss_jacobi_rule +from points import StroudPointSet, PointSet class QuadratureRule(object): - """Object representing - + """Object representing a quadrature rule as a set of points + and a set of weights. + + :param cell: The :class:`~FIAT.reference_element.ReferenceElement` + on which this quadrature rule is defined. + :param points: An instance of a subclass of :class:`points.PointSetBase` + giving the points for this quadrature rule. + :param weights: The quadrature weights. If ``points`` is a + :class:`points.TensorPointSet` then weights is an iterable whose + members are the sets of weights along the respective dimensions. + """ + + def __init__(self, cell, points, weights): + self.cell = cell + self.points = points + self.weights = weights + + +class StroudQuadrature(QuadratureRule): + def __init__(self, cell, degree): + """Stroud quadrature rule on simplices.""" + + sd = cell.get_spatial_dimension() + + if sd != len(cell.vertices) + 1: + raise ValueError("cell must be a simplex") + + points = np.zeros((sd, degree)) + weights = np.zeros((sd, degree)) + + for d in range(1, sd+1): + [x, w] = gauss_jacobi_rule(sd - d, 0, degree) + points[d-1, :] = 0.5 * (x + 1) + weights[d-1, :] = w + + scale = 0.5 + for d in range(1, sd+1): + weights[sd-d, :] *= scale + scale *= 0.5 + + super(StroudQuadrature, self).__init__( + cell, + StroudPointSet(map(PointSet, points)), + weights) From ba8b71517fc3ecc82def6aebd0b1f40001d18a2b Mon Sep 17 00:00:00 2001 From: Robert Kirby Date: Mon, 17 Nov 2014 16:39:42 -0600 Subject: [PATCH 067/749] fixed a few bugs, work on tensor product points & hierarchy --- finat/gauss_jacobi.py | 6 +++--- finat/points.py | 30 ++++++++++++++++++++++++++++-- finat/quadrature.py | 2 +- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/finat/gauss_jacobi.py b/finat/gauss_jacobi.py index 0ed8aeaa0..34efe19b7 100644 --- a/finat/gauss_jacobi.py +++ b/finat/gauss_jacobi.py @@ -21,8 +21,8 @@ """ import math -from numpy.math import factorial - +from math import factorial +import numpy def compute_gauss_jacobi_points(a, b, m): """Computes the m roots of P_{m}^{a,b} on [-1,1] by Newton's method. @@ -69,7 +69,7 @@ def gauss_jacobi_rule(a, b, m): ws = [a6 / (1.0 - x**2.0) / eval_jacobi_deriv(a, b, m, x)**2.0 for x in xs] - return xs, ws + return numpy.array(xs), numpy.array(ws) def eval_jacobi(a, b, n, x): diff --git a/finat/points.py b/finat/points.py index 27c9cefe6..5cbecffc7 100644 --- a/finat/points.py +++ b/finat/points.py @@ -66,9 +66,35 @@ def __init__(self, factor_sets): self.factor_sets = factor_sets + def points(self): + def helper(loi): + if len(loi) == 1: + return [[x] for x in loi[0]] + else: + return [[x]+y for x in loi[0] for y in helper(loi[1:])] + + return numpy.array(helper([fs.points.tolist() + for fs in self.factor_sets])) + + +class MappedMixin(object): + def __init__(self, *args): + super(MappedMixin, self).__init__(*args) + + def map_points(self): + raise NotImplementedError + + +class DuffyMappedMixin(MappedMixin): + def __init__(self, *args): + super(DuffyMappedMixin, self).__init__(*args) + + def map_points(self): + raise NotImplementedError + -class StroudPointSet(): +class StroudPointSet(TensorPointSet, DuffyMappedMixin): """A set of points with the structure required for Stroud quadrature.""" def __init__(self, factor_sets): - super(TensorPointSet, self).__init__(factor_sets) + super(StroudPointSet, self).__init__(factor_sets) diff --git a/finat/quadrature.py b/finat/quadrature.py index e87708474..32530647e 100644 --- a/finat/quadrature.py +++ b/finat/quadrature.py @@ -28,7 +28,7 @@ def __init__(self, cell, degree): sd = cell.get_spatial_dimension() - if sd != len(cell.vertices) + 1: + if sd + 1 != len(cell.vertices): raise ValueError("cell must be a simplex") points = np.zeros((sd, degree)) From 8c76ab8daf88428d59aa75a467f7438cbb2ba230 Mon Sep 17 00:00:00 2001 From: David Ham Date: Mon, 17 Nov 2014 22:40:02 +0000 Subject: [PATCH 068/749] Some more symbol stuff towards Bernstein --- finat/bernstein.py | 29 +++++++++++++++++++++++++++-- finat/utils.py | 16 ++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index 001dd99c6..293454048 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -1,5 +1,6 @@ from finiteelementbase import FiniteElementBase - +from points import StroudPointSet +from ast import ForAll, Recipe, Wave class Bernstein(FiniteElementBase): """Scalar-valued Bernstein element. Note: need to work out the @@ -11,7 +12,31 @@ def __init__(self, cell, degree): self._cell = cell self._degree = degree - def field_evaluation(self, field_var, q, kernel_data, derivative=None): + def _points_variable(self, points, kernel_data): + """Return the symbolic variables for the static data + corresponding to the :class:`PointSet` ``points``.""" + static_key = (id(points),) + if static_key in kernel_data.static: + xi = kernel_data.static[static_key][0] + else: + xi = p.Variable(kernel_data.point_variable_name(points)) + kernel_data.static[static_key] = (xi, lambda: points.points) + return xi + + def field_evaluation(self, field_var, q, kernel_data, derivative=None): + + if not isinstance(q.points, StroudPointSet): + raise ValueError("Only Stroud points may be employed with Bernstien polynomials") + # Get the symbolic names for the points. + xi = [self._points_variable(f, kernel_data) + for f in q.factors.points] + + qs = q.factors[0] + + # 1D first + ForAll(, + + ) diff --git a/finat/utils.py b/finat/utils.py index c56ac287d..d1145e2ab 100644 --- a/finat/utils.py +++ b/finat/utils.py @@ -36,6 +36,7 @@ def __init__(self, coordinate_element, coordinate_var=None, affine=None): self.tdim = coordinate_element._cell.get_spatial_dimension() self._variable_count = 0 + self._point_count = 0 self._variable_cache = {} def tabulation_variable_name(self, element, points): @@ -53,6 +54,21 @@ def tabulation_variable_name(self, element, points): self._variable_count += 1 return self._variable_cache[key] + def point_variable_name(self, points): + """Given a point set, return a variable name xi_n + where n is guaranteed to be unique to that set of + points.""" + + key = (id(points),) + + try: + return self._variable_cache[key] + except KeyError: + self._variable_cache[key] = u'\u03BE_'.encode("utf-8") \ + + str(self._point_count) + self._point_count += 1 + return self._variable_cache[key] + @property def J(self): '''The Jacobian of the coordinate transformation. From 3b3dc480ebd8608c3e1bb85dd0f8d8fd931ccd4d Mon Sep 17 00:00:00 2001 From: Robert Kirby Date: Tue, 18 Nov 2014 10:31:05 -0600 Subject: [PATCH 069/749] updates to wave --- finat/ast.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index 500399b4b..cd9da8099 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -263,8 +263,14 @@ def __str__(self): class Wave(p._MultiChildExpression): """A symbolic expression with loop-carried dependencies.""" - def __init__(self, var, index, base, expr): - pass + def __init__(self, var, index, base, update, body): + self.children = (var, index, base, update, body) + + def __getinitargs__(self): + return self.children + + def __str__(self): + return "Wave(%s, %s, %s, %s, %s)" % tuple(map(str, self.children)) class Let(p._MultiChildExpression): From 589bbc40132ac7b25e64fe3ce6b5ba032e7d75d2 Mon Sep 17 00:00:00 2001 From: David Ham Date: Tue, 18 Nov 2014 17:01:22 +0000 Subject: [PATCH 070/749] possible candidate 1D Bernstein --- finat/bernstein.py | 25 ++++++++++++++++++++----- finat/utils.py | 29 +++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index 293454048..fcfef887e 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -1,6 +1,8 @@ from finiteelementbase import FiniteElementBase from points import StroudPointSet -from ast import ForAll, Recipe, Wave +from ast import ForAll, Recipe, Wave, Let, IndexSum +import pymbolic as p +from index import BasisFunctionIndex class Bernstein(FiniteElementBase): """Scalar-valued Bernstein element. Note: need to work out the @@ -29,14 +31,27 @@ def field_evaluation(self, field_var, q, kernel_data, derivative=None): if not isinstance(q.points, StroudPointSet): raise ValueError("Only Stroud points may be employed with Bernstien polynomials") - + # Get the symbolic names for the points. xi = [self._points_variable(f, kernel_data) for f in q.factors.points] qs = q.factors[0] + r = kernel_data.new_variable("r") + w = kernel_data.new_variable("w") + alpha = BasisFunctionIndex(self.degree+1) + s = 1-xi[qs[0]] # 1D first - ForAll(, - - ) + expr = ForAll(qs[0], + Let((r, xi[qs[0]]/s), + IndexSum((alpha,), + Wave(w, + alpha, + s**self.degree, + w * r * (self.degree - alpha) / (alpha + 1.0), + w * field_var[alpha]) + ) + ) + ) + return Recipe(((), (), (q)), expr) diff --git a/finat/utils.py b/finat/utils.py index d1145e2ab..a1cb9fbb4 100644 --- a/finat/utils.py +++ b/finat/utils.py @@ -29,6 +29,7 @@ def __init__(self, coordinate_element, coordinate_var=None, affine=None): self.static = {} self.params = {} self.geometry = {} + self.variables = set() #: The geometric dimension of the physical space. self.gdim = coordinate_element._dimension @@ -49,9 +50,11 @@ def tabulation_variable_name(self, element, points): try: return self._variable_cache[key] except KeyError: - self._variable_cache[key] = u'\u03C6_'.encode("utf-8") \ - + str(self._variable_count) + name = u'\u03C6_'.encode("utf-8") \ + + str(self._variable_count) + self._variable_cache[key] = name self._variable_count += 1 + self.variables.add(name) return self._variable_cache[key] def point_variable_name(self, points): @@ -64,11 +67,29 @@ def point_variable_name(self, points): try: return self._variable_cache[key] except KeyError: - self._variable_cache[key] = u'\u03BE_'.encode("utf-8") \ - + str(self._point_count) + name = u'\u03BE_'.encode("utf-8") \ + + str(self._point_count) + self._variable_cache[key] = name self._point_count += 1 + self.variables.add(name) return self._variable_cache[key] + def new_variable(self, prefix=None): + """Create a variable guaranteed to be unique in the kernel context.""" + name = prefix or "tmp" + if name not in self.variables: + self.variables.add(name) + return name + + # Prefix was already in use, so append an index. + i = 0 + while True: + varname = "%s_%d" % (name, i) + if varname not in self.variables: + self.variables.add(varname) + return varname + i += 1 + @property def J(self): '''The Jacobian of the coordinate transformation. From c328786dca3c88448de8b6461a74947ec97bd199 Mon Sep 17 00:00:00 2001 From: Robert Kirby Date: Tue, 18 Nov 2014 11:16:58 -0600 Subject: [PATCH 071/749] Bug fix in 1d field evaluation --- finat/bernstein.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index fcfef887e..8ce97d452 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -30,28 +30,30 @@ def _points_variable(self, points, kernel_data): def field_evaluation(self, field_var, q, kernel_data, derivative=None): if not isinstance(q.points, StroudPointSet): - raise ValueError("Only Stroud points may be employed with Bernstien polynomials") + raise ValueError("Only Stroud points may be employed with Bernstein polynomials") + + if derivative is not None: + raise NotImplementedError # Get the symbolic names for the points. xi = [self._points_variable(f, kernel_data) for f in q.factors.points] - qs = q.factors[0] + qs = q.factors r = kernel_data.new_variable("r") w = kernel_data.new_variable("w") alpha = BasisFunctionIndex(self.degree+1) s = 1-xi[qs[0]] # 1D first - expr = ForAll(qs[0], - Let((r, xi[qs[0]]/s), - IndexSum((alpha,), - Wave(w, - alpha, - s**self.degree, - w * r * (self.degree - alpha) / (alpha + 1.0), - w * field_var[alpha]) - ) - ) - ) + expr = Let((r, xi[qs[0]]/s), + IndexSum((alpha,), + Wave(w, + alpha, + s**self.degree, + w * r * (self.degree - alpha) / (alpha + 1.0), + w * field_var[alpha]) + ) + ) + return Recipe(((), (), (q)), expr) From 32996789cda5becb3129dc573c8f6f5df6f3f023 Mon Sep 17 00:00:00 2001 From: David Ham Date: Tue, 18 Nov 2014 20:04:26 +0000 Subject: [PATCH 072/749] whitespace errors --- finat/ast.py | 11 +++++------ finat/bernstein.py | 1 + finat/gauss_jacobi.py | 13 +++++++------ finat/points.py | 2 +- finat/quadrature.py | 10 +++++----- 5 files changed, 19 insertions(+), 18 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index cd9da8099..3f1315723 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -51,9 +51,9 @@ def map_recipe(self, expr, enclosing_prec, indent=None, *args, **kwargs): if indent is None: fmt = "Recipe(%s, %s)" else: - oldidt = " "*indent + oldidt = " " * indent indent += 4 - idt = " "*indent + idt = " " * indent fmt = "Recipe(%s,\n" + idt + "%s\n" + oldidt + ")" return self.format(fmt, @@ -64,9 +64,9 @@ def map_let(self, expr, enclosing_prec, indent=None, *args, **kwargs): if indent is None: fmt = "Let(%s, %s)" else: - oldidt = " "*indent + oldidt = " " * indent indent += 4 - idt = " "*indent + idt = " " * indent fmt = "Let(\n" + idt + "%s,\n" + idt + "%s\n" + oldidt + ")" return self.format(fmt, @@ -294,9 +294,8 @@ def __init__(self, bindings, body): raise FInATSyntaxError("Let bindings must be a tuple of pairs") super(Let, self).__init__((bindings, body)) - - self.bindings, self.body = self.children + self.bindings, self.body = self.children def __str__(self): return "Let(%s)" % self.children diff --git a/finat/bernstein.py b/finat/bernstein.py index fcfef887e..dedb185b2 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -4,6 +4,7 @@ import pymbolic as p from index import BasisFunctionIndex + class Bernstein(FiniteElementBase): """Scalar-valued Bernstein element. Note: need to work out the correct heirarchy for different Bernstein elements.""" diff --git a/finat/gauss_jacobi.py b/finat/gauss_jacobi.py index 34efe19b7..3fb02a9b9 100644 --- a/finat/gauss_jacobi.py +++ b/finat/gauss_jacobi.py @@ -24,6 +24,7 @@ from math import factorial import numpy + def compute_gauss_jacobi_points(a, b, m): """Computes the m roots of P_{m}^{a,b} on [-1,1] by Newton's method. The initial guesses are the Chebyshev points. Algorithm @@ -33,9 +34,9 @@ def compute_gauss_jacobi_points(a, b, m): eps = 1.e-8 max_iter = 100 for k in range(0, m): - r = -math.cos((2.0*k + 1.0) * math.pi / (2.0 * m)) + r = -math.cos((2.0 * k + 1.0) * math.pi / (2.0 * m)) if k > 0: - r = 0.5 * (r + x[k-1]) + r = 0.5 * (r + x[k - 1]) j = 0 delta = 2 * eps while j < max_iter: @@ -59,14 +60,14 @@ def compute_gauss_jacobi_points(a, b, m): def gauss_jacobi_rule(a, b, m): xs = compute_gauss_jacobi_points(a, b, m) - a1 = math.pow(2, a+b+1) + a1 = math.pow(2, a + b + 1) a2 = math.gamma(a + m + 1) a3 = math.gamma(b + m + 1) a4 = math.gamma(a + b + m + 1) a5 = factorial(m) a6 = a1 * a2 * a3 / a4 / a5 - ws = [a6 / (1.0 - x**2.0) / eval_jacobi_deriv(a, b, m, x)**2.0 + ws = [a6 / (1.0 - x ** 2.0) / eval_jacobi_deriv(a, b, m, x) ** 2.0 for x in xs] return numpy.array(xs), numpy.array(ws) @@ -86,7 +87,7 @@ def eval_jacobi(a, b, n, x): pn2 = 1.0 pn1 = 0.5 * (a - b + (apb + 2.0) * x) p = 0 - for k in range(2, n+1): + for k in range(2, n + 1): a1 = 2.0 * k * (k + apb) * (2.0 * k + apb - 2.0) a2 = (2.0 * k + apb - 1.0) * (a * a - b * b) a3 = (2.0 * k + apb - 2.0) \ @@ -108,4 +109,4 @@ def eval_jacobi_deriv(a, b, n, x): if n == 0: return 0.0 else: - return 0.5 * (a + b + n + 1) * eval_jacobi(a+1, b+1, n-1, x) + return 0.5 * (a + b + n + 1) * eval_jacobi(a + 1, b + 1, n - 1, x) diff --git a/finat/points.py b/finat/points.py index 5cbecffc7..0a6d283c2 100644 --- a/finat/points.py +++ b/finat/points.py @@ -71,7 +71,7 @@ def helper(loi): if len(loi) == 1: return [[x] for x in loi[0]] else: - return [[x]+y for x in loi[0] for y in helper(loi[1:])] + return [[x] + y for x in loi[0] for y in helper(loi[1:])] return numpy.array(helper([fs.points.tolist() for fs in self.factor_sets])) diff --git a/finat/quadrature.py b/finat/quadrature.py index 32530647e..191f058b6 100644 --- a/finat/quadrature.py +++ b/finat/quadrature.py @@ -34,14 +34,14 @@ def __init__(self, cell, degree): points = np.zeros((sd, degree)) weights = np.zeros((sd, degree)) - for d in range(1, sd+1): + for d in range(1, sd + 1): [x, w] = gauss_jacobi_rule(sd - d, 0, degree) - points[d-1, :] = 0.5 * (x + 1) - weights[d-1, :] = w + points[d - 1, :] = 0.5 * (x + 1) + weights[d - 1, :] = w scale = 0.5 - for d in range(1, sd+1): - weights[sd-d, :] *= scale + for d in range(1, sd + 1): + weights[sd - d, :] *= scale scale *= 0.5 super(StroudQuadrature, self).__init__( From d707b4d0595a7d04e608dd9c1a99d6fc5335f688 Mon Sep 17 00:00:00 2001 From: David Ham Date: Tue, 18 Nov 2014 21:48:05 +0000 Subject: [PATCH 073/749] fix some test fails --- finat/ast.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index 3f1315723..d3464122a 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -81,9 +81,9 @@ def map_index_sum(self, expr, enclosing_prec, indent=None, *args, **kwargs): if indent is None or enclosing_prec is not PREC_NONE: fmt = "IndexSum((%s), %s) " else: - oldidt = " "*indent + oldidt = " " * indent indent += 4 - idt = " "*indent + idt = " " * indent fmt = "IndexSum((%s),\n" + idt + "%s\n" + oldidt + ")" return self.format(fmt, From 3a75d9e4ffe9879310799b6ab8fb9b8dd3e6c97f Mon Sep 17 00:00:00 2001 From: David Ham Date: Tue, 18 Nov 2014 21:49:27 +0000 Subject: [PATCH 074/749] pep8 --- finat/bernstein.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index 01ead63f8..949dff86a 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -43,15 +43,15 @@ def field_evaluation(self, field_var, q, kernel_data, derivative=None): qs = q.factors r = kernel_data.new_variable("r") w = kernel_data.new_variable("w") - alpha = BasisFunctionIndex(self.degree+1) - s = 1-xi[qs[0]] + alpha = BasisFunctionIndex(self.degree + 1) + s = 1 - xi[qs[0]] # 1D first - expr = Let((r, xi[qs[0]]/s), + expr = Let((r, xi[qs[0]] / s), IndexSum((alpha,), Wave(w, alpha, - s**self.degree, + s ** self.degree, w * r * (self.degree - alpha) / (alpha + 1.0), w * field_var[alpha]) ) From 1dd318fe10f4e21a3081360bbe796fa70b87f722 Mon Sep 17 00:00:00 2001 From: David Ham Date: Tue, 18 Nov 2014 21:56:25 +0000 Subject: [PATCH 075/749] Fix the Let in Bernstein --- finat/bernstein.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index 949dff86a..64d58ddeb 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -47,7 +47,7 @@ def field_evaluation(self, field_var, q, kernel_data, derivative=None): s = 1 - xi[qs[0]] # 1D first - expr = Let((r, xi[qs[0]] / s), + expr = Let(((r, xi[qs[0]] / s),), IndexSum((alpha,), Wave(w, alpha, From e6ee1bc063f925c1c008b6ea6d51c872c9bd8671 Mon Sep 17 00:00:00 2001 From: David Ham Date: Tue, 18 Nov 2014 22:24:41 +0000 Subject: [PATCH 076/749] first draft of mapper --- finat/ast.py | 2 ++ finat/interpreter.py | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/finat/ast.py b/finat/ast.py index d3464122a..c8c224ff9 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -272,6 +272,8 @@ def __getinitargs__(self): def __str__(self): return "Wave(%s, %s, %s, %s, %s)" % tuple(map(str, self.children)) + mapper_method = "map_wave" + class Let(p._MultiChildExpression): """A Let expression enables local variable bindings in an diff --git a/finat/interpreter.py b/finat/interpreter.py index 2637c27e3..a6ef4aa6b 100644 --- a/finat/interpreter.py +++ b/finat/interpreter.py @@ -23,6 +23,9 @@ def __init__(self, context={}): self.indices = {} + # Storage for wave variables while they are out of scope. + self.wave_vars = {} + def map_variable(self, expr): try: var = self.context[expr.name] @@ -90,6 +93,30 @@ def map_for_all(self, expr): return np.array(total) + def map_wave(self, expr): + + (var, index, base, update, body) = expr.children + + try: + self.context[var] = self.wave_vars[var] + self.context[var] = self.rec(update) + except KeyError: + # We're at the start of the loop over index. + assert self.rec(index) == index.extent.start + self.context[var] = self.rec(base) + + self.wave_vars[var] = self.context[var] + + # Execute the body. + result = self.rec(body) + + # Remove the wave variable from scope. + self.context.pop(var) + if self.rec(index) >= index.extent.stop - 1: + self.wave_vars.pop(var) + + return result + def map_levi_civita(self, expr): free, bound, body = expr.children From b67cdd298d3ec5deab4fbe6e652ffb905f17ebc9 Mon Sep 17 00:00:00 2001 From: Robert Kirby Date: Tue, 18 Nov 2014 16:36:19 -0600 Subject: [PATCH 077/749] 2d and 3d beginnings in bernstein --- finat/bernstein.py | 85 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 69 insertions(+), 16 deletions(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index 8ce97d452..bd9c0109a 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -2,7 +2,7 @@ from points import StroudPointSet from ast import ForAll, Recipe, Wave, Let, IndexSum import pymbolic as p -from index import BasisFunctionIndex +from index import BasisFunctionIndex, PointIndex class Bernstein(FiniteElementBase): """Scalar-valued Bernstein element. Note: need to work out the @@ -40,20 +40,73 @@ def field_evaluation(self, field_var, q, kernel_data, derivative=None): for f in q.factors.points] qs = q.factors - r = kernel_data.new_variable("r") - w = kernel_data.new_variable("w") - alpha = BasisFunctionIndex(self.degree+1) - s = 1-xi[qs[0]] # 1D first - expr = Let((r, xi[qs[0]]/s), - IndexSum((alpha,), - Wave(w, - alpha, - s**self.degree, - w * r * (self.degree - alpha) / (alpha + 1.0), - w * field_var[alpha]) - ) - ) - - return Recipe(((), (), (q)), expr) + if self.cell.get_spatial_dimension() == 1: + r = kernel_data.new_variable("r") + w = kernel_data.new_variable("w") + alpha = BasisFunctionIndex(self.degree+1) + s = 1-xi[0][qs[0]] + + expr = Let((r, xi[0][qs[0]]/s), + IndexSum((alpha,), + Wave(w, + alpha, + s**self.degree, + w * r * (self.degree - alpha) / (alpha + 1.0), + w * field_var[alpha]) + ) + ) + return Recipe(((), (), (q)), expr) + elif self.cell.get_spatial_dimension() == 2: + deg = self.degree + r = kernel_data.new_variable("r") + w = kernel_data.new_variable("w") + tmp = kernel_data.new_variable("tmp") + alpha1 = BasisFunctionIndex(deg+1) + alpha2 = BasisFunctionIndex(deg+1-alpha1) + q2 = PointIndex(q.points.factor_set[1]) + s = 1-xi[0][qs[0]] + tmp_expr = Let((r, xi[1][q2]/s), + IndexSum((alpha2,), + Wave(w, + alpha2, + s**(deg - alpha1), + w * r * (deg-alpha1-alpha2)/(1.0 + alpha2), + w * field_var[alpha1*(2*deg-alpha1+3)/2]) + ) + ) + expr = Let((tmp, tmp_expr), + Let((r, xi[0][qs[0]]), + IndexSum((alpha1,), + Wave(w, + alpha1, + s**deg, + w * r * (deg-alpha1)/(1. + alpha1), + w * tmp[alpha1, qs[1]] + ) + ) + ) + ) + return Recipe(((), (), (q)), expr) + elif self.cell.get_spatial_dimension() == 3: + deg = self.degree + r = kernel_data.new_variable("r") + w = kernel_data.new_variable("w") + tmp0 = kernel_data.new_variable("tmp0") + tmp1 = kernel_data.new_variable("tmp1") + alpha1 = BasisFunctionIndex(deg+1) + alpha2 = BasisFunctionIndex(deg+1-alpha1) + alpha3 = BasisFunctionIndex(deg+1-alpha1-alpha2) + q3 = PointIndex(q.points.factor_set[2]) + + tmp0_expr = Let((r, xi[2][q3]/s), + IndexSum((q3,), + Wave(w, + alpha3, + s**(deg-alpha1-alpha2), + w * r * (deg-alpha1-alpha2-alpha3)/(1.+alpha3), + w * field_var[] + + + From fb6e8d4c0f32a076316b4865a2bec80bdb4d0f3b Mon Sep 17 00:00:00 2001 From: David Ham Date: Tue, 18 Nov 2014 22:47:26 +0000 Subject: [PATCH 078/749] draft implementation of interpreter for Let --- finat/interpreter.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/finat/interpreter.py b/finat/interpreter.py index a6ef4aa6b..69b836005 100644 --- a/finat/interpreter.py +++ b/finat/interpreter.py @@ -117,6 +117,21 @@ def map_wave(self, expr): return result + def map_let(self, expr): + + for var, value in expr.bindings: + if var in self.context: + raise ValueError("Let variable %s was already in scope." + % var.name) + self.context[var] = self.rec(value) + + result = self.rec(expr.body) + + for var, value in expr.bindings: + self.context.pop(var) + + return result + def map_levi_civita(self, expr): free, bound, body = expr.children From 2b2c70beaa0c41d7d60206f1ecc28ae17fd8faba Mon Sep 17 00:00:00 2001 From: David Ham Date: Wed, 19 Nov 2014 15:56:16 +0000 Subject: [PATCH 079/749] Catch name binding errors --- finat/ast.py | 7 +++++++ finat/indices.py | 7 ++++++- finat/interpreter.py | 15 ++++++++++++--- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index c8c224ff9..66d8289b9 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -7,6 +7,10 @@ from indices import IndexBase +class FInATSyntaxError(Exception): + """Exception to raise when users break the rules of the FInAT ast.""" + + class IdentityMapper(IM): def __init__(self): super(IdentityMapper, self).__init__() @@ -77,6 +81,9 @@ def map_delta(self, expr, *args, **kwargs): return self.format("Delta(%s, %s)", *[self.rec(c, *args, **kwargs) for c in expr.children]) + def map_index(self, expr, *args, **kwargs): + return str(expr) + def map_index_sum(self, expr, enclosing_prec, indent=None, *args, **kwargs): if indent is None or enclosing_prec is not PREC_NONE: fmt = "IndexSum((%s), %s) " diff --git a/finat/indices.py b/finat/indices.py index b600f6124..05ceaa39d 100644 --- a/finat/indices.py +++ b/finat/indices.py @@ -26,7 +26,7 @@ def _str_extent(self): self._extent.stop, self._extent.step or 1) - mapper_method = intern("map_index") + mapper_method = "map_index" def get_mapper_method(self, mapper): @@ -35,6 +35,11 @@ def get_mapper_method(self, mapper): else: raise AttributeError() + def __repr__(self): + + return "%s(%s)" % (self.__class__.__name__, self.name) + + class PointIndex(IndexBase): '''An index running over a set of points, for example quadrature points.''' diff --git a/finat/interpreter.py b/finat/interpreter.py index 69b836005..6d8ef472f 100644 --- a/finat/interpreter.py +++ b/finat/interpreter.py @@ -2,7 +2,7 @@ performant, but rather to provide a test facility for FInAT code.""" import pymbolic.primitives as p from pymbolic.mapper.evaluator import FloatEvaluationMapper, UnknownVariableError -from ast import IndexSum, ForAll, LeviCivita +from ast import IndexSum, ForAll, LeviCivita, FInATSyntaxError import numpy as np import copy @@ -55,6 +55,9 @@ def map_index_sum(self, expr): idx = indices[0] + if idx in self.indices: + raise FInATSyntaxError("Attempting to bind the name %s which is already bound" % idx) + e = idx.extent total = 0.0 @@ -82,6 +85,9 @@ def map_for_all(self, expr): idx = indices[0] + if idx in self.indices: + raise FInATSyntaxError("Attempting to bind the name %s which is already bound" % idx) + e = idx.extent total = [] @@ -97,6 +103,9 @@ def map_wave(self, expr): (var, index, base, update, body) = expr.children + if index not in self.indices: + raise FInATSyntaxError("Wave variable depends on %s, which is not in scope" % index) + try: self.context[var] = self.wave_vars[var] self.context[var] = self.rec(update) @@ -121,8 +130,8 @@ def map_let(self, expr): for var, value in expr.bindings: if var in self.context: - raise ValueError("Let variable %s was already in scope." - % var.name) + raise FInATSyntaxError("Let variable %s was already in scope." + % var.name) self.context[var] = self.rec(value) result = self.rec(expr.body) From dd07963a4ca8c663c042d16dadaf4a4e9995d732 Mon Sep 17 00:00:00 2001 From: Robert Kirby Date: Wed, 19 Nov 2014 12:21:45 -0600 Subject: [PATCH 080/749] field evaluation for bernstein --- finat/bernstein.py | 90 +++++++++++++++++++++++++++++++++------------- 1 file changed, 65 insertions(+), 25 deletions(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index 5818c093f..c835c0cd5 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -47,14 +47,13 @@ def field_evaluation(self, field_var, q, kernel_data, derivative=None): r = kernel_data.new_variable("r") w = kernel_data.new_variable("w") alpha = BasisFunctionIndex(self.degree+1) - s = 1-xi[0][qs[0]] - - expr = Let(((r, xi[0][qs[0]]/s),), + s = 1 - xi[0][qs[0]] + expr = Let(((r, xi[0][qs[0]]/s)), IndexSum((alpha,), Wave(w, alpha, s**self.degree, - w * r * (self.degree - alpha) / (alpha + 1.0), + w * r * (self.degree-alpha)/(alpha+1.0), w * field_var[alpha]) ) ) @@ -67,8 +66,8 @@ def field_evaluation(self, field_var, q, kernel_data, derivative=None): alpha1 = BasisFunctionIndex(deg+1) alpha2 = BasisFunctionIndex(deg+1-alpha1) q2 = PointIndex(q.points.factor_set[1]) - s = 1-xi[0][qs[0]] - tmp_expr = Let((r, xi[1][q2]/s), + s = 1 - xi[1][q2] + tmp_expr = Let(((r, xi[1][q2]/s),), IndexSum((alpha2,), Wave(w, alpha2, @@ -77,18 +76,19 @@ def field_evaluation(self, field_var, q, kernel_data, derivative=None): w * field_var[alpha1*(2*deg-alpha1+3)/2]) ) ) - expr = Let((tmp, tmp_expr), - Let((r, xi[0][qs[0]]), - IndexSum((alpha1,), - Wave(w, - alpha1, - s**deg, - w * r * (deg-alpha1)/(1. + alpha1), - w * tmp[alpha1, qs[1]] - ) - ) - ) + s = 1 - xi[0][qs[0]] + expr = Let(((tmp, tmp_expr), + (r, xi[0][qs[0]]/s)), + IndexSum((alpha1,), + Wave(w, + alpha1, + s**deg, + w * r * (deg-alpha1)/(1. + alpha1), + w * tmp[alpha1, qs[1]] + ) + ) ) + return Recipe(((), (), (q)), expr) elif self.cell.get_spatial_dimension() == 3: deg = self.degree @@ -99,14 +99,54 @@ def field_evaluation(self, field_var, q, kernel_data, derivative=None): alpha1 = BasisFunctionIndex(deg+1) alpha2 = BasisFunctionIndex(deg+1-alpha1) alpha3 = BasisFunctionIndex(deg+1-alpha1-alpha2) + q2 = PointIndex(q.points.factor_set[1]) q3 = PointIndex(q.points.factor_set[2]) - pass -# tmp0_expr = Let((r, xi[2][q3]/s), -# IndexSum((q3,), -# Wave(w, -# alpha3, -# s**(deg-alpha1-alpha2), -# w * r * (deg-alpha1-alpha2-alpha3)/(1.+alpha3), -# w * field_var[] + def pd(sd, d): + if sd == 3: + return (d+1)*(d+2)*(d+3)/6 + elif sd == 2: + return (d+1)*(d+2)/2 + else: + raise NotImplementedError + + s = 1.0 - xi[2][q3] + tmp0_expr = Let(((r, xi[2][q3]/s),), + IndexSum((alpha3,), + Wave(w, + alpha3, + s**(deg-alpha1-alpha2), + w * r * (deg-alpha1-alpha2-alpha3)/(1.+alpha3), + w * field_var[pd(3, deg)-pd(3, deg-alpha1) + + pd(2, deg - alpha1)-pd(2, deg - alpha1 - alpha2) + + alpha3] + ) + ) + ) + s = 1.0 - xi[1][q2] + tmp1_expr = Let(((tmp0, tmp0_expr), + (r, xi[1][q2]/s)), + IndexSum((alpha2,), + Wave(w, + alpha2, + s**(deg-alpha1), + w*r*(deg-alpha1-alpha2)/(1.0+alpha2), + w*tmp0[alpha1, alpha2, q3] + ) + ) + ) + + s = 1.0 - xi[0][qs[0]] + expr = Let(((tmp1, tmp1_expr), + (r, xi[0][qs[0]]/s)), + IndexSum((alpha1,), + Wave(w, + alpha1, + s**deg, + w*r*(deg-alpha1)/(1.+alpha1), + w*tmp1[alpha1, qs[1], qs[2]] + ) + ) + ) + return Recipe(((), (), (q)), expr) From f5af83b3bb5294b6593c9b1110b228feb7eef535 Mon Sep 17 00:00:00 2001 From: David Ham Date: Wed, 19 Nov 2014 18:21:43 +0000 Subject: [PATCH 081/749] Fix some interpreter issues. --- finat/interpreter.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/finat/interpreter.py b/finat/interpreter.py index 6d8ef472f..6756f525f 100644 --- a/finat/interpreter.py +++ b/finat/interpreter.py @@ -77,6 +77,10 @@ def map_for_all(self, expr): indices, body = expr.children + # Deal gracefully with the zero index case. + if not indices: + return self.rec(body) + # Execute over multiple indices recursively. if len(indices) > 1: expr = ForAll(indices[1:], body) From c02188a3b20e9932c092807b3455615834f787eb Mon Sep 17 00:00:00 2001 From: David Ham Date: Wed, 19 Nov 2014 18:40:59 +0000 Subject: [PATCH 082/749] fix let in interpreter --- finat/interpreter.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/finat/interpreter.py b/finat/interpreter.py index 6756f525f..8177b26de 100644 --- a/finat/interpreter.py +++ b/finat/interpreter.py @@ -111,22 +111,22 @@ def map_wave(self, expr): raise FInATSyntaxError("Wave variable depends on %s, which is not in scope" % index) try: - self.context[var] = self.wave_vars[var] - self.context[var] = self.rec(update) + self.context[var.name] = self.wave_vars[var.name] + self.context[var.name] = self.rec(update) except KeyError: # We're at the start of the loop over index. assert self.rec(index) == index.extent.start - self.context[var] = self.rec(base) + self.context[var.name] = self.rec(base) - self.wave_vars[var] = self.context[var] + self.wave_vars[var.name] = self.context[var.name] # Execute the body. result = self.rec(body) # Remove the wave variable from scope. - self.context.pop(var) + self.context.pop(var.name) if self.rec(index) >= index.extent.stop - 1: - self.wave_vars.pop(var) + self.wave_vars.pop(var.name) return result @@ -136,12 +136,12 @@ def map_let(self, expr): if var in self.context: raise FInATSyntaxError("Let variable %s was already in scope." % var.name) - self.context[var] = self.rec(value) + self.context[var.name] = self.rec(value) result = self.rec(expr.body) for var, value in expr.bindings: - self.context.pop(var) + self.context.pop(var.name) return result From 896b268e1cc9b04579936253e26a9387ec26e6fd Mon Sep 17 00:00:00 2001 From: David Ham Date: Wed, 19 Nov 2014 18:48:18 +0000 Subject: [PATCH 083/749] working test with wave --- finat/interpreter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/interpreter.py b/finat/interpreter.py index 8177b26de..550179eaa 100644 --- a/finat/interpreter.py +++ b/finat/interpreter.py @@ -115,7 +115,7 @@ def map_wave(self, expr): self.context[var.name] = self.rec(update) except KeyError: # We're at the start of the loop over index. - assert self.rec(index) == index.extent.start + assert self.rec(index) == (index.extent.start or 0) self.context[var.name] = self.rec(base) self.wave_vars[var.name] = self.context[var.name] From 3f159d8db595561f4dfd652136677b35d1a5884b Mon Sep 17 00:00:00 2001 From: David Ham Date: Wed, 19 Nov 2014 20:36:34 +0000 Subject: [PATCH 084/749] bugfixes --- finat/__init__.py | 2 ++ finat/bernstein.py | 8 ++++---- finat/indices.py | 16 +++++++++++++++- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/finat/__init__.py b/finat/__init__.py index 8e8626d7f..ac10ccea6 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -1,8 +1,10 @@ from lagrange import Lagrange, DiscontinuousLagrange from hdiv import RaviartThomas, BrezziDouglasMarini, BrezziDouglasFortinMarini +from bernstein import Bernstein from vectorfiniteelement import VectorFiniteElement from points import PointSet from utils import KernelData from derivatives import div, grad, curl import interpreter +import quadrature from geometry_mapper import GeometryMapper diff --git a/finat/bernstein.py b/finat/bernstein.py index c835c0cd5..f5d0e5154 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -1,8 +1,8 @@ from finiteelementbase import FiniteElementBase from points import StroudPointSet from ast import ForAll, Recipe, Wave, Let, IndexSum -import pymbolic as p -from index import BasisFunctionIndex, PointIndex +import pymbolic.primitives as p +from indices import BasisFunctionIndex, PointIndex class Bernstein(FiniteElementBase): @@ -37,8 +37,8 @@ def field_evaluation(self, field_var, q, kernel_data, derivative=None): raise NotImplementedError # Get the symbolic names for the points. - xi = [self._points_variable(f, kernel_data) - for f in q.factors.points] + xi = [self._points_variable(f.points, kernel_data) + for f in q.factors] qs = q.factors diff --git a/finat/indices.py b/finat/indices.py index 05ceaa39d..40730499b 100644 --- a/finat/indices.py +++ b/finat/indices.py @@ -40,7 +40,6 @@ def __repr__(self): return "%s(%s)" % (self.__class__.__name__, self.name) - class PointIndex(IndexBase): '''An index running over a set of points, for example quadrature points.''' def __init__(self, pointset): @@ -55,6 +54,21 @@ def __init__(self, pointset): _count = 0 +class TensorPointIndex(IndexBase): + """An index running over a set of points which have a tensor product + structure. This index is actually composed of multiple factors.""" + def __init__(self, pointset): + + self.points = pointset + + name = 'q_' + str(PointIndex._count) + PointIndex._count += 1 + + super(TensorPointIndex, self).__init__(-1, name) + + self.factors = [PointIndex(f) for f in pointset.factor_sets] + + class BasisFunctionIndex(IndexBase): '''An index over a local set of basis functions. E.g. test functions on an element.''' From b0d5eb66922f46025d0d201a97d18c200a940218 Mon Sep 17 00:00:00 2001 From: David Ham Date: Wed, 19 Nov 2014 21:45:06 +0000 Subject: [PATCH 085/749] minor spelling issues --- finat/bernstein.py | 6 +++--- finat/indices.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index f5d0e5154..fe1b9698a 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -65,7 +65,7 @@ def field_evaluation(self, field_var, q, kernel_data, derivative=None): tmp = kernel_data.new_variable("tmp") alpha1 = BasisFunctionIndex(deg+1) alpha2 = BasisFunctionIndex(deg+1-alpha1) - q2 = PointIndex(q.points.factor_set[1]) + q2 = PointIndex(q.points.factor_sets[1]) s = 1 - xi[1][q2] tmp_expr = Let(((r, xi[1][q2]/s),), IndexSum((alpha2,), @@ -99,8 +99,8 @@ def field_evaluation(self, field_var, q, kernel_data, derivative=None): alpha1 = BasisFunctionIndex(deg+1) alpha2 = BasisFunctionIndex(deg+1-alpha1) alpha3 = BasisFunctionIndex(deg+1-alpha1-alpha2) - q2 = PointIndex(q.points.factor_set[1]) - q3 = PointIndex(q.points.factor_set[2]) + q2 = PointIndex(q.points.factor_sets[1]) + q3 = PointIndex(q.points.factor_sets[2]) def pd(sd, d): if sd == 3: diff --git a/finat/indices.py b/finat/indices.py index 40730499b..f4fef89a8 100644 --- a/finat/indices.py +++ b/finat/indices.py @@ -8,7 +8,8 @@ def __init__(self, extent, name): super(IndexBase, self).__init__(name) if isinstance(extent, slice): self._extent = extent - elif isinstance(extent, int): + elif (isinstance(extent, int) + or isinstance(extent, p.Expression)): self._extent = slice(extent) else: raise TypeError("Extent must be a slice or an int") From e86f401ef2819d0c939155ef49d3677d36146b60 Mon Sep 17 00:00:00 2001 From: David Ham Date: Wed, 19 Nov 2014 22:37:11 +0000 Subject: [PATCH 086/749] print wave --- finat/ast.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/finat/ast.py b/finat/ast.py index 66d8289b9..0d1ca53f8 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -84,6 +84,20 @@ def map_delta(self, expr, *args, **kwargs): def map_index(self, expr, *args, **kwargs): return str(expr) + def map_wave(self, expr, enclosing_prec, indent=None, *args, **kwargs): + if indent is None or enclosing_prec is not PREC_NONE: + fmt = "Wave(%s, %s) " + else: + oldidt = " " * indent + indent += 4 + idt = " " * indent + fmt = "Wave(%s,\n" + idt + "%s\n" + oldidt + ")" + + return self.format(fmt, + " ".join(self.rec(c, PREC_NONE, *args, **kwargs) + "," for c in expr.children[:-1]), + self.rec(expr.children[-1], PREC_NONE, indent=indent, *args, **kwargs)) + + def map_index_sum(self, expr, enclosing_prec, indent=None, *args, **kwargs): if indent is None or enclosing_prec is not PREC_NONE: fmt = "IndexSum((%s), %s) " From 03e26598b8fc1631378282be5379f5122e1c4487 Mon Sep 17 00:00:00 2001 From: David Ham Date: Wed, 19 Nov 2014 22:37:24 +0000 Subject: [PATCH 087/749] typo --- finat/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/finat/utils.py b/finat/utils.py index a1cb9fbb4..a8ac7ff01 100644 --- a/finat/utils.py +++ b/finat/utils.py @@ -79,7 +79,7 @@ def new_variable(self, prefix=None): name = prefix or "tmp" if name not in self.variables: self.variables.add(name) - return name + return p.Variable(name) # Prefix was already in use, so append an index. i = 0 @@ -87,7 +87,7 @@ def new_variable(self, prefix=None): varname = "%s_%d" % (name, i) if varname not in self.variables: self.variables.add(varname) - return varname + return p.Variable(varname) i += 1 @property From 1e8973c37e7527915eeee1d19aeb03eb9e899f30 Mon Sep 17 00:00:00 2001 From: David Ham Date: Wed, 19 Nov 2014 22:55:01 +0000 Subject: [PATCH 088/749] the infernal comma --- finat/bernstein.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index fe1b9698a..0461875ec 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -57,7 +57,7 @@ def field_evaluation(self, field_var, q, kernel_data, derivative=None): w * field_var[alpha]) ) ) - return Recipe(((), (), (q)), expr) + return Recipe(((), (), (q,)), expr) elif self.cell.get_spatial_dimension() == 2: deg = self.degree r = kernel_data.new_variable("r") @@ -89,7 +89,7 @@ def field_evaluation(self, field_var, q, kernel_data, derivative=None): ) ) - return Recipe(((), (), (q)), expr) + return Recipe(((), (), (q,)), expr) elif self.cell.get_spatial_dimension() == 3: deg = self.degree r = kernel_data.new_variable("r") @@ -149,4 +149,4 @@ def pd(sd, d): ) ) - return Recipe(((), (), (q)), expr) + return Recipe(((), (), (q,)), expr) From 025904a08448ad36f82d0f1108c53a4f20ad9469 Mon Sep 17 00:00:00 2001 From: David Ham Date: Thu, 20 Nov 2014 15:22:03 +0000 Subject: [PATCH 089/749] expand tensor indices in forall --- finat/interpreter.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/finat/interpreter.py b/finat/interpreter.py index 550179eaa..3d56e8432 100644 --- a/finat/interpreter.py +++ b/finat/interpreter.py @@ -3,6 +3,7 @@ import pymbolic.primitives as p from pymbolic.mapper.evaluator import FloatEvaluationMapper, UnknownVariableError from ast import IndexSum, ForAll, LeviCivita, FInATSyntaxError +from indices import TensorPointIndex import numpy as np import copy @@ -84,6 +85,10 @@ def map_for_all(self, expr): # Execute over multiple indices recursively. if len(indices) > 1: expr = ForAll(indices[1:], body) + # Expand tensor indices + elif isinstance(indices[0], TensorPointIndex): + indices = indices[0].factors + expr = ForAll(indices[1:], body) else: expr = body From 7bcdd266866c0d140ccfdd01748841b606751f54 Mon Sep 17 00:00:00 2001 From: David Ham Date: Thu, 20 Nov 2014 15:25:34 +0000 Subject: [PATCH 090/749] somewhat better error message for unbound variables --- finat/interpreter.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/finat/interpreter.py b/finat/interpreter.py index 3d56e8432..1924cac7d 100644 --- a/finat/interpreter.py +++ b/finat/interpreter.py @@ -72,7 +72,10 @@ def map_index_sum(self, expr): def map_index(self, expr): - return self.indices[expr] + try: + return self.indices[expr] + except KeyError: + raise FInATSyntaxError("Access to unbound variable name %s." % expr) def map_for_all(self, expr): From 2fdd4419e8f40b73173bf0b57f8456c0953e1d5f Mon Sep 17 00:00:00 2001 From: David Ham Date: Thu, 20 Nov 2014 15:56:34 +0000 Subject: [PATCH 091/749] bind variables in bernstein --- finat/bernstein.py | 2 +- finat/interpreter.py | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index 0461875ec..f06041aa7 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -77,7 +77,7 @@ def field_evaluation(self, field_var, q, kernel_data, derivative=None): ) ) s = 1 - xi[0][qs[0]] - expr = Let(((tmp, tmp_expr), + expr = Let(((tmp, Recipe(((), (alpha1,), (q2,)), tmp_expr)), (r, xi[0][qs[0]]/s)), IndexSum((alpha1,), Wave(w, diff --git a/finat/interpreter.py b/finat/interpreter.py index 1924cac7d..4b79a0285 100644 --- a/finat/interpreter.py +++ b/finat/interpreter.py @@ -8,12 +8,6 @@ import copy -def _as_range(e): - """Convert a slice to a range.""" - - return range(e.start or 0, e.stop, e.step or 1) - - class FinatEvaluationMapper(FloatEvaluationMapper): def __init__(self, context={}): @@ -27,6 +21,15 @@ def __init__(self, context={}): # Storage for wave variables while they are out of scope. self.wave_vars = {} + def _as_range(self, e): + """Convert a slice to a range. If the range has expressions as bounds, + evaluate them. + """ + + return range(int(self.rec(e.start or 0)), + int(self.rec(e.stop)), + int(self.rec(e.step or 1))) + def map_variable(self, expr): try: var = self.context[expr.name] @@ -62,7 +65,7 @@ def map_index_sum(self, expr): e = idx.extent total = 0.0 - for i in _as_range(e): + for i in self._as_range(e): self.indices[idx] = i total += self.rec(expr) @@ -103,7 +106,7 @@ def map_for_all(self, expr): e = idx.extent total = [] - for i in _as_range(e): + for i in self._as_range(e): self.indices[idx] = i total.append(self.rec(expr)) From 07e183e9e03a203a31feee86da2507e7378a7f4a Mon Sep 17 00:00:00 2001 From: David Ham Date: Thu, 20 Nov 2014 17:47:20 +0000 Subject: [PATCH 092/749] remove pullback function --- finat/finiteelementbase.py | 15 +++++++++------ finat/lagrange.py | 10 ---------- finat/vectorfiniteelement.py | 10 ---------- 3 files changed, 9 insertions(+), 26 deletions(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 314d6f2c0..e1c026f2c 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -53,6 +53,15 @@ def facet_support_dofs(self): raise NotImplementedError + @property + def dofs_shape(self): + '''Return a tuple indicating the number of degrees of freedom in the + element. For example a scalar quadratic Lagrange element on a triangle + would return (6,) while a vector valued version of the same element + would return (6, 2)''' + + raise NotImplementedError + def field_evaluation(self, field_var, q, kernel_data, derivative=None): '''Return code for evaluating a known field at known points on the reference element. @@ -83,12 +92,6 @@ def basis_evaluation(self, q, kernel_data, derivative=None): raise NotImplementedError - def pullback(self, derivative): - '''Return symbolic information about how this element pulls back - under this derivative.''' - - raise NotImplementedError - def moment_evaluation(self, value, weights, q, kernel_data, derivative=None): '''Return code for evaluating: diff --git a/finat/lagrange.py b/finat/lagrange.py index ab412e97b..42b7a9f41 100644 --- a/finat/lagrange.py +++ b/finat/lagrange.py @@ -59,16 +59,6 @@ def moment_evaluation(self, value, weights, q, return super(ScalarElement, self).moment_evaluation( value, weights, q, kernel_data, derivative, pullback) - def pullback(self, phi, kernel_data, derivative=None): - - if derivative is None: - return phi - elif derivative == grad: - return None # dot(invJ, grad(phi)) - else: - raise ValueError( - "Scalar elements do not have a %s operation") % derivative - class Lagrange(ScalarElement): def __init__(self, cell, degree): diff --git a/finat/vectorfiniteelement.py b/finat/vectorfiniteelement.py index 286bf8bad..9c27d248c 100644 --- a/finat/vectorfiniteelement.py +++ b/finat/vectorfiniteelement.py @@ -223,13 +223,3 @@ def moment_evaluation(self, value, weights, q, expression = IndexSum(d + p, psi * phi * w[p]) return Recipe(((), b + beta + b_, ()), expression) - - def pullback(self, phi, kernel_data, derivative=None): - - if derivative is None: - return phi - elif derivative == grad: - return None # IndexSum(alpha, Jinv[:, alpha] * grad(phi)[:,alpha]) - else: - raise ValueError( - "Lagrange elements do not have a %s operation") % derivative From c56d17ca104b86a6e897931c8881d5f4314af81f Mon Sep 17 00:00:00 2001 From: David Ham Date: Thu, 20 Nov 2014 19:25:47 +0000 Subject: [PATCH 093/749] pullbacks and dofs_shape --- finat/bernstein.py | 9 +++++++++ finat/finiteelementbase.py | 14 +++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index f06041aa7..2abf5931f 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -3,6 +3,7 @@ from ast import ForAll, Recipe, Wave, Let, IndexSum import pymbolic.primitives as p from indices import BasisFunctionIndex, PointIndex +import numpy as np class Bernstein(FiniteElementBase): @@ -28,6 +29,14 @@ def _points_variable(self, points, kernel_data): return xi + @property + def dofs_shape(self): + + degree = self.degree + dim = self.cell.get_spatial_dimension() + return (int(np.prod(xrange(degree + 1, degree + 1 + dim)) + / np.prod(xrange(1, dim + 1))),) + def field_evaluation(self, field_var, q, kernel_data, derivative=None): if not isinstance(q.points, StroudPointSet): diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index e1c026f2c..5c0575ba4 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -62,7 +62,8 @@ def dofs_shape(self): raise NotImplementedError - def field_evaluation(self, field_var, q, kernel_data, derivative=None): + def field_evaluation(self, field_var, q, kernel_data, derivative=None, + pullback=None): '''Return code for evaluating a known field at known points on the reference element. @@ -72,12 +73,13 @@ def field_evaluation(self, field_var, q, kernel_data, derivative=None): :param kernel_data: the :class:`.KernelData` object corresponding to the current kernel. :param derivative: the derivative to take of the test function. - + :param pullback: whether to pull back to the reference cell. ''' raise NotImplementedError - def basis_evaluation(self, q, kernel_data, derivative=None): + def basis_evaluation(self, q, kernel_data, derivative=None, + pullback=None): '''Return code for evaluating a known field at known points on the reference element. @@ -87,12 +89,13 @@ def basis_evaluation(self, q, kernel_data, derivative=None): :param kernel_data: the :class:`.KernelData` object corresponding to the current kernel. :param derivative: the derivative to take of the test function. - + :param pullback: whether to pull back to the reference cell. ''' raise NotImplementedError - def moment_evaluation(self, value, weights, q, kernel_data, derivative=None): + def moment_evaluation(self, value, weights, q, kernel_data, + derivative=None, pullback=None): '''Return code for evaluating: .. math:: @@ -108,6 +111,7 @@ def moment_evaluation(self, value, weights, q, kernel_data, derivative=None): at which to evaluate. :param kernel_data: the :class:`.KernelData` object corresponding to the current kernel. :param derivative: the derivative to take of the test function. + :param pullback: whether to pull back to the reference cell. ''' raise NotImplementedError From ebb42778f1755adfd8fae3bd34b50b0980dd0ade Mon Sep 17 00:00:00 2001 From: David Ham Date: Thu, 20 Nov 2014 20:26:31 +0000 Subject: [PATCH 094/749] Fix forall for zero trip loops --- finat/interpreter.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/finat/interpreter.py b/finat/interpreter.py index 4b79a0285..05ea06673 100644 --- a/finat/interpreter.py +++ b/finat/interpreter.py @@ -65,6 +65,9 @@ def map_index_sum(self, expr): e = idx.extent total = 0.0 + + self.indices[idx] = None + for i in self._as_range(e): self.indices[idx] = i total += self.rec(expr) @@ -136,7 +139,7 @@ def map_wave(self, expr): # Remove the wave variable from scope. self.context.pop(var.name) - if self.rec(index) >= index.extent.stop - 1: + if self.rec(index) >= self.rec(index.extent.stop) - 1: self.wave_vars.pop(var.name) return result From e22d6e36c62f1b607210903da9567b5a7a9b7126 Mon Sep 17 00:00:00 2001 From: Robert Kirby Date: Thu, 20 Nov 2014 13:31:53 -0600 Subject: [PATCH 095/749] generalize bernstein to nd --- finat/bernstein.py | 105 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 96 insertions(+), 9 deletions(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index 2abf5931f..2427b794c 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -45,13 +45,107 @@ def field_evaluation(self, field_var, q, kernel_data, derivative=None): if derivative is not None: raise NotImplementedError + def pd(sd, d): + if sd == 3: + return (d+1)*(d+2)*(d+3)/6 + elif sd == 2: + return (d+1)*(d+2)/2 + elif sd == 1: + return d+1 + else: + raise NotImplementedError + + # Get the symbolic names for the points. xi = [self._points_variable(f.points, kernel_data) for f in q.factors] qs = q.factors - # 1D first + deg = self.degree + + sd = self.cell.get_spatial_dimension() + + # reimplement using reduce to avoid problem with infinite loop into pymbolic + def mysum(vals): + return reduce(lambda a, b: a+b, vals) + + # Create basis function indices that run over + # the possible multiindex space. These have + # to be jagged + alphas = [BasisFunctionIndex(deg+1)] + for d in range(1, sd): + asum = mysum(alphas) + alpha_cur = BasisFunctionIndex(deg+1-asum) + alphas.append(alpha_cur) + + qs_internal = [PointIndex(qf) for qf in q.points.factor_sets] + + r = kernel_data.new_variable("r") + w = kernel_data.new_variable("w") + tmps = [kernel_data.new_variable("tmp") for d in range(sd-1)] + + + # every phase of sum-factorization *except* the last one + # will make use of the qs_internal to index into + # quadrature points, but the last phase will use the + # index set from the point set given as an argument. + # To avoid some if statements, we will create a list of + # what quadrature point indices should be used at which phase + qs_per_phase = [qs_internal]*(sd-1) + qs + + # The first phase is different since we read from field_var + # instead of one of the temporaries -- field_var is stored + # by a single index and the rest are stored as tensor products. + # This code computes the offset into the field_var storage + # for the internal sum variable loop. + offset = 0 + for d in range(sd-1): + deg_begin = deg - mysum(alphas[:d]) + deg_end = deg - alphas[d] + offset += pd(sd-d, deg_begin) - pd(sd-d, deg_end) + + # each phase of the sum-factored algorithm reads from a particular + # location. The first of these is field_var, the rest are the + # temporaries. + read_locs = [field_var[alphas[-1]+offset]] \ + + [tmps[d][tuple(alphas[:(-d)]+qs_internal[(-d):])] + for d in range(sd-1)] + + # In the first phase of the sum-factorization we don't have a previous + # result to bind, so the Let expression is different. + qs_cur = qs_per_phase[0] + s = 1.0 - xi[-1][qs_cur[-1]] + expr = Let(((r, xi[-1][qs_cur[-1]]/s),), + IndexSum((alphas[-1],), + Wave(w, + alphas[-1], + s**(deg-mysum(alphas[:(sd-1)])), + w * r * (deg-mysum(alphas)) / (1.0 + alphas[-1]), + w * read_locs[0] + ) + ) + ) + + for d in range(sd-1): + qs_cur = qs_per_phase[d+1] + b_ind = -(d+2) # index into several things counted backward + s = 1.0 - xi[b_ind][qs_cur[b_ind]] + recipe_args = ((), + tuple(alphas[:(b_ind+1)]), + tuple(qs_cur[(b_ind+1):])) + expr = Let(((tmps[d], Recipe(recipe_args, expr)), + (r, xi[b_ind][qs_cur[b_ind]]/s)), + IndexSum((alphas[b_ind],), + Wave(w, + alphas[b_ind], + s**(deg-mysum(alphas[:b_ind])), + w * r * (deg-mysum(alphas[:(b_ind+1)]))/(1.0+alphas[b_ind]), + w * read_locs[d+1] + ) + ) + ) + if self.cell.get_spatial_dimension() == 1: r = kernel_data.new_variable("r") w = kernel_data.new_variable("w") @@ -76,6 +170,7 @@ def field_evaluation(self, field_var, q, kernel_data, derivative=None): alpha2 = BasisFunctionIndex(deg+1-alpha1) q2 = PointIndex(q.points.factor_sets[1]) s = 1 - xi[1][q2] + tmp_expr = Let(((r, xi[1][q2]/s),), IndexSum((alpha2,), Wave(w, @@ -111,14 +206,6 @@ def field_evaluation(self, field_var, q, kernel_data, derivative=None): q2 = PointIndex(q.points.factor_sets[1]) q3 = PointIndex(q.points.factor_sets[2]) - def pd(sd, d): - if sd == 3: - return (d+1)*(d+2)*(d+3)/6 - elif sd == 2: - return (d+1)*(d+2)/2 - else: - raise NotImplementedError - s = 1.0 - xi[2][q3] tmp0_expr = Let(((r, xi[2][q3]/s),), IndexSum((alpha3,), From d83b86318938d2a301340ea162d674a9a720ba4f Mon Sep 17 00:00:00 2001 From: Robert Kirby Date: Thu, 20 Nov 2014 14:41:52 -0600 Subject: [PATCH 096/749] work on nd bernstein that doesn't work --- finat/bernstein.py | 213 +++++++++++++++++++++++---------------------- 1 file changed, 108 insertions(+), 105 deletions(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index 2427b794c..4b2ebb14f 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -38,7 +38,6 @@ def dofs_shape(self): / np.prod(xrange(1, dim + 1))),) def field_evaluation(self, field_var, q, kernel_data, derivative=None): - if not isinstance(q.points, StroudPointSet): raise ValueError("Only Stroud points may be employed with Bernstein polynomials") @@ -68,7 +67,7 @@ def pd(sd, d): # reimplement using reduce to avoid problem with infinite loop into pymbolic def mysum(vals): - return reduce(lambda a, b: a+b, vals) + return reduce(lambda a, b: a+b, vals, 0) # Create basis function indices that run over # the possible multiindex space. These have @@ -85,7 +84,6 @@ def mysum(vals): w = kernel_data.new_variable("w") tmps = [kernel_data.new_variable("tmp") for d in range(sd-1)] - # every phase of sum-factorization *except* the last one # will make use of the qs_internal to index into # quadrature points, but the last phase will use the @@ -131,9 +129,12 @@ def mysum(vals): qs_cur = qs_per_phase[d+1] b_ind = -(d+2) # index into several things counted backward s = 1.0 - xi[b_ind][qs_cur[b_ind]] + recipe_args = ((), - tuple(alphas[:(b_ind+1)]), - tuple(qs_cur[(b_ind+1):])) + [a for a in alphas[:(b_ind+1)]], + [qi for qi in qs_cur[(b_ind+1):]]) + print "hi there\n\n\n" + expr = Let(((tmps[d], Recipe(recipe_args, expr)), (r, xi[b_ind][qs_cur[b_ind]]/s)), IndexSum((alphas[b_ind],), @@ -146,103 +147,105 @@ def mysum(vals): ) ) - if self.cell.get_spatial_dimension() == 1: - r = kernel_data.new_variable("r") - w = kernel_data.new_variable("w") - alpha = BasisFunctionIndex(self.degree+1) - s = 1 - xi[0][qs[0]] - expr = Let(((r, xi[0][qs[0]]/s)), - IndexSum((alpha,), - Wave(w, - alpha, - s**self.degree, - w * r * (self.degree-alpha)/(alpha+1.0), - w * field_var[alpha]) - ) - ) - return Recipe(((), (), (q,)), expr) - elif self.cell.get_spatial_dimension() == 2: - deg = self.degree - r = kernel_data.new_variable("r") - w = kernel_data.new_variable("w") - tmp = kernel_data.new_variable("tmp") - alpha1 = BasisFunctionIndex(deg+1) - alpha2 = BasisFunctionIndex(deg+1-alpha1) - q2 = PointIndex(q.points.factor_sets[1]) - s = 1 - xi[1][q2] - - tmp_expr = Let(((r, xi[1][q2]/s),), - IndexSum((alpha2,), - Wave(w, - alpha2, - s**(deg - alpha1), - w * r * (deg-alpha1-alpha2)/(1.0 + alpha2), - w * field_var[alpha1*(2*deg-alpha1+3)/2]) - ) - ) - s = 1 - xi[0][qs[0]] - expr = Let(((tmp, Recipe(((), (alpha1,), (q2,)), tmp_expr)), - (r, xi[0][qs[0]]/s)), - IndexSum((alpha1,), - Wave(w, - alpha1, - s**deg, - w * r * (deg-alpha1)/(1. + alpha1), - w * tmp[alpha1, qs[1]] - ) - ) - ) - - return Recipe(((), (), (q,)), expr) - elif self.cell.get_spatial_dimension() == 3: - deg = self.degree - r = kernel_data.new_variable("r") - w = kernel_data.new_variable("w") - tmp0 = kernel_data.new_variable("tmp0") - tmp1 = kernel_data.new_variable("tmp1") - alpha1 = BasisFunctionIndex(deg+1) - alpha2 = BasisFunctionIndex(deg+1-alpha1) - alpha3 = BasisFunctionIndex(deg+1-alpha1-alpha2) - q2 = PointIndex(q.points.factor_sets[1]) - q3 = PointIndex(q.points.factor_sets[2]) - - s = 1.0 - xi[2][q3] - tmp0_expr = Let(((r, xi[2][q3]/s),), - IndexSum((alpha3,), - Wave(w, - alpha3, - s**(deg-alpha1-alpha2), - w * r * (deg-alpha1-alpha2-alpha3)/(1.+alpha3), - w * field_var[pd(3, deg)-pd(3, deg-alpha1) - + pd(2, deg - alpha1)-pd(2, deg - alpha1 - alpha2) - + alpha3] - ) - ) - ) - s = 1.0 - xi[1][q2] - tmp1_expr = Let(((tmp0, tmp0_expr), - (r, xi[1][q2]/s)), - IndexSum((alpha2,), - Wave(w, - alpha2, - s**(deg-alpha1), - w*r*(deg-alpha1-alpha2)/(1.0+alpha2), - w*tmp0[alpha1, alpha2, q3] - ) - ) - ) - - s = 1.0 - xi[0][qs[0]] - expr = Let(((tmp1, tmp1_expr), - (r, xi[0][qs[0]]/s)), - IndexSum((alpha1,), - Wave(w, - alpha1, - s**deg, - w*r*(deg-alpha1)/(1.+alpha1), - w*tmp1[alpha1, qs[1], qs[2]] - ) - ) - ) - - return Recipe(((), (), (q,)), expr) + return Recipe(((), (), (q,)), expr) + +# if self.cell.get_spatial_dimension() == 1: +# r = kernel_data.new_variable("r") +# w = kernel_data.new_variable("w") +# alpha = BasisFunctionIndex(self.degree+1) +# s = 1 - xi[0][qs[0]] +# expr = Let(((r, xi[0][qs[0]]/s)), +# IndexSum((alpha,), +# Wave(w, +# alpha, +# s**self.degree, +# w * r * (self.degree-alpha)/(alpha+1.0), +# w * field_var[alpha]) +# ) +# ) +# return Recipe(((), (), (q,)), expr) +# elif self.cell.get_spatial_dimension() == 2: +# deg = self.degree +# r = kernel_data.new_variable("r") +# w = kernel_data.new_variable("w") +# tmp = kernel_data.new_variable("tmp") +# alpha1 = BasisFunctionIndex(deg+1) +# alpha2 = BasisFunctionIndex(deg+1-alpha1) +# q2 = PointIndex(q.points.factor_sets[1]) +# s = 1 - xi[1][q2] + +# tmp_expr = Let(((r, xi[1][q2]/s),), +# IndexSum((alpha2,), +# Wave(w, +# alpha2, +# s**(deg - alpha1), +# w * r * (deg-alpha1-alpha2)/(1.0 + alpha2), +# w * field_var[alpha1*(2*deg-alpha1+3)/2]) +# ) +# ) +# s = 1 - xi[0][qs[0]] +# expr = Let(((tmp, Recipe(((), (alpha1,), (q2,)), tmp_expr)), +# (r, xi[0][qs[0]]/s)), +# IndexSum((alpha1,), +# Wave(w, +# alpha1, +# s**deg, +# w * r * (deg-alpha1)/(1. + alpha1), +# w * tmp[alpha1, qs[1]] +# ) +# ) +# ) +# +# return Recipe(((), (), (q,)), expr) +# elif self.cell.get_spatial_dimension() == 3: +# deg = self.degree +# r = kernel_data.new_variable("r") +# w = kernel_data.new_variable("w") +# tmp0 = kernel_data.new_variable("tmp0") +# tmp1 = kernel_data.new_variable("tmp1") +# alpha1 = BasisFunctionIndex(deg+1) +# alpha2 = BasisFunctionIndex(deg+1-alpha1) +# alpha3 = BasisFunctionIndex(deg+1-alpha1-alpha2) +# q2 = PointIndex(q.points.factor_sets[1]) +# q3 = PointIndex(q.points.factor_sets[2]) +# +# s = 1.0 - xi[2][q3] +# tmp0_expr = Let(((r, xi[2][q3]/s),), +# IndexSum((alpha3,), +# Wave(w, +# alpha3, +# s**(deg-alpha1-alpha2), +# w * r * (deg-alpha1-alpha2-alpha3)/(1.+alpha3), +# w * field_var[pd(3, deg)-pd(3, deg-alpha1) +# + pd(2, deg - alpha1)-pd(2, deg - alpha1 -# alpha2) +# + alpha3] +# ) +# ) +# ) +# s = 1.0 - xi[1][q2] +# tmp1_expr = Let(((tmp0, tmp0_expr), +# (r, xi[1][q2]/s)), +# IndexSum((alpha2,), +# Wave(w, +# alpha2, +# s**(deg-alpha1), +# w*r*(deg-alpha1-alpha2)/(1.0+alpha2), +# w*tmp0[alpha1, alpha2, q3] +# ) +# ) +# ) +# +# s = 1.0 - xi[0][qs[0]] +# expr = Let(((tmp1, tmp1_expr), +# (r, xi[0][qs[0]]/s)), +# IndexSum((alpha1,), +# Wave(w, +# alpha1, +# s**deg, +# w*r*(deg-alpha1)/(1.+alpha1), +# w*tmp1[alpha1, qs[1], qs[2]] +# ) +# ) +# ) +# +# return Recipe(((), (), (q,)), expr) From 20a8d0c2076e92c6ac6e44097039374c09d3a5e0 Mon Sep 17 00:00:00 2001 From: Robert Kirby Date: Thu, 20 Nov 2014 14:53:21 -0600 Subject: [PATCH 097/749] fixed infinite loop --- finat/bernstein.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index 4b2ebb14f..c3102095c 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -90,7 +90,7 @@ def mysum(vals): # index set from the point set given as an argument. # To avoid some if statements, we will create a list of # what quadrature point indices should be used at which phase - qs_per_phase = [qs_internal]*(sd-1) + qs + qs_per_phase = [qs_internal]*(sd-1) + [qs] # The first phase is different since we read from field_var # instead of one of the temporaries -- field_var is stored @@ -131,9 +131,8 @@ def mysum(vals): s = 1.0 - xi[b_ind][qs_cur[b_ind]] recipe_args = ((), - [a for a in alphas[:(b_ind+1)]], - [qi for qi in qs_cur[(b_ind+1):]]) - print "hi there\n\n\n" + tuple(alphas[:(b_ind+1)]), + tuple(qs_cur[(b_ind+1):])) expr = Let(((tmps[d], Recipe(recipe_args, expr)), (r, xi[b_ind][qs_cur[b_ind]]/s)), From b405a922789b7fce6d8692cc3b29b07606974ecc Mon Sep 17 00:00:00 2001 From: David Ham Date: Fri, 21 Nov 2014 01:03:57 +0000 Subject: [PATCH 098/749] stringify fix --- finat/ast.py | 74 ++++++++++++++++++---------------------------------- 1 file changed, 26 insertions(+), 48 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index 0d1ca53f8..7d43dd507 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -70,11 +70,12 @@ def map_let(self, expr, enclosing_prec, indent=None, *args, **kwargs): else: oldidt = " " * indent indent += 4 + inner_idt = " " * (indent + 4) idt = " " * indent - fmt = "Let(\n" + idt + "%s,\n" + idt + "%s\n" + oldidt + ")" + fmt = "Let(\n" + inner_idt + "%s,\n" + idt + "%s\n" + oldidt + ")" return self.format(fmt, - self.rec(expr.bindings, PREC_NONE, indent=None, *args, **kwargs), + self.rec(expr.bindings, PREC_NONE, indent=indent, *args, **kwargs), self.rec(expr.body, PREC_NONE, indent=indent, *args, **kwargs)) def map_delta(self, expr, *args, **kwargs): @@ -124,6 +125,20 @@ def map_det(self, expr, *args, **kwargs): self.rec(expr.expression, *args, **kwargs)) +class StringifyMixin(object): + """Mixin class to set stringification options correctly for pymbolic subclasses.""" + def __str__(self): + """Use the :meth:`stringifier` to return a human-readable + string representation of *self*. + """ + + from pymbolic.mapper.stringifier import PREC_NONE + return self.stringifier()()(self, PREC_NONE, indent=0) + + def stringifier(self): + return _StringifyMapper + + class Array(p.Variable): """A pymbolic variable of known extent.""" def __init__(self, name, shape): @@ -132,7 +147,7 @@ def __init__(self, name, shape): self.shape = shape -class Recipe(p.Expression): +class Recipe(StringifyMixin, p.Expression): """AST snippets and data corresponding to some form of finite element evaluation. @@ -176,17 +191,6 @@ def __getitem__(self, index): return self.replace_indices(replacements) - def __str__(self): - """Use the :meth:`stringifier` to return a human-readable - string representation of *self*. - """ - - from pymbolic.mapper.stringifier import PREC_NONE - return self.stringifier()()(self, PREC_NONE, indent=0) - - def stringifier(self): - return _StringifyMapper - def replace_indices(self, replacements): """Return a copy of this :class:`Recipe` with some of the indices substituted.""" @@ -197,7 +201,7 @@ def replace_indices(self, replacements): return _IndexMapper(replacements)(self) -class IndexSum(p._MultiChildExpression): +class IndexSum(StringifyMixin, p._MultiChildExpression): """A symbolic expression for a sum over one or more indices. :param indices: a sequence of indices over which to sum. @@ -223,13 +227,10 @@ def __init__(self, indices, body): def __getinitargs__(self): return self.children - def stringifier(self): - return _StringifyMapper - mapper_method = "map_index_sum" -class LeviCivita(p._MultiChildExpression): +class LeviCivita(StringifyMixin, p._MultiChildExpression): r"""The Levi-Civita symbol expressed as an operator. :param free: A tuple of free indices. @@ -252,13 +253,10 @@ def __init__(self, free, bound, body): def __getinitargs__(self): return self.children - def stringifier(self): - return _StringifyMapper - mapper_method = "map_index_sum" -class ForAll(p._MultiChildExpression): +class ForAll(StringifyMixin, p._MultiChildExpression): """A symbolic expression to indicate that the body will actually be evaluated for all of the values of its free indices. This enables index simplification to take place. @@ -274,14 +272,10 @@ def __init__(self, indices, body): def __getinitargs__(self): return self.children - def __str__(self): - return "ForAll(%s, %s)" % (str([x._str_extent for x in self.children[0]]), - self.children[1]) - mapper_method = "map_for_all" -class Wave(p._MultiChildExpression): +class Wave(StringifyMixin, p._MultiChildExpression): """A symbolic expression with loop-carried dependencies.""" def __init__(self, var, index, base, update, body): @@ -290,13 +284,10 @@ def __init__(self, var, index, base, update, body): def __getinitargs__(self): return self.children - def __str__(self): - return "Wave(%s, %s, %s, %s, %s)" % tuple(map(str, self.children)) - mapper_method = "map_wave" -class Let(p._MultiChildExpression): +class Let(StringifyMixin, p._MultiChildExpression): """A Let expression enables local variable bindings in an expression. This feature is lifted more or less directly from Scheme. @@ -320,13 +311,10 @@ def __init__(self, bindings, body): self.bindings, self.body = self.children - def __str__(self): - return "Let(%s)" % self.children - mapper_method = "map_let" -class Delta(p._MultiChildExpression): +class Delta(StringifyMixin, p._MultiChildExpression): """The Kronecker delta expressed as a ternary operator: .. math:: @@ -355,11 +343,8 @@ def __str__(self): mapper_method = "map_delta" - def stringifier(self): - return _StringifyMapper - -class Inverse(p.Expression): +class Inverse(StringifyMixin, p.Expression): """The inverse of a matrix-valued expression. Where the expression is not square, this is the Moore-Penrose pseudo-inverse. @@ -371,11 +356,8 @@ def __init__(self, expression): mapper_method = "map_inverse" - def stringifier(self): - return _StringifyMapper - -class Det(p.Expression): +class Det(StringifyMixin, p.Expression): """The determinant of a matrix-valued expression. Where the expression is evaluated at a number of points, the @@ -387,10 +369,6 @@ def __init__(self, expression): mapper_method = "map_det" - def stringifier(self): - return _StringifyMapper - - class FInATSyntaxError(Exception): """Exception raised when the syntax rules of the FInAT ast are violated.""" pass From 21a39c0fde50ab963e0e0cf5d364718423715f5f Mon Sep 17 00:00:00 2001 From: David Ham Date: Fri, 21 Nov 2014 15:35:43 +0000 Subject: [PATCH 099/749] indentation fix --- finat/ast.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index 7d43dd507..d461601cb 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -67,15 +67,17 @@ def map_recipe(self, expr, enclosing_prec, indent=None, *args, **kwargs): def map_let(self, expr, enclosing_prec, indent=None, *args, **kwargs): if indent is None: fmt = "Let(%s, %s)" + inner_indent = None else: oldidt = " " * indent indent += 4 - inner_idt = " " * (indent + 4) + inner_indent = indent + 4 + inner_idt = " " * inner_indent idt = " " * indent fmt = "Let(\n" + inner_idt + "%s,\n" + idt + "%s\n" + oldidt + ")" return self.format(fmt, - self.rec(expr.bindings, PREC_NONE, indent=indent, *args, **kwargs), + self.rec(expr.bindings, PREC_NONE, indent=inner_indent, *args, **kwargs), self.rec(expr.body, PREC_NONE, indent=indent, *args, **kwargs)) def map_delta(self, expr, *args, **kwargs): From 47d1cabf6442ba7f32700bd7d01d88f783292983 Mon Sep 17 00:00:00 2001 From: David Ham Date: Fri, 21 Nov 2014 16:19:53 +0000 Subject: [PATCH 100/749] Pretty-printing of ast --- finat/ast.py | 56 ++++++++++++++++++++++++++++++++---------------- finat/indices.py | 1 + 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index d461601cb..82da6540e 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -5,6 +5,10 @@ from pymbolic.mapper import IdentityMapper as IM from pymbolic.mapper.stringifier import StringifyMapper, PREC_NONE from indices import IndexBase +try: + from termcolor import colored +except ImportError: + colored = lambda string, color: string class FInATSyntaxError(Exception): @@ -53,12 +57,12 @@ class _StringifyMapper(StringifyMapper): def map_recipe(self, expr, enclosing_prec, indent=None, *args, **kwargs): if indent is None: - fmt = "Recipe(%s, %s)" + fmt = expr.name + "(%s, %s)" else: oldidt = " " * indent indent += 4 idt = " " * indent - fmt = "Recipe(%s,\n" + idt + "%s\n" + oldidt + ")" + fmt = expr.name + "(%s,\n" + idt + "%s\n" + oldidt + ")" return self.format(fmt, self.rec(expr.indices, PREC_NONE, indent=indent, *args, **kwargs), @@ -66,7 +70,7 @@ def map_recipe(self, expr, enclosing_prec, indent=None, *args, **kwargs): def map_let(self, expr, enclosing_prec, indent=None, *args, **kwargs): if indent is None: - fmt = "Let(%s, %s)" + fmt = expr.name + "(%s, %s)" inner_indent = None else: oldidt = " " * indent @@ -74,61 +78,68 @@ def map_let(self, expr, enclosing_prec, indent=None, *args, **kwargs): inner_indent = indent + 4 inner_idt = " " * inner_indent idt = " " * indent - fmt = "Let(\n" + inner_idt + "%s,\n" + idt + "%s\n" + oldidt + ")" + fmt = expr.name + "(\n" + inner_idt + "%s,\n" + idt + "%s\n" + oldidt + ")" return self.format(fmt, self.rec(expr.bindings, PREC_NONE, indent=inner_indent, *args, **kwargs), self.rec(expr.body, PREC_NONE, indent=indent, *args, **kwargs)) def map_delta(self, expr, *args, **kwargs): - return self.format("Delta(%s, %s)", + return self.format(expr.name + "(%s, %s)", *[self.rec(c, *args, **kwargs) for c in expr.children]) def map_index(self, expr, *args, **kwargs): - return str(expr) + return colored(str(expr), expr._color) def map_wave(self, expr, enclosing_prec, indent=None, *args, **kwargs): if indent is None or enclosing_prec is not PREC_NONE: - fmt = "Wave(%s, %s) " + fmt = expr.name + "(%s, %s) " else: oldidt = " " * indent indent += 4 idt = " " * indent - fmt = "Wave(%s,\n" + idt + "%s\n" + oldidt + ")" + fmt = expr.name + "(%s,\n" + idt + "%s\n" + oldidt + ")" return self.format(fmt, " ".join(self.rec(c, PREC_NONE, *args, **kwargs) + "," for c in expr.children[:-1]), self.rec(expr.children[-1], PREC_NONE, indent=indent, *args, **kwargs)) - def map_index_sum(self, expr, enclosing_prec, indent=None, *args, **kwargs): if indent is None or enclosing_prec is not PREC_NONE: - fmt = "IndexSum((%s), %s) " + fmt = expr.name + "((%s), %s) " else: oldidt = " " * indent indent += 4 idt = " " * indent - fmt = "IndexSum((%s),\n" + idt + "%s\n" + oldidt + ")" + fmt = expr.name + "((%s),\n" + idt + "%s\n" + oldidt + ")" return self.format(fmt, " ".join(self.rec(c, PREC_NONE, *args, **kwargs) + "," for c in expr.children[0]), self.rec(expr.children[1], PREC_NONE, indent=indent, *args, **kwargs)) def map_levi_civita(self, expr, *args, **kwargs): - return self.format("LeviCivita(%s)", + return self.format(expr.name + "(%s)", self.join_rec(", ", expr.children, *args, **kwargs)) def map_inverse(self, expr, *args, **kwargs): - return self.format("Inverse(%s)", + return self.format(expr.name + "(%s)", self.rec(expr.expression, *args, **kwargs)) def map_det(self, expr, *args, **kwargs): - return self.format("Det(%s)", + return self.format(expr.name + "(%s)", self.rec(expr.expression, *args, **kwargs)) + def map_variable(self, expr, enclosing_prec, *args, **kwargs): + try: + return colored(expr.name, expr._color) + except AttributeError: + return colored(expr.name, "cyan") + + class StringifyMixin(object): """Mixin class to set stringification options correctly for pymbolic subclasses.""" + def __str__(self): """Use the :meth:`stringifier` to return a human-readable string representation of *self*. @@ -140,6 +151,10 @@ def __str__(self): def stringifier(self): return _StringifyMapper + @property + def name(self): + return colored(str(self.__class__.__name__), self._color) + class Array(p.Variable): """A pymbolic variable of known extent.""" @@ -170,6 +185,7 @@ def __init__(self, indices, body): raise FInATSyntaxError("Indices must be a triple of tuples") self.indices = tuple(indices) self.body = body + self._color = "blue" mapper_method = "map_recipe" @@ -225,6 +241,7 @@ def __init__(self, indices, body): self.indices = self.children[0] self.body = self.children[1] + self._color = "blue" def __getinitargs__(self): return self.children @@ -251,6 +268,7 @@ class LeviCivita(StringifyMixin, p._MultiChildExpression): def __init__(self, free, bound, body): self.children = (free, bound, body) + self._color = "blue" def __getinitargs__(self): return self.children @@ -270,6 +288,7 @@ class ForAll(StringifyMixin, p._MultiChildExpression): def __init__(self, indices, body): self.children = (indices, body) + self._color = "blue" def __getinitargs__(self): return self.children @@ -282,6 +301,7 @@ class Wave(StringifyMixin, p._MultiChildExpression): def __init__(self, var, index, base, update, body): self.children = (var, index, base, update, body) + self._color = "blue" def __getinitargs__(self): return self.children @@ -312,6 +332,7 @@ def __init__(self, bindings, body): super(Let, self).__init__((bindings, body)) self.bindings, self.body = self.children + self._color = "blue" mapper_method = "map_let" @@ -336,6 +357,7 @@ def __init__(self, indices, body): "Delta statement requires exactly two indices") super(Delta, self).__init__((indices, body)) + self._color = "blue" def __getinitargs__(self): return self.children @@ -355,6 +377,7 @@ class Inverse(StringifyMixin, p.Expression): """ def __init__(self, expression): self.expression = expression + self._color = "blue" mapper_method = "map_inverse" @@ -368,9 +391,6 @@ class Det(StringifyMixin, p.Expression): def __init__(self, expression): self.expression = expression + self._color = "blue" mapper_method = "map_det" - -class FInATSyntaxError(Exception): - """Exception raised when the syntax rules of the FInAT ast are violated.""" - pass diff --git a/finat/indices.py b/finat/indices.py index f4fef89a8..21206fbd9 100644 --- a/finat/indices.py +++ b/finat/indices.py @@ -13,6 +13,7 @@ def __init__(self, extent, name): self._extent = slice(extent) else: raise TypeError("Extent must be a slice or an int") + self._color = "yellow" @property def extent(self): From 5313c72ba6d705cf69688df219a0e5e978212307 Mon Sep 17 00:00:00 2001 From: David Ham Date: Fri, 21 Nov 2014 17:16:35 +0000 Subject: [PATCH 101/749] Working colorised errors --- finat/ast.py | 27 +++++++++++++++++++-------- finat/indices.py | 11 +++++++++++ finat/interpreter.py | 41 +++++++++++++++++++++++++++++++++++++---- 3 files changed, 67 insertions(+), 12 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index 82da6540e..e7e7b7fab 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -8,7 +8,7 @@ try: from termcolor import colored except ImportError: - colored = lambda string, color: string + colored = lambda string, color, attrs=[]: string class FInATSyntaxError(Exception): @@ -89,7 +89,10 @@ def map_delta(self, expr, *args, **kwargs): *[self.rec(c, *args, **kwargs) for c in expr.children]) def map_index(self, expr, *args, **kwargs): - return colored(str(expr), expr._color) + if hasattr(expr, "_error"): + return colored(str(expr), "red", attrs=["bold"]) + else: + return colored(str(expr), expr._color) def map_wave(self, expr, enclosing_prec, indent=None, *args, **kwargs): if indent is None or enclosing_prec is not PREC_NONE: @@ -130,11 +133,13 @@ def map_det(self, expr, *args, **kwargs): self.rec(expr.expression, *args, **kwargs)) def map_variable(self, expr, enclosing_prec, *args, **kwargs): - try: - return colored(expr.name, expr._color) - except AttributeError: - return colored(expr.name, "cyan") - + if hasattr(expr, "_error"): + return colored(str(expr.name), "red", attrs=["bold"]) + else: + try: + return colored(expr.name, expr._color) + except AttributeError: + return colored(expr.name, "cyan") class StringifyMixin(object): @@ -153,7 +158,13 @@ def stringifier(self): @property def name(self): - return colored(str(self.__class__.__name__), self._color) + if hasattr(self, "_error"): + return colored(str(self.__class__.__name__), "red", attrs=["bold"]) + else: + return colored(str(self.__class__.__name__), self._color) + + def set_error(self): + self._error = True class Array(p.Variable): diff --git a/finat/indices.py b/finat/indices.py index 21206fbd9..cbd197cfc 100644 --- a/finat/indices.py +++ b/finat/indices.py @@ -41,6 +41,9 @@ def __repr__(self): return "%s(%s)" % (self.__class__.__name__, self.name) + def set_error(self): + self._error = True + class PointIndex(IndexBase): '''An index running over a set of points, for example quadrature points.''' @@ -70,6 +73,14 @@ def __init__(self, pointset): self.factors = [PointIndex(f) for f in pointset.factor_sets] + def __getattr__(self, name): + + if name == "_error": + if any([hasattr(x, "_error") for x in self.factors]): + return True + + raise AttributeError + class BasisFunctionIndex(IndexBase): '''An index over a local set of basis functions. diff --git a/finat/interpreter.py b/finat/interpreter.py index 05ea06673..d96070cd1 100644 --- a/finat/interpreter.py +++ b/finat/interpreter.py @@ -38,6 +38,7 @@ def map_variable(self, expr): else: return var except KeyError: + expr.set_error() raise UnknownVariableError(expr.name) def map_recipe(self, expr): @@ -45,11 +46,18 @@ def map_recipe(self, expr): d, b, p = expr.indices - return self.rec(ForAll(d + b + p, expr.body)) + try: + forall = ForAll(d + b + p, expr.body) + return self.rec(forall) + except: + if hasattr(forall, "_error"): + expr.set_error() + raise def map_index_sum(self, expr): indices, body = expr.children + expr_in = expr # Sum over multiple indices recursively. if len(indices) > 1: @@ -60,6 +68,8 @@ def map_index_sum(self, expr): idx = indices[0] if idx in self.indices: + expr_in.set_error() + idx.set_error() raise FInATSyntaxError("Attempting to bind the name %s which is already bound" % idx) e = idx.extent @@ -70,7 +80,12 @@ def map_index_sum(self, expr): for i in self._as_range(e): self.indices[idx] = i - total += self.rec(expr) + try: + total += self.rec(expr) + except: + if hasattr(expr, "_error"): + expr_in.set_error() + raise self.indices.pop(idx) @@ -81,11 +96,13 @@ def map_index(self, expr): try: return self.indices[expr] except KeyError: + expr.set_error() raise FInATSyntaxError("Access to unbound variable name %s." % expr) def map_for_all(self, expr): indices, body = expr.children + expr_in = expr # Deal gracefully with the zero index case. if not indices: @@ -104,6 +121,8 @@ def map_for_all(self, expr): idx = indices[0] if idx in self.indices: + expr_in.set_error() + idx.set_error() raise FInATSyntaxError("Attempting to bind the name %s which is already bound" % idx) e = idx.extent @@ -111,7 +130,12 @@ def map_for_all(self, expr): total = [] for i in self._as_range(e): self.indices[idx] = i - total.append(self.rec(expr)) + try: + total.append(self.rec(expr)) + except: + if hasattr(expr, "_error"): + expr_in.set_error() + raise self.indices.pop(idx) @@ -122,6 +146,8 @@ def map_wave(self, expr): (var, index, base, update, body) = expr.children if index not in self.indices: + expr.set_error() + index.set_error() raise FInATSyntaxError("Wave variable depends on %s, which is not in scope" % index) try: @@ -148,6 +174,8 @@ def map_let(self, expr): for var, value in expr.bindings: if var in self.context: + expr.set_error() + var.set_error() raise FInATSyntaxError("Let variable %s was already in scope." % var.name) self.context[var.name] = self.rec(value) @@ -213,6 +241,7 @@ def map_levi_civita(self, expr): else: return 0 + expr.set_error() raise NotImplementedError @@ -228,4 +257,8 @@ def evaluate(expression, context={}, kernel_data=None): for var in kernel_data.static.values(): context[var[0].name] = var[1]() - return FinatEvaluationMapper(context)(expression) + try: + return FinatEvaluationMapper(context)(expression) + except: + print expression + raise From 41bf730500d6291e7e703812531fb930263fbc4a Mon Sep 17 00:00:00 2001 From: Robert Kirby Date: Fri, 21 Nov 2014 09:35:57 -0600 Subject: [PATCH 102/749] small update on bernstein --- finat/bernstein.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index c3102095c..d0e40278c 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -79,7 +79,7 @@ def mysum(vals): alphas.append(alpha_cur) qs_internal = [PointIndex(qf) for qf in q.points.factor_sets] - + r = kernel_data.new_variable("r") w = kernel_data.new_variable("w") tmps = [kernel_data.new_variable("tmp") for d in range(sd-1)] @@ -127,14 +127,16 @@ def mysum(vals): for d in range(sd-1): qs_cur = qs_per_phase[d+1] + qs_prev = qs_per_phase[d] b_ind = -(d+2) # index into several things counted backward s = 1.0 - xi[b_ind][qs_cur[b_ind]] - recipe_args = ((), - tuple(alphas[:(b_ind+1)]), - tuple(qs_cur[(b_ind+1):])) - - expr = Let(((tmps[d], Recipe(recipe_args, expr)), + + expr = Let(((tmps[d], + Recipe(((), + tuple(alphas[:(b_ind+1)]), + tuple(qs_prev[(b_ind+1):])), + expr)), (r, xi[b_ind][qs_cur[b_ind]]/s)), IndexSum((alphas[b_ind],), Wave(w, @@ -146,6 +148,7 @@ def mysum(vals): ) ) + print Recipe(((), (), (q,)), expr) return Recipe(((), (), (q,)), expr) # if self.cell.get_spatial_dimension() == 1: From 354196bb4e45fcc4fbdadc2ae895cc54153656a5 Mon Sep 17 00:00:00 2001 From: Robert Kirby Date: Fri, 21 Nov 2014 11:28:06 -0600 Subject: [PATCH 103/749] updated general bernstein --- finat/bernstein.py | 213 ++++++++++++++------------------------------- 1 file changed, 64 insertions(+), 149 deletions(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index d0e40278c..b28d82148 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -54,7 +54,6 @@ def pd(sd, d): else: raise NotImplementedError - # Get the symbolic names for the points. xi = [self._points_variable(f.points, kernel_data) for f in q.factors] @@ -65,10 +64,15 @@ def pd(sd, d): sd = self.cell.get_spatial_dimension() - # reimplement using reduce to avoid problem with infinite loop into pymbolic + # reimplement sum using reduce to avoid problem with infinite loop + # into pymbolic def mysum(vals): return reduce(lambda a, b: a+b, vals, 0) + r = kernel_data.new_variable("r") + w = kernel_data.new_variable("w") + tmps = [kernel_data.new_variable("tmp") for d in range(sd-1)] + # Create basis function indices that run over # the possible multiindex space. These have # to be jagged @@ -78,23 +82,22 @@ def mysum(vals): alpha_cur = BasisFunctionIndex(deg+1-asum) alphas.append(alpha_cur) + # temporary quadrature indices so I don't clobber the ones that + # have to be free for the entire recipe qs_internal = [PointIndex(qf) for qf in q.points.factor_sets] - - r = kernel_data.new_variable("r") - w = kernel_data.new_variable("w") - tmps = [kernel_data.new_variable("tmp") for d in range(sd-1)] - # every phase of sum-factorization *except* the last one - # will make use of the qs_internal to index into - # quadrature points, but the last phase will use the - # index set from the point set given as an argument. - # To avoid some if statements, we will create a list of - # what quadrature point indices should be used at which phase - qs_per_phase = [qs_internal]*(sd-1) + [qs] - - # The first phase is different since we read from field_var - # instead of one of the temporaries -- field_var is stored - # by a single index and the rest are stored as tensor products. + # For each phase I need to figure out the free variables of + # that phase + free_vars_per_phase = [] + for d in range(sd-1): + alphas_free_cur = tuple(alphas[:(-1-d)]) + qs_free_cur = tuple(qs_internal[(-1-d):]) + free_vars_per_phase.append(((), alphas_free_cur, qs_free_cur)) + # last phase: the free variables are the free quadrature point indices + free_vars_per_phase.append(((), (), (q,))) + + # the first phase reads from the field_var storage + # the rest of the phases will read from a tmp variable # This code computes the offset into the field_var storage # for the internal sum variable loop. offset = 0 @@ -106,148 +109,60 @@ def mysum(vals): # each phase of the sum-factored algorithm reads from a particular # location. The first of these is field_var, the rest are the # temporaries. - read_locs = [field_var[alphas[-1]+offset]] \ - + [tmps[d][tuple(alphas[:(-d)]+qs_internal[(-d):])] - for d in range(sd-1)] - - # In the first phase of the sum-factorization we don't have a previous - # result to bind, so the Let expression is different. - qs_cur = qs_per_phase[0] - s = 1.0 - xi[-1][qs_cur[-1]] - expr = Let(((r, xi[-1][qs_cur[-1]]/s),), + read_locs = [field_var[alphas[-1]+offset]] + if sd > 1: + # intermediate phases will read from the alphas and + # internal quadrature points + for d in range(1, sd-1): + tmp_cur = tmps[d-1] + read_alphas = alphas[:(-d)] + read_qs = qs_internal[(-d):] + read_locs.append(tmp_cur[tuple(read_alphas+read_qs)]) + + # last phase reads from the last alpha and the incoming quadrature points + read_locs.append(tmps[-1][tuple(alphas[:1]+qs[1:])]) + + # Figure out the "xi" for each phase being used in the recurrence. + # In the last phase, it has to refer to one of the free incoming + # quadrature points, and it refers to the internal ones in previous phases. + xi_per_phase = [xi[d][qs_internal[-d]] for d in range(sd-1)]\ + + [xi[-1][qs[0]]] + + # first phase: no previous phase to bind + xi_cur = xi_per_phase[0] + s = 1 - xi_cur + expr = Let(((r, xi_cur/s),), IndexSum((alphas[-1],), Wave(w, alphas[-1], s**(deg-mysum(alphas[:(sd-1)])), - w * r * (deg-mysum(alphas)) / (1.0 + alphas[-1]), - w * read_locs[0] + w*r*(deg-mysum(alphas))/(1.+alphas[-1]), + w*read_locs[0] ) ) ) + recipe_cur = Recipe(free_vars_per_phase[0], expr) - for d in range(sd-1): - qs_cur = qs_per_phase[d+1] - qs_prev = qs_per_phase[d] - b_ind = -(d+2) # index into several things counted backward - s = 1.0 - xi[b_ind][qs_cur[b_ind]] - - - expr = Let(((tmps[d], - Recipe(((), - tuple(alphas[:(b_ind+1)]), - tuple(qs_prev[(b_ind+1):])), - expr)), - (r, xi[b_ind][qs_cur[b_ind]]/s)), - IndexSum((alphas[b_ind],), + for d in range(1, sd): + # Need to bind the free variables that came before in Let + # then do what I think is right. + xi_cur = xi_per_phase[d] + s = 1 - xi_cur + alpha_cur = alphas[-(d+1)] + asum0 = mysum(alphas[:(sd-d-1)]) + asum1 = mysum(alphas[:(sd-d)]) + + expr = Let(((tmps[d-1], recipe_cur), + (r, xi_cur/s)), + IndexSum((alpha_cur,), Wave(w, - alphas[b_ind], - s**(deg-mysum(alphas[:b_ind])), - w * r * (deg-mysum(alphas[:(b_ind+1)]))/(1.0+alphas[b_ind]), - w * read_locs[d+1] + alpha_cur, + s**(deg-asum0), + w*r*(deg-asum1)/(1.+alpha_cur), + w*read_locs[d] ) ) ) + recipe_cur = Recipe(free_vars_per_phase[d], expr) - print Recipe(((), (), (q,)), expr) - return Recipe(((), (), (q,)), expr) - -# if self.cell.get_spatial_dimension() == 1: -# r = kernel_data.new_variable("r") -# w = kernel_data.new_variable("w") -# alpha = BasisFunctionIndex(self.degree+1) -# s = 1 - xi[0][qs[0]] -# expr = Let(((r, xi[0][qs[0]]/s)), -# IndexSum((alpha,), -# Wave(w, -# alpha, -# s**self.degree, -# w * r * (self.degree-alpha)/(alpha+1.0), -# w * field_var[alpha]) -# ) -# ) -# return Recipe(((), (), (q,)), expr) -# elif self.cell.get_spatial_dimension() == 2: -# deg = self.degree -# r = kernel_data.new_variable("r") -# w = kernel_data.new_variable("w") -# tmp = kernel_data.new_variable("tmp") -# alpha1 = BasisFunctionIndex(deg+1) -# alpha2 = BasisFunctionIndex(deg+1-alpha1) -# q2 = PointIndex(q.points.factor_sets[1]) -# s = 1 - xi[1][q2] - -# tmp_expr = Let(((r, xi[1][q2]/s),), -# IndexSum((alpha2,), -# Wave(w, -# alpha2, -# s**(deg - alpha1), -# w * r * (deg-alpha1-alpha2)/(1.0 + alpha2), -# w * field_var[alpha1*(2*deg-alpha1+3)/2]) -# ) -# ) -# s = 1 - xi[0][qs[0]] -# expr = Let(((tmp, Recipe(((), (alpha1,), (q2,)), tmp_expr)), -# (r, xi[0][qs[0]]/s)), -# IndexSum((alpha1,), -# Wave(w, -# alpha1, -# s**deg, -# w * r * (deg-alpha1)/(1. + alpha1), -# w * tmp[alpha1, qs[1]] -# ) -# ) -# ) -# -# return Recipe(((), (), (q,)), expr) -# elif self.cell.get_spatial_dimension() == 3: -# deg = self.degree -# r = kernel_data.new_variable("r") -# w = kernel_data.new_variable("w") -# tmp0 = kernel_data.new_variable("tmp0") -# tmp1 = kernel_data.new_variable("tmp1") -# alpha1 = BasisFunctionIndex(deg+1) -# alpha2 = BasisFunctionIndex(deg+1-alpha1) -# alpha3 = BasisFunctionIndex(deg+1-alpha1-alpha2) -# q2 = PointIndex(q.points.factor_sets[1]) -# q3 = PointIndex(q.points.factor_sets[2]) -# -# s = 1.0 - xi[2][q3] -# tmp0_expr = Let(((r, xi[2][q3]/s),), -# IndexSum((alpha3,), -# Wave(w, -# alpha3, -# s**(deg-alpha1-alpha2), -# w * r * (deg-alpha1-alpha2-alpha3)/(1.+alpha3), -# w * field_var[pd(3, deg)-pd(3, deg-alpha1) -# + pd(2, deg - alpha1)-pd(2, deg - alpha1 -# alpha2) -# + alpha3] -# ) -# ) -# ) -# s = 1.0 - xi[1][q2] -# tmp1_expr = Let(((tmp0, tmp0_expr), -# (r, xi[1][q2]/s)), -# IndexSum((alpha2,), -# Wave(w, -# alpha2, -# s**(deg-alpha1), -# w*r*(deg-alpha1-alpha2)/(1.0+alpha2), -# w*tmp0[alpha1, alpha2, q3] -# ) -# ) -# ) -# -# s = 1.0 - xi[0][qs[0]] -# expr = Let(((tmp1, tmp1_expr), -# (r, xi[0][qs[0]]/s)), -# IndexSum((alpha1,), -# Wave(w, -# alpha1, -# s**deg, -# w*r*(deg-alpha1)/(1.+alpha1), -# w*tmp1[alpha1, qs[1], qs[2]] -# ) -# ) -# ) -# -# return Recipe(((), (), (q,)), expr) + return recipe_cur From c88ec857ca08fb02bbedc88d2aef8764537307e7 Mon Sep 17 00:00:00 2001 From: Robert Kirby Date: Fri, 21 Nov 2014 12:44:35 -0600 Subject: [PATCH 104/749] more nd things. --- finat/bernstein.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index b28d82148..0262ad108 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -125,8 +125,8 @@ def mysum(vals): # Figure out the "xi" for each phase being used in the recurrence. # In the last phase, it has to refer to one of the free incoming # quadrature points, and it refers to the internal ones in previous phases. - xi_per_phase = [xi[d][qs_internal[-d]] for d in range(sd-1)]\ - + [xi[-1][qs[0]]] + xi_per_phase = [xi[-(d+1)][qs_internal[-(d+1)]] for d in range(sd-1)]\ + + [xi[0][qs[0]]] # first phase: no previous phase to bind xi_cur = xi_per_phase[0] From b22e355dba6f94620d432325130d4a236d17d45e Mon Sep 17 00:00:00 2001 From: Robert Kirby Date: Fri, 21 Nov 2014 13:21:27 -0600 Subject: [PATCH 105/749] 2d works --- finat/bernstein.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index 0262ad108..a2a62c342 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -136,7 +136,7 @@ def mysum(vals): Wave(w, alphas[-1], s**(deg-mysum(alphas[:(sd-1)])), - w*r*(deg-mysum(alphas))/(1.+alphas[-1]), + w*r*(deg-mysum(alphas)+1)/(alphas[-1]), w*read_locs[0] ) ) @@ -158,7 +158,7 @@ def mysum(vals): Wave(w, alpha_cur, s**(deg-asum0), - w*r*(deg-asum1)/(1.+alpha_cur), + w*r*(deg-asum1+1)/alpha_cur, w*read_locs[d] ) ) From 9131e67d58bcbc558f7b50ef836a15fe51a6fc35 Mon Sep 17 00:00:00 2001 From: David Ham Date: Fri, 21 Nov 2014 19:56:19 +0000 Subject: [PATCH 106/749] remove spurious comma --- finat/ast.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index e7e7b7fab..32858dd6d 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -96,12 +96,12 @@ def map_index(self, expr, *args, **kwargs): def map_wave(self, expr, enclosing_prec, indent=None, *args, **kwargs): if indent is None or enclosing_prec is not PREC_NONE: - fmt = expr.name + "(%s, %s) " + fmt = expr.name + "(%s %s) " else: oldidt = " " * indent indent += 4 idt = " " * indent - fmt = expr.name + "(%s,\n" + idt + "%s\n" + oldidt + ")" + fmt = expr.name + "(%s\n" + idt + "%s\n" + oldidt + ")" return self.format(fmt, " ".join(self.rec(c, PREC_NONE, *args, **kwargs) + "," for c in expr.children[:-1]), From 307817dc2bd634cb2c44751bc1e1546738446797 Mon Sep 17 00:00:00 2001 From: David Ham Date: Fri, 21 Nov 2014 20:19:35 +0000 Subject: [PATCH 107/749] pep8 --- finat/bernstein.py | 62 +++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index a2a62c342..eef7730c6 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -46,11 +46,11 @@ def field_evaluation(self, field_var, q, kernel_data, derivative=None): def pd(sd, d): if sd == 3: - return (d+1)*(d+2)*(d+3)/6 + return (d + 1) * (d + 2) * (d + 3) / 6 elif sd == 2: - return (d+1)*(d+2)/2 + return (d + 1) * (d + 2) / 2 elif sd == 1: - return d+1 + return d + 1 else: raise NotImplementedError @@ -67,19 +67,19 @@ def pd(sd, d): # reimplement sum using reduce to avoid problem with infinite loop # into pymbolic def mysum(vals): - return reduce(lambda a, b: a+b, vals, 0) + return reduce(lambda a, b: a + b, vals, 0) r = kernel_data.new_variable("r") w = kernel_data.new_variable("w") - tmps = [kernel_data.new_variable("tmp") for d in range(sd-1)] + tmps = [kernel_data.new_variable("tmp") for d in range(sd - 1)] # Create basis function indices that run over # the possible multiindex space. These have # to be jagged - alphas = [BasisFunctionIndex(deg+1)] + alphas = [BasisFunctionIndex(deg + 1)] for d in range(1, sd): asum = mysum(alphas) - alpha_cur = BasisFunctionIndex(deg+1-asum) + alpha_cur = BasisFunctionIndex(deg + 1 - asum) alphas.append(alpha_cur) # temporary quadrature indices so I don't clobber the ones that @@ -89,9 +89,9 @@ def mysum(vals): # For each phase I need to figure out the free variables of # that phase free_vars_per_phase = [] - for d in range(sd-1): - alphas_free_cur = tuple(alphas[:(-1-d)]) - qs_free_cur = tuple(qs_internal[(-1-d):]) + for d in range(sd - 1): + alphas_free_cur = tuple(alphas[:(-1 - d)]) + qs_free_cur = tuple(qs_internal[(-1 - d):]) free_vars_per_phase.append(((), alphas_free_cur, qs_free_cur)) # last phase: the free variables are the free quadrature point indices free_vars_per_phase.append(((), (), (q,))) @@ -101,43 +101,43 @@ def mysum(vals): # This code computes the offset into the field_var storage # for the internal sum variable loop. offset = 0 - for d in range(sd-1): + for d in range(sd - 1): deg_begin = deg - mysum(alphas[:d]) deg_end = deg - alphas[d] - offset += pd(sd-d, deg_begin) - pd(sd-d, deg_end) + offset += pd(sd - d, deg_begin) - pd(sd - d, deg_end) # each phase of the sum-factored algorithm reads from a particular # location. The first of these is field_var, the rest are the # temporaries. - read_locs = [field_var[alphas[-1]+offset]] + read_locs = [field_var[alphas[-1] + offset]] if sd > 1: # intermediate phases will read from the alphas and # internal quadrature points - for d in range(1, sd-1): - tmp_cur = tmps[d-1] + for d in range(1, sd - 1): + tmp_cur = tmps[d - 1] read_alphas = alphas[:(-d)] read_qs = qs_internal[(-d):] - read_locs.append(tmp_cur[tuple(read_alphas+read_qs)]) + read_locs.append(tmp_cur[tuple(read_alphas + read_qs)]) # last phase reads from the last alpha and the incoming quadrature points - read_locs.append(tmps[-1][tuple(alphas[:1]+qs[1:])]) + read_locs.append(tmps[-1][tuple(alphas[:1] + qs[1:])]) # Figure out the "xi" for each phase being used in the recurrence. # In the last phase, it has to refer to one of the free incoming # quadrature points, and it refers to the internal ones in previous phases. - xi_per_phase = [xi[-(d+1)][qs_internal[-(d+1)]] for d in range(sd-1)]\ - + [xi[0][qs[0]]] + xi_per_phase = [xi[-(d + 1)][qs_internal[-(d + 1)]] + for d in range(sd - 1)] + [xi[0][qs[0]]] # first phase: no previous phase to bind xi_cur = xi_per_phase[0] s = 1 - xi_cur - expr = Let(((r, xi_cur/s),), + expr = Let(((r, xi_cur / s),), IndexSum((alphas[-1],), Wave(w, alphas[-1], - s**(deg-mysum(alphas[:(sd-1)])), - w*r*(deg-mysum(alphas)+1)/(alphas[-1]), - w*read_locs[0] + s ** (deg - mysum(alphas[:(sd - 1)])), + w * r * (deg - mysum(alphas) + 1) / (alphas[-1]), + w * read_locs[0] ) ) ) @@ -148,18 +148,18 @@ def mysum(vals): # then do what I think is right. xi_cur = xi_per_phase[d] s = 1 - xi_cur - alpha_cur = alphas[-(d+1)] - asum0 = mysum(alphas[:(sd-d-1)]) - asum1 = mysum(alphas[:(sd-d)]) + alpha_cur = alphas[-(d + 1)] + asum0 = mysum(alphas[:(sd - d - 1)]) + asum1 = mysum(alphas[:(sd - d)]) - expr = Let(((tmps[d-1], recipe_cur), - (r, xi_cur/s)), + expr = Let(((tmps[d - 1], recipe_cur), + (r, xi_cur / s)), IndexSum((alpha_cur,), Wave(w, alpha_cur, - s**(deg-asum0), - w*r*(deg-asum1+1)/alpha_cur, - w*read_locs[d] + s ** (deg - asum0), + w * r * (deg - asum1 + 1) / alpha_cur, + w * read_locs[d] ) ) ) From a7b02235994652e1065a855fca0572a22039355d Mon Sep 17 00:00:00 2001 From: Robert Kirby Date: Wed, 7 Jan 2015 13:19:16 -0600 Subject: [PATCH 108/749] Added graded bf index, modified Bernstein's field evaluation to use it. Tests still seem to pass --- finat/bernstein.py | 15 +++++++++------ finat/indices.py | 26 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index eef7730c6..b932bf84f 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -2,7 +2,7 @@ from points import StroudPointSet from ast import ForAll, Recipe, Wave, Let, IndexSum import pymbolic.primitives as p -from indices import BasisFunctionIndex, PointIndex +from indices import BasisFunctionIndex, PointIndex, SimpliciallyGradedBasisFunctionIndex # noqa import numpy as np @@ -76,11 +76,14 @@ def mysum(vals): # Create basis function indices that run over # the possible multiindex space. These have # to be jagged - alphas = [BasisFunctionIndex(deg + 1)] - for d in range(1, sd): - asum = mysum(alphas) - alpha_cur = BasisFunctionIndex(deg + 1 - asum) - alphas.append(alpha_cur) + + alpha = SimpliciallyGradedBasisFunctionIndex(sd, deg) + alphas = alpha.factors +# alphas = [BasisFunctionIndex(deg + 1)] +# for d in range(1, sd): +# asum = mysum(alphas) +# alpha_cur = BasisFunctionIndex(deg + 1 - asum) +# alphas.append(alpha_cur) # temporary quadrature indices so I don't clobber the ones that # have to be free for the entire recipe diff --git a/finat/indices.py b/finat/indices.py index cbd197cfc..8db395f73 100644 --- a/finat/indices.py +++ b/finat/indices.py @@ -95,6 +95,32 @@ def __init__(self, extent): _count = 0 +class SimpliciallyGradedBasisFunctionIndex(BasisFunctionIndex): + '''An index over simplicial polynomials with a grade, such + as Dubiner or Bernstein. Implies a simplicial iteration space.''' + def __init__(self, sdim, deg): + + # creates name and increments counter + super(SimpliciallyGradedBasisFunctionIndex, self).__init__(-1) + + self.factors = [BasisFunctionIndex(deg + 1)] + + def mysum(vals): + return reduce(lambda a, b: a + b, vals, 0) + + for sd in range(1, sdim): + acur = BasisFunctionIndex(deg + 1 - mysum(self.factors)) + self.factors.append(acur) + + def __getattr__(self, name): + + if name == "_error": + if any([hasattr(x, "_error") for x in self.factors]): + return True + + raise AttributeError + + class DimensionIndex(IndexBase): '''An index over data dimension. For example over topological, geometric or vector components.''' From 2f34c8d2df9bd6b542fa81c2cdb97fe58199a6ff Mon Sep 17 00:00:00 2001 From: Robert Kirby Date: Fri, 16 Jan 2015 15:36:38 -0600 Subject: [PATCH 109/749] Major work toward bernstein moments, including some new indices. But the interpreter doesn't like something yet. --- finat/bernstein.py | 135 ++++++++++++++++++++++++++++++++++++++------- finat/indices.py | 2 +- finat/utils.py | 17 ++++++ 3 files changed, 133 insertions(+), 21 deletions(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index b932bf84f..343f3da39 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -6,6 +6,23 @@ import numpy as np +# reimplement sum using reduce to avoid problem with infinite loop +# into pymbolic +def mysum(vals): + return reduce(lambda a, b: a + b, vals, 0) + + +def pd(sd, d): + if sd == 3: + return (d + 1) * (d + 2) * (d + 3) / 6 + elif sd == 2: + return (d + 1) * (d + 2) / 2 + elif sd == 1: + return d + 1 + else: + raise NotImplementedError + + class Bernstein(FiniteElementBase): """Scalar-valued Bernstein element. Note: need to work out the correct heirarchy for different Bernstein elements.""" @@ -29,6 +46,19 @@ def _points_variable(self, points, kernel_data): return xi + def _weights_variable(self, weights, kernel_data): + """Return the symbolic variables for the static data + corresponding to the array of weights in a quadrature rule.""" + + static_key = (id(weights),) + if static_key in kernel_data.static: + wt = kernel_data.static[static_key][0] + else: + wt = p.Variable(kernel_data.weight_variable_name(weights)) + kernel_data.static[static_key] = (wt, lambda: np.array(weights)) + + return wt + @property def dofs_shape(self): @@ -44,16 +74,6 @@ def field_evaluation(self, field_var, q, kernel_data, derivative=None): if derivative is not None: raise NotImplementedError - def pd(sd, d): - if sd == 3: - return (d + 1) * (d + 2) * (d + 3) / 6 - elif sd == 2: - return (d + 1) * (d + 2) / 2 - elif sd == 1: - return d + 1 - else: - raise NotImplementedError - # Get the symbolic names for the points. xi = [self._points_variable(f.points, kernel_data) for f in q.factors] @@ -64,13 +84,10 @@ def pd(sd, d): sd = self.cell.get_spatial_dimension() - # reimplement sum using reduce to avoid problem with infinite loop - # into pymbolic - def mysum(vals): - return reduce(lambda a, b: a + b, vals, 0) - r = kernel_data.new_variable("r") w = kernel_data.new_variable("w") +# r = p.Variable("r") +# w = p.Variable("w") tmps = [kernel_data.new_variable("tmp") for d in range(sd - 1)] # Create basis function indices that run over @@ -79,11 +96,6 @@ def mysum(vals): alpha = SimpliciallyGradedBasisFunctionIndex(sd, deg) alphas = alpha.factors -# alphas = [BasisFunctionIndex(deg + 1)] -# for d in range(1, sd): -# asum = mysum(alphas) -# alpha_cur = BasisFunctionIndex(deg + 1 - asum) -# alphas.append(alpha_cur) # temporary quadrature indices so I don't clobber the ones that # have to be free for the entire recipe @@ -169,3 +181,86 @@ def mysum(vals): recipe_cur = Recipe(free_vars_per_phase[d], expr) return recipe_cur + + def moment_evaluation(self, value, weights, q, kernel_data, + derivative=None, pullback=None): + if not isinstance(q.points, StroudPointSet): + raise ValueError("Only Stroud points may be employed with Bernstein polynomials") + + if derivative is not None: + raise NotImplementedError + + qs = q.factors + + wt = [self._weights_variable(weights[d], kernel_data) + for d in range(len(weights))] + + xi = [self._points_variable(f.points, kernel_data) + for f in q.factors] + + deg = self.degree + + sd = self.cell.get_spatial_dimension() + + r = kernel_data.new_variable("r") + w = kernel_data.new_variable("w") + tmps = [kernel_data.new_variable("tmp") for d in range(sd - 1)] + + # the output recipe is parameterized over these + alpha = SimpliciallyGradedBasisFunctionIndex(sd, deg) + alphas = alpha.factors + + read_locs = [value[q]] + for d in range(1, sd-1): + tmp_cur = tmps[d-1] + read_alphas = alphas[:d] + read_qs = qs[-d:] + read_locs.append(tmp_cur[tuple(read_alphas+read_qs)]) + d = sd-1 + tmp_cur = tmps[d-1] + read_alphas = alphas[:d] + read_qs = qs[-d:] + read_locs.append(tmp_cur[tuple(read_alphas+read_qs)]) + + free_vars_per_phase = [] + for d in range(1, sd): + alphas_free_cur = tuple(alphas[:d]) + qs_free_cur = tuple(qs[-d:]) + free_vars_per_phase.append(((), alphas_free_cur, qs_free_cur)) + free_vars_per_phase.append(((), (), (alpha,))) + + xi_cur = xi[0][qs[0]] + s = 1 - xi_cur + expr = Let(((r, xi_cur/s),), + IndexSum((qs[0],), + Wave(w, + alphas[0], + wt[0][qs[0]] * s**deg, + w*r*(deg-alphas[0]-1)/alphas[0], + w * read_locs[0] + ) + ) + ) + + recipe_cur = Recipe(free_vars_per_phase[0], expr) + + for d in range(1, sd): + xi_cur = xi[d] + s = 1 - xi_cur + acur = alphas[d] + asum0 = mysum(alphas[:d]) + asum1 = asum0 - acur + expr = Let(((tmps[d-1], recipe_cur), + (r, xi_cur/s)), + IndexSum((qs[d],), + Wave(w, + acur, + wt[d][qs[d]]*s**(deg-asum0), + w*r*(deg-asum1-1)/acur, + w*read_locs[d] + ) + ) + ) + recipe_cur = Recipe(free_vars_per_phase[d], expr) + + return recipe_cur diff --git a/finat/indices.py b/finat/indices.py index 8db395f73..d60b7b9b3 100644 --- a/finat/indices.py +++ b/finat/indices.py @@ -120,7 +120,7 @@ def __getattr__(self, name): raise AttributeError - + class DimensionIndex(IndexBase): '''An index over data dimension. For example over topological, geometric or vector components.''' diff --git a/finat/utils.py b/finat/utils.py index a8ac7ff01..d8f848c0a 100644 --- a/finat/utils.py +++ b/finat/utils.py @@ -38,6 +38,7 @@ def __init__(self, coordinate_element, coordinate_var=None, affine=None): self._variable_count = 0 self._point_count = 0 + self._wt_count = 0 self._variable_cache = {} def tabulation_variable_name(self, element, points): @@ -74,6 +75,22 @@ def point_variable_name(self, points): self.variables.add(name) return self._variable_cache[key] + def weight_variable_name(self, weights): + """Given an iterable of weights set, return a variable name wt_n + where n is guaranteed to be unique to that set of weights.""" + + key = (id(weights),) + + try: + return self._variable_cache[key] + except KeyError: + name = u'\u03BE_'.encode("utf-8") \ + + str(self._wt_count) + self._variable_cache[key] = name + self._wt_count += 1 + self.variables.add(name) + return self._variable_cache[key] + def new_variable(self, prefix=None): """Create a variable guaranteed to be unique in the kernel context.""" name = prefix or "tmp" From b031068fab003abedff289474ca759187c6e0e33 Mon Sep 17 00:00:00 2001 From: Robert Kirby Date: Tue, 27 Jan 2015 21:58:51 -0600 Subject: [PATCH 110/749] Added preliminary facility for printing finat ast to dot format. --- finat/ast.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/finat/ast.py b/finat/ast.py index 32858dd6d..0c4f5d014 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -4,6 +4,8 @@ import pymbolic.primitives as p from pymbolic.mapper import IdentityMapper as IM from pymbolic.mapper.stringifier import StringifyMapper, PREC_NONE +from pymbolic.mapper import WalkMapper as WM +from pymbolic.mapper.graphviz import GraphvizMapper as GVM from indices import IndexBase try: from termcolor import colored @@ -142,6 +144,41 @@ def map_variable(self, expr, enclosing_prec, *args, **kwargs): return colored(expr.name, "cyan") +class WalkMapper(WM): + def __init__(self): + super(WalkMapper, self).__init__() + + def map_recipe(self, expr, *args, **kwargs): + if not self.visit(expr, *args, **kwargs): + return + for indices in expr.indices: + for index in indices: + self.rec(index, *args, **kwargs) + self.rec(expr.body, *args, **kwargs) + self.post_visit(expr, *args, **kwargs) + + def map_index(self, expr, *args, **kwargs): + if not self.visit(expr, *args, **kwargs): + return + + # I don't want to recur on the extent. That's ugly. + + self.post_visit(expr, *args, **kwargs) + + map_delta = map_recipe + map_let = map_recipe + map_for_all = map_recipe + map_wave = map_recipe + map_index_sum = map_recipe + map_levi_civita = map_recipe + map_inverse = map_recipe + map_det = map_recipe + + +class GraphvizMapper(WalkMapper, GVM): + pass + + class StringifyMixin(object): """Mixin class to set stringification options correctly for pymbolic subclasses.""" From 8e2f2b53e18fbd402ba95f83afe618c78dc81d9e Mon Sep 17 00:00:00 2001 From: Robert Kirby Date: Wed, 28 Jan 2015 13:32:19 -0600 Subject: [PATCH 111/749] bug fix in WalkMapper --- finat/ast.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index 0c4f5d014..8b29ad4fa 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -165,14 +165,21 @@ def map_index(self, expr, *args, **kwargs): self.post_visit(expr, *args, **kwargs) - map_delta = map_recipe - map_let = map_recipe - map_for_all = map_recipe - map_wave = map_recipe - map_index_sum = map_recipe - map_levi_civita = map_recipe - map_inverse = map_recipe - map_det = map_recipe + def map_index_sum(self, expr, *args, **kwargs): + if not self.visit(expr, *args, **kwargs): + return + for index in expr.indices: + self.rec(index, *args, **kwargs) + self.rec(expr.body, *args, **kwargs) + self.post_visit(expr, *args, **kwargs) + + map_delta = map_index_sum + map_let = map_index_sum + map_for_all = map_index_sum + map_wave = map_index_sum + map_levi_civita = map_index_sum + map_inverse = map_index_sum + map_det = map_index_sum class GraphvizMapper(WalkMapper, GVM): From 90accbd6505f79b17f0ee1489d16fe47cf1d666c Mon Sep 17 00:00:00 2001 From: David A Ham Date: Fri, 30 Jan 2015 15:49:03 +0000 Subject: [PATCH 112/749] pep8 --- finat/ast.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index 500399b4b..904c8d47d 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -51,9 +51,9 @@ def map_recipe(self, expr, enclosing_prec, indent=None, *args, **kwargs): if indent is None: fmt = "Recipe(%s, %s)" else: - oldidt = " "*indent + oldidt = " " * indent indent += 4 - idt = " "*indent + idt = " " * indent fmt = "Recipe(%s,\n" + idt + "%s\n" + oldidt + ")" return self.format(fmt, @@ -64,9 +64,9 @@ def map_let(self, expr, enclosing_prec, indent=None, *args, **kwargs): if indent is None: fmt = "Let(%s, %s)" else: - oldidt = " "*indent + oldidt = " " * indent indent += 4 - idt = " "*indent + idt = " " * indent fmt = "Let(\n" + idt + "%s,\n" + idt + "%s\n" + oldidt + ")" return self.format(fmt, @@ -81,9 +81,9 @@ def map_index_sum(self, expr, enclosing_prec, indent=None, *args, **kwargs): if indent is None or enclosing_prec is not PREC_NONE: fmt = "IndexSum((%s), %s) " else: - oldidt = " "*indent + oldidt = " " * indent indent += 4 - idt = " "*indent + idt = " " * indent fmt = "IndexSum((%s),\n" + idt + "%s\n" + oldidt + ")" return self.format(fmt, @@ -288,9 +288,8 @@ def __init__(self, bindings, body): raise FInATSyntaxError("Let bindings must be a tuple of pairs") super(Let, self).__init__((bindings, body)) - - self.bindings, self.body = self.children + self.bindings, self.body = self.children def __str__(self): return "Let(%s)" % self.children From aaf955836afe57688c4f067f344b31f85e6a9e3e Mon Sep 17 00:00:00 2001 From: David A Ham Date: Sat, 31 Jan 2015 13:31:57 +0000 Subject: [PATCH 113/749] PEP8 --- finat/bernstein.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index 343f3da39..1d6359022 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -24,7 +24,7 @@ def pd(sd, d): class Bernstein(FiniteElementBase): - """Scalar-valued Bernstein element. Note: need to work out the + """Scalar - valued Bernstein element. Note: need to work out the correct heirarchy for different Bernstein elements.""" def __init__(self, cell, degree): @@ -121,7 +121,7 @@ def field_evaluation(self, field_var, q, kernel_data, derivative=None): deg_end = deg - alphas[d] offset += pd(sd - d, deg_begin) - pd(sd - d, deg_end) - # each phase of the sum-factored algorithm reads from a particular + # each phase of the sum - factored algorithm reads from a particular # location. The first of these is field_var, the rest are the # temporaries. read_locs = [field_var[alphas[-1] + offset]] @@ -211,16 +211,16 @@ def moment_evaluation(self, value, weights, q, kernel_data, alphas = alpha.factors read_locs = [value[q]] - for d in range(1, sd-1): - tmp_cur = tmps[d-1] + for d in range(1, sd - 1): + tmp_cur = tmps[d - 1] read_alphas = alphas[:d] read_qs = qs[-d:] - read_locs.append(tmp_cur[tuple(read_alphas+read_qs)]) - d = sd-1 - tmp_cur = tmps[d-1] + read_locs.append(tmp_cur[tuple(read_alphas + read_qs)]) + d = sd - 1 + tmp_cur = tmps[d - 1] read_alphas = alphas[:d] read_qs = qs[-d:] - read_locs.append(tmp_cur[tuple(read_alphas+read_qs)]) + read_locs.append(tmp_cur[tuple(read_alphas + read_qs)]) free_vars_per_phase = [] for d in range(1, sd): @@ -231,12 +231,12 @@ def moment_evaluation(self, value, weights, q, kernel_data, xi_cur = xi[0][qs[0]] s = 1 - xi_cur - expr = Let(((r, xi_cur/s),), + expr = Let(((r, xi_cur / s),), IndexSum((qs[0],), Wave(w, alphas[0], - wt[0][qs[0]] * s**deg, - w*r*(deg-alphas[0]-1)/alphas[0], + wt[0][qs[0]] * s ** deg, + w * r * (deg - alphas[0] - 1) / alphas[0], w * read_locs[0] ) ) @@ -250,14 +250,14 @@ def moment_evaluation(self, value, weights, q, kernel_data, acur = alphas[d] asum0 = mysum(alphas[:d]) asum1 = asum0 - acur - expr = Let(((tmps[d-1], recipe_cur), - (r, xi_cur/s)), + expr = Let(((tmps[d - 1], recipe_cur), + (r, xi_cur / s)), IndexSum((qs[d],), Wave(w, acur, - wt[d][qs[d]]*s**(deg-asum0), - w*r*(deg-asum1-1)/acur, - w*read_locs[d] + wt[d][qs[d]] * s ** (deg - asum0), + w * r * (deg - asum1 - 1) / acur, + w * read_locs[d] ) ) ) From 4c5a94ad9f88da18bd640745fc34755fa248bff1 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Sat, 31 Jan 2015 13:50:29 +0000 Subject: [PATCH 114/749] fix merge --- finat/ast.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index 461e6a1c7..c876bc3de 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -385,11 +385,8 @@ def __init__(self, bindings, body): raise FInATSyntaxError("Let bindings must be a tuple of pairs") super(Let, self).__init__((bindings, body)) -<<<<<<< HEAD -======= self.bindings, self.body = self.children ->>>>>>> master self.bindings, self.body = self.children self._color = "blue" From e5c5b92a38edfbcec642c40123a76b4484451c70 Mon Sep 17 00:00:00 2001 From: Robert Kirby Date: Tue, 17 Feb 2015 09:49:43 -0600 Subject: [PATCH 115/749] few fixes to graphviz --- finat/ast.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index 8b29ad4fa..cb2ba697d 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -173,10 +173,24 @@ def map_index_sum(self, expr, *args, **kwargs): self.rec(expr.body, *args, **kwargs) self.post_visit(expr, *args, **kwargs) + def map_let(self, expr, *args, **kwargs): + if not self.visit(expr, *args, **kwargs): + return + for binding in expr.bindings: + for b in binding: + self.rec(b, *args, **kwargs) + self.rec(expr.body, *args, **kwargs) + self.post_visit(expr, *args, **kwargs) + + def map_wave(self, expr, *args, **kwargs): + if not self.visit(expr, *args, **kwargs): + return + for child in expr.children: + self.rec(child, *args, **kwargs) + self.post_visit(expr, *args, **kwargs) + map_delta = map_index_sum - map_let = map_index_sum map_for_all = map_index_sum - map_wave = map_index_sum map_levi_civita = map_index_sum map_inverse = map_index_sum map_det = map_index_sum From 7970a29f1bb6d18c480b5c9abc343aa08b2bf272 Mon Sep 17 00:00:00 2001 From: Robert Kirby Date: Thu, 19 Feb 2015 13:48:07 -0600 Subject: [PATCH 116/749] work on basic 2d moment evaluation --- finat/bernstein.py | 47 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index 1d6359022..6170693cd 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -206,6 +206,51 @@ def moment_evaluation(self, value, weights, q, kernel_data, w = kernel_data.new_variable("w") tmps = [kernel_data.new_variable("tmp") for d in range(sd - 1)] + if sd == 2: + alpha = SimpliciallyGradedBasisFunctionIndex(sd, deg) + alphas = alpha.factors + xi_cur = xi[0] + s = 1 - xi_cur + expr0 = Let(((r, xi_cur / s), ), + IndexSum((qs[0], ), + Wave(w, + alphas[0], + wt[0][qs[0]] * (s**deg), + w * r * (deg - alphas[0]) / alphas[0], + w * value[qs[0], qs[1]]) + ) + ) + return Recipe(((), (alphas[0], ), (qs[1], )), + expr0) + + else: + raise NotImplementedError + + + def moment_evaluation_general(self, value, weights, q, kernel_data, + derivative=None, pullback=None): + if not isinstance(q.points, StroudPointSet): + raise ValueError("Only Stroud points may be employed with Bernstein polynomials") + + if derivative is not None: + raise NotImplementedError + + qs = q.factors + + wt = [self._weights_variable(weights[d], kernel_data) + for d in range(len(weights))] + + xi = [self._points_variable(f.points, kernel_data) + for f in q.factors] + + deg = self.degree + + sd = self.cell.get_spatial_dimension() + + r = kernel_data.new_variable("r") + w = kernel_data.new_variable("w") + tmps = [kernel_data.new_variable("tmp") for d in range(sd - 1)] + # the output recipe is parameterized over these alpha = SimpliciallyGradedBasisFunctionIndex(sd, deg) alphas = alpha.factors @@ -227,7 +272,7 @@ def moment_evaluation(self, value, weights, q, kernel_data, alphas_free_cur = tuple(alphas[:d]) qs_free_cur = tuple(qs[-d:]) free_vars_per_phase.append(((), alphas_free_cur, qs_free_cur)) - free_vars_per_phase.append(((), (), (alpha,))) + free_vars_per_phase.append(((), (), tuple(alphas))) xi_cur = xi[0][qs[0]] s = 1 - xi_cur From a0ccf07d8ddde3e66bc22e0be7cb593604ea650e Mon Sep 17 00:00:00 2001 From: David A Ham Date: Fri, 20 Feb 2015 16:44:18 +0000 Subject: [PATCH 117/749] Deal with more complex wave cases. The wave variable was updating every time we passed through the Wave, not just when the index on which it depends changes. This commit fixes this. --- finat/interpreter.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/finat/interpreter.py b/finat/interpreter.py index d96070cd1..7d7391cc8 100644 --- a/finat/interpreter.py +++ b/finat/interpreter.py @@ -78,6 +78,9 @@ def map_index_sum(self, expr): self.indices[idx] = None + # Clear the set of variables dependent on this index. + self.wave_vars[idx] = {} + for i in self._as_range(e): self.indices[idx] = i try: @@ -128,6 +131,8 @@ def map_for_all(self, expr): e = idx.extent total = [] + # Clear the set of variables dependent on this index. + self.wave_vars[idx] = {} for i in self._as_range(e): self.indices[idx] = i try: @@ -151,22 +156,23 @@ def map_wave(self, expr): raise FInATSyntaxError("Wave variable depends on %s, which is not in scope" % index) try: - self.context[var.name] = self.wave_vars[var.name] - self.context[var.name] = self.rec(update) + index_val = self.rec(index) + self.context[var.name], old_val = self.wave_vars[index][var.name] + # Check for index update. + if index_val != old_val: + self.context[var.name] = self.rec(update) except KeyError: # We're at the start of the loop over index. - assert self.rec(index) == (index.extent.start or 0) + assert index_val == (index.extent.start or 0) self.context[var.name] = self.rec(base) - self.wave_vars[var.name] = self.context[var.name] + self.wave_vars[index][var.name] = self.context[var.name], index_val # Execute the body. result = self.rec(body) # Remove the wave variable from scope. self.context.pop(var.name) - if self.rec(index) >= self.rec(index.extent.stop) - 1: - self.wave_vars.pop(var.name) return result From f2bedcdf6c7957d9cd2b258db2028f36b61df238 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Mon, 16 Feb 2015 13:34:57 +0000 Subject: [PATCH 118/749] shut pep8 up --- finat/bernstein.py | 4 ++-- finat/finiteelementbase.py | 4 ++-- finat/gauss_jacobi.py | 8 ++++---- finat/geometry_mapper.py | 6 ++---- finat/hcurl.py | 16 ++++++++-------- finat/hdiv.py | 12 ++++++------ finat/indices.py | 4 ++-- finat/utils.py | 10 +++++----- 8 files changed, 31 insertions(+), 33 deletions(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index 1d6359022..38a6cc801 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -64,8 +64,8 @@ def dofs_shape(self): degree = self.degree dim = self.cell.get_spatial_dimension() - return (int(np.prod(xrange(degree + 1, degree + 1 + dim)) - / np.prod(xrange(1, dim + 1))),) + return (int(np.prod(xrange(degree + 1, degree + 1 + dim)) / + np.prod(xrange(1, dim + 1))),) def field_evaluation(self, field_var, q, kernel_data, derivative=None): if not isinstance(q.points, StroudPointSet): diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 5c0575ba4..8b81188ea 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -177,8 +177,8 @@ def _tabulated_basis(self, points, kernel_data, derivative): if static_key in kernel_data.static: phi = kernel_data.static[static_key][0] else: - phi = p.Variable(("d" if derivative else "") - + kernel_data.tabulation_variable_name(self, points)) + phi = p.Variable(("d" if derivative else "") + + kernel_data.tabulation_variable_name(self, points)) data = self._tabulate(points, derivative) kernel_data.static[static_key] = (phi, lambda: data) diff --git a/finat/gauss_jacobi.py b/finat/gauss_jacobi.py index 3fb02a9b9..cf24682ba 100644 --- a/finat/gauss_jacobi.py +++ b/finat/gauss_jacobi.py @@ -1,4 +1,8 @@ """Basic tools for getting Gauss points and quadrature rules.""" +import math +from math import factorial +import numpy + __copyright__ = "Copyright (C) 2014 Robert C. Kirby" __license__ = """ Permission is hereby granted, free of charge, to any person obtaining a copy @@ -20,10 +24,6 @@ THE SOFTWARE. """ -import math -from math import factorial -import numpy - def compute_gauss_jacobi_points(a, b, m): """Computes the m roots of P_{m}^{a,b} on [-1,1] by Newton's method. diff --git a/finat/geometry_mapper.py b/finat/geometry_mapper.py index afaa6947a..ecac03798 100644 --- a/finat/geometry_mapper.py +++ b/finat/geometry_mapper.py @@ -75,10 +75,8 @@ def _bind_geometry(self, q, body): element = kd.coordinate_element J = element.field_evaluation(phi_x, q, kd, grad, pullback=False) - inner_lets = (((kd.detJ, Det(kd.J)),) - if kd.detJ in self.local_geometry else () - + ((kd.invJ, Inverse(kd.J)),) - if kd.invJ in self.local_geometry else ()) + inner_lets = (((kd.detJ, Det(kd.J)),) if kd.detJ in self.local_geometry else () + + ((kd.invJ, Inverse(kd.J)),) if kd.invJ in self.local_geometry else ()) # The local geometry goes out of scope at this point. self.local_geometry = set() diff --git a/finat/hcurl.py b/finat/hcurl.py index 47d5d43ed..3a921ecfc 100644 --- a/finat/hcurl.py +++ b/finat/hcurl.py @@ -36,8 +36,8 @@ def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): if pullback: beta = tIndex() gamma = gIndex() - expr = IndexSum((gamma,), invJ()[alpha, gamma] * invJ()[beta, gamma] - * phi[(alpha, beta, i, q)]) + expr = IndexSum((gamma,), invJ()[alpha, gamma] * invJ()[beta, gamma] * + phi[(alpha, beta, i, q)]) else: expr = IndexSum((alpha,), phi[(alpha, alpha, i, q)]) ind = ((), (i,), (q,)) @@ -46,8 +46,8 @@ def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): beta = tIndex() gamma = gIndex() delta = gIndex() - expr = IndexSum((alpha, beta), invJ()[alpha, gamma] * invJ()[beta, delta] - * phi[(alpha, beta, i, q)]) + expr = IndexSum((alpha, beta), invJ()[alpha, gamma] * invJ()[beta, delta] * + phi[(alpha, beta, i, q)]) ind = ((gamma, delta), (i,), (q,)) else: beta = tIndex() @@ -61,11 +61,11 @@ def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): if d == 3: gamma = tIndex() expr = IndexSum((gamma,), kernel_data.J(zeta, gamma) * - LeviCivita((gamma,), (alpha, beta), phi[(alpha, beta, i, q)])) \ - / detJ() + LeviCivita((gamma,), (alpha, beta), phi[(alpha, beta, i, q)])) / \ + detJ() elif d == 2: - expr = LeviCivita((2,), (alpha, beta), phi[(alpha, beta, i, q)]) \ - / detJ() + expr = LeviCivita((2,), (alpha, beta), phi[(alpha, beta, i, q)]) / \ + detJ() else: if d == 3: expr = LeviCivita((zeta,), (alpha, beta), phi[(alpha, beta, i, q)]) diff --git a/finat/hdiv.py b/finat/hdiv.py index a3ca1fced..e590d04dc 100644 --- a/finat/hdiv.py +++ b/finat/hdiv.py @@ -29,8 +29,8 @@ def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): if pullback: beta = alpha alpha = gIndex() - expr = IndexSum((beta,), J()[alpha, beta] * phi[beta, i, q] - / detJ()) + expr = IndexSum((beta,), J()[alpha, beta] * phi[beta, i, q] / + detJ()) else: expr = phi[alpha, i, q] ind = ((alpha,), (i,), (q,)) @@ -45,8 +45,8 @@ def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): beta = tIndex() gamma = gIndex() delta = gIndex() - expr = IndexSum((alpha, beta), J()[gamma, alpha] * invJ()[beta, delta] - * phi[alpha, beta, i, q]) / detJ() + expr = IndexSum((alpha, beta), J()[gamma, alpha] * invJ()[beta, delta] * + phi[alpha, beta, i, q]) / detJ() ind = ((gamma, delta), (i,), (q,)) else: beta = indices.DimensionIndex(kernel_data.tdim) @@ -60,8 +60,8 @@ def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): delta = gIndex() zeta = gIndex() expr = LeviCivita((zeta,), (gamma, delta), - IndexSum((alpha, beta), J()[gamma, alpha] * invJ()[beta, delta] - * phi[alpha, beta, i, q])) / detJ() + IndexSum((alpha, beta), J()[gamma, alpha] * invJ()[beta, delta] * + phi[alpha, beta, i, q])) / detJ() else: d = kernel_data.tdim zeta = tIndex() diff --git a/finat/indices.py b/finat/indices.py index d60b7b9b3..4761f94b0 100644 --- a/finat/indices.py +++ b/finat/indices.py @@ -8,8 +8,8 @@ def __init__(self, extent, name): super(IndexBase, self).__init__(name) if isinstance(extent, slice): self._extent = extent - elif (isinstance(extent, int) - or isinstance(extent, p.Expression)): + elif (isinstance(extent, int) or + isinstance(extent, p.Expression)): self._extent = slice(extent) else: raise TypeError("Extent must be a slice or an int") diff --git a/finat/utils.py b/finat/utils.py index d8f848c0a..e63d52e2b 100644 --- a/finat/utils.py +++ b/finat/utils.py @@ -21,8 +21,8 @@ def __init__(self, coordinate_element, coordinate_var=None, affine=None): self.coordinate_element = coordinate_element self.coordinate_var = coordinate_var if affine is None: - self.affine = coordinate_element.degree <= 1 \ - and isinstance(coordinate_element.cell, _simplex) + self.affine = coordinate_element.degree <= 1 and \ + isinstance(coordinate_element.cell, _simplex) else: self.affine = affine @@ -183,6 +183,6 @@ def bind_geometry(self, expression, points=None): # Tuple of simplex cells. This relies on the fact that FIAT only # defines simplex elements. _simplex = tuple(e for e in FIAT.reference_element.__dict__.values() - if (inspect.isclass(e) - and issubclass(e, FIAT.reference_element.ReferenceElement) - and e is not FIAT.reference_element.ReferenceElement)) + if (inspect.isclass(e) and + issubclass(e, FIAT.reference_element.ReferenceElement) and + e is not FIAT.reference_element.ReferenceElement)) From a4be6b3dc85015fde9af73248deff658c3b900cf Mon Sep 17 00:00:00 2001 From: David A Ham Date: Mon, 16 Feb 2015 13:34:57 +0000 Subject: [PATCH 119/749] shut pep8 up --- finat/bernstein.py | 4 ++-- finat/finiteelementbase.py | 4 ++-- finat/gauss_jacobi.py | 8 ++++---- finat/geometry_mapper.py | 6 ++---- finat/hcurl.py | 16 ++++++++-------- finat/hdiv.py | 12 ++++++------ finat/indices.py | 4 ++-- finat/utils.py | 10 +++++----- 8 files changed, 31 insertions(+), 33 deletions(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index 6170693cd..f30ac0516 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -64,8 +64,8 @@ def dofs_shape(self): degree = self.degree dim = self.cell.get_spatial_dimension() - return (int(np.prod(xrange(degree + 1, degree + 1 + dim)) - / np.prod(xrange(1, dim + 1))),) + return (int(np.prod(xrange(degree + 1, degree + 1 + dim)) / + np.prod(xrange(1, dim + 1))),) def field_evaluation(self, field_var, q, kernel_data, derivative=None): if not isinstance(q.points, StroudPointSet): diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 5c0575ba4..8b81188ea 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -177,8 +177,8 @@ def _tabulated_basis(self, points, kernel_data, derivative): if static_key in kernel_data.static: phi = kernel_data.static[static_key][0] else: - phi = p.Variable(("d" if derivative else "") - + kernel_data.tabulation_variable_name(self, points)) + phi = p.Variable(("d" if derivative else "") + + kernel_data.tabulation_variable_name(self, points)) data = self._tabulate(points, derivative) kernel_data.static[static_key] = (phi, lambda: data) diff --git a/finat/gauss_jacobi.py b/finat/gauss_jacobi.py index 3fb02a9b9..cf24682ba 100644 --- a/finat/gauss_jacobi.py +++ b/finat/gauss_jacobi.py @@ -1,4 +1,8 @@ """Basic tools for getting Gauss points and quadrature rules.""" +import math +from math import factorial +import numpy + __copyright__ = "Copyright (C) 2014 Robert C. Kirby" __license__ = """ Permission is hereby granted, free of charge, to any person obtaining a copy @@ -20,10 +24,6 @@ THE SOFTWARE. """ -import math -from math import factorial -import numpy - def compute_gauss_jacobi_points(a, b, m): """Computes the m roots of P_{m}^{a,b} on [-1,1] by Newton's method. diff --git a/finat/geometry_mapper.py b/finat/geometry_mapper.py index afaa6947a..ecac03798 100644 --- a/finat/geometry_mapper.py +++ b/finat/geometry_mapper.py @@ -75,10 +75,8 @@ def _bind_geometry(self, q, body): element = kd.coordinate_element J = element.field_evaluation(phi_x, q, kd, grad, pullback=False) - inner_lets = (((kd.detJ, Det(kd.J)),) - if kd.detJ in self.local_geometry else () - + ((kd.invJ, Inverse(kd.J)),) - if kd.invJ in self.local_geometry else ()) + inner_lets = (((kd.detJ, Det(kd.J)),) if kd.detJ in self.local_geometry else () + + ((kd.invJ, Inverse(kd.J)),) if kd.invJ in self.local_geometry else ()) # The local geometry goes out of scope at this point. self.local_geometry = set() diff --git a/finat/hcurl.py b/finat/hcurl.py index 47d5d43ed..3a921ecfc 100644 --- a/finat/hcurl.py +++ b/finat/hcurl.py @@ -36,8 +36,8 @@ def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): if pullback: beta = tIndex() gamma = gIndex() - expr = IndexSum((gamma,), invJ()[alpha, gamma] * invJ()[beta, gamma] - * phi[(alpha, beta, i, q)]) + expr = IndexSum((gamma,), invJ()[alpha, gamma] * invJ()[beta, gamma] * + phi[(alpha, beta, i, q)]) else: expr = IndexSum((alpha,), phi[(alpha, alpha, i, q)]) ind = ((), (i,), (q,)) @@ -46,8 +46,8 @@ def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): beta = tIndex() gamma = gIndex() delta = gIndex() - expr = IndexSum((alpha, beta), invJ()[alpha, gamma] * invJ()[beta, delta] - * phi[(alpha, beta, i, q)]) + expr = IndexSum((alpha, beta), invJ()[alpha, gamma] * invJ()[beta, delta] * + phi[(alpha, beta, i, q)]) ind = ((gamma, delta), (i,), (q,)) else: beta = tIndex() @@ -61,11 +61,11 @@ def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): if d == 3: gamma = tIndex() expr = IndexSum((gamma,), kernel_data.J(zeta, gamma) * - LeviCivita((gamma,), (alpha, beta), phi[(alpha, beta, i, q)])) \ - / detJ() + LeviCivita((gamma,), (alpha, beta), phi[(alpha, beta, i, q)])) / \ + detJ() elif d == 2: - expr = LeviCivita((2,), (alpha, beta), phi[(alpha, beta, i, q)]) \ - / detJ() + expr = LeviCivita((2,), (alpha, beta), phi[(alpha, beta, i, q)]) / \ + detJ() else: if d == 3: expr = LeviCivita((zeta,), (alpha, beta), phi[(alpha, beta, i, q)]) diff --git a/finat/hdiv.py b/finat/hdiv.py index a3ca1fced..e590d04dc 100644 --- a/finat/hdiv.py +++ b/finat/hdiv.py @@ -29,8 +29,8 @@ def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): if pullback: beta = alpha alpha = gIndex() - expr = IndexSum((beta,), J()[alpha, beta] * phi[beta, i, q] - / detJ()) + expr = IndexSum((beta,), J()[alpha, beta] * phi[beta, i, q] / + detJ()) else: expr = phi[alpha, i, q] ind = ((alpha,), (i,), (q,)) @@ -45,8 +45,8 @@ def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): beta = tIndex() gamma = gIndex() delta = gIndex() - expr = IndexSum((alpha, beta), J()[gamma, alpha] * invJ()[beta, delta] - * phi[alpha, beta, i, q]) / detJ() + expr = IndexSum((alpha, beta), J()[gamma, alpha] * invJ()[beta, delta] * + phi[alpha, beta, i, q]) / detJ() ind = ((gamma, delta), (i,), (q,)) else: beta = indices.DimensionIndex(kernel_data.tdim) @@ -60,8 +60,8 @@ def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): delta = gIndex() zeta = gIndex() expr = LeviCivita((zeta,), (gamma, delta), - IndexSum((alpha, beta), J()[gamma, alpha] * invJ()[beta, delta] - * phi[alpha, beta, i, q])) / detJ() + IndexSum((alpha, beta), J()[gamma, alpha] * invJ()[beta, delta] * + phi[alpha, beta, i, q])) / detJ() else: d = kernel_data.tdim zeta = tIndex() diff --git a/finat/indices.py b/finat/indices.py index d60b7b9b3..4761f94b0 100644 --- a/finat/indices.py +++ b/finat/indices.py @@ -8,8 +8,8 @@ def __init__(self, extent, name): super(IndexBase, self).__init__(name) if isinstance(extent, slice): self._extent = extent - elif (isinstance(extent, int) - or isinstance(extent, p.Expression)): + elif (isinstance(extent, int) or + isinstance(extent, p.Expression)): self._extent = slice(extent) else: raise TypeError("Extent must be a slice or an int") diff --git a/finat/utils.py b/finat/utils.py index d8f848c0a..e63d52e2b 100644 --- a/finat/utils.py +++ b/finat/utils.py @@ -21,8 +21,8 @@ def __init__(self, coordinate_element, coordinate_var=None, affine=None): self.coordinate_element = coordinate_element self.coordinate_var = coordinate_var if affine is None: - self.affine = coordinate_element.degree <= 1 \ - and isinstance(coordinate_element.cell, _simplex) + self.affine = coordinate_element.degree <= 1 and \ + isinstance(coordinate_element.cell, _simplex) else: self.affine = affine @@ -183,6 +183,6 @@ def bind_geometry(self, expression, points=None): # Tuple of simplex cells. This relies on the fact that FIAT only # defines simplex elements. _simplex = tuple(e for e in FIAT.reference_element.__dict__.values() - if (inspect.isclass(e) - and issubclass(e, FIAT.reference_element.ReferenceElement) - and e is not FIAT.reference_element.ReferenceElement)) + if (inspect.isclass(e) and + issubclass(e, FIAT.reference_element.ReferenceElement) and + e is not FIAT.reference_element.ReferenceElement)) From 8ae0ee64f120e83a3f9a05eb6c077ea7318e452a Mon Sep 17 00:00:00 2001 From: David A Ham Date: Sat, 31 Jan 2015 14:37:59 +0000 Subject: [PATCH 120/749] skeleton quads --- finat/quads.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 finat/quads.py diff --git a/finat/quads.py b/finat/quads.py new file mode 100644 index 000000000..090c5b914 --- /dev/null +++ b/finat/quads.py @@ -0,0 +1,36 @@ +"""Preliminary support for quadrilateral elements. Later to be +generalised to general tensor product elements.""" +from .finiteelementbase import FiniteElementBase +from .indices import TensorPointIndex + +class QuadrilateralElement(FiniteElementBase): + def __init__(self, h_element, v_element): + super(QuadrilateralElement, self).__init__() + + self.h_element = h_element + self.v_element = v_element + + def basis_evaluation(self, q, kernel_data, derivative=None, + pullback=True): + '''Produce the variable for the tabulation of the basis + functions or their derivative.''' + + assert isinstance(q, TensorPointIndex) + + if derivative not in (None, grad): + raise ValueError( + "Scalar elements do not have a %s operation") % derivative + + if derivative is grad: + raise NotImplementedError + + else: + raise NotImplementedError + + def field_evaluation(self, field_var, q, + kernel_data, derivative=None, pullback=True): + raise NotImplementedError + + def moment_evaluation(self, value, weights, q, + kernel_data, derivative=None, pullback=True): + raise NotImplementedError From 59e8cb63fad2eb5a1f0adee6acb614adf3e29cfc Mon Sep 17 00:00:00 2001 From: David A Ham Date: Sat, 31 Jan 2015 14:40:19 +0000 Subject: [PATCH 121/749] pep8 --- finat/quads.py | 1 + 1 file changed, 1 insertion(+) diff --git a/finat/quads.py b/finat/quads.py index 090c5b914..55f4e6832 100644 --- a/finat/quads.py +++ b/finat/quads.py @@ -3,6 +3,7 @@ from .finiteelementbase import FiniteElementBase from .indices import TensorPointIndex + class QuadrilateralElement(FiniteElementBase): def __init__(self, h_element, v_element): super(QuadrilateralElement, self).__init__() From ae22bab3754ab0bd7e859962155214f393e80b66 Mon Sep 17 00:00:00 2001 From: David Ham Date: Sat, 31 Jan 2015 23:10:25 +0000 Subject: [PATCH 122/749] basic quad functionality draft. --- finat/__init__.py | 1 + finat/finiteelementbase.py | 66 ++++++++++++++++++++------------------ finat/indices.py | 25 +++++++++++++++ finat/lagrange.py | 7 ++-- finat/quads.py | 32 ++++++++++-------- 5 files changed, 83 insertions(+), 48 deletions(-) diff --git a/finat/__init__.py b/finat/__init__.py index ac10ccea6..c4fba1b54 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -2,6 +2,7 @@ from hdiv import RaviartThomas, BrezziDouglasMarini, BrezziDouglasFortinMarini from bernstein import Bernstein from vectorfiniteelement import VectorFiniteElement +from quads import QuadrilateralElement from points import PointSet from utils import KernelData from derivatives import div, grad, curl diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 8b81188ea..a0bdfc3c0 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -125,7 +125,41 @@ def dual_evaluation(self, kernel_data): raise NotImplementedError -class FiatElementBase(FiniteElementBase): +class ScalarElementMixin(object): + """Mixin class containing field evaluation and moment rules for scalar + valued elements.""" + def field_evaluation(self, field_var, q, + kernel_data, derivative=None, pullback=True): + + basis = self.basis_evaluation(q, kernel_data, derivative, pullback) + (d, b, p_) = basis.indices + phi = basis.body + + expr = IndexSum(b, field_var[b[0]] * phi) + + return Recipe((d, (), p), expr) + + def moment_evaluation(self, value, weights, q, + kernel_data, derivative=None, pullback=True): + + basis = self.basis_evaluation(q, kernel_data, derivative, pullback) + (d, b, p__) = basis.indices + phi = basis.body + + (d_, b_, p_) = value.indices + psi = value.replace_indices(zip(d_ + p_, d + p__)).body + + w = weights.kernel_variable("w", kernel_data) + + expr = psi * phi * w[p__] + + if pullback: + expr *= kernel_data.detJ + + return Recipe(((), b + b_, ()), IndexSum(p__, expr)) + + +class FiatElementBase(ScalarElementMixin, FiniteElementBase): """Base class for finite elements for which the tabulation is provided by FIAT.""" def __init__(self, cell, degree): @@ -183,33 +217,3 @@ def _tabulated_basis(self, points, kernel_data, derivative): kernel_data.static[static_key] = (phi, lambda: data) return phi - - def field_evaluation(self, field_var, q, - kernel_data, derivative=None, pullback=True): - - basis = self.basis_evaluation(q, kernel_data, derivative, pullback) - (d, b, p) = basis.indices - phi = basis.body - - expr = IndexSum(b, field_var[b[0]] * phi) - - return Recipe((d, (), p), expr) - - def moment_evaluation(self, value, weights, q, - kernel_data, derivative=None, pullback=True): - - basis = self.basis_evaluation(q, kernel_data, derivative, pullback) - (d, b, p) = basis.indices - phi = basis.body - - (d_, b_, p_) = value.indices - psi = value.replace_indices(zip(d_ + p_, d + p)).body - - w = weights.kernel_variable("w", kernel_data) - - expr = psi * phi * w[p] - - if pullback: - expr *= kernel_data.detJ - - return Recipe(((), b + b_, ()), IndexSum(p, expr)) diff --git a/finat/indices.py b/finat/indices.py index 4761f94b0..9ba58ddb0 100644 --- a/finat/indices.py +++ b/finat/indices.py @@ -95,6 +95,31 @@ def __init__(self, extent): _count = 0 +class TensorBasisFunctionIndex(IndexBase): + """An index running over a set of basis functions which have a tensor + product structure. This index is actually composed of multiple + factors. + """ + def __init__(self, *args): + + assert all([isinstance(a, BasisFunctionIndex) for a in args]) + + name = 'i_' + str(BasisFunctionIndex._count) + BasisFunctionIndex._count += 1 + + super(TensorBasisFunctionIndex, self).__init__(-1, name) + + self.factors = args + + def __getattr__(self, name): + + if name == "_error": + if any([hasattr(x, "_error") for x in self.factors]): + return True + + raise AttributeError + + class SimpliciallyGradedBasisFunctionIndex(BasisFunctionIndex): '''An index over simplicial polynomials with a grade, such as Dubiner or Bernstein. Implies a simplicial iteration space.''' diff --git a/finat/lagrange.py b/finat/lagrange.py index 42b7a9f41..0d2d30d58 100644 --- a/finat/lagrange.py +++ b/finat/lagrange.py @@ -1,9 +1,8 @@ -import pymbolic.primitives as p -from finiteelementbase import FiatElementBase -from ast import Recipe, IndexSum +from .finiteelementbase import FiatElementBase +from .ast import Recipe, IndexSum import FIAT import indices -from derivatives import grad +from .derivatives import grad class ScalarElement(FiatElementBase): diff --git a/finat/quads.py b/finat/quads.py index 55f4e6832..baf198b7e 100644 --- a/finat/quads.py +++ b/finat/quads.py @@ -1,15 +1,18 @@ """Preliminary support for quadrilateral elements. Later to be generalised to general tensor product elements.""" -from .finiteelementbase import FiniteElementBase -from .indices import TensorPointIndex +from .finiteelementbase import ScalarElementMixin, FiniteElementBase +from .indices import TensorPointIndex, TensorBasisFunctionIndex +from .derivatives import grad +from .ast import Recipe -class QuadrilateralElement(FiniteElementBase): - def __init__(self, h_element, v_element): +class QuadrilateralElement(ScalarElementMixin, FiniteElementBase): + def __init__(self, *args): super(QuadrilateralElement, self).__init__() - self.h_element = h_element - self.v_element = v_element + assert all([isinstance(e, FiniteElementBase) for e in args]) + + self.factors = args def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): @@ -26,12 +29,15 @@ def basis_evaluation(self, q, kernel_data, derivative=None, raise NotImplementedError else: - raise NotImplementedError + # note - think about pullbacks. + phi = [e.basis_evaluation(q_, kernel_data) + for e, q_ in zip(self.factors, q.factors)] + + # note - think about what happens in the vector case. + i_ = [phi_.indices[1] for phi_ in phi] + i = TensorBasisFunctionIndex(*i_) - def field_evaluation(self, field_var, q, - kernel_data, derivative=None, pullback=True): - raise NotImplementedError + ind = ((), (i,), (q,)) + expr = reduce(lambda a, b: a.body * b.body, phi) - def moment_evaluation(self, value, weights, q, - kernel_data, derivative=None, pullback=True): - raise NotImplementedError + return Recipe(indices=ind, body=expr) From 2a3f6fcf1bf38de57f043abf903a80bafb092c76 Mon Sep 17 00:00:00 2001 From: David Ham Date: Wed, 4 Feb 2015 23:03:14 +0000 Subject: [PATCH 123/749] Draft of gradient with CompoundVector --- finat/ast.py | 34 +++++++++++++++++++++++++++++++++- finat/indices.py | 10 ++++++++++ finat/quads.py | 28 ++++++++++++++++++++++------ 3 files changed, 65 insertions(+), 7 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index c876bc3de..a1c9f5f61 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -38,6 +38,7 @@ def map_delta(self, expr, *args): map_levi_civita = map_delta map_inverse = map_delta map_det = map_delta + map_compound_vector = map_delta class _IndexMapper(IdentityMapper): @@ -134,6 +135,10 @@ def map_det(self, expr, *args, **kwargs): return self.format(expr.name + "(%s)", self.rec(expr.expression, *args, **kwargs)) + def map_compound_vector(self, expr, *args, **kwargs): + return self.format(expr.name + "(%s)", + self.join_rec(", ", expr.children, *args, **kwargs)) + def map_variable(self, expr, enclosing_prec, *args, **kwargs): if hasattr(expr, "_error"): return colored(str(expr.name), "red", attrs=["bold"]) @@ -180,6 +185,7 @@ def map_index_sum(self, expr, *args, **kwargs): map_levi_civita = map_index_sum map_inverse = map_index_sum map_det = map_index_sum + map_compound_vector = map_index_sum class GraphvizMapper(WalkMapper, GVM): @@ -388,7 +394,6 @@ def __init__(self, bindings, body): self.bindings, self.body = self.children - self.bindings, self.body = self.children self._color = "blue" mapper_method = "map_let" @@ -451,3 +456,30 @@ def __init__(self, expression): self._color = "blue" mapper_method = "map_det" + + +class CompoundVector(StringifyMixin, p.Expression): + """A vector expression composed by concatenating other expressions.""" + def __init__(self, index, indices, expressions): + """ + + :param index: The free :class:`~.DimensionIndex` created by + the :class:`CompoundVector` + :param indices: The sequence of dimension indices of the + expressions. For scalar components these should be ``None``. + :param expressions: The sequence of expressions making up + the compound. + + Each value that `index` takes will be mapped to the corresponding + value in indices and the matching expression will be evaluated. + """ + if len(indices) != len(expressions): + raise ValueError("The indices and expressions must be of equal length") + + super(CompoundVector, self).__init__((index, indices, expressions)) + + self.index, self.indices, self.expressions = self.children + + self._color = "blue" + + mapper_method = "map_compound_vector" diff --git a/finat/indices.py b/finat/indices.py index 9ba58ddb0..9520a020c 100644 --- a/finat/indices.py +++ b/finat/indices.py @@ -1,5 +1,6 @@ import pymbolic.primitives as p from pymbolic.mapper.stringifier import StringifyMapper +import math class IndexBase(p.Variable): @@ -20,6 +21,15 @@ def extent(self): '''A slice indicating the values this index can take.''' return self._extent + @property + def length(self): + '''The number of values this index can take.''' + start = self._extent.start or 0 + stop = self._extent.stop + step = self._extent.step or 1 + + return math.ceil((stop - start) / step) + @property def _str_extent(self): diff --git a/finat/quads.py b/finat/quads.py index baf198b7e..1441b1cdf 100644 --- a/finat/quads.py +++ b/finat/quads.py @@ -1,9 +1,9 @@ """Preliminary support for quadrilateral elements. Later to be generalised to general tensor product elements.""" from .finiteelementbase import ScalarElementMixin, FiniteElementBase -from .indices import TensorPointIndex, TensorBasisFunctionIndex +from .indices import TensorPointIndex, TensorBasisFunctionIndex, DimensionIndex from .derivatives import grad -from .ast import Recipe +from .ast import Recipe, CompoundVector class QuadrilateralElement(ScalarElementMixin, FiniteElementBase): @@ -25,17 +25,33 @@ def basis_evaluation(self, q, kernel_data, derivative=None, raise ValueError( "Scalar elements do not have a %s operation") % derivative + phi = [e.basis_evaluation(q_, kernel_data) + for e, q_ in zip(self.factors, q.factors)] + + i_ = [phi_.indices[1] for phi_ in phi] + i = TensorBasisFunctionIndex(*i_) + if derivative is grad: raise NotImplementedError + phi_d = [e.basis_evaluation(q_, kernel_data, grad=True) + for e, q_ in zip(self.factors, q.factors)] + + # Need to replace the basisfunctionindices on phi_d with i + expressions = [reduce(lambda a, b: a.body * b.body, + phi[:d] + [phi_d[d]] + phi[d + 1:]) + for d in len(phi)] + + d_ = [phi_.indices[0] for phi_ in phi_d] + d = DimensionIndex(sum(d__.length for d__ in d)) + + expr = CompoundVector(d, d_, expressions) + + ind = ((d,), (i,), (q,)) else: # note - think about pullbacks. - phi = [e.basis_evaluation(q_, kernel_data) - for e, q_ in zip(self.factors, q.factors)] # note - think about what happens in the vector case. - i_ = [phi_.indices[1] for phi_ in phi] - i = TensorBasisFunctionIndex(*i_) ind = ((), (i,), (q,)) expr = reduce(lambda a, b: a.body * b.body, phi) From fbf451c678cb82566b1382194ec33479dc36a2ab Mon Sep 17 00:00:00 2001 From: David A Ham Date: Mon, 16 Feb 2015 10:33:00 +0000 Subject: [PATCH 124/749] plausible pullbacks and gradients --- finat/quads.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/finat/quads.py b/finat/quads.py index 1441b1cdf..2f6572796 100644 --- a/finat/quads.py +++ b/finat/quads.py @@ -3,7 +3,7 @@ from .finiteelementbase import ScalarElementMixin, FiniteElementBase from .indices import TensorPointIndex, TensorBasisFunctionIndex, DimensionIndex from .derivatives import grad -from .ast import Recipe, CompoundVector +from .ast import Recipe, CompoundVector, IndexSum class QuadrilateralElement(ScalarElementMixin, FiniteElementBase): @@ -41,17 +41,21 @@ def basis_evaluation(self, q, kernel_data, derivative=None, phi[:d] + [phi_d[d]] + phi[d + 1:]) for d in len(phi)] - d_ = [phi_.indices[0] for phi_ in phi_d] - d = DimensionIndex(sum(d__.length for d__ in d)) + alpha_ = [phi_.indices[0] for phi_ in phi_d] + alpha = DimensionIndex(sum(alpha__.length for alpha__ in alpha_)) - expr = CompoundVector(d, d_, expressions) + assert alpha.length == kernel_data.gdim + expr = CompoundVector(alpha, alpha_, expressions) - ind = ((d,), (i,), (q,)) + if pullback: + beta = alpha + alpha = DimensionIndex(kernel_data.gdim) + invJ = kernel_data.invJ[(beta, alpha)] + expr = IndexSum((beta,), invJ * expr) - else: - # note - think about pullbacks. + ind = ((alpha,), (i,), (q,)) - # note - think about what happens in the vector case. + else: ind = ((), (i,), (q,)) expr = reduce(lambda a, b: a.body * b.body, phi) From c2c432aeb98cf7dac2a2f5442cb5863aab8945af Mon Sep 17 00:00:00 2001 From: David A Ham Date: Mon, 16 Feb 2015 10:36:11 +0000 Subject: [PATCH 125/749] rename product elements to more sane names --- finat/__init__.py | 1 + finat/{quads.py => product_elements.py} | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) rename finat/{quads.py => product_elements.py} (90%) diff --git a/finat/__init__.py b/finat/__init__.py index c4fba1b54..c409d0c2a 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -1,5 +1,6 @@ from lagrange import Lagrange, DiscontinuousLagrange from hdiv import RaviartThomas, BrezziDouglasMarini, BrezziDouglasFortinMarini +from product_elements import ScalarProductElement from bernstein import Bernstein from vectorfiniteelement import VectorFiniteElement from quads import QuadrilateralElement diff --git a/finat/quads.py b/finat/product_elements.py similarity index 90% rename from finat/quads.py rename to finat/product_elements.py index 2f6572796..3107e5f00 100644 --- a/finat/quads.py +++ b/finat/product_elements.py @@ -1,14 +1,14 @@ -"""Preliminary support for quadrilateral elements. Later to be -generalised to general tensor product elements.""" +"""Preliminary support for tensor product elements.""" from .finiteelementbase import ScalarElementMixin, FiniteElementBase from .indices import TensorPointIndex, TensorBasisFunctionIndex, DimensionIndex from .derivatives import grad from .ast import Recipe, CompoundVector, IndexSum -class QuadrilateralElement(ScalarElementMixin, FiniteElementBase): +class ScalarProductElement(ScalarElementMixin, FiniteElementBase): + """A scalar-valued tensor product element.""" def __init__(self, *args): - super(QuadrilateralElement, self).__init__() + super(ScalarProductElement, self).__init__() assert all([isinstance(e, FiniteElementBase) for e in args]) From ce12b1562546691562abe00bf94d667eb7abec09 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Mon, 16 Feb 2015 11:57:56 +0000 Subject: [PATCH 126/749] draft of compound vectors in the interpreter --- finat/ast.py | 6 +++++- finat/interpreter.py | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index a1c9f5f61..79f507f7c 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -474,7 +474,11 @@ def __init__(self, index, indices, expressions): value in indices and the matching expression will be evaluated. """ if len(indices) != len(expressions): - raise ValueError("The indices and expressions must be of equal length") + raise FInATSyntaxError("The indices and expressions must be of equal length") + + if sum([i.length for i in indices]) != index.length: + raise FInATSyntaxError("The length of the compound index must equal " + "the sum of the lengths of the components.") super(CompoundVector, self).__init__((index, indices, expressions)) diff --git a/finat/interpreter.py b/finat/interpreter.py index d96070cd1..5351f47bc 100644 --- a/finat/interpreter.py +++ b/finat/interpreter.py @@ -123,7 +123,8 @@ def map_for_all(self, expr): if idx in self.indices: expr_in.set_error() idx.set_error() - raise FInATSyntaxError("Attempting to bind the name %s which is already bound" % idx) + raise FInATSyntaxError( + "Attempting to bind the name %s which is already bound" % idx) e = idx.extent @@ -141,6 +142,32 @@ def map_for_all(self, expr): return np.array(total) + def map_compound_vector(self, expr): + + (index, indices, bodies) = expr.children + + if index not in self.indices: + expr.set_error() + index.set_error() + raise FInATSyntaxError( + "Compound vector depends on %s, which is not in scope" % index) + + alpha = self.indices[index] + + for idx, body in zip(indices, bodies): + if alpha < idx.length: + if idx in self.indices: + raise FInATSyntaxError( + "Attempting to bind the name %s which is already bound" % idx) + self.indices[idx] = self._as_range(idx)[alpha] + result = self.rec(body) + self.indices.pop(idx) + return result + else: + alpha -= idx.length + + raise FInATSyntaxError("Compound index %s out of bounds" % index) + def map_wave(self, expr): (var, index, base, update, body) = expr.children @@ -148,7 +175,8 @@ def map_wave(self, expr): if index not in self.indices: expr.set_error() index.set_error() - raise FInATSyntaxError("Wave variable depends on %s, which is not in scope" % index) + raise FInATSyntaxError( + "Wave variable depends on %s, which is not in scope" % index) try: self.context[var.name] = self.wave_vars[var.name] From a3691927c9e3cad7bb3b0fc6828a1e93f0f2870b Mon Sep 17 00:00:00 2001 From: David A Ham Date: Mon, 16 Feb 2015 12:03:58 +0000 Subject: [PATCH 127/749] get the name fix right this time --- finat/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/finat/__init__.py b/finat/__init__.py index c409d0c2a..d7d965a42 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -1,9 +1,8 @@ from lagrange import Lagrange, DiscontinuousLagrange from hdiv import RaviartThomas, BrezziDouglasMarini, BrezziDouglasFortinMarini -from product_elements import ScalarProductElement from bernstein import Bernstein from vectorfiniteelement import VectorFiniteElement -from quads import QuadrilateralElement +from product_elements import ScalarProductElement from points import PointSet from utils import KernelData from derivatives import div, grad, curl From 6efddc4436db38b929fa2e1420bd1ac20ef430fb Mon Sep 17 00:00:00 2001 From: David A Ham Date: Mon, 16 Feb 2015 17:46:50 +0000 Subject: [PATCH 128/749] Initial plumbing for PyOP2 interfacing. --- finat/__init__.py | 2 +- finat/bernstein.py | 2 ++ finat/finiteelementbase.py | 2 ++ finat/pyop2-interface.py | 39 ++++++++++++++++++++++++++++++++++++ finat/ufl-interface.py | 26 ++++++++++++++++++++++++ finat/utils.py | 13 +++++++++++- finat/vectorfiniteelement.py | 3 +++ 7 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 finat/pyop2-interface.py create mode 100644 finat/ufl-interface.py diff --git a/finat/__init__.py b/finat/__init__.py index d7d965a42..ab6b2a2c8 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -4,7 +4,7 @@ from vectorfiniteelement import VectorFiniteElement from product_elements import ScalarProductElement from points import PointSet -from utils import KernelData +from utils import KernelData, Kernel from derivatives import div, grad, curl import interpreter import quadrature diff --git a/finat/bernstein.py b/finat/bernstein.py index 38a6cc801..c7efc1359 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -74,6 +74,8 @@ def field_evaluation(self, field_var, q, kernel_data, derivative=None): if derivative is not None: raise NotImplementedError + kernel_data.kernel_args.add(field_var) + # Get the symbolic names for the points. xi = [self._points_variable(f.points, kernel_data) for f in q.factors] diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index a0bdfc3c0..760aed245 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -131,6 +131,8 @@ class ScalarElementMixin(object): def field_evaluation(self, field_var, q, kernel_data, derivative=None, pullback=True): + kernel_data.kernel_args.add(field_var) + basis = self.basis_evaluation(q, kernel_data, derivative, pullback) (d, b, p_) = basis.indices phi = basis.body diff --git a/finat/pyop2-interface.py b/finat/pyop2-interface.py new file mode 100644 index 000000000..4c38f3127 --- /dev/null +++ b/finat/pyop2-interface.py @@ -0,0 +1,39 @@ +try: + from pyop2.pyparloop import Kernel +except: + Kernel = None +from .interpreter import evaluate + + +def pyop2_kernel(kernel, kernel_args, interpreter=False): + """Return a :class:`pyop2.Kernel` from the recipe and kernel data + provided. + + :param kernel: The :class:`~.utils.Kernel` to map to PyOP2. + :param kernel_args: The ordered list of Pymbolic variables constituting + the kernel arguments, excluding the result of the recipe (the latter + should be prepended to the argument list). + :param interpreter: If set to ``True``, the kernel will be + evaluated using the FInAT interpreter instead of generating a + compiled kernel. + + :result: The :class:`pyop2.Kernel` + """ + + if Kernel is None: + raise ImportError("pyop2 was not imported. Is it installed?") + + if set(kernel_args) != kernel.kernel_data.kernel_args: + raise ValueError("Incomplete value list") + + if interpreter: + + def kernel_function(*args): + context = {kernel_args: args[1:]} + + args[0][:] = evaluate(kernel.recipe, context, kernel.kernel_data) + + return (Kernel(kernel_function), kernel_args) + + else: + raise NotImplementedError diff --git a/finat/ufl-interface.py b/finat/ufl-interface.py new file mode 100644 index 000000000..59e70ca99 --- /dev/null +++ b/finat/ufl-interface.py @@ -0,0 +1,26 @@ +"""Provide interface functions which take UFL objects and return FInAT ones.""" +import finat +import FIAT + +_cell_map = { + "triangle": FIAT.reference_element.UFCTriangle(), + "interval": FIAT.reference_element.UFCInterval() +} + +_element_map = { + "Lagrange": finat.Lagrange, + "Discontinuous Lagrange": finat.DiscontinuousLagrange +} + + +def cell_from_ufl(cell): + + return _cell_map[cell.name()] + + +def element_from_ufl(element): + + # Need to handle the product cases. + + return _element_map[element.family()](cell_from_ufl(element.cell()), + element.degree) diff --git a/finat/utils.py b/finat/utils.py index e63d52e2b..a2d216d94 100644 --- a/finat/utils.py +++ b/finat/utils.py @@ -5,6 +5,16 @@ from ast import Let, Array, Inverse, Det +class Kernel(object): + def __init__(self, recipe, kernel_data): + """An object bringing together a :class:`~.ast.Recipe` and its + corresponding :class:`~.utils.KernelData` context. + """ + + self.recipe = recipe + self.kernel_data = kernel_data + + class KernelData(object): def __init__(self, coordinate_element, coordinate_var=None, affine=None): """ @@ -27,7 +37,8 @@ def __init__(self, coordinate_element, coordinate_var=None, affine=None): self.affine = affine self.static = {} - self.params = {} + # The set of undefined symbols in this kernel. + self.kernel_args = set(coordinate_var) if coordinate_var else set() self.geometry = {} self.variables = set() diff --git a/finat/vectorfiniteelement.py b/finat/vectorfiniteelement.py index 9c27d248c..6a938ad64 100644 --- a/finat/vectorfiniteelement.py +++ b/finat/vectorfiniteelement.py @@ -106,6 +106,9 @@ def field_evaluation(self, field_var, q, \nabla\cdot\boldsymbol{f}_{q} = \sum_{i \alpha} f_{i \alpha}\nabla\phi_{\alpha i q} """ + + self.kernel_args.add(field_var) + # Produce the base scalar recipe. The scalar basis can only # take a grad. For other derivatives, we need to do the # transform here. From 6b22029e724a43a7fd4a12faf62ddb1c8e8f8f54 Mon Sep 17 00:00:00 2001 From: David Ham Date: Mon, 16 Feb 2015 22:01:11 +0000 Subject: [PATCH 129/749] You'd think I'd know Python syntax by now --- finat/{pyop2-interface.py => pyop2_interface.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename finat/{pyop2-interface.py => pyop2_interface.py} (100%) diff --git a/finat/pyop2-interface.py b/finat/pyop2_interface.py similarity index 100% rename from finat/pyop2-interface.py rename to finat/pyop2_interface.py From dbf7ecdbec3e96a3f770194d5a6e2b284cbb90fa Mon Sep 17 00:00:00 2001 From: David Ham Date: Mon, 16 Feb 2015 22:48:02 +0000 Subject: [PATCH 130/749] deal with zero args corner case and return correct type --- finat/pyop2_interface.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/finat/pyop2_interface.py b/finat/pyop2_interface.py index 4c38f3127..28ffd8428 100644 --- a/finat/pyop2_interface.py +++ b/finat/pyop2_interface.py @@ -23,7 +23,8 @@ def pyop2_kernel(kernel, kernel_args, interpreter=False): if Kernel is None: raise ImportError("pyop2 was not imported. Is it installed?") - if set(kernel_args) != kernel.kernel_data.kernel_args: + if kernel_args and \ + set(kernel_args) != kernel.kernel_data.kernel_args: raise ValueError("Incomplete value list") if interpreter: @@ -33,7 +34,7 @@ def kernel_function(*args): args[0][:] = evaluate(kernel.recipe, context, kernel.kernel_data) - return (Kernel(kernel_function), kernel_args) + return Kernel(kernel_function) else: raise NotImplementedError From 0413d87838a030141bcfd52616b7f58e7b7a1161 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Sun, 1 Mar 2015 13:52:49 +0000 Subject: [PATCH 131/749] Have our own Variable Subclass Pymbolic.variable so that we can control the color of output. Consequently refactor mappers into another module to avoid circular dependencies. --- finat/__init__.py | 1 + finat/ast.py | 206 ++----------------- finat/bernstein.py | 17 +- finat/finiteelementbase.py | 9 +- finat/geometry_mapper.py | 9 +- finat/indices.py | 3 +- finat/interpreter.py | 4 +- finat/mappers.py | 185 +++++++++++++++++ finat/points.py | 4 +- finat/pyop2_interface.py | 13 +- finat/{ufl-interface.py => ufl_interface.py} | 4 +- finat/utils.py | 13 +- finat/vectorfiniteelement.py | 2 +- 13 files changed, 247 insertions(+), 223 deletions(-) create mode 100644 finat/mappers.py rename finat/{ufl-interface.py => ufl_interface.py} (84%) diff --git a/finat/__init__.py b/finat/__init__.py index ab6b2a2c8..56bec9b39 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -8,4 +8,5 @@ from derivatives import div, grad, curl import interpreter import quadrature +import ufl_interface from geometry_mapper import GeometryMapper diff --git a/finat/ast.py b/finat/ast.py index 79f507f7c..957597d83 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -2,196 +2,17 @@ required to define Finite Element expressions in FInAT. """ import pymbolic.primitives as p -from pymbolic.mapper import IdentityMapper as IM -from pymbolic.mapper.stringifier import StringifyMapper, PREC_NONE -from pymbolic.mapper import WalkMapper as WM -from pymbolic.mapper.graphviz import GraphvizMapper as GVM -from indices import IndexBase try: from termcolor import colored except ImportError: - colored = lambda string, color, attrs=[]: string + def colored(string, color, attrs=[]): + return string class FInATSyntaxError(Exception): """Exception to raise when users break the rules of the FInAT ast.""" -class IdentityMapper(IM): - def __init__(self): - super(IdentityMapper, self).__init__() - - def map_recipe(self, expr, *args): - return expr.__class__(self.rec(expr.indices, *args), - self.rec(expr.body, *args)) - - def map_index(self, expr, *args): - return expr - - def map_delta(self, expr, *args): - return expr.__class__(*(self.rec(c, *args) for c in expr.children)) - - map_let = map_delta - map_for_all = map_delta - map_wave = map_delta - map_index_sum = map_delta - map_levi_civita = map_delta - map_inverse = map_delta - map_det = map_delta - map_compound_vector = map_delta - - -class _IndexMapper(IdentityMapper): - def __init__(self, replacements): - super(_IndexMapper, self).__init__() - - self.replacements = replacements - - def map_index(self, expr, *args): - '''Replace indices if they are in the replacements list''' - - try: - return(self.replacements[expr]) - except KeyError: - return expr - - -class _StringifyMapper(StringifyMapper): - - def map_recipe(self, expr, enclosing_prec, indent=None, *args, **kwargs): - if indent is None: - fmt = expr.name + "(%s, %s)" - else: - oldidt = " " * indent - indent += 4 - idt = " " * indent - fmt = expr.name + "(%s,\n" + idt + "%s\n" + oldidt + ")" - - return self.format(fmt, - self.rec(expr.indices, PREC_NONE, indent=indent, *args, **kwargs), - self.rec(expr.body, PREC_NONE, indent=indent, *args, **kwargs)) - - def map_let(self, expr, enclosing_prec, indent=None, *args, **kwargs): - if indent is None: - fmt = expr.name + "(%s, %s)" - inner_indent = None - else: - oldidt = " " * indent - indent += 4 - inner_indent = indent + 4 - inner_idt = " " * inner_indent - idt = " " * indent - fmt = expr.name + "(\n" + inner_idt + "%s,\n" + idt + "%s\n" + oldidt + ")" - - return self.format(fmt, - self.rec(expr.bindings, PREC_NONE, indent=inner_indent, *args, **kwargs), - self.rec(expr.body, PREC_NONE, indent=indent, *args, **kwargs)) - - def map_delta(self, expr, *args, **kwargs): - return self.format(expr.name + "(%s, %s)", - *[self.rec(c, *args, **kwargs) for c in expr.children]) - - def map_index(self, expr, *args, **kwargs): - if hasattr(expr, "_error"): - return colored(str(expr), "red", attrs=["bold"]) - else: - return colored(str(expr), expr._color) - - def map_wave(self, expr, enclosing_prec, indent=None, *args, **kwargs): - if indent is None or enclosing_prec is not PREC_NONE: - fmt = expr.name + "(%s %s) " - else: - oldidt = " " * indent - indent += 4 - idt = " " * indent - fmt = expr.name + "(%s\n" + idt + "%s\n" + oldidt + ")" - - return self.format(fmt, - " ".join(self.rec(c, PREC_NONE, *args, **kwargs) + "," for c in expr.children[:-1]), - self.rec(expr.children[-1], PREC_NONE, indent=indent, *args, **kwargs)) - - def map_index_sum(self, expr, enclosing_prec, indent=None, *args, **kwargs): - if indent is None or enclosing_prec is not PREC_NONE: - fmt = expr.name + "((%s), %s) " - else: - oldidt = " " * indent - indent += 4 - idt = " " * indent - fmt = expr.name + "((%s),\n" + idt + "%s\n" + oldidt + ")" - - return self.format(fmt, - " ".join(self.rec(c, PREC_NONE, *args, **kwargs) + "," for c in expr.children[0]), - self.rec(expr.children[1], PREC_NONE, indent=indent, *args, **kwargs)) - - def map_levi_civita(self, expr, *args, **kwargs): - return self.format(expr.name + "(%s)", - self.join_rec(", ", expr.children, *args, **kwargs)) - - def map_inverse(self, expr, *args, **kwargs): - return self.format(expr.name + "(%s)", - self.rec(expr.expression, *args, **kwargs)) - - def map_det(self, expr, *args, **kwargs): - return self.format(expr.name + "(%s)", - self.rec(expr.expression, *args, **kwargs)) - - def map_compound_vector(self, expr, *args, **kwargs): - return self.format(expr.name + "(%s)", - self.join_rec(", ", expr.children, *args, **kwargs)) - - def map_variable(self, expr, enclosing_prec, *args, **kwargs): - if hasattr(expr, "_error"): - return colored(str(expr.name), "red", attrs=["bold"]) - else: - try: - return colored(expr.name, expr._color) - except AttributeError: - return colored(expr.name, "cyan") - - -class WalkMapper(WM): - def __init__(self): - super(WalkMapper, self).__init__() - - def map_recipe(self, expr, *args, **kwargs): - if not self.visit(expr, *args, **kwargs): - return - for indices in expr.indices: - for index in indices: - self.rec(index, *args, **kwargs) - self.rec(expr.body, *args, **kwargs) - self.post_visit(expr, *args, **kwargs) - - def map_index(self, expr, *args, **kwargs): - if not self.visit(expr, *args, **kwargs): - return - - # I don't want to recur on the extent. That's ugly. - - self.post_visit(expr, *args, **kwargs) - - def map_index_sum(self, expr, *args, **kwargs): - if not self.visit(expr, *args, **kwargs): - return - for index in expr.indices: - self.rec(index, *args, **kwargs) - self.rec(expr.body, *args, **kwargs) - self.post_visit(expr, *args, **kwargs) - - map_delta = map_index_sum - map_let = map_index_sum - map_for_all = map_index_sum - map_wave = map_index_sum - map_levi_civita = map_index_sum - map_inverse = map_index_sum - map_det = map_index_sum - map_compound_vector = map_index_sum - - -class GraphvizMapper(WalkMapper, GVM): - pass - - class StringifyMixin(object): """Mixin class to set stringification options correctly for pymbolic subclasses.""" @@ -204,7 +25,8 @@ def __str__(self): return self.stringifier()()(self, PREC_NONE, indent=0) def stringifier(self): - return _StringifyMapper + from . import mappers + return mappers._StringifyMapper @property def name(self): @@ -217,8 +39,19 @@ def set_error(self): self._error = True -class Array(p.Variable): - """A pymbolic variable of known extent.""" +class Variable(p.Variable): + """A symbolic variable.""" + def __init__(self, name): + super(Variable, self).__init__(name) + + self._color = "cyan" + + def set_error(self): + self._error = True + + +class Array(Variable): + """A symbolic variable of known extent.""" def __init__(self, name, shape): super(Array, self).__init__(name) @@ -277,6 +110,7 @@ def replace_indices(self, replacements): if not isinstance(replacements, dict): replacements = {a: b for (a, b) in replacements} + from mappers import _IndexMapper return _IndexMapper(replacements)(self) @@ -287,7 +121,9 @@ class IndexSum(StringifyMixin, p._MultiChildExpression): :param body: the expression to sum. """ def __init__(self, indices, body): - + + # Inline import to avoid circular dependency. + from indices import IndexBase if isinstance(indices[0], IndexBase): indices = tuple(indices) else: diff --git a/finat/bernstein.py b/finat/bernstein.py index c7efc1359..004e3bd11 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -1,8 +1,7 @@ -from finiteelementbase import FiniteElementBase -from points import StroudPointSet -from ast import ForAll, Recipe, Wave, Let, IndexSum -import pymbolic.primitives as p -from indices import BasisFunctionIndex, PointIndex, SimpliciallyGradedBasisFunctionIndex # noqa +from .finiteelementbase import FiniteElementBase +from .points import StroudPointSet +from .ast import ForAll, Recipe, Wave, Let, IndexSum, Variable +from .indices import BasisFunctionIndex, PointIndex, SimpliciallyGradedBasisFunctionIndex # noqa import numpy as np @@ -41,7 +40,7 @@ def _points_variable(self, points, kernel_data): if static_key in kernel_data.static: xi = kernel_data.static[static_key][0] else: - xi = p.Variable(kernel_data.point_variable_name(points)) + xi = Variable(kernel_data.point_variable_name(points)) kernel_data.static[static_key] = (xi, lambda: points.points) return xi @@ -54,7 +53,7 @@ def _weights_variable(self, weights, kernel_data): if static_key in kernel_data.static: wt = kernel_data.static[static_key][0] else: - wt = p.Variable(kernel_data.weight_variable_name(weights)) + wt = Variable(kernel_data.weight_variable_name(weights)) kernel_data.static[static_key] = (wt, lambda: np.array(weights)) return wt @@ -88,8 +87,8 @@ def field_evaluation(self, field_var, q, kernel_data, derivative=None): r = kernel_data.new_variable("r") w = kernel_data.new_variable("w") -# r = p.Variable("r") -# w = p.Variable("w") +# r = Variable("r") +# w = Variable("w") tmps = [kernel_data.new_variable("tmp") for d in range(sd - 1)] # Create basis function indices that run over diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 760aed245..8627b66df 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -1,6 +1,5 @@ -import pymbolic.primitives as p import numpy as np -from ast import Recipe, IndexSum +from ast import Recipe, IndexSum, Variable class UndefinedError(Exception): @@ -134,7 +133,7 @@ def field_evaluation(self, field_var, q, kernel_data.kernel_args.add(field_var) basis = self.basis_evaluation(q, kernel_data, derivative, pullback) - (d, b, p_) = basis.indices + (d, b, p) = basis.indices phi = basis.body expr = IndexSum(b, field_var[b[0]] * phi) @@ -213,8 +212,8 @@ def _tabulated_basis(self, points, kernel_data, derivative): if static_key in kernel_data.static: phi = kernel_data.static[static_key][0] else: - phi = p.Variable(("d" if derivative else "") + - kernel_data.tabulation_variable_name(self, points)) + phi = Variable(("d" if derivative else "") + + kernel_data.tabulation_variable_name(self, points)) data = self._tabulate(points, derivative) kernel_data.static[static_key] = (phi, lambda: data) diff --git a/finat/geometry_mapper.py b/finat/geometry_mapper.py index ecac03798..0c233e6a6 100644 --- a/finat/geometry_mapper.py +++ b/finat/geometry_mapper.py @@ -1,7 +1,8 @@ -from points import PointSet -from indices import PointIndex -from ast import Let, Det, Inverse, Recipe, IdentityMapper -from derivatives import grad +from .points import PointSet +from .indices import PointIndex +from .ast import Let, Det, Inverse, Recipe +from .mappers import IdentityMapper +from .derivatives import grad class GeometryMapper(IdentityMapper): diff --git a/finat/indices.py b/finat/indices.py index 9520a020c..8828a037c 100644 --- a/finat/indices.py +++ b/finat/indices.py @@ -1,9 +1,10 @@ +from . import ast import pymbolic.primitives as p from pymbolic.mapper.stringifier import StringifyMapper import math -class IndexBase(p.Variable): +class IndexBase(ast.Variable): '''Base class for symbolic index objects.''' def __init__(self, extent, name): super(IndexBase, self).__init__(name) diff --git a/finat/interpreter.py b/finat/interpreter.py index 5351f47bc..2c86dfdb5 100644 --- a/finat/interpreter.py +++ b/finat/interpreter.py @@ -46,8 +46,10 @@ def map_recipe(self, expr): d, b, p = expr.indices + free_indices = [i for i in d + b + p if i not in self.indices] + try: - forall = ForAll(d + b + p, expr.body) + forall = ForAll(free_indices, expr.body) return self.rec(forall) except: if hasattr(forall, "_error"): diff --git a/finat/mappers.py b/finat/mappers.py new file mode 100644 index 000000000..4e743e746 --- /dev/null +++ b/finat/mappers.py @@ -0,0 +1,185 @@ +from pymbolic.mapper import IdentityMapper as IM +from pymbolic.mapper.stringifier import StringifyMapper, PREC_NONE +from pymbolic.mapper import WalkMapper as WM +from pymbolic.mapper.graphviz import GraphvizMapper as GVM +from .indices import IndexBase +try: + from termcolor import colored +except ImportError: + def colored(string, color, attrs=[]): + return string + + +class IdentityMapper(IM): + def __init__(self): + super(IdentityMapper, self).__init__() + + def map_recipe(self, expr, *args): + return expr.__class__(self.rec(expr.indices, *args), + self.rec(expr.body, *args)) + + def map_index(self, expr, *args): + return expr + + def map_delta(self, expr, *args): + return expr.__class__(*(self.rec(c, *args) for c in expr.children)) + + map_let = map_delta + map_for_all = map_delta + map_wave = map_delta + map_index_sum = map_delta + map_levi_civita = map_delta + map_inverse = map_delta + map_det = map_delta + map_compound_vector = map_delta + + +class _IndexMapper(IdentityMapper): + def __init__(self, replacements): + super(_IndexMapper, self).__init__() + + self.replacements = replacements + + def map_index(self, expr, *args): + '''Replace indices if they are in the replacements list''' + + try: + return(self.replacements[expr]) + except KeyError: + return expr + + +class _StringifyMapper(StringifyMapper): + + def map_recipe(self, expr, enclosing_prec, indent=None, *args, **kwargs): + if indent is None: + fmt = expr.name + "(%s, %s)" + else: + oldidt = " " * indent + indent += 4 + idt = " " * indent + fmt = expr.name + "(%s,\n" + idt + "%s\n" + oldidt + ")" + + return self.format(fmt, + self.rec(expr.indices, PREC_NONE, indent=indent, *args, **kwargs), + self.rec(expr.body, PREC_NONE, indent=indent, *args, **kwargs)) + + def map_let(self, expr, enclosing_prec, indent=None, *args, **kwargs): + if indent is None: + fmt = expr.name + "(%s, %s)" + inner_indent = None + else: + oldidt = " " * indent + indent += 4 + inner_indent = indent + 4 + inner_idt = " " * inner_indent + idt = " " * indent + fmt = expr.name + "(\n" + inner_idt + "%s,\n" + idt + "%s\n" + oldidt + ")" + + return self.format(fmt, + self.rec(expr.bindings, PREC_NONE, indent=inner_indent, *args, **kwargs), + self.rec(expr.body, PREC_NONE, indent=indent, *args, **kwargs)) + + def map_delta(self, expr, *args, **kwargs): + return self.format(expr.name + "(%s, %s)", + *[self.rec(c, *args, **kwargs) for c in expr.children]) + + def map_index(self, expr, *args, **kwargs): + if hasattr(expr, "_error"): + return colored(str(expr), "red", attrs=["bold"]) + else: + return colored(str(expr), expr._color) + + def map_wave(self, expr, enclosing_prec, indent=None, *args, **kwargs): + if indent is None or enclosing_prec is not PREC_NONE: + fmt = expr.name + "(%s %s) " + else: + oldidt = " " * indent + indent += 4 + idt = " " * indent + fmt = expr.name + "(%s\n" + idt + "%s\n" + oldidt + ")" + + return self.format(fmt, + " ".join(self.rec(c, PREC_NONE, *args, **kwargs) + "," for c in expr.children[:-1]), + self.rec(expr.children[-1], PREC_NONE, indent=indent, *args, **kwargs)) + + def map_index_sum(self, expr, enclosing_prec, indent=None, *args, **kwargs): + if indent is None or enclosing_prec is not PREC_NONE: + fmt = expr.name + "((%s), %s) " + else: + oldidt = " " * indent + indent += 4 + idt = " " * indent + fmt = expr.name + "((%s),\n" + idt + "%s\n" + oldidt + ")" + + return self.format(fmt, + " ".join(self.rec(c, PREC_NONE, *args, **kwargs) + "," for c in expr.children[0]), + self.rec(expr.children[1], PREC_NONE, indent=indent, *args, **kwargs)) + + def map_levi_civita(self, expr, *args, **kwargs): + return self.format(expr.name + "(%s)", + self.join_rec(", ", expr.children, *args, **kwargs)) + + def map_inverse(self, expr, *args, **kwargs): + return self.format(expr.name + "(%s)", + self.rec(expr.expression, *args, **kwargs)) + + def map_det(self, expr, *args, **kwargs): + return self.format(expr.name + "(%s)", + self.rec(expr.expression, *args, **kwargs)) + + def map_compound_vector(self, expr, *args, **kwargs): + return self.format(expr.name + "(%s)", + self.join_rec(", ", expr.children, *args, **kwargs)) + + def map_variable(self, expr, enclosing_prec, *args, **kwargs): + if hasattr(expr, "_error"): + return colored(str(expr.name), "red", attrs=["bold"]) + else: + try: + return colored(expr.name, expr._color) + except AttributeError: + return colored(expr.name, "cyan") + + +class WalkMapper(WM): + def __init__(self): + super(WalkMapper, self).__init__() + + def map_recipe(self, expr, *args, **kwargs): + if not self.visit(expr, *args, **kwargs): + return + for indices in expr.indices: + for index in indices: + self.rec(index, *args, **kwargs) + self.rec(expr.body, *args, **kwargs) + self.post_visit(expr, *args, **kwargs) + + def map_index(self, expr, *args, **kwargs): + if not self.visit(expr, *args, **kwargs): + return + + # I don't want to recur on the extent. That's ugly. + + self.post_visit(expr, *args, **kwargs) + + def map_index_sum(self, expr, *args, **kwargs): + if not self.visit(expr, *args, **kwargs): + return + for index in expr.indices: + self.rec(index, *args, **kwargs) + self.rec(expr.body, *args, **kwargs) + self.post_visit(expr, *args, **kwargs) + + map_delta = map_index_sum + map_let = map_index_sum + map_for_all = map_index_sum + map_wave = map_index_sum + map_levi_civita = map_index_sum + map_inverse = map_index_sum + map_det = map_index_sum + map_compound_vector = map_index_sum + + +class GraphvizMapper(WalkMapper, GVM): + pass diff --git a/finat/points.py b/finat/points.py index 0a6d283c2..a5aa34b6f 100644 --- a/finat/points.py +++ b/finat/points.py @@ -1,5 +1,5 @@ import numpy -import pymbolic.primitives as p +from .ast import Variable class PointSetBase(object): @@ -47,7 +47,7 @@ def kernel_variable(self, name, kernel_data): if static_key in static_data: w = static_data[static_key][0] else: - w = p.Variable(name) + w = Variable(name) data = self._points static_data[static_key] = (w, lambda: data) diff --git a/finat/pyop2_interface.py b/finat/pyop2_interface.py index 28ffd8428..1f0c330a3 100644 --- a/finat/pyop2_interface.py +++ b/finat/pyop2_interface.py @@ -5,9 +5,9 @@ from .interpreter import evaluate -def pyop2_kernel(kernel, kernel_args, interpreter=False): - """Return a :class:`pyop2.Kernel` from the recipe and kernel data - provided. +def pyop2_kernel_function(kernel, kernel_args, interpreter=False): + """Return a python function suitable to be called from PyOP2 from the + recipe and kernel data provided. :param kernel: The :class:`~.utils.Kernel` to map to PyOP2. :param kernel_args: The ordered list of Pymbolic variables constituting @@ -17,7 +17,8 @@ def pyop2_kernel(kernel, kernel_args, interpreter=False): evaluated using the FInAT interpreter instead of generating a compiled kernel. - :result: The :class:`pyop2.Kernel` + :result: A function which will execute the kernel. + """ if Kernel is None: @@ -30,11 +31,11 @@ def pyop2_kernel(kernel, kernel_args, interpreter=False): if interpreter: def kernel_function(*args): - context = {kernel_args: args[1:]} + context = dict(zip(kernel_args, args[1:])) args[0][:] = evaluate(kernel.recipe, context, kernel.kernel_data) - return Kernel(kernel_function) + return kernel_function else: raise NotImplementedError diff --git a/finat/ufl-interface.py b/finat/ufl_interface.py similarity index 84% rename from finat/ufl-interface.py rename to finat/ufl_interface.py index 59e70ca99..d6e80cd8a 100644 --- a/finat/ufl-interface.py +++ b/finat/ufl_interface.py @@ -15,7 +15,7 @@ def cell_from_ufl(cell): - return _cell_map[cell.name()] + return _cell_map[cell.cellname()] def element_from_ufl(element): @@ -23,4 +23,4 @@ def element_from_ufl(element): # Need to handle the product cases. return _element_map[element.family()](cell_from_ufl(element.cell()), - element.degree) + element.degree()) diff --git a/finat/utils.py b/finat/utils.py index a2d216d94..bf785a6a0 100644 --- a/finat/utils.py +++ b/finat/utils.py @@ -1,8 +1,7 @@ -import pymbolic.primitives as p import inspect import FIAT -from derivatives import grad -from ast import Let, Array, Inverse, Det +from .derivatives import grad +from .ast import Let, Array, Inverse, Det, Variable class Kernel(object): @@ -38,7 +37,7 @@ def __init__(self, coordinate_element, coordinate_var=None, affine=None): self.static = {} # The set of undefined symbols in this kernel. - self.kernel_args = set(coordinate_var) if coordinate_var else set() + self.kernel_args = set((coordinate_var,)) if coordinate_var else set() self.geometry = {} self.variables = set() @@ -107,7 +106,7 @@ def new_variable(self, prefix=None): name = prefix or "tmp" if name not in self.variables: self.variables.add(name) - return p.Variable(name) + return Variable(name) # Prefix was already in use, so append an index. i = 0 @@ -115,7 +114,7 @@ def new_variable(self, prefix=None): varname = "%s_%d" % (name, i) if varname not in self.variables: self.variables.add(varname) - return p.Variable(varname) + return Variable(varname) i += 1 @property @@ -160,7 +159,7 @@ def detJ(self): try: return self.geometry["detJ"] except KeyError: - self.geometry["detJ"] = p.Variable("detJ") + self.geometry["detJ"] = Variable("detJ") return self.geometry["detJ"] def bind_geometry(self, expression, points=None): diff --git a/finat/vectorfiniteelement.py b/finat/vectorfiniteelement.py index 6a938ad64..099911cda 100644 --- a/finat/vectorfiniteelement.py +++ b/finat/vectorfiniteelement.py @@ -107,7 +107,7 @@ def field_evaluation(self, field_var, q, \nabla\cdot\boldsymbol{f}_{q} = \sum_{i \alpha} f_{i \alpha}\nabla\phi_{\alpha i q} """ - self.kernel_args.add(field_var) + kernel_data.kernel_args.add(field_var) # Produce the base scalar recipe. The scalar basis can only # take a grad. For other derivatives, we need to do the From 174b2641539493f45aa57c3691e2c0ce552550b3 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Sun, 1 Mar 2015 13:58:53 +0000 Subject: [PATCH 132/749] fecking pep8 --- finat/ast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/ast.py b/finat/ast.py index 957597d83..91f5d3354 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -121,7 +121,7 @@ class IndexSum(StringifyMixin, p._MultiChildExpression): :param body: the expression to sum. """ def __init__(self, indices, body): - + # Inline import to avoid circular dependency. from indices import IndexBase if isinstance(indices[0], IndexBase): From 5e82f5a3d4d30d6f454db9a27ae1923eefc54362 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Sun, 1 Mar 2015 15:23:51 +0000 Subject: [PATCH 133/749] Increment, don't replace --- finat/pyop2_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/finat/pyop2_interface.py b/finat/pyop2_interface.py index 1f0c330a3..9cb43783a 100644 --- a/finat/pyop2_interface.py +++ b/finat/pyop2_interface.py @@ -31,9 +31,9 @@ def pyop2_kernel_function(kernel, kernel_args, interpreter=False): if interpreter: def kernel_function(*args): - context = dict(zip(kernel_args, args[1:])) + context = {var.name: val for (var, val) in zip(kernel_args, args[1:])} - args[0][:] = evaluate(kernel.recipe, context, kernel.kernel_data) + args[0][:] += evaluate(kernel.recipe, context, kernel.kernel_data) return kernel_function From 92af3163331d0b3505f9a072befde41f47702c95 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Sun, 1 Mar 2015 15:24:08 +0000 Subject: [PATCH 134/749] Det and Abs fixes --- finat/ast.py | 33 +++++++++++++++++++++++++++------ finat/finiteelementbase.py | 4 ++-- finat/interpreter.py | 8 ++++++++ finat/mappers.py | 9 +++++++-- 4 files changed, 44 insertions(+), 10 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index 91f5d3354..91417a497 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -184,7 +184,7 @@ class ForAll(StringifyMixin, p._MultiChildExpression): """ def __init__(self, indices, body): - self.children = (indices, body) + self.children = (tuple(indices), body) self._color = "blue" def __getinitargs__(self): @@ -277,23 +277,44 @@ def __init__(self, expression): self.expression = expression self._color = "blue" + super(Inverse, self).__init__() + + def __getinitargs__(self): + return (self.expression,) + mapper_method = "map_inverse" class Det(StringifyMixin, p.Expression): - """The determinant of a matrix-valued expression. - - Where the expression is evaluated at a number of points, the - inverse will be evaluated pointwise. - """ + """The determinant of a matrix-valued expression.""" def __init__(self, expression): self.expression = expression self._color = "blue" + super(Det, self).__init__() + + def __getinitargs__(self): + return (self.expression,) + mapper_method = "map_det" +class Abs(StringifyMixin, p.Expression): + """The absolute value of an expression.""" + def __init__(self, expression): + + self.expression = expression + self._color = "blue" + + super(Abs, self).__init__() + + def __getinitargs__(self): + return (self.expression,) + + mapper_method = "map_abs" + + class CompoundVector(StringifyMixin, p.Expression): """A vector expression composed by concatenating other expressions.""" def __init__(self, index, indices, expressions): diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 8627b66df..697c4f019 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -1,5 +1,5 @@ import numpy as np -from ast import Recipe, IndexSum, Variable +from ast import Recipe, IndexSum, Variable, Abs class UndefinedError(Exception): @@ -155,7 +155,7 @@ def moment_evaluation(self, value, weights, q, expr = psi * phi * w[p__] if pullback: - expr *= kernel_data.detJ + expr *= Abs(kernel_data.detJ) return Recipe(((), b + b_, ()), IndexSum(p__, expr)) diff --git a/finat/interpreter.py b/finat/interpreter.py index 2c86dfdb5..d159be6c7 100644 --- a/finat/interpreter.py +++ b/finat/interpreter.py @@ -274,6 +274,14 @@ def map_levi_civita(self, expr): expr.set_error() raise NotImplementedError + def map_det(self, expr): + + return np.linalg.det(self.rec(expr.expression)) + + def map_abs(self, expr): + + return abs(self.rec(expr.expression)) + def evaluate(expression, context={}, kernel_data=None): """Take a FInAT expression and a set of definitions for undefined diff --git a/finat/mappers.py b/finat/mappers.py index 4e743e746..2a62f2050 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -24,14 +24,17 @@ def map_index(self, expr, *args): def map_delta(self, expr, *args): return expr.__class__(*(self.rec(c, *args) for c in expr.children)) + def map_inverse(self, expr, *args): + return expr.__class__(self.rec(expr.expression, *args)) + map_let = map_delta map_for_all = map_delta map_wave = map_delta map_index_sum = map_delta map_levi_civita = map_delta - map_inverse = map_delta - map_det = map_delta map_compound_vector = map_delta + map_det = map_inverse + map_abs = map_inverse class _IndexMapper(IdentityMapper): @@ -128,6 +131,8 @@ def map_det(self, expr, *args, **kwargs): return self.format(expr.name + "(%s)", self.rec(expr.expression, *args, **kwargs)) + map_abs = map_det + def map_compound_vector(self, expr, *args, **kwargs): return self.format(expr.name + "(%s)", self.join_rec(", ", expr.children, *args, **kwargs)) From 18f6616f7deae7ca0dcb42957a1bef5a5ac8cc69 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 3 Mar 2015 18:44:13 +0000 Subject: [PATCH 135/749] Interpreter: Implement inverse as numpy.linalg.inv --- finat/interpreter.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/finat/interpreter.py b/finat/interpreter.py index d159be6c7..1d9a827e2 100644 --- a/finat/interpreter.py +++ b/finat/interpreter.py @@ -282,6 +282,10 @@ def map_abs(self, expr): return abs(self.rec(expr.expression)) + def map_inverse(self, expr): + + return np.linalg.inv(self.rec(expr.expression)) + def evaluate(expression, context={}, kernel_data=None): """Take a FInAT expression and a set of definitions for undefined From a5a7e4f9f960a2f99794c734a9033393ccd3b97d Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 3 Mar 2015 18:44:58 +0000 Subject: [PATCH 136/749] GeometryMapper: Fix inner lets with both invJ and detJ --- finat/geometry_mapper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/finat/geometry_mapper.py b/finat/geometry_mapper.py index 0c233e6a6..276a9b657 100644 --- a/finat/geometry_mapper.py +++ b/finat/geometry_mapper.py @@ -76,8 +76,8 @@ def _bind_geometry(self, q, body): element = kd.coordinate_element J = element.field_evaluation(phi_x, q, kd, grad, pullback=False) - inner_lets = (((kd.detJ, Det(kd.J)),) if kd.detJ in self.local_geometry else () + - ((kd.invJ, Inverse(kd.J)),) if kd.invJ in self.local_geometry else ()) + inner_lets = ((kd.detJ, Det(kd.J)),) if kd.detJ in self.local_geometry else () + inner_lets += ((kd.invJ, Inverse(kd.J)),) if kd.invJ in self.local_geometry else () # The local geometry goes out of scope at this point. self.local_geometry = set() From a91200c0ac273602f10bff8de91ea98e22c9d67e Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 3 Mar 2015 18:55:18 +0000 Subject: [PATCH 137/749] StringifyMapper: Visualise ForAll by reusing recipe method --- finat/ast.py | 4 +++- finat/interpreter.py | 2 +- finat/mappers.py | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index 91417a497..ebf12b013 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -184,7 +184,9 @@ class ForAll(StringifyMixin, p._MultiChildExpression): """ def __init__(self, indices, body): - self.children = (tuple(indices), body) + self.indices = indices + self.body = body + self.children = (self.indices, self.body) self._color = "blue" def __getinitargs__(self): diff --git a/finat/interpreter.py b/finat/interpreter.py index 1d9a827e2..85c09bb76 100644 --- a/finat/interpreter.py +++ b/finat/interpreter.py @@ -46,7 +46,7 @@ def map_recipe(self, expr): d, b, p = expr.indices - free_indices = [i for i in d + b + p if i not in self.indices] + free_indices = tuple([i for i in d + b + p if i not in self.indices]) try: forall = ForAll(free_indices, expr.body) diff --git a/finat/mappers.py b/finat/mappers.py index 2a62f2050..5e525f648 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -67,6 +67,8 @@ def map_recipe(self, expr, enclosing_prec, indent=None, *args, **kwargs): self.rec(expr.indices, PREC_NONE, indent=indent, *args, **kwargs), self.rec(expr.body, PREC_NONE, indent=indent, *args, **kwargs)) + map_for_all = map_recipe + def map_let(self, expr, enclosing_prec, indent=None, *args, **kwargs): if indent is None: fmt = expr.name + "(%s, %s)" From 5bb1cccaa21a8db0bbb2777f7c66698837f82bca Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 4 Mar 2015 09:03:32 +0000 Subject: [PATCH 138/749] BindingMapper: Inserts ForAlls that bind free indices in Recipes --- finat/mappers.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/finat/mappers.py b/finat/mappers.py index 5e525f648..02820ed8a 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -3,6 +3,7 @@ from pymbolic.mapper import WalkMapper as WM from pymbolic.mapper.graphviz import GraphvizMapper as GVM from .indices import IndexBase +from .ast import Recipe, ForAll, IndexSum try: from termcolor import colored except ImportError: @@ -190,3 +191,38 @@ def map_index_sum(self, expr, *args, **kwargs): class GraphvizMapper(WalkMapper, GVM): pass + + +class BindingMapper(IdentityMapper): + """A mapper that binds free indices in recipes using ForAlls.""" + + def __init__(self, kernel_data): + """ + :arg context: a mapping from variable names to values + """ + super(BindingMapper, self).__init__() + self.bound_above = set() + self.bound_below = set() + + def map_recipe(self, expr): + body = self.rec(expr.body) + + d, b, p = expr.indices + free_indices = tuple([i for i in d + b + p + if i not in self.bound_below and + i not in self.bound_above]) + + if len(free_indices) > 0: + expr = Recipe(expr.indices, ForAll(free_indices, body)) + + return expr + + def map_index_sum(self, expr): + indices = expr.indices + for idx in indices: + self.bound_above.add(idx) + body = self.rec(expr.body) + for idx in indices: + self.bound_above.remove(idx) + self.bound_below.add(idx) + return IndexSum(indices, body) From 92fb887ea5ac563427defb527114b8dbf91404f3 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 4 Mar 2015 09:04:14 +0000 Subject: [PATCH 139/749] Interpreter: Use BindingMapper instead of implicit ForAlls in Recipes --- finat/interpreter.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/finat/interpreter.py b/finat/interpreter.py index 85c09bb76..c97c2f88b 100644 --- a/finat/interpreter.py +++ b/finat/interpreter.py @@ -2,6 +2,7 @@ performant, but rather to provide a test facility for FInAT code.""" import pymbolic.primitives as p from pymbolic.mapper.evaluator import FloatEvaluationMapper, UnknownVariableError +from .mappers import BindingMapper from ast import IndexSum, ForAll, LeviCivita, FInATSyntaxError from indices import TensorPointIndex import numpy as np @@ -44,17 +45,7 @@ def map_variable(self, expr): def map_recipe(self, expr): """Evaluate expr for all values of free indices""" - d, b, p = expr.indices - - free_indices = tuple([i for i in d + b + p if i not in self.indices]) - - try: - forall = ForAll(free_indices, expr.body) - return self.rec(forall) - except: - if hasattr(forall, "_error"): - expr.set_error() - raise + return self.rec(expr.body) def map_index_sum(self, expr): @@ -300,6 +291,7 @@ def evaluate(expression, context={}, kernel_data=None): context[var[0].name] = var[1]() try: + expression = BindingMapper(context)(expression) return FinatEvaluationMapper(context)(expression) except: print expression From 101afc1980229ebc4561ca892830176e7fdc2d78 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 4 Mar 2015 11:32:19 +0000 Subject: [PATCH 140/749] Actually sum over the inner product in moment evaluation --- finat/finiteelementbase.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 697c4f019..860dba226 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -154,6 +154,9 @@ def moment_evaluation(self, value, weights, q, expr = psi * phi * w[p__] + if d: + expr = IndexSum(d, expr) + if pullback: expr *= Abs(kernel_data.detJ) From 02c5b616aa446190b0e56c753b43d2da2e1ea9ef Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 4 Mar 2015 13:25:12 +0000 Subject: [PATCH 141/749] BindingMapper: Make mapper idempotent by recording indices in ForAlls --- finat/mappers.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/finat/mappers.py b/finat/mappers.py index 02820ed8a..7f64f8a90 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -224,5 +224,14 @@ def map_index_sum(self, expr): body = self.rec(expr.body) for idx in indices: self.bound_above.remove(idx) - self.bound_below.add(idx) return IndexSum(indices, body) + + def map_for_all(self, expr): + indices = expr.indices + for idx in indices: + self.bound_above.add(idx) + body = self.rec(expr.body) + for idx in indices: + self.bound_above.remove(idx) + self.bound_below.add(idx) + return ForAll(indices, body) From 9961257b2fb06886cc7919b3268283fd7a5fee25 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 26 Feb 2015 10:05:05 +0000 Subject: [PATCH 142/749] Inverse: Add .children so Mappers deal with it correctly --- finat/ast.py | 1 + 1 file changed, 1 insertion(+) diff --git a/finat/ast.py b/finat/ast.py index ebf12b013..3fdc0e0fa 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -277,6 +277,7 @@ class Inverse(StringifyMixin, p.Expression): """ def __init__(self, expression): self.expression = expression + self.children = [expression] self._color = "blue" super(Inverse, self).__init__() From f45343b30f7cf3804f4feb5796b4c25ee2f5a558 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 16 Feb 2015 11:00:04 +0000 Subject: [PATCH 143/749] IndexSumMapper: Add mapper that binds IndexSums to temporary variables This may also later be used to do loop-unrolling fir IndexSums over DimensionIndices. --- finat/mappers.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/finat/mappers.py b/finat/mappers.py index 7f64f8a90..d47a3236e 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -235,3 +235,45 @@ def map_for_all(self, expr): self.bound_above.remove(idx) self.bound_below.add(idx) return ForAll(indices, body) + + +class IndexSumMapper(IdentityMapper): + """A mapper that binds unbound IndexSums to temporary variables + using Lets.""" + + def __init__(self, kernel_data): + """ + :arg context: a mapping from variable names to values + """ + super(IndexSumMapper, self).__init__() + self.kernel_data = kernel_data + self._isum_stack = {} + self._bound_isums = set() + + def map_recipe(self, expr): + indices = expr.indices + expr = self.rec(expr.body) + while len(self._isum_stack) > 0: + temp, isum = self._isum_stack.popitem() + tmap = (temp, isum) + expr = Let((tmap,), expr) + return Recipe(indices, expr) + + def map_let(self, expr): + # Record IndexSums already bound to a temporary + for v, e in expr.bindings: + if isinstance(e, IndexSum): + self._bound_isums.add(e) + elif isinstance(e, Recipe) and isinstance(e.body, IndexSum): + self._bound_isums.add(e.body) + return super(IndexSumMapper, self).map_let(expr) + + def map_index_sum(self, expr): + if expr in self._bound_isums: + return super(IndexSumMapper, self).map_index_sum(expr) + + # Replace IndexSum with temporary and add to stack + temp = self.kernel_data.new_variable("isum") + expr = IndexSum(expr.indices, self.rec(expr.body)) + self._isum_stack[temp] = expr + return temp From 0643e1a5b45598f9b258cd06a19a9c61ff02c58c Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 26 Feb 2015 11:06:48 +0000 Subject: [PATCH 144/749] IndexSumMapper: Bind IndexSums within Recipes, IndexSums and Lets --- finat/mappers.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/finat/mappers.py b/finat/mappers.py index d47a3236e..160988d6d 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -3,7 +3,7 @@ from pymbolic.mapper import WalkMapper as WM from pymbolic.mapper.graphviz import GraphvizMapper as GVM from .indices import IndexBase -from .ast import Recipe, ForAll, IndexSum +from .ast import Recipe, ForAll, IndexSum, Let try: from termcolor import colored except ImportError: @@ -250,23 +250,29 @@ def __init__(self, kernel_data): self._isum_stack = {} self._bound_isums = set() - def map_recipe(self, expr): - indices = expr.indices - expr = self.rec(expr.body) + def _bind_isums(self, expr): while len(self._isum_stack) > 0: temp, isum = self._isum_stack.popitem() tmap = (temp, isum) expr = Let((tmap,), expr) - return Recipe(indices, expr) + return expr + + def map_recipe(self, expr): + body = self._bind_isums(self.rec(expr.body)) + return Recipe(expr.indices, body) def map_let(self, expr): # Record IndexSums already bound to a temporary + new_bindings = [] for v, e in expr.bindings: if isinstance(e, IndexSum): self._bound_isums.add(e) elif isinstance(e, Recipe) and isinstance(e.body, IndexSum): self._bound_isums.add(e.body) - return super(IndexSumMapper, self).map_let(expr) + new_bindings.append((v, self.rec(e))) + + body = self._bind_isums(self.rec(expr.body)) + return Let(tuple(new_bindings), body) def map_index_sum(self, expr): if expr in self._bound_isums: @@ -274,6 +280,7 @@ def map_index_sum(self, expr): # Replace IndexSum with temporary and add to stack temp = self.kernel_data.new_variable("isum") - expr = IndexSum(expr.indices, self.rec(expr.body)) + body = self._bind_isums(self.rec(expr.body)) + expr = IndexSum(expr.indices, body) self._isum_stack[temp] = expr return temp From 53e7277f474d96015821ff6b1bbccffb57d19feb Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 5 Mar 2015 14:00:21 +0000 Subject: [PATCH 145/749] IndexSumMapper: Only insert bindings when we encounter a temporary --- finat/mappers.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/finat/mappers.py b/finat/mappers.py index 160988d6d..c89cfde4b 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -3,7 +3,7 @@ from pymbolic.mapper import WalkMapper as WM from pymbolic.mapper.graphviz import GraphvizMapper as GVM from .indices import IndexBase -from .ast import Recipe, ForAll, IndexSum, Let +from .ast import Recipe, ForAll, IndexSum, Let, Variable try: from termcolor import colored except ImportError: @@ -251,10 +251,22 @@ def __init__(self, kernel_data): self._bound_isums = set() def _bind_isums(self, expr): - while len(self._isum_stack) > 0: - temp, isum = self._isum_stack.popitem() - tmap = (temp, isum) - expr = Let((tmap,), expr) + bindings = [] + if isinstance(expr, Variable): + children = (expr,) + elif hasattr(expr, "children"): + children = expr.children + else: + return expr + + for temp in children: + if temp in self._isum_stack: + isum = self._isum_stack[temp] + bindings.append((temp, isum)) + for temp, isum in bindings: + del self._isum_stack[temp] + if len(bindings) > 0: + expr = Let(tuple(bindings), expr) return expr def map_recipe(self, expr): From 3f767b82997675b9b342c5e7f9154422d436757c Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 4 Feb 2015 08:20:52 +0100 Subject: [PATCH 146/749] Coffee: Add tests for a skeleton coffee compiler --- finat/__init__.py | 1 + finat/coffee_compiler.py | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 finat/coffee_compiler.py diff --git a/finat/__init__.py b/finat/__init__.py index 56bec9b39..f230aea8f 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -10,3 +10,4 @@ import quadrature import ufl_interface from geometry_mapper import GeometryMapper +import coffee_compiler diff --git a/finat/coffee_compiler.py b/finat/coffee_compiler.py new file mode 100644 index 000000000..fda6aa526 --- /dev/null +++ b/finat/coffee_compiler.py @@ -0,0 +1,6 @@ +"""A testing utility that compiles and executes COFFEE ASTs to +evaluate a given recipe. Provides the same interface as FInAT's +internal interpreter. """ + +def evaluate(expression, context={}, kernel_data=None): + print expression From 178a4846e5e53ec7126ab9537734b85c3e6c2022 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 5 Feb 2015 16:36:55 +0100 Subject: [PATCH 147/749] Coffee: First draft of a coffee-AST compiler/interpreter This packs free indices and context data into argument lists, generates a kernel function header around an empty kernel, compiles and loads the kernel and finally applies the kernel function to the context data. --- finat/coffee_compiler.py | 79 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/finat/coffee_compiler.py b/finat/coffee_compiler.py index fda6aa526..4bf7adb5e 100644 --- a/finat/coffee_compiler.py +++ b/finat/coffee_compiler.py @@ -2,5 +2,82 @@ evaluate a given recipe. Provides the same interface as FInAT's internal interpreter. """ +import coffee.base as coffee +import os +import subprocess +import ctypes +import numpy as np +from .utils import Kernel + + +class CoffeeKernel(Kernel): + + def generate_ast(self, context): + kernel_args = self.kernel_data.kernel_args + args_ast = [] + + # Generate declaration of result argument + result_shape = () + for index in self.recipe.indices: + for i in index: + result_shape += (i.extent.stop,) + result_ast = coffee.Symbol(kernel_args[0], result_shape) + args_ast.append(coffee.Decl("double", result_ast)) + + # Add argument declarations + for var in kernel_args[1:]: + var_ast = coffee.Symbol(str(var), context[var].shape) + args_ast.append(coffee.Decl("double", var_ast)) + + body = coffee.EmptyStatement(None, None) + return coffee.FunDecl("void", "coffee_kernel", args_ast, + body, headers=["stdio.h"]) + + def evaluate(expression, context={}, kernel_data=None): - print expression + index_shape = () + args_data = [] + + # Pack free indices as kernel arguments + for index in expression.indices: + for i in index: + index_shape += (i.extent.stop, ) + index_data = np.empty(index_shape, dtype=np.double) + args_data.append(index_data.ctypes.data) + kernel_data.kernel_args = ["A"] + + # Pack context arguments + for var, value in context.iteritems(): + kernel_data.kernel_args.append(var) + args_data.append(value.ctypes.data) + + # Generate kernel function + kernel = CoffeeKernel(expression, kernel_data).generate_ast(context) + basename = os.path.join(os.getcwd(), "coffee_kernel") + with file(basename + ".c", "w") as f: + f.write(str(kernel)) + + # Compile kernel function into .so + cc = [os.environ['CC'] if "CC" in os.environ else 'gcc'] + cc += ['-Wall', '-O0', '-g', '-fPIC', '-shared', '-std=c99'] + cc += ["-o", "%s.so" % basename, "%s.c" % basename] + try: + subprocess.check_call(cc) + except subprocess.CalledProcessError as e: + print "Compilation error: ", e + raise Exception("Failed to compile %s.c" % basename) + + # Load compiled .so + try: + kernel_lib = ctypes.cdll.LoadLibrary(basename + ".so") + except OSError as e: + print "Library load error: ", e + raise Exception("Failed to load %s.so" % basename) + + # Invoke compiled kernel with packed arguments + kernel_lib.coffee_kernel(*args_data) + + # Close compiled kernel library + ctypes.cdll.LoadLibrary('libdl.so').dlclose(kernel_lib._handle) + + return index_data From 32fb06ca7563438ffa6fbf23c013750d50ef4439 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 16 Feb 2015 12:06:43 +0000 Subject: [PATCH 148/749] Coffee: Add symbol translator to make utf-8 variables compilable --- finat/greek_alphabet.py | 63 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 finat/greek_alphabet.py diff --git a/finat/greek_alphabet.py b/finat/greek_alphabet.py new file mode 100644 index 000000000..3fb03d347 --- /dev/null +++ b/finat/greek_alphabet.py @@ -0,0 +1,63 @@ +"""Translation table from utf-8 to greek variable names, taken from: +https://gist.github.com/piquadrat/765262#file-greek_alphabet-py +""" + + +def translate_symbol(symbol): + """Translates utf-8 sub-strings into compilable variable names""" + name = symbol.decode("utf-8") + for k, v in greek_alphabet.iteritems(): + name = name.replace(k, v) + return name + + +greek_alphabet = { + u'\u0391': 'Alpha', + u'\u0392': 'Beta', + u'\u0393': 'Gamma', + u'\u0394': 'Delta', + u'\u0395': 'Epsilon', + u'\u0396': 'Zeta', + u'\u0397': 'Eta', + u'\u0398': 'Theta', + u'\u0399': 'Iota', + u'\u039A': 'Kappa', + u'\u039B': 'Lamda', + u'\u039C': 'Mu', + u'\u039D': 'Nu', + u'\u039E': 'Xi', + u'\u039F': 'Omicron', + u'\u03A0': 'Pi', + u'\u03A1': 'Rho', + u'\u03A3': 'Sigma', + u'\u03A4': 'Tau', + u'\u03A5': 'Upsilon', + u'\u03A6': 'Phi', + u'\u03A7': 'Chi', + u'\u03A8': 'Psi', + u'\u03A9': 'Omega', + u'\u03B1': 'alpha', + u'\u03B2': 'beta', + u'\u03B3': 'gamma', + u'\u03B4': 'delta', + u'\u03B5': 'epsilon', + u'\u03B6': 'zeta', + u'\u03B7': 'eta', + u'\u03B8': 'theta', + u'\u03B9': 'iota', + u'\u03BA': 'kappa', + u'\u03BB': 'lamda', + u'\u03BC': 'mu', + u'\u03BD': 'nu', + u'\u03BE': 'xi', + u'\u03BF': 'omicron', + u'\u03C0': 'pi', + u'\u03C1': 'rho', + u'\u03C3': 'sigma', + u'\u03C4': 'tau', + u'\u03C5': 'upsilon', + u'\u03C6': 'phi', + u'\u03C7': 'chi', + u'\u03C8': 'psi', + u'\u03C9': 'omega', +} From 51415883cfa7b391a09054b62aadf0fb9e494bf0 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 16 Feb 2015 11:56:43 +0000 Subject: [PATCH 149/749] Coffee: Add a FInAT->Coffee mapper --- finat/coffee_compiler.py | 56 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/finat/coffee_compiler.py b/finat/coffee_compiler.py index 4bf7adb5e..7e1b87f18 100644 --- a/finat/coffee_compiler.py +++ b/finat/coffee_compiler.py @@ -2,12 +2,68 @@ evaluate a given recipe. Provides the same interface as FInAT's internal interpreter. """ +from pymbolic.mapper import CombineMapper +from greek_alphabet import translate_symbol import coffee.base as coffee import os import subprocess import ctypes import numpy as np from .utils import Kernel +from .ast import Recipe + + +determinant = {1: lambda e: coffee.Det1(e), + 2: lambda e: coffee.Det2(e), + 3: lambda e: coffee.Det3(e)} + + +class CoffeeMapper(CombineMapper): + """A mapper that generates Coffee ASTs for FInAT expressions""" + + def __init__(self, kernel_data): + """ + :arg context: a mapping from variable names to values + """ + super(CoffeeMapper, self).__init__() + self.kernel_data = kernel_data + + def combine(self, values): + return list(values) + + def map_recipe(self, expr): + return self.rec(expr.body) + + def map_variable(self, expr): + return coffee.Symbol(translate_symbol(expr.name)) + + def map_constant(self, expr): + return expr.real + + def map_subscript(self, expr): + name = translate_symbol(expr.aggregate.name) + indices = expr.index if isinstance(expr.index, tuple) else (expr.index,) + return coffee.Symbol(name, rank=indices) + + def map_index_sum(self, expr): + return self.rec(expr.body) + + def map_product(self, expr): + prod = self.rec(expr.children[0]) + for factor in expr.children[1:]: + prod = coffee.Prod(prod, self.rec(factor)) + return prod + + def map_inverse(self, expr): + e = expr.expression + return coffee.Invert(self.rec(e), e.shape[0]) + + def map_det(self, expr): + e = expr.expression + return determinant[e.shape[0]](self.rec(e)) + + def map_abs(self, expr): + return self.rec(expr.expression) class CoffeeKernel(Kernel): From 4f319b35b0650979fde6e85297c7b89623d568c7 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 6 Feb 2015 13:10:32 +0100 Subject: [PATCH 150/749] Coffee: Add ASTs to initialise static kernel data Uses pprint.pformat to generate the string for static kernel data --- finat/coffee_compiler.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/finat/coffee_compiler.py b/finat/coffee_compiler.py index 7e1b87f18..2602db59d 100644 --- a/finat/coffee_compiler.py +++ b/finat/coffee_compiler.py @@ -11,6 +11,7 @@ import numpy as np from .utils import Kernel from .ast import Recipe +from pprint import pformat determinant = {1: lambda e: coffee.Det1(e), @@ -71,6 +72,9 @@ class CoffeeKernel(Kernel): def generate_ast(self, context): kernel_args = self.kernel_data.kernel_args args_ast = [] + body_ast = [] + + mapper = CoffeeMapper(self.kernel_data) # Generate declaration of result argument result_shape = () @@ -85,9 +89,18 @@ def generate_ast(self, context): var_ast = coffee.Symbol(str(var), context[var].shape) args_ast.append(coffee.Decl("double", var_ast)) - body = coffee.EmptyStatement(None, None) + # Write AST to initialise static kernel data + for data in self.kernel_data.static.values(): + values = data[1]() + val_str = pformat(values.tolist()) + val_str = val_str.replace('[', '{').replace(']', '}') + val_init = coffee.ArrayInit(val_str) + var = coffee.Symbol(mapper(data[0]), values.shape) + body_ast.append(coffee.Decl("double", var, init=val_init)) + return coffee.FunDecl("void", "coffee_kernel", args_ast, - body, headers=["stdio.h"]) + coffee.Block(body_ast), + headers=["stdio.h"]) def evaluate(expression, context={}, kernel_data=None): From a39e46ffbf0f2ff7fd59ae23860fac47e008cb67 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 6 Feb 2015 15:57:16 +0100 Subject: [PATCH 151/749] Coffee: Add AST for the inner kernel loop(s) --- finat/coffee_compiler.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/finat/coffee_compiler.py b/finat/coffee_compiler.py index 2602db59d..e6970317c 100644 --- a/finat/coffee_compiler.py +++ b/finat/coffee_compiler.py @@ -11,6 +11,7 @@ import numpy as np from .utils import Kernel from .ast import Recipe +from .mappers import BindingMapper from pprint import pformat @@ -22,12 +23,22 @@ class CoffeeMapper(CombineMapper): """A mapper that generates Coffee ASTs for FInAT expressions""" - def __init__(self, kernel_data): + def __init__(self, kernel_data, varname="A"): """ :arg context: a mapping from variable names to values + :arg varname: name of the implied outer variable """ super(CoffeeMapper, self).__init__() self.kernel_data = kernel_data + self.scope_var = varname + + def _create_loop(self, index, body): + itvar = self.rec(index) + extent = index.extent + init = coffee.Decl("int", itvar, extent.start or 0) + cond = coffee.Less(itvar, extent.stop) + incr = coffee.Incr(itvar, extent.step or 1) + return coffee.For(init, cond, incr, coffee.Block(body, open_scope=True)) def combine(self, values): return list(values) @@ -38,6 +49,8 @@ def map_recipe(self, expr): def map_variable(self, expr): return coffee.Symbol(translate_symbol(expr.name)) + map_index = map_variable + def map_constant(self, expr): return expr.real @@ -66,9 +79,22 @@ def map_det(self, expr): def map_abs(self, expr): return self.rec(expr.expression) + def map_for_all(self, expr): + var = coffee.Symbol(self.scope_var, expr.indices) + body = [coffee.Assign(var, self.rec(expr.body))] + for idx in expr.indices: + body = [self._create_loop(idx, body)] + return coffee.Block(body) + class CoffeeKernel(Kernel): + def __init__(self, recipe, kernel_data): + super(CoffeeKernel, self).__init__(recipe, kernel_data) + + # Apply pre-processing mapper to bind free indices + self.recipe = BindingMapper(self.kernel_data)(self.recipe) + def generate_ast(self, context): kernel_args = self.kernel_data.kernel_args args_ast = [] @@ -98,6 +124,9 @@ def generate_ast(self, context): var = coffee.Symbol(mapper(data[0]), values.shape) body_ast.append(coffee.Decl("double", var, init=val_init)) + # Convert the kernel recipe into an AST + body_ast.append(mapper(self.recipe)) + return coffee.FunDecl("void", "coffee_kernel", args_ast, coffee.Block(body_ast), headers=["stdio.h"]) From a4ab05fdf29c4a153a39a8434df4d9f8bffa3755 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 17 Feb 2015 09:56:16 +0000 Subject: [PATCH 152/749] Coffee: Handle IndexSums bound to variables via Lets IndexSums create multiple assignments while yielding an expression for the body. We deal with this by keeping a stack of statements for the intialisation and increment statements that gets inserted at the outer assignement level. This requires all IndexSums to be bound to a variable via a Let. --- finat/coffee_compiler.py | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/finat/coffee_compiler.py b/finat/coffee_compiler.py index e6970317c..1d5ab96be 100644 --- a/finat/coffee_compiler.py +++ b/finat/coffee_compiler.py @@ -10,9 +10,10 @@ import ctypes import numpy as np from .utils import Kernel -from .ast import Recipe -from .mappers import BindingMapper +from .ast import Recipe, IndexSum +from .mappers import BindingMapper, IndexSumMapper from pprint import pformat +from collections import deque determinant = {1: lambda e: coffee.Det1(e), @@ -31,6 +32,13 @@ def __init__(self, kernel_data, varname="A"): super(CoffeeMapper, self).__init__() self.kernel_data = kernel_data self.scope_var = varname + self.scope_ast = deque() + + def _push_scope(self): + self.scope_ast.append([]) + + def _pop_scope(self): + return self.scope_ast.pop() def _create_loop(self, index, body): itvar = self.rec(index) @@ -81,17 +89,42 @@ def map_abs(self, expr): def map_for_all(self, expr): var = coffee.Symbol(self.scope_var, expr.indices) - body = [coffee.Assign(var, self.rec(expr.body))] + self._push_scope() + body = self.rec(expr.body) + scope = self._pop_scope() + body = scope + [coffee.Assign(var, body)] for idx in expr.indices: body = [self._create_loop(idx, body)] return coffee.Block(body) + def map_let(self, expr): + for v, e in expr.bindings: + var = coffee.Symbol(self.rec(v)) + if isinstance(e, IndexSum): + # Recurse on expression in a new scope + self._push_scope() + body = self.rec(e.body) + scope = self._pop_scope() + lbody = scope + [coffee.Incr(var, body)] + + # Construct IndexSum loop and add to current scope + self.scope_ast[-1].append(coffee.Decl("double", var, init="0.")) + self.scope_ast[-1].append(self._create_loop(e.indices[0], lbody)) + else: + self.scope_ast[-1].append(coffee.Decl("double", var)) + self.scope_ast[-1].append(self.rec(e)) + + return self.rec(expr.body) + class CoffeeKernel(Kernel): def __init__(self, recipe, kernel_data): super(CoffeeKernel, self).__init__(recipe, kernel_data) + # Apply mapper to bind all IndexSums to temporaries + self.recipe = IndexSumMapper(self.kernel_data)(self.recipe) + # Apply pre-processing mapper to bind free indices self.recipe = BindingMapper(self.kernel_data)(self.recipe) From e17c5f577aea305f91d01dc0186cb6082b87d587 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 12 Mar 2015 14:11:57 +0000 Subject: [PATCH 153/749] Coffee: Recurse on expr body outside the ISum check --- finat/coffee_compiler.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/finat/coffee_compiler.py b/finat/coffee_compiler.py index 1d5ab96be..9331134df 100644 --- a/finat/coffee_compiler.py +++ b/finat/coffee_compiler.py @@ -100,11 +100,13 @@ def map_for_all(self, expr): def map_let(self, expr): for v, e in expr.bindings: var = coffee.Symbol(self.rec(v)) + + self._push_scope() + body = self.rec(e) + scope = self._pop_scope() + if isinstance(e, IndexSum): # Recurse on expression in a new scope - self._push_scope() - body = self.rec(e.body) - scope = self._pop_scope() lbody = scope + [coffee.Incr(var, body)] # Construct IndexSum loop and add to current scope @@ -112,7 +114,7 @@ def map_let(self, expr): self.scope_ast[-1].append(self._create_loop(e.indices[0], lbody)) else: self.scope_ast[-1].append(coffee.Decl("double", var)) - self.scope_ast[-1].append(self.rec(e)) + self.scope_ast[-1].append(body) return self.rec(expr.body) From e2a768154cf46c23ef7a614cdecbc3a1012625ce Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 6 Mar 2015 16:32:32 +0000 Subject: [PATCH 154/749] BindingMapper: Always rebuild Recipes --- finat/mappers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/finat/mappers.py b/finat/mappers.py index c89cfde4b..967252269 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -214,6 +214,8 @@ def map_recipe(self, expr): if len(free_indices) > 0: expr = Recipe(expr.indices, ForAll(free_indices, body)) + else: + expr = Recipe(expr.indices, body) return expr From fdcca125d7d5900ab73823452fd826cce489000c Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Sat, 7 Mar 2015 07:57:11 +0000 Subject: [PATCH 155/749] IndexMapper: Don't special-case Let-Recipe groups --- finat/mappers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/finat/mappers.py b/finat/mappers.py index 967252269..1ff39002d 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -281,8 +281,6 @@ def map_let(self, expr): for v, e in expr.bindings: if isinstance(e, IndexSum): self._bound_isums.add(e) - elif isinstance(e, Recipe) and isinstance(e.body, IndexSum): - self._bound_isums.add(e.body) new_bindings.append((v, self.rec(e))) body = self._bind_isums(self.rec(expr.body)) From b0672edb5cd327b7c5526273b5b1e65ddec8ad13 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 9 Mar 2015 06:33:04 +0000 Subject: [PATCH 156/749] Coffee: Recurse on subscript indices to get symbol translation --- finat/coffee_compiler.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/finat/coffee_compiler.py b/finat/coffee_compiler.py index 9331134df..e3a9b4f7c 100644 --- a/finat/coffee_compiler.py +++ b/finat/coffee_compiler.py @@ -64,7 +64,10 @@ def map_constant(self, expr): def map_subscript(self, expr): name = translate_symbol(expr.aggregate.name) - indices = expr.index if isinstance(expr.index, tuple) else (expr.index,) + if isinstance(expr.index, tuple): + indices = self.rec(expr.index) + else: + indices = (self.rec(expr.index),) return coffee.Symbol(name, rank=indices) def map_index_sum(self, expr): From 1740b6e732804d523ccfc35ae52e0da8cef3740b Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 10 Mar 2015 15:15:59 +0000 Subject: [PATCH 157/749] GeometryMapper: Fixing affine by replacing q with 0 --- finat/geometry_mapper.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/finat/geometry_mapper.py b/finat/geometry_mapper.py index 276a9b657..0feabf49d 100644 --- a/finat/geometry_mapper.py +++ b/finat/geometry_mapper.py @@ -76,6 +76,11 @@ def _bind_geometry(self, q, body): element = kd.coordinate_element J = element.field_evaluation(phi_x, q, kd, grad, pullback=False) + if self.kernel_data.affine: + d, b, q = J.indices + J = J.replace_indices(zip(q, (0,))) + J.indices = (d, b, ()) + inner_lets = ((kd.detJ, Det(kd.J)),) if kd.detJ in self.local_geometry else () inner_lets += ((kd.invJ, Inverse(kd.J)),) if kd.invJ in self.local_geometry else () From 4e4b50149600dd4cdac5ded009102f23db5782c5 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 11 Mar 2015 13:47:20 +0000 Subject: [PATCH 158/749] Coffee: Infer symbol shape in Let assignments --- finat/coffee_compiler.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/finat/coffee_compiler.py b/finat/coffee_compiler.py index e3a9b4f7c..f16142eb6 100644 --- a/finat/coffee_compiler.py +++ b/finat/coffee_compiler.py @@ -10,7 +10,7 @@ import ctypes import numpy as np from .utils import Kernel -from .ast import Recipe, IndexSum +from .ast import Recipe, IndexSum, Array from .mappers import BindingMapper, IndexSumMapper from pprint import pformat from collections import deque @@ -102,7 +102,8 @@ def map_for_all(self, expr): def map_let(self, expr): for v, e in expr.bindings: - var = coffee.Symbol(self.rec(v)) + shape = v.shape if isinstance(v, Array) else () + var = coffee.Symbol(self.rec(v), rank=shape) self._push_scope() body = self.rec(e) From 99e92d30db537abf4e1281abbc9e49356d5e2737 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 11 Mar 2015 13:59:51 +0000 Subject: [PATCH 159/749] Coffee: Keep a stack of scope variables for loop body assignments --- finat/coffee_compiler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/finat/coffee_compiler.py b/finat/coffee_compiler.py index f16142eb6..407fadec3 100644 --- a/finat/coffee_compiler.py +++ b/finat/coffee_compiler.py @@ -31,7 +31,7 @@ def __init__(self, kernel_data, varname="A"): """ super(CoffeeMapper, self).__init__() self.kernel_data = kernel_data - self.scope_var = varname + self.scope_var = deque(varname) self.scope_ast = deque() def _push_scope(self): @@ -91,7 +91,7 @@ def map_abs(self, expr): return self.rec(expr.expression) def map_for_all(self, expr): - var = coffee.Symbol(self.scope_var, expr.indices) + var = coffee.Symbol(self.scope_var[-1], self.rec(expr.indices)) self._push_scope() body = self.rec(expr.body) scope = self._pop_scope() @@ -104,6 +104,7 @@ def map_let(self, expr): for v, e in expr.bindings: shape = v.shape if isinstance(v, Array) else () var = coffee.Symbol(self.rec(v), rank=shape) + self.scope_var.append(v) self._push_scope() body = self.rec(e) @@ -120,6 +121,7 @@ def map_let(self, expr): self.scope_ast[-1].append(coffee.Decl("double", var)) self.scope_ast[-1].append(body) + self.scope_var.pop() return self.rec(expr.body) From 0e6f96ad8bf69510423bc88ecd0bab19a960719a Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 12 Mar 2015 05:41:08 +0000 Subject: [PATCH 160/749] Coffee: Add lapack/blas compiler flags --- finat/coffee_compiler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/finat/coffee_compiler.py b/finat/coffee_compiler.py index 407fadec3..651df396f 100644 --- a/finat/coffee_compiler.py +++ b/finat/coffee_compiler.py @@ -200,6 +200,7 @@ def evaluate(expression, context={}, kernel_data=None): cc = [os.environ['CC'] if "CC" in os.environ else 'gcc'] cc += ['-Wall', '-O0', '-g', '-fPIC', '-shared', '-std=c99'] cc += ["-o", "%s.so" % basename, "%s.c" % basename] + cc += ['-llapack', '-lblas'] try: subprocess.check_call(cc) except subprocess.CalledProcessError as e: From 42e5f5a41c2dff1f93c792cddeda85f2050866f5 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 12 Mar 2015 14:13:23 +0000 Subject: [PATCH 161/749] Coffee: Add Assign around expressions in Let bindings --- finat/coffee_compiler.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/finat/coffee_compiler.py b/finat/coffee_compiler.py index 651df396f..8aea1e5bd 100644 --- a/finat/coffee_compiler.py +++ b/finat/coffee_compiler.py @@ -117,6 +117,8 @@ def map_let(self, expr): # Construct IndexSum loop and add to current scope self.scope_ast[-1].append(coffee.Decl("double", var, init="0.")) self.scope_ast[-1].append(self._create_loop(e.indices[0], lbody)) + elif isinstance(body, coffee.Expr): + self.scope_ast[-1].append(coffee.Decl("double", var, init=body)) else: self.scope_ast[-1].append(coffee.Decl("double", var)) self.scope_ast[-1].append(body) From fb9309c6533ab520067276d1d7fe712a1b96fc3d Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 12 Mar 2015 16:50:26 +0000 Subject: [PATCH 162/749] Coffee: Add explicit memcpy to Let->Inverse constructs Coffee's current Invert statement inverts matrices in-place so we need to copy the source matrix first. --- finat/coffee_compiler.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/finat/coffee_compiler.py b/finat/coffee_compiler.py index 8aea1e5bd..6fde74da8 100644 --- a/finat/coffee_compiler.py +++ b/finat/coffee_compiler.py @@ -10,7 +10,7 @@ import ctypes import numpy as np from .utils import Kernel -from .ast import Recipe, IndexSum, Array +from .ast import Recipe, IndexSum, Array, Inverse from .mappers import BindingMapper, IndexSumMapper from pprint import pformat from collections import deque @@ -117,6 +117,17 @@ def map_let(self, expr): # Construct IndexSum loop and add to current scope self.scope_ast[-1].append(coffee.Decl("double", var, init="0.")) self.scope_ast[-1].append(self._create_loop(e.indices[0], lbody)) + + elif isinstance(e, Inverse): + # Coffee currently inverts matrices in-place + # so we need to memcpy the source matrix first + mcpy = coffee.FlatBlock("memcpy(%s, %s, %d*sizeof(double));\n" % + (v, e.expression, shape[0]*shape[1])) + e.expression = v + self.scope_ast[-1].append(coffee.Decl("double", var)) + self.scope_ast[-1].append(mcpy) + self.scope_ast[-1].append(self.rec(e)) + elif isinstance(body, coffee.Expr): self.scope_ast[-1].append(coffee.Decl("double", var, init=body)) else: From a97a7a27708113bfe11401daa3331b7865ad2c6a Mon Sep 17 00:00:00 2001 From: Robert Kirby Date: Fri, 13 Mar 2015 13:42:28 -0500 Subject: [PATCH 163/749] More messing with Bernstein --- finat/bernstein.py | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index f30ac0516..c2d2de27f 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -1,6 +1,6 @@ from finiteelementbase import FiniteElementBase from points import StroudPointSet -from ast import ForAll, Recipe, Wave, Let, IndexSum +from ast import Recipe, Wave, Let, IndexSum import pymbolic.primitives as p from indices import BasisFunctionIndex, PointIndex, SimpliciallyGradedBasisFunctionIndex # noqa import numpy as np @@ -207,26 +207,42 @@ def moment_evaluation(self, value, weights, q, kernel_data, tmps = [kernel_data.new_variable("tmp") for d in range(sd - 1)] if sd == 2: - alpha = SimpliciallyGradedBasisFunctionIndex(sd, deg) - alphas = alpha.factors - xi_cur = xi[0] + alpha_internal = SimpliciallyGradedBasisFunctionIndex(sd, deg) + alphas_int = alpha_internal.factors + xi_cur = xi[0][qs[1]] s = 1 - xi_cur expr0 = Let(((r, xi_cur / s), ), IndexSum((qs[0], ), Wave(w, - alphas[0], + alphas_int[0], wt[0][qs[0]] * (s**deg), - w * r * (deg - alphas[0]) / alphas[0], + w * r * (deg - alphas_int[0]) / alphas_int[0], w * value[qs[0], qs[1]]) ) ) - return Recipe(((), (alphas[0], ), (qs[1], )), - expr0) + recipe0 = Recipe(((), (alphas_int[0], ), (qs[1], )), + expr0) + xi_cur = xi[1] + s = 1 - xi_cur + alpha = SimpliciallyGradedBasisFunctionIndex(2, deg) + alphas = alpha.factors + r = xi_cur / s + expr1 = Let(((tmps[0], recipe0), ), + IndexSum((qs[1], ), + Wave(w, + alphas[1], + wt[1][qs[1]] * (s**(deg-alphas[0])), + w * r * (deg-alphas[0]-alphas[1]+1)/(alphas[1]), + w * tmps[0][alphas[0], qs[1]] + ) + ) + ) + return Recipe(((), (alphas[0], alphas[1]), ()), expr1) else: raise NotImplementedError - + def moment_evaluation_general(self, value, weights, q, kernel_data, derivative=None, pullback=None): if not isinstance(q.points, StroudPointSet): From 321be18f14cef2109a0b3b2eaf13c34339f4fffc Mon Sep 17 00:00:00 2001 From: David Ham Date: Mon, 16 Mar 2015 12:26:59 +0000 Subject: [PATCH 164/749] two typos --- finat/hdiv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/hdiv.py b/finat/hdiv.py index e590d04dc..8c32a8e29 100644 --- a/finat/hdiv.py +++ b/finat/hdiv.py @@ -13,7 +13,7 @@ def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): phi = self._tabulated_basis(q.points, kernel_data, derivative) - i = indices.BasisFunctionIndex(self.fiat_element.space_dimension()) + i = indices.BasisFunctionIndex(self._fiat_element.space_dimension()) tIndex = lambda: indices.DimensionIndex(kernel_data.tdim) gIndex = lambda: indices.DimensionIndex(kernel_data.gdim) From ac574fd49facd4f2a3e3d6c156789939d2e4a4ae Mon Sep 17 00:00:00 2001 From: Robert Kirby Date: Mon, 16 Mar 2015 10:21:03 -0600 Subject: [PATCH 165/749] Adding for alls judiciously to bernstein. We think there is an interpreter bug now. --- finat/bernstein.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index 3cf3accbd..af32e3728 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -222,8 +222,11 @@ def moment_evaluation(self, value, weights, q, kernel_data, w * value[qs[0], qs[1]]) ) ) + expr0prime = ForAll((qs[1],), + ForAll((alphas_int[0],), + expr0)) recipe0 = Recipe(((), (alphas_int[0], ), (qs[1], )), - expr0) + expr0prime) xi_cur = xi[1] s = 1 - xi_cur alpha = SimpliciallyGradedBasisFunctionIndex(2, deg) @@ -231,14 +234,19 @@ def moment_evaluation(self, value, weights, q, kernel_data, r = xi_cur / s expr1 = Let(((tmps[0], recipe0), ), IndexSum((qs[1], ), - Wave(w, - alphas[1], - wt[1][qs[1]] * (s**(deg-alphas[0])), - w * r * (deg-alphas[0]-alphas[1]+1)/(alphas[1]), - w * tmps[0][alphas[0], qs[1]] - ) + ForAll((alphas[0],), + ForAll((alphas[1],), + Wave(w, + alphas[1], + wt[1][qs[1]] * (s**(deg-alphas[0])), + w * r * (deg-alphas[0]-alphas[1]+1)/(alphas[1]), + w * tmps[0][alphas[0], qs[1]] + ) + ) + ) ) ) + return Recipe(((), (alphas[0], alphas[1]), ()), expr1) else: From 5722b48d3986f641c79a25ea485c995d232107f0 Mon Sep 17 00:00:00 2001 From: David Ham Date: Mon, 16 Mar 2015 16:24:12 +0000 Subject: [PATCH 166/749] quad fixes --- finat/ast.py | 2 +- finat/indices.py | 2 +- finat/lagrange.py | 2 +- finat/product_elements.py | 13 ++++++++----- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index 3fdc0e0fa..4c6d299c2 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -318,7 +318,7 @@ def __getinitargs__(self): mapper_method = "map_abs" -class CompoundVector(StringifyMixin, p.Expression): +class CompoundVector(StringifyMixin, p._MultiChildExpression): """A vector expression composed by concatenating other expressions.""" def __init__(self, index, indices, expressions): """ diff --git a/finat/indices.py b/finat/indices.py index 8828a037c..3ed3c267c 100644 --- a/finat/indices.py +++ b/finat/indices.py @@ -29,7 +29,7 @@ def length(self): stop = self._extent.stop step = self._extent.step or 1 - return math.ceil((stop - start) / step) + return int(math.ceil((stop - start) / step)) @property def _str_extent(self): diff --git a/finat/lagrange.py b/finat/lagrange.py index 0d2d30d58..44dac6cdf 100644 --- a/finat/lagrange.py +++ b/finat/lagrange.py @@ -25,7 +25,7 @@ def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): i = indices.BasisFunctionIndex(self._fiat_element.space_dimension()) if derivative is grad: - alpha = indices.DimensionIndex(kernel_data.tdim) + alpha = indices.DimensionIndex(self.cell.get_spatial_dimension()) if pullback: beta = alpha alpha = indices.DimensionIndex(kernel_data.gdim) diff --git a/finat/product_elements.py b/finat/product_elements.py index 3107e5f00..971deb74f 100644 --- a/finat/product_elements.py +++ b/finat/product_elements.py @@ -14,6 +14,9 @@ def __init__(self, *args): self.factors = args + self._degree = max([a._degree for a in args]) + self._cell = None + def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): '''Produce the variable for the tabulation of the basis @@ -28,20 +31,20 @@ def basis_evaluation(self, q, kernel_data, derivative=None, phi = [e.basis_evaluation(q_, kernel_data) for e, q_ in zip(self.factors, q.factors)] - i_ = [phi_.indices[1] for phi_ in phi] + i_ = [phi_.indices[1][0] for phi_ in phi] i = TensorBasisFunctionIndex(*i_) if derivative is grad: - raise NotImplementedError - phi_d = [e.basis_evaluation(q_, kernel_data, grad=True) + + phi_d = [e.basis_evaluation(q_, kernel_data, derivative=grad, pullback=False) for e, q_ in zip(self.factors, q.factors)] # Need to replace the basisfunctionindices on phi_d with i expressions = [reduce(lambda a, b: a.body * b.body, phi[:d] + [phi_d[d]] + phi[d + 1:]) - for d in len(phi)] + for d in range(len(phi))] - alpha_ = [phi_.indices[0] for phi_ in phi_d] + alpha_ = [phi_.indices[0][0] for phi_ in phi_d] alpha = DimensionIndex(sum(alpha__.length for alpha__ in alpha_)) assert alpha.length == kernel_data.gdim From df74900c8f4b484d7c71622275b3ec8eed237d52 Mon Sep 17 00:00:00 2001 From: David Ham Date: Sun, 22 Mar 2015 14:21:55 +0000 Subject: [PATCH 167/749] pep8 fixes --- finat/bernstein.py | 16 +++++++--------- finat/coffee_compiler.py | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/finat/bernstein.py b/finat/bernstein.py index af32e3728..ff9f307f1 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -217,7 +217,7 @@ def moment_evaluation(self, value, weights, q, kernel_data, IndexSum((qs[0], ), Wave(w, alphas_int[0], - wt[0][qs[0]] * (s**deg), + wt[0][qs[0]] * (s ** deg), w * r * (deg - alphas_int[0]) / alphas_int[0], w * value[qs[0], qs[1]]) ) @@ -238,12 +238,11 @@ def moment_evaluation(self, value, weights, q, kernel_data, ForAll((alphas[1],), Wave(w, alphas[1], - wt[1][qs[1]] * (s**(deg-alphas[0])), - w * r * (deg-alphas[0]-alphas[1]+1)/(alphas[1]), - w * tmps[0][alphas[0], qs[1]] - ) - ) - ) + wt[1][qs[1]] * (s ** (deg - alphas[0])), + w * r * (deg - alphas[0] - alphas[1] + 1) / (alphas[1]), + w * tmps[0][alphas[0], qs[1]]) + ) + ) ) ) @@ -252,9 +251,8 @@ def moment_evaluation(self, value, weights, q, kernel_data, else: raise NotImplementedError - def moment_evaluation_general(self, value, weights, q, kernel_data, - derivative=None, pullback=None): + derivative=None, pullback=None): if not isinstance(q.points, StroudPointSet): raise ValueError("Only Stroud points may be employed with Bernstein polynomials") diff --git a/finat/coffee_compiler.py b/finat/coffee_compiler.py index 6fde74da8..7085c590f 100644 --- a/finat/coffee_compiler.py +++ b/finat/coffee_compiler.py @@ -122,7 +122,7 @@ def map_let(self, expr): # Coffee currently inverts matrices in-place # so we need to memcpy the source matrix first mcpy = coffee.FlatBlock("memcpy(%s, %s, %d*sizeof(double));\n" % - (v, e.expression, shape[0]*shape[1])) + (v, e.expression, shape[0] * shape[1])) e.expression = v self.scope_ast[-1].append(coffee.Decl("double", var)) self.scope_ast[-1].append(mcpy) From 068076dd60f2f3d67c590c75ea767cb85d6aacd7 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 18 Mar 2015 17:38:39 +0000 Subject: [PATCH 168/749] Coffee: Pass kernel args into CoffeeKernel as Array types with shape --- finat/coffee_compiler.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/finat/coffee_compiler.py b/finat/coffee_compiler.py index 7085c590f..434b4ce43 100644 --- a/finat/coffee_compiler.py +++ b/finat/coffee_compiler.py @@ -149,24 +149,17 @@ def __init__(self, recipe, kernel_data): # Apply pre-processing mapper to bind free indices self.recipe = BindingMapper(self.kernel_data)(self.recipe) - def generate_ast(self, context): - kernel_args = self.kernel_data.kernel_args + def generate_ast(self, kernel_args=None): + if kernel_args is None: + kernel_args = self.kernel_data.kernel_args args_ast = [] body_ast = [] mapper = CoffeeMapper(self.kernel_data) - # Generate declaration of result argument - result_shape = () - for index in self.recipe.indices: - for i in index: - result_shape += (i.extent.stop,) - result_ast = coffee.Symbol(kernel_args[0], result_shape) - args_ast.append(coffee.Decl("double", result_ast)) - # Add argument declarations - for var in kernel_args[1:]: - var_ast = coffee.Symbol(str(var), context[var].shape) + for var in kernel_args: + var_ast = coffee.Symbol(var.name, var.shape) args_ast.append(coffee.Decl("double", var_ast)) # Write AST to initialise static kernel data @@ -196,15 +189,15 @@ def evaluate(expression, context={}, kernel_data=None): index_shape += (i.extent.stop, ) index_data = np.empty(index_shape, dtype=np.double) args_data.append(index_data.ctypes.data) - kernel_data.kernel_args = ["A"] + kernel_data.kernel_args = [Array("A", shape=index_shape)] # Pack context arguments for var, value in context.iteritems(): - kernel_data.kernel_args.append(var) + kernel_data.kernel_args.append(Array(var, shape=value.shape)) args_data.append(value.ctypes.data) # Generate kernel function - kernel = CoffeeKernel(expression, kernel_data).generate_ast(context) + kernel = CoffeeKernel(expression, kernel_data).generate_ast() basename = os.path.join(os.getcwd(), "coffee_kernel") with file(basename + ".c", "w") as f: f.write(str(kernel)) From ec02d4c09c524560c51c1e6f35dc6ae736ed3100 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Sat, 21 Mar 2015 06:23:03 +0000 Subject: [PATCH 169/749] Coffee: Implement Abs as a "fabs" function call Also adds the required headers for "fabs" and "memcpy". --- finat/coffee_compiler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/finat/coffee_compiler.py b/finat/coffee_compiler.py index 434b4ce43..b2f9156b9 100644 --- a/finat/coffee_compiler.py +++ b/finat/coffee_compiler.py @@ -88,7 +88,7 @@ def map_det(self, expr): return determinant[e.shape[0]](self.rec(e)) def map_abs(self, expr): - return self.rec(expr.expression) + return coffee.FunCall("fabs", self.rec(expr.expression)) def map_for_all(self, expr): var = coffee.Symbol(self.scope_var[-1], self.rec(expr.indices)) @@ -176,7 +176,7 @@ def generate_ast(self, kernel_args=None): return coffee.FunDecl("void", "coffee_kernel", args_ast, coffee.Block(body_ast), - headers=["stdio.h"]) + headers=["math.h", "string.h"]) def evaluate(expression, context={}, kernel_data=None): From efaf96340e6ed692ea480687b7ed36aadd417816 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Sat, 21 Mar 2015 07:14:59 +0000 Subject: [PATCH 170/749] Coffee: Store intent (assign/increment) with scope variables Allows kernel generator to dictate the intent of the outer recipe. --- finat/coffee_compiler.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/finat/coffee_compiler.py b/finat/coffee_compiler.py index b2f9156b9..a2c635ba3 100644 --- a/finat/coffee_compiler.py +++ b/finat/coffee_compiler.py @@ -24,15 +24,21 @@ class CoffeeMapper(CombineMapper): """A mapper that generates Coffee ASTs for FInAT expressions""" - def __init__(self, kernel_data, varname="A"): + def __init__(self, kernel_data, varname="A", increment=False): """ :arg context: a mapping from variable names to values :arg varname: name of the implied outer variable + :arg increment: flag indicating that the kernel should + increment result values instead of assigning them """ super(CoffeeMapper, self).__init__() self.kernel_data = kernel_data - self.scope_var = deque(varname) + self.scope_var = deque() self.scope_ast = deque() + if increment: + self.scope_var.append((varname, coffee.Incr)) + else: + self.scope_var.append((varname, coffee.Assign)) def _push_scope(self): self.scope_ast.append([]) @@ -91,11 +97,12 @@ def map_abs(self, expr): return coffee.FunCall("fabs", self.rec(expr.expression)) def map_for_all(self, expr): - var = coffee.Symbol(self.scope_var[-1], self.rec(expr.indices)) + name, stmt = self.scope_var[-1] + var = coffee.Symbol(name, self.rec(expr.indices)) self._push_scope() body = self.rec(expr.body) scope = self._pop_scope() - body = scope + [coffee.Assign(var, body)] + body = scope + [stmt(var, body)] for idx in expr.indices: body = [self._create_loop(idx, body)] return coffee.Block(body) @@ -104,7 +111,7 @@ def map_let(self, expr): for v, e in expr.bindings: shape = v.shape if isinstance(v, Array) else () var = coffee.Symbol(self.rec(v), rank=shape) - self.scope_var.append(v) + self.scope_var.append((v, coffee.Assign)) self._push_scope() body = self.rec(e) From 33c7ba466b33503635b55fa9f2abaf5a0f551a7f Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Sat, 21 Mar 2015 07:26:42 +0000 Subject: [PATCH 171/749] Coffee: Implementing PyOP2 interface for RHS assembly Uses double indirection for all kernel arguments and renames the kernel to "finat_kernel"; plus other small fixes. --- finat/coffee_compiler.py | 17 +++++++++++------ finat/pyop2_interface.py | 15 +++++++++++++-- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/finat/coffee_compiler.py b/finat/coffee_compiler.py index a2c635ba3..dd0b94a6e 100644 --- a/finat/coffee_compiler.py +++ b/finat/coffee_compiler.py @@ -156,17 +156,21 @@ def __init__(self, recipe, kernel_data): # Apply pre-processing mapper to bind free indices self.recipe = BindingMapper(self.kernel_data)(self.recipe) - def generate_ast(self, kernel_args=None): + def generate_ast(self, kernel_args=None, varname="A", increment=False): if kernel_args is None: kernel_args = self.kernel_data.kernel_args args_ast = [] body_ast = [] - mapper = CoffeeMapper(self.kernel_data) + mapper = CoffeeMapper(self.kernel_data, varname=varname, + increment=increment) # Add argument declarations for var in kernel_args: - var_ast = coffee.Symbol(var.name, var.shape) + if isinstance(var, Array): + var_ast = coffee.Symbol(var.name, var.shape) + else: + var_ast = coffee.Symbol("**" + var.name) args_ast.append(coffee.Decl("double", var_ast)) # Write AST to initialise static kernel data @@ -181,7 +185,7 @@ def generate_ast(self, kernel_args=None): # Convert the kernel recipe into an AST body_ast.append(mapper(self.recipe)) - return coffee.FunDecl("void", "coffee_kernel", args_ast, + return coffee.FunDecl("void", "finat_kernel", args_ast, coffee.Block(body_ast), headers=["math.h", "string.h"]) @@ -205,7 +209,8 @@ def evaluate(expression, context={}, kernel_data=None): # Generate kernel function kernel = CoffeeKernel(expression, kernel_data).generate_ast() - basename = os.path.join(os.getcwd(), "coffee_kernel") + + basename = os.path.join(os.getcwd(), "finat_kernel") with file(basename + ".c", "w") as f: f.write(str(kernel)) @@ -228,7 +233,7 @@ def evaluate(expression, context={}, kernel_data=None): raise Exception("Failed to load %s.so" % basename) # Invoke compiled kernel with packed arguments - kernel_lib.coffee_kernel(*args_data) + kernel_lib.finat_kernel(*args_data) # Close compiled kernel library ctypes.cdll.LoadLibrary('libdl.so').dlclose(kernel_lib._handle) diff --git a/finat/pyop2_interface.py b/finat/pyop2_interface.py index 9cb43783a..431cf418d 100644 --- a/finat/pyop2_interface.py +++ b/finat/pyop2_interface.py @@ -1,8 +1,10 @@ try: - from pyop2.pyparloop import Kernel + from pyop2 import Kernel except: Kernel = None from .interpreter import evaluate +from .ast import Array +from .coffee_compiler import CoffeeKernel def pyop2_kernel_function(kernel, kernel_args, interpreter=False): @@ -38,4 +40,13 @@ def kernel_function(*args): return kernel_function else: - raise NotImplementedError + index_shape = () + for index in kernel.recipe.indices: + for i in index: + index_shape += (i.extent.stop, ) + kernel_args.insert(0, Array("*A", shape=index_shape)) + + coffee_kernel = CoffeeKernel(kernel.recipe, kernel.kernel_data) + kernel_ast = coffee_kernel.generate_ast(kernel_args=kernel_args, + varname="*A", increment=True) + return Kernel(kernel_ast, "finat_kernel") From 2cc204581e10e038ce7d1ba72397bbffb03c32ca Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 25 Mar 2015 11:13:33 +0000 Subject: [PATCH 172/749] Coffee: Rename determinant classes according to upstream changes For details see COFFEE PR #37. --- finat/coffee_compiler.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/finat/coffee_compiler.py b/finat/coffee_compiler.py index dd0b94a6e..91dde8d76 100644 --- a/finat/coffee_compiler.py +++ b/finat/coffee_compiler.py @@ -16,9 +16,9 @@ from collections import deque -determinant = {1: lambda e: coffee.Det1(e), - 2: lambda e: coffee.Det2(e), - 3: lambda e: coffee.Det3(e)} +determinant = {1: lambda e: coffee.Determinant1x1(e), + 2: lambda e: coffee.Determinant2x2(e), + 3: lambda e: coffee.Determinant3x3(e)} class CoffeeMapper(CombineMapper): From c0c6af4da7fb580af45448cd4640d57c424e6c3e Mon Sep 17 00:00:00 2001 From: David Ham Date: Sun, 29 Mar 2015 15:02:56 +0100 Subject: [PATCH 173/749] Suitable xfailing test created --- finat/__init__.py | 2 ++ finat/ast.py | 4 ++++ finat/indices.py | 5 +++++ 3 files changed, 11 insertions(+) diff --git a/finat/__init__.py b/finat/__init__.py index f230aea8f..1c0e320b4 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -6,6 +6,8 @@ from points import PointSet from utils import KernelData, Kernel from derivatives import div, grad, curl +from indices import * +from ast import * import interpreter import quadrature import ufl_interface diff --git a/finat/ast.py b/finat/ast.py index 4c6d299c2..406c30d1f 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -8,6 +8,10 @@ def colored(string, color, attrs=[]): return string +__all__ = ["Variable", "Array", "Recipe", "IndexSum", "LeviCivita", + "ForAll", "Wave", "Let", "Delta", "Inverse", "Det", "Abs", + "CompoundVector"] + class FInATSyntaxError(Exception): """Exception to raise when users break the rules of the FInAT ast.""" diff --git a/finat/indices.py b/finat/indices.py index 3ed3c267c..461fb839e 100644 --- a/finat/indices.py +++ b/finat/indices.py @@ -3,6 +3,11 @@ from pymbolic.mapper.stringifier import StringifyMapper import math +__all__ = ["PointIndex", "TensorPointIndex", "BasisFunctionIndex", + "TensorBasisFunctionIndex", + "SimpliciallyGradedBasisFunctionIndex", + "DimensionIndex"] + class IndexBase(ast.Variable): '''Base class for symbolic index objects.''' From 84f7f77d0c50fa8d78edcd4afeaa42da13603fd2 Mon Sep 17 00:00:00 2001 From: David Ham Date: Sun, 29 Mar 2015 16:52:03 +0100 Subject: [PATCH 174/749] Make kerneldata actually optional --- finat/coffee_compiler.py | 5 ++++- finat/utils.py | 11 +++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/finat/coffee_compiler.py b/finat/coffee_compiler.py index 91dde8d76..7a9a52bb7 100644 --- a/finat/coffee_compiler.py +++ b/finat/coffee_compiler.py @@ -9,7 +9,7 @@ import subprocess import ctypes import numpy as np -from .utils import Kernel +from .utils import Kernel, KernelData from .ast import Recipe, IndexSum, Array, Inverse from .mappers import BindingMapper, IndexSumMapper from pprint import pformat @@ -194,6 +194,9 @@ def evaluate(expression, context={}, kernel_data=None): index_shape = () args_data = [] + if not kernel_data: + kernel_data = KernelData() + # Pack free indices as kernel arguments for index in expression.indices: for i in index: diff --git a/finat/utils.py b/finat/utils.py index bf785a6a0..662379bac 100644 --- a/finat/utils.py +++ b/finat/utils.py @@ -15,7 +15,8 @@ def __init__(self, recipe, kernel_data): class KernelData(object): - def __init__(self, coordinate_element, coordinate_var=None, affine=None): + def __init__(self, coordinate_element=None, coordinate_var=None, + affine=None): """ :param coordinate_element: the (vector-valued) finite element for the coordinate field. @@ -29,7 +30,7 @@ def __init__(self, coordinate_element, coordinate_var=None, affine=None): self.coordinate_element = coordinate_element self.coordinate_var = coordinate_var - if affine is None: + if affine is None and coordinate_element: self.affine = coordinate_element.degree <= 1 and \ isinstance(coordinate_element.cell, _simplex) else: @@ -42,9 +43,11 @@ def __init__(self, coordinate_element, coordinate_var=None, affine=None): self.variables = set() #: The geometric dimension of the physical space. - self.gdim = coordinate_element._dimension + self.gdim = (coordinate_element._dimension + if coordinate_element else None) #: The topological dimension of the reference element - self.tdim = coordinate_element._cell.get_spatial_dimension() + self.tdim = (coordinate_element._cell.get_spatial_dimension() + if coordinate_element else None) self._variable_count = 0 self._point_count = 0 From 7837dc2fd780ae66a8609cf006c114a3538fde31 Mon Sep 17 00:00:00 2001 From: David Ham Date: Sun, 29 Mar 2015 16:52:50 +0100 Subject: [PATCH 175/749] Fix BindingMapper Make the BindingMapper stateless and hence fix a scoping problem with indices in Let. --- finat/mappers.py | 76 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 51 insertions(+), 25 deletions(-) diff --git a/finat/mappers.py b/finat/mappers.py index 9a58fb674..270405c23 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -1,3 +1,4 @@ +from collections import deque from pymbolic.mapper import IdentityMapper as IM from pymbolic.mapper.stringifier import StringifyMapper, PREC_NONE from pymbolic.mapper import WalkMapper as WM @@ -15,18 +16,19 @@ class IdentityMapper(IM): def __init__(self): super(IdentityMapper, self).__init__() - def map_recipe(self, expr, *args): - return expr.__class__(self.rec(expr.indices, *args), - self.rec(expr.body, *args)) + def map_recipe(self, expr, *args, **kwargs): + return expr.__class__(self.rec(expr.indices, *args, **kwargs), + self.rec(expr.body, *args, **kwargs)) - def map_index(self, expr, *args): + def map_index(self, expr, *args, **kwargs): return expr - def map_delta(self, expr, *args): - return expr.__class__(*(self.rec(c, *args) for c in expr.children)) + def map_delta(self, expr, *args, **kwargs): + return expr.__class__(*(self.rec(c, *args, **kwargs) + for c in expr.children)) - def map_inverse(self, expr, *args): - return expr.__class__(self.rec(expr.expression, *args)) + def map_inverse(self, expr, *args, **kwargs): + return expr.__class__(self.rec(expr.expression, *args, **kwargs)) map_let = map_delta map_for_all = map_delta @@ -44,7 +46,7 @@ def __init__(self, replacements): self.replacements = replacements - def map_index(self, expr, *args): + def map_index(self, expr, *args, **kwargs): '''Replace indices if they are in the replacements list''' try: @@ -200,16 +202,31 @@ def __init__(self, kernel_data): :arg context: a mapping from variable names to values """ super(BindingMapper, self).__init__() - self.bound_above = set() - self.bound_below = set() - def map_recipe(self, expr): - body = self.rec(expr.body) + def map_recipe(self, expr, bound_above=None, bound_below=None): + if bound_above is None: + bound_above = set() + if bound_below is None: + bound_below = deque() + + body = self.rec(expr.body, bound_above, bound_below) d, b, p = expr.indices - free_indices = tuple([i for i in d + b + p - if i not in self.bound_below and - i not in self.bound_above]) + recipe_indices = tuple([i for i in d + b + p + if i not in bound_above]) + free_indices = tuple([i for i in recipe_indices + if i not in bound_below]) + + bound_below.extendleft(reversed(free_indices)) + # Calculate the permutation from the order of loops actually + # employed to the ordering of indices in the Recipe. + try: + transpose = [recipe_indices.index(i) for i in bound_below] + except ValueError: + print "recipe_indices", recipe_indices + print "missing index", i + i.set_error() + raise if len(free_indices) > 0: expr = Recipe(expr.indices, ForAll(free_indices, body)) @@ -218,23 +235,32 @@ def map_recipe(self, expr): return expr - def map_index_sum(self, expr): + def map_let(self, expr, bound_above, bound_below): + + # Indices bound in the Let bindings should not count as + # bound_below for nodes higher in the tree. + return Let(tuple((symbol, self.rec(letexpr, bound_above, + bound_below=None)) + for symbol, letexpr in expr.bindings), + self.rec(expr.body, bound_above, bound_below)) + + def map_index_sum(self, expr, bound_above, bound_below): indices = expr.indices for idx in indices: - self.bound_above.add(idx) - body = self.rec(expr.body) + bound_above.add(idx) + body = self.rec(expr.body, bound_above, bound_below) for idx in indices: - self.bound_above.remove(idx) + bound_above.remove(idx) return IndexSum(indices, body) - def map_for_all(self, expr): + def map_for_all(self, expr, bound_above, bound_below): indices = expr.indices for idx in indices: - self.bound_above.add(idx) - body = self.rec(expr.body) + bound_above.add(idx) + body = self.rec(expr.body, bound_above, bound_below) for idx in indices: - self.bound_above.remove(idx) - self.bound_below.add(idx) + bound_above.remove(idx) + bound_below.appendleft(idx) return ForAll(indices, body) From ab404968885c8ac6e5c64a79fd6d15ba291f993e Mon Sep 17 00:00:00 2001 From: David Ham Date: Sun, 29 Mar 2015 18:31:13 +0100 Subject: [PATCH 176/749] Fixed transposition bug --- finat/ast.py | 5 +++-- finat/interpreter.py | 5 ++++- finat/mappers.py | 21 +++++++++++++++++---- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index 406c30d1f..215306e29 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -74,15 +74,16 @@ class Recipe(StringifyMixin, p.Expression): the second is a tuple of :class:`BasisFunctionIndex`, and the third is a tuple of :class:`PointIndex`. Any of the tuples may be empty. - :param expression: The expression returned by this :class:`Recipe`. + :param body: The expression returned by this :class:`Recipe`. """ - def __init__(self, indices, body): + def __init__(self, indices, body, _transpose=None): try: assert len(indices) == 3 except: raise FInATSyntaxError("Indices must be a triple of tuples") self.indices = tuple(indices) self.body = body + self._transpose = _transpose self._color = "blue" mapper_method = "map_recipe" diff --git a/finat/interpreter.py b/finat/interpreter.py index 0592828b6..8210512d7 100644 --- a/finat/interpreter.py +++ b/finat/interpreter.py @@ -45,7 +45,10 @@ def map_variable(self, expr): def map_recipe(self, expr): """Evaluate expr for all values of free indices""" - return self.rec(expr.body) + if expr._transpose: + return self.rec(expr.body).transpose(expr._transpose) + else: + return self.rec(expr.body) def map_index_sum(self, expr): diff --git a/finat/mappers.py b/finat/mappers.py index 270405c23..92839d68d 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -18,7 +18,8 @@ def __init__(self): def map_recipe(self, expr, *args, **kwargs): return expr.__class__(self.rec(expr.indices, *args, **kwargs), - self.rec(expr.body, *args, **kwargs)) + self.rec(expr.body, *args, **kwargs), + expr._transpose) def map_index(self, expr, *args, **kwargs): return expr @@ -221,7 +222,18 @@ def map_recipe(self, expr, bound_above=None, bound_below=None): # Calculate the permutation from the order of loops actually # employed to the ordering of indices in the Recipe. try: - transpose = [recipe_indices.index(i) for i in bound_below] + def expand_tensors(indices): + result = [] + if indices: + for i in indices: + try: + result += i.factors + except AttributeError: + result.append(i) + return result + + tmp = expand_tensors(recipe_indices) + transpose = [tmp.index(i) for i in expand_tensors(bound_below)] except ValueError: print "recipe_indices", recipe_indices print "missing index", i @@ -229,9 +241,10 @@ def map_recipe(self, expr, bound_above=None, bound_below=None): raise if len(free_indices) > 0: - expr = Recipe(expr.indices, ForAll(free_indices, body)) + expr = Recipe(expr.indices, ForAll(free_indices, body), + _transpose=transpose) else: - expr = Recipe(expr.indices, body) + expr = Recipe(expr.indices, body, _transpose=transpose) return expr From 967b2ef08cf4af0ad12cf2255429edbab7d78999 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Thu, 2 Apr 2015 16:52:17 +0100 Subject: [PATCH 177/749] Working tensor product deltas --- finat/__init__.py | 2 +- finat/finiteelementbase.py | 18 ++++++++---- finat/points.py | 6 ++++ finat/product_elements.py | 7 ++++- finat/quadrature.py | 35 ++++++++++++++++++++++- finat/{lagrange.py => scalar_elements.py} | 31 +++++++++++++++++++- finat/ufl_interface.py | 17 +++++++++-- 7 files changed, 104 insertions(+), 12 deletions(-) rename finat/{lagrange.py => scalar_elements.py} (67%) diff --git a/finat/__init__.py b/finat/__init__.py index 1c0e320b4..ad410da3a 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -1,4 +1,4 @@ -from lagrange import Lagrange, DiscontinuousLagrange +from scalar_elements import Lagrange, DiscontinuousLagrange, GaussLobatto from hdiv import RaviartThomas, BrezziDouglasMarini, BrezziDouglasFortinMarini from bernstein import Bernstein from vectorfiniteelement import VectorFiniteElement diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 860dba226..657c62efd 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -1,5 +1,6 @@ import numpy as np -from ast import Recipe, IndexSum, Variable, Abs +from .ast import Recipe, IndexSum, Variable, Abs +from .indices import TensorPointIndex class UndefinedError(Exception): @@ -144,15 +145,20 @@ def moment_evaluation(self, value, weights, q, kernel_data, derivative=None, pullback=True): basis = self.basis_evaluation(q, kernel_data, derivative, pullback) - (d, b, p__) = basis.indices + (d, b, p) = basis.indices phi = basis.body (d_, b_, p_) = value.indices - psi = value.replace_indices(zip(d_ + p_, d + p__)).body + psi = value.replace_indices(zip(d_ + p_, d + p)).body - w = weights.kernel_variable("w", kernel_data) + if isinstance(p[0], TensorPointIndex): + ws = [w_.kernel_variable("w", kernel_data)[p__] + for w_, p__ in zip(weights, p[0].factors)] + w = reduce(lambda a, b: a*b, ws) + else: + w = weights.kernel_variable("w", kernel_data)[p] - expr = psi * phi * w[p__] + expr = psi * phi * w if d: expr = IndexSum(d, expr) @@ -160,7 +166,7 @@ def moment_evaluation(self, value, weights, q, if pullback: expr *= Abs(kernel_data.detJ) - return Recipe(((), b + b_, ()), IndexSum(p__, expr)) + return Recipe(((), b + b_, ()), IndexSum(p, expr)) class FiatElementBase(ScalarElementMixin, FiniteElementBase): diff --git a/finat/points.py b/finat/points.py index a5aa34b6f..611af5a46 100644 --- a/finat/points.py +++ b/finat/points.py @@ -66,6 +66,7 @@ def __init__(self, factor_sets): self.factor_sets = factor_sets + @property def points(self): def helper(loi): if len(loi) == 1: @@ -98,3 +99,8 @@ class StroudPointSet(TensorPointSet, DuffyMappedMixin): def __init__(self, factor_sets): super(StroudPointSet, self).__init__(factor_sets) + + +class GaussLobattoPointSet(PointSet): + """A set of 1D Gauss Lobatto points. This is a separate class in order + to allow elements to apply spectral element tricks.""" diff --git a/finat/product_elements.py b/finat/product_elements.py index 971deb74f..bc5ace5f2 100644 --- a/finat/product_elements.py +++ b/finat/product_elements.py @@ -3,6 +3,7 @@ from .indices import TensorPointIndex, TensorBasisFunctionIndex, DimensionIndex from .derivatives import grad from .ast import Recipe, CompoundVector, IndexSum +from FIAT.reference_element import two_product_cell class ScalarProductElement(ScalarElementMixin, FiniteElementBase): @@ -15,7 +16,11 @@ def __init__(self, *args): self.factors = args self._degree = max([a._degree for a in args]) - self._cell = None + + cellprod = lambda cells: two_product_cell(cells[0], cells[1] if len(cells) < 3 + else cellprod(cells[1:])) + + self._cell = cellprod([a.cell for a in args]) def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): diff --git a/finat/quadrature.py b/finat/quadrature.py index 191f058b6..d3e0debf8 100644 --- a/finat/quadrature.py +++ b/finat/quadrature.py @@ -1,6 +1,7 @@ import numpy as np from gauss_jacobi import gauss_jacobi_rule -from points import StroudPointSet, PointSet +from points import StroudPointSet, PointSet, TensorPointSet, GaussLobattoPointSet +import FIAT class QuadratureRule(object): @@ -48,3 +49,35 @@ def __init__(self, cell, degree): cell, StroudPointSet(map(PointSet, points)), weights) + + +class GaussLobattoQuadrature(QuadratureRule): + def __init__(self, cell, points): + """Gauss-Lobatto-Legendre quadrature on hypercubes. + :param cell: The reference cell on which to define the quadrature. + :param points: The number of points. In more than one dimension, a + tuple of points in each dimension. + """ + + def expand_quad(cell): + if cell.get_spatial_dimension() == 1: + return [FIAT.quadrature.GaussLobattoQuadratureLineRule(cell, points)] + else: + try: + return expand_quad(cell.A) + expand_quad(cell.B) + except AttributeError(): + raise ValueError("Unable to create Gauss-Lobatto quadrature on ", + + str(cell)) + + q = expand_quad(cell) + + if len(q) == 1: + super(GaussLobattoQuadrature, self).__init__( + cell, + GaussLobattoPointSet(q[0].get_points()), + PointSet(q[0].get_weights())) + else: + super(GaussLobattoQuadrature, self).__init__( + cell, + TensorPointSet([GaussLobattoPointSet(q_.get_points()) for q_ in q]), + [PointSet(q_.get_weights()) for q_ in q]) diff --git a/finat/lagrange.py b/finat/scalar_elements.py similarity index 67% rename from finat/lagrange.py rename to finat/scalar_elements.py index 44dac6cdf..2abe92470 100644 --- a/finat/lagrange.py +++ b/finat/scalar_elements.py @@ -1,8 +1,9 @@ from .finiteelementbase import FiatElementBase -from .ast import Recipe, IndexSum +from .ast import Recipe, IndexSum, Delta import FIAT import indices from .derivatives import grad +from .points import GaussLobattoPointSet class ScalarElement(FiatElementBase): @@ -66,6 +67,34 @@ def __init__(self, cell, degree): self._fiat_element = FIAT.Lagrange(cell, degree) +class GaussLobatto(ScalarElement): + def __init__(self, cell, degree): + super(GaussLobatto, self).__init__(cell, degree) + + self._fiat_element = FIAT.GaussLobatto(cell, degree) + + def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): + '''Produce the variable for the tabulation of the basis + functions or their derivative. Also return the relevant indices. + + For basis evaluation with no gradient on a matching + Gauss-Lobatto quadrature, this implements the standard + spectral element diagonal mass trick by returning a delta + function. + ''' + if (derivative is None and isinstance(q.points, GaussLobattoPointSet) and + q.length == self._fiat_element.space_dimension()): + + i = indices.BasisFunctionIndex(self._fiat_element.space_dimension()) + + return Recipe(((), (i,), (q,)), Delta((i, q), 1.0)) + + else: + # Fall through to the default recipe. + return super(GaussLobatto, self).basis_evaluation(q, kernel_data, + derivative, pullback) + + class DiscontinuousLagrange(ScalarElement): def __init__(self, cell, degree): super(Lagrange, self).__init__(cell, degree) diff --git a/finat/ufl_interface.py b/finat/ufl_interface.py index d6e80cd8a..9d789ab66 100644 --- a/finat/ufl_interface.py +++ b/finat/ufl_interface.py @@ -2,14 +2,27 @@ import finat import FIAT + +def _q_element(cell, degree): + # Produce a Q element from GLL elements. + + if cell.get_spatial_dimension() == 1: + return finat.GaussLobatto(cell, degree) + else: + return finat.ScalarProductElement(_q_element(cell.A, degree), + _q_element(cell.B, degree)) + + _cell_map = { "triangle": FIAT.reference_element.UFCTriangle(), - "interval": FIAT.reference_element.UFCInterval() + "interval": FIAT.reference_element.UFCInterval(), + "quadrilateral": FIAT.reference_element.FiredrakeQuadrilateral() } _element_map = { "Lagrange": finat.Lagrange, - "Discontinuous Lagrange": finat.DiscontinuousLagrange + "Discontinuous Lagrange": finat.DiscontinuousLagrange, + "Q": _q_element } From e9d45ca67710f33723a32dcb7ea5d4dd832f6ce2 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Tue, 7 Apr 2015 10:59:18 +0100 Subject: [PATCH 178/749] Possibly working delta factorisation --- finat/ast.py | 2 ++ finat/mappers.py | 27 ++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/finat/ast.py b/finat/ast.py index 215306e29..9563ab3d7 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -262,6 +262,8 @@ def __init__(self, indices, body): "Delta statement requires exactly two indices") super(Delta, self).__init__((indices, body)) + self.indices = indices + self.body = body self._color = "blue" def __getinitargs__(self): diff --git a/finat/mappers.py b/finat/mappers.py index 92839d68d..a0965efe1 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -4,7 +4,7 @@ from pymbolic.mapper import WalkMapper as WM from pymbolic.mapper.graphviz import GraphvizMapper as GVM from .indices import IndexBase -from .ast import Recipe, ForAll, IndexSum, Let, Variable +from .ast import Recipe, ForAll, IndexSum, Let, Variable, Delta try: from termcolor import colored except ImportError: @@ -334,3 +334,28 @@ def map_index_sum(self, expr): expr = IndexSum(expr.indices, body) self._isum_stack[temp] = expr return temp + + +class FactorDeltaMapper(IdentityMapper): + """Class to pull deltas up the expression tree to maximise the opportunities for cancellation.""" + + def map_product(self, expr, *args, **kwargs): + + children = (self.rec(child, *args, **kwargs) for child in expr.children) + factors = [] + deltas = [] + + for child in children: + if isinstance(child, Delta): + deltas.append(child) + factors.append(child.body) + else: + factors.append(child) + + from pymbolic.primitives import flattened_product + result = flattened_product(tuple(factors)) + + for delta in deltas: + result = Delta(delta.indices, result) + + return result From 047265f18ca2d1833e704a95c8e8100dbe24dbf6 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Tue, 7 Apr 2015 11:00:29 +0100 Subject: [PATCH 179/749] pep8 --- finat/finiteelementbase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 657c62efd..a882ceee7 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -154,7 +154,7 @@ def moment_evaluation(self, value, weights, q, if isinstance(p[0], TensorPointIndex): ws = [w_.kernel_variable("w", kernel_data)[p__] for w_, p__ in zip(weights, p[0].factors)] - w = reduce(lambda a, b: a*b, ws) + w = reduce(lambda a, b: a * b, ws) else: w = weights.kernel_variable("w", kernel_data)[p] From 9321fb7fe333f53c39a34cc6e53a7bfa4179b784 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Tue, 7 Apr 2015 17:05:22 +0100 Subject: [PATCH 180/749] Deprive tensor indices of their identity. Tensor indices don't really have their own identity, so it doesn't make sense to create a new unique variable name. Instead they are given a name from the combination of their factors. This allows a tensor index to be reconstructed correctly if you know its factors. --- finat/indices.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/finat/indices.py b/finat/indices.py index 461fb839e..ecb9e3a88 100644 --- a/finat/indices.py +++ b/finat/indices.py @@ -75,19 +75,27 @@ def __init__(self, pointset): _count = 0 -class TensorPointIndex(IndexBase): +class TensorIndex(IndexBase): + """A mixin to create tensor product indices.""" + def __init__(self, factors): + + self.factors = factors + + name = "_x_".join(f.name for f in factors) + + super(TensorIndex, self).__init__(-1, name) + + +class TensorPointIndex(TensorIndex): """An index running over a set of points which have a tensor product structure. This index is actually composed of multiple factors.""" def __init__(self, pointset): self.points = pointset - name = 'q_' + str(PointIndex._count) - PointIndex._count += 1 - - super(TensorPointIndex, self).__init__(-1, name) + factors = [PointIndex(f) for f in pointset.factor_sets] - self.factors = [PointIndex(f) for f in pointset.factor_sets] + super(TensorPointIndex, self).__init__(factors) def __getattr__(self, name): @@ -111,7 +119,7 @@ def __init__(self, extent): _count = 0 -class TensorBasisFunctionIndex(IndexBase): +class TensorBasisFunctionIndex(TensorIndex): """An index running over a set of basis functions which have a tensor product structure. This index is actually composed of multiple factors. @@ -120,12 +128,7 @@ def __init__(self, *args): assert all([isinstance(a, BasisFunctionIndex) for a in args]) - name = 'i_' + str(BasisFunctionIndex._count) - BasisFunctionIndex._count += 1 - - super(TensorBasisFunctionIndex, self).__init__(-1, name) - - self.factors = args + super(TensorBasisFunctionIndex, self).__init__(args) def __getattr__(self, name): From 008a27b32826bbfc54c80660d56d0feec9a13878 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Tue, 7 Apr 2015 17:08:26 +0100 Subject: [PATCH 181/749] Working simple case of Delta cancellation. This produces the optimal complexity algorithm for the under-integrated spectral mass matrix. Now on to something less trivial. --- finat/mappers.py | 111 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 1 deletion(-) diff --git a/finat/mappers.py b/finat/mappers.py index a0965efe1..ff3cf380f 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -337,7 +337,7 @@ def map_index_sum(self, expr): class FactorDeltaMapper(IdentityMapper): - """Class to pull deltas up the expression tree to maximise the opportunities for cancellation.""" + """Mapper to pull deltas up the expression tree to maximise the opportunities for cancellation.""" def map_product(self, expr, *args, **kwargs): @@ -359,3 +359,112 @@ def map_product(self, expr, *args, **kwargs): result = Delta(delta.indices, result) return result + + +class CancelDeltaMapper(IdentityMapper): + """Mapper to cancel and/or replace indices according to the rules for Deltas.""" + + def map_index_sum(self, expr, replace=None, sum_indices=(), *args, **kwargs): + + if replace: + raise NotImplementedError + elif replace is None: + replace = {} + + def flatten(index): + try: + return (index,) + reduce((lambda a, b: a + b), map(flatten, index.factors)) + except AttributeError: + return (index,) + + flattened = map(flatten, expr.indices) + sum_indices += reduce((lambda a, b: a + b), flattened) + + if type(expr.body) in (IndexSum, ForAll, Delta): + # New index replacements are only possible in chains of sums, foralls and deltas. + body = self.rec(expr.body, *args, replace=replace, sum_indices=sum_indices, **kwargs) + else: + body = self.rec(expr.body, *args, replace=replace, **kwargs) + + new_indices = [] + for index in flattened: + if index[0] in replace: + # Replaced indices are dropped. + pass + elif any(i in replace for i in index[1:]): + for i in index[1:]: + if i not in replace: + new_indices.append(i) + else: + new_indices.append(index[0]) + + if new_indices: + return IndexSum(new_indices, body) + else: + return body + + def map_delta(self, expr, replace=None, sum_indices=(), *args, **kwargs): + + # For the moment let's just go with the delta has two indices idea + assert len(expr.indices) == 2 + + if replace is not None: + indices = tuple(replace[index] if index in replace.keys() else index for index in expr.indices) + else: + indices = expr.indices + + if indices[1] in sum_indices: + replace[indices[1]] = indices[0] + indices = (indices[0], indices[0]) + elif indices[0] in sum_indices: + replace[indices[0]] = indices[1] + indices = (indices[1], indices[1]) + + if indices[0] != indices[1]: + targets = replace.values() + if indices[0] in targets and indices[1] not in targets: + replace[indices[1]] = indices[0] + indices = (indices[0], indices[0]) + elif indices[1] in targets and indices[0] not in targets: + replace[indices[0]] = indices[1] + indices = (indices[0], indices[0]) + else: + # I don't think this can happen. + raise NotImplementedError + + if type(expr.body) in (IndexSum, ForAll, Delta): + # New index replacements are only possible in chains of sums, foralls and deltas. + body = self.rec(expr.body, *args, replace=replace, sum_indices=sum_indices, **kwargs) + else: + body = self.rec(expr.body, *args, replace=replace, **kwargs) + + if indices[0] == indices[1]: + return body + else: + return Delta(indices, body) + + def map_recipe(self, expr, replace=None, *args, **kwargs): + if replace is None: + replace = {} + + body = self.rec(expr.body, *args, replace=replace, **kwargs) + + def recurse_replace(index): + if index in replace: + return replace[index] + else: + try: + return type(index)(*map(recurse_replace, index.factors)) + except AttributeError: + return index + + if replace: + indices = tuple(tuple(map(recurse_replace, itype)) for itype in expr.indices) + else: + indices = expr.indices + + return Recipe(indices, body) + + def map_index(self, expr, replace=None, *args, **kwargs): + + return replace[expr] if replace and expr in replace else expr From 0356024ea507241f8c894dca7a0716c19bd745fb Mon Sep 17 00:00:00 2001 From: David A Ham Date: Wed, 8 Apr 2015 18:10:54 +0100 Subject: [PATCH 182/749] get the indices right on product gradients --- finat/product_elements.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/finat/product_elements.py b/finat/product_elements.py index bc5ace5f2..2ce01864e 100644 --- a/finat/product_elements.py +++ b/finat/product_elements.py @@ -44,12 +44,15 @@ def basis_evaluation(self, q, kernel_data, derivative=None, phi_d = [e.basis_evaluation(q_, kernel_data, derivative=grad, pullback=False) for e, q_ in zip(self.factors, q.factors)] - # Need to replace the basisfunctionindices on phi_d with i - expressions = [reduce(lambda a, b: a.body * b.body, - phi[:d] + [phi_d[d]] + phi[d + 1:]) - for d in range(len(phi))] + # Replace the basisfunctionindices on phi_d with i + phi_d = [p.replace_indices(zip(p.indices[1], (i__,))) + for p, i__ in zip(phi_d, i_)] - alpha_ = [phi_.indices[0][0] for phi_ in phi_d] + expressions = tuple(reduce(lambda a, b: a.body * b.body, + phi[:d] + [phi_d[d]] + phi[d + 1:]) + for d in range(len(phi))) + + alpha_ = tuple(phi_.indices[0][0] for phi_ in phi_d) alpha = DimensionIndex(sum(alpha__.length for alpha__ in alpha_)) assert alpha.length == kernel_data.gdim From f517ecaa537faa00bd26564c93054af5a6482d31 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Wed, 8 Apr 2015 18:46:16 +0100 Subject: [PATCH 183/749] Stack of bugfixes and let mapping rules --- finat/geometry_mapper.py | 14 ++++++++---- finat/indices.py | 21 +++++++++++++----- finat/mappers.py | 48 +++++++++++++++++++++++++++------------- 3 files changed, 59 insertions(+), 24 deletions(-) diff --git a/finat/geometry_mapper.py b/finat/geometry_mapper.py index 0feabf49d..119da3269 100644 --- a/finat/geometry_mapper.py +++ b/finat/geometry_mapper.py @@ -1,5 +1,5 @@ from .points import PointSet -from .indices import PointIndex +from .indices import PointIndex, PointIndexBase from .ast import Let, Det, Inverse, Recipe from .mappers import IdentityMapper from .derivatives import grad @@ -39,6 +39,9 @@ def __call__(self, expr, *args, **kwargs): body = self._bind_geometry(q, body) elif self.local_geometry: + for s in self.local_geometry: + s.set_error() + print expr raise ValueError("Unbound local geometry in tree") # Reconstruct the recipe @@ -58,7 +61,7 @@ def map_index_sum(self, expr): if not self.kernel_data.affine \ and self.local_geometry \ - and isinstance(expr.indices[-1], PointIndex): + and isinstance(expr.indices[-1], PointIndexBase): q = expr.indices[-1] body = self._bind_geometry(q, body) @@ -76,10 +79,13 @@ def _bind_geometry(self, q, body): element = kd.coordinate_element J = element.field_evaluation(phi_x, q, kd, grad, pullback=False) + d, b, q = J.indices + # In the affine case, there is only one point. In the + # non-affine case, binding the point index is the problem of + # kernel as a whole if self.kernel_data.affine: - d, b, q = J.indices J = J.replace_indices(zip(q, (0,))) - J.indices = (d, b, ()) + J.indices = (d, b, ()) inner_lets = ((kd.detJ, Det(kd.J)),) if kd.detJ in self.local_geometry else () inner_lets += ((kd.invJ, Inverse(kd.J)),) if kd.invJ in self.local_geometry else () diff --git a/finat/indices.py b/finat/indices.py index ecb9e3a88..cdd2f40ae 100644 --- a/finat/indices.py +++ b/finat/indices.py @@ -2,6 +2,7 @@ import pymbolic.primitives as p from pymbolic.mapper.stringifier import StringifyMapper import math +from .points import TensorPointSet __all__ = ["PointIndex", "TensorPointIndex", "BasisFunctionIndex", "TensorBasisFunctionIndex", @@ -61,7 +62,12 @@ def set_error(self): self._error = True -class PointIndex(IndexBase): +class PointIndexBase(object): + # Marker class for point indices. + pass + + +class PointIndex(IndexBase, PointIndexBase): '''An index running over a set of points, for example quadrature points.''' def __init__(self, pointset): @@ -86,14 +92,19 @@ def __init__(self, factors): super(TensorIndex, self).__init__(-1, name) -class TensorPointIndex(TensorIndex): +class TensorPointIndex(TensorIndex, PointIndexBase): """An index running over a set of points which have a tensor product structure. This index is actually composed of multiple factors.""" - def __init__(self, pointset): + def __init__(self, *args): - self.points = pointset + if isinstance(args[0], TensorPointSet): + assert len(args) == 1 + + self.points = args[0] - factors = [PointIndex(f) for f in pointset.factor_sets] + factors = [PointIndex(f) for f in args[0].factor_sets] + else: + factors = args super(TensorPointIndex, self).__init__(factors) diff --git a/finat/mappers.py b/finat/mappers.py index ff3cf380f..4b0d3b99c 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -215,8 +215,8 @@ def map_recipe(self, expr, bound_above=None, bound_below=None): d, b, p = expr.indices recipe_indices = tuple([i for i in d + b + p if i not in bound_above]) - free_indices = tuple([i for i in recipe_indices - if i not in bound_below]) + free_indices = tuple(set([i for i in recipe_indices + if i not in bound_below])) bound_below.extendleft(reversed(free_indices)) # Calculate the permutation from the order of loops actually @@ -364,11 +364,12 @@ def map_product(self, expr, *args, **kwargs): class CancelDeltaMapper(IdentityMapper): """Mapper to cancel and/or replace indices according to the rules for Deltas.""" + # Those nodes through which it is legal to transmit sum_indices. + _transmitting_nodes = (IndexSum, ForAll, Delta, Let) + def map_index_sum(self, expr, replace=None, sum_indices=(), *args, **kwargs): - if replace: - raise NotImplementedError - elif replace is None: + if replace is None: replace = {} def flatten(index): @@ -380,8 +381,8 @@ def flatten(index): flattened = map(flatten, expr.indices) sum_indices += reduce((lambda a, b: a + b), flattened) - if type(expr.body) in (IndexSum, ForAll, Delta): - # New index replacements are only possible in chains of sums, foralls and deltas. + if type(expr.body) in self._transmitting_nodes: + # New index replacements are only possible in chains certain ast nodes. body = self.rec(expr.body, *args, replace=replace, sum_indices=sum_indices, **kwargs) else: body = self.rec(expr.body, *args, replace=replace, **kwargs) @@ -420,7 +421,8 @@ def map_delta(self, expr, replace=None, sum_indices=(), *args, **kwargs): replace[indices[0]] = indices[1] indices = (indices[1], indices[1]) - if indices[0] != indices[1]: + # Only attempt new replacements if we are in transmitting node stacks. + if sum_indices and indices[0] != indices[1]: targets = replace.values() if indices[0] in targets and indices[1] not in targets: replace[indices[1]] = indices[0] @@ -428,13 +430,14 @@ def map_delta(self, expr, replace=None, sum_indices=(), *args, **kwargs): elif indices[1] in targets and indices[0] not in targets: replace[indices[0]] = indices[1] indices = (indices[0], indices[0]) - else: - # I don't think this can happen. - raise NotImplementedError - - if type(expr.body) in (IndexSum, ForAll, Delta): - # New index replacements are only possible in chains of sums, foralls and deltas. - body = self.rec(expr.body, *args, replace=replace, sum_indices=sum_indices, **kwargs) + #else: + # # I don't think this can happen. + # raise NotImplementedError + + if type(expr.body) in self._transmitting_nodes: + # New index replacements are only possible in chains of certain ast nodes. + body = self.rec(expr.body, *args, replace=replace, + sum_indices=sum_indices, **kwargs) else: body = self.rec(expr.body, *args, replace=replace, **kwargs) @@ -465,6 +468,21 @@ def recurse_replace(index): return Recipe(indices, body) + def map_let(self, expr, replace=None, sum_indices=(), *args, **kwargs): + # Propagate changes first into the body. Then do any required + # substitutions on the bindings. + + body = self.rec(expr.body, *args, replace=replace, + sum_indices=sum_indices, **kwargs) + + # Need to think about conveying information from the body to + # the bindings about diagonalisations which might occur. + + bindings = self.rec(expr.bindings, *args, replace=replace, + sum_indices=sum_indices, **kwargs) + + return Let(bindings, body) + def map_index(self, expr, replace=None, *args, **kwargs): return replace[expr] if replace and expr in replace else expr From d3492b5a7967d30f1c989e97a57314b9b5144c78 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Wed, 15 Apr 2015 15:52:04 +0100 Subject: [PATCH 184/749] Consistent naming for children of CompoundVector --- finat/ast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/ast.py b/finat/ast.py index 9563ab3d7..8066f2605 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -349,7 +349,7 @@ def __init__(self, index, indices, expressions): super(CompoundVector, self).__init__((index, indices, expressions)) - self.index, self.indices, self.expressions = self.children + self.index, self.indices, self.body = self.children self._color = "blue" From 840714407b88cf8e7a428e97f82c4d41012aa994 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Wed, 15 Apr 2015 15:52:31 +0100 Subject: [PATCH 185/749] fix some index/range behaviour --- finat/indices.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/finat/indices.py b/finat/indices.py index cdd2f40ae..dbace9703 100644 --- a/finat/indices.py +++ b/finat/indices.py @@ -23,6 +23,10 @@ def __init__(self, extent, name): raise TypeError("Extent must be a slice or an int") self._color = "yellow" + self.start = self._extent.start + self.stop = self._extent.stop + self.step = self._extent.step + @property def extent(self): '''A slice indicating the values this index can take.''' @@ -37,6 +41,16 @@ def length(self): return int(math.ceil((stop - start) / step)) + @property + def as_range(self): + """Convert a slice to a range. If the range has expressions as bounds, + evaluate them. + """ + + return range(int(self._extent.start or 0), + int(self._extent.stop), + int(self._extent.step or 1)) + @property def _str_extent(self): From d931bb4f6e197bfd689c26326518134369af881c Mon Sep 17 00:00:00 2001 From: David A Ham Date: Wed, 15 Apr 2015 15:53:13 +0100 Subject: [PATCH 186/749] interpreter no longer needs _as_range because indices have it --- finat/interpreter.py | 9 ---- finat/mappers.py | 108 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 105 insertions(+), 12 deletions(-) diff --git a/finat/interpreter.py b/finat/interpreter.py index 8210512d7..f526a83cc 100644 --- a/finat/interpreter.py +++ b/finat/interpreter.py @@ -22,15 +22,6 @@ def __init__(self, context={}): # Storage for wave variables while they are out of scope. self.wave_vars = {} - def _as_range(self, e): - """Convert a slice to a range. If the range has expressions as bounds, - evaluate them. - """ - - return range(int(self.rec(e.start or 0)), - int(self.rec(e.stop)), - int(self.rec(e.step or 1))) - def map_variable(self, expr): try: var = self.context[expr.name] diff --git a/finat/mappers.py b/finat/mappers.py index 4b0d3b99c..56661bbe0 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -3,8 +3,9 @@ from pymbolic.mapper.stringifier import StringifyMapper, PREC_NONE from pymbolic.mapper import WalkMapper as WM from pymbolic.mapper.graphviz import GraphvizMapper as GVM +from pymbolic.primitives import Product, Sum from .indices import IndexBase -from .ast import Recipe, ForAll, IndexSum, Let, Variable, Delta +from .ast import Recipe, ForAll, IndexSum, Let, Variable, Delta, CompoundVector try: from termcolor import colored except ImportError: @@ -25,8 +26,8 @@ def map_index(self, expr, *args, **kwargs): return expr def map_delta(self, expr, *args, **kwargs): - return expr.__class__(*(self.rec(c, *args, **kwargs) - for c in expr.children)) + return expr.__class__(*tuple(self.rec(c, *args, **kwargs) + for c in expr.children)) def map_inverse(self, expr, *args, **kwargs): return expr.__class__(self.rec(expr.expression, *args, **kwargs)) @@ -55,6 +56,34 @@ def map_index(self, expr, *args, **kwargs): except KeyError: return expr + def map_compound_vector(self, expr, *args, **kwargs): + # Symbolic replacement of indices on a CompoundVector Just + # Works. However replacing the compound vector index with a + # number should collapse the CompoundVector. + + if expr.index in self.replacements and\ + not isinstance(self.replacements[expr.index], IndexBase): + # Work out which subvector we are in and what the index value is. + i = expr.index + val = self.replacements[expr.index] + + pos = (val - (i.start or 0))/(i.step or 1) + assert pos <= i.length + + for subindex, body in zip(expr.indices, expr.body): + if pos < subindex.length: + sub_i = pos * (subindex.step or 1) + (subindex.start or 0) + self.replacements[subindex] = sub_i + result = self.rec(body, *args, **kwargs) + self.replacements.pop(subindex) + return result + else: + pos -= subindex.length + + raise ValueError("Illegal index value.") + else: + return super(_IndexMapper, self).map_compound_vector(expr, *args, **kwargs) + class _StringifyMapper(StringifyMapper): @@ -336,6 +365,79 @@ def map_index_sum(self, expr): return temp +class CancelCompoundVectorMapper(IdentityMapper): + """Mapper to find and expand reductions over CompoundVectors. + + Eventually this probably needs some policy support to decide which + cases it is worth expanding and cancelling and which not. + """ + def map_index_sum(self, expr, *args, **kwargs): + + if isinstance(expr.body, (Product, Sum)): + fs = [] + body = self.rec(expr.body, *args, + sum_indices=expr.indices, factored_summands=fs) + + if fs: + assert len(fs) == 1 + indices = tuple(i for i in expr.indices if i != fs[0][0]) + if indices: + return IndexSum(indices, fs[0][1]) + else: + return fs[0][1] + else: + return IndexSum(expr.indices, body) + else: + return super(CancelCompoundVectorMapper, self).map_index_sum( + expr, *args, **kwargs) + + def map_product(self, expr, sum_indices=None, factored_summands=None, *args, + **kwargs): + + try: + if not sum_indices: + raise ValueError + vec_i = None + vectors = [] + factors = [] + for c in expr.children: + if isinstance(c, CompoundVector): + if vec_i is None: + vec_i = c.index + elif c.index != vec_i: + raise ValueError + vectors.append(c) + else: + factors.append(c) + + if vec_i is None: + raise ValueError # No CompoundVector + if vec_i in sum_indices: + # Flatten the CompoundVector. + flattened = 0 + + r = {} + replacer = _IndexMapper(r) + for i in vec_i.as_range: + r[vec_i] = i + prod = 1 + for c in expr.children: + prod *= replacer(c) + flattened += prod + + factored_summands.append((vec_i, flattened)) + return 0.0 + + else: + # Eventually we want to push the sum inside the vector. + raise ValueError + + except ValueError: + # Drop to here if this is not a cancellation opportunity for whatever reason. + return super(CancelCompoundVectorMapper, self).map_product( + expr, *args, **kwargs) + + class FactorDeltaMapper(IdentityMapper): """Mapper to pull deltas up the expression tree to maximise the opportunities for cancellation.""" From 672e1717d50e9629608792a2393be5cc7eeabe4d Mon Sep 17 00:00:00 2001 From: David A Ham Date: Wed, 15 Apr 2015 16:20:07 +0100 Subject: [PATCH 187/749] Turns out we do need _as_range --- finat/interpreter.py | 9 +++++++++ finat/mappers.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/finat/interpreter.py b/finat/interpreter.py index f526a83cc..8210512d7 100644 --- a/finat/interpreter.py +++ b/finat/interpreter.py @@ -22,6 +22,15 @@ def __init__(self, context={}): # Storage for wave variables while they are out of scope. self.wave_vars = {} + def _as_range(self, e): + """Convert a slice to a range. If the range has expressions as bounds, + evaluate them. + """ + + return range(int(self.rec(e.start or 0)), + int(self.rec(e.stop)), + int(self.rec(e.step or 1))) + def map_variable(self, expr): try: var = self.context[expr.name] diff --git a/finat/mappers.py b/finat/mappers.py index 56661bbe0..d3a9ceda0 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -9,7 +9,7 @@ try: from termcolor import colored except ImportError: - def colored(string, color, attrs=[]): + def colored(string, color, attrs=None): return string From 8726f6fb50aa65790e5bde73e1926f6b9cb85c10 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Thu, 16 Apr 2015 10:47:22 +0100 Subject: [PATCH 188/749] Make elements hashable. Make elements hashable and comparable and use this to eliminate duplicate element tabulation storage in the kernel. --- finat/finiteelementbase.py | 11 +++++++++++ finat/product_elements.py | 13 ++++++++++++- finat/utils.py | 2 +- finat/vectorfiniteelement.py | 13 +++++++++++++ 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index a882ceee7..a7661e056 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -124,6 +124,17 @@ def dual_evaluation(self, kernel_data): raise NotImplementedError + def __hash__(self): + """Elements are equal if they have the same class, degree, and cell.""" + + return hash((type(self), self._cell, self._degree)) + + def __eq__(self, other): + """Elements are equal if they have the same class, degree, and cell.""" + + return type(self) == type(other) and self._cell == other._cell and\ + self._degree == other._degree + class ScalarElementMixin(object): """Mixin class containing field evaluation and moment rules for scalar diff --git a/finat/product_elements.py b/finat/product_elements.py index 2ce01864e..6d7c6c01c 100644 --- a/finat/product_elements.py +++ b/finat/product_elements.py @@ -13,7 +13,7 @@ def __init__(self, *args): assert all([isinstance(e, FiniteElementBase) for e in args]) - self.factors = args + self.factors = tuple(args) self._degree = max([a._degree for a in args]) @@ -72,3 +72,14 @@ def basis_evaluation(self, q, kernel_data, derivative=None, expr = reduce(lambda a, b: a.body * b.body, phi) return Recipe(indices=ind, body=expr) + + def __hash__(self): + """ScalarProductElements are equal if their factors are equal""" + + return hash(self.factors) + + def __eq__(self, other): + """VectorFiniteElements are equal if they have the same base element + and dimension.""" + + return self.factors == other.factors diff --git a/finat/utils.py b/finat/utils.py index 662379bac..279d72dfc 100644 --- a/finat/utils.py +++ b/finat/utils.py @@ -59,7 +59,7 @@ def tabulation_variable_name(self, element, points): where n is guaranteed to be unique to that combination of element and points.""" - key = (id(element), id(points)) + key = (element, id(points)) try: return self._variable_cache[key] diff --git a/finat/vectorfiniteelement.py b/finat/vectorfiniteelement.py index 099911cda..bc297ec5e 100644 --- a/finat/vectorfiniteelement.py +++ b/finat/vectorfiniteelement.py @@ -226,3 +226,16 @@ def moment_evaluation(self, value, weights, q, expression = IndexSum(d + p, psi * phi * w[p]) return Recipe(((), b + beta + b_, ()), expression) + + def __hash__(self): + """VectorFiniteElements are equal if they have the same base element + and dimension.""" + + return hash((self._dimension, self._base_element)) + + def __eq__(self, other): + """VectorFiniteElements are equal if they have the same base element + and dimension.""" + + return self._dimension == other._dimension and\ + self._base_element == other._base_element From 9a9b00bbeb5157ffb033af855cf333e380c923be Mon Sep 17 00:00:00 2001 From: David A Ham Date: Thu, 16 Apr 2015 10:49:51 +0100 Subject: [PATCH 189/749] Eliminate spurious duplicates from quadrature --- finat/quadrature.py | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/finat/quadrature.py b/finat/quadrature.py index d3e0debf8..f94f31252 100644 --- a/finat/quadrature.py +++ b/finat/quadrature.py @@ -55,29 +55,44 @@ class GaussLobattoQuadrature(QuadratureRule): def __init__(self, cell, points): """Gauss-Lobatto-Legendre quadrature on hypercubes. :param cell: The reference cell on which to define the quadrature. - :param points: The number of points. In more than one dimension, a - tuple of points in each dimension. + :param points: The number of points, or a tuple giving the number of + points in each dimension. """ - def expand_quad(cell): - if cell.get_spatial_dimension() == 1: - return [FIAT.quadrature.GaussLobattoQuadratureLineRule(cell, points)] + def expand_quad(cell, points): + d = cell.get_spatial_dimension() + if d == 1: + return ((cell, points, + FIAT.quadrature.GaussLobattoQuadratureLineRule(cell, points[0])),) else: try: - return expand_quad(cell.A) + expand_quad(cell.B) + d_a = cell.A.get_spatial_dimension() + return expand_quad(cell.A, points[:d_a])\ + + expand_quad(cell.B, points[d_a:]) except AttributeError(): raise ValueError("Unable to create Gauss-Lobatto quadrature on ", + str(cell)) + try: + points = tuple(points) + except TypeError: + points = (points,) - q = expand_quad(cell) + if len(points) == 1: + points *= cell.get_spatial_dimension() - if len(q) == 1: + cpq = expand_quad(cell, points) + + # uniquify q. + lookup = {(c, p): (GaussLobattoPointSet(q.get_points()), + PointSet(q.get_weights())) for c, p, q in cpq} + pointset = tuple(lookup[c, p][0] for c, p, _ in cpq) + weightset = tuple(lookup[c, p][1] for c, p, _ in cpq) + + if len(cpq) == 1: super(GaussLobattoQuadrature, self).__init__( - cell, - GaussLobattoPointSet(q[0].get_points()), - PointSet(q[0].get_weights())) + cell, pointset[0], weightset[0]) else: super(GaussLobattoQuadrature, self).__init__( cell, - TensorPointSet([GaussLobattoPointSet(q_.get_points()) for q_ in q]), - [PointSet(q_.get_weights()) for q_ in q]) + TensorPointSet(pointset), + weightset) From 66cca76ff8c885e8557db87c4f84806986ffeede Mon Sep 17 00:00:00 2001 From: David A Ham Date: Thu, 16 Apr 2015 10:50:20 +0100 Subject: [PATCH 190/749] Remove the spurious let on the kernel output --- finat/mappers.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/finat/mappers.py b/finat/mappers.py index d3a9ceda0..b25775bf0 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -319,6 +319,14 @@ def __init__(self, kernel_data): self._isum_stack = {} self._bound_isums = set() + def __call__(self, expr): + + if isinstance(expr.body, IndexSum): + self._bound_isums.add(expr.body) + + return super(IndexSumMapper, self).__call__(expr) + + def _bind_isums(self, expr): bindings = [] if isinstance(expr, Variable): From d0717a69c4185eb4052e94d280302f68ce5776b0 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Thu, 16 Apr 2015 15:59:38 +0100 Subject: [PATCH 191/749] Better flattening --- finat/finiteelementbase.py | 5 ++++- finat/mappers.py | 32 ++++++++++++++------------------ 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index a7661e056..32ee5cfd8 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -169,11 +169,14 @@ def moment_evaluation(self, value, weights, q, else: w = weights.kernel_variable("w", kernel_data)[p] - expr = psi * phi * w + expr = psi * phi if d: expr = IndexSum(d, expr) + # The quadrature weights go outside any indexsum! + expr *= w + if pullback: expr *= Abs(kernel_data.detJ) diff --git a/finat/mappers.py b/finat/mappers.py index b25775bf0..416d00b49 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -420,25 +420,21 @@ def map_product(self, expr, sum_indices=None, factored_summands=None, *args, if vec_i is None: raise ValueError # No CompoundVector - if vec_i in sum_indices: - # Flatten the CompoundVector. - flattened = 0 - - r = {} - replacer = _IndexMapper(r) - for i in vec_i.as_range: - r[vec_i] = i - prod = 1 - for c in expr.children: - prod *= replacer(c) - flattened += prod - - factored_summands.append((vec_i, flattened)) - return 0.0 - else: - # Eventually we want to push the sum inside the vector. - raise ValueError + # Flatten the CompoundVector. + flattened = 0 + + r = {} + replacer = _IndexMapper(r) + for i in vec_i.as_range: + r[vec_i] = i + prod = 1 + for c in expr.children: + prod *= replacer(c) + flattened += prod + + factored_summands.append((vec_i, flattened)) + return 0.0 except ValueError: # Drop to here if this is not a cancellation opportunity for whatever reason. From f968ab6eba285a1756547214862b86b82e5fb37a Mon Sep 17 00:00:00 2001 From: David A Ham Date: Fri, 17 Apr 2015 15:34:51 +0100 Subject: [PATCH 192/749] Beginnings of IndicesMapper --- finat/mappers.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/finat/mappers.py b/finat/mappers.py index 416d00b49..6565415c7 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -220,6 +220,27 @@ def map_index_sum(self, expr, *args, **kwargs): map_compound_vector = map_index_sum +class IndicesMapper(WalkMapper): + """Label an AST with the indices which occur below each node.""" + + def __init__(self): + self._index_stack = [set()] + + def visit(self, expr, *args, **kwargs): + # Put a new index frame onto the stack. + self._index_stack.append(set()) + return True + + def post_visit(self, expr, *args, **kwargs): + # The frame contains any indices we directly saw: + expr._indices_below = tuple(self._index_stack.pop()) + + if isinstance(expr, IndexBase): + expr._indices_below += expr + + self._index_stack[-1].union(expr._indices_below) + + class GraphvizMapper(WalkMapper, GVM): pass From d4fb7068385aeb285ca5699ff775631c27365d09 Mon Sep 17 00:00:00 2001 From: David Ham Date: Tue, 21 Apr 2015 09:11:08 +0100 Subject: [PATCH 193/749] pep8 --- finat/mappers.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/finat/mappers.py b/finat/mappers.py index 6565415c7..5b1970106 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -67,7 +67,7 @@ def map_compound_vector(self, expr, *args, **kwargs): i = expr.index val = self.replacements[expr.index] - pos = (val - (i.start or 0))/(i.step or 1) + pos = (val - (i.start or 0)) / (i.step or 1) assert pos <= i.length for subindex, body in zip(expr.indices, expr.body): @@ -230,7 +230,7 @@ def visit(self, expr, *args, **kwargs): # Put a new index frame onto the stack. self._index_stack.append(set()) return True - + def post_visit(self, expr, *args, **kwargs): # The frame contains any indices we directly saw: expr._indices_below = tuple(self._index_stack.pop()) @@ -347,7 +347,6 @@ def __call__(self, expr): return super(IndexSumMapper, self).__call__(expr) - def _bind_isums(self, expr): bindings = [] if isinstance(expr, Variable): From 0856c99b5ce30d23b3d4e175c86d651691418c32 Mon Sep 17 00:00:00 2001 From: David Ham Date: Tue, 21 Apr 2015 09:49:16 +0100 Subject: [PATCH 194/749] pep8 --- finat/mappers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/mappers.py b/finat/mappers.py index 5b1970106..bfb70372d 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -556,7 +556,7 @@ def map_delta(self, expr, replace=None, sum_indices=(), *args, **kwargs): elif indices[1] in targets and indices[0] not in targets: replace[indices[0]] = indices[1] indices = (indices[0], indices[0]) - #else: + # else: # # I don't think this can happen. # raise NotImplementedError From 171ad775a3c611b30099becf5b2246b3d08ea6a2 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Thu, 14 May 2015 16:00:10 +0100 Subject: [PATCH 195/749] Some form of sum factorisation --- finat/indices.py | 9 +- finat/mappers.py | 242 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 246 insertions(+), 5 deletions(-) diff --git a/finat/indices.py b/finat/indices.py index dbace9703..afab7d1b2 100644 --- a/finat/indices.py +++ b/finat/indices.py @@ -131,7 +131,12 @@ def __getattr__(self, name): raise AttributeError -class BasisFunctionIndex(IndexBase): +class BasisFunctionIndexBase(object): + # Marker class for point indices. + pass + + +class BasisFunctionIndex(BasisFunctionIndexBase, IndexBase): '''An index over a local set of basis functions. E.g. test functions on an element.''' def __init__(self, extent): @@ -144,7 +149,7 @@ def __init__(self, extent): _count = 0 -class TensorBasisFunctionIndex(TensorIndex): +class TensorBasisFunctionIndex(BasisFunctionIndexBase, TensorIndex): """An index running over a set of basis functions which have a tensor product structure. This index is actually composed of multiple factors. diff --git a/finat/mappers.py b/finat/mappers.py index bfb70372d..b0afcff2e 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -3,8 +3,9 @@ from pymbolic.mapper.stringifier import StringifyMapper, PREC_NONE from pymbolic.mapper import WalkMapper as WM from pymbolic.mapper.graphviz import GraphvizMapper as GVM -from pymbolic.primitives import Product, Sum -from .indices import IndexBase +from pymbolic.primitives import Product, Sum, flattened_product, flattened_sum +from .indices import IndexBase, TensorIndex, PointIndexBase, BasisFunctionIndexBase,\ + DimensionIndex from .ast import Recipe, ForAll, IndexSum, Let, Variable, Delta, CompoundVector try: from termcolor import colored @@ -478,7 +479,6 @@ def map_product(self, expr, *args, **kwargs): else: factors.append(child) - from pymbolic.primitives import flattened_product result = flattened_product(tuple(factors)) for delta in deltas: @@ -612,3 +612,239 @@ def map_let(self, expr, replace=None, sum_indices=(), *args, **kwargs): def map_index(self, expr, replace=None, *args, **kwargs): return replace[expr] if replace and expr in replace else expr + + +class _DoNotFactorSet(set): + """Dummy set object used to indicate that sum factorisation of a subtree is invalid.""" + pass + + +class SumFactorMapper(IdentityMapper): + """Mapper to attempt sum factorisation. This is currently a sketch + implementation which is not safe for particularly general cases.""" + + # Internal communication of separable index sets is achieved by + # the index_groups argument. This is a set containing tuples of + # grouped indices. + + def __init__(self, kernel_data): + + super(SumFactorMapper, self).__init__() + + self.kernel_data = kernel_data + + @staticmethod + def factor_indices(indices, index_groups): + # Determine the factorisability of the expression with the + # given index_groups assuming an IndexSum over indices. + if len(indices) != 1 or not isinstance(indices[0], TensorIndex) \ + or len(indices[0].factors) != 2: + return False + + i = list(indices[0].factors) + + # Try to factor the longest length first. + if i[0].length < i[1].length: + i.reverse() + + for n in range(2): + factorstack = [] + nontrivial = False + for g in index_groups: + if i[n] in g: + factorstack.append(g) + elif i[(n+1) % 2] in g: + nontrivial = True + + if factorstack and nontrivial: + return i[n] + + return False + + def map_index_sum(self, expr, index_groups=None, *args, **kwargs): + """Discover how factorisable this IndexSum is.""" + + body_igroups = set() + body = self.rec(expr.body, *args, index_groups=body_igroups, **kwargs) + + factor_index = self.factor_indices(expr.indices, body_igroups) + if factor_index: + factorised = SumFactorSubTreeMapper(factor_index)(expr.body) + try: + return factorised.generate_factored_expression(self.kernel_data, expr.indices[0].factors) + except: + pass + + return expr.__class__(expr.indices, body) + + def map_index(self, expr, index_groups=None, *args, **kwargs): + """Add this index into all the sets in index_groups.""" + + if index_groups is None: + return expr + elif hasattr(expr, "factors"): + return expr.__class__(*self.rec(expr.factors, *args, index_groups=index_groups, **kwargs)) + elif index_groups: + news = [] + for s in index_groups: + news.append(s + (expr,)) + index_groups.clear() + index_groups.update(news) + else: + index_groups.add((expr,)) + + return expr + + def map_product(self, expr, index_groups=None, *args, **kwargs): + """Union of the index groups of the children.""" + + if index_groups is None: + return super(SumFactorMapper, self).map_product(expr, *args, **kwargs) + else: + result = 1 + for c in expr.children: + igroup = set() + result *= self.rec(c, *args, index_groups=igroup, **kwargs) + index_groups |= igroup + + return result + + def map_sum(self, expr, index_groups=None, *args, **kwargs): + """If the summands have the same factors, propagate them up. + Otherwise (for the moment) put a _DoNotFactorSet in the output. + """ + + igroups = [set() for c in expr.children] + new_children = [self.rec(c, *args, index_groups=i, **kwargs) + for c, i in zip(expr.children, igroups)] + + if index_groups is not None: + # index_groups really should be empty. + if index_groups: + raise ValueError("Can't happen!") + + # This is not quite safe as it imposes additional ordering. + if all([i == igroups[0] for i in igroups[1:]]): + index_groups.update(igroups[0]) + else: + raise ValueError("Don't know how to do this") + + return flattened_sum(tuple(new_children)) + + +class _Factors(object): + """A product factorised by the presence or absence of the index provided.""" + def __init__(self, index, expr=None, indices=None): + self.index = index + self.factor = 1 + self.remainder = 1 + + if expr: + self.insert(expr, indices) + + def insert(self, expr, indices): + if self.index in indices: + self.factor *= expr + else: + self.remainder *= expr + + def __imul__(self, other): + + if isinstance(other, _Factors): + if other.index != self.index: + raise ValueError("Can only multiply _Factors with the same index") + self.factor *= other.factor + self.remainder *= other.remainder + else: + self.insert(*other) + return self + + def generate_factored_expression(self, kernel_data, indices): + # Generate the factored expression using the set of indices provided. + indices = list(indices) + indices.remove(self.index) + + temp = kernel_data.new_variable("isum") + d = tuple(i for i in indices if isinstance(i, DimensionIndex)) + b = tuple(i for i in indices if isinstance(i, BasisFunctionIndexBase)) + p = tuple(i for i in indices if isinstance(i, PointIndexBase)) + return Let(((temp, Recipe((d, b, p), IndexSum((self.index,), self.factor))),), + IndexSum(tuple(indices), temp[d+b+p]*self.remainder)) + + +class _FactorSum(object): + """A sum of _Factors.""" + + def __init__(self, factors=None): + + self.factors = list(factors or []) + + def __iadd__(self, factor): + + self.factors.append(factor) + return self + + def __imul__(self, other): + + assert isinstance(other, _Factors) + for f in self.factors: + f *= other + return self + + def generate_factored_expression(self, kernel_data, indices): + # Generate the factored expression using the set of indices provided. + + return flattened_sum([f.generate_factored_expression(kernel_data, indices) + for f in self.factors]) + + +class SumFactorSubTreeMapper(IdentityMapper): + """Mapper to actually impose a defined factorisation on a subtree.""" + + def __init__(self, factor_index): + + super(SumFactorSubTreeMapper, self).__init__() + + # The index with respect to which the factorisation should occur. + self.factor_index = factor_index + + def map_index(self, expr, indices=None, *args, **kwargs): + """Add this index into all the sets in index_groups.""" + + if indices is None: + return expr + elif hasattr(expr, "factors"): + return expr.__class__(*self.rec(expr.factors, *args, indices=indices, **kwargs)) + else: + indices.add(expr) + + return expr + + def map_product(self, expr, indices=None, *args, **kwargs): + + f = _Factors(self.factor_index) + for c in expr.children: + i = set() + rc = self.rec(c, *args, indices=i, **kwargs) + if isinstance(rc, _FactorSum): + rc *= f + f = rc + elif isinstance(rc, _Factors): + f *= rc + else: + f *= (rc, i) + + return f + + def map_sum(self, expr, indices=None, *args, **kwargs): + + f = _FactorSum() + for c in expr.children: + i = set() + rc = self.rec(c, *args, indices=i, **kwargs) + if isinstance(rc, (_Factors, _FactorSum)): + f += rc + else: + f += _Factors(self.factor_index, rc, i) + + return f From 8eec3081f2f33951b5c718ba9d8c7db259a689c9 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Thu, 14 May 2015 16:57:36 +0100 Subject: [PATCH 196/749] better ast generation from sums in sum factorisation --- finat/mappers.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/finat/mappers.py b/finat/mappers.py index b0afcff2e..26be5fb5d 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -794,8 +794,15 @@ def __imul__(self, other): def generate_factored_expression(self, kernel_data, indices): # Generate the factored expression using the set of indices provided. - return flattened_sum([f.generate_factored_expression(kernel_data, indices) - for f in self.factors]) + genexprs = [f.generate_factored_expression(kernel_data, indices) + for f in self.factors] + + if all(self.factors[0].index == f.index for f in self.factors[1:]): + return Let(tuple(g.bindings[0] for g in genexprs), + IndexSum(genexprs[0].body.indices, + flattened_sum(tuple(g.body.body for g in genexprs)))) + else: + return flattened_sum(genexprs) class SumFactorSubTreeMapper(IdentityMapper): From ac5a06f3489c1592908140a071cfbaf068d55c33 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Thu, 14 May 2015 17:10:45 +0100 Subject: [PATCH 197/749] better ast generation from sums in sum factorisation --- finat/mappers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/finat/mappers.py b/finat/mappers.py index 26be5fb5d..13fdd8dd2 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -797,10 +797,10 @@ def generate_factored_expression(self, kernel_data, indices): genexprs = [f.generate_factored_expression(kernel_data, indices) for f in self.factors] + # This merges some indexsums but that gets in the way of delta cancellation. if all(self.factors[0].index == f.index for f in self.factors[1:]): return Let(tuple(g.bindings[0] for g in genexprs), - IndexSum(genexprs[0].body.indices, - flattened_sum(tuple(g.body.body for g in genexprs)))) + flattened_sum(tuple(g.body for g in genexprs))) else: return flattened_sum(genexprs) From 9bba90a27a4f174eae6893bdb1a36a5db7e552fa Mon Sep 17 00:00:00 2001 From: David A Ham Date: Thu, 14 May 2015 17:17:24 +0100 Subject: [PATCH 198/749] pep8 --- finat/mappers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/finat/mappers.py b/finat/mappers.py index 13fdd8dd2..11c2a3636 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -653,7 +653,7 @@ def factor_indices(indices, index_groups): for g in index_groups: if i[n] in g: factorstack.append(g) - elif i[(n+1) % 2] in g: + elif i[(n + 1) % 2] in g: nontrivial = True if factorstack and nontrivial: @@ -769,7 +769,7 @@ def generate_factored_expression(self, kernel_data, indices): b = tuple(i for i in indices if isinstance(i, BasisFunctionIndexBase)) p = tuple(i for i in indices if isinstance(i, PointIndexBase)) return Let(((temp, Recipe((d, b, p), IndexSum((self.index,), self.factor))),), - IndexSum(tuple(indices), temp[d+b+p]*self.remainder)) + IndexSum(tuple(indices), temp[d + b + p] * self.remainder)) class _FactorSum(object): @@ -800,7 +800,7 @@ def generate_factored_expression(self, kernel_data, indices): # This merges some indexsums but that gets in the way of delta cancellation. if all(self.factors[0].index == f.index for f in self.factors[1:]): return Let(tuple(g.bindings[0] for g in genexprs), - flattened_sum(tuple(g.body for g in genexprs))) + flattened_sum(tuple(g.body for g in genexprs))) else: return flattened_sum(genexprs) From 7e5125b0894adb5d0c3c7a387162eaec07d6bdd2 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Thu, 14 May 2015 17:50:14 +0100 Subject: [PATCH 199/749] Fix scoping bug in delta cancellation --- finat/mappers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/finat/mappers.py b/finat/mappers.py index 11c2a3636..3b7a93b04 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -517,7 +517,8 @@ def flatten(index): for index in flattened: if index[0] in replace: # Replaced indices are dropped. - pass + replace.pop(index[0]) + elif any(i in replace for i in index[1:]): for i in index[1:]: if i not in replace: @@ -540,6 +541,7 @@ def map_delta(self, expr, replace=None, sum_indices=(), *args, **kwargs): else: indices = expr.indices + # fix this so that the replacements happen from the bottom up. if indices[1] in sum_indices: replace[indices[1]] = indices[0] indices = (indices[0], indices[0]) From 83f6d51bf5c23358f57bd433036e9ba16177b473 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Tue, 19 May 2015 15:13:41 +0100 Subject: [PATCH 200/749] product element mixin --- finat/product_elements.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/finat/product_elements.py b/finat/product_elements.py index 6d7c6c01c..3742585b2 100644 --- a/finat/product_elements.py +++ b/finat/product_elements.py @@ -5,8 +5,10 @@ from .ast import Recipe, CompoundVector, IndexSum from FIAT.reference_element import two_product_cell +class ProductElement(object): + """Mixin class describing product elements.""" -class ScalarProductElement(ScalarElementMixin, FiniteElementBase): +class ScalarProductElement(ProductElement, ScalarElementMixin, FiniteElementBase): """A scalar-valued tensor product element.""" def __init__(self, *args): super(ScalarProductElement, self).__init__() From d109be3f592845886addca79bdd2efb2634365ff Mon Sep 17 00:00:00 2001 From: David A Ham Date: Tue, 19 May 2015 15:14:20 +0100 Subject: [PATCH 201/749] Don't subscript index unless you know you can --- finat/ast.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index 8066f2605..4dd4d343e 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -129,10 +129,10 @@ def __init__(self, indices, body): # Inline import to avoid circular dependency. from indices import IndexBase - if isinstance(indices[0], IndexBase): - indices = tuple(indices) - else: + if isinstance(indices, IndexBase): indices = (indices,) + else: + indices = tuple(indices) # Perform trivial simplification of repeated indexsum. if isinstance(body, IndexSum): From 02ebb5a999005e3ad447c59783b0517939291e74 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Tue, 19 May 2015 15:15:18 +0100 Subject: [PATCH 202/749] flatten operation --- finat/indices.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/finat/indices.py b/finat/indices.py index afab7d1b2..9e90c0c8c 100644 --- a/finat/indices.py +++ b/finat/indices.py @@ -75,6 +75,12 @@ def __repr__(self): def set_error(self): self._error = True + @property + def flattened(self): + """For tensor product indices, this returns their factors. In the + simple index case, this returns the 1-tuple of the list itself.""" + return (self,) + class PointIndexBase(object): # Marker class for point indices. @@ -105,6 +111,12 @@ def __init__(self, factors): super(TensorIndex, self).__init__(-1, name) + @property + def flattened(self): + """Return the tuple of scalar indices of which this tensor index is made.""" + + return reduce(tuple.__add__, (f.flattened for f in self.factors)) + class TensorPointIndex(TensorIndex, PointIndexBase): """An index running over a set of points which have a tensor product @@ -156,8 +168,6 @@ class TensorBasisFunctionIndex(BasisFunctionIndexBase, TensorIndex): """ def __init__(self, *args): - assert all([isinstance(a, BasisFunctionIndex) for a in args]) - super(TensorBasisFunctionIndex, self).__init__(args) def __getattr__(self, name): From eecbba5c996f16a9e27952cd29a6432e22c1ac0a Mon Sep 17 00:00:00 2001 From: David A Ham Date: Tue, 19 May 2015 16:32:59 +0100 Subject: [PATCH 203/749] pep8 --- finat/product_elements.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/finat/product_elements.py b/finat/product_elements.py index 3742585b2..bed5c7383 100644 --- a/finat/product_elements.py +++ b/finat/product_elements.py @@ -5,9 +5,11 @@ from .ast import Recipe, CompoundVector, IndexSum from FIAT.reference_element import two_product_cell + class ProductElement(object): """Mixin class describing product elements.""" + class ScalarProductElement(ProductElement, ScalarElementMixin, FiniteElementBase): """A scalar-valued tensor product element.""" def __init__(self, *args): From eeffa40b6f91fbdef009644141859ef820a5be90 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Tue, 19 May 2015 16:37:14 +0100 Subject: [PATCH 204/749] index flattening operator --- finat/indices.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/finat/indices.py b/finat/indices.py index 9e90c0c8c..b8e5721e3 100644 --- a/finat/indices.py +++ b/finat/indices.py @@ -7,7 +7,15 @@ __all__ = ["PointIndex", "TensorPointIndex", "BasisFunctionIndex", "TensorBasisFunctionIndex", "SimpliciallyGradedBasisFunctionIndex", - "DimensionIndex"] + "DimensionIndex", "flattened"] + + +def flattened(indices): + """Flatten an index or a tuple of indices into a tuple of scalar indices.""" + if isinstance(indices, IndexBase): + return indices.flattened + else: + return reduce(tuple.__add__, (flattened(i) for i in indices)) class IndexBase(ast.Variable): From 344c7145068d4429690e2ca54e2a38e701136b84 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Tue, 19 May 2015 16:48:59 +0100 Subject: [PATCH 205/749] some more sum factorisation voodoo - which still doesn't work --- finat/mappers.py | 117 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 81 insertions(+), 36 deletions(-) diff --git a/finat/mappers.py b/finat/mappers.py index 3b7a93b04..f598c4c94 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -5,7 +5,7 @@ from pymbolic.mapper.graphviz import GraphvizMapper as GVM from pymbolic.primitives import Product, Sum, flattened_product, flattened_sum from .indices import IndexBase, TensorIndex, PointIndexBase, BasisFunctionIndexBase,\ - DimensionIndex + DimensionIndex, flattened from .ast import Recipe, ForAll, IndexSum, Let, Variable, Delta, CompoundVector try: from termcolor import colored @@ -402,25 +402,48 @@ class CancelCompoundVectorMapper(IdentityMapper): """ def map_index_sum(self, expr, *args, **kwargs): - if isinstance(expr.body, (Product, Sum)): - fs = [] - body = self.rec(expr.body, *args, - sum_indices=expr.indices, factored_summands=fs) + body = self.rec(expr.body, *args, sum_indices=expr.indices) + + if isinstance(body, CompoundVector): + + if body.index in flattened(expr.indices): + + # Flatten the CompoundVector. + flattened_vector = 0 + r = {} + replacer = _IndexMapper(r) + for i in body.index.as_range: + r[body.index] = i + flattened_vector += replacer(body) + + indices = tuple(i for i in expr.indices if i != body.index) - if fs: - assert len(fs) == 1 - indices = tuple(i for i in expr.indices if i != fs[0][0]) if indices: - return IndexSum(indices, fs[0][1]) + return IndexSum(indices, flattened_vector) else: - return fs[0][1] + return flattened_vector + else: - return IndexSum(expr.indices, body) + # Push the indexsum inside the CompoundVector in the hope + # that better cancellation will occur. + return CompoundVector(body.index, body.indices, + tuple(IndexSum(expr.indices, b) for b in body.body)) else: - return super(CancelCompoundVectorMapper, self).map_index_sum( - expr, *args, **kwargs) - - def map_product(self, expr, sum_indices=None, factored_summands=None, *args, + return IndexSum(expr.indices, body) + # if fs: + # assert len(fs) == 1 + # indices = tuple(i for i in expr.indices if i != fs[0][0]) + # if indices: + # return IndexSum(indices, fs[0][1]) + # else: + # return fs[0][1] + # else: + # return IndexSum(expr.indices, body) + # else: + # return super(CancelCompoundVectorMapper, self).map_index_sum( + # expr, *args, **kwargs) + + def map_product(self, expr, sum_indices=None, *args, **kwargs): try: @@ -442,20 +465,34 @@ def map_product(self, expr, sum_indices=None, factored_summands=None, *args, if vec_i is None: raise ValueError # No CompoundVector - # Flatten the CompoundVector. - flattened = 0 - - r = {} - replacer = _IndexMapper(r) - for i in vec_i.as_range: - r[vec_i] = i - prod = 1 - for c in expr.children: - prod *= replacer(c) - flattened += prod - - factored_summands.append((vec_i, flattened)) - return 0.0 + # if vec_i in sum_indices: + # # Flatten the CompoundVector. + # flattened = 0 + + # r = {} + # replacer = _IndexMapper(r) + # for i in vec_i.as_range: + # r[vec_i] = i + # prod = 1 + # for c in expr.children: + # prod *= replacer(c) + # flattened += prod + + # factored_summands.append((vec_i, flattened)) + # return 0.0 + + elif len(vectors) == 1: + # Push the factors inside the CompoundVector in the + # hope that something further can be done. + vector = vectors[0] + bodies = list(vector.body) + for f in factors: + for b in range(len(bodies)): + bodies[b] *= f + + return CompoundVector(vector.index, vector.indices, tuple(bodies)) + else: + raise ValueError except ValueError: # Drop to here if this is not a cancellation opportunity for whatever reason. @@ -525,6 +562,7 @@ def flatten(index): new_indices.append(i) else: new_indices.append(index[0]) + # Do we need to also drop indices on the RHS of replaces? if new_indices: return IndexSum(new_indices, body) @@ -542,12 +580,15 @@ def map_delta(self, expr, replace=None, sum_indices=(), *args, **kwargs): indices = expr.indices # fix this so that the replacements happen from the bottom up. - if indices[1] in sum_indices: - replace[indices[1]] = indices[0] - indices = (indices[0], indices[0]) - elif indices[0] in sum_indices: - replace[indices[0]] = indices[1] - indices = (indices[1], indices[1]) + for i in sum_indices[::-1]: + if i == indices[1]: + replace[indices[1]] = indices[0] + indices = (indices[0], indices[0]) + break + elif i == indices[0]: + replace[indices[0]] = indices[1] + indices = (indices[1], indices[1]) + break # Only attempt new replacements if we are in transmitting node stacks. if sum_indices and indices[0] != indices[1]: @@ -613,7 +654,11 @@ def map_let(self, expr, replace=None, sum_indices=(), *args, **kwargs): def map_index(self, expr, replace=None, *args, **kwargs): - return replace[expr] if replace and expr in replace else expr + if hasattr(expr, "factors"): + return expr.__class__(*self.rec(expr.factors, *args, + replace=replace, **kwargs)) + else: + return replace[expr] if replace and expr in replace else expr class _DoNotFactorSet(set): From e48a6b5e390444059f1aeb0d81341b67a9d8551e Mon Sep 17 00:00:00 2001 From: David A Ham Date: Tue, 19 May 2015 17:38:56 +0100 Subject: [PATCH 206/749] Bug fix for delta cancellation. --- finat/mappers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/finat/mappers.py b/finat/mappers.py index f598c4c94..809aa1edf 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -528,7 +528,7 @@ class CancelDeltaMapper(IdentityMapper): """Mapper to cancel and/or replace indices according to the rules for Deltas.""" # Those nodes through which it is legal to transmit sum_indices. - _transmitting_nodes = (IndexSum, ForAll, Delta, Let) + _transmitting_nodes = (IndexSum, ForAll, Delta) def map_index_sum(self, expr, replace=None, sum_indices=(), *args, **kwargs): @@ -560,6 +560,8 @@ def flatten(index): for i in index[1:]: if i not in replace: new_indices.append(i) + else: + replace.pop(i) else: new_indices.append(index[0]) # Do we need to also drop indices on the RHS of replaces? From 2e857071ef8d398bee0f451d1daaad409cb0561f Mon Sep 17 00:00:00 2001 From: David A Ham Date: Wed, 20 May 2015 10:54:16 +0100 Subject: [PATCH 207/749] Correct handling of delta in sum factorisation. --- finat/mappers.py | 46 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/finat/mappers.py b/finat/mappers.py index 809aa1edf..472834121 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -758,6 +758,19 @@ def map_product(self, expr, index_groups=None, *args, **kwargs): return result + def map_delta(self, expr, index_groups=None, *args, **kwargs): + """Treat this as the delta times its body.""" + if index_groups is None: + return super(SumFactorMapper, self).map_delta(expr, *args, **kwargs) + else: + igroup = set() + body = self.rec(expr.body, *args, index_groups=igroup, **kwargs) + index_groups |= igroup + igroup = set() + indices = self.rec(expr.indices, *args, index_groups=igroup, **kwargs) + index_groups |= igroup + return expr.__class__(indices, body) + def map_sum(self, expr, index_groups=None, *args, **kwargs): """If the summands have the same factors, propagate them up. Otherwise (for the moment) put a _DoNotFactorSet in the output. @@ -786,7 +799,7 @@ class _Factors(object): def __init__(self, index, expr=None, indices=None): self.index = index self.factor = 1 - self.remainder = 1 + self.multiplicand = 1 if expr: self.insert(expr, indices) @@ -795,7 +808,7 @@ def insert(self, expr, indices): if self.index in indices: self.factor *= expr else: - self.remainder *= expr + self.multiplicand *= expr def __imul__(self, other): @@ -803,7 +816,7 @@ def __imul__(self, other): if other.index != self.index: raise ValueError("Can only multiply _Factors with the same index") self.factor *= other.factor - self.remainder *= other.remainder + self.multiplicand *= other.multiplicand else: self.insert(*other) return self @@ -818,7 +831,7 @@ def generate_factored_expression(self, kernel_data, indices): b = tuple(i for i in indices if isinstance(i, BasisFunctionIndexBase)) p = tuple(i for i in indices if isinstance(i, PointIndexBase)) return Let(((temp, Recipe((d, b, p), IndexSum((self.index,), self.factor))),), - IndexSum(tuple(indices), temp[d + b + p] * self.remainder)) + IndexSum(tuple(indices), temp[d + b + p] * self.multiplicand)) class _FactorSum(object): @@ -876,6 +889,31 @@ def map_index(self, expr, indices=None, *args, **kwargs): return expr + def map_delta(self, expr, indices=None, *args, **kwargs): + """Turn the delta back into a product and recurse on that.""" + + def deltafactor(f, indices): + if self.factor_index in indices: + f.factor = Delta(expr.indices, f.factor) + else: + f.multiplicand = Delta(expr.indices, f.multiplicand) + + i=set() + rc = self.rec(expr.body, *args, indices=i, **kwargs) + indices = flattened(expr.indices) + if isinstance(rc, _Factors): + deltafactor(rc, indices) + elif isinstance(rc, _FactorSum): + for f in rc.factors: + deltafactor(f, indices) + else: + f = _Factors(self.factor_index) + f *= (rc, i) + deltafactor(f, indices) + rc = f + + return rc + def map_product(self, expr, indices=None, *args, **kwargs): f = _Factors(self.factor_index) From 7e4a651cf96dd2a6edcd97057755950d79070579 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Wed, 20 May 2015 15:41:57 +0100 Subject: [PATCH 208/749] Distribution in delta factorisation. Apply distributive law to expose more delta cancellations. --- finat/mappers.py | 58 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/finat/mappers.py b/finat/mappers.py index 472834121..a5599f7ff 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -503,25 +503,71 @@ def map_product(self, expr, sum_indices=None, *args, class FactorDeltaMapper(IdentityMapper): """Mapper to pull deltas up the expression tree to maximise the opportunities for cancellation.""" - def map_product(self, expr, *args, **kwargs): + def map_index_sum(self, expr, deltas=None, *args, **kwargs): - children = (self.rec(child, *args, **kwargs) for child in expr.children) + d = [False] + body = self.rec(expr.body, *args, deltas=d, **kwargs) + + if isinstance(body, Sum) and d[0]: + body = tuple(IndexSum(expr.indices, b) for b in body.children) + return flattened_sum(body) + else: + return IndexSum(expr.indices, body) + + def map_product(self, expr, deltas=None, *args, **kwargs): + + if deltas is not None: + in_deltas = deltas + else: + in_deltas = [False] + + child_deltas = tuple([False] for i in expr.children) + children = (self.rec(child, *args, deltas=delta, **kwargs) + for child, delta in zip(expr.children, child_deltas)) factors = [] + sums = [] deltas = [] - for child in children: + for child, delta in zip(children, child_deltas): if isinstance(child, Delta): deltas.append(child) factors.append(child.body) + elif isinstance(child, Sum) and delta[0]: + sums.append(child) else: factors.append(child) - result = flattened_product(tuple(factors)) + result = (flattened_product(tuple(factors)),) + + for s in sums: + result = (r*t for r in result for t in s.children) + if sums: + # We need to pull the Deltas up the terms we have just processed. + result = (self.rec(r, *args, **kwargs) for r in result) + # If sums then there are deltas in the sums. + in_deltas[0] = True for delta in deltas: - result = Delta(delta.indices, result) + result = (Delta(delta.indices, r) for r in result) + in_deltas[0] = True + + result = tuple(result) + + if len(result) == 1: + return result[0] + else: + return flattened_sum(result) + + def map_sum(self, expr, deltas=None, *args, **kwargs): + + terms = tuple(self.rec(c, *args, **kwargs) for c in expr.children) + + if deltas is not None: + for term in terms: + if isinstance(term, Delta): + deltas[0] = True - return result + return flattened_sum(terms) class CancelDeltaMapper(IdentityMapper): From 49573de3bc0c11ad288456d1155ee2793db65a75 Mon Sep 17 00:00:00 2001 From: David A Ham Date: Wed, 20 May 2015 16:09:32 +0100 Subject: [PATCH 209/749] pep8 --- finat/mappers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/finat/mappers.py b/finat/mappers.py index a5599f7ff..07c4fee33 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -540,7 +540,7 @@ def map_product(self, expr, deltas=None, *args, **kwargs): result = (flattened_product(tuple(factors)),) for s in sums: - result = (r*t for r in result for t in s.children) + result = (r * t for r in result for t in s.children) if sums: # We need to pull the Deltas up the terms we have just processed. result = (self.rec(r, *args, **kwargs) for r in result) @@ -944,7 +944,7 @@ def deltafactor(f, indices): else: f.multiplicand = Delta(expr.indices, f.multiplicand) - i=set() + i = set() rc = self.rec(expr.body, *args, indices=i, **kwargs) indices = flattened(expr.indices) if isinstance(rc, _Factors): From 4033736400552976f0b05f542391ee844880de40 Mon Sep 17 00:00:00 2001 From: David Ham Date: Thu, 25 Jun 2015 17:00:15 +0100 Subject: [PATCH 210/749] more bindings --- finat/ast.py | 17 +++++++++++++---- finat/mappers.py | 16 +++++++++++++--- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/finat/ast.py b/finat/ast.py index 4dd4d343e..139dbf3e0 100644 --- a/finat/ast.py +++ b/finat/ast.py @@ -181,17 +181,26 @@ def __getinitargs__(self): class ForAll(StringifyMixin, p._MultiChildExpression): """A symbolic expression to indicate that the body will actually be evaluated for all of the values of its free indices. This enables - index simplification to take place. + index simplification to take place. It may be possible for FInAT + to reason that it is not neccesary to iterate over some values of + the free indices. For this reason a list of index bindings may be + provided which bind the indices specified to the expressions + given. The indices so bound will not be iterated over. :param indices: a sequence of indices to bind. :param body: the expression to evaluate. - + :param bindings: a tuple of (index, expression) pairs for indices + which should take particular values rather than being iterated over. """ - def __init__(self, indices, body): + def __init__(self, indices, body, bindings=None): self.indices = indices self.body = body - self.children = (self.indices, self.body) + self.bindings = bindings + if bindings: + self.children = (self.indices, self.body, self.bindings) + else: + self.children = (self.indices, self.body) self._color = "blue" def __getinitargs__(self): diff --git a/finat/mappers.py b/finat/mappers.py index 07c4fee33..53a89f3e8 100644 --- a/finat/mappers.py +++ b/finat/mappers.py @@ -212,6 +212,14 @@ def map_index_sum(self, expr, *args, **kwargs): self.rec(expr.body, *args, **kwargs) self.post_visit(expr, *args, **kwargs) + def map_let(self, expr, *args, **kwargs): + if not self.visit(expr, *args, **kwargs): + return + for symbol, value in expr.bindings: + self.rec(symbol, *args, **kwargs) + self.rec(expr.body, *args, **kwargs) + self.post_visit(expr, *args, **kwargs) + map_delta = map_index_sum map_for_all = map_index_sum map_wave = map_index_sum @@ -234,12 +242,14 @@ def visit(self, expr, *args, **kwargs): def post_visit(self, expr, *args, **kwargs): # The frame contains any indices we directly saw: - expr._indices_below = tuple(self._index_stack.pop()) + indices_below = tuple(self._index_stack.pop()) if isinstance(expr, IndexBase): - expr._indices_below += expr + indices_below += flattened(expr) + + self._indices_below = indices_below - self._index_stack[-1].union(expr._indices_below) + self._index_stack[-1].union(indices_below) class GraphvizMapper(WalkMapper, GVM): From 5099fbf038e7c88bbbe4f1165341f991cc5e606c Mon Sep 17 00:00:00 2001 From: David Ham Date: Thu, 7 Apr 2016 15:55:22 +0100 Subject: [PATCH 211/749] Some fixes --- finat/product_elements.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/finat/product_elements.py b/finat/product_elements.py index bed5c7383..6978da0be 100644 --- a/finat/product_elements.py +++ b/finat/product_elements.py @@ -3,7 +3,7 @@ from .indices import TensorPointIndex, TensorBasisFunctionIndex, DimensionIndex from .derivatives import grad from .ast import Recipe, CompoundVector, IndexSum -from FIAT.reference_element import two_product_cell +from FIAT.reference_element import TensorProductCell class ProductElement(object): @@ -21,7 +21,7 @@ def __init__(self, *args): self._degree = max([a._degree for a in args]) - cellprod = lambda cells: two_product_cell(cells[0], cells[1] if len(cells) < 3 + cellprod = lambda cells: TensorProductCell(cells[0], cells[1] if len(cells) < 3 else cellprod(cells[1:])) self._cell = cellprod([a.cell for a in args]) From 642b51ddfa600060bc10d1d827ba34ef7352d38c Mon Sep 17 00:00:00 2001 From: David Ham Date: Thu, 7 Apr 2016 16:14:03 +0100 Subject: [PATCH 212/749] purge tests and fix pep8 --- finat/product_elements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/product_elements.py b/finat/product_elements.py index 6978da0be..726af71e1 100644 --- a/finat/product_elements.py +++ b/finat/product_elements.py @@ -22,7 +22,7 @@ def __init__(self, *args): self._degree = max([a._degree for a in args]) cellprod = lambda cells: TensorProductCell(cells[0], cells[1] if len(cells) < 3 - else cellprod(cells[1:])) + else cellprod(cells[1:])) self._cell = cellprod([a.cell for a in args]) From 569ab0c9d1023ceb49c3c955ec9db8ccdb2c2527 Mon Sep 17 00:00:00 2001 From: David Ham Date: Thu, 7 Apr 2016 18:12:11 +0100 Subject: [PATCH 213/749] great cull v1 --- finat/ast.py | 365 ------ finat/coffee_compiler.py | 244 ---- .../{scalar_elements.py => fiat_elements.py} | 0 finat/finiteelementbase.py | 134 +-- finat/geometry_mapper.py | 97 -- finat/hcurl.py | 83 -- finat/hdiv.py | 79 +- finat/interpreter.py | 307 ----- finat/mappers.py | 1000 ----------------- finat/pyop2_interface.py | 52 - finat/utils.py | 201 ---- 11 files changed, 9 insertions(+), 2553 deletions(-) delete mode 100644 finat/ast.py delete mode 100644 finat/coffee_compiler.py rename finat/{scalar_elements.py => fiat_elements.py} (100%) delete mode 100644 finat/geometry_mapper.py delete mode 100644 finat/hcurl.py delete mode 100644 finat/interpreter.py delete mode 100644 finat/mappers.py delete mode 100644 finat/pyop2_interface.py delete mode 100644 finat/utils.py diff --git a/finat/ast.py b/finat/ast.py deleted file mode 100644 index 139dbf3e0..000000000 --- a/finat/ast.py +++ /dev/null @@ -1,365 +0,0 @@ -"""This module defines the additional Pymbolic nodes which are -required to define Finite Element expressions in FInAT. -""" -import pymbolic.primitives as p -try: - from termcolor import colored -except ImportError: - def colored(string, color, attrs=[]): - return string - -__all__ = ["Variable", "Array", "Recipe", "IndexSum", "LeviCivita", - "ForAll", "Wave", "Let", "Delta", "Inverse", "Det", "Abs", - "CompoundVector"] - - -class FInATSyntaxError(Exception): - """Exception to raise when users break the rules of the FInAT ast.""" - - -class StringifyMixin(object): - """Mixin class to set stringification options correctly for pymbolic subclasses.""" - - def __str__(self): - """Use the :meth:`stringifier` to return a human-readable - string representation of *self*. - """ - - from pymbolic.mapper.stringifier import PREC_NONE - return self.stringifier()()(self, PREC_NONE, indent=0) - - def stringifier(self): - from . import mappers - return mappers._StringifyMapper - - @property - def name(self): - if hasattr(self, "_error"): - return colored(str(self.__class__.__name__), "red", attrs=["bold"]) - else: - return colored(str(self.__class__.__name__), self._color) - - def set_error(self): - self._error = True - - -class Variable(p.Variable): - """A symbolic variable.""" - def __init__(self, name): - super(Variable, self).__init__(name) - - self._color = "cyan" - - def set_error(self): - self._error = True - - -class Array(Variable): - """A symbolic variable of known extent.""" - def __init__(self, name, shape): - super(Array, self).__init__(name) - - self.shape = shape - - -class Recipe(StringifyMixin, p.Expression): - """AST snippets and data corresponding to some form of finite element - evaluation. - - A :class:`Recipe` associates an ordered set of indices with an - expression. - - :param indices: A 3-tuple containing the ordered free indices in the - expression. The first entry is a tuple of :class:`DimensionIndex`, - the second is a tuple of :class:`BasisFunctionIndex`, and - the third is a tuple of :class:`PointIndex`. - Any of the tuples may be empty. - :param body: The expression returned by this :class:`Recipe`. - """ - def __init__(self, indices, body, _transpose=None): - try: - assert len(indices) == 3 - except: - raise FInATSyntaxError("Indices must be a triple of tuples") - self.indices = tuple(indices) - self.body = body - self._transpose = _transpose - self._color = "blue" - - mapper_method = "map_recipe" - - def __getinitargs__(self): - return self.indices, self.body - - def __getitem__(self, index): - - replacements = {} - - try: - for i in range(len(index)): - if index[i] == slice(None): - # Don't touch colon indices. - pass - else: - replacements[self.indices[i]] = index[i] - except TypeError: - # Index wasn't iterable. - replacements[self.indices[0]] = index - - return self.replace_indices(replacements) - - def replace_indices(self, replacements): - """Return a copy of this :class:`Recipe` with some of the indices - substituted.""" - - if not isinstance(replacements, dict): - replacements = {a: b for (a, b) in replacements} - - from mappers import _IndexMapper - return _IndexMapper(replacements)(self) - - -class IndexSum(StringifyMixin, p._MultiChildExpression): - """A symbolic expression for a sum over one or more indices. - - :param indices: a sequence of indices over which to sum. - :param body: the expression to sum. - """ - def __init__(self, indices, body): - - # Inline import to avoid circular dependency. - from indices import IndexBase - if isinstance(indices, IndexBase): - indices = (indices,) - else: - indices = tuple(indices) - - # Perform trivial simplification of repeated indexsum. - if isinstance(body, IndexSum): - indices += body.children[0] - body = body.children[1] - - self.children = (indices, body) - - self.indices = self.children[0] - self.body = self.children[1] - self._color = "blue" - - def __getinitargs__(self): - return self.children - - mapper_method = "map_index_sum" - - -class LeviCivita(StringifyMixin, p._MultiChildExpression): - r"""The Levi-Civita symbol expressed as an operator. - - :param free: A tuple of free indices. - :param bound: A tuple of indices over which to sum. - :param body: The summand. - - The length of free + bound must be exactly 3. The Levi-Civita - operator then represents the summation over the bound indices of - the Levi-Civita symbol times the body. For example in the case of - two bound indices: - - .. math:: - \mathrm{LeviCivita((\alpha,), (\beta, \gamma), body)} = \sum_{\beta,\gamma}\epsilon_{\alpha,\beta,\gamma} \mathrm{body} - - """ - def __init__(self, free, bound, body): - - self.children = (free, bound, body) - self._color = "blue" - - def __getinitargs__(self): - return self.children - - mapper_method = "map_index_sum" - - -class ForAll(StringifyMixin, p._MultiChildExpression): - """A symbolic expression to indicate that the body will actually be - evaluated for all of the values of its free indices. This enables - index simplification to take place. It may be possible for FInAT - to reason that it is not neccesary to iterate over some values of - the free indices. For this reason a list of index bindings may be - provided which bind the indices specified to the expressions - given. The indices so bound will not be iterated over. - - :param indices: a sequence of indices to bind. - :param body: the expression to evaluate. - :param bindings: a tuple of (index, expression) pairs for indices - which should take particular values rather than being iterated over. - """ - def __init__(self, indices, body, bindings=None): - - self.indices = indices - self.body = body - self.bindings = bindings - if bindings: - self.children = (self.indices, self.body, self.bindings) - else: - self.children = (self.indices, self.body) - self._color = "blue" - - def __getinitargs__(self): - return self.children - - mapper_method = "map_for_all" - - -class Wave(StringifyMixin, p._MultiChildExpression): - """A symbolic expression with loop-carried dependencies.""" - - def __init__(self, var, index, base, update, body): - self.children = (var, index, base, update, body) - self._color = "blue" - - def __getinitargs__(self): - return self.children - - mapper_method = "map_wave" - - -class Let(StringifyMixin, p._MultiChildExpression): - """A Let expression enables local variable bindings in an -expression. This feature is lifted more or less directly from -Scheme. - -:param bindings: A tuple of pairs. The first entry in each pair is a - :class:`pymbolic.Variable` to be defined as the second entry, which - must be an expression. -:param body: The expression making up the body of the expression. The - value of the Let expression is the value of this expression. - - """ - - def __init__(self, bindings, body): - try: - for b in bindings: - assert len(b) == 2 - except: - raise FInATSyntaxError("Let bindings must be a tuple of pairs") - - super(Let, self).__init__((bindings, body)) - - self.bindings, self.body = self.children - - self._color = "blue" - - mapper_method = "map_let" - - -class Delta(StringifyMixin, p._MultiChildExpression): - """The Kronecker delta expressed as a ternary operator: - -.. math:: - - \mathrm{Delta((i, j), body)} = \delta_{ij}*\mathrm{body}. - -:param indices: a sequence of indices. -:param body: an expression. - -The body expression will be returned if the values of the indices -match. Otherwise 0 will be returned. - - """ - def __init__(self, indices, body): - if len(indices) != 2: - raise FInATSyntaxError( - "Delta statement requires exactly two indices") - - super(Delta, self).__init__((indices, body)) - self.indices = indices - self.body = body - self._color = "blue" - - def __getinitargs__(self): - return self.children - - def __str__(self): - return "Delta(%s, %s)" % self.children - - mapper_method = "map_delta" - - -class Inverse(StringifyMixin, p.Expression): - """The inverse of a matrix-valued expression. Where the expression is - not square, this is the Moore-Penrose pseudo-inverse. - - Where the expression is evaluated at a number of points, the - inverse will be evaluated pointwise. - """ - def __init__(self, expression): - self.expression = expression - self.children = [expression] - self._color = "blue" - - super(Inverse, self).__init__() - - def __getinitargs__(self): - return (self.expression,) - - mapper_method = "map_inverse" - - -class Det(StringifyMixin, p.Expression): - """The determinant of a matrix-valued expression.""" - def __init__(self, expression): - - self.expression = expression - self._color = "blue" - - super(Det, self).__init__() - - def __getinitargs__(self): - return (self.expression,) - - mapper_method = "map_det" - - -class Abs(StringifyMixin, p.Expression): - """The absolute value of an expression.""" - def __init__(self, expression): - - self.expression = expression - self._color = "blue" - - super(Abs, self).__init__() - - def __getinitargs__(self): - return (self.expression,) - - mapper_method = "map_abs" - - -class CompoundVector(StringifyMixin, p._MultiChildExpression): - """A vector expression composed by concatenating other expressions.""" - def __init__(self, index, indices, expressions): - """ - - :param index: The free :class:`~.DimensionIndex` created by - the :class:`CompoundVector` - :param indices: The sequence of dimension indices of the - expressions. For scalar components these should be ``None``. - :param expressions: The sequence of expressions making up - the compound. - - Each value that `index` takes will be mapped to the corresponding - value in indices and the matching expression will be evaluated. - """ - if len(indices) != len(expressions): - raise FInATSyntaxError("The indices and expressions must be of equal length") - - if sum([i.length for i in indices]) != index.length: - raise FInATSyntaxError("The length of the compound index must equal " - "the sum of the lengths of the components.") - - super(CompoundVector, self).__init__((index, indices, expressions)) - - self.index, self.indices, self.body = self.children - - self._color = "blue" - - mapper_method = "map_compound_vector" diff --git a/finat/coffee_compiler.py b/finat/coffee_compiler.py deleted file mode 100644 index 7a9a52bb7..000000000 --- a/finat/coffee_compiler.py +++ /dev/null @@ -1,244 +0,0 @@ -"""A testing utility that compiles and executes COFFEE ASTs to -evaluate a given recipe. Provides the same interface as FInAT's -internal interpreter. """ - -from pymbolic.mapper import CombineMapper -from greek_alphabet import translate_symbol -import coffee.base as coffee -import os -import subprocess -import ctypes -import numpy as np -from .utils import Kernel, KernelData -from .ast import Recipe, IndexSum, Array, Inverse -from .mappers import BindingMapper, IndexSumMapper -from pprint import pformat -from collections import deque - - -determinant = {1: lambda e: coffee.Determinant1x1(e), - 2: lambda e: coffee.Determinant2x2(e), - 3: lambda e: coffee.Determinant3x3(e)} - - -class CoffeeMapper(CombineMapper): - """A mapper that generates Coffee ASTs for FInAT expressions""" - - def __init__(self, kernel_data, varname="A", increment=False): - """ - :arg context: a mapping from variable names to values - :arg varname: name of the implied outer variable - :arg increment: flag indicating that the kernel should - increment result values instead of assigning them - """ - super(CoffeeMapper, self).__init__() - self.kernel_data = kernel_data - self.scope_var = deque() - self.scope_ast = deque() - if increment: - self.scope_var.append((varname, coffee.Incr)) - else: - self.scope_var.append((varname, coffee.Assign)) - - def _push_scope(self): - self.scope_ast.append([]) - - def _pop_scope(self): - return self.scope_ast.pop() - - def _create_loop(self, index, body): - itvar = self.rec(index) - extent = index.extent - init = coffee.Decl("int", itvar, extent.start or 0) - cond = coffee.Less(itvar, extent.stop) - incr = coffee.Incr(itvar, extent.step or 1) - return coffee.For(init, cond, incr, coffee.Block(body, open_scope=True)) - - def combine(self, values): - return list(values) - - def map_recipe(self, expr): - return self.rec(expr.body) - - def map_variable(self, expr): - return coffee.Symbol(translate_symbol(expr.name)) - - map_index = map_variable - - def map_constant(self, expr): - return expr.real - - def map_subscript(self, expr): - name = translate_symbol(expr.aggregate.name) - if isinstance(expr.index, tuple): - indices = self.rec(expr.index) - else: - indices = (self.rec(expr.index),) - return coffee.Symbol(name, rank=indices) - - def map_index_sum(self, expr): - return self.rec(expr.body) - - def map_product(self, expr): - prod = self.rec(expr.children[0]) - for factor in expr.children[1:]: - prod = coffee.Prod(prod, self.rec(factor)) - return prod - - def map_inverse(self, expr): - e = expr.expression - return coffee.Invert(self.rec(e), e.shape[0]) - - def map_det(self, expr): - e = expr.expression - return determinant[e.shape[0]](self.rec(e)) - - def map_abs(self, expr): - return coffee.FunCall("fabs", self.rec(expr.expression)) - - def map_for_all(self, expr): - name, stmt = self.scope_var[-1] - var = coffee.Symbol(name, self.rec(expr.indices)) - self._push_scope() - body = self.rec(expr.body) - scope = self._pop_scope() - body = scope + [stmt(var, body)] - for idx in expr.indices: - body = [self._create_loop(idx, body)] - return coffee.Block(body) - - def map_let(self, expr): - for v, e in expr.bindings: - shape = v.shape if isinstance(v, Array) else () - var = coffee.Symbol(self.rec(v), rank=shape) - self.scope_var.append((v, coffee.Assign)) - - self._push_scope() - body = self.rec(e) - scope = self._pop_scope() - - if isinstance(e, IndexSum): - # Recurse on expression in a new scope - lbody = scope + [coffee.Incr(var, body)] - - # Construct IndexSum loop and add to current scope - self.scope_ast[-1].append(coffee.Decl("double", var, init="0.")) - self.scope_ast[-1].append(self._create_loop(e.indices[0], lbody)) - - elif isinstance(e, Inverse): - # Coffee currently inverts matrices in-place - # so we need to memcpy the source matrix first - mcpy = coffee.FlatBlock("memcpy(%s, %s, %d*sizeof(double));\n" % - (v, e.expression, shape[0] * shape[1])) - e.expression = v - self.scope_ast[-1].append(coffee.Decl("double", var)) - self.scope_ast[-1].append(mcpy) - self.scope_ast[-1].append(self.rec(e)) - - elif isinstance(body, coffee.Expr): - self.scope_ast[-1].append(coffee.Decl("double", var, init=body)) - else: - self.scope_ast[-1].append(coffee.Decl("double", var)) - self.scope_ast[-1].append(body) - - self.scope_var.pop() - return self.rec(expr.body) - - -class CoffeeKernel(Kernel): - - def __init__(self, recipe, kernel_data): - super(CoffeeKernel, self).__init__(recipe, kernel_data) - - # Apply mapper to bind all IndexSums to temporaries - self.recipe = IndexSumMapper(self.kernel_data)(self.recipe) - - # Apply pre-processing mapper to bind free indices - self.recipe = BindingMapper(self.kernel_data)(self.recipe) - - def generate_ast(self, kernel_args=None, varname="A", increment=False): - if kernel_args is None: - kernel_args = self.kernel_data.kernel_args - args_ast = [] - body_ast = [] - - mapper = CoffeeMapper(self.kernel_data, varname=varname, - increment=increment) - - # Add argument declarations - for var in kernel_args: - if isinstance(var, Array): - var_ast = coffee.Symbol(var.name, var.shape) - else: - var_ast = coffee.Symbol("**" + var.name) - args_ast.append(coffee.Decl("double", var_ast)) - - # Write AST to initialise static kernel data - for data in self.kernel_data.static.values(): - values = data[1]() - val_str = pformat(values.tolist()) - val_str = val_str.replace('[', '{').replace(']', '}') - val_init = coffee.ArrayInit(val_str) - var = coffee.Symbol(mapper(data[0]), values.shape) - body_ast.append(coffee.Decl("double", var, init=val_init)) - - # Convert the kernel recipe into an AST - body_ast.append(mapper(self.recipe)) - - return coffee.FunDecl("void", "finat_kernel", args_ast, - coffee.Block(body_ast), - headers=["math.h", "string.h"]) - - -def evaluate(expression, context={}, kernel_data=None): - index_shape = () - args_data = [] - - if not kernel_data: - kernel_data = KernelData() - - # Pack free indices as kernel arguments - for index in expression.indices: - for i in index: - index_shape += (i.extent.stop, ) - index_data = np.empty(index_shape, dtype=np.double) - args_data.append(index_data.ctypes.data) - kernel_data.kernel_args = [Array("A", shape=index_shape)] - - # Pack context arguments - for var, value in context.iteritems(): - kernel_data.kernel_args.append(Array(var, shape=value.shape)) - args_data.append(value.ctypes.data) - - # Generate kernel function - kernel = CoffeeKernel(expression, kernel_data).generate_ast() - - basename = os.path.join(os.getcwd(), "finat_kernel") - with file(basename + ".c", "w") as f: - f.write(str(kernel)) - - # Compile kernel function into .so - cc = [os.environ['CC'] if "CC" in os.environ else 'gcc'] - cc += ['-Wall', '-O0', '-g', '-fPIC', '-shared', '-std=c99'] - cc += ["-o", "%s.so" % basename, "%s.c" % basename] - cc += ['-llapack', '-lblas'] - try: - subprocess.check_call(cc) - except subprocess.CalledProcessError as e: - print "Compilation error: ", e - raise Exception("Failed to compile %s.c" % basename) - - # Load compiled .so - try: - kernel_lib = ctypes.cdll.LoadLibrary(basename + ".so") - except OSError as e: - print "Library load error: ", e - raise Exception("Failed to load %s.so" % basename) - - # Invoke compiled kernel with packed arguments - kernel_lib.finat_kernel(*args_data) - - # Close compiled kernel library - ctypes.cdll.LoadLibrary('libdl.so').dlclose(kernel_lib._handle) - - return index_data diff --git a/finat/scalar_elements.py b/finat/fiat_elements.py similarity index 100% rename from finat/scalar_elements.py rename to finat/fiat_elements.py diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 32ee5cfd8..ee21d4eec 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -1,6 +1,4 @@ import numpy as np -from .ast import Recipe, IndexSum, Variable, Abs -from .indices import TensorPointIndex class UndefinedError(Exception): @@ -46,13 +44,6 @@ def entity_closure_dofs(self): raise NotImplementedError - @property - def facet_support_dofs(self): - '''Return the map of facet id to the degrees of freedom for which the - corresponding basis functions take non-zero values.''' - - raise NotImplementedError - @property def dofs_shape(self): '''Return a tuple indicating the number of degrees of freedom in the @@ -62,54 +53,14 @@ def dofs_shape(self): raise NotImplementedError - def field_evaluation(self, field_var, q, kernel_data, derivative=None, + def basis_evaluation(self, index, q, q_index, derivative=None, pullback=None): - '''Return code for evaluating a known field at known points on the + '''Return code for evaluating the element at known points on the reference element. - :param field_var: the coefficient of the field at each basis function - :param q: a :class:`.PointIndex` corresponding to the points - at which to evaluate. - :param kernel_data: the :class:`.KernelData` object corresponding - to the current kernel. - :param derivative: the derivative to take of the test function. - :param pullback: whether to pull back to the reference cell. - ''' - - raise NotImplementedError - - def basis_evaluation(self, q, kernel_data, derivative=None, - pullback=None): - '''Return code for evaluating a known field at known points on the - reference element. - - :param field_var: the coefficient of the field at each basis function - :param q: a :class:`.PointIndex` corresponding to the points - at which to evaluate. - :param kernel_data: the :class:`.KernelData` object corresponding - to the current kernel. - :param derivative: the derivative to take of the test function. - :param pullback: whether to pull back to the reference cell. - ''' - - raise NotImplementedError - - def moment_evaluation(self, value, weights, q, kernel_data, - derivative=None, pullback=None): - '''Return code for evaluating: - - .. math:: - - \int \mathrm{value} : v\, \mathrm{d}x - - where :math:`v` is a test function or the derivative of a test - function, and : is the inner product operator. - - :param value: an expression. The free indices in value must match those in v. - :param weights: a point set of quadrature weights. - :param q: a :class:`.PointIndex` corresponding to the points - at which to evaluate. - :param kernel_data: the :class:`.KernelData` object corresponding to the current kernel. + :param index: the basis function index. + :param q: the quadrature rule. + :param q_index: the quadrature index. :param derivative: the derivative to take of the test function. :param pullback: whether to pull back to the reference cell. ''' @@ -136,54 +87,7 @@ def __eq__(self, other): self._degree == other._degree -class ScalarElementMixin(object): - """Mixin class containing field evaluation and moment rules for scalar - valued elements.""" - def field_evaluation(self, field_var, q, - kernel_data, derivative=None, pullback=True): - - kernel_data.kernel_args.add(field_var) - - basis = self.basis_evaluation(q, kernel_data, derivative, pullback) - (d, b, p) = basis.indices - phi = basis.body - - expr = IndexSum(b, field_var[b[0]] * phi) - - return Recipe((d, (), p), expr) - - def moment_evaluation(self, value, weights, q, - kernel_data, derivative=None, pullback=True): - - basis = self.basis_evaluation(q, kernel_data, derivative, pullback) - (d, b, p) = basis.indices - phi = basis.body - - (d_, b_, p_) = value.indices - psi = value.replace_indices(zip(d_ + p_, d + p)).body - - if isinstance(p[0], TensorPointIndex): - ws = [w_.kernel_variable("w", kernel_data)[p__] - for w_, p__ in zip(weights, p[0].factors)] - w = reduce(lambda a, b: a * b, ws) - else: - w = weights.kernel_variable("w", kernel_data)[p] - - expr = psi * phi - - if d: - expr = IndexSum(d, expr) - - # The quadrature weights go outside any indexsum! - expr *= w - - if pullback: - expr *= Abs(kernel_data.detJ) - - return Recipe(((), b + b_, ()), IndexSum(p, expr)) - - -class FiatElementBase(ScalarElementMixin, FiniteElementBase): +class FiatElementBase(FiniteElementBase): """Base class for finite elements for which the tabulation is provided by FIAT.""" def __init__(self, cell, degree): @@ -215,29 +119,3 @@ def facet_support_dofs(self): corresponding basis functions take non-zero values.''' return self._fiat_element.entity_support_dofs() - - def _tabulate(self, points, derivative): - - if derivative: - tab = self._fiat_element.tabulate(1, points.points) - - ind = np.eye(points.points.shape[1], dtype=int) - - return np.array([tab[tuple(i)] for i in ind]) - else: - return self._fiat_element.tabulate(0, points.points)[ - tuple([0] * points.points.shape[1])] - - def _tabulated_basis(self, points, kernel_data, derivative): - - static_key = (id(self), id(points), id(derivative)) - - if static_key in kernel_data.static: - phi = kernel_data.static[static_key][0] - else: - phi = Variable(("d" if derivative else "") + - kernel_data.tabulation_variable_name(self, points)) - data = self._tabulate(points, derivative) - kernel_data.static[static_key] = (phi, lambda: data) - - return phi diff --git a/finat/geometry_mapper.py b/finat/geometry_mapper.py deleted file mode 100644 index 119da3269..000000000 --- a/finat/geometry_mapper.py +++ /dev/null @@ -1,97 +0,0 @@ -from .points import PointSet -from .indices import PointIndex, PointIndexBase -from .ast import Let, Det, Inverse, Recipe -from .mappers import IdentityMapper -from .derivatives import grad - - -class GeometryMapper(IdentityMapper): - """A mapper which identifies Jacobians, inverse Jacobians and their - determinants in expressions, and inserts the code to define them at - the correct point in the tree.""" - - def __init__(self, kernel_data): - """ - :arg context: a mapping from variable names to values - """ - super(GeometryMapper, self).__init__() - - self.local_geometry = set() - - self.kernel_data = kernel_data - - def __call__(self, expr, *args, **kwargs): - """ - In the affine case we need to do geometry insertion at the top - level. - """ - - if not isinstance(expr, Recipe): - raise TypeError("Can only map geometry on a Recipe") - - body = self.rec(expr.body) - - if self.kernel_data.affine and self.local_geometry: - # This is the bottom corner. We actually want the - # circumcentre. - q = PointIndex(PointSet([[0] * self.kernel_data.tdim])) - - body = self._bind_geometry(q, body) - - elif self.local_geometry: - for s in self.local_geometry: - s.set_error() - print expr - raise ValueError("Unbound local geometry in tree") - - # Reconstruct the recipe - return expr.__class__(self.rec(expr.indices), - body) - - def map_variable(self, var): - - if var.name in ("J", "invJ", "detJ"): - self.local_geometry.add(var) - - return var - - def map_index_sum(self, expr): - - body = self.rec(expr.body) - - if not self.kernel_data.affine \ - and self.local_geometry \ - and isinstance(expr.indices[-1], PointIndexBase): - q = expr.indices[-1] - - body = self._bind_geometry(q, body) - - # Reconstruct the index_sum - return expr.__class__(self.rec(expr.indices), - body) - - def _bind_geometry(self, q, body): - - kd = self.kernel_data - - # Note that this may not be safe for tensor product elements. - phi_x = kd.coordinate_var - element = kd.coordinate_element - J = element.field_evaluation(phi_x, q, kd, grad, pullback=False) - - d, b, q = J.indices - # In the affine case, there is only one point. In the - # non-affine case, binding the point index is the problem of - # kernel as a whole - if self.kernel_data.affine: - J = J.replace_indices(zip(q, (0,))) - J.indices = (d, b, ()) - - inner_lets = ((kd.detJ, Det(kd.J)),) if kd.detJ in self.local_geometry else () - inner_lets += ((kd.invJ, Inverse(kd.J)),) if kd.invJ in self.local_geometry else () - - # The local geometry goes out of scope at this point. - self.local_geometry = set() - - return Let(((kd.J, J),), - Let(inner_lets, body) if inner_lets else body) diff --git a/finat/hcurl.py b/finat/hcurl.py deleted file mode 100644 index 3a921ecfc..000000000 --- a/finat/hcurl.py +++ /dev/null @@ -1,83 +0,0 @@ -from finiteelementbase import FiatElementBase -from ast import Recipe, IndexSum, LeviCivita -import FIAT -import indices -from derivatives import div, grad, curl - - -class HCurlElement(FiatElementBase): - def __init__(self, cell, degree): - super(HCurlElement, self).__init__(cell, degree) - - def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): - - phi = self._tabulated_basis(q.points, kernel_data, derivative) - - i = indices.BasisFunctionIndex(self.fiat_element.space_dimension()) - - tIndex = lambda: indices.DimensionIndex(kernel_data.tdim) - gIndex = lambda: indices.DimensionIndex(kernel_data.gdim) - - alpha = tIndex() - - # The lambda functions here prevent spurious instantiations of invJ and detJ - invJ = lambda: kernel_data.invJ - detJ = lambda: kernel_data.detJ - - if derivative is None: - if pullback: - beta = alpha - alpha = gIndex() - expr = IndexSum((beta,), invJ()[beta, alpha] * phi[(beta, i, q)]) - else: - expr = phi[(alpha, i, q)] - ind = ((alpha,), (i,), (q,)) - elif derivative is div: - if pullback: - beta = tIndex() - gamma = gIndex() - expr = IndexSum((gamma,), invJ()[alpha, gamma] * invJ()[beta, gamma] * - phi[(alpha, beta, i, q)]) - else: - expr = IndexSum((alpha,), phi[(alpha, alpha, i, q)]) - ind = ((), (i,), (q,)) - elif derivative is grad: - if pullback: - beta = tIndex() - gamma = gIndex() - delta = gIndex() - expr = IndexSum((alpha, beta), invJ()[alpha, gamma] * invJ()[beta, delta] * - phi[(alpha, beta, i, q)]) - ind = ((gamma, delta), (i,), (q,)) - else: - beta = tIndex() - expr = phi[(alpha, beta, i, q)] - ind = ((alpha, beta), (i,), (q,)) - elif derivative is curl: - beta = tIndex() - d = kernel_data.tdim - zeta = tIndex() - if pullback: - if d == 3: - gamma = tIndex() - expr = IndexSum((gamma,), kernel_data.J(zeta, gamma) * - LeviCivita((gamma,), (alpha, beta), phi[(alpha, beta, i, q)])) / \ - detJ() - elif d == 2: - expr = LeviCivita((2,), (alpha, beta), phi[(alpha, beta, i, q)]) / \ - detJ() - else: - if d == 3: - expr = LeviCivita((zeta,), (alpha, beta), phi[(alpha, beta, i, q)]) - if d == 2: - expr = LeviCivita((2,), (alpha, beta), phi[(alpha, beta, i, q)]) - if d == 2: - ind = ((), (i,), (q,)) - elif d == 3: - ind = ((zeta,), (i,), (q,)) - else: - raise NotImplementedError - else: - raise NotImplementedError - - return Recipe(ind, expr) diff --git a/finat/hdiv.py b/finat/hdiv.py index 8c32a8e29..5d47b9625 100644 --- a/finat/hdiv.py +++ b/finat/hdiv.py @@ -1,82 +1,9 @@ -from finiteelementbase import FiatElementBase -from ast import Recipe, IndexSum, LeviCivita +from .fiat_elements import FiatElement import FIAT -import indices -from derivatives import div, grad, curl -class HDivElement(FiatElementBase): - def __init__(self, cell, degree): - super(HDivElement, self).__init__(cell, degree) - - def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): - - phi = self._tabulated_basis(q.points, kernel_data, derivative) - - i = indices.BasisFunctionIndex(self._fiat_element.space_dimension()) - - tIndex = lambda: indices.DimensionIndex(kernel_data.tdim) - gIndex = lambda: indices.DimensionIndex(kernel_data.gdim) - - alpha = tIndex() - - # The lambda functions here prevent spurious instantiations of invJ and detJ - J = lambda: kernel_data.J - invJ = lambda: kernel_data.invJ - detJ = lambda: kernel_data.detJ - - if derivative is None: - if pullback: - beta = alpha - alpha = gIndex() - expr = IndexSum((beta,), J()[alpha, beta] * phi[beta, i, q] / - detJ()) - else: - expr = phi[alpha, i, q] - ind = ((alpha,), (i,), (q,)) - elif derivative is div: - if pullback: - expr = IndexSum((alpha,), phi[alpha, alpha, i, q] / detJ()) - else: - expr = IndexSum((alpha,), phi[alpha, alpha, i, q]) - ind = ((), (i,), (q,)) - elif derivative is grad: - if pullback: - beta = tIndex() - gamma = gIndex() - delta = gIndex() - expr = IndexSum((alpha, beta), J()[gamma, alpha] * invJ()[beta, delta] * - phi[alpha, beta, i, q]) / detJ() - ind = ((gamma, delta), (i,), (q,)) - else: - beta = indices.DimensionIndex(kernel_data.tdim) - expr = phi[alpha, beta, i, q] - ind = ((alpha, beta), (i,), (q,)) - elif derivative is curl: - beta = indices.DimensionIndex(kernel_data.tdim) - if pullback: - d = kernel_data.gdim - gamma = gIndex() - delta = gIndex() - zeta = gIndex() - expr = LeviCivita((zeta,), (gamma, delta), - IndexSum((alpha, beta), J()[gamma, alpha] * invJ()[beta, delta] * - phi[alpha, beta, i, q])) / detJ() - else: - d = kernel_data.tdim - zeta = tIndex() - expr = LeviCivita((zeta,), (alpha, beta), phi[alpha, beta, i, q]) - if d == 2: - expr = expr.replace_indices((zeta, 2)) - ind = ((), (i,), (q,)) - elif d == 3: - ind = ((zeta,), (i,), (q,)) - else: - raise NotImplementedError - else: - raise NotImplementedError - - return Recipe(ind, expr) +class HDivElement(FiatElement): + pass class RaviartThomas(HDivElement): diff --git a/finat/interpreter.py b/finat/interpreter.py deleted file mode 100644 index 8210512d7..000000000 --- a/finat/interpreter.py +++ /dev/null @@ -1,307 +0,0 @@ -"""An interpreter for FInAT recipes. This is not expected to be -performant, but rather to provide a test facility for FInAT code.""" -import pymbolic.primitives as p -from pymbolic.mapper.evaluator import FloatEvaluationMapper, UnknownVariableError -from .mappers import BindingMapper -from ast import IndexSum, ForAll, LeviCivita, FInATSyntaxError -from indices import TensorPointIndex -import numpy as np -import copy - - -class FinatEvaluationMapper(FloatEvaluationMapper): - - def __init__(self, context={}): - """ - :arg context: a mapping from variable names to values - """ - super(FinatEvaluationMapper, self).__init__(context) - - self.indices = {} - - # Storage for wave variables while they are out of scope. - self.wave_vars = {} - - def _as_range(self, e): - """Convert a slice to a range. If the range has expressions as bounds, - evaluate them. - """ - - return range(int(self.rec(e.start or 0)), - int(self.rec(e.stop)), - int(self.rec(e.step or 1))) - - def map_variable(self, expr): - try: - var = self.context[expr.name] - if isinstance(var, p.Expression): - return self.rec(var) - else: - return var - except KeyError: - expr.set_error() - raise UnknownVariableError(expr.name) - - def map_recipe(self, expr): - """Evaluate expr for all values of free indices""" - - if expr._transpose: - return self.rec(expr.body).transpose(expr._transpose) - else: - return self.rec(expr.body) - - def map_index_sum(self, expr): - - indices, body = expr.children - expr_in = expr - - # Sum over multiple indices recursively. - if len(indices) > 1: - expr = IndexSum(indices[1:], body) - else: - expr = body - - idx = indices[0] - - if idx in self.indices: - expr_in.set_error() - idx.set_error() - raise FInATSyntaxError("Attempting to bind the name %s which is already bound" % idx) - - e = idx.extent - - total = 0.0 - - self.indices[idx] = None - - # Clear the set of variables dependent on this index. - self.wave_vars[idx] = {} - - for i in self._as_range(e): - self.indices[idx] = i - try: - total += self.rec(expr) - except: - if hasattr(expr, "_error"): - expr_in.set_error() - raise - - self.indices.pop(idx) - - return total - - def map_index(self, expr): - - try: - return self.indices[expr] - except KeyError: - expr.set_error() - raise FInATSyntaxError("Access to unbound variable name %s." % expr) - - def map_for_all(self, expr): - - indices, body = expr.children - expr_in = expr - - # Deal gracefully with the zero index case. - if not indices: - return self.rec(body) - - # Execute over multiple indices recursively. - if len(indices) > 1: - expr = ForAll(indices[1:], body) - # Expand tensor indices - elif isinstance(indices[0], TensorPointIndex): - indices = indices[0].factors - expr = ForAll(indices[1:], body) - else: - expr = body - - idx = indices[0] - - if idx in self.indices: - expr_in.set_error() - idx.set_error() - raise FInATSyntaxError( - "Attempting to bind the name %s which is already bound" % idx) - - e = idx.extent - - total = [] - # Clear the set of variables dependent on this index. - self.wave_vars[idx] = {} - for i in self._as_range(e): - self.indices[idx] = i - try: - total.append(self.rec(expr)) - except: - if hasattr(expr, "_error"): - expr_in.set_error() - raise - - self.indices.pop(idx) - - return np.array(total) - - def map_compound_vector(self, expr): - - (index, indices, bodies) = expr.children - - if index not in self.indices: - expr.set_error() - index.set_error() - raise FInATSyntaxError( - "Compound vector depends on %s, which is not in scope" % index) - - alpha = self.indices[index] - - for idx, body in zip(indices, bodies): - if alpha < idx.length: - if idx in self.indices: - raise FInATSyntaxError( - "Attempting to bind the name %s which is already bound" % idx) - self.indices[idx] = self._as_range(idx)[alpha] - result = self.rec(body) - self.indices.pop(idx) - return result - else: - alpha -= idx.length - - raise FInATSyntaxError("Compound index %s out of bounds" % index) - - def map_wave(self, expr): - - (var, index, base, update, body) = expr.children - - if index not in self.indices: - expr.set_error() - index.set_error() - raise FInATSyntaxError( - "Wave variable depends on %s, which is not in scope" % index) - - try: - index_val = self.rec(index) - self.context[var.name], old_val = self.wave_vars[index][var.name] - # Check for index update. - if index_val != old_val: - self.context[var.name] = self.rec(update) - except KeyError: - # We're at the start of the loop over index. - assert index_val == (index.extent.start or 0) - self.context[var.name] = self.rec(base) - - self.wave_vars[index][var.name] = self.context[var.name], index_val - - # Execute the body. - result = self.rec(body) - - # Remove the wave variable from scope. - self.context.pop(var.name) - - return result - - def map_let(self, expr): - - for var, value in expr.bindings: - if var in self.context: - expr.set_error() - var.set_error() - raise FInATSyntaxError("Let variable %s was already in scope." - % var.name) - self.context[var.name] = self.rec(value) - - result = self.rec(expr.body) - - for var, value in expr.bindings: - self.context.pop(var.name) - - return result - - def map_levi_civita(self, expr): - - free, bound, body = expr.children - - if len(bound) == 3: - return self.rec(IndexSum(bound[:1], - LeviCivita(bound[:1], bound[1:], body))) - elif len(bound) == 2: - - self.indices[bound[0]] = (self.indices[free[0]] + 1) % 3 - self.indices[bound[1]] = (self.indices[free[0]] + 2) % 3 - tmp = self.rec(body) - self.indices[bound[1]] = (self.indices[free[0]] + 1) % 3 - self.indices[bound[0]] = (self.indices[free[0]] + 2) % 3 - tmp -= self.rec(body) - - self.indices.pop(bound[0]) - self.indices.pop(bound[1]) - - return tmp - - elif len(bound) == 1: - - i = self.indices[free[0]] - j = self.indices[free[1]] - - if i == j: - return 0 - elif j == (i + 1) % 3: - k = i + 2 % 3 - sign = 1 - elif j == (i + 2) % 3: - k = i + 1 % 3 - sign = -1 - - self.indices[bound[0]] = k - return sign * self.rec(body) - - elif len(bound) == 0: - - eijk = np.zeros((3, 3, 3)) - eijk[0, 1, 2] = eijk[1, 2, 0] = eijk[2, 0, 1] = 1 - eijk[0, 2, 1] = eijk[2, 1, 0] = eijk[1, 0, 2] = -1 - - i = self.indices[free[0]] - j = self.indices[free[1]] - k = self.indices[free[1]] - sign = eijk[i, j, k] - - if sign != 0: - return sign * self.rec(body) - else: - return 0 - - expr.set_error() - raise NotImplementedError - - def map_det(self, expr): - - return np.linalg.det(self.rec(expr.expression)) - - def map_abs(self, expr): - - return abs(self.rec(expr.expression)) - - def map_inverse(self, expr): - - return np.linalg.inv(self.rec(expr.expression)) - - -def evaluate(expression, context={}, kernel_data=None): - """Take a FInAT expression and a set of definitions for undefined - variables in the expression, and optionally the current kernel - data. Evaluate the expression returning a Float or numpy array - according to the size of the expression. - """ - - if kernel_data: - context = copy.copy(context) - for var in kernel_data.static.values(): - context[var[0].name] = var[1]() - - try: - expression = BindingMapper(context)(expression) - return FinatEvaluationMapper(context)(expression) - except: - print expression - raise diff --git a/finat/mappers.py b/finat/mappers.py deleted file mode 100644 index 53a89f3e8..000000000 --- a/finat/mappers.py +++ /dev/null @@ -1,1000 +0,0 @@ -from collections import deque -from pymbolic.mapper import IdentityMapper as IM -from pymbolic.mapper.stringifier import StringifyMapper, PREC_NONE -from pymbolic.mapper import WalkMapper as WM -from pymbolic.mapper.graphviz import GraphvizMapper as GVM -from pymbolic.primitives import Product, Sum, flattened_product, flattened_sum -from .indices import IndexBase, TensorIndex, PointIndexBase, BasisFunctionIndexBase,\ - DimensionIndex, flattened -from .ast import Recipe, ForAll, IndexSum, Let, Variable, Delta, CompoundVector -try: - from termcolor import colored -except ImportError: - def colored(string, color, attrs=None): - return string - - -class IdentityMapper(IM): - def __init__(self): - super(IdentityMapper, self).__init__() - - def map_recipe(self, expr, *args, **kwargs): - return expr.__class__(self.rec(expr.indices, *args, **kwargs), - self.rec(expr.body, *args, **kwargs), - expr._transpose) - - def map_index(self, expr, *args, **kwargs): - return expr - - def map_delta(self, expr, *args, **kwargs): - return expr.__class__(*tuple(self.rec(c, *args, **kwargs) - for c in expr.children)) - - def map_inverse(self, expr, *args, **kwargs): - return expr.__class__(self.rec(expr.expression, *args, **kwargs)) - - map_let = map_delta - map_for_all = map_delta - map_wave = map_delta - map_index_sum = map_delta - map_levi_civita = map_delta - map_compound_vector = map_delta - map_det = map_inverse - map_abs = map_inverse - - -class _IndexMapper(IdentityMapper): - def __init__(self, replacements): - super(_IndexMapper, self).__init__() - - self.replacements = replacements - - def map_index(self, expr, *args, **kwargs): - '''Replace indices if they are in the replacements list''' - - try: - return(self.replacements[expr]) - except KeyError: - return expr - - def map_compound_vector(self, expr, *args, **kwargs): - # Symbolic replacement of indices on a CompoundVector Just - # Works. However replacing the compound vector index with a - # number should collapse the CompoundVector. - - if expr.index in self.replacements and\ - not isinstance(self.replacements[expr.index], IndexBase): - # Work out which subvector we are in and what the index value is. - i = expr.index - val = self.replacements[expr.index] - - pos = (val - (i.start or 0)) / (i.step or 1) - assert pos <= i.length - - for subindex, body in zip(expr.indices, expr.body): - if pos < subindex.length: - sub_i = pos * (subindex.step or 1) + (subindex.start or 0) - self.replacements[subindex] = sub_i - result = self.rec(body, *args, **kwargs) - self.replacements.pop(subindex) - return result - else: - pos -= subindex.length - - raise ValueError("Illegal index value.") - else: - return super(_IndexMapper, self).map_compound_vector(expr, *args, **kwargs) - - -class _StringifyMapper(StringifyMapper): - - def map_recipe(self, expr, enclosing_prec, indent=None, *args, **kwargs): - if indent is None: - fmt = expr.name + "(%s, %s)" - else: - oldidt = " " * indent - indent += 4 - idt = " " * indent - fmt = expr.name + "(%s,\n" + idt + "%s\n" + oldidt + ")" - - return self.format(fmt, - self.rec(expr.indices, PREC_NONE, indent=indent, *args, **kwargs), - self.rec(expr.body, PREC_NONE, indent=indent, *args, **kwargs)) - - map_for_all = map_recipe - - def map_let(self, expr, enclosing_prec, indent=None, *args, **kwargs): - if indent is None: - fmt = expr.name + "(%s, %s)" - inner_indent = None - else: - oldidt = " " * indent - indent += 4 - inner_indent = indent + 4 - inner_idt = " " * inner_indent - idt = " " * indent - fmt = expr.name + "(\n" + inner_idt + "%s,\n" + idt + "%s\n" + oldidt + ")" - - return self.format(fmt, - self.rec(expr.bindings, PREC_NONE, indent=inner_indent, *args, **kwargs), - self.rec(expr.body, PREC_NONE, indent=indent, *args, **kwargs)) - - def map_delta(self, expr, *args, **kwargs): - return self.format(expr.name + "(%s, %s)", - *[self.rec(c, *args, **kwargs) for c in expr.children]) - - def map_index(self, expr, *args, **kwargs): - if hasattr(expr, "_error"): - return colored(str(expr), "red", attrs=["bold"]) - else: - return colored(str(expr), expr._color) - - def map_wave(self, expr, enclosing_prec, indent=None, *args, **kwargs): - if indent is None or enclosing_prec is not PREC_NONE: - fmt = expr.name + "(%s %s) " - else: - oldidt = " " * indent - indent += 4 - idt = " " * indent - fmt = expr.name + "(%s\n" + idt + "%s\n" + oldidt + ")" - - return self.format(fmt, - " ".join(self.rec(c, PREC_NONE, *args, **kwargs) + "," for c in expr.children[:-1]), - self.rec(expr.children[-1], PREC_NONE, indent=indent, *args, **kwargs)) - - def map_index_sum(self, expr, enclosing_prec, indent=None, *args, **kwargs): - if indent is None or enclosing_prec is not PREC_NONE: - fmt = expr.name + "((%s), %s) " - else: - oldidt = " " * indent - indent += 4 - idt = " " * indent - fmt = expr.name + "((%s),\n" + idt + "%s\n" + oldidt + ")" - - return self.format(fmt, - " ".join(self.rec(c, PREC_NONE, *args, **kwargs) + "," for c in expr.children[0]), - self.rec(expr.children[1], PREC_NONE, indent=indent, *args, **kwargs)) - - def map_levi_civita(self, expr, *args, **kwargs): - return self.format(expr.name + "(%s)", - self.join_rec(", ", expr.children, *args, **kwargs)) - - def map_inverse(self, expr, *args, **kwargs): - return self.format(expr.name + "(%s)", - self.rec(expr.expression, *args, **kwargs)) - - def map_det(self, expr, *args, **kwargs): - return self.format(expr.name + "(%s)", - self.rec(expr.expression, *args, **kwargs)) - - map_abs = map_det - - def map_compound_vector(self, expr, *args, **kwargs): - return self.format(expr.name + "(%s)", - self.join_rec(", ", expr.children, *args, **kwargs)) - - def map_variable(self, expr, enclosing_prec, *args, **kwargs): - if hasattr(expr, "_error"): - return colored(str(expr.name), "red", attrs=["bold"]) - else: - try: - return colored(expr.name, expr._color) - except AttributeError: - return colored(expr.name, "cyan") - - -class WalkMapper(WM): - def __init__(self): - super(WalkMapper, self).__init__() - - def map_recipe(self, expr, *args, **kwargs): - if not self.visit(expr, *args, **kwargs): - return - for indices in expr.indices: - for index in indices: - self.rec(index, *args, **kwargs) - self.rec(expr.body, *args, **kwargs) - self.post_visit(expr, *args, **kwargs) - - def map_index(self, expr, *args, **kwargs): - if not self.visit(expr, *args, **kwargs): - return - - # I don't want to recur on the extent. That's ugly. - - self.post_visit(expr, *args, **kwargs) - - def map_index_sum(self, expr, *args, **kwargs): - if not self.visit(expr, *args, **kwargs): - return - for index in expr.indices: - self.rec(index, *args, **kwargs) - self.rec(expr.body, *args, **kwargs) - self.post_visit(expr, *args, **kwargs) - - def map_let(self, expr, *args, **kwargs): - if not self.visit(expr, *args, **kwargs): - return - for symbol, value in expr.bindings: - self.rec(symbol, *args, **kwargs) - self.rec(expr.body, *args, **kwargs) - self.post_visit(expr, *args, **kwargs) - - map_delta = map_index_sum - map_for_all = map_index_sum - map_wave = map_index_sum - map_levi_civita = map_index_sum - map_inverse = map_index_sum - map_det = map_index_sum - map_compound_vector = map_index_sum - - -class IndicesMapper(WalkMapper): - """Label an AST with the indices which occur below each node.""" - - def __init__(self): - self._index_stack = [set()] - - def visit(self, expr, *args, **kwargs): - # Put a new index frame onto the stack. - self._index_stack.append(set()) - return True - - def post_visit(self, expr, *args, **kwargs): - # The frame contains any indices we directly saw: - indices_below = tuple(self._index_stack.pop()) - - if isinstance(expr, IndexBase): - indices_below += flattened(expr) - - self._indices_below = indices_below - - self._index_stack[-1].union(indices_below) - - -class GraphvizMapper(WalkMapper, GVM): - pass - - -class BindingMapper(IdentityMapper): - """A mapper that binds free indices in recipes using ForAlls.""" - - def __init__(self, kernel_data): - """ - :arg context: a mapping from variable names to values - """ - super(BindingMapper, self).__init__() - - def map_recipe(self, expr, bound_above=None, bound_below=None): - if bound_above is None: - bound_above = set() - if bound_below is None: - bound_below = deque() - - body = self.rec(expr.body, bound_above, bound_below) - - d, b, p = expr.indices - recipe_indices = tuple([i for i in d + b + p - if i not in bound_above]) - free_indices = tuple(set([i for i in recipe_indices - if i not in bound_below])) - - bound_below.extendleft(reversed(free_indices)) - # Calculate the permutation from the order of loops actually - # employed to the ordering of indices in the Recipe. - try: - def expand_tensors(indices): - result = [] - if indices: - for i in indices: - try: - result += i.factors - except AttributeError: - result.append(i) - return result - - tmp = expand_tensors(recipe_indices) - transpose = [tmp.index(i) for i in expand_tensors(bound_below)] - except ValueError: - print "recipe_indices", recipe_indices - print "missing index", i - i.set_error() - raise - - if len(free_indices) > 0: - expr = Recipe(expr.indices, ForAll(free_indices, body), - _transpose=transpose) - else: - expr = Recipe(expr.indices, body, _transpose=transpose) - - return expr - - def map_let(self, expr, bound_above, bound_below): - - # Indices bound in the Let bindings should not count as - # bound_below for nodes higher in the tree. - return Let(tuple((symbol, self.rec(letexpr, bound_above, - bound_below=None)) - for symbol, letexpr in expr.bindings), - self.rec(expr.body, bound_above, bound_below)) - - def map_index_sum(self, expr, bound_above, bound_below): - indices = expr.indices - for idx in indices: - bound_above.add(idx) - body = self.rec(expr.body, bound_above, bound_below) - for idx in indices: - bound_above.remove(idx) - return IndexSum(indices, body) - - def map_for_all(self, expr, bound_above, bound_below): - indices = expr.indices - for idx in indices: - bound_above.add(idx) - body = self.rec(expr.body, bound_above, bound_below) - for idx in indices: - bound_above.remove(idx) - bound_below.appendleft(idx) - return ForAll(indices, body) - - -class IndexSumMapper(IdentityMapper): - """A mapper that binds unbound IndexSums to temporary variables - using Lets.""" - - def __init__(self, kernel_data): - """ - :arg context: a mapping from variable names to values - """ - super(IndexSumMapper, self).__init__() - self.kernel_data = kernel_data - self._isum_stack = {} - self._bound_isums = set() - - def __call__(self, expr): - - if isinstance(expr.body, IndexSum): - self._bound_isums.add(expr.body) - - return super(IndexSumMapper, self).__call__(expr) - - def _bind_isums(self, expr): - bindings = [] - if isinstance(expr, Variable): - children = (expr,) - elif hasattr(expr, "children"): - children = expr.children - else: - return expr - - for temp in children: - if temp in self._isum_stack: - isum = self._isum_stack[temp] - bindings.append((temp, isum)) - for temp, isum in bindings: - del self._isum_stack[temp] - if len(bindings) > 0: - expr = Let(tuple(bindings), expr) - return expr - - def map_recipe(self, expr): - body = self._bind_isums(self.rec(expr.body)) - return Recipe(expr.indices, body) - - def map_let(self, expr): - # Record IndexSums already bound to a temporary - new_bindings = [] - for v, e in expr.bindings: - if isinstance(e, IndexSum): - self._bound_isums.add(e) - new_bindings.append((v, self.rec(e))) - - body = self._bind_isums(self.rec(expr.body)) - return Let(tuple(new_bindings), body) - - def map_index_sum(self, expr): - if expr in self._bound_isums: - return super(IndexSumMapper, self).map_index_sum(expr) - - # Replace IndexSum with temporary and add to stack - temp = self.kernel_data.new_variable("isum") - body = self._bind_isums(self.rec(expr.body)) - expr = IndexSum(expr.indices, body) - self._isum_stack[temp] = expr - return temp - - -class CancelCompoundVectorMapper(IdentityMapper): - """Mapper to find and expand reductions over CompoundVectors. - - Eventually this probably needs some policy support to decide which - cases it is worth expanding and cancelling and which not. - """ - def map_index_sum(self, expr, *args, **kwargs): - - body = self.rec(expr.body, *args, sum_indices=expr.indices) - - if isinstance(body, CompoundVector): - - if body.index in flattened(expr.indices): - - # Flatten the CompoundVector. - flattened_vector = 0 - r = {} - replacer = _IndexMapper(r) - for i in body.index.as_range: - r[body.index] = i - flattened_vector += replacer(body) - - indices = tuple(i for i in expr.indices if i != body.index) - - if indices: - return IndexSum(indices, flattened_vector) - else: - return flattened_vector - - else: - # Push the indexsum inside the CompoundVector in the hope - # that better cancellation will occur. - return CompoundVector(body.index, body.indices, - tuple(IndexSum(expr.indices, b) for b in body.body)) - else: - return IndexSum(expr.indices, body) - # if fs: - # assert len(fs) == 1 - # indices = tuple(i for i in expr.indices if i != fs[0][0]) - # if indices: - # return IndexSum(indices, fs[0][1]) - # else: - # return fs[0][1] - # else: - # return IndexSum(expr.indices, body) - # else: - # return super(CancelCompoundVectorMapper, self).map_index_sum( - # expr, *args, **kwargs) - - def map_product(self, expr, sum_indices=None, *args, - **kwargs): - - try: - if not sum_indices: - raise ValueError - vec_i = None - vectors = [] - factors = [] - for c in expr.children: - if isinstance(c, CompoundVector): - if vec_i is None: - vec_i = c.index - elif c.index != vec_i: - raise ValueError - vectors.append(c) - else: - factors.append(c) - - if vec_i is None: - raise ValueError # No CompoundVector - - # if vec_i in sum_indices: - # # Flatten the CompoundVector. - # flattened = 0 - - # r = {} - # replacer = _IndexMapper(r) - # for i in vec_i.as_range: - # r[vec_i] = i - # prod = 1 - # for c in expr.children: - # prod *= replacer(c) - # flattened += prod - - # factored_summands.append((vec_i, flattened)) - # return 0.0 - - elif len(vectors) == 1: - # Push the factors inside the CompoundVector in the - # hope that something further can be done. - vector = vectors[0] - bodies = list(vector.body) - for f in factors: - for b in range(len(bodies)): - bodies[b] *= f - - return CompoundVector(vector.index, vector.indices, tuple(bodies)) - else: - raise ValueError - - except ValueError: - # Drop to here if this is not a cancellation opportunity for whatever reason. - return super(CancelCompoundVectorMapper, self).map_product( - expr, *args, **kwargs) - - -class FactorDeltaMapper(IdentityMapper): - """Mapper to pull deltas up the expression tree to maximise the opportunities for cancellation.""" - - def map_index_sum(self, expr, deltas=None, *args, **kwargs): - - d = [False] - body = self.rec(expr.body, *args, deltas=d, **kwargs) - - if isinstance(body, Sum) and d[0]: - body = tuple(IndexSum(expr.indices, b) for b in body.children) - return flattened_sum(body) - else: - return IndexSum(expr.indices, body) - - def map_product(self, expr, deltas=None, *args, **kwargs): - - if deltas is not None: - in_deltas = deltas - else: - in_deltas = [False] - - child_deltas = tuple([False] for i in expr.children) - children = (self.rec(child, *args, deltas=delta, **kwargs) - for child, delta in zip(expr.children, child_deltas)) - factors = [] - sums = [] - deltas = [] - - for child, delta in zip(children, child_deltas): - if isinstance(child, Delta): - deltas.append(child) - factors.append(child.body) - elif isinstance(child, Sum) and delta[0]: - sums.append(child) - else: - factors.append(child) - - result = (flattened_product(tuple(factors)),) - - for s in sums: - result = (r * t for r in result for t in s.children) - if sums: - # We need to pull the Deltas up the terms we have just processed. - result = (self.rec(r, *args, **kwargs) for r in result) - # If sums then there are deltas in the sums. - in_deltas[0] = True - - for delta in deltas: - result = (Delta(delta.indices, r) for r in result) - in_deltas[0] = True - - result = tuple(result) - - if len(result) == 1: - return result[0] - else: - return flattened_sum(result) - - def map_sum(self, expr, deltas=None, *args, **kwargs): - - terms = tuple(self.rec(c, *args, **kwargs) for c in expr.children) - - if deltas is not None: - for term in terms: - if isinstance(term, Delta): - deltas[0] = True - - return flattened_sum(terms) - - -class CancelDeltaMapper(IdentityMapper): - """Mapper to cancel and/or replace indices according to the rules for Deltas.""" - - # Those nodes through which it is legal to transmit sum_indices. - _transmitting_nodes = (IndexSum, ForAll, Delta) - - def map_index_sum(self, expr, replace=None, sum_indices=(), *args, **kwargs): - - if replace is None: - replace = {} - - def flatten(index): - try: - return (index,) + reduce((lambda a, b: a + b), map(flatten, index.factors)) - except AttributeError: - return (index,) - - flattened = map(flatten, expr.indices) - sum_indices += reduce((lambda a, b: a + b), flattened) - - if type(expr.body) in self._transmitting_nodes: - # New index replacements are only possible in chains certain ast nodes. - body = self.rec(expr.body, *args, replace=replace, sum_indices=sum_indices, **kwargs) - else: - body = self.rec(expr.body, *args, replace=replace, **kwargs) - - new_indices = [] - for index in flattened: - if index[0] in replace: - # Replaced indices are dropped. - replace.pop(index[0]) - - elif any(i in replace for i in index[1:]): - for i in index[1:]: - if i not in replace: - new_indices.append(i) - else: - replace.pop(i) - else: - new_indices.append(index[0]) - # Do we need to also drop indices on the RHS of replaces? - - if new_indices: - return IndexSum(new_indices, body) - else: - return body - - def map_delta(self, expr, replace=None, sum_indices=(), *args, **kwargs): - - # For the moment let's just go with the delta has two indices idea - assert len(expr.indices) == 2 - - if replace is not None: - indices = tuple(replace[index] if index in replace.keys() else index for index in expr.indices) - else: - indices = expr.indices - - # fix this so that the replacements happen from the bottom up. - for i in sum_indices[::-1]: - if i == indices[1]: - replace[indices[1]] = indices[0] - indices = (indices[0], indices[0]) - break - elif i == indices[0]: - replace[indices[0]] = indices[1] - indices = (indices[1], indices[1]) - break - - # Only attempt new replacements if we are in transmitting node stacks. - if sum_indices and indices[0] != indices[1]: - targets = replace.values() - if indices[0] in targets and indices[1] not in targets: - replace[indices[1]] = indices[0] - indices = (indices[0], indices[0]) - elif indices[1] in targets and indices[0] not in targets: - replace[indices[0]] = indices[1] - indices = (indices[0], indices[0]) - # else: - # # I don't think this can happen. - # raise NotImplementedError - - if type(expr.body) in self._transmitting_nodes: - # New index replacements are only possible in chains of certain ast nodes. - body = self.rec(expr.body, *args, replace=replace, - sum_indices=sum_indices, **kwargs) - else: - body = self.rec(expr.body, *args, replace=replace, **kwargs) - - if indices[0] == indices[1]: - return body - else: - return Delta(indices, body) - - def map_recipe(self, expr, replace=None, *args, **kwargs): - if replace is None: - replace = {} - - body = self.rec(expr.body, *args, replace=replace, **kwargs) - - def recurse_replace(index): - if index in replace: - return replace[index] - else: - try: - return type(index)(*map(recurse_replace, index.factors)) - except AttributeError: - return index - - if replace: - indices = tuple(tuple(map(recurse_replace, itype)) for itype in expr.indices) - else: - indices = expr.indices - - return Recipe(indices, body) - - def map_let(self, expr, replace=None, sum_indices=(), *args, **kwargs): - # Propagate changes first into the body. Then do any required - # substitutions on the bindings. - - body = self.rec(expr.body, *args, replace=replace, - sum_indices=sum_indices, **kwargs) - - # Need to think about conveying information from the body to - # the bindings about diagonalisations which might occur. - - bindings = self.rec(expr.bindings, *args, replace=replace, - sum_indices=sum_indices, **kwargs) - - return Let(bindings, body) - - def map_index(self, expr, replace=None, *args, **kwargs): - - if hasattr(expr, "factors"): - return expr.__class__(*self.rec(expr.factors, *args, - replace=replace, **kwargs)) - else: - return replace[expr] if replace and expr in replace else expr - - -class _DoNotFactorSet(set): - """Dummy set object used to indicate that sum factorisation of a subtree is invalid.""" - pass - - -class SumFactorMapper(IdentityMapper): - """Mapper to attempt sum factorisation. This is currently a sketch - implementation which is not safe for particularly general cases.""" - - # Internal communication of separable index sets is achieved by - # the index_groups argument. This is a set containing tuples of - # grouped indices. - - def __init__(self, kernel_data): - - super(SumFactorMapper, self).__init__() - - self.kernel_data = kernel_data - - @staticmethod - def factor_indices(indices, index_groups): - # Determine the factorisability of the expression with the - # given index_groups assuming an IndexSum over indices. - if len(indices) != 1 or not isinstance(indices[0], TensorIndex) \ - or len(indices[0].factors) != 2: - return False - - i = list(indices[0].factors) - - # Try to factor the longest length first. - if i[0].length < i[1].length: - i.reverse() - - for n in range(2): - factorstack = [] - nontrivial = False - for g in index_groups: - if i[n] in g: - factorstack.append(g) - elif i[(n + 1) % 2] in g: - nontrivial = True - - if factorstack and nontrivial: - return i[n] - - return False - - def map_index_sum(self, expr, index_groups=None, *args, **kwargs): - """Discover how factorisable this IndexSum is.""" - - body_igroups = set() - body = self.rec(expr.body, *args, index_groups=body_igroups, **kwargs) - - factor_index = self.factor_indices(expr.indices, body_igroups) - if factor_index: - factorised = SumFactorSubTreeMapper(factor_index)(expr.body) - try: - return factorised.generate_factored_expression(self.kernel_data, expr.indices[0].factors) - except: - pass - - return expr.__class__(expr.indices, body) - - def map_index(self, expr, index_groups=None, *args, **kwargs): - """Add this index into all the sets in index_groups.""" - - if index_groups is None: - return expr - elif hasattr(expr, "factors"): - return expr.__class__(*self.rec(expr.factors, *args, index_groups=index_groups, **kwargs)) - elif index_groups: - news = [] - for s in index_groups: - news.append(s + (expr,)) - index_groups.clear() - index_groups.update(news) - else: - index_groups.add((expr,)) - - return expr - - def map_product(self, expr, index_groups=None, *args, **kwargs): - """Union of the index groups of the children.""" - - if index_groups is None: - return super(SumFactorMapper, self).map_product(expr, *args, **kwargs) - else: - result = 1 - for c in expr.children: - igroup = set() - result *= self.rec(c, *args, index_groups=igroup, **kwargs) - index_groups |= igroup - - return result - - def map_delta(self, expr, index_groups=None, *args, **kwargs): - """Treat this as the delta times its body.""" - if index_groups is None: - return super(SumFactorMapper, self).map_delta(expr, *args, **kwargs) - else: - igroup = set() - body = self.rec(expr.body, *args, index_groups=igroup, **kwargs) - index_groups |= igroup - igroup = set() - indices = self.rec(expr.indices, *args, index_groups=igroup, **kwargs) - index_groups |= igroup - return expr.__class__(indices, body) - - def map_sum(self, expr, index_groups=None, *args, **kwargs): - """If the summands have the same factors, propagate them up. - Otherwise (for the moment) put a _DoNotFactorSet in the output. - """ - - igroups = [set() for c in expr.children] - new_children = [self.rec(c, *args, index_groups=i, **kwargs) - for c, i in zip(expr.children, igroups)] - - if index_groups is not None: - # index_groups really should be empty. - if index_groups: - raise ValueError("Can't happen!") - - # This is not quite safe as it imposes additional ordering. - if all([i == igroups[0] for i in igroups[1:]]): - index_groups.update(igroups[0]) - else: - raise ValueError("Don't know how to do this") - - return flattened_sum(tuple(new_children)) - - -class _Factors(object): - """A product factorised by the presence or absence of the index provided.""" - def __init__(self, index, expr=None, indices=None): - self.index = index - self.factor = 1 - self.multiplicand = 1 - - if expr: - self.insert(expr, indices) - - def insert(self, expr, indices): - if self.index in indices: - self.factor *= expr - else: - self.multiplicand *= expr - - def __imul__(self, other): - - if isinstance(other, _Factors): - if other.index != self.index: - raise ValueError("Can only multiply _Factors with the same index") - self.factor *= other.factor - self.multiplicand *= other.multiplicand - else: - self.insert(*other) - return self - - def generate_factored_expression(self, kernel_data, indices): - # Generate the factored expression using the set of indices provided. - indices = list(indices) - indices.remove(self.index) - - temp = kernel_data.new_variable("isum") - d = tuple(i for i in indices if isinstance(i, DimensionIndex)) - b = tuple(i for i in indices if isinstance(i, BasisFunctionIndexBase)) - p = tuple(i for i in indices if isinstance(i, PointIndexBase)) - return Let(((temp, Recipe((d, b, p), IndexSum((self.index,), self.factor))),), - IndexSum(tuple(indices), temp[d + b + p] * self.multiplicand)) - - -class _FactorSum(object): - """A sum of _Factors.""" - - def __init__(self, factors=None): - - self.factors = list(factors or []) - - def __iadd__(self, factor): - - self.factors.append(factor) - return self - - def __imul__(self, other): - - assert isinstance(other, _Factors) - for f in self.factors: - f *= other - return self - - def generate_factored_expression(self, kernel_data, indices): - # Generate the factored expression using the set of indices provided. - - genexprs = [f.generate_factored_expression(kernel_data, indices) - for f in self.factors] - - # This merges some indexsums but that gets in the way of delta cancellation. - if all(self.factors[0].index == f.index for f in self.factors[1:]): - return Let(tuple(g.bindings[0] for g in genexprs), - flattened_sum(tuple(g.body for g in genexprs))) - else: - return flattened_sum(genexprs) - - -class SumFactorSubTreeMapper(IdentityMapper): - """Mapper to actually impose a defined factorisation on a subtree.""" - - def __init__(self, factor_index): - - super(SumFactorSubTreeMapper, self).__init__() - - # The index with respect to which the factorisation should occur. - self.factor_index = factor_index - - def map_index(self, expr, indices=None, *args, **kwargs): - """Add this index into all the sets in index_groups.""" - - if indices is None: - return expr - elif hasattr(expr, "factors"): - return expr.__class__(*self.rec(expr.factors, *args, indices=indices, **kwargs)) - else: - indices.add(expr) - - return expr - - def map_delta(self, expr, indices=None, *args, **kwargs): - """Turn the delta back into a product and recurse on that.""" - - def deltafactor(f, indices): - if self.factor_index in indices: - f.factor = Delta(expr.indices, f.factor) - else: - f.multiplicand = Delta(expr.indices, f.multiplicand) - - i = set() - rc = self.rec(expr.body, *args, indices=i, **kwargs) - indices = flattened(expr.indices) - if isinstance(rc, _Factors): - deltafactor(rc, indices) - elif isinstance(rc, _FactorSum): - for f in rc.factors: - deltafactor(f, indices) - else: - f = _Factors(self.factor_index) - f *= (rc, i) - deltafactor(f, indices) - rc = f - - return rc - - def map_product(self, expr, indices=None, *args, **kwargs): - - f = _Factors(self.factor_index) - for c in expr.children: - i = set() - rc = self.rec(c, *args, indices=i, **kwargs) - if isinstance(rc, _FactorSum): - rc *= f - f = rc - elif isinstance(rc, _Factors): - f *= rc - else: - f *= (rc, i) - - return f - - def map_sum(self, expr, indices=None, *args, **kwargs): - - f = _FactorSum() - for c in expr.children: - i = set() - rc = self.rec(c, *args, indices=i, **kwargs) - if isinstance(rc, (_Factors, _FactorSum)): - f += rc - else: - f += _Factors(self.factor_index, rc, i) - - return f diff --git a/finat/pyop2_interface.py b/finat/pyop2_interface.py deleted file mode 100644 index 431cf418d..000000000 --- a/finat/pyop2_interface.py +++ /dev/null @@ -1,52 +0,0 @@ -try: - from pyop2 import Kernel -except: - Kernel = None -from .interpreter import evaluate -from .ast import Array -from .coffee_compiler import CoffeeKernel - - -def pyop2_kernel_function(kernel, kernel_args, interpreter=False): - """Return a python function suitable to be called from PyOP2 from the - recipe and kernel data provided. - - :param kernel: The :class:`~.utils.Kernel` to map to PyOP2. - :param kernel_args: The ordered list of Pymbolic variables constituting - the kernel arguments, excluding the result of the recipe (the latter - should be prepended to the argument list). - :param interpreter: If set to ``True``, the kernel will be - evaluated using the FInAT interpreter instead of generating a - compiled kernel. - - :result: A function which will execute the kernel. - - """ - - if Kernel is None: - raise ImportError("pyop2 was not imported. Is it installed?") - - if kernel_args and \ - set(kernel_args) != kernel.kernel_data.kernel_args: - raise ValueError("Incomplete value list") - - if interpreter: - - def kernel_function(*args): - context = {var.name: val for (var, val) in zip(kernel_args, args[1:])} - - args[0][:] += evaluate(kernel.recipe, context, kernel.kernel_data) - - return kernel_function - - else: - index_shape = () - for index in kernel.recipe.indices: - for i in index: - index_shape += (i.extent.stop, ) - kernel_args.insert(0, Array("*A", shape=index_shape)) - - coffee_kernel = CoffeeKernel(kernel.recipe, kernel.kernel_data) - kernel_ast = coffee_kernel.generate_ast(kernel_args=kernel_args, - varname="*A", increment=True) - return Kernel(kernel_ast, "finat_kernel") diff --git a/finat/utils.py b/finat/utils.py deleted file mode 100644 index 279d72dfc..000000000 --- a/finat/utils.py +++ /dev/null @@ -1,201 +0,0 @@ -import inspect -import FIAT -from .derivatives import grad -from .ast import Let, Array, Inverse, Det, Variable - - -class Kernel(object): - def __init__(self, recipe, kernel_data): - """An object bringing together a :class:`~.ast.Recipe` and its - corresponding :class:`~.utils.KernelData` context. - """ - - self.recipe = recipe - self.kernel_data = kernel_data - - -class KernelData(object): - def __init__(self, coordinate_element=None, coordinate_var=None, - affine=None): - """ - :param coordinate_element: the (vector-valued) finite element for - the coordinate field. - :param coordinate_var: the symbolic variable for the - coordinate values. If no pullbacks are present in the kernel, - this may be omitted. - :param affine: Specifies whether the pullback is affine (and therefore - whether the Jacobian must be evaluated at every quadrature point). - If not specified, this is inferred from coordinate_element. - """ - - self.coordinate_element = coordinate_element - self.coordinate_var = coordinate_var - if affine is None and coordinate_element: - self.affine = coordinate_element.degree <= 1 and \ - isinstance(coordinate_element.cell, _simplex) - else: - self.affine = affine - - self.static = {} - # The set of undefined symbols in this kernel. - self.kernel_args = set((coordinate_var,)) if coordinate_var else set() - self.geometry = {} - self.variables = set() - - #: The geometric dimension of the physical space. - self.gdim = (coordinate_element._dimension - if coordinate_element else None) - #: The topological dimension of the reference element - self.tdim = (coordinate_element._cell.get_spatial_dimension() - if coordinate_element else None) - - self._variable_count = 0 - self._point_count = 0 - self._wt_count = 0 - self._variable_cache = {} - - def tabulation_variable_name(self, element, points): - """Given a finite element and a point set, return a variable name phi_n - where n is guaranteed to be unique to that combination of element and - points.""" - - key = (element, id(points)) - - try: - return self._variable_cache[key] - except KeyError: - name = u'\u03C6_'.encode("utf-8") \ - + str(self._variable_count) - self._variable_cache[key] = name - self._variable_count += 1 - self.variables.add(name) - return self._variable_cache[key] - - def point_variable_name(self, points): - """Given a point set, return a variable name xi_n - where n is guaranteed to be unique to that set of - points.""" - - key = (id(points),) - - try: - return self._variable_cache[key] - except KeyError: - name = u'\u03BE_'.encode("utf-8") \ - + str(self._point_count) - self._variable_cache[key] = name - self._point_count += 1 - self.variables.add(name) - return self._variable_cache[key] - - def weight_variable_name(self, weights): - """Given an iterable of weights set, return a variable name wt_n - where n is guaranteed to be unique to that set of weights.""" - - key = (id(weights),) - - try: - return self._variable_cache[key] - except KeyError: - name = u'\u03BE_'.encode("utf-8") \ - + str(self._wt_count) - self._variable_cache[key] = name - self._wt_count += 1 - self.variables.add(name) - return self._variable_cache[key] - - def new_variable(self, prefix=None): - """Create a variable guaranteed to be unique in the kernel context.""" - name = prefix or "tmp" - if name not in self.variables: - self.variables.add(name) - return Variable(name) - - # Prefix was already in use, so append an index. - i = 0 - while True: - varname = "%s_%d" % (name, i) - if varname not in self.variables: - self.variables.add(varname) - return Variable(varname) - i += 1 - - @property - def J(self): - '''The Jacobian of the coordinate transformation. - - .. math:: - - J_{\gamma,\tau} = \frac{\partial x_\gamma}{\partial X_\tau} - - Where :math:`x` is the physical coordinate and :math:`X` is the - local coordinate. - ''' - try: - return self.geometry["J"] - except KeyError: - self.geometry["J"] = Array("J", (self.gdim, self.tdim)) - return self.geometry["J"] - - @property - def invJ(self): - '''The Moore-Penrose pseudo-inverse of the coordinate transformation. - - .. math:: - - J^{-1}_{\tau,\gamma} = \frac{\partial X_\tau}{\partial x_\gamma} - - Where :math:`x` is the physical coordinate and :math:`X` is the - local coordinate. - ''' - - try: - return self.geometry["invJ"] - except KeyError: - self.geometry["invJ"] = Array("invJ", (self.tdim, self.gdim)) - return self.geometry["invJ"] - - @property - def detJ(self): - '''The determinant of the coordinate transformation.''' - - try: - return self.geometry["detJ"] - except KeyError: - self.geometry["detJ"] = Variable("detJ") - return self.geometry["detJ"] - - def bind_geometry(self, expression, points=None): - """Let statement defining the geometry for expression. If no geometry - is required, return expression.""" - - if len(self.geometry) == 0: - return expression - - g = self.geometry - - if points is None: - points = self._origin - - inner_lets = [] - if "invJ" in g: - inner_lets += (g["invJ"], Inverse(g["J"])) - if "detJ" in g: - inner_lets += (g["detJ"], Det(g["J"])) - - J_expr = self.coordinate_element.evaluate_field( - self.coordinate_var, points, self, derivative=grad, pullback=False) - if points: - J_expr = J_expr.replace_indices( - zip(J_expr.indices[-1], expression.indices[-1])) - - return Let((g["J"], J_expr), - Let(inner_lets, expression) if inner_lets else expression) - - -# Tuple of simplex cells. This relies on the fact that FIAT only -# defines simplex elements. -_simplex = tuple(e for e in FIAT.reference_element.__dict__.values() - if (inspect.isclass(e) and - issubclass(e, FIAT.reference_element.ReferenceElement) and - e is not FIAT.reference_element.ReferenceElement)) From c0d0e1a17a881966912980ec9ce09f5c40c9e02e Mon Sep 17 00:00:00 2001 From: David Ham Date: Thu, 7 Apr 2016 18:33:40 +0100 Subject: [PATCH 214/749] post cull. module is importable and passes pep8 --- finat/__init__.py | 9 +- finat/bernstein.py | 303 ----------------------------------- finat/fiat_elements.py | 130 +++++++-------- finat/indices.py | 226 -------------------------- finat/points.py | 23 ++- finat/product_elements.py | 58 +------ finat/vectorfiniteelement.py | 177 +------------------- 7 files changed, 69 insertions(+), 857 deletions(-) delete mode 100644 finat/indices.py diff --git a/finat/__init__.py b/finat/__init__.py index ad410da3a..960243fd2 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -1,15 +1,8 @@ -from scalar_elements import Lagrange, DiscontinuousLagrange, GaussLobatto +from fiat_elements import Lagrange, DiscontinuousLagrange, GaussLobatto from hdiv import RaviartThomas, BrezziDouglasMarini, BrezziDouglasFortinMarini from bernstein import Bernstein from vectorfiniteelement import VectorFiniteElement from product_elements import ScalarProductElement -from points import PointSet -from utils import KernelData, Kernel from derivatives import div, grad, curl -from indices import * -from ast import * -import interpreter import quadrature import ufl_interface -from geometry_mapper import GeometryMapper -import coffee_compiler diff --git a/finat/bernstein.py b/finat/bernstein.py index ff9f307f1..5f53839d1 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -1,17 +1,7 @@ from .finiteelementbase import FiniteElementBase -from .points import StroudPointSet -from .ast import ForAll, Recipe, Wave, Let, IndexSum, Variable -from .indices import BasisFunctionIndex, PointIndex, SimpliciallyGradedBasisFunctionIndex # noqa - import numpy as np -# reimplement sum using reduce to avoid problem with infinite loop -# into pymbolic -def mysum(vals): - return reduce(lambda a, b: a + b, vals, 0) - - def pd(sd, d): if sd == 3: return (d + 1) * (d + 2) * (d + 3) / 6 @@ -33,32 +23,6 @@ def __init__(self, cell, degree): self._cell = cell self._degree = degree - def _points_variable(self, points, kernel_data): - """Return the symbolic variables for the static data - corresponding to the :class:`PointSet` ``points``.""" - - static_key = (id(points),) - if static_key in kernel_data.static: - xi = kernel_data.static[static_key][0] - else: - xi = Variable(kernel_data.point_variable_name(points)) - kernel_data.static[static_key] = (xi, lambda: points.points) - - return xi - - def _weights_variable(self, weights, kernel_data): - """Return the symbolic variables for the static data - corresponding to the array of weights in a quadrature rule.""" - - static_key = (id(weights),) - if static_key in kernel_data.static: - wt = kernel_data.static[static_key][0] - else: - wt = Variable(kernel_data.weight_variable_name(weights)) - kernel_data.static[static_key] = (wt, lambda: np.array(weights)) - - return wt - @property def dofs_shape(self): @@ -66,270 +30,3 @@ def dofs_shape(self): dim = self.cell.get_spatial_dimension() return (int(np.prod(xrange(degree + 1, degree + 1 + dim)) / np.prod(xrange(1, dim + 1))),) - - def field_evaluation(self, field_var, q, kernel_data, derivative=None): - if not isinstance(q.points, StroudPointSet): - raise ValueError("Only Stroud points may be employed with Bernstein polynomials") - - if derivative is not None: - raise NotImplementedError - - kernel_data.kernel_args.add(field_var) - - # Get the symbolic names for the points. - xi = [self._points_variable(f.points, kernel_data) - for f in q.factors] - - qs = q.factors - - deg = self.degree - - sd = self.cell.get_spatial_dimension() - - r = kernel_data.new_variable("r") - w = kernel_data.new_variable("w") -# r = Variable("r") -# w = Variable("w") - tmps = [kernel_data.new_variable("tmp") for d in range(sd - 1)] - - # Create basis function indices that run over - # the possible multiindex space. These have - # to be jagged - - alpha = SimpliciallyGradedBasisFunctionIndex(sd, deg) - alphas = alpha.factors - - # temporary quadrature indices so I don't clobber the ones that - # have to be free for the entire recipe - qs_internal = [PointIndex(qf) for qf in q.points.factor_sets] - - # For each phase I need to figure out the free variables of - # that phase - free_vars_per_phase = [] - for d in range(sd - 1): - alphas_free_cur = tuple(alphas[:(-1 - d)]) - qs_free_cur = tuple(qs_internal[(-1 - d):]) - free_vars_per_phase.append(((), alphas_free_cur, qs_free_cur)) - # last phase: the free variables are the free quadrature point indices - free_vars_per_phase.append(((), (), (q,))) - - # the first phase reads from the field_var storage - # the rest of the phases will read from a tmp variable - # This code computes the offset into the field_var storage - # for the internal sum variable loop. - offset = 0 - for d in range(sd - 1): - deg_begin = deg - mysum(alphas[:d]) - deg_end = deg - alphas[d] - offset += pd(sd - d, deg_begin) - pd(sd - d, deg_end) - - # each phase of the sum - factored algorithm reads from a particular - # location. The first of these is field_var, the rest are the - # temporaries. - read_locs = [field_var[alphas[-1] + offset]] - if sd > 1: - # intermediate phases will read from the alphas and - # internal quadrature points - for d in range(1, sd - 1): - tmp_cur = tmps[d - 1] - read_alphas = alphas[:(-d)] - read_qs = qs_internal[(-d):] - read_locs.append(tmp_cur[tuple(read_alphas + read_qs)]) - - # last phase reads from the last alpha and the incoming quadrature points - read_locs.append(tmps[-1][tuple(alphas[:1] + qs[1:])]) - - # Figure out the "xi" for each phase being used in the recurrence. - # In the last phase, it has to refer to one of the free incoming - # quadrature points, and it refers to the internal ones in previous phases. - xi_per_phase = [xi[-(d + 1)][qs_internal[-(d + 1)]] - for d in range(sd - 1)] + [xi[0][qs[0]]] - - # first phase: no previous phase to bind - xi_cur = xi_per_phase[0] - s = 1 - xi_cur - expr = Let(((r, xi_cur / s),), - IndexSum((alphas[-1],), - Wave(w, - alphas[-1], - s ** (deg - mysum(alphas[:(sd - 1)])), - w * r * (deg - mysum(alphas) + 1) / (alphas[-1]), - w * read_locs[0] - ) - ) - ) - recipe_cur = Recipe(free_vars_per_phase[0], expr) - - for d in range(1, sd): - # Need to bind the free variables that came before in Let - # then do what I think is right. - xi_cur = xi_per_phase[d] - s = 1 - xi_cur - alpha_cur = alphas[-(d + 1)] - asum0 = mysum(alphas[:(sd - d - 1)]) - asum1 = mysum(alphas[:(sd - d)]) - - expr = Let(((tmps[d - 1], recipe_cur), - (r, xi_cur / s)), - IndexSum((alpha_cur,), - Wave(w, - alpha_cur, - s ** (deg - asum0), - w * r * (deg - asum1 + 1) / alpha_cur, - w * read_locs[d] - ) - ) - ) - recipe_cur = Recipe(free_vars_per_phase[d], expr) - - return recipe_cur - - def moment_evaluation(self, value, weights, q, kernel_data, - derivative=None, pullback=None): - if not isinstance(q.points, StroudPointSet): - raise ValueError("Only Stroud points may be employed with Bernstein polynomials") - - if derivative is not None: - raise NotImplementedError - - qs = q.factors - - wt = [self._weights_variable(weights[d], kernel_data) - for d in range(len(weights))] - - xi = [self._points_variable(f.points, kernel_data) - for f in q.factors] - - deg = self.degree - - sd = self.cell.get_spatial_dimension() - - r = kernel_data.new_variable("r") - w = kernel_data.new_variable("w") - tmps = [kernel_data.new_variable("tmp") for d in range(sd - 1)] - - if sd == 2: - alpha_internal = SimpliciallyGradedBasisFunctionIndex(sd, deg) - alphas_int = alpha_internal.factors - xi_cur = xi[0][qs[1]] - s = 1 - xi_cur - expr0 = Let(((r, xi_cur / s), ), - IndexSum((qs[0], ), - Wave(w, - alphas_int[0], - wt[0][qs[0]] * (s ** deg), - w * r * (deg - alphas_int[0]) / alphas_int[0], - w * value[qs[0], qs[1]]) - ) - ) - expr0prime = ForAll((qs[1],), - ForAll((alphas_int[0],), - expr0)) - recipe0 = Recipe(((), (alphas_int[0], ), (qs[1], )), - expr0prime) - xi_cur = xi[1] - s = 1 - xi_cur - alpha = SimpliciallyGradedBasisFunctionIndex(2, deg) - alphas = alpha.factors - r = xi_cur / s - expr1 = Let(((tmps[0], recipe0), ), - IndexSum((qs[1], ), - ForAll((alphas[0],), - ForAll((alphas[1],), - Wave(w, - alphas[1], - wt[1][qs[1]] * (s ** (deg - alphas[0])), - w * r * (deg - alphas[0] - alphas[1] + 1) / (alphas[1]), - w * tmps[0][alphas[0], qs[1]]) - ) - ) - ) - ) - - return Recipe(((), (alphas[0], alphas[1]), ()), expr1) - - else: - raise NotImplementedError - - def moment_evaluation_general(self, value, weights, q, kernel_data, - derivative=None, pullback=None): - if not isinstance(q.points, StroudPointSet): - raise ValueError("Only Stroud points may be employed with Bernstein polynomials") - - if derivative is not None: - raise NotImplementedError - - qs = q.factors - - wt = [self._weights_variable(weights[d], kernel_data) - for d in range(len(weights))] - - xi = [self._points_variable(f.points, kernel_data) - for f in q.factors] - - deg = self.degree - - sd = self.cell.get_spatial_dimension() - - r = kernel_data.new_variable("r") - w = kernel_data.new_variable("w") - tmps = [kernel_data.new_variable("tmp") for d in range(sd - 1)] - - # the output recipe is parameterized over these - alpha = SimpliciallyGradedBasisFunctionIndex(sd, deg) - alphas = alpha.factors - - read_locs = [value[q]] - for d in range(1, sd - 1): - tmp_cur = tmps[d - 1] - read_alphas = alphas[:d] - read_qs = qs[-d:] - read_locs.append(tmp_cur[tuple(read_alphas + read_qs)]) - d = sd - 1 - tmp_cur = tmps[d - 1] - read_alphas = alphas[:d] - read_qs = qs[-d:] - read_locs.append(tmp_cur[tuple(read_alphas + read_qs)]) - - free_vars_per_phase = [] - for d in range(1, sd): - alphas_free_cur = tuple(alphas[:d]) - qs_free_cur = tuple(qs[-d:]) - free_vars_per_phase.append(((), alphas_free_cur, qs_free_cur)) - free_vars_per_phase.append(((), (), tuple(alphas))) - - xi_cur = xi[0][qs[0]] - s = 1 - xi_cur - expr = Let(((r, xi_cur / s),), - IndexSum((qs[0],), - Wave(w, - alphas[0], - wt[0][qs[0]] * s ** deg, - w * r * (deg - alphas[0] - 1) / alphas[0], - w * read_locs[0] - ) - ) - ) - - recipe_cur = Recipe(free_vars_per_phase[0], expr) - - for d in range(1, sd): - xi_cur = xi[d] - s = 1 - xi_cur - acur = alphas[d] - asum0 = mysum(alphas[:d]) - asum1 = asum0 - acur - expr = Let(((tmps[d - 1], recipe_cur), - (r, xi_cur / s)), - IndexSum((qs[d],), - Wave(w, - acur, - wt[d][qs[d]] * s ** (deg - asum0), - w * r * (deg - asum1 - 1) / acur, - w * read_locs[d] - ) - ) - ) - recipe_cur = Recipe(free_vars_per_phase[d], expr) - - return recipe_cur diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 2abe92470..cb1d4bfe1 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -1,101 +1,79 @@ from .finiteelementbase import FiatElementBase -from .ast import Recipe, IndexSum, Delta import FIAT -import indices -from .derivatives import grad -from .points import GaussLobattoPointSet -class ScalarElement(FiatElementBase): +class FiatElement(FiatElementBase): def __init__(self, cell, degree): - super(ScalarElement, self).__init__(cell, degree) - - def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): - '''Produce the variable for the tabulation of the basis - functions or their derivative. Also return the relevant indices. - - updates the requisite static kernel data, which in this case - is just the matrix. - ''' - if derivative not in (None, grad): - raise ValueError( - "Scalar elements do not have a %s operation") % derivative - - phi = self._tabulated_basis(q.points, kernel_data, derivative) - - i = indices.BasisFunctionIndex(self._fiat_element.space_dimension()) - - if derivative is grad: - alpha = indices.DimensionIndex(self.cell.get_spatial_dimension()) - if pullback: - beta = alpha - alpha = indices.DimensionIndex(kernel_data.gdim) - invJ = kernel_data.invJ[(beta, alpha)] - expr = IndexSum((beta,), invJ * phi[(beta, i, q)]) - else: - expr = phi[(alpha, i, q)] - ind = ((alpha,), (i,), (q,)) - else: - ind = ((), (i,), (q,)) - expr = phi[(i, q)] - - return Recipe(indices=ind, body=expr) - - def field_evaluation(self, field_var, q, - kernel_data, derivative=None, pullback=True): - if derivative not in (None, grad): - raise ValueError( - "Scalar elements do not have a %s operation") % derivative - - return super(ScalarElement, self).field_evaluation( - field_var, q, kernel_data, derivative, pullback) - - def moment_evaluation(self, value, weights, q, - kernel_data, derivative=None, pullback=True): - if derivative not in (None, grad): - raise ValueError( - "Scalar elements do not have a %s operation") % derivative - - return super(ScalarElement, self).moment_evaluation( - value, weights, q, kernel_data, derivative, pullback) - - -class Lagrange(ScalarElement): + super(FiatElement, self).__init__(cell, degree) + + # def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): + # '''Produce the variable for the tabulation of the basis + # functions or their derivative. Also return the relevant indices. + + # updates the requisite static kernel data, which in this case + # is just the matrix. + # ''' + # if derivative not in (None, grad): + # raise ValueError( + # "Scalar elements do not have a %s operation") % derivative + + # phi = self._tabulated_basis(q.points, kernel_data, derivative) + + # i = indices.BasisFunctionIndex(self._fiat_element.space_dimension()) + + # if derivative is grad: + # alpha = indices.DimensionIndex(self.cell.get_spatial_dimension()) + # if pullback: + # beta = alpha + # alpha = indices.DimensionIndex(kernel_data.gdim) + # invJ = kernel_data.invJ[(beta, alpha)] + # expr = IndexSum((beta,), invJ * phi[(beta, i, q)]) + # else: + # expr = phi[(alpha, i, q)] + # ind = ((alpha,), (i,), (q,)) + # else: + # ind = ((), (i,), (q,)) + # expr = phi[(i, q)] + + # return Recipe(indices=ind, body=expr) + + +class Lagrange(FiatElement): def __init__(self, cell, degree): super(Lagrange, self).__init__(cell, degree) self._fiat_element = FIAT.Lagrange(cell, degree) -class GaussLobatto(ScalarElement): +class GaussLobatto(FiatElement): def __init__(self, cell, degree): super(GaussLobatto, self).__init__(cell, degree) self._fiat_element = FIAT.GaussLobatto(cell, degree) - def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): - '''Produce the variable for the tabulation of the basis - functions or their derivative. Also return the relevant indices. + # def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): + # '''Produce the variable for the tabulation of the basis + # functions or their derivative. Also return the relevant indices. - For basis evaluation with no gradient on a matching - Gauss-Lobatto quadrature, this implements the standard - spectral element diagonal mass trick by returning a delta - function. - ''' - if (derivative is None and isinstance(q.points, GaussLobattoPointSet) and - q.length == self._fiat_element.space_dimension()): + # For basis evaluation with no gradient on a matching + # Gauss-Lobatto quadrature, this implements the standard + # spectral element diagonal mass trick by returning a delta + # function. + # ''' + # if (derivative is None and isinstance(q.points, GaussLobattoPointSet) and + # q.length == self._fiat_element.space_dimension()): - i = indices.BasisFunctionIndex(self._fiat_element.space_dimension()) + # i = indices.BasisFunctionIndex(self._fiat_element.space_dimension()) - return Recipe(((), (i,), (q,)), Delta((i, q), 1.0)) + # return Recipe(((), (i,), (q,)), Delta((i, q), 1.0)) - else: - # Fall through to the default recipe. - return super(GaussLobatto, self).basis_evaluation(q, kernel_data, - derivative, pullback) + # else: + # # Fall through to the default recipe. + # return super(GaussLobatto, self).basis_evaluation(q, kernel_data, + # derivative, pullback) -class DiscontinuousLagrange(ScalarElement): +class DiscontinuousLagrange(FiatElement): def __init__(self, cell, degree): super(Lagrange, self).__init__(cell, degree) diff --git a/finat/indices.py b/finat/indices.py deleted file mode 100644 index b8e5721e3..000000000 --- a/finat/indices.py +++ /dev/null @@ -1,226 +0,0 @@ -from . import ast -import pymbolic.primitives as p -from pymbolic.mapper.stringifier import StringifyMapper -import math -from .points import TensorPointSet - -__all__ = ["PointIndex", "TensorPointIndex", "BasisFunctionIndex", - "TensorBasisFunctionIndex", - "SimpliciallyGradedBasisFunctionIndex", - "DimensionIndex", "flattened"] - - -def flattened(indices): - """Flatten an index or a tuple of indices into a tuple of scalar indices.""" - if isinstance(indices, IndexBase): - return indices.flattened - else: - return reduce(tuple.__add__, (flattened(i) for i in indices)) - - -class IndexBase(ast.Variable): - '''Base class for symbolic index objects.''' - def __init__(self, extent, name): - super(IndexBase, self).__init__(name) - if isinstance(extent, slice): - self._extent = extent - elif (isinstance(extent, int) or - isinstance(extent, p.Expression)): - self._extent = slice(extent) - else: - raise TypeError("Extent must be a slice or an int") - self._color = "yellow" - - self.start = self._extent.start - self.stop = self._extent.stop - self.step = self._extent.step - - @property - def extent(self): - '''A slice indicating the values this index can take.''' - return self._extent - - @property - def length(self): - '''The number of values this index can take.''' - start = self._extent.start or 0 - stop = self._extent.stop - step = self._extent.step or 1 - - return int(math.ceil((stop - start) / step)) - - @property - def as_range(self): - """Convert a slice to a range. If the range has expressions as bounds, - evaluate them. - """ - - return range(int(self._extent.start or 0), - int(self._extent.stop), - int(self._extent.step or 1)) - - @property - def _str_extent(self): - - return "%s=(%s:%s:%s)" % (str(self), - self._extent.start or 0, - self._extent.stop, - self._extent.step or 1) - - mapper_method = "map_index" - - def get_mapper_method(self, mapper): - - if isinstance(mapper, StringifyMapper): - return mapper.map_variable - else: - raise AttributeError() - - def __repr__(self): - - return "%s(%s)" % (self.__class__.__name__, self.name) - - def set_error(self): - self._error = True - - @property - def flattened(self): - """For tensor product indices, this returns their factors. In the - simple index case, this returns the 1-tuple of the list itself.""" - return (self,) - - -class PointIndexBase(object): - # Marker class for point indices. - pass - - -class PointIndex(IndexBase, PointIndexBase): - '''An index running over a set of points, for example quadrature points.''' - def __init__(self, pointset): - - self.points = pointset - - name = 'q_' + str(PointIndex._count) - PointIndex._count += 1 - - super(PointIndex, self).__init__(pointset.extent, name) - - _count = 0 - - -class TensorIndex(IndexBase): - """A mixin to create tensor product indices.""" - def __init__(self, factors): - - self.factors = factors - - name = "_x_".join(f.name for f in factors) - - super(TensorIndex, self).__init__(-1, name) - - @property - def flattened(self): - """Return the tuple of scalar indices of which this tensor index is made.""" - - return reduce(tuple.__add__, (f.flattened for f in self.factors)) - - -class TensorPointIndex(TensorIndex, PointIndexBase): - """An index running over a set of points which have a tensor product - structure. This index is actually composed of multiple factors.""" - def __init__(self, *args): - - if isinstance(args[0], TensorPointSet): - assert len(args) == 1 - - self.points = args[0] - - factors = [PointIndex(f) for f in args[0].factor_sets] - else: - factors = args - - super(TensorPointIndex, self).__init__(factors) - - def __getattr__(self, name): - - if name == "_error": - if any([hasattr(x, "_error") for x in self.factors]): - return True - - raise AttributeError - - -class BasisFunctionIndexBase(object): - # Marker class for point indices. - pass - - -class BasisFunctionIndex(BasisFunctionIndexBase, IndexBase): - '''An index over a local set of basis functions. - E.g. test functions on an element.''' - def __init__(self, extent): - - name = 'i_' + str(BasisFunctionIndex._count) - BasisFunctionIndex._count += 1 - - super(BasisFunctionIndex, self).__init__(extent, name) - - _count = 0 - - -class TensorBasisFunctionIndex(BasisFunctionIndexBase, TensorIndex): - """An index running over a set of basis functions which have a tensor - product structure. This index is actually composed of multiple - factors. - """ - def __init__(self, *args): - - super(TensorBasisFunctionIndex, self).__init__(args) - - def __getattr__(self, name): - - if name == "_error": - if any([hasattr(x, "_error") for x in self.factors]): - return True - - raise AttributeError - - -class SimpliciallyGradedBasisFunctionIndex(BasisFunctionIndex): - '''An index over simplicial polynomials with a grade, such - as Dubiner or Bernstein. Implies a simplicial iteration space.''' - def __init__(self, sdim, deg): - - # creates name and increments counter - super(SimpliciallyGradedBasisFunctionIndex, self).__init__(-1) - - self.factors = [BasisFunctionIndex(deg + 1)] - - def mysum(vals): - return reduce(lambda a, b: a + b, vals, 0) - - for sd in range(1, sdim): - acur = BasisFunctionIndex(deg + 1 - mysum(self.factors)) - self.factors.append(acur) - - def __getattr__(self, name): - - if name == "_error": - if any([hasattr(x, "_error") for x in self.factors]): - return True - - raise AttributeError - - -class DimensionIndex(IndexBase): - '''An index over data dimension. For example over topological, - geometric or vector components.''' - def __init__(self, extent): - - name = u'\u03B1_'.encode("utf-8") + str(DimensionIndex._count) - DimensionIndex._count += 1 - - super(DimensionIndex, self).__init__(extent, name) - - _count = 0 diff --git a/finat/points.py b/finat/points.py index 611af5a46..afac14c03 100644 --- a/finat/points.py +++ b/finat/points.py @@ -1,5 +1,4 @@ import numpy -from .ast import Variable class PointSetBase(object): @@ -38,20 +37,20 @@ def points(self): return self._points - def kernel_variable(self, name, kernel_data): - '''Produce a variable in the kernel data for this point set.''' - static_key = (id(self), ) + # def kernel_variable(self, name, kernel_data): + # '''Produce a variable in the kernel data for this point set.''' + # static_key = (id(self), ) - static_data = kernel_data.static + # static_data = kernel_data.static - if static_key in static_data: - w = static_data[static_key][0] - else: - w = Variable(name) - data = self._points - static_data[static_key] = (w, lambda: data) + # if static_key in static_data: + # w = static_data[static_key][0] + # else: + # w = Variable(name) + # data = self._points + # static_data[static_key] = (w, lambda: data) - return w + # return w def __getitem__(self, i): if isinstance(i, int): diff --git a/finat/product_elements.py b/finat/product_elements.py index 726af71e1..ce40395cd 100644 --- a/finat/product_elements.py +++ b/finat/product_elements.py @@ -1,8 +1,5 @@ """Preliminary support for tensor product elements.""" -from .finiteelementbase import ScalarElementMixin, FiniteElementBase -from .indices import TensorPointIndex, TensorBasisFunctionIndex, DimensionIndex -from .derivatives import grad -from .ast import Recipe, CompoundVector, IndexSum +from .finiteelementbase import FiniteElementBase from FIAT.reference_element import TensorProductCell @@ -10,7 +7,7 @@ class ProductElement(object): """Mixin class describing product elements.""" -class ScalarProductElement(ProductElement, ScalarElementMixin, FiniteElementBase): +class ScalarProductElement(ProductElement, FiniteElementBase): """A scalar-valued tensor product element.""" def __init__(self, *args): super(ScalarProductElement, self).__init__() @@ -26,57 +23,6 @@ def __init__(self, *args): self._cell = cellprod([a.cell for a in args]) - def basis_evaluation(self, q, kernel_data, derivative=None, - pullback=True): - '''Produce the variable for the tabulation of the basis - functions or their derivative.''' - - assert isinstance(q, TensorPointIndex) - - if derivative not in (None, grad): - raise ValueError( - "Scalar elements do not have a %s operation") % derivative - - phi = [e.basis_evaluation(q_, kernel_data) - for e, q_ in zip(self.factors, q.factors)] - - i_ = [phi_.indices[1][0] for phi_ in phi] - i = TensorBasisFunctionIndex(*i_) - - if derivative is grad: - - phi_d = [e.basis_evaluation(q_, kernel_data, derivative=grad, pullback=False) - for e, q_ in zip(self.factors, q.factors)] - - # Replace the basisfunctionindices on phi_d with i - phi_d = [p.replace_indices(zip(p.indices[1], (i__,))) - for p, i__ in zip(phi_d, i_)] - - expressions = tuple(reduce(lambda a, b: a.body * b.body, - phi[:d] + [phi_d[d]] + phi[d + 1:]) - for d in range(len(phi))) - - alpha_ = tuple(phi_.indices[0][0] for phi_ in phi_d) - alpha = DimensionIndex(sum(alpha__.length for alpha__ in alpha_)) - - assert alpha.length == kernel_data.gdim - expr = CompoundVector(alpha, alpha_, expressions) - - if pullback: - beta = alpha - alpha = DimensionIndex(kernel_data.gdim) - invJ = kernel_data.invJ[(beta, alpha)] - expr = IndexSum((beta,), invJ * expr) - - ind = ((alpha,), (i,), (q,)) - - else: - - ind = ((), (i,), (q,)) - expr = reduce(lambda a, b: a.body * b.body, phi) - - return Recipe(indices=ind, body=expr) - def __hash__(self): """ScalarProductElements are equal if their factors are equal""" diff --git a/finat/vectorfiniteelement.py b/finat/vectorfiniteelement.py index bc297ec5e..7c860fc37 100644 --- a/finat/vectorfiniteelement.py +++ b/finat/vectorfiniteelement.py @@ -1,7 +1,4 @@ from finiteelementbase import FiniteElementBase -from derivatives import div, grad, curl -from ast import Recipe, IndexSum, Delta, LeviCivita -import indices class VectorFiniteElement(FiniteElementBase): @@ -53,179 +50,7 @@ def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): \nabla\cdot\boldsymbol\phi_{(i \beta) q} = \nabla\phi_{\beta i q} """ - - # Produce the base scalar recipe. The scalar basis can only - # take a grad. For other derivatives, we need to do the - # transform here. - sr = self._base_element.basis_evaluation(q, kernel_data, - derivative and grad, - pullback) - phi = sr.body - d, b, p = sr.indices - - # Additional basis function index along the vector dimension. - beta = (indices.BasisFunctionIndex(self._dimension),) - - if derivative is div: - - return Recipe((d[:-1], b + beta, p), - sr.replace_indices({d[-1]: beta[0]}).body) - - elif derivative is curl: - if self.dimension == 2: - - return Recipe((d[:-1], b + beta, p), LeviCivita((2,) + beta, d[-1:], phi)) - elif self.dimension == 3: - alpha = (indices.DimensionIndex(self._dimension),) - - return Recipe((d[:-1] + alpha, b + beta, p), LeviCivita(alpha + beta, d[-1:], phi)) - else: - raise NotImplementedError - - else: - # Additional dimension index along the vector dimension. Note - # to self: is this the right order or does this index come - # after any derivative index? - alpha = (indices.DimensionIndex(self._dimension),) - - return Recipe((alpha + d, b + beta, p), Delta(alpha + beta, phi)) - - def field_evaluation(self, field_var, q, - kernel_data, derivative=None, pullback=True): - r"""Produce the recipe for the evaluation of a field f at a set of -points :math:`q`: - - .. math:: - \boldsymbol{f}_{\alpha q} = \sum_i f_{i \alpha}\phi_{i q} - - \nabla\boldsymbol{f}_{\alpha \beta q} = \sum_i f_{i \alpha}\nabla\phi_{\beta i q} - - \nabla\times\boldsymbol{f}_{q} = \epsilon_{2 \beta \gamma}\sum_{i} f_{i \beta}\nabla\phi_{\gamma i q} \qquad\textrm{(2D)} - - \nabla\times\boldsymbol{f}_{\alpha q} = \epsilon_{\alpha \beta \gamma}\sum_{i} f_{i \beta}\nabla\phi_{\gamma i q} \qquad\textrm{(3D)} - - \nabla\cdot\boldsymbol{f}_{q} = \sum_{i \alpha} f_{i \alpha}\nabla\phi_{\alpha i q} - """ - - kernel_data.kernel_args.add(field_var) - - # Produce the base scalar recipe. The scalar basis can only - # take a grad. For other derivatives, we need to do the - # transform here. - sr = self._base_element.basis_evaluation(q, kernel_data, - derivative and grad, - pullback) - phi = sr.body - d, b, p = sr.indices - - if derivative is div: - - expression = IndexSum(b + d[-1:], field_var[b + d[-1:]] * phi) - - return Recipe((d[:-1], (), p), expression) - - elif derivative is curl: - if self.dimension == 2: - beta = (indices.BasisFunctionIndex(self._dimension),) - - expression = LeviCivita((2,), beta + d[-1:], - IndexSum(b, field_var[b + beta] * phi)) - - return Recipe((d[:-1], (), p), expression) - elif self.dimension == 3: - # Additional basis function index along the vector dimension. - alpha = (indices.DimensionIndex(self._dimension),) - beta = (indices.BasisFunctionIndex(self._dimension),) - - expression = LeviCivita(alpha, beta + d[-1:], IndexSum(b, field_var[b + beta] * phi)) - - return Recipe((d[:-1] + alpha, (), p), expression) - else: - raise NotImplementedError - else: - # Additional basis function index along the vector dimension. - alpha = (indices.DimensionIndex(self._dimension),) - - expression = IndexSum(b, field_var[b + alpha] * phi) - - return Recipe((alpha + d, (), p), expression) - - def moment_evaluation(self, value, weights, q, - kernel_data, derivative=None, pullback=True): - r"""Produce the recipe for the evaluation of the moment of - :math:`u_{\alpha,q}` against a test function :math:`v_{\beta,q}`. - - .. math:: - \int \boldsymbol{u} \cdot \boldsymbol\phi_{(i \beta)}\ \mathrm{d}x = - \sum_q \boldsymbol{u}_{\beta q}\phi_{i q}w_q - - \int \boldsymbol{u}_{(\alpha \gamma)} \nabla \boldsymbol\phi_{(\alpha \gamma) (i \beta)}\ \mathrm{d}x = - \sum_{\gamma q} \boldsymbol{u}_{(\beta \gamma) q}\nabla\phi_{\gamma i q}w_q - - \int u \nabla \times \boldsymbol\phi_{(i \beta)}\ \mathrm{d}x = - \sum_{q} u_{q}\epsilon_{2\beta\gamma}\nabla\phi_{\gamma i q}w_q \qquad\textrm{(2D)} - - \int u_{\alpha} \nabla \times \boldsymbol\phi_{\alpha (i \beta)}\ \mathrm{d}x = - \sum_{\alpha q} u_{\alpha q}\epsilon_{\alpha\beta\gamma}\nabla\phi_{\gamma i q}w_q \qquad\textrm{(3D)} - - \int u \nabla \cdot \boldsymbol\phi_{(i \beta)}\ \mathrm{d}x = - \sum_{q} u_q\nabla\phi_{\beta i q}w_q - - Appropriate code is also generated where the value contains - trial functions. - """ - - # Produce the base scalar recipe. The scalar basis can only - # take a grad. For other derivatives, we need to do the - # transform here. - sr = self._base_element.basis_evaluation(q, kernel_data, - derivative and grad, - pullback) - - phi = sr.body - d, b, p = sr.indices - - beta = (indices.BasisFunctionIndex(self._dimension),) - - (d_, b_, p_) = value.indices - - w = weights.kernel_variable("w", kernel_data) - - if derivative is div: - beta = d[-1:] - - psi = value.replace_indices(zip(d_ + p_, d[:-1] + p)).body - - expression = IndexSum(d[:-1] + p, psi * phi * w[p]) - - elif derivative is curl: - if self.dimension == 2: - - beta = (indices.BasisFunctionIndex(self._dimension),) - gamma = d[-1:] - - psi = value.replace_indices((d_ + p_, d[:-1] + p)).body - - expression = IndexSum(p, psi * LeviCivita((2,) + beta, gamma, phi) * w[p]) - elif self.dimension == 3: - - alpha = d_[-1:] - beta = (indices.BasisFunctionIndex(self._dimension),) - gamma = d[-1:] - - psi = value.replace_indices((d_[:-1] + p_, d[:-1] + p)).body - - expression = IndexSum(alpha + p, psi * LeviCivita(alpha + beta, gamma, phi) * w[p]) - else: - raise NotImplementedError - else: - beta = (indices.BasisFunctionIndex(self._dimension),) - - psi = value.replace_indices(zip(d_ + p_, beta + d + p)).body - - expression = IndexSum(d + p, psi * phi * w[p]) - - return Recipe(((), b + beta + b_, ()), expression) + raise NotImplementedError def __hash__(self): """VectorFiniteElements are equal if they have the same base element From 1921359cd062415f8fa3dcdea50e1fb7130cc0ce Mon Sep 17 00:00:00 2001 From: David Ham Date: Fri, 8 Apr 2016 09:47:06 +0100 Subject: [PATCH 215/749] ignore fiat requirement for now --- finat/finiteelementbase.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index ee21d4eec..7fca7e6f0 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -53,16 +53,15 @@ def dofs_shape(self): raise NotImplementedError - def basis_evaluation(self, index, q, q_index, derivative=None, - pullback=None): + def basis_evaluation(self, index, q, q_index, entity=None, derivative=None): '''Return code for evaluating the element at known points on the reference element. :param index: the basis function index. :param q: the quadrature rule. :param q_index: the quadrature index. - :param derivative: the derivative to take of the test function. - :param pullback: whether to pull back to the reference cell. + :param entity: the cell entity on which to tabulate. + :param derivative: the derivative to take of the basis functions. ''' raise NotImplementedError From 80b81165070f02e150ca843026d0a87b24e760a8 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 8 Apr 2016 19:37:49 +0100 Subject: [PATCH 216/749] spin off gem from tsfc --- gem/__init__.py | 0 gem/gem.py | 506 ++++++++++++++++++++++++++++++++++++++++++++ gem/impero.py | 152 +++++++++++++ gem/impero_utils.py | 323 ++++++++++++++++++++++++++++ gem/interpreter.py | 305 ++++++++++++++++++++++++++ gem/node.py | 217 +++++++++++++++++++ gem/optimise.py | 95 +++++++++ gem/scheduling.py | 195 +++++++++++++++++ 8 files changed, 1793 insertions(+) create mode 100644 gem/__init__.py create mode 100644 gem/gem.py create mode 100644 gem/impero.py create mode 100644 gem/impero_utils.py create mode 100644 gem/interpreter.py create mode 100644 gem/node.py create mode 100644 gem/optimise.py create mode 100644 gem/scheduling.py diff --git a/gem/__init__.py b/gem/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/gem/gem.py b/gem/gem.py new file mode 100644 index 000000000..eb8dbbedc --- /dev/null +++ b/gem/gem.py @@ -0,0 +1,506 @@ +"""GEM is the intermediate language of TSFC for describing +tensor-valued mathematical expressions and tensor operations. +It is similar to Einstein's notation. + +Its design was heavily inspired by UFL, with some major differences: + - GEM has got nothing FEM-specific. + - In UFL free indices are just unrolled shape, thus UFL is very + restrictive about operations on expressions with different sets of + free indices. GEM is much more relaxed about free indices. + +Similarly to UFL, all GEM nodes have 'shape' and 'free_indices' +attributes / properties. Unlike UFL, however, index extents live on +the Index objects in GEM, not on all the nodes that have those free +indices. +""" + +from __future__ import absolute_import + +from itertools import chain +from numpy import asarray, unique + +from gem.node import Node as NodeBase + + +class NodeMeta(type): + """Metaclass of GEM nodes. + + When a GEM node is constructed, this metaclass automatically + collects its free indices if 'free_indices' has not been set yet. + """ + + def __call__(self, *args, **kwargs): + # Create and initialise object + obj = super(NodeMeta, self).__call__(*args, **kwargs) + + # Set free_indices if not set already + if not hasattr(obj, 'free_indices'): + cfi = list(chain(*[c.free_indices for c in obj.children])) + obj.free_indices = tuple(unique(cfi)) + + return obj + + +class Node(NodeBase): + """Abstract GEM node class.""" + + __metaclass__ = NodeMeta + + __slots__ = ('free_indices') + + def is_equal(self, other): + """Common subexpression eliminating equality predicate. + + When two (sub)expressions are equal, the children of one + object are reassigned to the children of the other, so some + duplicated subexpressions are eliminated. + """ + result = NodeBase.is_equal(self, other) + if result: + self.children = other.children + return result + + +class Terminal(Node): + """Abstract class for terminal GEM nodes.""" + + __slots__ = () + + children = () + + is_equal = NodeBase.is_equal + + +class Scalar(Node): + """Abstract class for scalar-valued GEM nodes.""" + + __slots__ = () + + shape = () + + +class Zero(Terminal): + """Symbolic zero tensor""" + + __slots__ = ('shape',) + __front__ = ('shape',) + + def __init__(self, shape=()): + self.shape = shape + + @property + def value(self): + assert not self.shape + return 0.0 + + +class Literal(Terminal): + """Tensor-valued constant""" + + __slots__ = ('array',) + __front__ = ('array',) + + def __new__(cls, array): + array = asarray(array) + if (array == 0).all(): + # All zeros, make symbolic zero + return Zero(array.shape) + else: + return super(Literal, cls).__new__(cls) + + def __init__(self, array): + self.array = asarray(array, dtype=float) + + def is_equal(self, other): + if type(self) != type(other): + return False + if self.shape != other.shape: + return False + return tuple(self.array.flat) == tuple(other.array.flat) + + def get_hash(self): + return hash((type(self), self.shape, tuple(self.array.flat))) + + @property + def value(self): + return float(self.array) + + @property + def shape(self): + return self.array.shape + + +class Variable(Terminal): + """Symbolic variable tensor""" + + __slots__ = ('name', 'shape') + __front__ = ('name', 'shape') + + def __init__(self, name, shape): + self.name = name + self.shape = shape + + +class Sum(Scalar): + __slots__ = ('children',) + + def __new__(cls, a, b): + assert not a.shape + assert not b.shape + + # Zero folding + if isinstance(a, Zero): + return b + elif isinstance(b, Zero): + return a + + self = super(Sum, cls).__new__(cls) + self.children = a, b + return self + + +class Product(Scalar): + __slots__ = ('children',) + + def __new__(cls, a, b): + assert not a.shape + assert not b.shape + + # Zero folding + if isinstance(a, Zero) or isinstance(b, Zero): + return Zero() + + self = super(Product, cls).__new__(cls) + self.children = a, b + return self + + +class Division(Scalar): + __slots__ = ('children',) + + def __new__(cls, a, b): + assert not a.shape + assert not b.shape + + # Zero folding + if isinstance(b, Zero): + raise ValueError("division by zero") + if isinstance(a, Zero): + return Zero() + + self = super(Division, cls).__new__(cls) + self.children = a, b + return self + + +class Power(Scalar): + __slots__ = ('children',) + + def __new__(cls, base, exponent): + assert not base.shape + assert not exponent.shape + + # Zero folding + if isinstance(base, Zero): + if isinstance(exponent, Zero): + raise ValueError("cannot solve 0^0") + return Zero() + elif isinstance(exponent, Zero): + return Literal(1) + + self = super(Power, cls).__new__(cls) + self.children = base, exponent + return self + + +class MathFunction(Scalar): + __slots__ = ('name', 'children') + __front__ = ('name',) + + def __init__(self, name, argument): + assert isinstance(name, str) + assert not argument.shape + + self.name = name + self.children = argument, + + +class MinValue(Scalar): + __slots__ = ('children',) + + def __init__(self, a, b): + assert not a.shape + assert not b.shape + + self.children = a, b + + +class MaxValue(Scalar): + __slots__ = ('children',) + + def __init__(self, a, b): + assert not a.shape + assert not b.shape + + self.children = a, b + + +class Comparison(Scalar): + __slots__ = ('operator', 'children') + __front__ = ('operator',) + + def __init__(self, op, a, b): + assert not a.shape + assert not b.shape + + if op not in [">", ">=", "==", "!=", "<", "<="]: + raise ValueError("invalid operator") + + self.operator = op + self.children = a, b + + +class LogicalNot(Scalar): + __slots__ = ('children',) + + def __init__(self, expression): + assert not expression.shape + + self.children = expression, + + +class LogicalAnd(Scalar): + __slots__ = ('children',) + + def __init__(self, a, b): + assert not a.shape + assert not b.shape + + self.children = a, b + + +class LogicalOr(Scalar): + __slots__ = ('children',) + + def __init__(self, a, b): + assert not a.shape + assert not b.shape + + self.children = a, b + + +class Conditional(Node): + __slots__ = ('children', 'shape') + + def __init__(self, condition, then, else_): + assert not condition.shape + assert then.shape == else_.shape + + self.children = condition, then, else_ + self.shape = then.shape + + +class Index(object): + """Free index""" + + # Not true object count, just for naming purposes + _count = 0 + + __slots__ = ('name', 'extent', 'count') + + def __init__(self, name=None): + self.name = name + Index._count += 1 + self.count = Index._count + # Initialise with indefinite extent + self.extent = None + + def set_extent(self, value): + # Set extent, check for consistency + if self.extent is None: + self.extent = value + elif self.extent != value: + raise ValueError("Inconsistent index extents!") + + def __str__(self): + if self.name is None: + return "i_%d" % self.count + return self.name + + def __repr__(self): + if self.name is None: + return "Index(%r)" % self.count + return "Index(%r)" % self.name + + +class VariableIndex(object): + """An index that is constant during a single execution of the + kernel, but whose value is not known at compile time.""" + + def __init__(self, expression): + assert isinstance(expression, Node) + assert not expression.free_indices + assert not expression.shape + self.expression = expression + + def __eq__(self, other): + if self is other: + return True + if type(self) is not type(other): + return False + return self.expression == other.expression + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash((VariableIndex, self.expression)) + + +class Indexed(Scalar): + __slots__ = ('children', 'multiindex') + __back__ = ('multiindex',) + + def __new__(cls, aggregate, multiindex): + # Set index extents from shape + assert len(aggregate.shape) == len(multiindex) + for index, extent in zip(multiindex, aggregate.shape): + if isinstance(index, Index): + index.set_extent(extent) + + # Zero folding + if isinstance(aggregate, Zero): + return Zero() + + # All indices fixed + if all(isinstance(i, int) for i in multiindex): + if isinstance(aggregate, Literal): + return Literal(aggregate.array[multiindex]) + elif isinstance(aggregate, ListTensor): + return aggregate.array[multiindex] + + self = super(Indexed, cls).__new__(cls) + self.children = (aggregate,) + self.multiindex = multiindex + + new_indices = tuple(i for i in multiindex if isinstance(i, Index)) + self.free_indices = tuple(unique(aggregate.free_indices + new_indices)) + + return self + + +class ComponentTensor(Node): + __slots__ = ('children', 'multiindex', 'shape') + __back__ = ('multiindex',) + + def __new__(cls, expression, multiindex): + assert not expression.shape + + # Collect shape + shape = tuple(index.extent for index in multiindex) + assert all(shape) + + # Zero folding + if isinstance(expression, Zero): + return Zero(shape) + + self = super(ComponentTensor, cls).__new__(cls) + self.children = (expression,) + self.multiindex = multiindex + self.shape = shape + + # Collect free indices + assert set(multiindex) <= set(expression.free_indices) + self.free_indices = tuple(set(expression.free_indices) - set(multiindex)) + + return self + + +class IndexSum(Scalar): + __slots__ = ('children', 'index') + __back__ = ('index',) + + def __new__(cls, summand, index): + # Sum zeros + assert not summand.shape + if isinstance(summand, Zero): + return summand + + # Sum a single expression + if index.extent == 1: + return Indexed(ComponentTensor(summand, (index,)), (0,)) + + self = super(IndexSum, cls).__new__(cls) + self.children = (summand,) + self.index = index + + # Collect shape and free indices + assert index in summand.free_indices + self.free_indices = tuple(set(summand.free_indices) - {index}) + + return self + + +class ListTensor(Node): + __slots__ = ('array',) + + def __new__(cls, array): + array = asarray(array) + + # Zero folding + if all(isinstance(elem, Zero) for elem in array.flat): + assert all(elem.shape == () for elem in array.flat) + return Zero(array.shape) + + self = super(ListTensor, cls).__new__(cls) + self.array = array + return self + + @property + def children(self): + return tuple(self.array.flat) + + @property + def shape(self): + return self.array.shape + + def reconstruct(self, *args): + return ListTensor(asarray(args).reshape(self.array.shape)) + + def __repr__(self): + return "ListTensor(%r)" % self.array.tolist() + + def is_equal(self, other): + """Common subexpression eliminating equality predicate.""" + if type(self) != type(other): + return False + if (self.array == other.array).all(): + self.array = other.array + return True + return False + + def get_hash(self): + return hash((type(self), self.shape, self.children)) + + +def partial_indexed(tensor, indices): + """Generalised indexing into a tensor. The number of indices may + be less than or equal to the rank of the tensor, so the result may + have a non-empty shape. + + :arg tensor: tensor-valued GEM expression + :arg indices: indices, at most as many as the rank of the tensor + :returns: a potentially tensor-valued expression + """ + if len(indices) == 0: + return tensor + elif len(indices) < len(tensor.shape): + rank = len(tensor.shape) - len(indices) + shape_indices = tuple(Index() for i in range(rank)) + return ComponentTensor( + Indexed(tensor, indices + shape_indices), + shape_indices) + elif len(indices) == len(tensor.shape): + return Indexed(tensor, indices) + else: + raise ValueError("More indices than rank!") diff --git a/gem/impero.py b/gem/impero.py new file mode 100644 index 000000000..fb662e1d9 --- /dev/null +++ b/gem/impero.py @@ -0,0 +1,152 @@ +"""Impero is a helper AST for generating C code (or equivalent, +e.g. COFFEE) from GEM. An Impero expression is a proper tree, not +directed acyclic graph (DAG). Impero is a helper AST, not a +standalone language; it is incomplete without GEM as its terminals +refer to nodes from GEM expressions. + +Trivia: + - Impero helps translating GEM into an imperative language. + - Byzantine units in Age of Empires II sometimes say 'Impero?' + (Command?) after clicking on them. +""" + +from __future__ import absolute_import + +from abc import ABCMeta, abstractmethod + +from gem.node import Node as NodeBase + + +class Node(NodeBase): + """Base class of all Impero nodes""" + + __slots__ = () + + +class Terminal(Node): + """Abstract class for terminal Impero nodes""" + + __metaclass__ = ABCMeta + + __slots__ = () + + children = () + + @abstractmethod + def loop_shape(self, free_indices): + """Gives the loop shape, an ordering of indices for an Impero + terminal. + + :arg free_indices: a callable mapping of GEM expressions to + ordered free indices. + """ + pass + + +class Evaluate(Terminal): + """Assign the value of a GEM expression to a temporary.""" + + __slots__ = ('expression',) + __front__ = ('expression',) + + def __init__(self, expression): + self.expression = expression + + def loop_shape(self, free_indices): + return free_indices(self.expression) + + +class Initialise(Terminal): + """Initialise an :class:`gem.IndexSum`.""" + + __slots__ = ('indexsum',) + __front__ = ('indexsum',) + + def __init__(self, indexsum): + self.indexsum = indexsum + + def loop_shape(self, free_indices): + return free_indices(self.indexsum) + + +class Accumulate(Terminal): + """Accumulate terms into an :class:`gem.IndexSum`.""" + + __slots__ = ('indexsum',) + __front__ = ('indexsum',) + + def __init__(self, indexsum): + self.indexsum = indexsum + + def loop_shape(self, free_indices): + return free_indices(self.indexsum.children[0]) + + +class Noop(Terminal): + """No-op terminal. Does not generate code, but wraps a GEM + expression to have a loop shape, thus affects loop fusion.""" + + __slots__ = ('expression',) + __front__ = ('expression',) + + def __init__(self, expression): + self.expression = expression + + def loop_shape(self, free_indices): + return free_indices(self.expression) + + +class Return(Terminal): + """Save value of GEM expression into an lvalue. Used to "return" + values from a kernel.""" + + __slots__ = ('variable', 'expression') + __front__ = ('variable', 'expression') + + def __init__(self, variable, expression): + assert set(variable.free_indices) >= set(expression.free_indices) + + self.variable = variable + self.expression = expression + + def loop_shape(self, free_indices): + return free_indices(self.variable) + + +class ReturnAccumulate(Terminal): + """Accumulate an :class:`gem.IndexSum` directly into a return + variable.""" + + __slots__ = ('variable', 'indexsum') + __front__ = ('variable', 'indexsum') + + def __init__(self, variable, indexsum): + assert set(variable.free_indices) == set(indexsum.free_indices) + + self.variable = variable + self.indexsum = indexsum + + def loop_shape(self, free_indices): + return free_indices(self.indexsum.children[0]) + + +class Block(Node): + """An ordered set of Impero expressions. Corresponds to a curly + braces block in C.""" + + __slots__ = ('children',) + + def __init__(self, statements): + self.children = tuple(statements) + + +class For(Node): + """For loop with an index which stores its extent, and a loop body + expression which is usually a :class:`Block`.""" + + __slots__ = ('index', 'children') + __front__ = ('index',) + + def __init__(self, index, statement): + self.index = index + self.children = (statement,) diff --git a/gem/impero_utils.py b/gem/impero_utils.py new file mode 100644 index 000000000..65ead3159 --- /dev/null +++ b/gem/impero_utils.py @@ -0,0 +1,323 @@ +"""Utilities for building an Impero AST from an ordered list of +terminal Impero operations, and for building any additional data +required for straightforward C code generation. + +What this module does is independent of whether we eventually generate +C code or a COFFEE AST. +""" + +from __future__ import absolute_import + +import collections +import itertools + +import numpy +from singledispatch import singledispatch + +from gem.node import traversal, collect_refcount +from gem import gem, impero as imp, optimise, scheduling + + +# ImperoC is named tuple for C code generation. +# +# Attributes: +# tree - Impero AST describing the loop structure and operations +# temporaries - List of GEM expressions which have assigned temporaries +# declare - Where to declare temporaries to get correct C code +# indices - Indices for declarations and referencing values +ImperoC = collections.namedtuple('ImperoC', ['tree', 'temporaries', 'declare', 'indices']) + + +class NoopError(Exception): + """No operations in the kernel.""" + pass + + +def compile_gem(return_variables, expressions, prefix_ordering, remove_zeros=False, coffee_licm=False): + """Compiles GEM to Impero. + + :arg return_variables: return variables for each root (type: GEM expressions) + :arg expressions: multi-root expression DAG (type: GEM expressions) + :arg prefix_ordering: outermost loop indices + :arg remove_zeros: remove zero assignment to return variables + :arg coffee_licm: trust COFFEE to do loop invariant code motion + """ + expressions = optimise.remove_componenttensors(expressions) + + # Remove zeros + if remove_zeros: + rv = [] + es = [] + for var, expr in zip(return_variables, expressions): + if not isinstance(expr, gem.Zero): + rv.append(var) + es.append(expr) + return_variables, expressions = rv, es + + # Collect indices in a deterministic order + indices = [] + for node in traversal(expressions): + if isinstance(node, gem.Indexed): + indices.extend(node.multiindex) + # The next two lines remove duplicate elements from the list, but + # preserve the ordering, i.e. all elements will appear only once, + # in the order of their first occurance in the original list. + _, unique_indices = numpy.unique(indices, return_index=True) + indices = numpy.asarray(indices)[numpy.sort(unique_indices)] + + # Build ordered index map + index_ordering = make_prefix_ordering(indices, prefix_ordering) + apply_ordering = make_index_orderer(index_ordering) + + get_indices = lambda expr: apply_ordering(expr.free_indices) + + # Build operation ordering + ops = scheduling.emit_operations(zip(return_variables, expressions), get_indices) + + # Empty kernel + if len(ops) == 0: + raise NoopError() + + # Drop unnecessary temporaries + ops = inline_temporaries(expressions, ops, coffee_licm=coffee_licm) + + # Build Impero AST + tree = make_loop_tree(ops, get_indices) + + # Collect temporaries + temporaries = collect_temporaries(ops) + + # Determine declarations + declare, indices = place_declarations(ops, tree, temporaries, get_indices) + + # Prepare ImperoC (Impero AST + other data for code generation) + return ImperoC(tree, temporaries, declare, indices) + + +def make_prefix_ordering(indices, prefix_ordering): + """Creates an ordering of ``indices`` which starts with those + indices in ``prefix_ordering``.""" + # Need to return deterministically ordered indices + return tuple(prefix_ordering) + tuple(k for k in indices if k not in prefix_ordering) + + +def make_index_orderer(index_ordering): + """Returns a function which given a set of indices returns those + indices in the order as they appear in ``index_ordering``.""" + idx2pos = {idx: pos for pos, idx in enumerate(index_ordering)} + + def apply_ordering(indices): + return tuple(sorted(indices, key=lambda i: idx2pos[i])) + return apply_ordering + + +def inline_temporaries(expressions, ops, coffee_licm=False): + """Inline temporaries which could be inlined without blowing up + the code. + + :arg expressions: a multi-root GEM expression DAG, used for + reference counting + :arg ops: ordered list of Impero terminals + :arg coffee_licm: Trust COFFEE to do LICM. If enabled, inlining + can move calculations inside inner loops. + :returns: a filtered ``ops``, without the unnecessary + :class:`impero.Evaluate`s + """ + refcount = collect_refcount(expressions) + + candidates = set() # candidates for inlining + for op in ops: + if isinstance(op, imp.Evaluate): + expr = op.expression + if expr.shape == () and refcount[expr] == 1: + candidates.add(expr) + + if not coffee_licm: + # Prevent inlining that pulls expressions into inner loops + for node in traversal(expressions): + for child in node.children: + if child in candidates and set(child.free_indices) < set(node.free_indices): + candidates.remove(child) + + # Filter out candidates + return [op for op in ops if not (isinstance(op, imp.Evaluate) and op.expression in candidates)] + + +def collect_temporaries(ops): + """Collects GEM expressions to assign to temporaries from a list + of Impero terminals.""" + result = [] + for op in ops: + # IndexSum temporaries should be added either at Initialise or + # at Accumulate. The difference is only in ordering + # (numbering). We chose Accumulate here. + if isinstance(op, imp.Accumulate): + result.append(op.indexsum) + elif isinstance(op, imp.Evaluate): + result.append(op.expression) + return result + + +def make_loop_tree(ops, get_indices, level=0): + """Creates an Impero AST with loops from a list of operations and + their respective free indices. + + :arg ops: a list of Impero terminal nodes + :arg get_indices: callable mapping from GEM nodes to an ordering + of free indices + :arg level: depth of loop nesting + :returns: Impero AST with loops, without declarations + """ + keyfunc = lambda op: op.loop_shape(get_indices)[level:level+1] + statements = [] + for first_index, op_group in itertools.groupby(ops, keyfunc): + if first_index: + inner_block = make_loop_tree(op_group, get_indices, level+1) + statements.append(imp.For(first_index[0], inner_block)) + else: + statements.extend(op_group) + # Remove no-op terminals from the tree + statements = filter(lambda s: not isinstance(s, imp.Noop), statements) + return imp.Block(statements) + + +def place_declarations(ops, tree, temporaries, get_indices): + """Determines where and how to declare temporaries for an Impero AST. + + :arg ops: terminals of ``tree`` + :arg tree: Impero AST to determine the declarations for + :arg temporaries: list of GEM expressions which are assigned to + temporaries + :arg get_indices: callable mapping from GEM nodes to an ordering + of free indices + """ + temporaries_set = set(temporaries) + assert len(temporaries_set) == len(temporaries) + + # Collect the total number of temporary references + total_refcount = collections.Counter() + for op in ops: + total_refcount.update(temp_refcount(temporaries_set, op)) + assert temporaries_set == set(total_refcount) + + # Result + declare = {} + indices = {} + + @singledispatch + def recurse(expr, loop_indices): + """Visit an Impero AST to collect declarations. + + :arg expr: Impero tree node + :arg loop_indices: loop indices (in order) from the outer + loops surrounding ``expr`` + :returns: :class:`collections.Counter` with the reference + counts for each temporary in the subtree whose root + is ``expr`` + """ + return AssertionError("unsupported expression type %s" % type(expr)) + + @recurse.register(imp.Terminal) + def recurse_terminal(expr, loop_indices): + return temp_refcount(temporaries_set, expr) + + @recurse.register(imp.For) + def recurse_for(expr, loop_indices): + return recurse(expr.children[0], loop_indices + (expr.index,)) + + @recurse.register(imp.Block) + def recurse_block(expr, loop_indices): + # Temporaries declared at the beginning of the block are + # collected here + declare[expr] = [] + + # Collect reference counts for the block + refcount = collections.Counter() + for statement in expr.children: + refcount.update(recurse(statement, loop_indices)) + + # Visit :class:`collections.Counter` in deterministic order + for e in sorted(refcount.keys(), key=temporaries.index): + if refcount[e] == total_refcount[e]: + # If all references are within this block, then this + # block is the right place to declare the temporary. + assert loop_indices == get_indices(e)[:len(loop_indices)] + indices[e] = get_indices(e)[len(loop_indices):] + if indices[e]: + # Scalar-valued temporaries are not declared until + # their value is assigned. This does not really + # matter, but produces a more compact and nicer to + # read C code. + declare[expr].append(e) + # Remove expression from the ``refcount`` so it will + # not be declared again. + del refcount[e] + return refcount + + # Populate result + remainder = recurse(tree, ()) + assert not remainder + + # Set in ``declare`` for Impero terminals whether they should + # declare the temporary that they are writing to. + for op in ops: + declare[op] = False + if isinstance(op, imp.Evaluate): + e = op.expression + elif isinstance(op, imp.Initialise): + e = op.indexsum + else: + continue + + if len(indices[e]) == 0: + declare[op] = True + + return declare, indices + + +def temp_refcount(temporaries, op): + """Collects the number of times temporaries are referenced when + generating code for an Impero terminal. + + :arg temporaries: set of temporaries + :arg op: Impero terminal + :returns: :class:`collections.Counter` object mapping some of + elements from ``temporaries`` to the number of times + they will referenced from ``op`` + """ + counter = collections.Counter() + + def recurse(o): + """Traverses expression until reaching temporaries, counting + temporary references.""" + if o in temporaries: + counter[o] += 1 + else: + for c in o.children: + recurse(c) + + def recurse_top(o): + """Traverses expression until reaching temporaries, counting + temporary references. Always descends into children at least + once, even when the root is a temporary.""" + if o in temporaries: + counter[o] += 1 + for c in o.children: + recurse(c) + + if isinstance(op, imp.Initialise): + counter[op.indexsum] += 1 + elif isinstance(op, imp.Accumulate): + recurse_top(op.indexsum) + elif isinstance(op, imp.Evaluate): + recurse_top(op.expression) + elif isinstance(op, imp.Return): + recurse(op.expression) + elif isinstance(op, imp.ReturnAccumulate): + recurse(op.indexsum.children[0]) + elif isinstance(op, imp.Noop): + pass + else: + raise AssertionError("unhandled operation: %s" % type(op)) + + return counter diff --git a/gem/interpreter.py b/gem/interpreter.py new file mode 100644 index 000000000..1ec064eb5 --- /dev/null +++ b/gem/interpreter.py @@ -0,0 +1,305 @@ +""" +An interpreter for GEM trees. +""" +from __future__ import absolute_import + +import numpy +import operator +import math +from singledispatch import singledispatch +import itertools + +from gem import gem, node + +__all__ = ("evaluate", ) + + +class Result(object): + """An array object that tracks which axes of the array correspond to + gem free indices (and what those free indices are). + + :arg arr: The array. + :arg fids: The free indices. + + The first ``len(fids)`` axes of the provided array correspond to + the free indices, the remaining axes are the shape of each entry. + """ + def __init__(self, arr, fids=None): + self.arr = arr + self.fids = fids if fids is not None else () + + def filter(self, idx, fids): + """Given an index tuple and some free indices, return a + "filtered" index tuple which removes entries that correspond + to indices in fids that are not in ``self.fids``. + + :arg idx: The index tuple to filter. + :arg fids: The free indices for the index tuple. + """ + return tuple(idx[fids.index(i)] for i in self.fids) + idx[len(fids):] + + def __getitem__(self, idx): + return self.arr[idx] + + def __setitem__(self, idx, val): + self.arr[idx] = val + + @property + def tshape(self): + """The total shape of the result array.""" + return self.arr.shape + + @property + def fshape(self): + """The shape of the free index part of the result array.""" + return self.tshape[:len(self.fids)] + + @property + def shape(self): + """The shape of the shape part of the result array.""" + return self.tshape[len(self.fids):] + + def __repr__(self): + return "Result(%r, %r)" % (self.arr, self.fids) + + def __str__(self): + return repr(self) + + @classmethod + def empty(cls, *children, **kwargs): + """Build an empty Result object. + + :arg children: The children used to determine the shape and + free indices. + :kwarg dtype: The data type of the result array. + """ + dtype = kwargs.get("dtype", float) + assert all(children[0].shape == c.shape for c in children) + fids = [] + for f in itertools.chain(*(c.fids for c in children)): + if f not in fids: + fids.append(f) + shape = tuple(i.extent for i in fids) + children[0].shape + return cls(numpy.empty(shape, dtype=dtype), tuple(fids)) + + +@singledispatch +def _evaluate(expression, self): + """Evaluate an expression using a provided callback handler. + + :arg expression: The expression to evaluation. + :arg self: The callback handler (should provide bindings). + """ + raise ValueError("Unhandled node type %s" % type(expression)) + + +@_evaluate.register(gem.Zero) # noqa: not actually redefinition +def _(e, self): + """Zeros produce an array of zeros.""" + return Result(numpy.zeros(e.shape, dtype=float)) + + +@_evaluate.register(gem.Literal) # noqa: not actually redefinition +def _(e, self): + """Literals return their array.""" + return Result(e.array) + + +@_evaluate.register(gem.Variable) # noqa: not actually redefinition +def _(e, self): + """Look up variables in the provided bindings.""" + try: + val = self.bindings[e] + except KeyError: + raise ValueError("Binding for %s not found" % e) + if val.shape != e.shape: + raise ValueError("Binding for %s has wrong shape. %s, not %s." % + (e, val.shape, e.shape)) + return Result(val) + + +@_evaluate.register(gem.Power) # noqa: not actually redefinition +@_evaluate.register(gem.Division) +@_evaluate.register(gem.Product) +@_evaluate.register(gem.Sum) +def _(e, self): + op = {gem.Product: operator.mul, + gem.Division: operator.div, + gem.Sum: operator.add, + gem.Power: operator.pow}[type(e)] + + a, b = [self(o) for o in e.children] + result = Result.empty(a, b) + fids = result.fids + for idx in numpy.ndindex(result.tshape): + result[idx] = op(a[a.filter(idx, fids)], b[b.filter(idx, fids)]) + return result + + +@_evaluate.register(gem.MathFunction) # noqa: not actually redefinition +def _(e, self): + ops = [self(o) for o in e.children] + result = Result.empty(*ops) + names = {"abs": abs, + "log": math.log} + op = names[e.name] + for idx in numpy.ndindex(result.tshape): + result[idx] = op(*(o[o.filter(idx, result.fids)] for o in ops)) + return result + + +@_evaluate.register(gem.MaxValue) # noqa: not actually redefinition +@_evaluate.register(gem.MinValue) +def _(e, self): + ops = [self(o) for o in e.children] + result = Result.empty(*ops) + op = {gem.MinValue: min, + gem.MaxValue: max}[type(e)] + for idx in numpy.ndindex(result.tshape): + result[idx] = op(*(o[o.filter(idx, result.fids)] for o in ops)) + return result + + +@_evaluate.register(gem.Comparison) # noqa: not actually redefinition +def _(e, self): + ops = [self(o) for o in e.children] + op = {">": operator.gt, + ">=": operator.ge, + "==": operator.eq, + "!=": operator.ne, + "<": operator.lt, + "<=": operator.le}[e.operator] + result = Result.empty(*ops, dtype=bool) + for idx in numpy.ndindex(result.tshape): + result[idx] = op(*(o[o.filter(idx, result.fids)] for o in ops)) + return result + + +@_evaluate.register(gem.LogicalNot) # noqa: not actually redefinition +def _(e, self): + val = self(e.children[0]) + assert val.arr.dtype == numpy.dtype("bool") + result = Result.empty(val, bool) + for idx in numpy.ndindex(result.tshape): + result[idx] = not val[val.filter(idx, result.fids)] + return result + + +@_evaluate.register(gem.LogicalAnd) # noqa: not actually redefinition +def _(e, self): + a, b = [self(o) for o in e.children] + assert a.arr.dtype == numpy.dtype("bool") + assert b.arr.dtype == numpy.dtype("bool") + result = Result.empty(a, b, bool) + for idx in numpy.ndindex(result.tshape): + result[idx] = a[a.filter(idx, result.fids)] and \ + b[b.filter(idx, result.fids)] + return result + + +@_evaluate.register(gem.LogicalOr) # noqa: not actually redefinition +def _(e, self): + a, b = [self(o) for o in e.children] + assert a.arr.dtype == numpy.dtype("bool") + assert b.arr.dtype == numpy.dtype("bool") + result = Result.empty(a, b, dtype=bool) + for idx in numpy.ndindex(result.tshape): + result[idx] = a[a.filter(idx, result.fids)] or \ + b[b.filter(idx, result.fids)] + return result + + +@_evaluate.register(gem.Conditional) # noqa: not actually redefinition +def _(e, self): + cond, then, else_ = [self(o) for o in e.children] + assert cond.arr.dtype == numpy.dtype("bool") + result = Result.empty(cond, then, else_) + for idx in numpy.ndindex(result.tshape): + if cond[cond.filter(idx, result.fids)]: + result[idx] = then[then.filter(idx, result.fids)] + else: + result[idx] = else_[else_.filter(idx, result.fids)] + return result + + +@_evaluate.register(gem.Indexed) # noqa: not actually redefinition +def _(e, self): + """Indexing maps shape to free indices""" + val = self(e.children[0]) + fids = tuple(i for i in e.multiindex if isinstance(i, gem.Index)) + + idx = [] + # First pick up all the existing free indices + for _ in val.fids: + idx.append(Ellipsis) + # Now grab the shape axes + for i in e.multiindex: + if isinstance(i, gem.Index): + # Free index, want entire extent + idx.append(Ellipsis) + elif isinstance(i, gem.VariableIndex): + # Variable index, evaluate inner expression + result, = self(i.expression) + assert not result.tshape + idx.append(result[()]) + else: + # Fixed index, just pick that value + idx.append(i) + assert len(idx) == len(val.tshape) + return Result(val[idx], val.fids + fids) + + +@_evaluate.register(gem.ComponentTensor) # noqa: not actually redefinition +def _(e, self): + """Component tensors map free indices to shape.""" + val = self(e.children[0]) + axes = [] + fids = [] + # First grab the free indices that aren't bound + for a, f in enumerate(val.fids): + if f not in e.multiindex: + axes.append(a) + fids.append(f) + # Now the bound free indices + for i in e.multiindex: + axes.append(val.fids.index(i)) + # Now the existing shape + axes.extend(range(len(val.fshape), len(val.tshape))) + return Result(numpy.transpose(val.arr, axes=axes), + tuple(fids)) + + +@_evaluate.register(gem.IndexSum) # noqa: not actually redefinition +def _(e, self): + """Index sums reduce over the given axis.""" + val = self(e.children[0]) + idx = val.fids.index(e.index) + return Result(val.arr.sum(axis=idx), + val.fids[:idx] + val.fids[idx+1:]) + + +@_evaluate.register(gem.ListTensor) # noqa: not actually redefinition +def _(e, self): + """List tensors just turn into arrays.""" + ops = [self(o) for o in e.children] + assert all(ops[0].fids == o.fids for o in ops) + return Result(numpy.asarray([o.arr for o in ops]).reshape(e.shape), + ops[0].fids) + + +def evaluate(expressions, bindings=None): + """Evaluate some GEM expressions given variable bindings. + + :arg expressions: A single GEM expression, or iterable of + expressions to evaluate. + :kwarg bindings: An optional dict mapping GEM :class:`gem.Variable` + nodes to data. + :returns: a list of the evaluated expressions. + """ + try: + exprs = tuple(expressions) + except TypeError: + exprs = (expressions, ) + mapper = node.Memoizer(_evaluate) + mapper.bindings = bindings if bindings is not None else {} + return map(mapper, exprs) diff --git a/gem/node.py b/gem/node.py new file mode 100644 index 000000000..d1c72cc71 --- /dev/null +++ b/gem/node.py @@ -0,0 +1,217 @@ +"""Generic abstract node class and utility functions for creating +expression DAG languages.""" + +from __future__ import absolute_import + +import collections + + +class Node(object): + """Abstract node class. + + Nodes are not meant to be modified. + + A node can reference other nodes; they are called children. A node + might contain data, or reference other objects which are not + themselves nodes; they are not called children. + + Both the children (if any) and non-child data (if any) are + required to create a node, or determine the equality of two + nodes. For reconstruction, however, only the new children are + necessary. + """ + + __slots__ = ('hash_value',) + + # Non-child data as the first arguments of the constructor. + # To be (potentially) overridden by derived node classes. + __front__ = () + + # Non-child data as the last arguments of the constructor. + # To be (potentially) overridden by derived node classes. + __back__ = () + + def __getinitargs__(self, children): + """Constructs an argument list for the constructor with + non-child data from 'self' and children from 'children'. + + Internally used utility function. + """ + front_args = [getattr(self, name) for name in self.__front__] + back_args = [getattr(self, name) for name in self.__back__] + + return tuple(front_args) + tuple(children) + tuple(back_args) + + def reconstruct(self, *args): + """Reconstructs the node with new children from + 'args'. Non-child data are copied from 'self'. + + Returns a new object. + """ + return type(self)(*self.__getinitargs__(args)) + + def __repr__(self): + init_args = self.__getinitargs__(self.children) + return "%s(%s)" % (type(self).__name__, ", ".join(map(repr, init_args))) + + def __eq__(self, other): + """Provides equality testing with quick positive and negative + paths based on :func:`id` and :meth:`__hash__`. + """ + if self is other: + return True + elif hash(self) != hash(other): + return False + else: + return self.is_equal(other) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + """Provides caching for hash values.""" + try: + return self.hash_value + except AttributeError: + self.hash_value = self.get_hash() + return self.hash_value + + def is_equal(self, other): + """Equality predicate. + + This is the method to potentially override in derived classes, + not :meth:`__eq__` or :meth:`__ne__`. + """ + if type(self) != type(other): + return False + self_initargs = self.__getinitargs__(self.children) + other_initargs = other.__getinitargs__(other.children) + return self_initargs == other_initargs + + def get_hash(self): + """Hash function. + + This is the method to potentially override in derived classes, + not :meth:`__hash__`. + """ + return hash((type(self),) + self.__getinitargs__(self.children)) + + +def traversal(expression_dags): + """Pre-order traversal of the nodes of expression DAGs.""" + seen = set() + lifo = [] + # Some roots might be same, but they must be visited only once. + # Keep the original ordering of roots, for deterministic code + # generation. + for root in expression_dags: + if root not in seen: + seen.add(root) + lifo.append(root) + + while lifo: + node = lifo.pop() + yield node + for child in node.children: + if child not in seen: + seen.add(child) + lifo.append(child) + + +def collect_refcount(expression_dags): + """Collects reference counts for a multi-root expression DAG.""" + result = collections.Counter(expression_dags) + for node in traversal(expression_dags): + result.update(node.children) + return result + + +def noop_recursive(function): + """No-op wrapper for functions with overridable recursive calls. + + :arg function: a function with parameters (value, rec), where + ``rec`` is expected to be a function used for + recursive calls. + :returns: a function with working recursion and nothing fancy + """ + def recursive(node): + return function(node, recursive) + return recursive + + +def noop_recursive_arg(function): + """No-op wrapper for functions with overridable recursive calls + and an argument. + + :arg function: a function with parameters (value, rec, arg), where + ``rec`` is expected to be a function used for + recursive calls. + :returns: a function with working recursion and nothing fancy + """ + def recursive(node, arg): + return function(node, recursive, arg) + return recursive + + +class Memoizer(object): + """Caching wrapper for functions with overridable recursive calls. + The lifetime of the cache is the lifetime of the object instance. + + :arg function: a function with parameters (value, rec), where + ``rec`` is expected to be a function used for + recursive calls. + :returns: a function with working recursion and caching + """ + def __init__(self, function): + self.cache = {} + self.function = function + + def __call__(self, node): + try: + return self.cache[node] + except KeyError: + result = self.function(node, self) + self.cache[node] = result + return result + + +class MemoizerArg(object): + """Caching wrapper for functions with overridable recursive calls + and an argument. The lifetime of the cache is the lifetime of the + object instance. + + :arg function: a function with parameters (value, rec, arg), where + ``rec`` is expected to be a function used for + recursive calls. + :returns: a function with working recursion and caching + """ + def __init__(self, function): + self.cache = {} + self.function = function + + def __call__(self, node, arg): + cache_key = (node, arg) + try: + return self.cache[cache_key] + except KeyError: + result = self.function(node, self, arg) + self.cache[cache_key] = result + return result + + +def reuse_if_untouched(node, self): + """Reuse if untouched recipe""" + new_children = map(self, node.children) + if all(nc == c for nc, c in zip(new_children, node.children)): + return node + else: + return node.reconstruct(*new_children) + + +def reuse_if_untouched_arg(node, self, arg): + """Reuse if touched recipe propagating an extra argument""" + new_children = [self(child, arg) for child in node.children] + if all(nc == c for nc, c in zip(new_children, node.children)): + return node + else: + return node.reconstruct(*new_children) diff --git a/gem/optimise.py b/gem/optimise.py new file mode 100644 index 000000000..c294b56bd --- /dev/null +++ b/gem/optimise.py @@ -0,0 +1,95 @@ +"""A set of routines implementing various transformations on GEM +expressions.""" + +from __future__ import absolute_import + +from singledispatch import singledispatch + +from gem.node import Memoizer, MemoizerArg, reuse_if_untouched, reuse_if_untouched_arg +from gem.gem import Node, Zero, Sum, Indexed, IndexSum, ComponentTensor + + +@singledispatch +def replace_indices(node, self, subst): + """Replace free indices in a GEM expression. + + :arg node: root of the expression + :arg self: function for recursive calls + :arg subst: tuple of pairs; each pair is a substitution + rule with a free index to replace and an index to + replace with. + """ + raise AssertionError("cannot handle type %s" % type(node)) + + +replace_indices.register(Node)(reuse_if_untouched_arg) + + +@replace_indices.register(Indexed) # noqa +def _(node, self, subst): + child, = node.children + substitute = dict(subst) + multiindex = tuple(substitute.get(i, i) for i in node.multiindex) + if isinstance(child, ComponentTensor): + # Indexing into ComponentTensor + # Inline ComponentTensor and augment the substitution rules + substitute.update(zip(child.multiindex, multiindex)) + return self(child.children[0], tuple(sorted(substitute.items()))) + else: + # Replace indices + new_child = self(child, subst) + if new_child == child and multiindex == node.multiindex: + return node + else: + return Indexed(new_child, multiindex) + + +def filtered_replace_indices(node, self, subst): + """Wrapper for :func:`replace_indices`. At each call removes + substitution rules that do not apply.""" + filtered_subst = tuple((k, v) for k, v in subst if k in node.free_indices) + return replace_indices(node, self, filtered_subst) + + +def remove_componenttensors(expressions): + """Removes all ComponentTensors from a list of expression DAGs.""" + mapper = MemoizerArg(filtered_replace_indices) + return [mapper(expression, ()) for expression in expressions] + + +@singledispatch +def _unroll_indexsum(node, self): + """Unrolls IndexSums below a certain extent. + + :arg node: root of the expression + :arg self: function for recursive calls + """ + raise AssertionError("cannot handle type %s" % type(node)) + + +_unroll_indexsum.register(Node)(reuse_if_untouched) + + +@_unroll_indexsum.register(IndexSum) # noqa +def _(node, self): + if node.index.extent <= self.max_extent: + # Unrolling + summand = self(node.children[0]) + return reduce(Sum, + (Indexed(ComponentTensor(summand, (node.index,)), (i,)) + for i in range(node.index.extent)), + Zero()) + else: + return reuse_if_untouched(node, self) + + +def unroll_indexsum(expressions, max_extent): + """Unrolls IndexSums below a specified extent. + + :arg expressions: list of expression DAGs + :arg max_extent: maximum extent for which IndexSums are unrolled + :returns: list of expression DAGs with some unrolled IndexSums + """ + mapper = Memoizer(_unroll_indexsum) + mapper.max_extent = max_extent + return map(mapper, expressions) diff --git a/gem/scheduling.py b/gem/scheduling.py new file mode 100644 index 000000000..e47174d4a --- /dev/null +++ b/gem/scheduling.py @@ -0,0 +1,195 @@ +"""Schedules operations to evaluate a multi-root expression DAG, +forming an ordered list of Impero terminals.""" + +from __future__ import absolute_import + +import collections +import functools + +from gem import gem, impero +from gem.node import collect_refcount + + +class OrderedDefaultDict(collections.OrderedDict): + """A dictionary that provides a default value and ordered iteration. + + :arg factory: The callable used to create the default value. + + See :class:`collections.OrderedDict` for description of the + remaining arguments. + """ + def __init__(self, factory, *args, **kwargs): + self.factory = factory + super(OrderedDefaultDict, self).__init__(*args, **kwargs) + + def __missing__(self, key): + val = self[key] = self.factory() + return val + + +class ReferenceStager(object): + """Provides staging for nodes in reference counted expression + DAGs. A callback function is called once the reference count is + exhausted.""" + + def __init__(self, reference_count, callback): + """Initialises a ReferenceStager. + + :arg reference_count: initial reference counts for all + expected nodes + :arg callback: function to call on each node when + reference count is exhausted + """ + self.waiting = reference_count.copy() + self.callback = callback + + def decref(self, o): + """Decreases the reference count of a node, and possibly + triggering a callback (when the reference count drops to + zero).""" + assert 1 <= self.waiting[o] + + self.waiting[o] -= 1 + if self.waiting[o] == 0: + self.callback(o) + + def empty(self): + """All reference counts exhausted?""" + return not any(self.waiting.values()) + + +class Queue(object): + """Special queue for operation scheduling. GEM / Impero nodes are + inserted when they are ready to be scheduled, i.e. any operation + which depends on the operation to be inserted must have been + scheduled already. This class implements a heuristic for ordering + operations within the constraints in a way which aims to achieve + maximum loop fusion to minimise the size of temporaries which need + to be introduced. + """ + def __init__(self, callback): + """Initialises a Queue. + + :arg callback: function called on each element "popped" from the queue + """ + # Must have deterministic iteration over the queue + self.queue = OrderedDefaultDict(list) + self.callback = callback + + def insert(self, indices, elem): + """Insert element into queue. + + :arg indices: loop indices used by the scheduling heuristic + :arg elem: element to be scheduled + """ + self.queue[indices].append(elem) + + def process(self): + """Pops elements from the queue and calls the callback + function on them until the queue is empty. The callback + function can insert further elements into the queue. + """ + indices = () + while self.queue: + # Find innermost non-empty outer loop + while indices not in (i[:len(indices)] for i in self.queue.keys()): + indices = indices[:-1] + + # Pick a loop + for i in self.queue.keys(): + if i[:len(indices)] == indices: + indices = i + break + + while self.queue[indices]: + self.callback(self.queue[indices].pop()) + del self.queue[indices] + + +def handle(ops, push, decref, node): + """Helper function for scheduling""" + if isinstance(node, gem.Variable): + # Declared in the kernel header + pass + elif isinstance(node, gem.Literal): + # Constant literals inlined, unless tensor-valued + if node.shape: + ops.append(impero.Evaluate(node)) + elif isinstance(node, gem.Zero): # should rarely happen + assert not node.shape + elif isinstance(node, gem.Indexed): + # Indexing always inlined + decref(node.children[0]) + elif isinstance(node, gem.IndexSum): + ops.append(impero.Noop(node)) + push(impero.Accumulate(node)) + elif isinstance(node, gem.Node): + ops.append(impero.Evaluate(node)) + for child in node.children: + decref(child) + elif isinstance(node, impero.Initialise): + ops.append(node) + elif isinstance(node, impero.Accumulate): + ops.append(node) + push(impero.Initialise(node.indexsum)) + decref(node.indexsum.children[0]) + elif isinstance(node, impero.Return): + ops.append(node) + decref(node.expression) + elif isinstance(node, impero.ReturnAccumulate): + ops.append(node) + decref(node.indexsum.children[0]) + else: + raise AssertionError("no handler for node type %s" % type(node)) + + +def emit_operations(assignments, get_indices): + """Makes an ordering of operations to evaluate a multi-root + expression DAG. + + :arg assignments: Iterable of (variable, expression) pairs. + The value of expression is written into variable + upon execution. + :arg get_indices: mapping from GEM nodes to an ordering of free + indices + :returns: list of Impero terminals correctly ordered to evaluate + the assignments + """ + # Prepare reference counts + refcount = collect_refcount([e for v, e in assignments]) + + # Stage return operations + staging = [] + for variable, expression in assignments: + if refcount[expression] == 1 and isinstance(expression, gem.IndexSum) \ + and set(variable.free_indices) == set(expression.free_indices): + staging.append(impero.ReturnAccumulate(variable, expression)) + refcount[expression] -= 1 + else: + staging.append(impero.Return(variable, expression)) + + # Prepare data structures + def push_node(node): + queue.insert(get_indices(node), node) + + def push_op(op): + queue.insert(op.loop_shape(get_indices), op) + + ops = [] + + stager = ReferenceStager(refcount, push_node) + queue = Queue(functools.partial(handle, ops, push_op, stager.decref)) + + # Enqueue return operations + for op in staging: + push_op(op) + + # Schedule operations + queue.process() + + # Assert that nothing left unprocessed + assert stager.empty() + + # Return + ops.reverse() + return ops From a81fcefae8e0cfc10034cf8df706b0555a083d9a Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 8 Apr 2016 20:04:56 +0100 Subject: [PATCH 217/749] make gem package a little more convenient --- gem/__init__.py | 3 +++ gem/gem.py | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/gem/__init__.py b/gem/__init__.py index e69de29bb..521aea6b4 100644 --- a/gem/__init__.py +++ b/gem/__init__.py @@ -0,0 +1,3 @@ +from __future__ import absolute_import + +from gem.gem import * # noqa diff --git a/gem/gem.py b/gem/gem.py index eb8dbbedc..43a363266 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -22,6 +22,14 @@ from gem.node import Node as NodeBase +__all__ = ['Node', 'Literal', 'Zero', 'Variable', 'Sum', 'Product', + 'Division', 'Power', 'MathFunction', 'MinValue', + 'MaxValue', 'Comparison', 'LogicalNot', 'LogicalAnd', + 'LogicalOr', 'Conditional', 'Index', 'VariableIndex', + 'Indexed', 'ComponentTensor', 'IndexSum', 'ListTensor', + 'partial_indexed'] + + class NodeMeta(type): """Metaclass of GEM nodes. From 465d5194c58ae8d3ad6adb9dd20adac4484dc575 Mon Sep 17 00:00:00 2001 From: David Ham Date: Fri, 8 Apr 2016 23:35:41 +0100 Subject: [PATCH 218/749] Draft basis evaluation --- finat/finiteelementbase.py | 68 ++++++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 7fca7e6f0..71d3cb634 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -1,4 +1,5 @@ import numpy as np +import gem class UndefinedError(Exception): @@ -13,14 +14,14 @@ def __init__(self): @property def cell(self): - '''Return the reference cell on which we are defined. + '''The reference cell on which the element is defined. ''' return self._cell @property def degree(self): - '''Return the degree of the embedding polynomial space. + '''The degree of the embedding polynomial space. In the tensor case this is a tuple. ''' @@ -29,7 +30,7 @@ def degree(self): @property def entity_dofs(self): - '''Return the map of topological entities to degrees of + '''The map of topological entities to degrees of freedom for the finite element. Note that entity numbering needs to take into account the tensor case. @@ -39,21 +40,33 @@ def entity_dofs(self): @property def entity_closure_dofs(self): - '''Return the map of topological entities to degrees of + '''The map of topological entities to degrees of freedom on the closure of those entities for the finite element.''' raise NotImplementedError @property - def dofs_shape(self): - '''Return a tuple indicating the number of degrees of freedom in the + def index_shape(self): + '''A tuple indicating the number of degrees of freedom in the element. For example a scalar quadratic Lagrange element on a triangle would return (6,) while a vector valued version of the same element would return (6, 2)''' raise NotImplementedError - def basis_evaluation(self, index, q, q_index, entity=None, derivative=None): + @property + def value_shape(self): + '''A tuple indicating the shape of the element.''' + + raise NotImplementedError + + def get_indices(): + '''A tuple of GEM :class:`Index` of the correct extents to loop over + the basis functions of this element.''' + + raise NotImplementedError + + def basis_evaluation(self, q, entity=None, derivative=None): '''Return code for evaluating the element at known points on the reference element. @@ -95,9 +108,44 @@ def __init__(self, cell, degree): self._cell = cell self._degree = degree + def get_indices(): + '''A tuple of GEM :class:`Index` of the correct extents to loop over + the basis functions of this element.''' + + return (gem.Index(self_fiat_element.get_spatial_dimension()),) + + def basis_evaluation(self, q, entity=None, derivative=0): + '''Return code for evaluating the element at known points on the + reference element. + + :param q: the quadrature rule. + :param entity: the cell entity on which to tabulate. + :param derivative: the derivative to take of the basis functions. + ''' + + assert entity == None + + dim = self.cell.get_spatial_dimension() + + i = self.get_indices() + qi = q.get_indices() + di = tuple(gem.Index() for i in range(dim)) + + fiat_tab = self._fiat_element.tabulate(derivative, q.points) + + def tabtensor(pre_indices=()): + if len(pre_indices) < dim: + return gem.ListTensor([tabtensor(pre_indices + (i,)) + for i in range(derivative + 1)]) + else: + return gem.ListTensor([gem.Literal(fiat_tab.get(pre_indices + (i,)).T, None) + for i in range(derivative + 1)]) + + return ComponentTensor(Indexed(tabtensor(), di + qi + i), qi + i + di) + @property def entity_dofs(self): - '''Return the map of topological entities to degrees of + '''The map of topological entities to degrees of freedom for the finite element. Note that entity numbering needs to take into account the tensor case. @@ -107,14 +155,14 @@ def entity_dofs(self): @property def entity_closure_dofs(self): - '''Return the map of topological entities to degrees of + '''The map of topological entities to degrees of freedom on the closure of those entities for the finite element.''' return self._fiat_element.entity_closure_dofs() @property def facet_support_dofs(self): - '''Return the map of facet id to the degrees of freedom for which the + '''The map of facet id to the degrees of freedom for which the corresponding basis functions take non-zero values.''' return self._fiat_element.entity_support_dofs() From ef24253abf17af6191cf1fe49c34c24753f12f17 Mon Sep 17 00:00:00 2001 From: David Ham Date: Sat, 9 Apr 2016 23:44:17 +0100 Subject: [PATCH 219/749] draft fiat element and vector element implementation --- finat/fiat_elements.py | 80 ++++++++++++------------------------ finat/finiteelementbase.py | 36 ++++++++++++---- finat/hdiv.py | 27 ------------ finat/quadrature.py | 62 ++++++++++++++++++++++++---- finat/vectorfiniteelement.py | 33 +++++++++++---- 5 files changed, 136 insertions(+), 102 deletions(-) delete mode 100644 finat/hdiv.py diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index cb1d4bfe1..9f7594179 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -2,79 +2,53 @@ import FIAT -class FiatElement(FiatElementBase): - def __init__(self, cell, degree): - super(FiatElement, self).__init__(cell, degree) +class ScalarFiatElement(FiatElementBase): + def value_shape(self): + return () - # def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): - # '''Produce the variable for the tabulation of the basis - # functions or their derivative. Also return the relevant indices. - # updates the requisite static kernel data, which in this case - # is just the matrix. - # ''' - # if derivative not in (None, grad): - # raise ValueError( - # "Scalar elements do not have a %s operation") % derivative +class Lagrange(ScalarFiatElement): + def __init__(self, cell, degree): + super(Lagrange, self).__init__(cell, degree) - # phi = self._tabulated_basis(q.points, kernel_data, derivative) + self._fiat_element = FIAT.Lagrange(cell, degree) - # i = indices.BasisFunctionIndex(self._fiat_element.space_dimension()) - # if derivative is grad: - # alpha = indices.DimensionIndex(self.cell.get_spatial_dimension()) - # if pullback: - # beta = alpha - # alpha = indices.DimensionIndex(kernel_data.gdim) - # invJ = kernel_data.invJ[(beta, alpha)] - # expr = IndexSum((beta,), invJ * phi[(beta, i, q)]) - # else: - # expr = phi[(alpha, i, q)] - # ind = ((alpha,), (i,), (q,)) - # else: - # ind = ((), (i,), (q,)) - # expr = phi[(i, q)] +class GaussLobatto(ScalarFiatElement): + def __init__(self, cell, degree): + super(GaussLobatto, self).__init__(cell, degree) - # return Recipe(indices=ind, body=expr) + self._fiat_element = FIAT.GaussLobatto(cell, degree) -class Lagrange(FiatElement): +class DiscontinuousLagrange(ScalarFiatElement): def __init__(self, cell, degree): super(Lagrange, self).__init__(cell, degree) - self._fiat_element = FIAT.Lagrange(cell, degree) + self._fiat_element = FIAT.DiscontinuousLagrange(cell, degree) -class GaussLobatto(FiatElement): - def __init__(self, cell, degree): - super(GaussLobatto, self).__init__(cell, degree) +class VectorFiatElement(FiatElement): + def value_shape(self): + return (self.cell.get_spatial_dimension(),) - self._fiat_element = FIAT.GaussLobatto(cell, degree) - # def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): - # '''Produce the variable for the tabulation of the basis - # functions or their derivative. Also return the relevant indices. +class RaviartThomas(VectorFiatElement): + def __init__(self, cell, degree): + super(RaviartThomas, self).__init__(cell, degree) - # For basis evaluation with no gradient on a matching - # Gauss-Lobatto quadrature, this implements the standard - # spectral element diagonal mass trick by returning a delta - # function. - # ''' - # if (derivative is None and isinstance(q.points, GaussLobattoPointSet) and - # q.length == self._fiat_element.space_dimension()): + self._fiat_element = FIAT.RaviartThomas(cell, degree) - # i = indices.BasisFunctionIndex(self._fiat_element.space_dimension()) - # return Recipe(((), (i,), (q,)), Delta((i, q), 1.0)) +class BrezziDouglasMarini(VectorFiatElement): + def __init__(self, cell, degree): + super(BrezziDouglasMarini, self).__init__(cell, degree) - # else: - # # Fall through to the default recipe. - # return super(GaussLobatto, self).basis_evaluation(q, kernel_data, - # derivative, pullback) + self._fiat_element = FIAT.BrezziDouglasMarini(cell, degree) -class DiscontinuousLagrange(FiatElement): +class BrezziDouglasFortinMarini(VectorFiatElement): def __init__(self, cell, degree): - super(Lagrange, self).__init__(cell, degree) + super(BrezziDouglasFortinMarini, self).__init__(cell, degree) - self._fiat_element = FIAT.DiscontinuousLagrange(cell, degree) + self._fiat_element = FIAT.BrezziDouglasFortinMarini(cell, degree) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 71d3cb634..60a44ca1e 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -64,7 +64,13 @@ def get_indices(): '''A tuple of GEM :class:`Index` of the correct extents to loop over the basis functions of this element.''' - raise NotImplementedError + return tuple(gem.Index(d) for d in self.index_shape) + + def get_value_indices(): + '''A tuple of GEM :class:`~gem.Index` of the correct extents to loop over + the value shape of this element.''' + + return tuple(gem.Index(d) for d in self.value_shape) def basis_evaluation(self, q, entity=None, derivative=None): '''Return code for evaluating the element at known points on the @@ -79,6 +85,15 @@ def basis_evaluation(self, q, entity=None, derivative=None): raise NotImplementedError + @property + def preferred_quadrature(self): + '''A list of quadrature rules whose structure this element is capable + of exploiting. Each entry in the list should be a pair (rule, + degree), where the degree might be `None` if the element has + no preferred quadrature degree.''' + + return () + def dual_evaluation(self, kernel_data): '''Return code for evaluating an expression at the dual set. @@ -108,11 +123,13 @@ def __init__(self, cell, degree): self._cell = cell self._degree = degree - def get_indices(): - '''A tuple of GEM :class:`Index` of the correct extents to loop over - the basis functions of this element.''' + @property + def index_shape(self): + return (self._fiat_element.space_dimension(),) - return (gem.Index(self_fiat_element.get_spatial_dimension()),) + @property + def value_shape(self): + return self._fiat_element.value_shape() def basis_evaluation(self, q, entity=None, derivative=0): '''Return code for evaluating the element at known points on the @@ -128,20 +145,25 @@ def basis_evaluation(self, q, entity=None, derivative=0): dim = self.cell.get_spatial_dimension() i = self.get_indices() + vi = self.get_value_indices() qi = q.get_indices() di = tuple(gem.Index() for i in range(dim)) fiat_tab = self._fiat_element.tabulate(derivative, q.points) + # Work out the correct transposition between FIAT storage and ours. + tr = (2, 0, 1) if self.value_shape else (1, 0) + + # Convert the FIAT tabulation into a gem tensor. def tabtensor(pre_indices=()): if len(pre_indices) < dim: return gem.ListTensor([tabtensor(pre_indices + (i,)) for i in range(derivative + 1)]) else: - return gem.ListTensor([gem.Literal(fiat_tab.get(pre_indices + (i,)).T, None) + return gem.ListTensor([gem.Literal(fiat_tab.get(pre_indices + (i,)).transpose(tr), None) for i in range(derivative + 1)]) - return ComponentTensor(Indexed(tabtensor(), di + qi + i), qi + i + di) + return ComponentTensor(Indexed(tabtensor(), di + qi + i + vi), qi + i + vi + di) @property def entity_dofs(self): diff --git a/finat/hdiv.py b/finat/hdiv.py deleted file mode 100644 index 5d47b9625..000000000 --- a/finat/hdiv.py +++ /dev/null @@ -1,27 +0,0 @@ -from .fiat_elements import FiatElement -import FIAT - - -class HDivElement(FiatElement): - pass - - -class RaviartThomas(HDivElement): - def __init__(self, cell, degree): - super(RaviartThomas, self).__init__(cell, degree) - - self._fiat_element = FIAT.RaviartThomas(cell, degree) - - -class BrezziDouglasMarini(HDivElement): - def __init__(self, cell, degree): - super(RaviartThomas, self).__init__(cell, degree) - - self._fiat_element = FIAT.BrezziDouglasMarini(cell, degree) - - -class BrezziDouglasFortinMarini(HDivElement): - def __init__(self, cell, degree): - super(RaviartThomas, self).__init__(cell, degree) - - self._fiat_element = FIAT.BrezziDouglasFortinMarini(cell, degree) diff --git a/finat/quadrature.py b/finat/quadrature.py index f94f31252..ed83af22f 100644 --- a/finat/quadrature.py +++ b/finat/quadrature.py @@ -3,6 +3,13 @@ from points import StroudPointSet, PointSet, TensorPointSet, GaussLobattoPointSet import FIAT +def make_quadrature(cell, degree, preferred_quadrature): + '''Return a quadrature rule of the suggested degree in accordance with + the quadrature preferences provided.''' + + # Currently just do the dumb thing. Smart quadrature happens later. + return CollapsedGaussJacobiQuadrature(cell, degree) + class QuadratureRule(object): """Object representing a quadrature rule as a set of points @@ -10,17 +17,58 @@ class QuadratureRule(object): :param cell: The :class:`~FIAT.reference_element.ReferenceElement` on which this quadrature rule is defined. - :param points: An instance of a subclass of :class:`points.PointSetBase` - giving the points for this quadrature rule. - :param weights: The quadrature weights. If ``points`` is a - :class:`points.TensorPointSet` then weights is an iterable whose - members are the sets of weights along the respective dimensions. """ def __init__(self, cell, points, weights): self.cell = cell - self.points = points - self.weights = weights + self._points = points + self._weights = weights + + @property + def points(self): + '''The quadrature points. For a rule with internal structure, this is + the flattened points.''' + + return self._points + + @property + def weights(self): + '''The quadrature weights. For a rule with internal structure, this is + the flattenened weights.''' + + return self._weights + + @property + def index_shape(self): + '''A tuple indicating the shape of the indices needed to loop over the points.''' + + raise NotImplementedError + + def get_indices(self): + '''A tuple of GEM :class:`Index` of the correct extents to loop over + the basis functions of this element.''' + + return tuple(gem.Index(d) for d in self.index_shape) + + +class CollapsedGaussJacobiQuadrature(QuadratureRule): + def __init__(self, cell, degree): + """Gauss Jacobi Quadrature rule using collapsed coordinates to + accommodate higher order simplices.""" + + points = (degree + 1) // 2 + + rule = FIAT.make_quadrature(cell, points) + + super(CollapsedGaussJacobiQuadrature, self).__init__(cell, + rule.pts, + rule.wts) + + @property + def index_shape(self): + '''A tuple indicating the shape of the indices needed to loop over the points.''' + + return (len(self.points),) class StroudQuadrature(QuadratureRule): diff --git a/finat/vectorfiniteelement.py b/finat/vectorfiniteelement.py index 7c860fc37..179745159 100644 --- a/finat/vectorfiniteelement.py +++ b/finat/vectorfiniteelement.py @@ -1,4 +1,5 @@ from finiteelementbase import FiniteElementBase +import gem class VectorFiniteElement(FiniteElementBase): @@ -35,22 +36,38 @@ def __init__(self, element, dimension): self._base_element = element - def basis_evaluation(self, q, kernel_data, derivative=None, pullback=True): - r"""Produce the recipe for basis function evaluation at a set of points -:math:`q`: + @property + def index_shape(self): + return self._base_element.index_shape() + (self._dimension,) + + @property + def value_shape(self): + return self._base_element.value_shape() + (self._dimension,) + + def basis_evaluation(self, q, entity=None, derivative=0): + r"""Produce the recipe for basis function evaluation at a set of points :math:`q`: .. math:: \boldsymbol\phi_{\alpha (i \beta) q} = \delta_{\alpha \beta}\phi_{i q} \nabla\boldsymbol\phi_{(\alpha \gamma) (i \beta) q} = \delta_{\alpha \beta}\nabla\phi_{\gamma i q} + """ - \nabla\times\boldsymbol\phi_{(i \beta) q} = \epsilon_{2 \beta \gamma}\nabla\phi_{\gamma i q} \qquad\textrm{(2D)} + scalarbasis = self._base_element.basis_evaluation(q, entity, derivative) - \nabla\times\boldsymbol\phi_{\alpha (i \beta) q} = \epsilon_{\alpha \beta \gamma}\nabla\phi_{\gamma i q} \qquad\textrm{(3D)} + indices = tuple(gem.Index() for i in scalarbasis.shape) - \nabla\cdot\boldsymbol\phi_{(i \beta) q} = \nabla\phi_{\beta i q} - """ - raise NotImplementedError + # Work out which of the indices are for what. + qi = len(q.index_shape) + len(self._base_element.index_shape) + d = derivative + + # New basis function and value indices. + i = gem.Index(self._dimension) + vi = gem.Index(self._dimension) + + new_indices = indices[:qi] + i + indices[qi: -d] + vi + indices[-d:] + + return gem.ComponentTensor(gem.Product(gem.Delta(i, vi), scalarbasis), new_indices) def __hash__(self): """VectorFiniteElements are equal if they have the same base element From 3c6e3c13efba93a7d06fadb89042e18c9d932cf4 Mon Sep 17 00:00:00 2001 From: David Ham Date: Sun, 10 Apr 2016 18:50:06 +0100 Subject: [PATCH 220/749] Correct stupidly wrong derivative tabulation --- finat/finiteelementbase.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 60a44ca1e..5f4c37f6a 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -154,14 +154,14 @@ def basis_evaluation(self, q, entity=None, derivative=0): # Work out the correct transposition between FIAT storage and ours. tr = (2, 0, 1) if self.value_shape else (1, 0) - # Convert the FIAT tabulation into a gem tensor. - def tabtensor(pre_indices=()): - if len(pre_indices) < dim: - return gem.ListTensor([tabtensor(pre_indices + (i,)) - for i in range(derivative + 1)]) + # Convert the FIAT tabulation into a gem tensor. Note that + # this does not exploit the symmetry of the derivative tensor. + def tabtensor(index = (0,) * dim): + if sum(index) < derivative: + return gem.ListTensor([tabtensor(tuple(index[id] + (1 if id == i else 0) for id in range(dim))) + for i in range(dim)]) else: - return gem.ListTensor([gem.Literal(fiat_tab.get(pre_indices + (i,)).transpose(tr), None) - for i in range(derivative + 1)]) + return gem.Literal(fiat_tab[index].transpose(tr)) return ComponentTensor(Indexed(tabtensor(), di + qi + i + vi), qi + i + vi + di) From fd770a8419ce7f0771fc42d6cde10bbff6aef65b Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 2 Mar 2016 15:36:26 +0000 Subject: [PATCH 221/749] use coffee.SparseArrayInit and zero tracking --- gem/gem.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 43a363266..c900f027a 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -105,10 +105,11 @@ def value(self): class Literal(Terminal): """Tensor-valued constant""" - __slots__ = ('array',) + __slots__ = ('array', 'track_zeros') __front__ = ('array',) + __back__ = ('track_zeros',) - def __new__(cls, array): + def __new__(cls, array, track_zeros=None): array = asarray(array) if (array == 0).all(): # All zeros, make symbolic zero @@ -116,18 +117,19 @@ def __new__(cls, array): else: return super(Literal, cls).__new__(cls) - def __init__(self, array): + def __init__(self, array, track_zeros=None): self.array = asarray(array, dtype=float) + self.track_zeros = bool(track_zeros) def is_equal(self, other): if type(self) != type(other): return False if self.shape != other.shape: return False - return tuple(self.array.flat) == tuple(other.array.flat) + return tuple(self.array.flat) == tuple(other.array.flat) and self.track_zeros == other.track_zeros def get_hash(self): - return hash((type(self), self.shape, tuple(self.array.flat))) + return hash((type(self), self.shape, tuple(self.array.flat), self.track_zeros)) @property def value(self): From 47c8b8d708a3d53d6acbfcf7c4b929ebf898fe0a Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 5 Apr 2016 16:12:51 +0100 Subject: [PATCH 222/749] always use coffee.SparseArrayInit This partially reverts commit fd770a8419ce7f0771fc42d6cde10bbff6aef65b. --- gem/gem.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index c900f027a..43a363266 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -105,11 +105,10 @@ def value(self): class Literal(Terminal): """Tensor-valued constant""" - __slots__ = ('array', 'track_zeros') + __slots__ = ('array',) __front__ = ('array',) - __back__ = ('track_zeros',) - def __new__(cls, array, track_zeros=None): + def __new__(cls, array): array = asarray(array) if (array == 0).all(): # All zeros, make symbolic zero @@ -117,19 +116,18 @@ def __new__(cls, array, track_zeros=None): else: return super(Literal, cls).__new__(cls) - def __init__(self, array, track_zeros=None): + def __init__(self, array): self.array = asarray(array, dtype=float) - self.track_zeros = bool(track_zeros) def is_equal(self, other): if type(self) != type(other): return False if self.shape != other.shape: return False - return tuple(self.array.flat) == tuple(other.array.flat) and self.track_zeros == other.track_zeros + return tuple(self.array.flat) == tuple(other.array.flat) def get_hash(self): - return hash((type(self), self.shape, tuple(self.array.flat), self.track_zeros)) + return hash((type(self), self.shape, tuple(self.array.flat))) @property def value(self): From 7f2e83bd148dd148d4f138c85a53f3faf0f05b6c Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 11 Apr 2016 10:04:57 +0100 Subject: [PATCH 223/749] remove coffee_licm from parameters --- gem/impero_utils.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/gem/impero_utils.py b/gem/impero_utils.py index 65ead3159..5fceec744 100644 --- a/gem/impero_utils.py +++ b/gem/impero_utils.py @@ -33,14 +33,13 @@ class NoopError(Exception): pass -def compile_gem(return_variables, expressions, prefix_ordering, remove_zeros=False, coffee_licm=False): +def compile_gem(return_variables, expressions, prefix_ordering, remove_zeros=False): """Compiles GEM to Impero. :arg return_variables: return variables for each root (type: GEM expressions) :arg expressions: multi-root expression DAG (type: GEM expressions) :arg prefix_ordering: outermost loop indices :arg remove_zeros: remove zero assignment to return variables - :arg coffee_licm: trust COFFEE to do loop invariant code motion """ expressions = optimise.remove_componenttensors(expressions) @@ -79,7 +78,7 @@ def compile_gem(return_variables, expressions, prefix_ordering, remove_zeros=Fal raise NoopError() # Drop unnecessary temporaries - ops = inline_temporaries(expressions, ops, coffee_licm=coffee_licm) + ops = inline_temporaries(expressions, ops) # Build Impero AST tree = make_loop_tree(ops, get_indices) @@ -111,15 +110,13 @@ def apply_ordering(indices): return apply_ordering -def inline_temporaries(expressions, ops, coffee_licm=False): +def inline_temporaries(expressions, ops): """Inline temporaries which could be inlined without blowing up the code. :arg expressions: a multi-root GEM expression DAG, used for reference counting :arg ops: ordered list of Impero terminals - :arg coffee_licm: Trust COFFEE to do LICM. If enabled, inlining - can move calculations inside inner loops. :returns: a filtered ``ops``, without the unnecessary :class:`impero.Evaluate`s """ @@ -132,12 +129,11 @@ def inline_temporaries(expressions, ops, coffee_licm=False): if expr.shape == () and refcount[expr] == 1: candidates.add(expr) - if not coffee_licm: - # Prevent inlining that pulls expressions into inner loops - for node in traversal(expressions): - for child in node.children: - if child in candidates and set(child.free_indices) < set(node.free_indices): - candidates.remove(child) + # Prevent inlining that pulls expressions into inner loops + for node in traversal(expressions): + for child in node.children: + if child in candidates and set(child.free_indices) < set(node.free_indices): + candidates.remove(child) # Filter out candidates return [op for op in ops if not (isinstance(op, imp.Evaluate) and op.expression in candidates)] From 8c7bda7212ef3a9164f2f7860656c5e1f24d2400 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 11 Apr 2016 11:34:11 +0100 Subject: [PATCH 224/749] gem: introduce Constant and add Identity --- gem/gem.py | 40 ++++++++++++++++++++++++++++++++++------ gem/interpreter.py | 4 ++-- gem/scheduling.py | 2 +- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 43a363266..2739c8281 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -17,13 +17,13 @@ from __future__ import absolute_import from itertools import chain -from numpy import asarray, unique +from numpy import asarray, eye, unique from gem.node import Node as NodeBase -__all__ = ['Node', 'Literal', 'Zero', 'Variable', 'Sum', 'Product', - 'Division', 'Power', 'MathFunction', 'MinValue', +__all__ = ['Node', 'Identity', 'Literal', 'Zero', 'Variable', 'Sum', + 'Product', 'Division', 'Power', 'MathFunction', 'MinValue', 'MaxValue', 'Comparison', 'LogicalNot', 'LogicalAnd', 'LogicalOr', 'Conditional', 'Index', 'VariableIndex', 'Indexed', 'ComponentTensor', 'IndexSum', 'ListTensor', @@ -87,7 +87,17 @@ class Scalar(Node): shape = () -class Zero(Terminal): +class Constant(Terminal): + """Abstract base class for constant types. + + Convention: + - array: numpy array of values + - value: float value (scalars only) + """ + __slots__ = () + + +class Zero(Constant): """Symbolic zero tensor""" __slots__ = ('shape',) @@ -102,7 +112,25 @@ def value(self): return 0.0 -class Literal(Terminal): +class Identity(Constant): + """Identity matrix""" + + __slots__ = ('dim',) + __front__ = ('dim',) + + def __init__(self, dim): + self.dim = dim + + @property + def shape(self): + return (self.dim, self.dim) + + @property + def array(self): + return eye(self.dim) + + +class Literal(Constant): """Tensor-valued constant""" __slots__ = ('array',) @@ -382,7 +410,7 @@ def __new__(cls, aggregate, multiindex): # All indices fixed if all(isinstance(i, int) for i in multiindex): - if isinstance(aggregate, Literal): + if isinstance(aggregate, Constant): return Literal(aggregate.array[multiindex]) elif isinstance(aggregate, ListTensor): return aggregate.array[multiindex] diff --git a/gem/interpreter.py b/gem/interpreter.py index 1ec064eb5..20056d29f 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -99,9 +99,9 @@ def _(e, self): return Result(numpy.zeros(e.shape, dtype=float)) -@_evaluate.register(gem.Literal) # noqa: not actually redefinition +@_evaluate.register(gem.Constant) # noqa: not actually redefinition def _(e, self): - """Literals return their array.""" + """Constants return their array.""" return Result(e.array) diff --git a/gem/scheduling.py b/gem/scheduling.py index e47174d4a..d936fd7f3 100644 --- a/gem/scheduling.py +++ b/gem/scheduling.py @@ -111,7 +111,7 @@ def handle(ops, push, decref, node): if isinstance(node, gem.Variable): # Declared in the kernel header pass - elif isinstance(node, gem.Literal): + elif isinstance(node, gem.Constant): # Constant literals inlined, unless tensor-valued if node.shape: ops.append(impero.Evaluate(node)) From f2e642d9243a8ae84bd3f203c0a945ee4005967d Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 11 Apr 2016 11:42:18 +0100 Subject: [PATCH 225/749] gem: introduce IndexBase --- gem/gem.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 2739c8281..a9da2cb83 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -16,6 +16,7 @@ from __future__ import absolute_import +from abc import ABCMeta from itertools import chain from numpy import asarray, eye, unique @@ -336,7 +337,15 @@ def __init__(self, condition, then, else_): self.shape = then.shape -class Index(object): +class IndexBase(object): + """Abstract base class for indices.""" + + __metaclass__ = ABCMeta + +IndexBase.register(int) + + +class Index(IndexBase): """Free index""" # Not true object count, just for naming purposes @@ -369,7 +378,7 @@ def __repr__(self): return "Index(%r)" % self.name -class VariableIndex(object): +class VariableIndex(IndexBase): """An index that is constant during a single execution of the kernel, but whose value is not known at compile time.""" @@ -401,6 +410,7 @@ def __new__(cls, aggregate, multiindex): # Set index extents from shape assert len(aggregate.shape) == len(multiindex) for index, extent in zip(multiindex, aggregate.shape): + assert isinstance(index, IndexBase) if isinstance(index, Index): index.set_extent(extent) From 02fb756779bc1160320a6c576b55a5e54b298420 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 11 Apr 2016 14:41:38 +0100 Subject: [PATCH 226/749] gem: nascent Delta support --- gem/gem.py | 27 ++++++++++++++++++++++++++- gem/optimise.py | 47 ++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index a9da2cb83..0a65d90b4 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -28,7 +28,7 @@ 'MaxValue', 'Comparison', 'LogicalNot', 'LogicalAnd', 'LogicalOr', 'Conditional', 'Index', 'VariableIndex', 'Indexed', 'ComponentTensor', 'IndexSum', 'ListTensor', - 'partial_indexed'] + 'Delta', 'partial_indexed'] class NodeMeta(type): @@ -529,6 +529,31 @@ def get_hash(self): return hash((type(self), self.shape, self.children)) +class Delta(Scalar, Terminal): + __slots__ = ('i', 'j') + __front__ = ('i', 'j') + + def __new__(cls, i, j): + assert isinstance(i, IndexBase) + assert isinstance(j, IndexBase) + + # \delta_{i,i} = 1 + if i == j: + return Literal(1) + + # Fixed indices + if isinstance(i, int) and isinstance(j, int): + return Literal(int(i == j)) + + self = super(Delta, cls).__new__(cls) + self.i = i + self.j = j + # Set up free indices + free_indices = tuple(index for index in (i, j) if isinstance(index, Index)) + self.free_indices = tuple(unique(free_indices)) + return self + + def partial_indexed(tensor, indices): """Generalised indexing into a tensor. The number of indices may be less than or equal to the rank of the tensor, so the result may diff --git a/gem/optimise.py b/gem/optimise.py index c294b56bd..f0bdfc12b 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -6,7 +6,9 @@ from singledispatch import singledispatch from gem.node import Memoizer, MemoizerArg, reuse_if_untouched, reuse_if_untouched_arg -from gem.gem import Node, Zero, Sum, Indexed, IndexSum, ComponentTensor +from gem.gem import (Node, Identity, Literal, Zero, Sum, Comparison, + Conditional, Index, VariableIndex, Indexed, + IndexSum, ComponentTensor, Delta) @singledispatch @@ -21,7 +23,6 @@ def replace_indices(node, self, subst): """ raise AssertionError("cannot handle type %s" % type(node)) - replace_indices.register(Node)(reuse_if_untouched_arg) @@ -52,11 +53,51 @@ def filtered_replace_indices(node, self, subst): def remove_componenttensors(expressions): - """Removes all ComponentTensors from a list of expression DAGs.""" + """Removes all ComponentTensors in multi-root expression DAG.""" mapper = MemoizerArg(filtered_replace_indices) return [mapper(expression, ()) for expression in expressions] +@singledispatch +def _replace_delta(node, self): + raise AssertionError("cannot handle type %s" % type(node)) + +_replace_delta.register(Node)(reuse_if_untouched) + + +@_replace_delta.register(Delta) +def _replace_delta_delta(node, self): + i, j = node.i, node.j + + if isinstance(i, Index) or isinstance(j, Index): + if isinstance(i, Index) and isinstance(j, Index): + assert i.extent == j.extent + if isinstance(i, Index): + assert i.extent is not None + size = i.extent + if isinstance(j, Index): + assert j.extent is not None + size = j.extent + return Indexed(Identity(size), (i, j)) + else: + def expression(index): + if isinstance(index, int): + return Literal(index) + elif isinstance(index, VariableIndex): + return index.expression + else: + raise ValueError("Cannot convert running index to expression.") + e_i = expression(i) + e_j = expression(j) + return Conditional(Comparison("==", e_i, e_j), Literal(1), Zero()) + + +def replace_delta(expressions): + """Lowers all Deltas in a multi-root expression DAG.""" + mapper = Memoizer(_replace_delta) + return map(mapper, expressions) + + @singledispatch def _unroll_indexsum(node, self): """Unrolls IndexSums below a certain extent. From 25172af2cff58547e4bd90e22cb0c1c94dbaff90 Mon Sep 17 00:00:00 2001 From: David Ham Date: Mon, 11 Apr 2016 15:28:32 +0100 Subject: [PATCH 227/749] Use IndexTensor correctly --- finat/finiteelementbase.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 5f4c37f6a..9a6f4e03b 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -147,7 +147,7 @@ def basis_evaluation(self, q, entity=None, derivative=0): i = self.get_indices() vi = self.get_value_indices() qi = q.get_indices() - di = tuple(gem.Index() for i in range(dim)) + di = tuple(gem.Index() for i in range(dim)) fiat_tab = self._fiat_element.tabulate(derivative, q.points) @@ -156,14 +156,19 @@ def basis_evaluation(self, q, entity=None, derivative=0): # Convert the FIAT tabulation into a gem tensor. Note that # this does not exploit the symmetry of the derivative tensor. - def tabtensor(index = (0,) * dim): - if sum(index) < derivative: - return gem.ListTensor([tabtensor(tuple(index[id] + (1 if id == i else 0) for id in range(dim))) - for i in range(dim)]) - else: - return gem.Literal(fiat_tab[index].transpose(tr)) - - return ComponentTensor(Indexed(tabtensor(), di + qi + i + vi), qi + i + vi + di) + i = np.eye(dim, dtype=np.int) + + if derivative: + tensor = np.empty((dim,) * derivative, dtype=np.object) + it = np.nditer(tensor, flags=['multi_index'], op_flags=["writeonly"]) + while not it.finished: + derivative_multi_index = tuple(i[it.index, :].sum(0)) + it[0] = gem.Literal(fiat_tab[derivative_multi_index].transpose(tr)) + it.iternext() + else: + tensor = gem.Literal(fiat_tab[(0,) * dim].transpose(tr)) + + return gem.ComponentTensor(gem.Indexed(tensor(), di + qi + i + vi), qi + i + vi + di) @property def entity_dofs(self): From 614fb9ee94e7955574a9ddf351ff1cb680cd4e78 Mon Sep 17 00:00:00 2001 From: David Ham Date: Mon, 11 Apr 2016 15:32:17 +0100 Subject: [PATCH 228/749] pep8 --- finat/finiteelementbase.py | 2 +- finat/quadrature.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 9a6f4e03b..b4b43dfe4 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -140,7 +140,7 @@ def basis_evaluation(self, q, entity=None, derivative=0): :param derivative: the derivative to take of the basis functions. ''' - assert entity == None + assert entity is None dim = self.cell.get_spatial_dimension() diff --git a/finat/quadrature.py b/finat/quadrature.py index ed83af22f..0484bb55b 100644 --- a/finat/quadrature.py +++ b/finat/quadrature.py @@ -3,6 +3,7 @@ from points import StroudPointSet, PointSet, TensorPointSet, GaussLobattoPointSet import FIAT + def make_quadrature(cell, degree, preferred_quadrature): '''Return a quadrature rule of the suggested degree in accordance with the quadrature preferences provided.''' From e2632cc98f57c7de3bd53b2a6be2c16fee93a3e7 Mon Sep 17 00:00:00 2001 From: David Ham Date: Mon, 11 Apr 2016 15:34:35 +0100 Subject: [PATCH 229/749] Correct use of Literal --- finat/finiteelementbase.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index b4b43dfe4..97211ab7d 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -166,9 +166,11 @@ def basis_evaluation(self, q, entity=None, derivative=0): it[0] = gem.Literal(fiat_tab[derivative_multi_index].transpose(tr)) it.iternext() else: - tensor = gem.Literal(fiat_tab[(0,) * dim].transpose(tr)) + tensor = fiat_tab[(0,) * dim].transpose(tr) - return gem.ComponentTensor(gem.Indexed(tensor(), di + qi + i + vi), qi + i + vi + di) + return gem.ComponentTensor(gem.Indexed(gem.Literal(tensor), + di + qi + i + vi), + qi + i + vi + di) @property def entity_dofs(self): From 95eb1e85b25d4a35aa283123f2067742f7cb00e5 Mon Sep 17 00:00:00 2001 From: David Ham Date: Mon, 11 Apr 2016 15:37:36 +0100 Subject: [PATCH 230/749] Put FiniteElementBase somewhere more sensible --- finat/fiat_elements.py | 88 +++++++++++++++++++++++++++++++++++++- finat/finiteelementbase.py | 83 ----------------------------------- 2 files changed, 86 insertions(+), 85 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 9f7594179..4e2fddd35 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -1,5 +1,89 @@ -from .finiteelementbase import FiatElementBase +from .finiteelementbase import FiniteElementBase import FIAT +import gem + + +class FiatElementBase(FiniteElementBase): + """Base class for finite elements for which the tabulation is provided + by FIAT.""" + def __init__(self, cell, degree): + super(FiatElementBase, self).__init__() + + self._cell = cell + self._degree = degree + + @property + def index_shape(self): + return (self._fiat_element.space_dimension(),) + + @property + def value_shape(self): + return self._fiat_element.value_shape() + + def basis_evaluation(self, q, entity=None, derivative=0): + '''Return code for evaluating the element at known points on the + reference element. + + :param q: the quadrature rule. + :param entity: the cell entity on which to tabulate. + :param derivative: the derivative to take of the basis functions. + ''' + + assert entity is None + + dim = self.cell.get_spatial_dimension() + + i = self.get_indices() + vi = self.get_value_indices() + qi = q.get_indices() + di = tuple(gem.Index() for i in range(dim)) + + fiat_tab = self._fiat_element.tabulate(derivative, q.points) + + # Work out the correct transposition between FIAT storage and ours. + tr = (2, 0, 1) if self.value_shape else (1, 0) + + # Convert the FIAT tabulation into a gem tensor. Note that + # this does not exploit the symmetry of the derivative tensor. + i = np.eye(dim, dtype=np.int) + + if derivative: + tensor = np.empty((dim,) * derivative, dtype=np.object) + it = np.nditer(tensor, flags=['multi_index'], op_flags=["writeonly"]) + while not it.finished: + derivative_multi_index = tuple(i[it.index, :].sum(0)) + it[0] = gem.Literal(fiat_tab[derivative_multi_index].transpose(tr)) + it.iternext() + else: + tensor = fiat_tab[(0,) * dim].transpose(tr) + + return gem.ComponentTensor(gem.Indexed(gem.Literal(tensor), + di + qi + i + vi), + qi + i + vi + di) + + @property + def entity_dofs(self): + '''The map of topological entities to degrees of + freedom for the finite element. + + Note that entity numbering needs to take into account the tensor case. + ''' + + return self._fiat_element.entity_dofs() + + @property + def entity_closure_dofs(self): + '''The map of topological entities to degrees of + freedom on the closure of those entities for the finite element.''' + + return self._fiat_element.entity_closure_dofs() + + @property + def facet_support_dofs(self): + '''The map of facet id to the degrees of freedom for which the + corresponding basis functions take non-zero values.''' + + return self._fiat_element.entity_support_dofs() class ScalarFiatElement(FiatElementBase): @@ -28,7 +112,7 @@ def __init__(self, cell, degree): self._fiat_element = FIAT.DiscontinuousLagrange(cell, degree) -class VectorFiatElement(FiatElement): +class VectorFiatElement(FiatElementBase): def value_shape(self): return (self.cell.get_spatial_dimension(),) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 97211ab7d..a2504e554 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -112,86 +112,3 @@ def __eq__(self, other): return type(self) == type(other) and self._cell == other._cell and\ self._degree == other._degree - - -class FiatElementBase(FiniteElementBase): - """Base class for finite elements for which the tabulation is provided - by FIAT.""" - def __init__(self, cell, degree): - super(FiatElementBase, self).__init__() - - self._cell = cell - self._degree = degree - - @property - def index_shape(self): - return (self._fiat_element.space_dimension(),) - - @property - def value_shape(self): - return self._fiat_element.value_shape() - - def basis_evaluation(self, q, entity=None, derivative=0): - '''Return code for evaluating the element at known points on the - reference element. - - :param q: the quadrature rule. - :param entity: the cell entity on which to tabulate. - :param derivative: the derivative to take of the basis functions. - ''' - - assert entity is None - - dim = self.cell.get_spatial_dimension() - - i = self.get_indices() - vi = self.get_value_indices() - qi = q.get_indices() - di = tuple(gem.Index() for i in range(dim)) - - fiat_tab = self._fiat_element.tabulate(derivative, q.points) - - # Work out the correct transposition between FIAT storage and ours. - tr = (2, 0, 1) if self.value_shape else (1, 0) - - # Convert the FIAT tabulation into a gem tensor. Note that - # this does not exploit the symmetry of the derivative tensor. - i = np.eye(dim, dtype=np.int) - - if derivative: - tensor = np.empty((dim,) * derivative, dtype=np.object) - it = np.nditer(tensor, flags=['multi_index'], op_flags=["writeonly"]) - while not it.finished: - derivative_multi_index = tuple(i[it.index, :].sum(0)) - it[0] = gem.Literal(fiat_tab[derivative_multi_index].transpose(tr)) - it.iternext() - else: - tensor = fiat_tab[(0,) * dim].transpose(tr) - - return gem.ComponentTensor(gem.Indexed(gem.Literal(tensor), - di + qi + i + vi), - qi + i + vi + di) - - @property - def entity_dofs(self): - '''The map of topological entities to degrees of - freedom for the finite element. - - Note that entity numbering needs to take into account the tensor case. - ''' - - return self._fiat_element.entity_dofs() - - @property - def entity_closure_dofs(self): - '''The map of topological entities to degrees of - freedom on the closure of those entities for the finite element.''' - - return self._fiat_element.entity_closure_dofs() - - @property - def facet_support_dofs(self): - '''The map of facet id to the degrees of freedom for which the - corresponding basis functions take non-zero values.''' - - return self._fiat_element.entity_support_dofs() From 25faffa6a069f33691c0246cbaa14afd1e6785ad Mon Sep 17 00:00:00 2001 From: David Ham Date: Mon, 11 Apr 2016 15:39:44 +0100 Subject: [PATCH 231/749] Write out 100 times: I will always pass self to instance methods. --- finat/finiteelementbase.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index a2504e554..303be38d3 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -60,13 +60,13 @@ def value_shape(self): raise NotImplementedError - def get_indices(): + def get_indices(self): '''A tuple of GEM :class:`Index` of the correct extents to loop over the basis functions of this element.''' return tuple(gem.Index(d) for d in self.index_shape) - def get_value_indices(): + def get_value_indices(self): '''A tuple of GEM :class:`~gem.Index` of the correct extents to loop over the value shape of this element.''' From e2760729bfb0d31afb05fc4e6722f72a01642e63 Mon Sep 17 00:00:00 2001 From: David Ham Date: Mon, 11 Apr 2016 15:42:59 +0100 Subject: [PATCH 232/749] actually import numpy --- finat/fiat_elements.py | 1 + 1 file changed, 1 insertion(+) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 4e2fddd35..14b11611b 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -1,6 +1,7 @@ from .finiteelementbase import FiniteElementBase import FIAT import gem +import numpy as np class FiatElementBase(FiniteElementBase): From 51620a124c8569f660420f494364ef45f5f5e891 Mon Sep 17 00:00:00 2001 From: David Ham Date: Mon, 11 Apr 2016 15:44:43 +0100 Subject: [PATCH 233/749] OK, so it turns out I wasn't thinking after all --- finat/fiat_elements.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 14b11611b..fab0245fa 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -55,10 +55,11 @@ def basis_evaluation(self, q, entity=None, derivative=0): derivative_multi_index = tuple(i[it.index, :].sum(0)) it[0] = gem.Literal(fiat_tab[derivative_multi_index].transpose(tr)) it.iternext() + tensor = gem.ListTensor(tensor) else: - tensor = fiat_tab[(0,) * dim].transpose(tr) + tensor = gem.Literal(fiat_tab[(0,) * dim].transpose(tr)) - return gem.ComponentTensor(gem.Indexed(gem.Literal(tensor), + return gem.ComponentTensor(gem.Indexed(tensor, di + qi + i + vi), qi + i + vi + di) From f230c93f3f118f7980ed4efaa2d02eb62758adf0 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 20 Apr 2016 14:10:37 +0100 Subject: [PATCH 234/749] allow building ListTensor from non-scalars --- gem/gem.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index a9da2cb83..a9722abdb 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -18,7 +18,9 @@ from abc import ABCMeta from itertools import chain -from numpy import asarray, eye, unique +import numpy +from numpy import asarray, unique +from operator import attrgetter from gem.node import Node as NodeBase @@ -128,7 +130,7 @@ def shape(self): @property def array(self): - return eye(self.dim) + return numpy.eye(self.dim) class Literal(Constant): @@ -492,11 +494,23 @@ class ListTensor(Node): def __new__(cls, array): array = asarray(array) - - # Zero folding - if all(isinstance(elem, Zero) for elem in array.flat): - assert all(elem.shape == () for elem in array.flat) - return Zero(array.shape) + assert numpy.prod(array.shape) + + # Handle children with shape + child_shape = array.flat[0].shape + assert all(elem.shape == child_shape for elem in array.flat) + + if child_shape: + # Destroy structure + direct_array = numpy.empty(array.shape + child_shape, dtype=object) + for alpha in numpy.ndindex(array.shape): + for beta in numpy.ndindex(child_shape): + direct_array[alpha + beta] = Indexed(array[alpha], beta) + array = direct_array + + # Constant folding + if all(isinstance(elem, Constant) for elem in array.flat): + return Literal(numpy.vectorize(attrgetter('value'))(array)) self = super(ListTensor, cls).__new__(cls) self.array = array From 4864f831511db4bfb39666fbfa325b0f385760fd Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 25 Apr 2016 11:08:27 +0100 Subject: [PATCH 235/749] fix missing and bogus imports --- finat/__init__.py | 1 - finat/quadrature.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/__init__.py b/finat/__init__.py index 960243fd2..1be2040f6 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -1,5 +1,4 @@ from fiat_elements import Lagrange, DiscontinuousLagrange, GaussLobatto -from hdiv import RaviartThomas, BrezziDouglasMarini, BrezziDouglasFortinMarini from bernstein import Bernstein from vectorfiniteelement import VectorFiniteElement from product_elements import ScalarProductElement diff --git a/finat/quadrature.py b/finat/quadrature.py index 0484bb55b..f5fd2e8c5 100644 --- a/finat/quadrature.py +++ b/finat/quadrature.py @@ -2,6 +2,7 @@ from gauss_jacobi import gauss_jacobi_rule from points import StroudPointSet, PointSet, TensorPointSet, GaussLobattoPointSet import FIAT +import gem def make_quadrature(cell, degree, preferred_quadrature): From a1c793402156d2f2c4b29045a657e69ea27a25ed Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 25 Apr 2016 11:09:10 +0100 Subject: [PATCH 236/749] add missing property --- finat/fiat_elements.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index fab0245fa..9ecb84d4c 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -89,6 +89,7 @@ def facet_support_dofs(self): class ScalarFiatElement(FiatElementBase): + @property def value_shape(self): return () @@ -115,6 +116,7 @@ def __init__(self, cell, degree): class VectorFiatElement(FiatElementBase): + @property def value_shape(self): return (self.cell.get_spatial_dimension(),) From 2bdf8f91155f58deb3833e2fc4efbd60a241103b Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 25 Apr 2016 11:14:39 +0100 Subject: [PATCH 237/749] fix gem.Index creations --- finat/finiteelementbase.py | 4 ++-- finat/quadrature.py | 2 +- finat/vectorfiniteelement.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 303be38d3..3fea00037 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -64,13 +64,13 @@ def get_indices(self): '''A tuple of GEM :class:`Index` of the correct extents to loop over the basis functions of this element.''' - return tuple(gem.Index(d) for d in self.index_shape) + return tuple(gem.Index(extent=d) for d in self.index_shape) def get_value_indices(self): '''A tuple of GEM :class:`~gem.Index` of the correct extents to loop over the value shape of this element.''' - return tuple(gem.Index(d) for d in self.value_shape) + return tuple(gem.Index(extent=d) for d in self.value_shape) def basis_evaluation(self, q, entity=None, derivative=None): '''Return code for evaluating the element at known points on the diff --git a/finat/quadrature.py b/finat/quadrature.py index f5fd2e8c5..bf531e716 100644 --- a/finat/quadrature.py +++ b/finat/quadrature.py @@ -50,7 +50,7 @@ def get_indices(self): '''A tuple of GEM :class:`Index` of the correct extents to loop over the basis functions of this element.''' - return tuple(gem.Index(d) for d in self.index_shape) + return tuple(gem.Index(extent=d) for d in self.index_shape) class CollapsedGaussJacobiQuadrature(QuadratureRule): diff --git a/finat/vectorfiniteelement.py b/finat/vectorfiniteelement.py index 179745159..f4847fc4b 100644 --- a/finat/vectorfiniteelement.py +++ b/finat/vectorfiniteelement.py @@ -62,8 +62,8 @@ def basis_evaluation(self, q, entity=None, derivative=0): d = derivative # New basis function and value indices. - i = gem.Index(self._dimension) - vi = gem.Index(self._dimension) + i = gem.Index(extent=self._dimension) + vi = gem.Index(extent=self._dimension) new_indices = indices[:qi] + i + indices[qi: -d] + vi + indices[-d:] From 92ed8afbebb0ea11886edd7c5a2a34800b00948d Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 25 Apr 2016 11:38:59 +0100 Subject: [PATCH 238/749] add extent= to gem.Index constructor --- gem/gem.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index c8ae7ada9..afef65e03 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -355,12 +355,11 @@ class Index(IndexBase): __slots__ = ('name', 'extent', 'count') - def __init__(self, name=None): + def __init__(self, name=None, extent=None): self.name = name Index._count += 1 self.count = Index._count - # Initialise with indefinite extent - self.extent = None + self.extent = extent def set_extent(self, value): # Set extent, check for consistency From a049f67dabdc66ce50f94f097d44f306e2e41e74 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 25 Apr 2016 11:39:49 +0100 Subject: [PATCH 239/749] fix a few things --- finat/fiat_elements.py | 4 +--- finat/vectorfiniteelement.py | 10 ++++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 9ecb84d4c..4591400c6 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -37,7 +37,7 @@ def basis_evaluation(self, q, entity=None, derivative=0): i = self.get_indices() vi = self.get_value_indices() qi = q.get_indices() - di = tuple(gem.Index() for i in range(dim)) + di = tuple(gem.Index(extent=dim) for i in range(derivative)) fiat_tab = self._fiat_element.tabulate(derivative, q.points) @@ -46,8 +46,6 @@ def basis_evaluation(self, q, entity=None, derivative=0): # Convert the FIAT tabulation into a gem tensor. Note that # this does not exploit the symmetry of the derivative tensor. - i = np.eye(dim, dtype=np.int) - if derivative: tensor = np.empty((dim,) * derivative, dtype=np.object) it = np.nditer(tensor, flags=['multi_index'], op_flags=["writeonly"]) diff --git a/finat/vectorfiniteelement.py b/finat/vectorfiniteelement.py index f4847fc4b..38a9bacc7 100644 --- a/finat/vectorfiniteelement.py +++ b/finat/vectorfiniteelement.py @@ -38,11 +38,11 @@ def __init__(self, element, dimension): @property def index_shape(self): - return self._base_element.index_shape() + (self._dimension,) + return self._base_element.index_shape + (self._dimension,) @property def value_shape(self): - return self._base_element.value_shape() + (self._dimension,) + return self._base_element.value_shape + (self._dimension,) def basis_evaluation(self, q, entity=None, derivative=0): r"""Produce the recipe for basis function evaluation at a set of points :math:`q`: @@ -65,9 +65,11 @@ def basis_evaluation(self, q, entity=None, derivative=0): i = gem.Index(extent=self._dimension) vi = gem.Index(extent=self._dimension) - new_indices = indices[:qi] + i + indices[qi: -d] + vi + indices[-d:] + new_indices = indices[:qi] + (i,) + indices[qi: len(indices) - d] + (vi,) + indices[len(indices) - d:] - return gem.ComponentTensor(gem.Product(gem.Delta(i, vi), scalarbasis), new_indices) + return gem.ComponentTensor(gem.Product(gem.Delta(i, vi), + gem.Indexed(scalarbasis, indices)), + new_indices) def __hash__(self): """VectorFiniteElements are equal if they have the same base element From b0c408d0dcafb00d46cf06c1382a79d344d28eb0 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 25 Apr 2016 17:19:27 +0100 Subject: [PATCH 240/749] WIP: FInAT integration --- gem/impero_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gem/impero_utils.py b/gem/impero_utils.py index 65ead3159..99d7e3693 100644 --- a/gem/impero_utils.py +++ b/gem/impero_utils.py @@ -42,6 +42,7 @@ def compile_gem(return_variables, expressions, prefix_ordering, remove_zeros=Fal :arg remove_zeros: remove zero assignment to return variables :arg coffee_licm: trust COFFEE to do loop invariant code motion """ + expressions = optimise.replace_delta(expressions) expressions = optimise.remove_componenttensors(expressions) # Remove zeros From c80547f54e63527af84e51147b32276ac8d076c3 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 25 Apr 2016 17:20:35 +0100 Subject: [PATCH 241/749] fix derivatives --- finat/fiat_elements.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 4591400c6..023e8acc3 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -47,10 +47,11 @@ def basis_evaluation(self, q, entity=None, derivative=0): # Convert the FIAT tabulation into a gem tensor. Note that # this does not exploit the symmetry of the derivative tensor. if derivative: + e = np.eye(dim, dtype=np.int) tensor = np.empty((dim,) * derivative, dtype=np.object) - it = np.nditer(tensor, flags=['multi_index'], op_flags=["writeonly"]) + it = np.nditer(tensor, flags=['multi_index', 'refs_ok'], op_flags=["writeonly"]) while not it.finished: - derivative_multi_index = tuple(i[it.index, :].sum(0)) + derivative_multi_index = tuple(e[it.multi_index, :].sum(0)) it[0] = gem.Literal(fiat_tab[derivative_multi_index].transpose(tr)) it.iternext() tensor = gem.ListTensor(tensor) From 54d95caa591704e967b3fd4962d9dafd46daa535 Mon Sep 17 00:00:00 2001 From: David Ham Date: Tue, 26 Apr 2016 22:20:13 +0100 Subject: [PATCH 242/749] AffineIndex and IndexIterator Implement affine index groups and an iterator for multi-indices over sets of Index and AffineIndex. --- gem/gem.py | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 3 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index afef65e03..33a25664b 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -28,9 +28,9 @@ __all__ = ['Node', 'Identity', 'Literal', 'Zero', 'Variable', 'Sum', 'Product', 'Division', 'Power', 'MathFunction', 'MinValue', 'MaxValue', 'Comparison', 'LogicalNot', 'LogicalAnd', - 'LogicalOr', 'Conditional', 'Index', 'VariableIndex', - 'Indexed', 'ComponentTensor', 'IndexSum', 'ListTensor', - 'Delta', 'partial_indexed'] + 'LogicalOr', 'Conditional', 'Index', 'AffineIndex', + 'VariableIndex', 'Indexed', 'ComponentTensor', 'IndexSum', + 'ListTensor', 'Delta', 'IndexIterator', 'affine_index_group', 'partial_indexed'] class NodeMeta(type): @@ -379,6 +379,22 @@ def __repr__(self): return "Index(%r)" % self.name +class AffineIndex(Index): + """An index in an affine_index_group. Do not instantiate directly but + instead call :func:`affine_index_group`.""" + __slots__ = ('name', 'extent', 'count', 'group') + + def __str__(self): + if self.name is None: + return "i_%d" % self.count + return self.name + + def __repr__(self): + if self.name is None: + return "AffineIndex(%r)" % self.count + return "AffineIndex(%r)" % self.name + + class VariableIndex(IndexBase): """An index that is constant during a single execution of the kernel, but whose value is not known at compile time.""" @@ -566,6 +582,55 @@ def __new__(cls, i, j): self.free_indices = tuple(unique(free_indices)) return self + +class IndexIterator(object): + """An iterator whose value is a multi-index (tuple) iterating over the + extent of the supplied :class:`.Index` objects in a last index varies + fastest (ie 'c') ordering. + + :arg *indices: the indices over whose extent to iterate.""" + def __init__(self, *indices): + + self.affine_groups = set() + for i in indices: + if isinstance(i, AffineIndex): + try: + pos = tuple(indices.index(g) for g in i.group) + except ValueError: + raise ValueError("Only able to iterate over all indices in an affine group at once") + self.affine_groups.add((i.group, pos)) + + self.ndindex = numpy.ndindex(tuple(i.extent for i in indices)) + + def _affine_groups_legal(self, multiindex): + for group, pos in self.affine_groups: + if sum(multiindex[p] for p in pos) >= group[0].extent: + return False + return True + + def __iter__(self): + # Fix this for affine index groups. + while True: + multiindex = self.ndindex.next() + if self._affine_groups_legal(multiindex): + yield multiindex + + +def affine_index_group(n, extent): + """A set of indices whose values are constrained to lie in a simplex + subset of the iteration space. + + :arg n: the number of indices in the group. + :arg extent: sum(indices) < extent + """ + + group = tuple(AffineIndex(extent=extent) for i in range(n)) + + for i in range(n): + group[i].group = group + + return group + def partial_indexed(tensor, indices): """Generalised indexing into a tensor. The number of indices may From 9187d62c3e697e5f4127086861d1b7929e0af8df Mon Sep 17 00:00:00 2001 From: David Ham Date: Wed, 27 Apr 2016 11:39:39 +0100 Subject: [PATCH 243/749] Slightly more ideomatic python --- gem/gem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 33a25664b..2345300dc 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -626,8 +626,8 @@ def affine_index_group(n, extent): group = tuple(AffineIndex(extent=extent) for i in range(n)) - for i in range(n): - group[i].group = group + for g in group: + g.group = group return group From df1de10a57282e1dd889f1685a8896d98686b032 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 12 May 2016 14:19:51 +0100 Subject: [PATCH 244/749] fix up copy-paste mistake --- finat/fiat_elements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 023e8acc3..68cba84b1 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -109,7 +109,7 @@ def __init__(self, cell, degree): class DiscontinuousLagrange(ScalarFiatElement): def __init__(self, cell, degree): - super(Lagrange, self).__init__(cell, degree) + super(DiscontinuousLagrange, self).__init__(cell, degree) self._fiat_element = FIAT.DiscontinuousLagrange(cell, degree) From 4f60e64b5343b36ba1d3321e70ca7dbf127c9e29 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 12 May 2016 14:27:26 +0100 Subject: [PATCH 245/749] export RT/BDM/BDFM elements --- finat/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/finat/__init__.py b/finat/__init__.py index 1be2040f6..aebd0fa89 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -1,4 +1,5 @@ from fiat_elements import Lagrange, DiscontinuousLagrange, GaussLobatto +from fiat_elements import RaviartThomas, BrezziDouglasMarini, BrezziDouglasFortinMarini from bernstein import Bernstein from vectorfiniteelement import VectorFiniteElement from product_elements import ScalarProductElement From 307dcf37684dff7343ce4bbaeb49d0e5a10aeac6 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 12 May 2016 15:37:07 +0100 Subject: [PATCH 246/749] fix flake8 --- gem/gem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 2345300dc..51b88322e 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -582,7 +582,7 @@ def __new__(cls, i, j): self.free_indices = tuple(unique(free_indices)) return self - + class IndexIterator(object): """An iterator whose value is a multi-index (tuple) iterating over the extent of the supplied :class:`.Index` objects in a last index varies @@ -599,7 +599,7 @@ def __init__(self, *indices): except ValueError: raise ValueError("Only able to iterate over all indices in an affine group at once") self.affine_groups.add((i.group, pos)) - + self.ndindex = numpy.ndindex(tuple(i.extent for i in indices)) def _affine_groups_legal(self, multiindex): @@ -607,7 +607,7 @@ def _affine_groups_legal(self, multiindex): if sum(multiindex[p] for p in pos) >= group[0].extent: return False return True - + def __iter__(self): # Fix this for affine index groups. while True: From bf1d7c2ad04ce8d0354bf72b85760f684707132b Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 25 Jul 2016 13:50:12 +0100 Subject: [PATCH 247/749] fix linting for new flake8 version --- gem/interpreter.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/gem/interpreter.py b/gem/interpreter.py index 20056d29f..8ab5205e0 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -93,19 +93,19 @@ def _evaluate(expression, self): raise ValueError("Unhandled node type %s" % type(expression)) -@_evaluate.register(gem.Zero) # noqa: not actually redefinition +@_evaluate.register(gem.Zero) # noqa: F811 def _(e, self): """Zeros produce an array of zeros.""" return Result(numpy.zeros(e.shape, dtype=float)) -@_evaluate.register(gem.Constant) # noqa: not actually redefinition +@_evaluate.register(gem.Constant) # noqa: F811 def _(e, self): """Constants return their array.""" return Result(e.array) -@_evaluate.register(gem.Variable) # noqa: not actually redefinition +@_evaluate.register(gem.Variable) # noqa: F811 def _(e, self): """Look up variables in the provided bindings.""" try: @@ -118,7 +118,7 @@ def _(e, self): return Result(val) -@_evaluate.register(gem.Power) # noqa: not actually redefinition +@_evaluate.register(gem.Power) # noqa: F811 @_evaluate.register(gem.Division) @_evaluate.register(gem.Product) @_evaluate.register(gem.Sum) @@ -136,7 +136,7 @@ def _(e, self): return result -@_evaluate.register(gem.MathFunction) # noqa: not actually redefinition +@_evaluate.register(gem.MathFunction) # noqa: F811 def _(e, self): ops = [self(o) for o in e.children] result = Result.empty(*ops) @@ -148,7 +148,7 @@ def _(e, self): return result -@_evaluate.register(gem.MaxValue) # noqa: not actually redefinition +@_evaluate.register(gem.MaxValue) # noqa: F811 @_evaluate.register(gem.MinValue) def _(e, self): ops = [self(o) for o in e.children] @@ -160,7 +160,7 @@ def _(e, self): return result -@_evaluate.register(gem.Comparison) # noqa: not actually redefinition +@_evaluate.register(gem.Comparison) # noqa: F811 def _(e, self): ops = [self(o) for o in e.children] op = {">": operator.gt, @@ -175,7 +175,7 @@ def _(e, self): return result -@_evaluate.register(gem.LogicalNot) # noqa: not actually redefinition +@_evaluate.register(gem.LogicalNot) # noqa: F811 def _(e, self): val = self(e.children[0]) assert val.arr.dtype == numpy.dtype("bool") @@ -185,7 +185,7 @@ def _(e, self): return result -@_evaluate.register(gem.LogicalAnd) # noqa: not actually redefinition +@_evaluate.register(gem.LogicalAnd) # noqa: F811 def _(e, self): a, b = [self(o) for o in e.children] assert a.arr.dtype == numpy.dtype("bool") @@ -197,7 +197,7 @@ def _(e, self): return result -@_evaluate.register(gem.LogicalOr) # noqa: not actually redefinition +@_evaluate.register(gem.LogicalOr) # noqa: F811 def _(e, self): a, b = [self(o) for o in e.children] assert a.arr.dtype == numpy.dtype("bool") @@ -209,7 +209,7 @@ def _(e, self): return result -@_evaluate.register(gem.Conditional) # noqa: not actually redefinition +@_evaluate.register(gem.Conditional) # noqa: F811 def _(e, self): cond, then, else_ = [self(o) for o in e.children] assert cond.arr.dtype == numpy.dtype("bool") @@ -222,7 +222,7 @@ def _(e, self): return result -@_evaluate.register(gem.Indexed) # noqa: not actually redefinition +@_evaluate.register(gem.Indexed) # noqa: F811 def _(e, self): """Indexing maps shape to free indices""" val = self(e.children[0]) @@ -249,7 +249,7 @@ def _(e, self): return Result(val[idx], val.fids + fids) -@_evaluate.register(gem.ComponentTensor) # noqa: not actually redefinition +@_evaluate.register(gem.ComponentTensor) # noqa: F811 def _(e, self): """Component tensors map free indices to shape.""" val = self(e.children[0]) @@ -269,7 +269,7 @@ def _(e, self): tuple(fids)) -@_evaluate.register(gem.IndexSum) # noqa: not actually redefinition +@_evaluate.register(gem.IndexSum) # noqa: F811 def _(e, self): """Index sums reduce over the given axis.""" val = self(e.children[0]) @@ -278,7 +278,7 @@ def _(e, self): val.fids[:idx] + val.fids[idx+1:]) -@_evaluate.register(gem.ListTensor) # noqa: not actually redefinition +@_evaluate.register(gem.ListTensor) # noqa: F811 def _(e, self): """List tensors just turn into arrays.""" ops = [self(o) for o in e.children] From 82d7c8b976465c86014b076408c21181765a754b Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 9 Sep 2016 16:14:26 +0100 Subject: [PATCH 248/749] WIP: FlexiblyIndexed --- gem/gem.py | 55 ++++++++++++++++++++++++++++++++++++++++++++++++- gem/optimise.py | 25 +++++++++++++++++++--- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index a9722abdb..64d99804a 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -30,7 +30,7 @@ 'MaxValue', 'Comparison', 'LogicalNot', 'LogicalAnd', 'LogicalOr', 'Conditional', 'Index', 'VariableIndex', 'Indexed', 'ComponentTensor', 'IndexSum', 'ListTensor', - 'partial_indexed'] + 'partial_indexed', 'reshape'] class NodeMeta(type): @@ -437,6 +437,41 @@ def __new__(cls, aggregate, multiindex): return self +class FlexiblyIndexed(Scalar): + __slots__ = ('children', 'dim2idxs') + __back__ = ('dim2idxs',) + + def __init__(self, variable, dim2idxs): + assert isinstance(variable, Variable) + assert len(variable.shape) == len(dim2idxs) + + indices = [] + for dim, (offset, idxs) in zip(variable.shape, dim2idxs): + strides = [] + for idx in idxs: + index, stride = idx + strides.append(stride) + + if isinstance(index, Index): + if index.extent is None: + index.set_extent(stride) + elif not (index.extent <= stride): + raise ValueError("Index extent cannot exceed stride") + indices.append(index) + elif isinstance(index, int): + if not (index <= stride): + raise ValueError("Index cannot exceed stride") + else: + raise ValueError("Unexpected index type for flexible indexing") + + if dim is not None and offset + numpy.prod(strides) > dim: + raise ValueError("Offset {0} and indices {1} exceed dimension {2}".format(offset, idxs, dim)) + + self.children = (variable,) + self.dim2idxs = dim2idxs + self.free_indices = tuple(unique(indices)) + + class ComponentTensor(Node): __slots__ = ('children', 'multiindex', 'shape') __back__ = ('multiindex',) @@ -564,3 +599,21 @@ def partial_indexed(tensor, indices): return Indexed(tensor, indices) else: raise ValueError("More indices than rank!") + + +def reshape(variable, *shapes): + dim2idxs = [] + indices = [] + for shape in shapes: + idxs = [] + for e in shape: + i = Index() + i.set_extent(e) + idxs.append((i, e)) + indices.append(i) + dim2idxs.append((0, tuple(idxs))) + expr = FlexiblyIndexed(variable, tuple(dim2idxs)) + if indices: + return ComponentTensor(expr, tuple(indices)) + else: + return expr diff --git a/gem/optimise.py b/gem/optimise.py index c294b56bd..9cb2609d3 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -6,7 +6,8 @@ from singledispatch import singledispatch from gem.node import Memoizer, MemoizerArg, reuse_if_untouched, reuse_if_untouched_arg -from gem.gem import Node, Zero, Sum, Indexed, IndexSum, ComponentTensor +from gem.gem import (Node, Terminal, Zero, Sum, Indexed, IndexSum, + ComponentTensor, FlexiblyIndexed) @singledispatch @@ -25,8 +26,8 @@ def replace_indices(node, self, subst): replace_indices.register(Node)(reuse_if_untouched_arg) -@replace_indices.register(Indexed) # noqa -def _(node, self, subst): +@replace_indices.register(Indexed) +def replace_indices_indexed(node, self, subst): child, = node.children substitute = dict(subst) multiindex = tuple(substitute.get(i, i) for i in node.multiindex) @@ -44,6 +45,24 @@ def _(node, self, subst): return Indexed(new_child, multiindex) +@replace_indices.register(FlexiblyIndexed) +def replace_indices_flexiblyindexed(node, self, subst): + child, = node.children + assert isinstance(child, Terminal) + assert not child.free_indices + + substitute = dict(subst) + dim2idxs = tuple( + (offset, tuple((substitute.get(i, i), s) for i, s in idxs)) + for offset, idxs in node.dim2idxs + ) + + if dim2idxs == node.dim2idxs: + return node + else: + return FlexiblyIndexed(child, dim2idxs) + + def filtered_replace_indices(node, self, subst): """Wrapper for :func:`replace_indices`. At each call removes substitution rules that do not apply.""" From 4d844458f62783fee4eca6bfbe99b67538297b25 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 12 Sep 2016 11:28:35 +0100 Subject: [PATCH 249/749] clean up a little --- gem/gem.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 64d99804a..9aa33b048 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -479,6 +479,10 @@ class ComponentTensor(Node): def __new__(cls, expression, multiindex): assert not expression.shape + # Empty multiindex + if not multiindex: + return expression + # Collect shape shape = tuple(index.extent for index in multiindex) assert all(shape) @@ -613,7 +617,4 @@ def reshape(variable, *shapes): indices.append(i) dim2idxs.append((0, tuple(idxs))) expr = FlexiblyIndexed(variable, tuple(dim2idxs)) - if indices: - return ComponentTensor(expr, tuple(indices)) - else: - return expr + return ComponentTensor(expr, tuple(indices)) From 6370ec002a1089243d911ac598c970537123687e Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 12 Sep 2016 12:05:56 +0100 Subject: [PATCH 250/749] fix bugs --- gem/impero_utils.py | 3 +-- gem/scheduling.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/gem/impero_utils.py b/gem/impero_utils.py index 5fceec744..1e2557cae 100644 --- a/gem/impero_utils.py +++ b/gem/impero_utils.py @@ -56,8 +56,7 @@ def compile_gem(return_variables, expressions, prefix_ordering, remove_zeros=Fal # Collect indices in a deterministic order indices = [] for node in traversal(expressions): - if isinstance(node, gem.Indexed): - indices.extend(node.multiindex) + indices.extend(node.free_indices) # The next two lines remove duplicate elements from the list, but # preserve the ordering, i.e. all elements will appear only once, # in the order of their first occurance in the original list. diff --git a/gem/scheduling.py b/gem/scheduling.py index d936fd7f3..0a99f5783 100644 --- a/gem/scheduling.py +++ b/gem/scheduling.py @@ -117,7 +117,7 @@ def handle(ops, push, decref, node): ops.append(impero.Evaluate(node)) elif isinstance(node, gem.Zero): # should rarely happen assert not node.shape - elif isinstance(node, gem.Indexed): + elif isinstance(node, (gem.Indexed, gem.FlexiblyIndexed)): # Indexing always inlined decref(node.children[0]) elif isinstance(node, gem.IndexSum): From 67c491c8f6871821eae5abc0fdab0ef9a9dcddb5 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 12 Sep 2016 13:37:06 +0100 Subject: [PATCH 251/749] add docstrings --- gem/gem.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/gem/gem.py b/gem/gem.py index 9aa33b048..6da1b902d 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -438,10 +438,27 @@ def __new__(cls, aggregate, multiindex): class FlexiblyIndexed(Scalar): + """Flexible indexing of :py:class:`Variable`s to implement views and + reshapes (splitting dimensions only).""" + __slots__ = ('children', 'dim2idxs') __back__ = ('dim2idxs',) def __init__(self, variable, dim2idxs): + """Construct a flexibly indexed node. + + :arg variable: a :py:class:`Variable` + :arg dim2idxs: describes the mapping of indices + + For example, if ``variable`` is rank two, and ``dim2idxs`` is + + ((1, ((i, 2), (j, 3), (k, 4))), (0, ())) + + then this corresponds to the indexing: + + variable[1 + i*12 + j*4 + k][0] + + """ assert isinstance(variable, Variable) assert len(variable.shape) == len(dim2idxs) @@ -606,6 +623,11 @@ def partial_indexed(tensor, indices): def reshape(variable, *shapes): + """Reshape a variable (splitting indices only). + + :arg variable: a :py:class:`Variable` + :arg shapes: one shape tuple for each dimension of the variable. + """ dim2idxs = [] indices = [] for shape in shapes: From d0285b2a3545b28bb64f047003524dc5c8b21109 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 13 Sep 2016 16:29:53 +0100 Subject: [PATCH 252/749] flake8 fixes --- finat/__init__.py | 16 ++++++++-------- finat/finiteelementbase.py | 1 - 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/finat/__init__.py b/finat/__init__.py index aebd0fa89..1df0c9e98 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -1,8 +1,8 @@ -from fiat_elements import Lagrange, DiscontinuousLagrange, GaussLobatto -from fiat_elements import RaviartThomas, BrezziDouglasMarini, BrezziDouglasFortinMarini -from bernstein import Bernstein -from vectorfiniteelement import VectorFiniteElement -from product_elements import ScalarProductElement -from derivatives import div, grad, curl -import quadrature -import ufl_interface +from fiat_elements import Lagrange, DiscontinuousLagrange, GaussLobatto # noqa: F401 +from fiat_elements import RaviartThomas, BrezziDouglasMarini, BrezziDouglasFortinMarini # noqa: F401 +from bernstein import Bernstein # noqa: F401 +from vectorfiniteelement import VectorFiniteElement # noqa: F401 +from product_elements import ScalarProductElement # noqa: F401 +from derivatives import div, grad, curl # noqa: F401 +import quadrature # noqa: F401 +import ufl_interface # noqa: F401 diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 3fea00037..69a092611 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -1,4 +1,3 @@ -import numpy as np import gem From 508eed28f076e86b5866fec882fac26b24d8d970 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 13 Sep 2016 17:14:57 +0100 Subject: [PATCH 253/749] add future imports --- finat/__init__.py | 18 ++++++++++-------- finat/bernstein.py | 10 ++++++---- finat/derivatives.py | 2 ++ finat/fiat_elements.py | 2 ++ finat/finiteelementbase.py | 2 ++ finat/gauss_jacobi.py | 3 +++ finat/greek_alphabet.py | 2 ++ finat/points.py | 2 ++ finat/product_elements.py | 3 +++ finat/quadrature.py | 6 ++++-- finat/ufl_interface.py | 3 +++ finat/vectorfiniteelement.py | 4 +++- 12 files changed, 42 insertions(+), 15 deletions(-) diff --git a/finat/__init__.py b/finat/__init__.py index 1df0c9e98..7ef79b593 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -1,8 +1,10 @@ -from fiat_elements import Lagrange, DiscontinuousLagrange, GaussLobatto # noqa: F401 -from fiat_elements import RaviartThomas, BrezziDouglasMarini, BrezziDouglasFortinMarini # noqa: F401 -from bernstein import Bernstein # noqa: F401 -from vectorfiniteelement import VectorFiniteElement # noqa: F401 -from product_elements import ScalarProductElement # noqa: F401 -from derivatives import div, grad, curl # noqa: F401 -import quadrature # noqa: F401 -import ufl_interface # noqa: F401 +from __future__ import absolute_import, print_function, division + +from .fiat_elements import Lagrange, DiscontinuousLagrange, GaussLobatto # noqa: F401 +from .fiat_elements import RaviartThomas, BrezziDouglasMarini, BrezziDouglasFortinMarini # noqa: F401 +from .bernstein import Bernstein # noqa: F401 +from .vectorfiniteelement import VectorFiniteElement # noqa: F401 +from .product_elements import ScalarProductElement # noqa: F401 +from .derivatives import div, grad, curl # noqa: F401 +from . import quadrature # noqa: F401 +from . import ufl_interface # noqa: F401 diff --git a/finat/bernstein.py b/finat/bernstein.py index 5f53839d1..245a88001 100644 --- a/finat/bernstein.py +++ b/finat/bernstein.py @@ -1,12 +1,14 @@ +from __future__ import absolute_import, print_function, division + from .finiteelementbase import FiniteElementBase import numpy as np def pd(sd, d): if sd == 3: - return (d + 1) * (d + 2) * (d + 3) / 6 + return (d + 1) * (d + 2) * (d + 3) // 6 elif sd == 2: - return (d + 1) * (d + 2) / 2 + return (d + 1) * (d + 2) // 2 elif sd == 1: return d + 1 else: @@ -28,5 +30,5 @@ def dofs_shape(self): degree = self.degree dim = self.cell.get_spatial_dimension() - return (int(np.prod(xrange(degree + 1, degree + 1 + dim)) / - np.prod(xrange(1, dim + 1))),) + return (np.prod(range(degree + 1, degree + 1 + dim)) // + np.prod(range(1, dim + 1)),) diff --git a/finat/derivatives.py b/finat/derivatives.py index 14c8e700b..080c32bae 100644 --- a/finat/derivatives.py +++ b/finat/derivatives.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import, print_function, division + class Derivative(object): "Abstract symbolic object for a derivative." diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 68cba84b1..84d9d3661 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import, print_function, division + from .finiteelementbase import FiniteElementBase import FIAT import gem diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 69a092611..0e3018f5c 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import, print_function, division + import gem diff --git a/finat/gauss_jacobi.py b/finat/gauss_jacobi.py index cf24682ba..7a2ee928d 100644 --- a/finat/gauss_jacobi.py +++ b/finat/gauss_jacobi.py @@ -1,4 +1,7 @@ """Basic tools for getting Gauss points and quadrature rules.""" + +from __future__ import absolute_import, print_function, division + import math from math import factorial import numpy diff --git a/finat/greek_alphabet.py b/finat/greek_alphabet.py index 3fb03d347..e1d3de7d7 100644 --- a/finat/greek_alphabet.py +++ b/finat/greek_alphabet.py @@ -2,6 +2,8 @@ https://gist.github.com/piquadrat/765262#file-greek_alphabet-py """ +from __future__ import absolute_import, print_function, division + def translate_symbol(symbol): """Translates utf-8 sub-strings into compilable variable names""" diff --git a/finat/points.py b/finat/points.py index afac14c03..380fbec00 100644 --- a/finat/points.py +++ b/finat/points.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import, print_function, division + import numpy diff --git a/finat/product_elements.py b/finat/product_elements.py index ce40395cd..6192f9f7a 100644 --- a/finat/product_elements.py +++ b/finat/product_elements.py @@ -1,4 +1,7 @@ """Preliminary support for tensor product elements.""" + +from __future__ import absolute_import, print_function, division + from .finiteelementbase import FiniteElementBase from FIAT.reference_element import TensorProductCell diff --git a/finat/quadrature.py b/finat/quadrature.py index bf531e716..0cefbe2e3 100644 --- a/finat/quadrature.py +++ b/finat/quadrature.py @@ -1,6 +1,8 @@ +from __future__ import absolute_import, print_function, division + import numpy as np -from gauss_jacobi import gauss_jacobi_rule -from points import StroudPointSet, PointSet, TensorPointSet, GaussLobattoPointSet +from .gauss_jacobi import gauss_jacobi_rule +from .points import StroudPointSet, PointSet, TensorPointSet, GaussLobattoPointSet import FIAT import gem diff --git a/finat/ufl_interface.py b/finat/ufl_interface.py index 9d789ab66..af4655c29 100644 --- a/finat/ufl_interface.py +++ b/finat/ufl_interface.py @@ -1,4 +1,7 @@ """Provide interface functions which take UFL objects and return FInAT ones.""" + +from __future__ import absolute_import, print_function, division + import finat import FIAT diff --git a/finat/vectorfiniteelement.py b/finat/vectorfiniteelement.py index 38a9bacc7..c203b348a 100644 --- a/finat/vectorfiniteelement.py +++ b/finat/vectorfiniteelement.py @@ -1,4 +1,6 @@ -from finiteelementbase import FiniteElementBase +from __future__ import absolute_import, print_function, division + +from .finiteelementbase import FiniteElementBase import gem From 320759f6f86658b225c50780a33ffb70b76599f9 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 13 Sep 2016 17:53:46 +0100 Subject: [PATCH 254/749] export FlexiblyIndexed --- gem/gem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 6da1b902d..5a5d9ffb5 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -29,8 +29,8 @@ 'Product', 'Division', 'Power', 'MathFunction', 'MinValue', 'MaxValue', 'Comparison', 'LogicalNot', 'LogicalAnd', 'LogicalOr', 'Conditional', 'Index', 'VariableIndex', - 'Indexed', 'ComponentTensor', 'IndexSum', 'ListTensor', - 'partial_indexed', 'reshape'] + 'Indexed', 'FlexiblyIndexed', 'ComponentTensor', + 'IndexSum', 'ListTensor', 'partial_indexed', 'reshape'] class NodeMeta(type): From cb8e92c67bb048b6d4740df554e7813668f7d0a1 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 13 Sep 2016 14:53:35 +0100 Subject: [PATCH 255/749] generalise {Vector -> Tensor}FiniteElement --- finat/__init__.py | 2 +- finat/tensorfiniteelement.py | 95 ++++++++++++++++++++++++++++++++++++ finat/vectorfiniteelement.py | 87 --------------------------------- 3 files changed, 96 insertions(+), 88 deletions(-) create mode 100644 finat/tensorfiniteelement.py delete mode 100644 finat/vectorfiniteelement.py diff --git a/finat/__init__.py b/finat/__init__.py index 7ef79b593..0874a930e 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -3,7 +3,7 @@ from .fiat_elements import Lagrange, DiscontinuousLagrange, GaussLobatto # noqa: F401 from .fiat_elements import RaviartThomas, BrezziDouglasMarini, BrezziDouglasFortinMarini # noqa: F401 from .bernstein import Bernstein # noqa: F401 -from .vectorfiniteelement import VectorFiniteElement # noqa: F401 +from .tensorfiniteelement import TensorFiniteElement # noqa: F401 from .product_elements import ScalarProductElement # noqa: F401 from .derivatives import div, grad, curl # noqa: F401 from . import quadrature # noqa: F401 diff --git a/finat/tensorfiniteelement.py b/finat/tensorfiniteelement.py new file mode 100644 index 000000000..ca72cf0a2 --- /dev/null +++ b/finat/tensorfiniteelement.py @@ -0,0 +1,95 @@ +from __future__ import absolute_import, print_function, division + +from .finiteelementbase import FiniteElementBase +import gem + + +class TensorFiniteElement(FiniteElementBase): + + def __init__(self, element, shape): + # TODO: Update docstring for arbitrary rank! + r"""A Finite element whose basis functions have the form: + + .. math:: + + \boldsymbol\phi_{i \alpha \beta} = \mathbf{e}_{\alpha} \mathbf{e}_{\beta}^{\mathrm{T}}\phi_i + + Where :math:`\{\mathbf{e}_\alpha,\, \alpha=0\ldots\mathrm{shape[0]}\}` and + :math:`\{\mathbf{e}_\beta,\, \beta=0\ldots\mathrm{shape[1]}\}` are + the bases for :math:`\mathbb{R}^{\mathrm{shape[0]}}` and + :math:`\mathbb{R}^{\mathrm{shape[1]}}` respectively; and + :math:`\{\phi_i\}` is the basis for the corresponding scalar + finite element space. + + :param element: The scalar finite element. + :param shape: The geometric shape of the tensor element. + + :math:`\boldsymbol\phi_{i\alpha\beta}` is, of course, tensor-valued. If + we subscript the vector-value with :math:`\gamma\epsilon` then we can write: + + .. math:: + \boldsymbol\phi_{\gamma\epsilon(i\alpha\beta)} = \delta_{\gamma\alpha}\delta{\epsilon\beta}\phi_i + + This form enables the simplification of the loop nests which + will eventually be created, so it is the form we employ here.""" + super(TensorFiniteElement, self).__init__() + + self._cell = element._cell + self._degree = element._degree + + self._shape = shape + + self._base_element = element + + @property + def base_element(self): + """The base element of this tensor element.""" + return self._base_element + + @property + def index_shape(self): + return self._base_element.index_shape + self._shape + + @property + def value_shape(self): + return self._base_element.value_shape + self._shape + + def basis_evaluation(self, q, entity=None, derivative=0): + r"""Produce the recipe for basis function evaluation at a set of points :math:`q`: + + .. math:: + \boldsymbol\phi_{(\gamma \epsilon) (i \alpha \beta) q} = \delta_{\alpha \gamma}\delta{\beta \epsilon}\phi_{i q} + + \nabla\boldsymbol\phi_{(\epsilon \gamma \zeta) (i \alpha \beta) q} = \delta_{\alpha \epsilon} \deta{\beta \gamma}\nabla\phi_{\zeta i q} + """ + + scalarbasis = self._base_element.basis_evaluation(q, entity, derivative) + + indices = tuple(gem.Index() for i in scalarbasis.shape) + + # Work out which of the indices are for what. + qi = len(q.index_shape) + len(self._base_element.index_shape) + d = derivative + + # New basis function and value indices. + i = tuple(gem.Index(extent=d) for d in self._shape) + vi = tuple(gem.Index(extent=d) for d in self._shape) + + new_indices = indices[:qi] + i + indices[qi: len(indices) - d] + vi + indices[len(indices) - d:] + + return gem.ComponentTensor(gem.Product(reduce(gem.Product, + (gem.Delta(j, k) + for j, k in zip(i, vi))), + gem.Indexed(scalarbasis, indices)), + new_indices) + + def __hash__(self): + """TensorFiniteElements are equal if they have the same base element + and shape.""" + return hash((self._shape, self._base_element)) + + def __eq__(self, other): + """TensorFiniteElements are equal if they have the same base element + and shape.""" + return type(self) == type(other) and self._shape == other._shape and \ + self._base_element == other._base_element diff --git a/finat/vectorfiniteelement.py b/finat/vectorfiniteelement.py deleted file mode 100644 index c203b348a..000000000 --- a/finat/vectorfiniteelement.py +++ /dev/null @@ -1,87 +0,0 @@ -from __future__ import absolute_import, print_function, division - -from .finiteelementbase import FiniteElementBase -import gem - - -class VectorFiniteElement(FiniteElementBase): - - def __init__(self, element, dimension): - r"""A Finite element whose basis functions have the form: - - .. math:: - - \boldsymbol\phi_{\beta i} = \mathbf{e}_{\beta}\phi_i - - Where :math:`\{\mathbf{e}_\beta,\, \beta=0\ldots\mathrm{dim}\}` is - the basis for :math:`\mathbb{R}^{\mathrm{dim}}` and - :math:`\{\phi_i\}` is the basis for the corresponding scalar - finite element space. - - :param element: The scalar finite element. - :param dimension: The geometric dimension of the vector element. - - :math:`\boldsymbol\phi_{i\beta}` is, of course, vector-valued. If - we subscript the vector-value with :math:`\alpha` then we can write: - - .. math:: - \boldsymbol\phi_{\alpha(i\beta)} = \delta_{\alpha\beta}\phi_i - - This form enables the simplification of the loop nests which - will eventually be created, so it is the form we employ here.""" - super(VectorFiniteElement, self).__init__() - - self._cell = element._cell - self._degree = element._degree - - self._dimension = dimension - - self._base_element = element - - @property - def index_shape(self): - return self._base_element.index_shape + (self._dimension,) - - @property - def value_shape(self): - return self._base_element.value_shape + (self._dimension,) - - def basis_evaluation(self, q, entity=None, derivative=0): - r"""Produce the recipe for basis function evaluation at a set of points :math:`q`: - - .. math:: - \boldsymbol\phi_{\alpha (i \beta) q} = \delta_{\alpha \beta}\phi_{i q} - - \nabla\boldsymbol\phi_{(\alpha \gamma) (i \beta) q} = \delta_{\alpha \beta}\nabla\phi_{\gamma i q} - """ - - scalarbasis = self._base_element.basis_evaluation(q, entity, derivative) - - indices = tuple(gem.Index() for i in scalarbasis.shape) - - # Work out which of the indices are for what. - qi = len(q.index_shape) + len(self._base_element.index_shape) - d = derivative - - # New basis function and value indices. - i = gem.Index(extent=self._dimension) - vi = gem.Index(extent=self._dimension) - - new_indices = indices[:qi] + (i,) + indices[qi: len(indices) - d] + (vi,) + indices[len(indices) - d:] - - return gem.ComponentTensor(gem.Product(gem.Delta(i, vi), - gem.Indexed(scalarbasis, indices)), - new_indices) - - def __hash__(self): - """VectorFiniteElements are equal if they have the same base element - and dimension.""" - - return hash((self._dimension, self._base_element)) - - def __eq__(self, other): - """VectorFiniteElements are equal if they have the same base element - and dimension.""" - - return self._dimension == other._dimension and\ - self._base_element == other._base_element From 615eae51c8f20e66684b72d4f642438514d23579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Homolya?= Date: Thu, 15 Sep 2016 14:57:22 +0100 Subject: [PATCH 256/749] Add missing import for Python 3 --- finat/tensorfiniteelement.py | 1 + 1 file changed, 1 insertion(+) diff --git a/finat/tensorfiniteelement.py b/finat/tensorfiniteelement.py index ca72cf0a2..1625a30ff 100644 --- a/finat/tensorfiniteelement.py +++ b/finat/tensorfiniteelement.py @@ -1,5 +1,6 @@ from __future__ import absolute_import, print_function, division +from functools import reduce from .finiteelementbase import FiniteElementBase import gem From cbfeb4986fb523fd8cf05e0a60cc20ce056b9f30 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 19 Sep 2016 10:50:08 +0100 Subject: [PATCH 257/749] make index ordering deterministic again Was broken in 6370ec002a1089243d911ac598c970537123687e. --- gem/impero_utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/gem/impero_utils.py b/gem/impero_utils.py index 1e2557cae..b6f5b87f5 100644 --- a/gem/impero_utils.py +++ b/gem/impero_utils.py @@ -56,7 +56,12 @@ def compile_gem(return_variables, expressions, prefix_ordering, remove_zeros=Fal # Collect indices in a deterministic order indices = [] for node in traversal(expressions): - indices.extend(node.free_indices) + if isinstance(node, gem.Indexed): + indices.extend(node.multiindex) + elif isinstance(node, gem.FlexiblyIndexed): + for offset, idxs in node.dim2idxs: + for index, stride in idxs: + indices.append(index) # The next two lines remove duplicate elements from the list, but # preserve the ordering, i.e. all elements will appear only once, # in the order of their first occurance in the original list. From 82bbaf5a1a6ad628505c9ec0a91c067686530c9b Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 19 Sep 2016 10:51:32 +0100 Subject: [PATCH 258/749] make free index ordering consistent ... within a single process, but the point is that x.free_indices == y.free_indices will always work correctly. --- gem/gem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 5a5d9ffb5..bcafcae9e 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -515,7 +515,7 @@ def __new__(cls, expression, multiindex): # Collect free indices assert set(multiindex) <= set(expression.free_indices) - self.free_indices = tuple(set(expression.free_indices) - set(multiindex)) + self.free_indices = tuple(unique(list(set(expression.free_indices) - set(multiindex)))) return self @@ -540,7 +540,7 @@ def __new__(cls, summand, index): # Collect shape and free indices assert index in summand.free_indices - self.free_indices = tuple(set(summand.free_indices) - {index}) + self.free_indices = tuple(unique(list(set(summand.free_indices) - {index}))) return self From 86509f0207e959a4126c4762bf87024a866d808e Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 23 Sep 2016 10:15:57 +0100 Subject: [PATCH 259/749] futurize --stage1 --all-imports --- gem/__init__.py | 2 +- gem/gem.py | 2 +- gem/impero.py | 2 +- gem/impero_utils.py | 2 +- gem/interpreter.py | 2 +- gem/node.py | 2 +- gem/optimise.py | 3 ++- gem/scheduling.py | 2 +- 8 files changed, 9 insertions(+), 8 deletions(-) diff --git a/gem/__init__.py b/gem/__init__.py index 521aea6b4..ae6533950 100644 --- a/gem/__init__.py +++ b/gem/__init__.py @@ -1,3 +1,3 @@ -from __future__ import absolute_import +from __future__ import absolute_import, print_function, division from gem.gem import * # noqa diff --git a/gem/gem.py b/gem/gem.py index bcafcae9e..8b2e90701 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -14,7 +14,7 @@ indices. """ -from __future__ import absolute_import +from __future__ import absolute_import, print_function, division from abc import ABCMeta from itertools import chain diff --git a/gem/impero.py b/gem/impero.py index fb662e1d9..04d393a7a 100644 --- a/gem/impero.py +++ b/gem/impero.py @@ -10,7 +10,7 @@ (Command?) after clicking on them. """ -from __future__ import absolute_import +from __future__ import absolute_import, print_function, division from abc import ABCMeta, abstractmethod diff --git a/gem/impero_utils.py b/gem/impero_utils.py index b6f5b87f5..f9b5bec29 100644 --- a/gem/impero_utils.py +++ b/gem/impero_utils.py @@ -6,7 +6,7 @@ C code or a COFFEE AST. """ -from __future__ import absolute_import +from __future__ import absolute_import, print_function, division import collections import itertools diff --git a/gem/interpreter.py b/gem/interpreter.py index 8ab5205e0..e309f1583 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -1,7 +1,7 @@ """ An interpreter for GEM trees. """ -from __future__ import absolute_import +from __future__ import absolute_import, print_function, division import numpy import operator diff --git a/gem/node.py b/gem/node.py index d1c72cc71..eeac833df 100644 --- a/gem/node.py +++ b/gem/node.py @@ -1,7 +1,7 @@ """Generic abstract node class and utility functions for creating expression DAG languages.""" -from __future__ import absolute_import +from __future__ import absolute_import, print_function, division import collections diff --git a/gem/optimise.py b/gem/optimise.py index 9cb2609d3..f5351a5f0 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -1,8 +1,9 @@ """A set of routines implementing various transformations on GEM expressions.""" -from __future__ import absolute_import +from __future__ import absolute_import, print_function, division +from functools import reduce from singledispatch import singledispatch from gem.node import Memoizer, MemoizerArg, reuse_if_untouched, reuse_if_untouched_arg diff --git a/gem/scheduling.py b/gem/scheduling.py index 0a99f5783..767abce4b 100644 --- a/gem/scheduling.py +++ b/gem/scheduling.py @@ -1,7 +1,7 @@ """Schedules operations to evaluate a multi-root expression DAG, forming an ordered list of Impero terminals.""" -from __future__ import absolute_import +from __future__ import absolute_import, print_function, division import collections import functools From 595f7c6f88bd1dc38d954280facaabea924a7089 Mon Sep 17 00:00:00 2001 From: David Ham Date: Wed, 3 Aug 2016 15:23:10 +0900 Subject: [PATCH 260/749] Add some more elements to FInAT. Conflicts: finat/__init__.py --- finat/__init__.py | 4 +++- finat/fiat_elements.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/finat/__init__.py b/finat/__init__.py index 0874a930e..f71288416 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -1,7 +1,9 @@ from __future__ import absolute_import, print_function, division from .fiat_elements import Lagrange, DiscontinuousLagrange, GaussLobatto # noqa: F401 -from .fiat_elements import RaviartThomas, BrezziDouglasMarini, BrezziDouglasFortinMarini # noqa: F401 +from .fiat_elements import RaviartThomas, DiscontinuousRaviartThomas # noqa: F401 +from .fiat_elements import BrezziDouglasMarini, BrezziDouglasFortinMarini # noqa: F401 +from .fiat_elements import Nedelec, NedelecSecondKind, Regge # noqa: F401 from .bernstein import Bernstein # noqa: F401 from .tensorfiniteelement import TensorFiniteElement # noqa: F401 from .product_elements import ScalarProductElement # noqa: F401 diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 84d9d3661..77710fb56 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -102,6 +102,13 @@ def __init__(self, cell, degree): self._fiat_element = FIAT.Lagrange(cell, degree) +class Regge(ScalarFiatElement): + def __init__(self, cell, degree): + super(Regge, self).__init__(cell, degree) + + self._fiat_element = FIAT.Regge(cell, degree) + + class GaussLobatto(ScalarFiatElement): def __init__(self, cell, degree): super(GaussLobatto, self).__init__(cell, degree) @@ -129,6 +136,13 @@ def __init__(self, cell, degree): self._fiat_element = FIAT.RaviartThomas(cell, degree) +class DiscontinuousRaviartThomas(VectorFiatElement): + def __init__(self, cell, degree): + super(DiscontinuousRaviartThomas, self).__init__(cell, degree) + + self._fiat_element = FIAT.DiscontinuousRaviartThomas(cell, degree) + + class BrezziDouglasMarini(VectorFiatElement): def __init__(self, cell, degree): super(BrezziDouglasMarini, self).__init__(cell, degree) @@ -141,3 +155,17 @@ def __init__(self, cell, degree): super(BrezziDouglasFortinMarini, self).__init__(cell, degree) self._fiat_element = FIAT.BrezziDouglasFortinMarini(cell, degree) + + +class Nedelec(VectorFiatElement): + def __init__(self, cell, degree): + super(Nedelec, self).__init__(cell, degree) + + self._fiat_element = FIAT.Nedelec(cell, degree) + + +class NedelecSecondKind(VectorFiatElement): + def __init__(self, cell, degree): + super(NedelecSecondKind, self).__init__(cell, degree) + + self._fiat_element = FIAT.NedelecSecondKind(cell, degree) From 40694e36b238a62fc770fd6a043d2c0b4cb1c717 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 23 Sep 2016 10:55:10 +0100 Subject: [PATCH 261/749] make metaclasses Python 2/3 compatible --- gem/gem.py | 13 ++++++------- gem/impero.py | 5 ++--- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 8b2e90701..4f36b0cfe 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -15,12 +15,14 @@ """ from __future__ import absolute_import, print_function, division +from six import with_metaclass from abc import ABCMeta from itertools import chain +from operator import attrgetter + import numpy from numpy import asarray, unique -from operator import attrgetter from gem.node import Node as NodeBase @@ -52,11 +54,9 @@ def __call__(self, *args, **kwargs): return obj -class Node(NodeBase): +class Node(with_metaclass(NodeMeta, NodeBase)): """Abstract GEM node class.""" - __metaclass__ = NodeMeta - __slots__ = ('free_indices') def is_equal(self, other): @@ -339,10 +339,9 @@ def __init__(self, condition, then, else_): self.shape = then.shape -class IndexBase(object): +class IndexBase(with_metaclass(ABCMeta)): """Abstract base class for indices.""" - - __metaclass__ = ABCMeta + pass IndexBase.register(int) diff --git a/gem/impero.py b/gem/impero.py index 04d393a7a..9994e5ff5 100644 --- a/gem/impero.py +++ b/gem/impero.py @@ -11,6 +11,7 @@ """ from __future__ import absolute_import, print_function, division +from six import with_metaclass from abc import ABCMeta, abstractmethod @@ -23,11 +24,9 @@ class Node(NodeBase): __slots__ = () -class Terminal(Node): +class Terminal(with_metaclass(ABCMeta, Node)): """Abstract class for terminal Impero nodes""" - __metaclass__ = ABCMeta - __slots__ = () children = () From bf60d9e93ad19887f818a9327e6ca819580c216d Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 23 Sep 2016 11:11:41 +0100 Subject: [PATCH 262/749] GEM: list/iterable compatibility improvements --- gem/impero_utils.py | 5 +++-- gem/interpreter.py | 3 ++- gem/node.py | 3 ++- gem/optimise.py | 3 ++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/gem/impero_utils.py b/gem/impero_utils.py index f9b5bec29..215fd3832 100644 --- a/gem/impero_utils.py +++ b/gem/impero_utils.py @@ -7,6 +7,7 @@ """ from __future__ import absolute_import, print_function, division +from six.moves import zip import collections import itertools @@ -75,7 +76,7 @@ def compile_gem(return_variables, expressions, prefix_ordering, remove_zeros=Fal get_indices = lambda expr: apply_ordering(expr.free_indices) # Build operation ordering - ops = scheduling.emit_operations(zip(return_variables, expressions), get_indices) + ops = scheduling.emit_operations(list(zip(return_variables, expressions)), get_indices) # Empty kernel if len(ops) == 0: @@ -177,7 +178,7 @@ def make_loop_tree(ops, get_indices, level=0): else: statements.extend(op_group) # Remove no-op terminals from the tree - statements = filter(lambda s: not isinstance(s, imp.Noop), statements) + statements = [s for s in statements if not isinstance(s, imp.Noop)] return imp.Block(statements) diff --git a/gem/interpreter.py b/gem/interpreter.py index e309f1583..1ec64391d 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -2,6 +2,7 @@ An interpreter for GEM trees. """ from __future__ import absolute_import, print_function, division +from six.moves import map import numpy import operator @@ -302,4 +303,4 @@ def evaluate(expressions, bindings=None): exprs = (expressions, ) mapper = node.Memoizer(_evaluate) mapper.bindings = bindings if bindings is not None else {} - return map(mapper, exprs) + return list(map(mapper, exprs)) diff --git a/gem/node.py b/gem/node.py index eeac833df..a5ed3a699 100644 --- a/gem/node.py +++ b/gem/node.py @@ -2,6 +2,7 @@ expression DAG languages.""" from __future__ import absolute_import, print_function, division +from six.moves import map import collections @@ -201,7 +202,7 @@ def __call__(self, node, arg): def reuse_if_untouched(node, self): """Reuse if untouched recipe""" - new_children = map(self, node.children) + new_children = list(map(self, node.children)) if all(nc == c for nc, c in zip(new_children, node.children)): return node else: diff --git a/gem/optimise.py b/gem/optimise.py index f5351a5f0..c97eb3253 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -2,6 +2,7 @@ expressions.""" from __future__ import absolute_import, print_function, division +from six.moves import map from functools import reduce from singledispatch import singledispatch @@ -112,4 +113,4 @@ def unroll_indexsum(expressions, max_extent): """ mapper = Memoizer(_unroll_indexsum) mapper.max_extent = max_extent - return map(mapper, expressions) + return list(map(mapper, expressions)) From a5d4050febc270c9bc6a3c597def57e193442ed7 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Sat, 24 Sep 2016 13:46:30 +0100 Subject: [PATCH 263/749] GEM: not all objects are comparable in Python 3 --- gem/gem.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 4f36b0cfe..5f34a9d60 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -22,7 +22,7 @@ from operator import attrgetter import numpy -from numpy import asarray, unique +from numpy import asarray from gem.node import Node as NodeBase @@ -48,8 +48,8 @@ def __call__(self, *args, **kwargs): # Set free_indices if not set already if not hasattr(obj, 'free_indices'): - cfi = list(chain(*[c.free_indices for c in obj.children])) - obj.free_indices = tuple(unique(cfi)) + obj.free_indices = unique(chain(*[c.free_indices + for c in obj.children])) return obj @@ -431,7 +431,7 @@ def __new__(cls, aggregate, multiindex): self.multiindex = multiindex new_indices = tuple(i for i in multiindex if isinstance(i, Index)) - self.free_indices = tuple(unique(aggregate.free_indices + new_indices)) + self.free_indices = unique(aggregate.free_indices + new_indices) return self @@ -485,7 +485,7 @@ def __init__(self, variable, dim2idxs): self.children = (variable,) self.dim2idxs = dim2idxs - self.free_indices = tuple(unique(indices)) + self.free_indices = unique(indices) class ComponentTensor(Node): @@ -514,7 +514,7 @@ def __new__(cls, expression, multiindex): # Collect free indices assert set(multiindex) <= set(expression.free_indices) - self.free_indices = tuple(unique(list(set(expression.free_indices) - set(multiindex)))) + self.free_indices = unique(set(expression.free_indices) - set(multiindex)) return self @@ -539,7 +539,7 @@ def __new__(cls, summand, index): # Collect shape and free indices assert index in summand.free_indices - self.free_indices = tuple(unique(list(set(summand.free_indices) - {index}))) + self.free_indices = unique(set(summand.free_indices) - {index}) return self @@ -598,6 +598,15 @@ def get_hash(self): return hash((type(self), self.shape, self.children)) +def unique(indices): + """Sorts free indices and eliminates duplicates. + + :arg indices: iterable of indices + :returns: sorted tuple of unique free indices + """ + return tuple(sorted(set(indices), key=id)) + + def partial_indexed(tensor, indices): """Generalised indexing into a tensor. The number of indices may be less than or equal to the rank of the tensor, so the result may From a3e26fcd6c56500b14b1c8623c625ac196dbfd43 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Sat, 24 Sep 2016 16:48:58 +0100 Subject: [PATCH 264/749] GEM: more on comparability of indices --- gem/gem.py | 4 ++++ gem/impero_utils.py | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 5f34a9d60..9e1baa6d4 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -378,6 +378,10 @@ def __repr__(self): return "Index(%r)" % self.count return "Index(%r)" % self.name + def __lt__(self, other): + # Allow sorting of free indices in Python 3 + return id(self) < id(other) + class VariableIndex(IndexBase): """An index that is constant during a single execution of the diff --git a/gem/impero_utils.py b/gem/impero_utils.py index 215fd3832..ae08d1f0b 100644 --- a/gem/impero_utils.py +++ b/gem/impero_utils.py @@ -58,11 +58,14 @@ def compile_gem(return_variables, expressions, prefix_ordering, remove_zeros=Fal indices = [] for node in traversal(expressions): if isinstance(node, gem.Indexed): - indices.extend(node.multiindex) + for index in node.multiindex: + if isinstance(index, gem.Index): + indices.append(index) elif isinstance(node, gem.FlexiblyIndexed): for offset, idxs in node.dim2idxs: for index, stride in idxs: - indices.append(index) + if isinstance(index, gem.Index): + indices.append(index) # The next two lines remove duplicate elements from the list, but # preserve the ordering, i.e. all elements will appear only once, # in the order of their first occurance in the original list. From 9af0dfe30a9aac292306bbd5e4468788b6aec95a Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 26 Sep 2016 12:19:38 +0100 Subject: [PATCH 265/749] fix facet integral performance issue --- gem/__init__.py | 1 + gem/optimise.py | 73 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/gem/__init__.py b/gem/__init__.py index 521aea6b4..32f33f76a 100644 --- a/gem/__init__.py +++ b/gem/__init__.py @@ -1,3 +1,4 @@ from __future__ import absolute_import from gem.gem import * # noqa +from gem.optimise import select_expression # noqa diff --git a/gem/optimise.py b/gem/optimise.py index 7380af675..f9230b123 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -9,7 +9,8 @@ from gem.gem import (Node, Terminal, Identity, Literal, Zero, Sum, Comparison, Conditional, Index, VariableIndex, Indexed, FlexiblyIndexed, IndexSum, - ComponentTensor, Delta) + ComponentTensor, ListTensor, Delta, + partial_indexed) @singledispatch @@ -27,6 +28,17 @@ def replace_indices(node, self, subst): replace_indices.register(Node)(reuse_if_untouched_arg) +@replace_indices.register(Delta) +def replace_indices_delta(node, self, subst): + substitute = dict(subst) + i = substitute.get(node.i, node.i) + j = substitute.get(node.j, node.j) + if i == node.i and j == node.j: + return node + else: + return Delta(i, j) + + @replace_indices.register(Indexed) def replace_indices_indexed(node, self, subst): child, = node.children @@ -77,6 +89,65 @@ def remove_componenttensors(expressions): return [mapper(expression, ()) for expression in expressions] +def _select_expression(expressions, index): + """Helper function to select an expression from a list of + expressions with an index. This function expect sanitised input, + one should normally call :py:func:`select_expression` instead. + + :arg expressions: a list of expressions + :arg index: an index (free, fixed or variable) + :returns: an expression + """ + expr = expressions[0] + if all(e == expr for e in expressions): + return expr + + cls = type(expr) + if all(type(e) == cls for e in expressions): + if not cls.__front__ and not cls.__back__: + assert all(len(e.children) == len(expr.children) for e in expressions) + assert len(expr.children) > 0 + + return cls(*[_select_expression(nth_children, index) + for nth_children in zip(*[e.children + for e in expressions])]) + elif issubclass(cls, Indexed): + assert all(e.multiindex == expr.multiindex for e in expressions) + return Indexed(_select_expression([e.children[0] + for e in expressions], index), expr.multiindex) + elif issubclass(cls, Literal): + return partial_indexed(ListTensor(expressions), (index,)) + else: + assert False + else: + assert False + + +def select_expression(expressions, index): + """Select an expression from a list of expressions with an index. + Semantically equivalent to + + partial_indexed(ListTensor(expressions), (index,)) + + but has a much more optimised implementation. + + :arg expressions: a list of expressions of the same shape + :arg index: an index (free, fixed or variable) + :returns: an expression of the same shape as the given expressions + """ + # Check arguments + shape = expressions[0].shape + assert all(e.shape == shape for e in expressions) + + # Sanitise input expressions + alpha = tuple(Index() for s in shape) + exprs = remove_componenttensors([Indexed(e, alpha) for e in expressions]) + + # Factor the expressions recursively and convert result + selected = _select_expression(exprs, index) + return ComponentTensor(selected, alpha) + + @singledispatch def _replace_delta(node, self): raise AssertionError("cannot handle type %s" % type(node)) From 69b8dab2adee9cacfcbdd0d83daa100054711388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Homolya?= Date: Wed, 5 Oct 2016 23:38:17 -0400 Subject: [PATCH 266/749] remove file gauss_jacobi.py Duplicates FIAT code. --- finat/gauss_jacobi.py | 115 ------------------------------------------ finat/quadrature.py | 2 +- 2 files changed, 1 insertion(+), 116 deletions(-) delete mode 100644 finat/gauss_jacobi.py diff --git a/finat/gauss_jacobi.py b/finat/gauss_jacobi.py deleted file mode 100644 index 7a2ee928d..000000000 --- a/finat/gauss_jacobi.py +++ /dev/null @@ -1,115 +0,0 @@ -"""Basic tools for getting Gauss points and quadrature rules.""" - -from __future__ import absolute_import, print_function, division - -import math -from math import factorial -import numpy - -__copyright__ = "Copyright (C) 2014 Robert C. Kirby" -__license__ = """ -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" - - -def compute_gauss_jacobi_points(a, b, m): - """Computes the m roots of P_{m}^{a,b} on [-1,1] by Newton's method. - The initial guesses are the Chebyshev points. Algorithm - implemented in Python from the pseudocode given by Karniadakis and - Sherwin""" - x = [] - eps = 1.e-8 - max_iter = 100 - for k in range(0, m): - r = -math.cos((2.0 * k + 1.0) * math.pi / (2.0 * m)) - if k > 0: - r = 0.5 * (r + x[k - 1]) - j = 0 - delta = 2 * eps - while j < max_iter: - s = 0 - for i in range(0, k): - s = s + 1.0 / (r - x[i]) - f = eval_jacobi(a, b, m, r) - fp = eval_jacobi_deriv(a, b, m, r) - delta = f / (fp - f * s) - - r = r - delta - if math.fabs(delta) < eps: - break - else: - j = j + 1 - - x.append(r) - return x - - -def gauss_jacobi_rule(a, b, m): - xs = compute_gauss_jacobi_points(a, b, m) - - a1 = math.pow(2, a + b + 1) - a2 = math.gamma(a + m + 1) - a3 = math.gamma(b + m + 1) - a4 = math.gamma(a + b + m + 1) - a5 = factorial(m) - a6 = a1 * a2 * a3 / a4 / a5 - - ws = [a6 / (1.0 - x ** 2.0) / eval_jacobi_deriv(a, b, m, x) ** 2.0 - for x in xs] - - return numpy.array(xs), numpy.array(ws) - - -def eval_jacobi(a, b, n, x): - """Evaluates the nth jacobi polynomial with weight parameters a,b at a - point x. Recurrence relations implemented from the pseudocode - given in Karniadakis and Sherwin, Appendix B""" - - if 0 == n: - return 1.0 - elif 1 == n: - return 0.5 * (a - b + (a + b + 2.0) * x) - else: # 2 <= n - apb = a + b - pn2 = 1.0 - pn1 = 0.5 * (a - b + (apb + 2.0) * x) - p = 0 - for k in range(2, n + 1): - a1 = 2.0 * k * (k + apb) * (2.0 * k + apb - 2.0) - a2 = (2.0 * k + apb - 1.0) * (a * a - b * b) - a3 = (2.0 * k + apb - 2.0) \ - * (2.0 * k + apb - 1.0) \ - * (2.0 * k + apb) - a4 = 2.0 * (k + a - 1.0) * (k + b - 1.0) \ - * (2.0 * k + apb) - a2 = a2 / a1 - a3 = a3 / a1 - a4 = a4 / a1 - p = (a2 + a3 * x) * pn1 - a4 * pn2 - pn2 = pn1 - pn1 = p - return p - - -def eval_jacobi_deriv(a, b, n, x): - """Evaluates the first derivative of P_{n}^{a,b} at a point x.""" - if n == 0: - return 0.0 - else: - return 0.5 * (a + b + n + 1) * eval_jacobi(a + 1, b + 1, n - 1, x) diff --git a/finat/quadrature.py b/finat/quadrature.py index 0cefbe2e3..d6b0e0495 100644 --- a/finat/quadrature.py +++ b/finat/quadrature.py @@ -1,7 +1,7 @@ from __future__ import absolute_import, print_function, division import numpy as np -from .gauss_jacobi import gauss_jacobi_rule +from FIAT.gauss_jacobi import gauss_jacobi_rule from .points import StroudPointSet, PointSet, TensorPointSet, GaussLobattoPointSet import FIAT import gem From 4764f6678e4fca1e77ae08d2ce451134cff0b4df Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 11 Oct 2016 14:04:33 +0100 Subject: [PATCH 267/749] create gem.utils and add OrderedSet --- gem/impero_utils.py | 13 ++++------ gem/utils.py | 58 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 gem/utils.py diff --git a/gem/impero_utils.py b/gem/impero_utils.py index ae08d1f0b..9e33476c1 100644 --- a/gem/impero_utils.py +++ b/gem/impero_utils.py @@ -12,10 +12,10 @@ import collections import itertools -import numpy from singledispatch import singledispatch from gem.node import traversal, collect_refcount +from gem.utils import OrderedSet from gem import gem, impero as imp, optimise, scheduling @@ -55,22 +55,17 @@ def compile_gem(return_variables, expressions, prefix_ordering, remove_zeros=Fal return_variables, expressions = rv, es # Collect indices in a deterministic order - indices = [] + indices = OrderedSet() for node in traversal(expressions): if isinstance(node, gem.Indexed): for index in node.multiindex: if isinstance(index, gem.Index): - indices.append(index) + indices.add(index) elif isinstance(node, gem.FlexiblyIndexed): for offset, idxs in node.dim2idxs: for index, stride in idxs: if isinstance(index, gem.Index): - indices.append(index) - # The next two lines remove duplicate elements from the list, but - # preserve the ordering, i.e. all elements will appear only once, - # in the order of their first occurance in the original list. - _, unique_indices = numpy.unique(indices, return_index=True) - indices = numpy.asarray(indices)[numpy.sort(unique_indices)] + indices.add(index) # Build ordered index map index_ordering = make_prefix_ordering(indices, prefix_ordering) diff --git a/gem/utils.py b/gem/utils.py new file mode 100644 index 000000000..024a526ae --- /dev/null +++ b/gem/utils.py @@ -0,0 +1,58 @@ +from __future__ import absolute_import, print_function, division + +import collections + + +# This is copied from PyOP2, and it is here to be available for both +# FInAT and TSFC without depending on PyOP2. +class cached_property(object): + """A read-only @property that is only evaluated once. The value is cached + on the object itself rather than the function or class; this should prevent + memory leakage.""" + def __init__(self, fget, doc=None): + self.fget = fget + self.__doc__ = doc or fget.__doc__ + self.__name__ = fget.__name__ + self.__module__ = fget.__module__ + + def __get__(self, obj, cls): + if obj is None: + return self + obj.__dict__[self.__name__] = result = self.fget(obj) + return result + + +class OrderedSet(collections.MutableSet): + """A set that preserves ordering, useful for deterministic code + generation.""" + + def __init__(self, iterable=None): + self._list = list() + self._set = set() + + if iterable is not None: + for item in iterable: + self.add(item) + + def __contains__(self, item): + return item in self._set + + def __iter__(self): + return iter(self._list) + + def __len__(self): + return len(self._list) + + def __repr__(self): + return "OrderedSet({0})".format(self._list) + + def add(self, value): + if value not in self._set: + self._list.append(value) + self._set.add(value) + + def discard(self, value): + # O(n) time complexity: do not use this! + if value in self._set: + self._list.remove(value) + self._set.discard(value) From c2875a4a386b8aabd619c139dc0d1a02af6838aa Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 12 Oct 2016 13:47:34 +0100 Subject: [PATCH 268/749] add proxy class infrastructure --- gem/utils.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/gem/utils.py b/gem/utils.py index 024a526ae..bf5c1973b 100644 --- a/gem/utils.py +++ b/gem/utils.py @@ -22,6 +22,22 @@ def __get__(self, obj, cls): return result +class unset_attribute(object): + """Decorator for listing and documenting instance attributes without + setting a default value.""" + + def __init__(self, f): + """Initialise this property. + + :arg f: dummy method + """ + self.__doc__ = f.__doc__ + self.__name__ = f.__name__ + + def __get__(self, obj, cls): + raise AttributeError("'{0}' object has no attribute '{1}'".format(cls.__name__, self.__name__)) + + class OrderedSet(collections.MutableSet): """A set that preserves ordering, useful for deterministic code generation.""" @@ -56,3 +72,26 @@ def discard(self, value): if value in self._set: self._list.remove(value) self._set.discard(value) + + +def make_proxy_class(name, cls): + """Constructs a proxy class for a given class. Instance attributes + are supposed to be listed e.g. with the unset_attribute decorator, + so that this function find them and create wrappers for them. + + :arg name: name of the new proxy class + :arg cls: the wrapee class to create a proxy for + """ + def __init__(self, wrapee): + self._wrapee = wrapee + + def make_proxy_property(name): + def getter(self): + return getattr(self._wrapee, name) + return property(getter) + + dct = {'__init__': __init__} + for attr in dir(cls): + if not attr.startswith('_'): + dct[attr] = make_proxy_property(attr) + return type(name, (), dct) From cfaf14f5c04fd2de6626bd2141bcbc8f6eebf780 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 17 Oct 2016 13:01:10 +0100 Subject: [PATCH 269/749] Parameters -> Context --- gem/utils.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/gem/utils.py b/gem/utils.py index bf5c1973b..b2804d16f 100644 --- a/gem/utils.py +++ b/gem/utils.py @@ -22,22 +22,6 @@ def __get__(self, obj, cls): return result -class unset_attribute(object): - """Decorator for listing and documenting instance attributes without - setting a default value.""" - - def __init__(self, f): - """Initialise this property. - - :arg f: dummy method - """ - self.__doc__ = f.__doc__ - self.__name__ = f.__name__ - - def __get__(self, obj, cls): - raise AttributeError("'{0}' object has no attribute '{1}'".format(cls.__name__, self.__name__)) - - class OrderedSet(collections.MutableSet): """A set that preserves ordering, useful for deterministic code generation.""" From 9fe31cd55dac2a9d0c0d9a5775178f97e20d6815 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 18 Oct 2016 13:03:12 +0100 Subject: [PATCH 270/749] add optional extent= argument to gem.Index --- gem/gem.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 9e1baa6d4..eaf23ecdc 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -354,12 +354,11 @@ class Index(IndexBase): __slots__ = ('name', 'extent', 'count') - def __init__(self, name=None): + def __init__(self, name=None, extent=None): self.name = name Index._count += 1 self.count = Index._count - # Initialise with indefinite extent - self.extent = None + self.extent = extent def set_extent(self, value): # Set extent, check for consistency @@ -645,8 +644,7 @@ def reshape(variable, *shapes): for shape in shapes: idxs = [] for e in shape: - i = Index() - i.set_extent(e) + i = Index(extent=e) idxs.append((i, e)) indices.append(i) dim2idxs.append((0, tuple(idxs))) From 967f7bbbecb21b69f8b67d83a4fcee32f2b11408 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 19 Oct 2016 10:18:30 +0100 Subject: [PATCH 271/749] major redesign of quadrature rules and point sets --- finat/point_set.py | 110 ++++++++++++++++ finat/points.py | 107 ---------------- finat/quadrature.py | 304 +++++++++++++++++++++++++------------------- 3 files changed, 285 insertions(+), 236 deletions(-) create mode 100644 finat/point_set.py delete mode 100644 finat/points.py diff --git a/finat/point_set.py b/finat/point_set.py new file mode 100644 index 000000000..4d4faec42 --- /dev/null +++ b/finat/point_set.py @@ -0,0 +1,110 @@ +from __future__ import absolute_import, print_function, division +from six import with_metaclass +from six.moves import range + +from abc import ABCMeta, abstractproperty +from itertools import chain, product + +import numpy + +import gem +from gem.utils import cached_property + + +class AbstractPointSet(with_metaclass(ABCMeta)): + """A way of specifying a known set of points, perhaps with some + (tensor) structure.""" + + @abstractproperty + def points(self): + """A flattened numpy array of points with shape + (# of points, point dimension).""" + + @property + def dimension(self): + """Point dimension.""" + _, dim = self.points.shape + return dim + + @abstractproperty + def indices(self): + """GEM indices with matching shape and extent to the structure of the + point set.""" + + @abstractproperty + def expression(self): + """GEM expression describing the points, with free indices + ``self.indices`` and shape (point dimension,).""" + + +class PointSet(AbstractPointSet): + """A basic point set with no internal structure.""" + + def __init__(self, points): + points = numpy.asarray(points) + assert len(points.shape) == 2 + self.points = points + + @cached_property + def points(self): + pass # set at initialisation + + @cached_property + def indices(self): + return (gem.Index(extent=len(self.points)),) + + @cached_property + def expression(self): + return gem.partial_indexed(gem.Literal(self.points), self.indices) + + +class TensorPointSet(AbstractPointSet): + + def __init__(self, factors): + self.factors = tuple(factors) + + @cached_property + def points(self): + return numpy.array([list(chain(*pt_tuple)) + for pt_tuple in product(*[ps.points + for ps in self.factors])]) + + @cached_property + def indices(self): + return tuple(chain(*[ps.indices for ps in self.factors])) + + @cached_property + def expression(self): + result = [] + for point_set in self.factors: + for i in range(point_set.dimension): + result.append(gem.Indexed(point_set.expression, (i,))) + return gem.ListTensor(result) + + +# class MappedMixin(object): +# def __init__(self, *args): +# super(MappedMixin, self).__init__(*args) + +# def map_points(self): +# raise NotImplementedError + + +# class DuffyMappedMixin(MappedMixin): +# def __init__(self, *args): +# super(DuffyMappedMixin, self).__init__(*args) + +# def map_points(self): +# raise NotImplementedError + + +# class StroudPointSet(TensorPointSet, DuffyMappedMixin): +# """A set of points with the structure required for Stroud quadrature.""" + +# def __init__(self, factor_sets): +# super(StroudPointSet, self).__init__(factor_sets) + + +# class GaussLobattoPointSet(PointSet): +# """A set of 1D Gauss Lobatto points. This is a separate class in order +# to allow elements to apply spectral element tricks.""" diff --git a/finat/points.py b/finat/points.py deleted file mode 100644 index 380fbec00..000000000 --- a/finat/points.py +++ /dev/null @@ -1,107 +0,0 @@ -from __future__ import absolute_import, print_function, division - -import numpy - - -class PointSetBase(object): - """A way of specifying a known set of points, perhaps with some - (tensor) structure.""" - - def __init__(self): - pass - - @property - def points(self): - """Return a flattened numpy array of points. - - The array has shape (num_points, topological_dim). - """ - - raise NotImplementedError - - -class PointSet(PointSetBase): - """A basic pointset with no internal structure.""" - - def __init__(self, points): - self._points = numpy.array(points) - - self.extent = slice(self._points.shape[0]) - """A slice which describes how to iterate over this - :class:`PointSet`""" - - @property - def points(self): - """A flattened numpy array of points. - - The array has shape (num_points, topological_dim). - """ - - return self._points - - # def kernel_variable(self, name, kernel_data): - # '''Produce a variable in the kernel data for this point set.''' - # static_key = (id(self), ) - - # static_data = kernel_data.static - - # if static_key in static_data: - # w = static_data[static_key][0] - # else: - # w = Variable(name) - # data = self._points - # static_data[static_key] = (w, lambda: data) - - # return w - - def __getitem__(self, i): - if isinstance(i, int): - return PointSet([self.points[i]]) - else: - return PointSet(self.points[i]) - - -class TensorPointSet(PointSetBase): - def __init__(self, factor_sets): - super(TensorPointSet, self).__init__() - - self.factor_sets = factor_sets - - @property - def points(self): - def helper(loi): - if len(loi) == 1: - return [[x] for x in loi[0]] - else: - return [[x] + y for x in loi[0] for y in helper(loi[1:])] - - return numpy.array(helper([fs.points.tolist() - for fs in self.factor_sets])) - - -class MappedMixin(object): - def __init__(self, *args): - super(MappedMixin, self).__init__(*args) - - def map_points(self): - raise NotImplementedError - - -class DuffyMappedMixin(MappedMixin): - def __init__(self, *args): - super(DuffyMappedMixin, self).__init__(*args) - - def map_points(self): - raise NotImplementedError - - -class StroudPointSet(TensorPointSet, DuffyMappedMixin): - """A set of points with the structure required for Stroud quadrature.""" - - def __init__(self, factor_sets): - super(StroudPointSet, self).__init__(factor_sets) - - -class GaussLobattoPointSet(PointSet): - """A set of 1D Gauss Lobatto points. This is a separate class in order - to allow elements to apply spectral element tricks.""" diff --git a/finat/quadrature.py b/finat/quadrature.py index d6b0e0495..35c2f5a49 100644 --- a/finat/quadrature.py +++ b/finat/quadrature.py @@ -1,150 +1,196 @@ from __future__ import absolute_import, print_function, division +from six import with_metaclass -import numpy as np -from FIAT.gauss_jacobi import gauss_jacobi_rule -from .points import StroudPointSet, PointSet, TensorPointSet, GaussLobattoPointSet -import FIAT -import gem +from abc import ABCMeta, abstractproperty +from functools import reduce +import numpy -def make_quadrature(cell, degree, preferred_quadrature): - '''Return a quadrature rule of the suggested degree in accordance with - the quadrature preferences provided.''' +import gem +from gem.utils import cached_property - # Currently just do the dumb thing. Smart quadrature happens later. - return CollapsedGaussJacobiQuadrature(cell, degree) +from FIAT.reference_element import QUADRILATERAL, TENSORPRODUCT +# from FIAT.quadrature import compute_gauss_jacobi_rule as gauss_jacobi_rule +from FIAT.quadrature_schemes import create_quadrature as fiat_scheme +from finat.point_set import PointSet, TensorPointSet -class QuadratureRule(object): - """Object representing a quadrature rule as a set of points - and a set of weights. - :param cell: The :class:`~FIAT.reference_element.ReferenceElement` - on which this quadrature rule is defined. +def make_quadrature(ref_el, degree, scheme="default"): """ + Generate quadrature rule for given reference element + that will integrate an polynomial of order 'degree' exactly. - def __init__(self, cell, points, weights): - self.cell = cell - self._points = points - self._weights = weights - - @property - def points(self): - '''The quadrature points. For a rule with internal structure, this is - the flattened points.''' - - return self._points - - @property - def weights(self): - '''The quadrature weights. For a rule with internal structure, this is - the flattenened weights.''' - - return self._weights - - @property - def index_shape(self): - '''A tuple indicating the shape of the indices needed to loop over the points.''' - - raise NotImplementedError - - def get_indices(self): - '''A tuple of GEM :class:`Index` of the correct extents to loop over - the basis functions of this element.''' - - return tuple(gem.Index(extent=d) for d in self.index_shape) - - -class CollapsedGaussJacobiQuadrature(QuadratureRule): - def __init__(self, cell, degree): - """Gauss Jacobi Quadrature rule using collapsed coordinates to - accommodate higher order simplices.""" - - points = (degree + 1) // 2 - - rule = FIAT.make_quadrature(cell, points) - - super(CollapsedGaussJacobiQuadrature, self).__init__(cell, - rule.pts, - rule.wts) - - @property - def index_shape(self): - '''A tuple indicating the shape of the indices needed to loop over the points.''' - - return (len(self.points),) + For low-degree (<=6) polynomials on triangles and tetrahedra, this + uses hard-coded rules, otherwise it falls back to a collapsed + Gauss scheme on simplices. On tensor-product cells, it is a + tensor-product quadrature rule of the subcells. + :arg cell: The FIAT cell to create the quadrature for. + :arg degree: The degree of polynomial that the rule should + integrate exactly. + """ + if ref_el.get_shape() == TENSORPRODUCT: + try: + degree = tuple(degree) + except TypeError: + degree = (degree,) * len(ref_el.cells) -class StroudQuadrature(QuadratureRule): - def __init__(self, cell, degree): - """Stroud quadrature rule on simplices.""" + assert len(ref_el.cells) == len(degree) + quad_rules = [make_quadrature(c, d, scheme) + for c, d in zip(ref_el.cells, degree)] + return TensorProductQuadratureRule(quad_rules) - sd = cell.get_spatial_dimension() + if ref_el.get_shape() == QUADRILATERAL: + return make_quadrature(ref_el.product, degree, scheme) - if sd + 1 != len(cell.vertices): - raise ValueError("cell must be a simplex") + if degree < 0: + raise ValueError("Need positive degree, not %d" % degree) - points = np.zeros((sd, degree)) - weights = np.zeros((sd, degree)) + fiat_rule = fiat_scheme(ref_el, degree, scheme) + return QuadratureRule(fiat_rule.get_points(), fiat_rule.get_weights()) - for d in range(1, sd + 1): - [x, w] = gauss_jacobi_rule(sd - d, 0, degree) - points[d - 1, :] = 0.5 * (x + 1) - weights[d - 1, :] = w - scale = 0.5 - for d in range(1, sd + 1): - weights[sd - d, :] *= scale - scale *= 0.5 +class AbstractQuadratureRule(with_metaclass(ABCMeta)): + """Abstract class representing a quadrature rule as point set and a + corresponding set of weights.""" - super(StroudQuadrature, self).__init__( - cell, - StroudPointSet(map(PointSet, points)), - weights) + @abstractproperty + def point_set(self): + """Point set object representing the quadrature points.""" + @abstractproperty + def weight_expression(self): + """GEM expression describing the weights, with the same free indices + as the point set.""" -class GaussLobattoQuadrature(QuadratureRule): - def __init__(self, cell, points): - """Gauss-Lobatto-Legendre quadrature on hypercubes. - :param cell: The reference cell on which to define the quadrature. - :param points: The number of points, or a tuple giving the number of - points in each dimension. - """ - def expand_quad(cell, points): - d = cell.get_spatial_dimension() - if d == 1: - return ((cell, points, - FIAT.quadrature.GaussLobattoQuadratureLineRule(cell, points[0])),) - else: - try: - d_a = cell.A.get_spatial_dimension() - return expand_quad(cell.A, points[:d_a])\ - + expand_quad(cell.B, points[d_a:]) - except AttributeError(): - raise ValueError("Unable to create Gauss-Lobatto quadrature on ", - + str(cell)) - try: - points = tuple(points) - except TypeError: - points = (points,) - - if len(points) == 1: - points *= cell.get_spatial_dimension() - - cpq = expand_quad(cell, points) - - # uniquify q. - lookup = {(c, p): (GaussLobattoPointSet(q.get_points()), - PointSet(q.get_weights())) for c, p, q in cpq} - pointset = tuple(lookup[c, p][0] for c, p, _ in cpq) - weightset = tuple(lookup[c, p][1] for c, p, _ in cpq) - - if len(cpq) == 1: - super(GaussLobattoQuadrature, self).__init__( - cell, pointset[0], weightset[0]) - else: - super(GaussLobattoQuadrature, self).__init__( - cell, - TensorPointSet(pointset), - weightset) +class QuadratureRule(AbstractQuadratureRule): + """Generic quadrature rule with no internal structure.""" + + def __init__(self, points, weights): + weights = numpy.asarray(weights) + assert len(points) == len(weights) + + self._points = numpy.asarray(points) + self.weights = numpy.asarray(weights) + + @cached_property + def point_set(self): + return PointSet(self._points) + + @cached_property + def weight_expression(self): + return gem.Indexed(gem.Literal(self.weights), self.point_set.indices) + + +class TensorProductQuadratureRule(AbstractQuadratureRule): + """Quadrature rule which is a tensor product of other rules.""" + + def __init__(self, factors): + self.factors = tuple(factors) + + @cached_property + def point_set(self): + return TensorPointSet(q.point_set for q in self.factors) + + @cached_property + def weight_expression(self): + return reduce(gem.Product, (q.weight_expression for q in self.factors)) + + # def refactor(self, dims): + # """Refactor this quadrature rule into a tuple of quadrature rules with + # the dimensions specified.""" + + # qs = [] + # i = 0 + # next_i = 0 + # for dim in dims: + # i = next_i + # next_dim = 0 + # while next_dim < dim: + # next_dim += self.factors[next_i].spatial_dimension + # if next_dim > dim: + # raise ValueError("Element and quadrature incompatible") + # next_i += 1 + # if next_i - i > 1: + # qs.append(TensorProductQuadratureRule(*self.factors[i: next_i])) + # else: + # qs.append(self.factors[i]) + # return tuple(qs) + + +# class StroudQuadrature(QuadratureRule): +# def __init__(self, cell, degree): +# """Stroud quadrature rule on simplices.""" + +# sd = cell.get_spatial_dimension() + +# if sd + 1 != len(cell.vertices): +# raise ValueError("cell must be a simplex") + +# points = numpy.zeros((sd, degree)) +# weights = numpy.zeros((sd, degree)) + +# for d in range(1, sd + 1): +# [x, w] = gauss_jacobi_rule(sd - d, 0, degree) +# points[d - 1, :] = 0.5 * (x + 1) +# weights[d - 1, :] = w + +# scale = 0.5 +# for d in range(1, sd + 1): +# weights[sd - d, :] *= scale +# scale *= 0.5 + +# super(StroudQuadrature, self).__init__( +# cell, +# StroudPointSet(map(PointSet, points)), +# weights) + + +# class GaussLobattoQuadrature(QuadratureRule): +# def __init__(self, cell, points): +# """Gauss-Lobatto-Legendre quadrature on hypercubes. +# :param cell: The reference cell on which to define the quadrature. +# :param points: The number of points, or a tuple giving the number of +# points in each dimension. +# """ + +# def expand_quad(cell, points): +# d = cell.get_spatial_dimension() +# if d == 1: +# return ((cell, points, +# FIAT.quadrature.GaussLobattoQuadratureLineRule(cell, points[0])),) +# else: +# try: +# # Note this requires generalisation for n-way products. +# d_a = cell.A.get_spatial_dimension() +# return expand_quad(cell.A, points[:d_a])\ +# + expand_quad(cell.B, points[d_a:]) +# except AttributeError(): +# raise ValueError("Unable to create Gauss-Lobatto quadrature on ", +# + str(cell)) +# try: +# points = tuple(points) +# except TypeError: +# points = (points,) + +# if len(points) == 1: +# points *= cell.get_spatial_dimension() + +# cpq = expand_quad(cell, points) + +# # uniquify q. +# lookup = {(c, p): (GaussLobattoPointSet(q.get_points()), +# PointSet(q.get_weights())) for c, p, q in cpq} +# pointset = tuple(lookup[c, p][0] for c, p, _ in cpq) +# weightset = tuple(lookup[c, p][1] for c, p, _ in cpq) + +# if len(cpq) == 1: +# super(GaussLobattoQuadrature, self).__init__( +# cell, pointset[0], weightset[0]) +# else: +# super(GaussLobattoQuadrature, self).__init__( +# cell, +# TensorPointSet(pointset), +# weightset) From d15b7d4d0d216bd7e26bd6e41c6ebb1dde08fb5d Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 19 Oct 2016 10:19:14 +0100 Subject: [PATCH 272/749] make finite elements accept point sets rather than quadrature rules --- finat/fiat_elements.py | 23 +++++++++++++---------- finat/finiteelementbase.py | 5 ++--- finat/tensorfiniteelement.py | 8 ++++---- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 77710fb56..5e615209b 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -23,25 +23,24 @@ def index_shape(self): def value_shape(self): return self._fiat_element.value_shape() - def basis_evaluation(self, q, entity=None, derivative=0): + def basis_evaluation(self, ps, entity=None, derivative=0): '''Return code for evaluating the element at known points on the reference element. - :param q: the quadrature rule. + :param ps: the point set. :param entity: the cell entity on which to tabulate. :param derivative: the derivative to take of the basis functions. ''' - assert entity is None - dim = self.cell.get_spatial_dimension() i = self.get_indices() vi = self.get_value_indices() - qi = q.get_indices() + pi = ps.indices di = tuple(gem.Index(extent=dim) for i in range(derivative)) + q_shape = tuple(pd.extent for pd in pi) - fiat_tab = self._fiat_element.tabulate(derivative, q.points) + fiat_tab = self._fiat_element.tabulate(derivative, ps.points, entity) # Work out the correct transposition between FIAT storage and ours. tr = (2, 0, 1) if self.value_shape else (1, 0) @@ -54,15 +53,19 @@ def basis_evaluation(self, q, entity=None, derivative=0): it = np.nditer(tensor, flags=['multi_index', 'refs_ok'], op_flags=["writeonly"]) while not it.finished: derivative_multi_index = tuple(e[it.multi_index, :].sum(0)) - it[0] = gem.Literal(fiat_tab[derivative_multi_index].transpose(tr)) + tab = fiat_tab[derivative_multi_index].transpose(tr) + tab = tab.reshape(q_shape + tab.shape[1:]) + it[0] = gem.Literal(tab) it.iternext() tensor = gem.ListTensor(tensor) else: - tensor = gem.Literal(fiat_tab[(0,) * dim].transpose(tr)) + tab = fiat_tab[(0,) * dim].transpose(tr) + tab = tab.reshape(q_shape + tab.shape[1:]) + tensor = gem.Literal(tab) return gem.ComponentTensor(gem.Indexed(tensor, - di + qi + i + vi), - qi + i + vi + di) + di + pi + i + vi), + pi + i + vi + di) @property def entity_dofs(self): diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 0e3018f5c..469f0716b 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -73,13 +73,12 @@ def get_value_indices(self): return tuple(gem.Index(extent=d) for d in self.value_shape) - def basis_evaluation(self, q, entity=None, derivative=None): + def basis_evaluation(self, ps, entity=None, derivative=None): '''Return code for evaluating the element at known points on the reference element. :param index: the basis function index. - :param q: the quadrature rule. - :param q_index: the quadrature index. + :param ps: the point set object. :param entity: the cell entity on which to tabulate. :param derivative: the derivative to take of the basis functions. ''' diff --git a/finat/tensorfiniteelement.py b/finat/tensorfiniteelement.py index 1625a30ff..cf9f6ab8d 100644 --- a/finat/tensorfiniteelement.py +++ b/finat/tensorfiniteelement.py @@ -55,7 +55,7 @@ def index_shape(self): def value_shape(self): return self._base_element.value_shape + self._shape - def basis_evaluation(self, q, entity=None, derivative=0): + def basis_evaluation(self, ps, entity=None, derivative=0): r"""Produce the recipe for basis function evaluation at a set of points :math:`q`: .. math:: @@ -64,19 +64,19 @@ def basis_evaluation(self, q, entity=None, derivative=0): \nabla\boldsymbol\phi_{(\epsilon \gamma \zeta) (i \alpha \beta) q} = \delta_{\alpha \epsilon} \deta{\beta \gamma}\nabla\phi_{\zeta i q} """ - scalarbasis = self._base_element.basis_evaluation(q, entity, derivative) + scalarbasis = self._base_element.basis_evaluation(ps, entity, derivative) indices = tuple(gem.Index() for i in scalarbasis.shape) # Work out which of the indices are for what. - qi = len(q.index_shape) + len(self._base_element.index_shape) + pi = len(ps.indices) + len(self._base_element.index_shape) d = derivative # New basis function and value indices. i = tuple(gem.Index(extent=d) for d in self._shape) vi = tuple(gem.Index(extent=d) for d in self._shape) - new_indices = indices[:qi] + i + indices[qi: len(indices) - d] + vi + indices[len(indices) - d:] + new_indices = indices[:pi] + i + indices[pi: len(indices) - d] + vi + indices[len(indices) - d:] return gem.ComponentTensor(gem.Product(reduce(gem.Product, (gem.Delta(j, k) From 662d982e51e7f9fe83073a59a6897294ec342e45 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 19 Oct 2016 11:31:34 +0100 Subject: [PATCH 273/749] add gem.index_sum --- gem/gem.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index aa43966cf..bad749943 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -33,8 +33,8 @@ 'LogicalOr', 'Conditional', 'Index', 'AffineIndex', 'VariableIndex', 'Indexed', 'FlexiblyIndexed', 'ComponentTensor', 'IndexSum', 'ListTensor', 'Delta', - 'IndexIterator', 'affine_index_group', 'partial_indexed', - 'reshape'] + 'IndexIterator', 'affine_index_group', 'index_sum', + 'partial_indexed', 'reshape'] class NodeMeta(type): @@ -702,6 +702,16 @@ def unique(indices): return tuple(sorted(set(indices), key=id)) +def index_sum(expression, index): + """Eliminates an index from the free indices of an expression by + summing over it. Returns the expression unchanged if the index is + not a free index of the expression.""" + if index in expression.free_indices: + return IndexSum(expression, index) + else: + return expression + + def partial_indexed(tensor, indices): """Generalised indexing into a tensor. The number of indices may be less than or equal to the rank of the tensor, so the result may From 372b23cdff8e2802bfe8e3316af266e9f693d8a5 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 19 Oct 2016 11:31:50 +0100 Subject: [PATCH 274/749] add and use restore_shape --- finat/fiat_elements.py | 10 +++------- finat/point_set.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 5e615209b..2faf009d5 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -1,5 +1,6 @@ from __future__ import absolute_import, print_function, division +from .point_set import restore_shape from .finiteelementbase import FiniteElementBase import FIAT import gem @@ -38,7 +39,6 @@ def basis_evaluation(self, ps, entity=None, derivative=0): vi = self.get_value_indices() pi = ps.indices di = tuple(gem.Index(extent=dim) for i in range(derivative)) - q_shape = tuple(pd.extent for pd in pi) fiat_tab = self._fiat_element.tabulate(derivative, ps.points, entity) @@ -53,15 +53,11 @@ def basis_evaluation(self, ps, entity=None, derivative=0): it = np.nditer(tensor, flags=['multi_index', 'refs_ok'], op_flags=["writeonly"]) while not it.finished: derivative_multi_index = tuple(e[it.multi_index, :].sum(0)) - tab = fiat_tab[derivative_multi_index].transpose(tr) - tab = tab.reshape(q_shape + tab.shape[1:]) - it[0] = gem.Literal(tab) + it[0] = gem.Literal(restore_shape(fiat_tab[derivative_multi_index].transpose(tr), ps)) it.iternext() tensor = gem.ListTensor(tensor) else: - tab = fiat_tab[(0,) * dim].transpose(tr) - tab = tab.reshape(q_shape + tab.shape[1:]) - tensor = gem.Literal(tab) + tensor = gem.Literal(restore_shape(fiat_tab[(0,) * dim].transpose(tr), ps)) return gem.ComponentTensor(gem.Indexed(tensor, di + pi + i + vi), diff --git a/finat/point_set.py b/finat/point_set.py index 4d4faec42..3808941bc 100644 --- a/finat/point_set.py +++ b/finat/point_set.py @@ -108,3 +108,16 @@ def expression(self): # class GaussLobattoPointSet(PointSet): # """A set of 1D Gauss Lobatto points. This is a separate class in order # to allow elements to apply spectral element tricks.""" + + +def restore_shape(array, ps): + """Restores the shape of a point set for a numpy array. + + :arg array: numpy array of arbitrary rank with the first dimension + representing the points. + :arg ps: point set object + :returns: reshaped numpy array with the first dimension replaced + with the shape of the point set. + """ + shape = tuple(index.extent for index in ps.indices) + return array.reshape(shape + array.shape[1:]) From f9c8df01b5e26e337b3642871c754dc3ac7eb728 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 20 Oct 2016 10:04:39 +0100 Subject: [PATCH 275/749] new return value convention --- finat/fiat_elements.py | 2 +- finat/tensorfiniteelement.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 2faf009d5..a94973ae5 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -61,7 +61,7 @@ def basis_evaluation(self, ps, entity=None, derivative=0): return gem.ComponentTensor(gem.Indexed(tensor, di + pi + i + vi), - pi + i + vi + di) + i + vi + di) @property def entity_dofs(self): diff --git a/finat/tensorfiniteelement.py b/finat/tensorfiniteelement.py index cf9f6ab8d..703d2d0af 100644 --- a/finat/tensorfiniteelement.py +++ b/finat/tensorfiniteelement.py @@ -69,7 +69,7 @@ def basis_evaluation(self, ps, entity=None, derivative=0): indices = tuple(gem.Index() for i in scalarbasis.shape) # Work out which of the indices are for what. - pi = len(ps.indices) + len(self._base_element.index_shape) + pi = len(self._base_element.index_shape) d = derivative # New basis function and value indices. From d3bead8b2917b2c03d6bdc4e422a94740ccd58da Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 20 Oct 2016 10:45:27 +0100 Subject: [PATCH 276/749] refactor derivative presentation --- finat/fiat_elements.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index a94973ae5..28c84824f 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -45,23 +45,20 @@ def basis_evaluation(self, ps, entity=None, derivative=0): # Work out the correct transposition between FIAT storage and ours. tr = (2, 0, 1) if self.value_shape else (1, 0) - # Convert the FIAT tabulation into a gem tensor. Note that - # this does not exploit the symmetry of the derivative tensor. if derivative: e = np.eye(dim, dtype=np.int) tensor = np.empty((dim,) * derivative, dtype=np.object) it = np.nditer(tensor, flags=['multi_index', 'refs_ok'], op_flags=["writeonly"]) while not it.finished: derivative_multi_index = tuple(e[it.multi_index, :].sum(0)) - it[0] = gem.Literal(restore_shape(fiat_tab[derivative_multi_index].transpose(tr), ps)) + it[0] = gem.Indexed(gem.Literal(restore_shape(fiat_tab[derivative_multi_index].transpose(tr), ps)), + pi + i + vi) it.iternext() - tensor = gem.ListTensor(tensor) + tensor = gem.Indexed(gem.ListTensor(tensor), di) else: - tensor = gem.Literal(restore_shape(fiat_tab[(0,) * dim].transpose(tr), ps)) + tensor = gem.Indexed(gem.Literal(restore_shape(fiat_tab[(0,) * dim].transpose(tr), ps)), pi + i + vi) - return gem.ComponentTensor(gem.Indexed(tensor, - di + pi + i + vi), - i + vi + di) + return gem.ComponentTensor(tensor, i + vi + di) @property def entity_dofs(self): From ef823ff009105fb6327151f4d7a65eb440945684 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 20 Oct 2016 10:58:33 +0100 Subject: [PATCH 277/749] deduplicate derivative / non-derivative cases --- finat/fiat_elements.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 28c84824f..a29916204 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -45,18 +45,19 @@ def basis_evaluation(self, ps, entity=None, derivative=0): # Work out the correct transposition between FIAT storage and ours. tr = (2, 0, 1) if self.value_shape else (1, 0) + e = np.eye(dim, dtype=np.int) + tensor = np.empty((dim,) * derivative, dtype=np.object) + it = np.nditer(tensor, flags=['multi_index', 'refs_ok'], op_flags=["writeonly"]) + while not it.finished: + derivative_multi_index = tuple(e[it.multi_index, :].sum(0)) + it[0] = gem.Indexed(gem.Literal(restore_shape(fiat_tab[derivative_multi_index].transpose(tr), ps)), + pi + i + vi) + it.iternext() + if derivative: - e = np.eye(dim, dtype=np.int) - tensor = np.empty((dim,) * derivative, dtype=np.object) - it = np.nditer(tensor, flags=['multi_index', 'refs_ok'], op_flags=["writeonly"]) - while not it.finished: - derivative_multi_index = tuple(e[it.multi_index, :].sum(0)) - it[0] = gem.Indexed(gem.Literal(restore_shape(fiat_tab[derivative_multi_index].transpose(tr), ps)), - pi + i + vi) - it.iternext() tensor = gem.Indexed(gem.ListTensor(tensor), di) else: - tensor = gem.Indexed(gem.Literal(restore_shape(fiat_tab[(0,) * dim].transpose(tr), ps)), pi + i + vi) + tensor = tensor[()] return gem.ComponentTensor(tensor, i + vi + di) From fb3a4785a931225048728e6ad2e459d1bbf61c3e Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 20 Oct 2016 11:44:51 +0100 Subject: [PATCH 278/749] report cellwise constantness --- finat/fiat_elements.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index a29916204..8e3b3d1fc 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -1,6 +1,5 @@ from __future__ import absolute_import, print_function, division -from .point_set import restore_shape from .finiteelementbase import FiniteElementBase import FIAT import gem @@ -32,15 +31,24 @@ def basis_evaluation(self, ps, entity=None, derivative=0): :param entity: the cell entity on which to tabulate. :param derivative: the derivative to take of the basis functions. ''' - dim = self.cell.get_spatial_dimension() i = self.get_indices() vi = self.get_value_indices() - pi = ps.indices di = tuple(gem.Index(extent=dim) for i in range(derivative)) - fiat_tab = self._fiat_element.tabulate(derivative, ps.points, entity) + if derivative < self._degree: + points = ps.points + pi = ps.indices + elif derivative == self._degree: + # Tabulate on cell centre + points = np.mean(self._cell.get_vertices(), axis=0, keepdims=True) + entity = (self._cell.get_dimension(), 0) + pi = () # no point indices used + else: + return gem.Zero(tuple(index.extent for index in i + vi + di)) + + fiat_tab = self._fiat_element.tabulate(derivative, points, entity) # Work out the correct transposition between FIAT storage and ours. tr = (2, 0, 1) if self.value_shape else (1, 0) @@ -49,8 +57,12 @@ def basis_evaluation(self, ps, entity=None, derivative=0): tensor = np.empty((dim,) * derivative, dtype=np.object) it = np.nditer(tensor, flags=['multi_index', 'refs_ok'], op_flags=["writeonly"]) while not it.finished: + def restore_shape(array, indices): + shape = tuple(index.extent for index in indices) + return array.reshape(shape + array.shape[1:]) + derivative_multi_index = tuple(e[it.multi_index, :].sum(0)) - it[0] = gem.Indexed(gem.Literal(restore_shape(fiat_tab[derivative_multi_index].transpose(tr), ps)), + it[0] = gem.Indexed(gem.Literal(restore_shape(fiat_tab[derivative_multi_index].transpose(tr), pi)), pi + i + vi) it.iternext() From cd7f6e2e276c916f9d3e41cd5db39ec94e8cd241 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 20 Oct 2016 11:54:22 +0100 Subject: [PATCH 279/749] remove restore_shape --- finat/point_set.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/finat/point_set.py b/finat/point_set.py index 3808941bc..4d4faec42 100644 --- a/finat/point_set.py +++ b/finat/point_set.py @@ -108,16 +108,3 @@ def expression(self): # class GaussLobattoPointSet(PointSet): # """A set of 1D Gauss Lobatto points. This is a separate class in order # to allow elements to apply spectral element tricks.""" - - -def restore_shape(array, ps): - """Restores the shape of a point set for a numpy array. - - :arg array: numpy array of arbitrary rank with the first dimension - representing the points. - :arg ps: point set object - :returns: reshaped numpy array with the first dimension replaced - with the shape of the point set. - """ - shape = tuple(index.extent for index in ps.indices) - return array.reshape(shape + array.shape[1:]) From 630d0a5848a01076ce891808df79dc934de927e1 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 20 Oct 2016 12:13:07 +0100 Subject: [PATCH 280/749] clean up a bit --- finat/fiat_elements.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 8e3b3d1fc..b69c075fa 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -39,12 +39,12 @@ def basis_evaluation(self, ps, entity=None, derivative=0): if derivative < self._degree: points = ps.points - pi = ps.indices + point_indices = ps.indices elif derivative == self._degree: # Tabulate on cell centre points = np.mean(self._cell.get_vertices(), axis=0, keepdims=True) entity = (self._cell.get_dimension(), 0) - pi = () # no point indices used + point_indices = () # no point indices used else: return gem.Zero(tuple(index.extent for index in i + vi + di)) @@ -53,18 +53,17 @@ def basis_evaluation(self, ps, entity=None, derivative=0): # Work out the correct transposition between FIAT storage and ours. tr = (2, 0, 1) if self.value_shape else (1, 0) + def restore_point_shape(array): + shape = tuple(index.extent for index in point_indices) + return array.reshape(shape + array.shape[1:]) + e = np.eye(dim, dtype=np.int) tensor = np.empty((dim,) * derivative, dtype=np.object) - it = np.nditer(tensor, flags=['multi_index', 'refs_ok'], op_flags=["writeonly"]) - while not it.finished: - def restore_shape(array, indices): - shape = tuple(index.extent for index in indices) - return array.reshape(shape + array.shape[1:]) - - derivative_multi_index = tuple(e[it.multi_index, :].sum(0)) - it[0] = gem.Indexed(gem.Literal(restore_shape(fiat_tab[derivative_multi_index].transpose(tr), pi)), - pi + i + vi) - it.iternext() + for multi_index in np.ndindex(tensor.shape): + derivative_multi_index = tuple(e[multi_index, :].sum(axis=0)) + transposed_table = fiat_tab[derivative_multi_index].transpose(tr) + tensor[multi_index] = gem.Indexed(gem.Literal(restore_point_shape(transposed_table)), + point_indices + i + vi) if derivative: tensor = gem.Indexed(gem.ListTensor(tensor), di) From 7570b1c401f8f3da4a6c84fc16cb5d3bf5e91142 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 20 Oct 2016 10:38:50 +0100 Subject: [PATCH 281/749] use reconstruct because of ListTensor --- gem/optimise.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index b8b737a11..1466e1f43 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -110,9 +110,9 @@ def _select_expression(expressions, index): assert all(len(e.children) == len(expr.children) for e in expressions) assert len(expr.children) > 0 - return cls(*[_select_expression(nth_children, index) - for nth_children in zip(*[e.children - for e in expressions])]) + return expr.reconstruct(*[_select_expression(nth_children, index) + for nth_children in zip(*[e.children + for e in expressions])]) elif issubclass(cls, Indexed): assert all(e.multiindex == expr.multiindex for e in expressions) return Indexed(_select_expression([e.children[0] From e16792bf307fac33c51109a68317023c5e4bd5e6 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 20 Oct 2016 16:14:49 +0100 Subject: [PATCH 282/749] something that supposedly might work --- finat/__init__.py | 2 +- finat/product_elements.py | 38 --------------------- finat/tensor_product.py | 70 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 39 deletions(-) delete mode 100644 finat/product_elements.py create mode 100644 finat/tensor_product.py diff --git a/finat/__init__.py b/finat/__init__.py index f71288416..9773b71d1 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -6,7 +6,7 @@ from .fiat_elements import Nedelec, NedelecSecondKind, Regge # noqa: F401 from .bernstein import Bernstein # noqa: F401 from .tensorfiniteelement import TensorFiniteElement # noqa: F401 -from .product_elements import ScalarProductElement # noqa: F401 +from .tensor_product import TensorProductElement # noqa: F401 from .derivatives import div, grad, curl # noqa: F401 from . import quadrature # noqa: F401 from . import ufl_interface # noqa: F401 diff --git a/finat/product_elements.py b/finat/product_elements.py deleted file mode 100644 index 6192f9f7a..000000000 --- a/finat/product_elements.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Preliminary support for tensor product elements.""" - -from __future__ import absolute_import, print_function, division - -from .finiteelementbase import FiniteElementBase -from FIAT.reference_element import TensorProductCell - - -class ProductElement(object): - """Mixin class describing product elements.""" - - -class ScalarProductElement(ProductElement, FiniteElementBase): - """A scalar-valued tensor product element.""" - def __init__(self, *args): - super(ScalarProductElement, self).__init__() - - assert all([isinstance(e, FiniteElementBase) for e in args]) - - self.factors = tuple(args) - - self._degree = max([a._degree for a in args]) - - cellprod = lambda cells: TensorProductCell(cells[0], cells[1] if len(cells) < 3 - else cellprod(cells[1:])) - - self._cell = cellprod([a.cell for a in args]) - - def __hash__(self): - """ScalarProductElements are equal if their factors are equal""" - - return hash(self.factors) - - def __eq__(self, other): - """VectorFiniteElements are equal if they have the same base element - and dimension.""" - - return self.factors == other.factors diff --git a/finat/tensor_product.py b/finat/tensor_product.py new file mode 100644 index 000000000..88cb1019a --- /dev/null +++ b/finat/tensor_product.py @@ -0,0 +1,70 @@ +from __future__ import absolute_import, print_function, division +from six.moves import range, zip + +from functools import reduce +from itertools import chain + +import numpy + +from FIAT.reference_element import TensorProductCell + +import gem + +from finat.finiteelementbase import FiniteElementBase +from finat.point_set import TensorPointSet + + +class TensorProductElement(FiniteElementBase): + + def __init__(self, factors): + self.factors = tuple(factors) + assert all(fe.value_shape == () for fe in self.factors) + + self._cell = TensorProductCell(*[fe.cell for fe in self.factors]) + # self._degree = sum(fe.degree for fe in factors) # correct? + # self._degree = max(fe.degree for fe in factors) # FIAT + self._degree = None # not used? + + @property + def index_shape(self): + return tuple(chain(*[fe.index_shape for fe in self.factors])) + + def basis_evaluation(self, ps, entity=None, derivative=0): + if not isinstance(ps, TensorPointSet): + raise NotImplementedError("How to tabulate TensorProductElement on non-TensorPointSet?") + assert len(ps.factors) == len(self.factors) + + if entity is None: + entity = (self.cell.get_dimension(), 0) + entity_dim, entity_id = entity + + assert isinstance(entity_dim, tuple) + assert len(entity_dim) == len(self.factors) + + shape = tuple(len(c.get_topology()[d]) + for c, d in zip(self.cell.cells, entity_dim)) + entities = list(zip(entity_dim, numpy.unravel_index(entity_id, shape))) + + dimension = self.cell.get_spatial_dimension() + tensor = numpy.empty((dimension,) * derivative, dtype=object) + eye = numpy.eye(dimension, dtype=int) + alphas = [fe.get_indices() for fe in self.factors] + dim_slices = TensorProductCell._split_slices([c.get_spatial_dimension() + for c in self.cell.cells]) + for delta in numpy.ndindex(tensor.shape): + D_ = tuple(eye[delta, :].sum(axis=0)) + Ds = [D_[s] for s in dim_slices] + scalars = [] + for fe, ps_, e, D, alpha in zip(self.factors, ps.factors, entities, Ds, alphas): + value = fe.basis_evaluation(ps_, entity=e, derivative=sum(D)) + d = tuple(chain(*[(dim,) * count for dim, count in enumerate(D)])) + scalars.append(gem.Indexed(value, alpha + d)) + tensor[delta] = reduce(gem.Product, scalars) + + delta = tuple(gem.Index(extent=dimension) for i in range(derivative)) + if derivative: + value = gem.Indexed(gem.ListTensor(tensor), delta) + else: + value = tensor[()] + + return gem.ComponentTensor(value, tuple(chain(*alphas)) + delta) From 9414a9e14e209c923e8c27f130328d463b15337d Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 21 Oct 2016 11:07:12 +0100 Subject: [PATCH 283/749] evaluate TensorProductElement on a basic PointSet as well Known current use: UFL interpolation onto extruded meshes --- finat/tensor_product.py | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/finat/tensor_product.py b/finat/tensor_product.py index 88cb1019a..6b156be48 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -11,7 +11,7 @@ import gem from finat.finiteelementbase import FiniteElementBase -from finat.point_set import TensorPointSet +from finat.point_set import PointSet, TensorPointSet class TensorProductElement(FiniteElementBase): @@ -30,10 +30,6 @@ def index_shape(self): return tuple(chain(*[fe.index_shape for fe in self.factors])) def basis_evaluation(self, ps, entity=None, derivative=0): - if not isinstance(ps, TensorPointSet): - raise NotImplementedError("How to tabulate TensorProductElement on non-TensorPointSet?") - assert len(ps.factors) == len(self.factors) - if entity is None: entity = (self.cell.get_dimension(), 0) entity_dim, entity_id = entity @@ -45,6 +41,8 @@ def basis_evaluation(self, ps, entity=None, derivative=0): for c, d in zip(self.cell.cells, entity_dim)) entities = list(zip(entity_dim, numpy.unravel_index(entity_id, shape))) + ps_factors = factor_point_set(self.cell, entity_dim, ps) + dimension = self.cell.get_spatial_dimension() tensor = numpy.empty((dimension,) * derivative, dtype=object) eye = numpy.eye(dimension, dtype=int) @@ -55,7 +53,7 @@ def basis_evaluation(self, ps, entity=None, derivative=0): D_ = tuple(eye[delta, :].sum(axis=0)) Ds = [D_[s] for s in dim_slices] scalars = [] - for fe, ps_, e, D, alpha in zip(self.factors, ps.factors, entities, Ds, alphas): + for fe, ps_, e, D, alpha in zip(self.factors, ps_factors, entities, Ds, alphas): value = fe.basis_evaluation(ps_, entity=e, derivative=sum(D)) d = tuple(chain(*[(dim,) * count for dim, count in enumerate(D)])) scalars.append(gem.Indexed(value, alpha + d)) @@ -68,3 +66,26 @@ def basis_evaluation(self, ps, entity=None, derivative=0): value = tensor[()] return gem.ComponentTensor(value, tuple(chain(*alphas)) + delta) + + +def factor_point_set(product_cell, product_dim, point_set): + assert len(product_cell.cells) == len(product_dim) + point_dims = [cell.construct_subelement(dim).get_spatial_dimension() + for cell, dim in zip(product_cell.cells, product_dim)] + + if isinstance(point_set, TensorPointSet): + assert len(point_set.factors) == len(point_dims) + assert all(ps.dimension == dim + for ps, dim in zip(point_set.factors, point_dims)) + return point_set.factors + elif isinstance(point_set, PointSet): + assert point_set.dimension == sum(point_dims) + slices = TensorProductCell._split_slices(point_dims) + result = [] + for s in slices: + ps = PointSet(point_set.points[:, s]) + ps.indices = point_set.indices + result.append(ps) + return result + else: + raise NotImplementedError("How to tabulate TensorProductElement on %s?" % (type(point_set).__name__,)) From 339633174d6814f0fec5134d59cbc9b0023a8089 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 21 Oct 2016 11:42:36 +0100 Subject: [PATCH 284/749] add missing value_shape --- finat/tensor_product.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/finat/tensor_product.py b/finat/tensor_product.py index 6b156be48..2da6b5410 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -29,6 +29,10 @@ def __init__(self, factors): def index_shape(self): return tuple(chain(*[fe.index_shape for fe in self.factors])) + @property + def value_shape(self): + return () # TODO: non-scalar factors not supported yet + def basis_evaluation(self, ps, entity=None, derivative=0): if entity is None: entity = (self.cell.get_dimension(), 0) From d6c0e0bc2d73863f3de2e83069f8def6db418375 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 21 Oct 2016 15:12:49 +0100 Subject: [PATCH 285/749] explain in comments what the code does --- finat/tensor_product.py | 71 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/finat/tensor_product.py b/finat/tensor_product.py index 2da6b5410..a7604be5c 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -47,28 +47,99 @@ def basis_evaluation(self, ps, entity=None, derivative=0): ps_factors = factor_point_set(self.cell, entity_dim, ps) + # Okay, so we need to introduce some terminology before we are + # able to explain what is going on below. A key difficulty is + # the necessity to use two different derivative multiindex + # types, and convert between them back and forth. + # + # First, let us consider a scalar-valued function `u` in a + # `d`-dimensional space: + # + # u : R^d -> R + # + # Let a = (a_1, a_2, ..., a_d) be a multiindex. You might + # recognise this notation: + # + # D^a u + # + # For example, a = (1, 2) means a third derivative: first + # derivative in the x-direction, and second derivative in the + # y-direction. We call `a` "canonical derivative multiindex", + # or canonical multiindex for short. + # + # Now we will have populate what we call a derivative tensor. + # For example, the third derivative of `u` is a d x d x d + # tensor: + # + # \grad \grad \grad u + # + # A multiindex can denote an entry of this derivative tensor. + # For example, i = (1, 0, 1) refers deriving `u` first in the + # y-direction (second direction, indexing starts at zero), + # then in the x-direction, and finally in the second direction + # again. Usually the order of these derivatives does not + # matter, so the derivative tensor is symmetric. For specific + # conditions about when this actually holds, please refer to + # your favourite calculus textbook. We call `i` here a + # "derivative tensor multiindex", or tensor multiindex for + # short. For example, the following tensor multiindices + # correspond to the (1, 2) canonical multiindex: + # + # (0, 1, 1) + # (1, 0, 1) + # (1, 1, 0) + # + # Now you should be ready to get go. + + # Spatial dimension dimension = self.cell.get_spatial_dimension() + # The derivative tensor tensor = numpy.empty((dimension,) * derivative, dtype=object) + # The identity matrix, used to facilitate conversion from + # tensor multiindex to canonical multiindex form. eye = numpy.eye(dimension, dtype=int) + # A list of multiindices, one multiindex per subelement, each + # multiindex describing the shape of basis functions of the + # subelement. alphas = [fe.get_indices() for fe in self.factors] + # A list of slices that are used to select dimensions + # corresponding to each subelement. dim_slices = TensorProductCell._split_slices([c.get_spatial_dimension() for c in self.cell.cells]) + # 'delta' is a tensor multiindex consisting of only fixed + # indices for populating the entries of the derivative tensor. for delta in numpy.ndindex(tensor.shape): + # Get the canonical multiindex corresponding to 'delta'. D_ = tuple(eye[delta, :].sum(axis=0)) + # Split this canonical multiindex for the subelements. Ds = [D_[s] for s in dim_slices] + # GEM scalars (can have free indices) for collecting the + # contributions from the subelements. scalars = [] for fe, ps_, e, D, alpha in zip(self.factors, ps_factors, entities, Ds, alphas): + # Ask the subelement to tabulate at the required derivative order. value = fe.basis_evaluation(ps_, entity=e, derivative=sum(D)) + # Nice, but now we have got a subelement derivative + # tensor of the given order, while we only need a + # specific derivative. So we convert the subelement + # canonical multiindex to tensor multiindex. d = tuple(chain(*[(dim,) * count for dim, count in enumerate(D)])) + # Turn basis shape to free indices, select the right + # derivative entry, and collect the result. scalars.append(gem.Indexed(value, alpha + d)) + # Multiply the values from the subelements and insert the + # result into the derivative tensor. tensor[delta] = reduce(gem.Product, scalars) + # Convert the derivative tensor from a numpy object array to a + # GEM expression with only free indices (and scalar shape). delta = tuple(gem.Index(extent=dimension) for i in range(derivative)) if derivative: value = gem.Indexed(gem.ListTensor(tensor), delta) else: value = tensor[()] + # Wrap up non-point indices into shape return gem.ComponentTensor(value, tuple(chain(*alphas)) + delta) From ef10c3b0639085b4bd880747831669b102875eb8 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 21 Oct 2016 15:27:08 +0100 Subject: [PATCH 286/749] doc point set splitting --- finat/tensor_product.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/finat/tensor_product.py b/finat/tensor_product.py index a7604be5c..3ff76b5ed 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -144,16 +144,27 @@ def basis_evaluation(self, ps, entity=None, derivative=0): def factor_point_set(product_cell, product_dim, point_set): + """Factors a point set for the product element into a point sets for + each subelement. + + :arg product_cell: a TensorProductCell + :arg product_dim: entity dimension for the product cell + :arg point_set: point set for the product element + """ assert len(product_cell.cells) == len(product_dim) point_dims = [cell.construct_subelement(dim).get_spatial_dimension() for cell, dim in zip(product_cell.cells, product_dim)] if isinstance(point_set, TensorPointSet): + # Just give the factors asserting matching dimensions. assert len(point_set.factors) == len(point_dims) assert all(ps.dimension == dim for ps, dim in zip(point_set.factors, point_dims)) return point_set.factors elif isinstance(point_set, PointSet): + # Split the point coordinates along the point dimensions + # required by the subelements, but use the same point index + # for the new point sets. assert point_set.dimension == sum(point_dims) slices = TensorProductCell._split_slices(point_dims) result = [] From a20592367bcd029b622e0ff63070335ba545cd79 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 21 Oct 2016 15:40:57 +0100 Subject: [PATCH 287/749] pull quadrilateral flattenizer here --- finat/__init__.py | 1 + finat/quadrilateral.py | 57 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 finat/quadrilateral.py diff --git a/finat/__init__.py b/finat/__init__.py index 9773b71d1..a9e3cf02f 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -7,6 +7,7 @@ from .bernstein import Bernstein # noqa: F401 from .tensorfiniteelement import TensorFiniteElement # noqa: F401 from .tensor_product import TensorProductElement # noqa: F401 +from .quadrilateral import QuadrilateralElement # noqa: F401 from .derivatives import div, grad, curl # noqa: F401 from . import quadrature # noqa: F401 from . import ufl_interface # noqa: F401 diff --git a/finat/quadrilateral.py b/finat/quadrilateral.py new file mode 100644 index 000000000..0c26a5ca8 --- /dev/null +++ b/finat/quadrilateral.py @@ -0,0 +1,57 @@ +from __future__ import absolute_import, print_function, division + +from FIAT.reference_element import FiredrakeQuadrilateral + +from finat.finiteelementbase import FiniteElementBase + + +class QuadrilateralElement(FiniteElementBase): + """Class for elements on quadrilaterals. Wraps a tensor product + element on an interval x interval cell, but appears on a + quadrilateral cell to the outside world.""" + + def __init__(self, element): + super(QuadrilateralElement, self).__init__() + self._cell = FiredrakeQuadrilateral() + self._degree = None # Who cares? Not used. + + self.product = element + + def basis_evaluation(self, ps, entity=None, derivative=0): + """Return code for evaluating the element at known points on the + reference element. + + :param ps: the point set object. + :param entity: the cell entity on which to tabulate. + :param derivative: the derivative to take of the basis functions. + """ + if entity is None: + entity = (2, 0) + + # Entity is provided in flattened form (d, i) + # We factor the entity and construct an appropriate + # entity id for a TensorProductCell: ((d1, d2), i) + entity_dim, entity_id = entity + if entity_dim == 2: + assert entity_id == 0 + product_entity = ((1, 1), 0) + elif entity_dim == 1: + facets = [((0, 1), 0), + ((0, 1), 1), + ((1, 0), 0), + ((1, 0), 1)] + product_entity = facets[entity_id] + elif entity_dim == 0: + raise NotImplementedError("Not implemented for 0 dimension entities") + else: + raise ValueError("Illegal entity dimension %s" % entity_dim) + + return self.product.basis_evaluation(ps, product_entity, derivative) + + @property + def index_shape(self): + return self.product.index_shape + + @property + def value_shape(self): + return self.product.value_shape From a374fb5001cf46e72dd68e449f1a2e18c3a6b869 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 24 Oct 2016 10:54:14 +0100 Subject: [PATCH 288/749] refactor basis_evaluation API to be more like FIAT --- finat/fiat_elements.py | 70 +++++++++----------- finat/finiteelementbase.py | 5 +- finat/quadrilateral.py | 6 +- finat/tensor_product.py | 122 ++++++++++------------------------- finat/tensorfiniteelement.py | 47 +++++++------- 5 files changed, 93 insertions(+), 157 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index b69c075fa..8dd2c5cf0 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -1,4 +1,6 @@ from __future__ import absolute_import, print_function, division +from six import iteritems +from six.moves import range from .finiteelementbase import FiniteElementBase import FIAT @@ -23,7 +25,7 @@ def index_shape(self): def value_shape(self): return self._fiat_element.value_shape() - def basis_evaluation(self, ps, entity=None, derivative=0): + def basis_evaluation(self, order, ps, entity=None): '''Return code for evaluating the element at known points on the reference element. @@ -31,46 +33,32 @@ def basis_evaluation(self, ps, entity=None, derivative=0): :param entity: the cell entity on which to tabulate. :param derivative: the derivative to take of the basis functions. ''' - dim = self.cell.get_spatial_dimension() - - i = self.get_indices() - vi = self.get_value_indices() - di = tuple(gem.Index(extent=dim) for i in range(derivative)) - - if derivative < self._degree: - points = ps.points - point_indices = ps.indices - elif derivative == self._degree: - # Tabulate on cell centre - points = np.mean(self._cell.get_vertices(), axis=0, keepdims=True) - entity = (self._cell.get_dimension(), 0) - point_indices = () # no point indices used - else: - return gem.Zero(tuple(index.extent for index in i + vi + di)) - - fiat_tab = self._fiat_element.tabulate(derivative, points, entity) - - # Work out the correct transposition between FIAT storage and ours. - tr = (2, 0, 1) if self.value_shape else (1, 0) - - def restore_point_shape(array): - shape = tuple(index.extent for index in point_indices) - return array.reshape(shape + array.shape[1:]) - - e = np.eye(dim, dtype=np.int) - tensor = np.empty((dim,) * derivative, dtype=np.object) - for multi_index in np.ndindex(tensor.shape): - derivative_multi_index = tuple(e[multi_index, :].sum(axis=0)) - transposed_table = fiat_tab[derivative_multi_index].transpose(tr) - tensor[multi_index] = gem.Indexed(gem.Literal(restore_point_shape(transposed_table)), - point_indices + i + vi) - - if derivative: - tensor = gem.Indexed(gem.ListTensor(tensor), di) - else: - tensor = tensor[()] - - return gem.ComponentTensor(tensor, i + vi + di) + fiat_result = self._fiat_element.tabulate(order, ps.points, entity) + result = {} + for alpha, table in iteritems(fiat_result): + # Points be the first dimension, not last. + reordering = list(range(len(table.shape))) + reordering = [reordering[-1]] + reordering[:-1] + table = table.transpose(reordering) + + derivative = sum(alpha) + if derivative < self._degree: + point_indices = ps.indices + point_shape = tuple(index.extent for index in point_indices) + shape = point_shape + self.index_shape + self.value_shape + result[alpha] = gem.partial_indexed( + gem.Literal(table.reshape(shape)), + point_indices + ) + elif derivative == self._degree: + # Make sure numerics satisfies theory + assert np.allclose(table, table.mean(axis=0, keepdims=True)) + result[alpha] = gem.Literal(table[0]) + else: + # Make sure numerics satisfies theory + assert np.allclose(table, 0.0) + result[alpha] = gem.Zero(self.index_shape + self.value_shape) + return result @property def entity_dofs(self): diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 469f0716b..c057dbead 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -73,14 +73,13 @@ def get_value_indices(self): return tuple(gem.Index(extent=d) for d in self.value_shape) - def basis_evaluation(self, ps, entity=None, derivative=None): + def basis_evaluation(self, order, ps, entity=None): '''Return code for evaluating the element at known points on the reference element. - :param index: the basis function index. + :param order: return derivatives up to this order. :param ps: the point set object. :param entity: the cell entity on which to tabulate. - :param derivative: the derivative to take of the basis functions. ''' raise NotImplementedError diff --git a/finat/quadrilateral.py b/finat/quadrilateral.py index 0c26a5ca8..252ef0401 100644 --- a/finat/quadrilateral.py +++ b/finat/quadrilateral.py @@ -17,13 +17,13 @@ def __init__(self, element): self.product = element - def basis_evaluation(self, ps, entity=None, derivative=0): + def basis_evaluation(self, order, ps, entity=None): """Return code for evaluating the element at known points on the reference element. + :param order: return derivatives up to this order. :param ps: the point set object. :param entity: the cell entity on which to tabulate. - :param derivative: the derivative to take of the basis functions. """ if entity is None: entity = (2, 0) @@ -46,7 +46,7 @@ def basis_evaluation(self, ps, entity=None, derivative=0): else: raise ValueError("Illegal entity dimension %s" % entity_dim) - return self.product.basis_evaluation(ps, product_entity, derivative) + return self.product.basis_evaluation(order, ps, product_entity) @property def index_shape(self): diff --git a/finat/tensor_product.py b/finat/tensor_product.py index 3ff76b5ed..b42115a40 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -6,6 +6,7 @@ import numpy +from FIAT.polynomial_set import mis from FIAT.reference_element import TensorProductCell import gem @@ -33,11 +34,13 @@ def index_shape(self): def value_shape(self): return () # TODO: non-scalar factors not supported yet - def basis_evaluation(self, ps, entity=None, derivative=0): + def basis_evaluation(self, order, ps, entity=None): + # Default entity if entity is None: entity = (self.cell.get_dimension(), 0) entity_dim, entity_id = entity + # Factor entity assert isinstance(entity_dim, tuple) assert len(entity_dim) == len(self.factors) @@ -45,102 +48,45 @@ def basis_evaluation(self, ps, entity=None, derivative=0): for c, d in zip(self.cell.cells, entity_dim)) entities = list(zip(entity_dim, numpy.unravel_index(entity_id, shape))) + # Factor point set ps_factors = factor_point_set(self.cell, entity_dim, ps) - # Okay, so we need to introduce some terminology before we are - # able to explain what is going on below. A key difficulty is - # the necessity to use two different derivative multiindex - # types, and convert between them back and forth. - # - # First, let us consider a scalar-valued function `u` in a - # `d`-dimensional space: - # - # u : R^d -> R - # - # Let a = (a_1, a_2, ..., a_d) be a multiindex. You might - # recognise this notation: - # - # D^a u - # - # For example, a = (1, 2) means a third derivative: first - # derivative in the x-direction, and second derivative in the - # y-direction. We call `a` "canonical derivative multiindex", - # or canonical multiindex for short. - # - # Now we will have populate what we call a derivative tensor. - # For example, the third derivative of `u` is a d x d x d - # tensor: - # - # \grad \grad \grad u - # - # A multiindex can denote an entry of this derivative tensor. - # For example, i = (1, 0, 1) refers deriving `u` first in the - # y-direction (second direction, indexing starts at zero), - # then in the x-direction, and finally in the second direction - # again. Usually the order of these derivatives does not - # matter, so the derivative tensor is symmetric. For specific - # conditions about when this actually holds, please refer to - # your favourite calculus textbook. We call `i` here a - # "derivative tensor multiindex", or tensor multiindex for - # short. For example, the following tensor multiindices - # correspond to the (1, 2) canonical multiindex: - # - # (0, 1, 1) - # (1, 0, 1) - # (1, 1, 0) - # - # Now you should be ready to get go. + # Subelement results + factor_results = [fe.basis_evaluation(order, ps_, e) + for fe, ps_, e in zip(self.factors, ps_factors, entities)] # Spatial dimension dimension = self.cell.get_spatial_dimension() - # The derivative tensor - tensor = numpy.empty((dimension,) * derivative, dtype=object) - # The identity matrix, used to facilitate conversion from - # tensor multiindex to canonical multiindex form. - eye = numpy.eye(dimension, dtype=int) - # A list of multiindices, one multiindex per subelement, each - # multiindex describing the shape of basis functions of the - # subelement. - alphas = [fe.get_indices() for fe in self.factors] + # A list of slices that are used to select dimensions # corresponding to each subelement. dim_slices = TensorProductCell._split_slices([c.get_spatial_dimension() for c in self.cell.cells]) - # 'delta' is a tensor multiindex consisting of only fixed - # indices for populating the entries of the derivative tensor. - for delta in numpy.ndindex(tensor.shape): - # Get the canonical multiindex corresponding to 'delta'. - D_ = tuple(eye[delta, :].sum(axis=0)) - # Split this canonical multiindex for the subelements. - Ds = [D_[s] for s in dim_slices] - # GEM scalars (can have free indices) for collecting the - # contributions from the subelements. - scalars = [] - for fe, ps_, e, D, alpha in zip(self.factors, ps_factors, entities, Ds, alphas): - # Ask the subelement to tabulate at the required derivative order. - value = fe.basis_evaluation(ps_, entity=e, derivative=sum(D)) - # Nice, but now we have got a subelement derivative - # tensor of the given order, while we only need a - # specific derivative. So we convert the subelement - # canonical multiindex to tensor multiindex. - d = tuple(chain(*[(dim,) * count for dim, count in enumerate(D)])) - # Turn basis shape to free indices, select the right - # derivative entry, and collect the result. - scalars.append(gem.Indexed(value, alpha + d)) - # Multiply the values from the subelements and insert the - # result into the derivative tensor. - tensor[delta] = reduce(gem.Product, scalars) - - # Convert the derivative tensor from a numpy object array to a - # GEM expression with only free indices (and scalar shape). - delta = tuple(gem.Index(extent=dimension) for i in range(derivative)) - if derivative: - value = gem.Indexed(gem.ListTensor(tensor), delta) - else: - value = tensor[()] - - # Wrap up non-point indices into shape - return gem.ComponentTensor(value, tuple(chain(*alphas)) + delta) + + # A list of multiindices, one multiindex per subelement, each + # multiindex describing the shape of basis functions of the + # subelement. + alphas = [fe.get_indices() for fe in self.factors] + + result = {} + for derivative in range(order + 1): + for Delta in mis(dimension, derivative): + # Split the multiindex for the subelements + deltas = [Delta[s] for s in dim_slices] + # GEM scalars (can have free indices) for collecting + # the contributions from the subelements. + scalars = [] + for fr, delta, alpha in zip(factor_results, deltas, alphas): + # Turn basis shape to free indices, select the + # right derivative entry, and collect the result. + scalars.append(gem.Indexed(fr[delta], alpha)) + # Multiply the values from the subelements and wrap up + # non-point indices into shape. + result[Delta] = gem.ComponentTensor( + reduce(gem.Product, scalars), + tuple(chain(*alphas)) + ) + return result def factor_point_set(product_cell, product_dim, point_set): diff --git a/finat/tensorfiniteelement.py b/finat/tensorfiniteelement.py index 703d2d0af..101b26d90 100644 --- a/finat/tensorfiniteelement.py +++ b/finat/tensorfiniteelement.py @@ -1,9 +1,12 @@ from __future__ import absolute_import, print_function, division +from six import iteritems from functools import reduce -from .finiteelementbase import FiniteElementBase + import gem +from finat.finiteelementbase import FiniteElementBase + class TensorFiniteElement(FiniteElementBase): @@ -55,7 +58,7 @@ def index_shape(self): def value_shape(self): return self._base_element.value_shape + self._shape - def basis_evaluation(self, ps, entity=None, derivative=0): + def basis_evaluation(self, order, ps, entity=None): r"""Produce the recipe for basis function evaluation at a set of points :math:`q`: .. math:: @@ -63,26 +66,26 @@ def basis_evaluation(self, ps, entity=None, derivative=0): \nabla\boldsymbol\phi_{(\epsilon \gamma \zeta) (i \alpha \beta) q} = \delta_{\alpha \epsilon} \deta{\beta \gamma}\nabla\phi_{\zeta i q} """ - - scalarbasis = self._base_element.basis_evaluation(ps, entity, derivative) - - indices = tuple(gem.Index() for i in scalarbasis.shape) - - # Work out which of the indices are for what. - pi = len(self._base_element.index_shape) - d = derivative - - # New basis function and value indices. - i = tuple(gem.Index(extent=d) for d in self._shape) - vi = tuple(gem.Index(extent=d) for d in self._shape) - - new_indices = indices[:pi] + i + indices[pi: len(indices) - d] + vi + indices[len(indices) - d:] - - return gem.ComponentTensor(gem.Product(reduce(gem.Product, - (gem.Delta(j, k) - for j, k in zip(i, vi))), - gem.Indexed(scalarbasis, indices)), - new_indices) + # Old basis function and value indices + scalar_i = self._base_element.get_indices() + scalar_vi = self._base_element.get_value_indices() + + # New basis function and value indices + tensor_i = tuple(gem.Index(extent=d) for d in self._shape) + tensor_vi = tuple(gem.Index(extent=d) for d in self._shape) + + # Couple new basis function and value indices + deltas = reduce(gem.Product, (gem.Delta(j, k) + for j, k in zip(tensor_i, tensor_vi))) + + scalar_result = self._base_element.basis_evaluation(order, ps, entity) + result = {} + for alpha, expr in iteritems(scalar_result): + result[alpha] = gem.ComponentTensor( + gem.Product(deltas, gem.Indexed(expr, scalar_i + scalar_vi)), + scalar_i + tensor_i + scalar_vi + tensor_vi + ) + return result def __hash__(self): """TensorFiniteElements are equal if they have the same base element From d3e8689f4e8870f36d07ad7b9e9bf16548bb378c Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 24 Oct 2016 11:39:52 +0100 Subject: [PATCH 289/749] fix things for PR comments --- finat/fiat_elements.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 8dd2c5cf0..83883eea6 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -1,6 +1,5 @@ from __future__ import absolute_import, print_function, division from six import iteritems -from six.moves import range from .finiteelementbase import FiniteElementBase import FIAT @@ -29,17 +28,15 @@ def basis_evaluation(self, order, ps, entity=None): '''Return code for evaluating the element at known points on the reference element. + :param order: return derivatives up to this order. :param ps: the point set. :param entity: the cell entity on which to tabulate. - :param derivative: the derivative to take of the basis functions. ''' fiat_result = self._fiat_element.tabulate(order, ps.points, entity) result = {} for alpha, table in iteritems(fiat_result): # Points be the first dimension, not last. - reordering = list(range(len(table.shape))) - reordering = [reordering[-1]] + reordering[:-1] - table = table.transpose(reordering) + table = np.rollaxis(table, -1, 0) derivative = sum(alpha) if derivative < self._degree: From 2371dda3f11bd08e9253b615348b04fe9c2c574e Mon Sep 17 00:00:00 2001 From: Thomas Gibson Date: Mon, 6 Jun 2016 16:40:25 +0100 Subject: [PATCH 290/749] Trace element and new FIAT compatibility --- gem/gem.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/gem/gem.py b/gem/gem.py index eaf23ecdc..0d437ad30 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -32,7 +32,8 @@ 'MaxValue', 'Comparison', 'LogicalNot', 'LogicalAnd', 'LogicalOr', 'Conditional', 'Index', 'VariableIndex', 'Indexed', 'FlexiblyIndexed', 'ComponentTensor', - 'IndexSum', 'ListTensor', 'partial_indexed', 'reshape'] + 'IndexSum', 'ListTensor', 'partial_indexed', 'reshape', + 'Failure'] class NodeMeta(type): @@ -90,6 +91,17 @@ class Scalar(Node): shape = () +class Failure(Terminal): + """Abstract class for failure GEM nodes.""" + + __slots__ = ('shape', 'exc_info') + __front__ = ('shape', 'exc_info') + + def __init__(self, shape, exc_info): + self.shape = shape + self.exc_info = exc_info + + class Constant(Terminal): """Abstract base class for constant types. From 50d0f20864e4ffbbc48718f4b59de25ff316d9b4 Mon Sep 17 00:00:00 2001 From: Thomas Gibson Date: Wed, 12 Oct 2016 16:12:12 +0100 Subject: [PATCH 291/749] GEM Failure works - improved error handling --- gem/gem.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 0d437ad30..dbb76f84c 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -94,12 +94,12 @@ class Scalar(Node): class Failure(Terminal): """Abstract class for failure GEM nodes.""" - __slots__ = ('shape', 'exc_info') - __front__ = ('shape', 'exc_info') + __slots__ = ('shape', 'exception') + __front__ = ('shape', 'exception') - def __init__(self, shape, exc_info): + def __init__(self, shape, exception): self.shape = shape - self.exc_info = exc_info + self.exception = exception class Constant(Terminal): @@ -126,6 +126,10 @@ def value(self): assert not self.shape return 0.0 + @property + def array(self): + return numpy.zeros(self.shape) + class Identity(Constant): """Identity matrix""" From 7afcfc1ad88d9f564fda074b19a106186e2bee32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Homolya?= Date: Fri, 4 Nov 2016 12:23:03 +0100 Subject: [PATCH 292/749] introduce as_gem --- gem/gem.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index dbb76f84c..55eda4dc5 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -16,9 +16,12 @@ from __future__ import absolute_import, print_function, division from six import with_metaclass +from six.moves import map from abc import ABCMeta +import collections from itertools import chain +import numbers from operator import attrgetter import numpy @@ -27,13 +30,13 @@ from gem.node import Node as NodeBase -__all__ = ['Node', 'Identity', 'Literal', 'Zero', 'Variable', 'Sum', - 'Product', 'Division', 'Power', 'MathFunction', 'MinValue', - 'MaxValue', 'Comparison', 'LogicalNot', 'LogicalAnd', - 'LogicalOr', 'Conditional', 'Index', 'VariableIndex', - 'Indexed', 'FlexiblyIndexed', 'ComponentTensor', - 'IndexSum', 'ListTensor', 'partial_indexed', 'reshape', - 'Failure'] +__all__ = ['Node', 'Identity', 'Literal', 'Zero', 'Failure', + 'Variable', 'Sum', 'Product', 'Division', 'Power', + 'MathFunction', 'MinValue', 'MaxValue', 'Comparison', + 'LogicalNot', 'LogicalAnd', 'LogicalOr', 'Conditional', + 'Index', 'VariableIndex', 'Indexed', 'FlexiblyIndexed', + 'ComponentTensor', 'IndexSum', 'ListTensor', 'as_gem', + 'partial_indexed', 'reshape'] class NodeMeta(type): @@ -626,6 +629,20 @@ def unique(indices): return tuple(sorted(set(indices), key=id)) +def as_gem(expr): + """Cast an expression to GEM.""" + if isinstance(expr, Node): + return expr + elif isinstance(expr, numbers.Number): + return Literal(expr) + elif isinstance(expr, numpy.ndarray) and expr.dtype != object: + return Literal(expr) + elif isinstance(expr, collections.Iterable): + return ListTensor(list(map(as_gem, expr))) + else: + raise ValueError("Cannot cast %s to GEM." % (expr,)) + + def partial_indexed(tensor, indices): """Generalised indexing into a tensor. The number of indices may be less than or equal to the rank of the tensor, so the result may From 6246f10dc22359e8f70308c752e00119825a73a6 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 4 Nov 2016 19:48:42 +0000 Subject: [PATCH 293/749] implement FFC rounding on GEM expressions --- gem/optimise.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/gem/optimise.py b/gem/optimise.py index 1466e1f43..cbd62943f 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -15,6 +15,43 @@ partial_indexed) +@singledispatch +def literal_rounding(node, self): + """Perform FFC rounding of FIAT tabulation matrices on the literals of + a GEM expression. + + :arg node: root of the expression + :arg self: function for recursive calls + """ + raise AssertionError("cannot handle type %s" % type(node)) + +literal_rounding.register(Node)(reuse_if_untouched) + + +@literal_rounding.register(Literal) +def literal_rounding_literal(node, self): + table = node.array + epsilon = self.epsilon + table[abs(table) < epsilon] = 0 + table[abs(table - 1.0) < epsilon] = 1.0 + table[abs(table + 1.0) < epsilon] = -1.0 + table[abs(table - 0.5) < epsilon] = 0.5 + table[abs(table + 0.5) < epsilon] = -0.5 + return Literal(table) + + +def ffc_rounding(expression, epsilon): + """Perform FFC rounding of FIAT tabulation matrices on the literals of + a GEM expression. + + :arg expression: GEM expression + :arg epsilon: tolerance limit for rounding + """ + mapper = Memoizer(literal_rounding) + mapper.epsilon = epsilon + return mapper(expression) + + @singledispatch def replace_indices(node, self, subst): """Replace free indices in a GEM expression. From 910da799f588cb6d66d6481fef866e9231eacda8 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 4 Nov 2016 20:24:55 +0000 Subject: [PATCH 294/749] add comments --- gem/optimise.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gem/optimise.py b/gem/optimise.py index cbd62943f..4d084bbd6 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -32,6 +32,7 @@ def literal_rounding(node, self): def literal_rounding_literal(node, self): table = node.array epsilon = self.epsilon + # Copied from FFC (ffc/quadrature/quadratureutils.py) table[abs(table) < epsilon] = 0 table[abs(table - 1.0) < epsilon] = 1.0 table[abs(table + 1.0) < epsilon] = -1.0 From dddfc05f9b53fb0bd4edddf1b04f706a14509af6 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 4 Nov 2016 22:08:16 +0000 Subject: [PATCH 295/749] handle new FIAT H(div) trace element --- finat/fiat_elements.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 83883eea6..3fadd1009 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -35,6 +35,10 @@ def basis_evaluation(self, order, ps, entity=None): fiat_result = self._fiat_element.tabulate(order, ps.points, entity) result = {} for alpha, table in iteritems(fiat_result): + if isinstance(table, Exception): + result[alpha] = gem.Failure(self.index_shape + self.value_shape, table) + continue + # Points be the first dimension, not last. table = np.rollaxis(table, -1, 0) From b956e008f6caa64284121e69194309bf788e7d41 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 16 Nov 2016 14:25:15 +0000 Subject: [PATCH 296/749] fix latest flake8 --- gem/gem.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gem/gem.py b/gem/gem.py index 55eda4dc5..eaedea8d8 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -362,6 +362,7 @@ class IndexBase(with_metaclass(ABCMeta)): """Abstract base class for indices.""" pass + IndexBase.register(int) From f4995f8ab5940de96828352ae58f44d4cd73f622 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 24 Oct 2016 16:51:19 +0100 Subject: [PATCH 297/749] delta elimination and sum factorisation --- gem/optimise.py | 145 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 143 insertions(+), 2 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index 1c5c71da6..8cb07e5c6 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -2,17 +2,22 @@ expressions.""" from __future__ import absolute_import, print_function, division -from six.moves import map +from six.moves import map, range, zip +from collections import deque from functools import reduce +from itertools import permutations + +import numpy from singledispatch import singledispatch from gem.node import Memoizer, MemoizerArg, reuse_if_untouched, reuse_if_untouched_arg from gem.gem import (Node, Terminal, Failure, Identity, Literal, Zero, - Sum, Comparison, Conditional, Index, + Product, Sum, Comparison, Conditional, Index, VariableIndex, Indexed, FlexiblyIndexed, IndexSum, ComponentTensor, ListTensor, Delta, partial_indexed) +from gem.utils import OrderedSet @singledispatch @@ -190,6 +195,142 @@ def select_expression(expressions, index): return ComponentTensor(selected, alpha) +@singledispatch +def _pull_delta_from_listtensor(node, self): + raise AssertionError("cannot handle type %s" % type(node)) + + +_pull_delta_from_listtensor.register(Node)(reuse_if_untouched) + + +@_pull_delta_from_listtensor.register(ListTensor) +def _pull_delta_from_listtensor_listtensor(node, self): + # Separate Delta nodes from other expressions + deltaz = [] + rests = [] + + for child in node.children: + deltas = OrderedSet() + others = [] + + # Traverse Product tree + queue = deque([child]) + while queue: + expr = queue.popleft() + if isinstance(expr, Product): + queue.extend(expr.children) + elif isinstance(expr, Delta): + assert expr not in deltas + deltas.add(expr) + else: + others.append(self(expr)) # looks for more ListTensors inside + + deltaz.append(deltas) + rests.append(reduce(Product, others)) + + # Factor out common Delta factors + common_deltas = set.intersection(*[set(ds) for ds in deltaz]) + deltaz = [[d for d in ds if d not in common_deltas] for ds in deltaz] + + # Rebuild ListTensor + new_children = [reduce(Product, ds, rest) + for ds, rest in zip(deltaz, rests)] + result = node.reconstruct(*new_children) + + # Apply common Delta factors + if common_deltas: + alpha = tuple(Index(extent=d) for d in result.shape) + expr = reduce(Product, common_deltas, Indexed(result, alpha)) + result = ComponentTensor(expr, alpha) + return result + + +def pull_delta_from_listtensor(expression): + mapper = Memoizer(_pull_delta_from_listtensor) + return mapper(expression) + + +def contraction(expression): + # Pull Delta nodes out of annoying ListTensors, and eliminate + # annoying ComponentTensors + expression, = remove_componenttensors([expression]) + expression = pull_delta_from_listtensor(expression) + expression, = remove_componenttensors([expression]) + + # Flatten a product tree + sum_indices = [] + factors = [] + + queue = deque([expression]) + while queue: + expr = queue.popleft() + if isinstance(expr, IndexSum): + queue.append(expr.children[0]) + sum_indices.append(expr.index) + elif isinstance(expr, Product): + queue.extend(expr.children) + else: + factors.append(expr) + + # Try to eliminate Delta nodes + delta_queue = [(f, index) + for f in factors if isinstance(f, Delta) + for index in (f.i, f.j) if index in sum_indices] + while delta_queue: + delta, from_ = delta_queue[0] + to_, = list({delta.i, delta.j} - {from_}) + + sum_indices.remove(from_) + + mapper = MemoizerArg(filtered_replace_indices) + factors = [mapper(e, ((from_, to_),)) for e in factors] + + delta_queue = [(f, index) + for f in factors if isinstance(f, Delta) + for index in (f.i, f.j) if index in sum_indices] + + # Drop ones + factors = [e for e in factors if e != Literal(1)] + + # Sum factorisation + expression = None + best_flops = numpy.inf + + for ordering in permutations(factors): + deps = [set(sum_indices) & set(factor.free_indices) + for factor in ordering] + + scan_deps = [None] * len(factors) + scan_deps[0] = deps[0] + for i in range(1, len(factors)): + scan_deps[i] = scan_deps[i - 1] | deps[i] + + sum_at = [None] * len(factors) + sum_at[0] = scan_deps[0] + for i in range(1, len(factors)): + sum_at[i] = scan_deps[i] - scan_deps[i - 1] + + expr = None + flops = 0 + for s, f in reversed(list(zip(sum_at, ordering))): + if expr is None: + expr = f + else: + expr = Product(f, expr) + flops += numpy.prod([i.extent for i in expr.free_indices], dtype=int) + if s: + flops += numpy.prod([i.extent for i in s]) + for i in sum_indices: + if i in s: + expr = IndexSum(expr, i) + + if flops < best_flops: + expression = expr + best_flops = flops + + return expression + + @singledispatch def _replace_delta(node, self): raise AssertionError("cannot handle type %s" % type(node)) From b04324e267515ec4b199f04c9768c768dc05736c Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 24 Oct 2016 17:55:42 +0100 Subject: [PATCH 298/749] do not generate empty for loops COFFEE loop merger does not like them. --- gem/impero.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/gem/impero.py b/gem/impero.py index 9994e5ff5..b990aa993 100644 --- a/gem/impero.py +++ b/gem/impero.py @@ -146,6 +146,13 @@ class For(Node): __slots__ = ('index', 'children') __front__ = ('index',) + def __new__(cls, index, statement): + assert isinstance(statement, Block) + if not statement.children: + return Noop(None) + else: + return super(For, cls).__new__(cls) + def __init__(self, index, statement): self.index = index self.children = (statement,) From de819be8f57b025c1d737b8c88db6bb3933e69c6 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 24 Oct 2016 19:27:51 +0100 Subject: [PATCH 299/749] IndexSum with multiindex --- gem/gem.py | 34 +++++++++++++++++----------------- gem/optimise.py | 21 ++++++++++++--------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 5251e40ff..1713fc68b 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -553,26 +553,27 @@ def __new__(cls, expression, multiindex): class IndexSum(Scalar): - __slots__ = ('children', 'index') - __back__ = ('index',) + __slots__ = ('children', 'multiindex') + __back__ = ('multiindex',) - def __new__(cls, summand, index): + def __new__(cls, summand, multiindex): # Sum zeros assert not summand.shape if isinstance(summand, Zero): return summand - # Sum a single expression - if index.extent == 1: - return Indexed(ComponentTensor(summand, (index,)), (0,)) + # No indices case + multiindex = tuple(multiindex) + if not multiindex: + return summand self = super(IndexSum, cls).__new__(cls) self.children = (summand,) - self.index = index + self.multiindex = multiindex # Collect shape and free indices - assert index in summand.free_indices - self.free_indices = unique(set(summand.free_indices) - {index}) + assert set(multiindex) <= set(summand.free_indices) + self.free_indices = unique(set(summand.free_indices) - set(multiindex)) return self @@ -714,14 +715,13 @@ def unique(indices): return tuple(sorted(set(indices), key=id)) -def index_sum(expression, index): - """Eliminates an index from the free indices of an expression by - summing over it. Returns the expression unchanged if the index is - not a free index of the expression.""" - if index in expression.free_indices: - return IndexSum(expression, index) - else: - return expression +def index_sum(expression, indices): + """Eliminates indices from the free indices of an expression by + summing over them. Skips any index that is not a free index of + the expression.""" + multiindex = tuple(index for index in indices + if index in expression.free_indices) + return IndexSum(expression, multiindex) def partial_indexed(tensor, indices): diff --git a/gem/optimise.py b/gem/optimise.py index 8cb07e5c6..7784e2bab 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -266,7 +266,7 @@ def contraction(expression): expr = queue.popleft() if isinstance(expr, IndexSum): queue.append(expr.children[0]) - sum_indices.append(expr.index) + sum_indices.extend(expr.multiindex) elif isinstance(expr, Product): queue.extend(expr.children) else: @@ -320,9 +320,7 @@ def contraction(expression): flops += numpy.prod([i.extent for i in expr.free_indices], dtype=int) if s: flops += numpy.prod([i.extent for i in s]) - for i in sum_indices: - if i in s: - expr = IndexSum(expr, i) + expr = IndexSum(expr, tuple(i for i in sum_indices if i in s)) if flops < best_flops: expression = expr @@ -387,13 +385,18 @@ def _unroll_indexsum(node, self): @_unroll_indexsum.register(IndexSum) # noqa def _(node, self): - if node.index.extent <= self.max_extent: + unroll = tuple(index for index in node.multiindex + if index.extent <= self.max_extent) + if unroll: # Unrolling summand = self(node.children[0]) - return reduce(Sum, - (Indexed(ComponentTensor(summand, (node.index,)), (i,)) - for i in range(node.index.extent)), - Zero()) + shape = tuple(index.extent for index in unroll) + unrolled = reduce(Sum, + (Indexed(ComponentTensor(summand, unroll), alpha) + for alpha in numpy.ndindex(shape)), + Zero()) + return IndexSum(unrolled, tuple(index for index in node.multiindex + if index not in unroll)) else: return reuse_if_untouched(node, self) From 18ee7eed042d145ea6a0334d49473a522cb28f5f Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 16 Nov 2016 16:08:44 +0000 Subject: [PATCH 300/749] little clean up --- gem/optimise.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gem/optimise.py b/gem/optimise.py index 7784e2bab..820799991 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -367,7 +367,7 @@ def expression(index): def replace_delta(expressions): """Lowers all Deltas in a multi-root expression DAG.""" mapper = Memoizer(_replace_delta) - return map(mapper, expressions) + return list(map(mapper, expressions)) @singledispatch From 58e57e816464b87c5d7fd46fc03ed7a688416554 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 16 Nov 2016 16:35:10 +0000 Subject: [PATCH 301/749] add docstrings --- gem/optimise.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/gem/optimise.py b/gem/optimise.py index 820799991..51be1ceaf 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -197,6 +197,11 @@ def select_expression(expressions, index): @singledispatch def _pull_delta_from_listtensor(node, self): + """Pull common delta factors out of ListTensor entries. + + :arg node: root of the expression + :arg self: function for recursive calls + """ raise AssertionError("cannot handle type %s" % type(node)) @@ -246,11 +251,21 @@ def _pull_delta_from_listtensor_listtensor(node, self): def pull_delta_from_listtensor(expression): + """Pull common delta factors out of ListTensor entries.""" mapper = Memoizer(_pull_delta_from_listtensor) return mapper(expression) def contraction(expression): + """Optimise the contractions of the tensor product at the root of + the expression, including: + + - IndexSum-Delta cancellation + - Sum factorisation + + This routine was designed with finite element coefficient + evaluation in mind. + """ # Pull Delta nodes out of annoying ListTensors, and eliminate # annoying ComponentTensors expression, = remove_componenttensors([expression]) From c5d4866e8c204a5c37b278d82a22d1a959080139 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 21 Nov 2016 11:57:57 +0000 Subject: [PATCH 302/749] limit O(N!) algorithm to N <= 5 --- gem/optimise.py | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index 51be1ceaf..e4521f302 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -308,21 +308,19 @@ def contraction(expression): factors = [e for e in factors if e != Literal(1)] # Sum factorisation - expression = None - best_flops = numpy.inf - - for ordering in permutations(factors): + def construct(ordering): + """Construct tensor product from a given ordering.""" deps = [set(sum_indices) & set(factor.free_indices) for factor in ordering] - scan_deps = [None] * len(factors) + scan_deps = [None] * len(ordering) scan_deps[0] = deps[0] - for i in range(1, len(factors)): + for i in range(1, len(ordering)): scan_deps[i] = scan_deps[i - 1] | deps[i] - sum_at = [None] * len(factors) + sum_at = [None] * len(ordering) sum_at[0] = scan_deps[0] - for i in range(1, len(factors)): + for i in range(1, len(ordering)): sum_at[i] = scan_deps[i] - scan_deps[i - 1] expr = None @@ -336,10 +334,25 @@ def contraction(expression): if s: flops += numpy.prod([i.extent for i in s]) expr = IndexSum(expr, tuple(i for i in sum_indices if i in s)) + return expr, flops + + if len(factors) <= 5: + expression = None + best_flops = numpy.inf + + for ordering in permutations(factors): + expr, flops = construct(ordering) + if flops < best_flops: + expression = expr + best_flops = flops + else: + # Cheap heuristic + def key(factor): + return len(set(sum_indices) & set(factor.free_indices)) + ordering = sorted(factors, key=key) - if flops < best_flops: - expression = expr - best_flops = flops + # FIXME: Log this unexpected case. + expression, flops = construct(ordering) return expression From 6c8528f11aff8f411c5d3a3ee4802bcdd79ff800 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 21 Nov 2016 13:00:39 +0000 Subject: [PATCH 303/749] log warning for unexpected case --- gem/optimise.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index e4521f302..447d80291 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -256,7 +256,7 @@ def pull_delta_from_listtensor(expression): return mapper(expression) -def contraction(expression): +def contraction(expression, logger=None): """Optimise the contractions of the tensor product at the root of the expression, including: @@ -347,11 +347,13 @@ def construct(ordering): best_flops = flops else: # Cheap heuristic + logger.warning("Unexpectedly many terms for sum factorisation: %d" + "; falling back on cheap heuristic.", len(factors)) + def key(factor): return len(set(sum_indices) & set(factor.free_indices)) ordering = sorted(factors, key=key) - # FIXME: Log this unexpected case. expression, flops = construct(ordering) return expression From efb62dbff60edb2d0204c280172da96225af7a7f Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 21 Nov 2016 13:15:09 +0000 Subject: [PATCH 304/749] add more comments --- gem/impero.py | 4 ++++ gem/optimise.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/gem/impero.py b/gem/impero.py index b990aa993..3eee267e1 100644 --- a/gem/impero.py +++ b/gem/impero.py @@ -147,8 +147,12 @@ class For(Node): __front__ = ('index',) def __new__(cls, index, statement): + # In case of an empty loop, create a Noop instead. + # Related: https://github.com/coneoproject/COFFEE/issues/98 assert isinstance(statement, Block) if not statement.children: + # This "works" because the loop_shape of this node is not + # asked any more. return Noop(None) else: return super(For, cls).__new__(cls) diff --git a/gem/optimise.py b/gem/optimise.py index 447d80291..15b18172f 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -310,19 +310,24 @@ def contraction(expression, logger=None): # Sum factorisation def construct(ordering): """Construct tensor product from a given ordering.""" + # deps: Indices for each term that need to be summed over. deps = [set(sum_indices) & set(factor.free_indices) for factor in ordering] + # scan_deps: Scan deps to the right with union operation. scan_deps = [None] * len(ordering) scan_deps[0] = deps[0] for i in range(1, len(ordering)): scan_deps[i] = scan_deps[i - 1] | deps[i] + # sum_at: What IndexSum nodes should be inserted before each + # term. An IndexSum binds all terms to its right. sum_at = [None] * len(ordering) sum_at[0] = scan_deps[0] for i in range(1, len(ordering)): sum_at[i] = scan_deps[i] - scan_deps[i - 1] + # Construct expression and count floating-point operations expr = None flops = 0 for s, f in reversed(list(zip(sum_at, ordering))): From 4e84edcbc8bf21107888d0c19b33e90c8ce5a7ef Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 24 Nov 2016 16:36:33 +0000 Subject: [PATCH 305/749] revise coefficient evaluation Seems to fix sum factorisation for coefficient derivatives. --- gem/gem.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gem/gem.py b/gem/gem.py index 1713fc68b..c298261a0 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -448,6 +448,10 @@ def __new__(cls, aggregate, multiindex): if isinstance(index, Index): index.set_extent(extent) + # Empty multiindex + if not multiindex: + return aggregate + # Zero folding if isinstance(aggregate, Zero): return Zero() From 3397778dbb0d98d0f478cb49a03215ee3d639e61 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 24 Nov 2016 11:03:28 +0000 Subject: [PATCH 306/749] improve constant folding --- gem/gem.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index c298261a0..a5c1ec593 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -200,12 +200,15 @@ def __new__(cls, a, b): assert not a.shape assert not b.shape - # Zero folding + # Constant folding if isinstance(a, Zero): return b elif isinstance(b, Zero): return a + if isinstance(a, Constant) and isinstance(b, Constant): + return Literal(a.value + b.value) + self = super(Sum, cls).__new__(cls) self.children = a, b return self @@ -218,10 +221,18 @@ def __new__(cls, a, b): assert not a.shape assert not b.shape - # Zero folding + # Constant folding if isinstance(a, Zero) or isinstance(b, Zero): return Zero() + if a == one: + return b + if b == one: + return a + + if isinstance(a, Constant) and isinstance(b, Constant): + return Literal(a.value * b.value) + self = super(Product, cls).__new__(cls) self.children = a, b return self @@ -234,12 +245,18 @@ def __new__(cls, a, b): assert not a.shape assert not b.shape - # Zero folding + # Constant folding if isinstance(b, Zero): raise ValueError("division by zero") if isinstance(a, Zero): return Zero() + if b == one: + return a + + if isinstance(a, Constant) and isinstance(b, Constant): + return Literal(a.value / b.value) + self = super(Division, cls).__new__(cls) self.children = a, b return self @@ -258,7 +275,7 @@ def __new__(cls, base, exponent): raise ValueError("cannot solve 0^0") return Zero() elif isinstance(exponent, Zero): - return Literal(1) + return one self = super(Power, cls).__new__(cls) self.children = base, exponent @@ -646,7 +663,7 @@ def __new__(cls, i, j): # \delta_{i,i} = 1 if i == j: - return Literal(1) + return one # Fixed indices if isinstance(i, int) and isinstance(j, int): @@ -768,3 +785,7 @@ def reshape(variable, *shapes): dim2idxs.append((0, tuple(idxs))) expr = FlexiblyIndexed(variable, tuple(dim2idxs)) return ComponentTensor(expr, tuple(indices)) + + +# Static one object for quicker constant folding +one = Literal(1) From b2415fe0cc8b71a645dd4332d90a81fb6c54f99e Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 24 Nov 2016 11:45:53 +0000 Subject: [PATCH 307/749] generate fast Jacobian code snippets --- gem/optimise.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/gem/optimise.py b/gem/optimise.py index 15b18172f..b32a2d3ab 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -446,3 +446,19 @@ def unroll_indexsum(expressions, max_extent): mapper = Memoizer(_unroll_indexsum) mapper.max_extent = max_extent return list(map(mapper, expressions)) + + +def aggressive_unroll(expression): + """Aggressively unrolls all loop structures.""" + # Unroll expression shape + if expression.shape: + tensor = numpy.empty(expression.shape, dtype=object) + for alpha in numpy.ndindex(expression.shape): + tensor[alpha] = Indexed(expression, alpha) + expression = ListTensor(tensor) + expression, = remove_componenttensors((ListTensor(tensor),)) + + # Unroll summation + expression, = unroll_indexsum((expression,), max_extent=numpy.inf) + expression, = remove_componenttensors((expression,)) + return expression From 9981dcdcad643d9fd0c5be8f77e19964bd61cf08 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 24 Nov 2016 17:13:40 +0000 Subject: [PATCH 308/749] remove Delta * ListTensor factorisation Not need any more. --- gem/optimise.py | 67 +------------------------------------------------ 1 file changed, 1 insertion(+), 66 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index b32a2d3ab..4cf8e9308 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -17,7 +17,6 @@ VariableIndex, Indexed, FlexiblyIndexed, IndexSum, ComponentTensor, ListTensor, Delta, partial_indexed) -from gem.utils import OrderedSet @singledispatch @@ -195,67 +194,6 @@ def select_expression(expressions, index): return ComponentTensor(selected, alpha) -@singledispatch -def _pull_delta_from_listtensor(node, self): - """Pull common delta factors out of ListTensor entries. - - :arg node: root of the expression - :arg self: function for recursive calls - """ - raise AssertionError("cannot handle type %s" % type(node)) - - -_pull_delta_from_listtensor.register(Node)(reuse_if_untouched) - - -@_pull_delta_from_listtensor.register(ListTensor) -def _pull_delta_from_listtensor_listtensor(node, self): - # Separate Delta nodes from other expressions - deltaz = [] - rests = [] - - for child in node.children: - deltas = OrderedSet() - others = [] - - # Traverse Product tree - queue = deque([child]) - while queue: - expr = queue.popleft() - if isinstance(expr, Product): - queue.extend(expr.children) - elif isinstance(expr, Delta): - assert expr not in deltas - deltas.add(expr) - else: - others.append(self(expr)) # looks for more ListTensors inside - - deltaz.append(deltas) - rests.append(reduce(Product, others)) - - # Factor out common Delta factors - common_deltas = set.intersection(*[set(ds) for ds in deltaz]) - deltaz = [[d for d in ds if d not in common_deltas] for ds in deltaz] - - # Rebuild ListTensor - new_children = [reduce(Product, ds, rest) - for ds, rest in zip(deltaz, rests)] - result = node.reconstruct(*new_children) - - # Apply common Delta factors - if common_deltas: - alpha = tuple(Index(extent=d) for d in result.shape) - expr = reduce(Product, common_deltas, Indexed(result, alpha)) - result = ComponentTensor(expr, alpha) - return result - - -def pull_delta_from_listtensor(expression): - """Pull common delta factors out of ListTensor entries.""" - mapper = Memoizer(_pull_delta_from_listtensor) - return mapper(expression) - - def contraction(expression, logger=None): """Optimise the contractions of the tensor product at the root of the expression, including: @@ -266,10 +204,7 @@ def contraction(expression, logger=None): This routine was designed with finite element coefficient evaluation in mind. """ - # Pull Delta nodes out of annoying ListTensors, and eliminate - # annoying ComponentTensors - expression, = remove_componenttensors([expression]) - expression = pull_delta_from_listtensor(expression) + # Eliminate annoying ComponentTensors expression, = remove_componenttensors([expression]) # Flatten a product tree From 9c57ba2c2247ed8a8ab693d91952bff1685c5da5 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 25 Nov 2016 11:23:44 +0000 Subject: [PATCH 309/749] use one in gem.optimise --- gem/optimise.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index 4cf8e9308..d1b6a13b4 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -16,7 +16,7 @@ Product, Sum, Comparison, Conditional, Index, VariableIndex, Indexed, FlexiblyIndexed, IndexSum, ComponentTensor, ListTensor, Delta, - partial_indexed) + partial_indexed, one) @singledispatch @@ -240,7 +240,7 @@ def contraction(expression, logger=None): for index in (f.i, f.j) if index in sum_indices] # Drop ones - factors = [e for e in factors if e != Literal(1)] + factors = [e for e in factors if e != one] # Sum factorisation def construct(ordering): @@ -331,7 +331,7 @@ def expression(index): raise ValueError("Cannot convert running index to expression.") e_i = expression(i) e_j = expression(j) - return Conditional(Comparison("==", e_i, e_j), Literal(1), Zero()) + return Conditional(Comparison("==", e_i, e_j), one, Zero()) def replace_delta(expressions): From 67d439658dcc5ad98256da22d411b5aa0c7f5364 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 25 Nov 2016 12:08:09 +0000 Subject: [PATCH 310/749] replace sum factorisation algorithm --- gem/optimise.py | 83 ++++++++++++++++++------------------------------- 1 file changed, 30 insertions(+), 53 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index d1b6a13b4..b5d37346c 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -2,9 +2,10 @@ expressions.""" from __future__ import absolute_import, print_function, division -from six.moves import map, range, zip +from six import itervalues +from six.moves import map, zip -from collections import deque +from collections import OrderedDict, deque from functools import reduce from itertools import permutations @@ -242,59 +243,35 @@ def contraction(expression, logger=None): # Drop ones factors = [e for e in factors if e != one] - # Sum factorisation - def construct(ordering): - """Construct tensor product from a given ordering.""" - # deps: Indices for each term that need to be summed over. - deps = [set(sum_indices) & set(factor.free_indices) - for factor in ordering] - - # scan_deps: Scan deps to the right with union operation. - scan_deps = [None] * len(ordering) - scan_deps[0] = deps[0] - for i in range(1, len(ordering)): - scan_deps[i] = scan_deps[i - 1] | deps[i] - - # sum_at: What IndexSum nodes should be inserted before each - # term. An IndexSum binds all terms to its right. - sum_at = [None] * len(ordering) - sum_at[0] = scan_deps[0] - for i in range(1, len(ordering)): - sum_at[i] = scan_deps[i] - scan_deps[i - 1] - - # Construct expression and count floating-point operations - expr = None - flops = 0 - for s, f in reversed(list(zip(sum_at, ordering))): - if expr is None: - expr = f - else: - expr = Product(f, expr) - flops += numpy.prod([i.extent for i in expr.free_indices], dtype=int) - if s: - flops += numpy.prod([i.extent for i in s]) - expr = IndexSum(expr, tuple(i for i in sum_indices if i in s)) - return expr, flops - - if len(factors) <= 5: - expression = None - best_flops = numpy.inf - - for ordering in permutations(factors): - expr, flops = construct(ordering) - if flops < best_flops: - expression = expr - best_flops = flops - else: - # Cheap heuristic - logger.warning("Unexpectedly many terms for sum factorisation: %d" - "; falling back on cheap heuristic.", len(factors)) + # Form groups by free indices + groups = OrderedDict() + for factor in factors: + groups[factor.free_indices] = [] + for factor in factors: + groups[factor.free_indices].append(factor) + groups = [reduce(Product, terms) for terms in itervalues(groups)] - def key(factor): - return len(set(sum_indices) & set(factor.free_indices)) - ordering = sorted(factors, key=key) + # Sum factorisation + expression = None + best_flops = numpy.inf - expression, flops = construct(ordering) + for ordering in permutations(sum_indices): + terms = groups[:] + flops = 0 + for sum_index in ordering: + contract = [t for t in terms if sum_index in t.free_indices] + deferred = [t for t in terms if sum_index not in t.free_indices] + + product = reduce(Product, contract) + term = IndexSum(product, (sum_index,)) + flops += len(contract) * numpy.prod([i.extent for i in product.free_indices], dtype=int) + terms = deferred + [term] + expr = reduce(Product, terms) + flops += (len(terms) - 1) * numpy.prod([i.extent for i in expr.free_indices], dtype=int) + + if flops < best_flops: + expression = expr + best_flops = flops return expression From 56ba21737ec27b198a591f5e871579d3610af2f8 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 25 Nov 2016 15:34:14 +0000 Subject: [PATCH 311/749] clean up sum factorisation --- gem/optimise.py | 90 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 63 insertions(+), 27 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index b5d37346c..05c1baa48 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -195,35 +195,15 @@ def select_expression(expressions, index): return ComponentTensor(selected, alpha) -def contraction(expression, logger=None): - """Optimise the contractions of the tensor product at the root of - the expression, including: - - - IndexSum-Delta cancellation - - Sum factorisation +def delta_elimination(sum_indices, factors): + """IndexSum-Delta cancellation. - This routine was designed with finite element coefficient - evaluation in mind. + :arg sum_indices: free indices for contractions + :arg factors: product factors + :returns: optimised (sum_indices, factors) """ - # Eliminate annoying ComponentTensors - expression, = remove_componenttensors([expression]) - - # Flatten a product tree - sum_indices = [] - factors = [] - - queue = deque([expression]) - while queue: - expr = queue.popleft() - if isinstance(expr, IndexSum): - queue.append(expr.children[0]) - sum_indices.extend(expr.multiindex) - elif isinstance(expr, Product): - queue.extend(expr.children) - else: - factors.append(expr) + sum_indices = list(sum_indices) # copy for modification - # Try to eliminate Delta nodes delta_queue = [(f, index) for f in factors if isinstance(f, Delta) for index in (f.i, f.j) if index in sum_indices] @@ -241,7 +221,18 @@ def contraction(expression, logger=None): for index in (f.i, f.j) if index in sum_indices] # Drop ones - factors = [e for e in factors if e != one] + return sum_indices, [e for e in factors if e != one] + + +def sum_factorise(sum_indices, factors): + """Optimise a tensor product throw sum factorisation. + + :arg sum_indices: free indices for contractions + :arg factors: product factors + :returns: optimised GEM expression + """ + if len(sum_indices) > 5: + raise NotImplementedError("Too many indices for sum factorisation!") # Form groups by free indices groups = OrderedDict() @@ -255,17 +246,31 @@ def contraction(expression, logger=None): expression = None best_flops = numpy.inf + # Consider all orderings of contraction indices for ordering in permutations(sum_indices): terms = groups[:] flops = 0 + # Apply contraction index by index for sum_index in ordering: + # Select terms that need to be part of the contraction contract = [t for t in terms if sum_index in t.free_indices] deferred = [t for t in terms if sum_index not in t.free_indices] + # A further optimisation opportunity is to consider + # various ways of building the product tree. product = reduce(Product, contract) term = IndexSum(product, (sum_index,)) + # For the operation count estimation we assume that no + # operations were saved with the particular product tree + # that we built above. flops += len(contract) * numpy.prod([i.extent for i in product.free_indices], dtype=int) + + # Replace the contracted terms with the result of the + # contraction. terms = deferred + [term] + + # If some contraction indices were independent, then we may + # still have several terms at this point. expr = reduce(Product, terms) flops += (len(terms) - 1) * numpy.prod([i.extent for i in expr.free_indices], dtype=int) @@ -276,6 +281,37 @@ def contraction(expression, logger=None): return expression +def contraction(expression): + """Optimise the contractions of the tensor product at the root of + the expression, including: + + - IndexSum-Delta cancellation + - Sum factorisation + + This routine was designed with finite element coefficient + evaluation in mind. + """ + # Eliminate annoying ComponentTensors + expression, = remove_componenttensors([expression]) + + # Flatten a product tree + sum_indices = [] + factors = [] + + queue = deque([expression]) + while queue: + expr = queue.popleft() + if isinstance(expr, IndexSum): + queue.append(expr.children[0]) + sum_indices.extend(expr.multiindex) + elif isinstance(expr, Product): + queue.extend(expr.children) + else: + factors.append(expr) + + return sum_factorise(*delta_elimination(sum_indices, factors)) + + @singledispatch def _replace_delta(node, self): raise AssertionError("cannot handle type %s" % type(node)) From c521fd5f7945f182befdff5d39f3aeff230ae1c9 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 28 Nov 2016 12:34:12 +0000 Subject: [PATCH 312/749] use setdefault --- gem/optimise.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index 05c1baa48..c8e9def13 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -237,9 +237,7 @@ def sum_factorise(sum_indices, factors): # Form groups by free indices groups = OrderedDict() for factor in factors: - groups[factor.free_indices] = [] - for factor in factors: - groups[factor.free_indices].append(factor) + groups.setdefault(factor.free_indices, []).append(factor) groups = [reduce(Product, terms) for terms in itervalues(groups)] # Sum factorisation From 371cb27736103ecd028e699b3c145a1282352e29 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 28 Nov 2016 13:42:59 +0000 Subject: [PATCH 313/749] remove redundant line --- gem/optimise.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gem/optimise.py b/gem/optimise.py index c8e9def13..58078d2b6 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -401,7 +401,6 @@ def aggressive_unroll(expression): tensor = numpy.empty(expression.shape, dtype=object) for alpha in numpy.ndindex(expression.shape): tensor[alpha] = Indexed(expression, alpha) - expression = ListTensor(tensor) expression, = remove_componenttensors((ListTensor(tensor),)) # Unroll summation From 4de2f145cd35c864bdd4db25864f92fd02c5177d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Homolya?= Date: Tue, 6 Dec 2016 16:21:27 +0000 Subject: [PATCH 314/749] fix latest flake8 issue --- finat/derivatives.py | 1 + 1 file changed, 1 insertion(+) diff --git a/finat/derivatives.py b/finat/derivatives.py index 080c32bae..ba3275f1f 100644 --- a/finat/derivatives.py +++ b/finat/derivatives.py @@ -11,6 +11,7 @@ def __init__(self, name, doc): def __str__(self): return self.name + grad = Derivative("grad", "Symbol for the gradient operation") div = Derivative("div", "Symbol for the divergence operation") curl = Derivative("curl", "Symbol for the curl operation") From bdbb3eb6e6d4c972f6ccb276525b6164fb4bc86d Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 6 Dec 2016 16:18:29 +0000 Subject: [PATCH 315/749] split (value) components of FIAT tabulation matrices --- finat/fiat_elements.py | 58 +++++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 3fadd1009..2edd717ee 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -32,33 +32,51 @@ def basis_evaluation(self, order, ps, entity=None): :param ps: the point set. :param entity: the cell entity on which to tabulate. ''' + space_dimension = self._fiat_element.space_dimension() + value_size = np.prod(self._fiat_element.value_shape(), dtype=int) fiat_result = self._fiat_element.tabulate(order, ps.points, entity) result = {} - for alpha, table in iteritems(fiat_result): - if isinstance(table, Exception): - result[alpha] = gem.Failure(self.index_shape + self.value_shape, table) + for alpha, fiat_table in iteritems(fiat_result): + if isinstance(fiat_table, Exception): + result[alpha] = gem.Failure(self.index_shape + self.value_shape, fiat_table) continue - # Points be the first dimension, not last. - table = np.rollaxis(table, -1, 0) - derivative = sum(alpha) - if derivative < self._degree: - point_indices = ps.indices - point_shape = tuple(index.extent for index in point_indices) - shape = point_shape + self.index_shape + self.value_shape - result[alpha] = gem.partial_indexed( - gem.Literal(table.reshape(shape)), - point_indices + table_roll = fiat_table.reshape( + space_dimension, value_size, len(ps.points) + ).transpose(1, 2, 0) + + exprs = [] + for table in table_roll: + if derivative < self._degree: + point_indices = ps.indices + point_shape = tuple(index.extent for index in point_indices) + exprs.append(gem.partial_indexed( + gem.Literal(table.reshape(point_shape + self.index_shape)), + point_indices + )) + elif derivative == self._degree: + # Make sure numerics satisfies theory + assert np.allclose(table, table.mean(axis=0, keepdims=True)) + exprs.append(gem.Literal(table[0])) + else: + # Make sure numerics satisfies theory + assert np.allclose(table, 0.0) + exprs.append(gem.Zero(self.index_shape)) + if self.value_shape: + beta = self.get_indices() + zeta = self.get_value_indices() + result[alpha] = gem.ComponentTensor( + gem.Indexed( + gem.ListTensor(np.array( + [gem.Indexed(expr, beta) for expr in exprs] + ).reshape(self.value_shape)), + zeta), + beta + zeta ) - elif derivative == self._degree: - # Make sure numerics satisfies theory - assert np.allclose(table, table.mean(axis=0, keepdims=True)) - result[alpha] = gem.Literal(table[0]) else: - # Make sure numerics satisfies theory - assert np.allclose(table, 0.0) - result[alpha] = gem.Zero(self.index_shape + self.value_shape) + expr, = exprs + result[alpha] = expr return result @property From 1490f15ba0016e2333f942238abd51a510571a64 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 6 Dec 2016 19:23:20 +0000 Subject: [PATCH 316/749] revise the rounding of FIAT tables --- gem/optimise.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index 58078d2b6..af51b35f5 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -38,13 +38,11 @@ def literal_rounding(node, self): def literal_rounding_literal(node, self): table = node.array epsilon = self.epsilon - # Copied from FFC (ffc/quadrature/quadratureutils.py) - table[abs(table) < epsilon] = 0 - table[abs(table - 1.0) < epsilon] = 1.0 - table[abs(table + 1.0) < epsilon] = -1.0 - table[abs(table - 0.5) < epsilon] = 0.5 - table[abs(table + 0.5) < epsilon] = -0.5 - return Literal(table) + # Mimic the rounding applied at COFFEE formatting, which in turn + # mimics FFC formatting. + one_decimal = numpy.round(table, 1) + one_decimal[numpy.logical_not(one_decimal)] = 0 # no minus zeros + return Literal(numpy.where(abs(table - one_decimal) < epsilon, one_decimal, table)) def ffc_rounding(expression, epsilon): From 931f517e3dcb2612665ff5ba6d488789c3dfed10 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 7 Dec 2016 09:13:10 +0000 Subject: [PATCH 317/749] revise expression selection Handle Zeros better. --- gem/optimise.py | 46 +++++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index af51b35f5..08548f89a 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -147,25 +147,33 @@ def _select_expression(expressions, index): if all(e == expr for e in expressions): return expr - cls = type(expr) - if all(type(e) == cls for e in expressions): - if not cls.__front__ and not cls.__back__: - assert all(len(e.children) == len(expr.children) for e in expressions) - assert len(expr.children) > 0 - - return expr.reconstruct(*[_select_expression(nth_children, index) - for nth_children in zip(*[e.children - for e in expressions])]) - elif issubclass(cls, Indexed): - assert all(e.multiindex == expr.multiindex for e in expressions) - return Indexed(_select_expression([e.children[0] - for e in expressions], index), expr.multiindex) - elif issubclass(cls, (Literal, Failure)): - return partial_indexed(ListTensor(expressions), (index,)) - else: - assert False - else: - assert False + types = set(map(type, expressions)) + if types <= {Indexed, Zero}: + multiindex, = set(e.multiindex for e in expressions if isinstance(e, Indexed)) + shape = tuple(i.extent for i in multiindex) + + def child(expression): + if isinstance(expression, Indexed): + return expression.children[0] + elif isinstance(expression, Zero): + return Zero(shape) + return Indexed(_select_expression(list(map(child, expressions)), index), multiindex) + + if types <= {Literal, Zero, Failure}: + return partial_indexed(ListTensor(expressions), (index,)) + + if len(types) == 1: + cls, = types + if cls.__front__ or cls.__back__: + raise NotImplementedError + assert all(len(e.children) == len(expr.children) for e in expressions) + assert len(expr.children) > 0 + + return expr.reconstruct(*[_select_expression(nth_children, index) + for nth_children in zip(*[e.children + for e in expressions])]) + + raise NotImplementedError def select_expression(expressions, index): From 64a0914e437a05020afefb609a49bb04e5c36522 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 7 Dec 2016 09:21:20 +0000 Subject: [PATCH 318/749] restore automatic unrolling of length one sums --- gem/gem.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/gem/gem.py b/gem/gem.py index a5c1ec593..76ef5035e 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -583,6 +583,15 @@ def __new__(cls, summand, multiindex): if isinstance(summand, Zero): return summand + # Unroll singleton sums + unroll = tuple(index for index in multiindex if index.extent <= 1) + if unroll: + assert numpy.prod([index.extent for index in unroll]) == 1 + summand = Indexed(ComponentTensor(summand, unroll), + (0,) * len(unroll)) + multiindex = tuple(index for index in multiindex + if index not in unroll) + # No indices case multiindex = tuple(multiindex) if not multiindex: From a0fc29ef0c469bc9aa79d7eee38e64a382ff6b38 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 7 Dec 2016 16:20:56 +0000 Subject: [PATCH 319/749] delete dead code --- finat/__init__.py | 3 -- finat/bernstein.py | 34 ------------- finat/derivatives.py | 17 ------- finat/fiat_elements.py | 24 ---------- finat/finiteelementbase.py | 49 +++---------------- finat/greek_alphabet.py | 65 ------------------------- finat/point_set.py | 28 ----------- finat/quadrature.py | 97 -------------------------------------- finat/ufl_interface.py | 42 ----------------- 9 files changed, 7 insertions(+), 352 deletions(-) delete mode 100644 finat/bernstein.py delete mode 100644 finat/derivatives.py delete mode 100644 finat/greek_alphabet.py delete mode 100644 finat/ufl_interface.py diff --git a/finat/__init__.py b/finat/__init__.py index a9e3cf02f..b9377f20e 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -4,10 +4,7 @@ from .fiat_elements import RaviartThomas, DiscontinuousRaviartThomas # noqa: F401 from .fiat_elements import BrezziDouglasMarini, BrezziDouglasFortinMarini # noqa: F401 from .fiat_elements import Nedelec, NedelecSecondKind, Regge # noqa: F401 -from .bernstein import Bernstein # noqa: F401 from .tensorfiniteelement import TensorFiniteElement # noqa: F401 from .tensor_product import TensorProductElement # noqa: F401 from .quadrilateral import QuadrilateralElement # noqa: F401 -from .derivatives import div, grad, curl # noqa: F401 from . import quadrature # noqa: F401 -from . import ufl_interface # noqa: F401 diff --git a/finat/bernstein.py b/finat/bernstein.py deleted file mode 100644 index 245a88001..000000000 --- a/finat/bernstein.py +++ /dev/null @@ -1,34 +0,0 @@ -from __future__ import absolute_import, print_function, division - -from .finiteelementbase import FiniteElementBase -import numpy as np - - -def pd(sd, d): - if sd == 3: - return (d + 1) * (d + 2) * (d + 3) // 6 - elif sd == 2: - return (d + 1) * (d + 2) // 2 - elif sd == 1: - return d + 1 - else: - raise NotImplementedError - - -class Bernstein(FiniteElementBase): - """Scalar - valued Bernstein element. Note: need to work out the - correct heirarchy for different Bernstein elements.""" - - def __init__(self, cell, degree): - super(Bernstein, self).__init__() - - self._cell = cell - self._degree = degree - - @property - def dofs_shape(self): - - degree = self.degree - dim = self.cell.get_spatial_dimension() - return (np.prod(range(degree + 1, degree + 1 + dim)) // - np.prod(range(1, dim + 1)),) diff --git a/finat/derivatives.py b/finat/derivatives.py deleted file mode 100644 index ba3275f1f..000000000 --- a/finat/derivatives.py +++ /dev/null @@ -1,17 +0,0 @@ -from __future__ import absolute_import, print_function, division - - -class Derivative(object): - "Abstract symbolic object for a derivative." - - def __init__(self, name, doc): - self.__doc__ = doc - self.name = name - - def __str__(self): - return self.name - - -grad = Derivative("grad", "Symbol for the gradient operation") -div = Derivative("div", "Symbol for the divergence operation") -curl = Derivative("curl", "Symbol for the curl operation") diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 2edd717ee..bf5b7cfdc 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -79,30 +79,6 @@ def basis_evaluation(self, order, ps, entity=None): result[alpha] = expr return result - @property - def entity_dofs(self): - '''The map of topological entities to degrees of - freedom for the finite element. - - Note that entity numbering needs to take into account the tensor case. - ''' - - return self._fiat_element.entity_dofs() - - @property - def entity_closure_dofs(self): - '''The map of topological entities to degrees of - freedom on the closure of those entities for the finite element.''' - - return self._fiat_element.entity_closure_dofs() - - @property - def facet_support_dofs(self): - '''The map of facet id to the degrees of freedom for which the - corresponding basis functions take non-zero values.''' - - return self._fiat_element.entity_support_dofs() - class ScalarFiatElement(FiatElementBase): @property diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index c057dbead..91a523d63 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -1,17 +1,12 @@ from __future__ import absolute_import, print_function, division +from six import with_metaclass -import gem - - -class UndefinedError(Exception): - def __init__(self, *args, **kwargs): - Exception.__init__(self, *args, **kwargs) +from abc import ABCMeta, abstractproperty, abstractmethod +import gem -class FiniteElementBase(object): - def __init__(self): - pass +class FiniteElementBase(with_metaclass(ABCMeta)): @property def cell(self): @@ -29,38 +24,17 @@ def degree(self): return self._degree - @property - def entity_dofs(self): - '''The map of topological entities to degrees of - freedom for the finite element. - - Note that entity numbering needs to take into account the tensor case. - ''' - - raise NotImplementedError - - @property - def entity_closure_dofs(self): - '''The map of topological entities to degrees of - freedom on the closure of those entities for the finite element.''' - - raise NotImplementedError - - @property + @abstractproperty def index_shape(self): '''A tuple indicating the number of degrees of freedom in the element. For example a scalar quadratic Lagrange element on a triangle would return (6,) while a vector valued version of the same element would return (6, 2)''' - raise NotImplementedError - - @property + @abstractproperty def value_shape(self): '''A tuple indicating the shape of the element.''' - raise NotImplementedError - def get_indices(self): '''A tuple of GEM :class:`Index` of the correct extents to loop over the basis functions of this element.''' @@ -73,6 +47,7 @@ def get_value_indices(self): return tuple(gem.Index(extent=d) for d in self.value_shape) + @abstractmethod def basis_evaluation(self, order, ps, entity=None): '''Return code for evaluating the element at known points on the reference element. @@ -82,8 +57,6 @@ def basis_evaluation(self, order, ps, entity=None): :param entity: the cell entity on which to tabulate. ''' - raise NotImplementedError - @property def preferred_quadrature(self): '''A list of quadrature rules whose structure this element is capable @@ -93,14 +66,6 @@ def preferred_quadrature(self): return () - def dual_evaluation(self, kernel_data): - '''Return code for evaluating an expression at the dual set. - - Note: what does the expression need to look like? - ''' - - raise NotImplementedError - def __hash__(self): """Elements are equal if they have the same class, degree, and cell.""" diff --git a/finat/greek_alphabet.py b/finat/greek_alphabet.py deleted file mode 100644 index e1d3de7d7..000000000 --- a/finat/greek_alphabet.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Translation table from utf-8 to greek variable names, taken from: -https://gist.github.com/piquadrat/765262#file-greek_alphabet-py -""" - -from __future__ import absolute_import, print_function, division - - -def translate_symbol(symbol): - """Translates utf-8 sub-strings into compilable variable names""" - name = symbol.decode("utf-8") - for k, v in greek_alphabet.iteritems(): - name = name.replace(k, v) - return name - - -greek_alphabet = { - u'\u0391': 'Alpha', - u'\u0392': 'Beta', - u'\u0393': 'Gamma', - u'\u0394': 'Delta', - u'\u0395': 'Epsilon', - u'\u0396': 'Zeta', - u'\u0397': 'Eta', - u'\u0398': 'Theta', - u'\u0399': 'Iota', - u'\u039A': 'Kappa', - u'\u039B': 'Lamda', - u'\u039C': 'Mu', - u'\u039D': 'Nu', - u'\u039E': 'Xi', - u'\u039F': 'Omicron', - u'\u03A0': 'Pi', - u'\u03A1': 'Rho', - u'\u03A3': 'Sigma', - u'\u03A4': 'Tau', - u'\u03A5': 'Upsilon', - u'\u03A6': 'Phi', - u'\u03A7': 'Chi', - u'\u03A8': 'Psi', - u'\u03A9': 'Omega', - u'\u03B1': 'alpha', - u'\u03B2': 'beta', - u'\u03B3': 'gamma', - u'\u03B4': 'delta', - u'\u03B5': 'epsilon', - u'\u03B6': 'zeta', - u'\u03B7': 'eta', - u'\u03B8': 'theta', - u'\u03B9': 'iota', - u'\u03BA': 'kappa', - u'\u03BB': 'lamda', - u'\u03BC': 'mu', - u'\u03BD': 'nu', - u'\u03BE': 'xi', - u'\u03BF': 'omicron', - u'\u03C0': 'pi', - u'\u03C1': 'rho', - u'\u03C3': 'sigma', - u'\u03C4': 'tau', - u'\u03C5': 'upsilon', - u'\u03C6': 'phi', - u'\u03C7': 'chi', - u'\u03C8': 'psi', - u'\u03C9': 'omega', -} diff --git a/finat/point_set.py b/finat/point_set.py index 4d4faec42..19c47c425 100644 --- a/finat/point_set.py +++ b/finat/point_set.py @@ -80,31 +80,3 @@ def expression(self): for i in range(point_set.dimension): result.append(gem.Indexed(point_set.expression, (i,))) return gem.ListTensor(result) - - -# class MappedMixin(object): -# def __init__(self, *args): -# super(MappedMixin, self).__init__(*args) - -# def map_points(self): -# raise NotImplementedError - - -# class DuffyMappedMixin(MappedMixin): -# def __init__(self, *args): -# super(DuffyMappedMixin, self).__init__(*args) - -# def map_points(self): -# raise NotImplementedError - - -# class StroudPointSet(TensorPointSet, DuffyMappedMixin): -# """A set of points with the structure required for Stroud quadrature.""" - -# def __init__(self, factor_sets): -# super(StroudPointSet, self).__init__(factor_sets) - - -# class GaussLobattoPointSet(PointSet): -# """A set of 1D Gauss Lobatto points. This is a separate class in order -# to allow elements to apply spectral element tricks.""" diff --git a/finat/quadrature.py b/finat/quadrature.py index 35c2f5a49..fea017c43 100644 --- a/finat/quadrature.py +++ b/finat/quadrature.py @@ -97,100 +97,3 @@ def point_set(self): @cached_property def weight_expression(self): return reduce(gem.Product, (q.weight_expression for q in self.factors)) - - # def refactor(self, dims): - # """Refactor this quadrature rule into a tuple of quadrature rules with - # the dimensions specified.""" - - # qs = [] - # i = 0 - # next_i = 0 - # for dim in dims: - # i = next_i - # next_dim = 0 - # while next_dim < dim: - # next_dim += self.factors[next_i].spatial_dimension - # if next_dim > dim: - # raise ValueError("Element and quadrature incompatible") - # next_i += 1 - # if next_i - i > 1: - # qs.append(TensorProductQuadratureRule(*self.factors[i: next_i])) - # else: - # qs.append(self.factors[i]) - # return tuple(qs) - - -# class StroudQuadrature(QuadratureRule): -# def __init__(self, cell, degree): -# """Stroud quadrature rule on simplices.""" - -# sd = cell.get_spatial_dimension() - -# if sd + 1 != len(cell.vertices): -# raise ValueError("cell must be a simplex") - -# points = numpy.zeros((sd, degree)) -# weights = numpy.zeros((sd, degree)) - -# for d in range(1, sd + 1): -# [x, w] = gauss_jacobi_rule(sd - d, 0, degree) -# points[d - 1, :] = 0.5 * (x + 1) -# weights[d - 1, :] = w - -# scale = 0.5 -# for d in range(1, sd + 1): -# weights[sd - d, :] *= scale -# scale *= 0.5 - -# super(StroudQuadrature, self).__init__( -# cell, -# StroudPointSet(map(PointSet, points)), -# weights) - - -# class GaussLobattoQuadrature(QuadratureRule): -# def __init__(self, cell, points): -# """Gauss-Lobatto-Legendre quadrature on hypercubes. -# :param cell: The reference cell on which to define the quadrature. -# :param points: The number of points, or a tuple giving the number of -# points in each dimension. -# """ - -# def expand_quad(cell, points): -# d = cell.get_spatial_dimension() -# if d == 1: -# return ((cell, points, -# FIAT.quadrature.GaussLobattoQuadratureLineRule(cell, points[0])),) -# else: -# try: -# # Note this requires generalisation for n-way products. -# d_a = cell.A.get_spatial_dimension() -# return expand_quad(cell.A, points[:d_a])\ -# + expand_quad(cell.B, points[d_a:]) -# except AttributeError(): -# raise ValueError("Unable to create Gauss-Lobatto quadrature on ", -# + str(cell)) -# try: -# points = tuple(points) -# except TypeError: -# points = (points,) - -# if len(points) == 1: -# points *= cell.get_spatial_dimension() - -# cpq = expand_quad(cell, points) - -# # uniquify q. -# lookup = {(c, p): (GaussLobattoPointSet(q.get_points()), -# PointSet(q.get_weights())) for c, p, q in cpq} -# pointset = tuple(lookup[c, p][0] for c, p, _ in cpq) -# weightset = tuple(lookup[c, p][1] for c, p, _ in cpq) - -# if len(cpq) == 1: -# super(GaussLobattoQuadrature, self).__init__( -# cell, pointset[0], weightset[0]) -# else: -# super(GaussLobattoQuadrature, self).__init__( -# cell, -# TensorPointSet(pointset), -# weightset) diff --git a/finat/ufl_interface.py b/finat/ufl_interface.py deleted file mode 100644 index af4655c29..000000000 --- a/finat/ufl_interface.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Provide interface functions which take UFL objects and return FInAT ones.""" - -from __future__ import absolute_import, print_function, division - -import finat -import FIAT - - -def _q_element(cell, degree): - # Produce a Q element from GLL elements. - - if cell.get_spatial_dimension() == 1: - return finat.GaussLobatto(cell, degree) - else: - return finat.ScalarProductElement(_q_element(cell.A, degree), - _q_element(cell.B, degree)) - - -_cell_map = { - "triangle": FIAT.reference_element.UFCTriangle(), - "interval": FIAT.reference_element.UFCInterval(), - "quadrilateral": FIAT.reference_element.FiredrakeQuadrilateral() -} - -_element_map = { - "Lagrange": finat.Lagrange, - "Discontinuous Lagrange": finat.DiscontinuousLagrange, - "Q": _q_element -} - - -def cell_from_ufl(cell): - - return _cell_map[cell.cellname()] - - -def element_from_ufl(element): - - # Need to handle the product cases. - - return _element_map[element.family()](cell_from_ufl(element.cell()), - element.degree()) From 16d83e29cac1001df43d2e27202a382ba8433eb9 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 7 Dec 2016 17:14:43 +0000 Subject: [PATCH 320/749] improve finite elements * remove some unused or incomplete features * atone for element construction sins --- finat/fiat_elements.py | 67 +++++++++++++++--------------------- finat/finiteelementbase.py | 31 ++--------------- finat/quadrilateral.py | 13 +++++-- finat/tensor_product.py | 13 ++++--- finat/tensorfiniteelement.py | 26 +++++--------- 5 files changed, 58 insertions(+), 92 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index bf5b7cfdc..4234f0dcc 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -10,19 +10,26 @@ class FiatElementBase(FiniteElementBase): """Base class for finite elements for which the tabulation is provided by FIAT.""" - def __init__(self, cell, degree): + def __init__(self, fiat_element): super(FiatElementBase, self).__init__() + self._element = fiat_element + + @property + def cell(self): + return self._element.get_reference_element() - self._cell = cell - self._degree = degree + @property + def degree(self): + # Requires FIAT.CiarletElement + return self._element.degree() @property def index_shape(self): - return (self._fiat_element.space_dimension(),) + return (self._element.space_dimension(),) @property def value_shape(self): - return self._fiat_element.value_shape() + return self._element.value_shape() def basis_evaluation(self, order, ps, entity=None): '''Return code for evaluating the element at known points on the @@ -32,9 +39,9 @@ def basis_evaluation(self, order, ps, entity=None): :param ps: the point set. :param entity: the cell entity on which to tabulate. ''' - space_dimension = self._fiat_element.space_dimension() - value_size = np.prod(self._fiat_element.value_shape(), dtype=int) - fiat_result = self._fiat_element.tabulate(order, ps.points, entity) + space_dimension = self._element.space_dimension() + value_size = np.prod(self._element.value_shape(), dtype=int) + fiat_result = self._element.tabulate(order, ps.points, entity) result = {} for alpha, fiat_table in iteritems(fiat_result): if isinstance(fiat_table, Exception): @@ -48,14 +55,14 @@ def basis_evaluation(self, order, ps, entity=None): exprs = [] for table in table_roll: - if derivative < self._degree: + if derivative < self.degree: point_indices = ps.indices point_shape = tuple(index.extent for index in point_indices) exprs.append(gem.partial_indexed( gem.Literal(table.reshape(point_shape + self.index_shape)), point_indices )) - elif derivative == self._degree: + elif derivative == self.degree: # Make sure numerics satisfies theory assert np.allclose(table, table.mean(axis=0, keepdims=True)) exprs.append(gem.Literal(table[0])) @@ -88,30 +95,22 @@ def value_shape(self): class Lagrange(ScalarFiatElement): def __init__(self, cell, degree): - super(Lagrange, self).__init__(cell, degree) - - self._fiat_element = FIAT.Lagrange(cell, degree) + super(Lagrange, self).__init__(FIAT.Lagrange(cell, degree)) class Regge(ScalarFiatElement): def __init__(self, cell, degree): - super(Regge, self).__init__(cell, degree) - - self._fiat_element = FIAT.Regge(cell, degree) + super(Regge, self).__init__(FIAT.Regge(cell, degree)) class GaussLobatto(ScalarFiatElement): def __init__(self, cell, degree): - super(GaussLobatto, self).__init__(cell, degree) - - self._fiat_element = FIAT.GaussLobatto(cell, degree) + super(GaussLobatto, self).__init__(FIAT.GaussLobatto(cell, degree)) class DiscontinuousLagrange(ScalarFiatElement): def __init__(self, cell, degree): - super(DiscontinuousLagrange, self).__init__(cell, degree) - - self._fiat_element = FIAT.DiscontinuousLagrange(cell, degree) + super(DiscontinuousLagrange, self).__init__(FIAT.DiscontinuousLagrange(cell, degree)) class VectorFiatElement(FiatElementBase): @@ -122,41 +121,29 @@ def value_shape(self): class RaviartThomas(VectorFiatElement): def __init__(self, cell, degree): - super(RaviartThomas, self).__init__(cell, degree) - - self._fiat_element = FIAT.RaviartThomas(cell, degree) + super(RaviartThomas, self).__init__(FIAT.RaviartThomas(cell, degree)) class DiscontinuousRaviartThomas(VectorFiatElement): def __init__(self, cell, degree): - super(DiscontinuousRaviartThomas, self).__init__(cell, degree) - - self._fiat_element = FIAT.DiscontinuousRaviartThomas(cell, degree) + super(DiscontinuousRaviartThomas, self).__init__(FIAT.DiscontinuousRaviartThomas(cell, degree)) class BrezziDouglasMarini(VectorFiatElement): def __init__(self, cell, degree): - super(BrezziDouglasMarini, self).__init__(cell, degree) - - self._fiat_element = FIAT.BrezziDouglasMarini(cell, degree) + super(BrezziDouglasMarini, self).__init__(FIAT.BrezziDouglasMarini(cell, degree)) class BrezziDouglasFortinMarini(VectorFiatElement): def __init__(self, cell, degree): - super(BrezziDouglasFortinMarini, self).__init__(cell, degree) - - self._fiat_element = FIAT.BrezziDouglasFortinMarini(cell, degree) + super(BrezziDouglasFortinMarini, self).__init__(FIAT.BrezziDouglasFortinMarini(cell, degree)) class Nedelec(VectorFiatElement): def __init__(self, cell, degree): - super(Nedelec, self).__init__(cell, degree) - - self._fiat_element = FIAT.Nedelec(cell, degree) + super(Nedelec, self).__init__(FIAT.Nedelec(cell, degree)) class NedelecSecondKind(VectorFiatElement): def __init__(self, cell, degree): - super(NedelecSecondKind, self).__init__(cell, degree) - - self._fiat_element = FIAT.NedelecSecondKind(cell, degree) + super(NedelecSecondKind, self).__init__(FIAT.NedelecSecondKind(cell, degree)) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 91a523d63..6b61a332c 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -8,22 +8,17 @@ class FiniteElementBase(with_metaclass(ABCMeta)): - @property + @abstractproperty def cell(self): - '''The reference cell on which the element is defined. - ''' - - return self._cell + '''The reference cell on which the element is defined.''' - @property + @abstractproperty def degree(self): '''The degree of the embedding polynomial space. In the tensor case this is a tuple. ''' - return self._degree - @abstractproperty def index_shape(self): '''A tuple indicating the number of degrees of freedom in the @@ -56,23 +51,3 @@ def basis_evaluation(self, order, ps, entity=None): :param ps: the point set object. :param entity: the cell entity on which to tabulate. ''' - - @property - def preferred_quadrature(self): - '''A list of quadrature rules whose structure this element is capable - of exploiting. Each entry in the list should be a pair (rule, - degree), where the degree might be `None` if the element has - no preferred quadrature degree.''' - - return () - - def __hash__(self): - """Elements are equal if they have the same class, degree, and cell.""" - - return hash((type(self), self._cell, self._degree)) - - def __eq__(self, other): - """Elements are equal if they have the same class, degree, and cell.""" - - return type(self) == type(other) and self._cell == other._cell and\ - self._degree == other._degree diff --git a/finat/quadrilateral.py b/finat/quadrilateral.py index 252ef0401..c0a582056 100644 --- a/finat/quadrilateral.py +++ b/finat/quadrilateral.py @@ -2,6 +2,8 @@ from FIAT.reference_element import FiredrakeQuadrilateral +from gem.utils import cached_property + from finat.finiteelementbase import FiniteElementBase @@ -12,11 +14,16 @@ class QuadrilateralElement(FiniteElementBase): def __init__(self, element): super(QuadrilateralElement, self).__init__() - self._cell = FiredrakeQuadrilateral() - self._degree = None # Who cares? Not used. - self.product = element + @cached_property + def cell(self): + return FiredrakeQuadrilateral() + + @property + def degree(self): + raise NotImplementedError("Unused property.") + def basis_evaluation(self, order, ps, entity=None): """Return code for evaluating the element at known points on the reference element. diff --git a/finat/tensor_product.py b/finat/tensor_product.py index b42115a40..08d24305e 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -10,6 +10,7 @@ from FIAT.reference_element import TensorProductCell import gem +from gem.utils import cached_property from finat.finiteelementbase import FiniteElementBase from finat.point_set import PointSet, TensorPointSet @@ -18,13 +19,17 @@ class TensorProductElement(FiniteElementBase): def __init__(self, factors): + super(TensorProductElement, self).__init__() self.factors = tuple(factors) assert all(fe.value_shape == () for fe in self.factors) - self._cell = TensorProductCell(*[fe.cell for fe in self.factors]) - # self._degree = sum(fe.degree for fe in factors) # correct? - # self._degree = max(fe.degree for fe in factors) # FIAT - self._degree = None # not used? + @cached_property + def cell(self): + return TensorProductCell(*[fe.cell for fe in self.factors]) + + @property + def degree(self): + raise NotImplementedError("Unused property.") @property def index_shape(self): diff --git a/finat/tensorfiniteelement.py b/finat/tensorfiniteelement.py index 101b26d90..e86631772 100644 --- a/finat/tensorfiniteelement.py +++ b/finat/tensorfiniteelement.py @@ -37,19 +37,22 @@ def __init__(self, element, shape): This form enables the simplification of the loop nests which will eventually be created, so it is the form we employ here.""" super(TensorFiniteElement, self).__init__() - - self._cell = element._cell - self._degree = element._degree - - self._shape = shape - self._base_element = element + self._shape = shape @property def base_element(self): """The base element of this tensor element.""" return self._base_element + @property + def cell(self): + return self._base_element.cell + + @property + def degree(self): + return self._base_element.degree + @property def index_shape(self): return self._base_element.index_shape + self._shape @@ -86,14 +89,3 @@ def basis_evaluation(self, order, ps, entity=None): scalar_i + tensor_i + scalar_vi + tensor_vi ) return result - - def __hash__(self): - """TensorFiniteElements are equal if they have the same base element - and shape.""" - return hash((self._shape, self._base_element)) - - def __eq__(self, other): - """TensorFiniteElements are equal if they have the same base element - and shape.""" - return type(self) == type(other) and self._shape == other._shape and \ - self._base_element == other._base_element From 490f76174eb5f32dce3e65e222ab6ab6e144db8f Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 7 Dec 2016 17:30:24 +0000 Subject: [PATCH 321/749] remove affine index groups They are not used for now. --- gem/gem.py | 68 +----------------------------------------------------- 1 file changed, 1 insertion(+), 67 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 76ef5035e..1e88ead82 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -33,8 +33,7 @@ 'LogicalNot', 'LogicalAnd', 'LogicalOr', 'Conditional', 'Index', 'VariableIndex', 'Indexed', 'FlexiblyIndexed', 'ComponentTensor', 'IndexSum', 'ListTensor', 'Delta', - 'IndexIterator', 'affine_index_group', 'index_sum', - 'partial_indexed', 'reshape'] + 'index_sum', 'partial_indexed', 'reshape'] class NodeMeta(type): @@ -413,22 +412,6 @@ def __lt__(self, other): return id(self) < id(other) -class AffineIndex(Index): - """An index in an affine_index_group. Do not instantiate directly but - instead call :func:`affine_index_group`.""" - __slots__ = ('name', 'extent', 'count', 'group') - - def __str__(self): - if self.name is None: - return "i_%d" % self.count - return self.name - - def __repr__(self): - if self.name is None: - return "AffineIndex(%r)" % self.count - return "AffineIndex(%r)" % self.name - - class VariableIndex(IndexBase): """An index that is constant during a single execution of the kernel, but whose value is not known at compile time.""" @@ -687,55 +670,6 @@ def __new__(cls, i, j): return self -class IndexIterator(object): - """An iterator whose value is a multi-index (tuple) iterating over the - extent of the supplied :class:`.Index` objects in a last index varies - fastest (ie 'c') ordering. - - :arg *indices: the indices over whose extent to iterate.""" - def __init__(self, *indices): - - self.affine_groups = set() - for i in indices: - if isinstance(i, AffineIndex): - try: - pos = tuple(indices.index(g) for g in i.group) - except ValueError: - raise ValueError("Only able to iterate over all indices in an affine group at once") - self.affine_groups.add((i.group, pos)) - - self.ndindex = numpy.ndindex(tuple(i.extent for i in indices)) - - def _affine_groups_legal(self, multiindex): - for group, pos in self.affine_groups: - if sum(multiindex[p] for p in pos) >= group[0].extent: - return False - return True - - def __iter__(self): - # Fix this for affine index groups. - while True: - multiindex = self.ndindex.next() - if self._affine_groups_legal(multiindex): - yield multiindex - - -def affine_index_group(n, extent): - """A set of indices whose values are constrained to lie in a simplex - subset of the iteration space. - - :arg n: the number of indices in the group. - :arg extent: sum(indices) < extent - """ - - group = tuple(AffineIndex(extent=extent) for i in range(n)) - - for g in group: - g.group = group - - return group - - def unique(indices): """Sorts free indices and eliminates duplicates. From d25a7d3f60896df8ab2a5f56b280f30523a5c929 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 7 Dec 2016 17:54:43 +0000 Subject: [PATCH 322/749] do not duplicate work --- gem/impero_utils.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/gem/impero_utils.py b/gem/impero_utils.py index 138feaff8..72be93d9b 100644 --- a/gem/impero_utils.py +++ b/gem/impero_utils.py @@ -34,6 +34,13 @@ class NoopError(Exception): pass +def preprocess_gem(expressions): + """Lower GEM nodes that cannot be translated to C directly.""" + expressions = optimise.replace_delta(expressions) + expressions = optimise.remove_componenttensors(expressions) + return expressions + + def compile_gem(return_variables, expressions, prefix_ordering, remove_zeros=False): """Compiles GEM to Impero. @@ -42,9 +49,6 @@ def compile_gem(return_variables, expressions, prefix_ordering, remove_zeros=Fal :arg prefix_ordering: outermost loop indices :arg remove_zeros: remove zero assignment to return variables """ - expressions = optimise.replace_delta(expressions) - expressions = optimise.remove_componenttensors(expressions) - # Remove zeros if remove_zeros: rv = [] From 5bc328b97749adb7843dead73bd7ba90f98b51bf Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 8 Dec 2016 14:11:51 +0000 Subject: [PATCH 323/749] change the internal representation of FlexiblyIndexed --- gem/gem.py | 84 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 63 insertions(+), 21 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 1e88ead82..0d28967fe 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -16,6 +16,7 @@ from __future__ import absolute_import, print_function, division from six import with_metaclass +from six.moves import range, zip from abc import ABCMeta from itertools import chain @@ -33,7 +34,7 @@ 'LogicalNot', 'LogicalAnd', 'LogicalOr', 'Conditional', 'Index', 'VariableIndex', 'Indexed', 'FlexiblyIndexed', 'ComponentTensor', 'IndexSum', 'ListTensor', 'Delta', - 'index_sum', 'partial_indexed', 'reshape'] + 'index_sum', 'partial_indexed', 'reshape', 'view'] class NodeMeta(type): @@ -488,7 +489,7 @@ def __init__(self, variable, dim2idxs): For example, if ``variable`` is rank two, and ``dim2idxs`` is - ((1, ((i, 2), (j, 3), (k, 4))), (0, ())) + ((1, ((i, 12), (j, 4), (k, 1))), (0, ())) then this corresponds to the indexing: @@ -498,31 +499,32 @@ def __init__(self, variable, dim2idxs): assert isinstance(variable, Variable) assert len(variable.shape) == len(dim2idxs) - indices = [] + dim2idxs_ = [] + free_indices = [] for dim, (offset, idxs) in zip(variable.shape, dim2idxs): - strides = [] + offset_ = offset + idxs_ = [] + last = 0 for idx in idxs: index, stride = idx - strides.append(stride) - if isinstance(index, Index): - if index.extent is None: - index.set_extent(stride) - elif not (index.extent <= stride): - raise ValueError("Index extent cannot exceed stride") - indices.append(index) + assert index.extent is not None + free_indices.append(index) + idxs_.append((index, stride)) + last += (index.extent - 1) * stride elif isinstance(index, int): - if not (index <= stride): - raise ValueError("Index cannot exceed stride") + offset_ += index * stride else: raise ValueError("Unexpected index type for flexible indexing") - if dim is not None and offset + numpy.prod(strides) > dim: + if dim is not None and offset_ + last >= dim: raise ValueError("Offset {0} and indices {1} exceed dimension {2}".format(offset, idxs, dim)) + dim2idxs_.append((offset_, tuple(idxs_))) + self.children = (variable,) - self.dim2idxs = dim2idxs - self.free_indices = unique(indices) + self.dim2idxs = tuple(dim2idxs_) + self.free_indices = unique(free_indices) class ComponentTensor(Node): @@ -711,6 +713,18 @@ def partial_indexed(tensor, indices): raise ValueError("More indices than rank!") +def strides_of(shape): + """Calculate cumulative strides from per-dimension capacities. + + For example: + + [2, 3, 4] ==> [12, 4, 1] + + """ + temp = numpy.flipud(numpy.cumprod(numpy.flipud(list(shape)[1:]))) + return list(temp) + [1] + + def reshape(variable, *shapes): """Reshape a variable (splitting indices only). @@ -719,16 +733,44 @@ def reshape(variable, *shapes): """ dim2idxs = [] indices = [] - for shape in shapes: + for shape, dim in zip(shapes, variable.shape): + if dim is not None and numpy.prod(shape) != dim: + raise ValueError("Shape {} does not match extent {}.".format(shape, dim)) + + strides = strides_of(shape) idxs = [] - for e in shape: - i = Index(extent=e) - idxs.append((i, e)) - indices.append(i) + for extent, stride in zip(shape, strides): + index = Index(extent=extent) + idxs.append((index, stride)) + indices.append(index) dim2idxs.append((0, tuple(idxs))) expr = FlexiblyIndexed(variable, tuple(dim2idxs)) return ComponentTensor(expr, tuple(indices)) +def view(variable, *slices): + """View a part of a variable. + + :arg variable: a :py:class:`Variable` + :arg slices: one slice object for each dimension of the variable. + """ + dim2idxs = [] + indices = [] + for s, dim in zip(slices, variable.shape): + start = s.start or 0 + stop = s.stop or dim + if stop is None: + raise ValueError("Unknown extent!") + if dim is not None and stop > dim: + raise ValueError("Slice exceeds dimension extent!") + step = s.step or 1 + extent = 1 + (stop - start - 1) // step + index = Index(extent=extent) + dim2idxs.append((s.start, ((index, step),))) + indices.append(index) + expr = FlexiblyIndexed(variable, tuple(dim2idxs)) + return ComponentTensor(expr, tuple(indices)) + + # Static one object for quicker constant folding one = Literal(1) From a9f4ab402dc8f6796d636391123a830cc4ad9ab2 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 8 Dec 2016 14:42:23 +0000 Subject: [PATCH 324/749] generalised gem.view --- gem/gem.py | 55 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 0d28967fe..a84c65a38 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -748,27 +748,50 @@ def reshape(variable, *shapes): return ComponentTensor(expr, tuple(indices)) -def view(variable, *slices): +def view(var, *slices): """View a part of a variable. - :arg variable: a :py:class:`Variable` + :arg var: a :py:class:`Variable` :arg slices: one slice object for each dimension of the variable. """ - dim2idxs = [] + if isinstance(var, Variable): + variable = var + indexes = tuple(Index(extent=extent) for extent in variable.shape) + dim2idxs = tuple((0, ((index, 1),)) for index in indexes) + slice_of = dict(zip(indexes, slices)) + elif isinstance(var, ComponentTensor) and isinstance(var.children[0], FlexiblyIndexed): + variable = var.children[0].children[0] + dim2idxs = var.children[0].dim2idxs + slice_of = dict(zip(var.multiindex, slices)) + else: + raise ValueError("Cannot view {} objects.".format(type(var).__name__)) + + dim2idxs_ = [] indices = [] - for s, dim in zip(slices, variable.shape): - start = s.start or 0 - stop = s.stop or dim - if stop is None: - raise ValueError("Unknown extent!") - if dim is not None and stop > dim: - raise ValueError("Slice exceeds dimension extent!") - step = s.step or 1 - extent = 1 + (stop - start - 1) // step - index = Index(extent=extent) - dim2idxs.append((s.start, ((index, step),))) - indices.append(index) - expr = FlexiblyIndexed(variable, tuple(dim2idxs)) + for offset, idxs in dim2idxs: + offset_ = offset + idxs_ = [] + for idx in idxs: + index, stride = idx + assert isinstance(index, Index) + assert index.extent is not None + dim = index.extent + s = slice_of[index] + start = s.start or 0 + stop = s.stop or dim + if stop is None: + raise ValueError("Unknown extent!") + if dim is not None and stop > dim: + raise ValueError("Slice exceeds dimension extent!") + step = s.step or 1 + offset_ += start * stride + extent = 1 + (stop - start - 1) // step + index_ = Index(extent=extent) + indices.append(index_) + idxs_.append((index_, step * stride)) + dim2idxs_.append((offset_, tuple(idxs_))) + + expr = FlexiblyIndexed(variable, tuple(dim2idxs_)) return ComponentTensor(expr, tuple(indices)) From 013213fee950fb2b77c2114090b13bedcf943062 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 8 Dec 2016 14:58:12 +0000 Subject: [PATCH 325/749] generalise gem.reshape --- gem/gem.py | 53 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index a84c65a38..31fdf9128 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -32,9 +32,9 @@ 'Variable', 'Sum', 'Product', 'Division', 'Power', 'MathFunction', 'MinValue', 'MaxValue', 'Comparison', 'LogicalNot', 'LogicalAnd', 'LogicalOr', 'Conditional', - 'Index', 'VariableIndex', 'Indexed', 'FlexiblyIndexed', - 'ComponentTensor', 'IndexSum', 'ListTensor', 'Delta', - 'index_sum', 'partial_indexed', 'reshape', 'view'] + 'Index', 'VariableIndex', 'Indexed', 'ComponentTensor', + 'IndexSum', 'ListTensor', 'Delta', 'index_sum', + 'partial_indexed', 'reshape', 'view'] class NodeMeta(type): @@ -725,26 +725,43 @@ def strides_of(shape): return list(temp) + [1] -def reshape(variable, *shapes): +def reshape(var, *shapes): """Reshape a variable (splitting indices only). - :arg variable: a :py:class:`Variable` + :arg var: a :py:class:`Variable` :arg shapes: one shape tuple for each dimension of the variable. """ - dim2idxs = [] + if isinstance(var, Variable): + variable = var + indexes = tuple(Index(extent=extent) for extent in variable.shape) + dim2idxs = tuple((0, ((index, 1),)) for index in indexes) + shape_of = dict(zip(indexes, shapes)) + elif isinstance(var, ComponentTensor) and isinstance(var.children[0], FlexiblyIndexed): + variable = var.children[0].children[0] + dim2idxs = var.children[0].dim2idxs + shape_of = dict(zip(var.multiindex, shapes)) + else: + raise ValueError("Cannot view {} objects.".format(type(var).__name__)) + + dim2idxs_ = [] indices = [] - for shape, dim in zip(shapes, variable.shape): - if dim is not None and numpy.prod(shape) != dim: - raise ValueError("Shape {} does not match extent {}.".format(shape, dim)) - - strides = strides_of(shape) - idxs = [] - for extent, stride in zip(shape, strides): - index = Index(extent=extent) - idxs.append((index, stride)) - indices.append(index) - dim2idxs.append((0, tuple(idxs))) - expr = FlexiblyIndexed(variable, tuple(dim2idxs)) + for offset, idxs in dim2idxs: + idxs_ = [] + for idx in idxs: + index, stride = idx + assert isinstance(index, Index) + dim = index.extent + shape = shape_of[index] + if dim is not None and numpy.prod(shape) != dim: + raise ValueError("Shape {} does not match extent {}.".format(shape, dim)) + strides = strides_of(shape) + for extent, stride_ in zip(shape, strides): + index = Index(extent=extent) + idxs_.append((index, stride_ * stride)) + indices.append(index) + dim2idxs_.append((offset, tuple(idxs_))) + + expr = FlexiblyIndexed(variable, tuple(dim2idxs_)) return ComponentTensor(expr, tuple(indices)) From 0a2755481f2206a64d982337176f1101779da28d Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 8 Dec 2016 16:32:05 +0000 Subject: [PATCH 326/749] fix assertions --- gem/gem.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gem/gem.py b/gem/gem.py index 31fdf9128..2f60d5cf8 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -735,10 +735,12 @@ def reshape(var, *shapes): variable = var indexes = tuple(Index(extent=extent) for extent in variable.shape) dim2idxs = tuple((0, ((index, 1),)) for index in indexes) + assert len(indexes) == len(shapes) shape_of = dict(zip(indexes, shapes)) elif isinstance(var, ComponentTensor) and isinstance(var.children[0], FlexiblyIndexed): variable = var.children[0].children[0] dim2idxs = var.children[0].dim2idxs + assert len(var.multiindex) == len(shapes) shape_of = dict(zip(var.multiindex, shapes)) else: raise ValueError("Cannot view {} objects.".format(type(var).__name__)) @@ -775,10 +777,12 @@ def view(var, *slices): variable = var indexes = tuple(Index(extent=extent) for extent in variable.shape) dim2idxs = tuple((0, ((index, 1),)) for index in indexes) + assert len(indexes) == len(slices) slice_of = dict(zip(indexes, slices)) elif isinstance(var, ComponentTensor) and isinstance(var.children[0], FlexiblyIndexed): variable = var.children[0].children[0] dim2idxs = var.children[0].dim2idxs + assert len(var.multiindex) == len(slices) slice_of = dict(zip(var.multiindex, slices)) else: raise ValueError("Cannot view {} objects.".format(type(var).__name__)) @@ -791,7 +795,6 @@ def view(var, *slices): for idx in idxs: index, stride = idx assert isinstance(index, Index) - assert index.extent is not None dim = index.extent s = slice_of[index] start = s.start or 0 From d23fe6e4617f43671bdfee4546ce2fa7895fbea0 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 9 Dec 2016 15:50:17 +0000 Subject: [PATCH 327/749] factor out common code of gem.view and gem.reshape --- gem/gem.py | 56 +++++++++++++++++++++++++----------------------------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 2f60d5cf8..7dddb830d 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -725,25 +725,31 @@ def strides_of(shape): return list(temp) + [1] -def reshape(var, *shapes): +def decompose_variable_view(expression): + """Extract ComponentTensor + FlexiblyIndexed view onto a variable.""" + if isinstance(expression, Variable): + variable = expression + indexes = tuple(Index(extent=extent) for extent in expression.shape) + dim2idxs = tuple((0, ((index, 1),)) for index in indexes) + elif isinstance(expression, ComponentTensor) and isinstance(expression.children[0], FlexiblyIndexed): + variable = expression.children[0].children[0] + indexes = expression.multiindex + dim2idxs = expression.children[0].dim2idxs + else: + raise ValueError("Cannot handle {} objects.".format(type(expression).__name__)) + + return variable, dim2idxs, indexes + + +def reshape(expression, *shapes): """Reshape a variable (splitting indices only). - :arg var: a :py:class:`Variable` + :arg expression: view of a :py:class:`Variable` :arg shapes: one shape tuple for each dimension of the variable. """ - if isinstance(var, Variable): - variable = var - indexes = tuple(Index(extent=extent) for extent in variable.shape) - dim2idxs = tuple((0, ((index, 1),)) for index in indexes) - assert len(indexes) == len(shapes) - shape_of = dict(zip(indexes, shapes)) - elif isinstance(var, ComponentTensor) and isinstance(var.children[0], FlexiblyIndexed): - variable = var.children[0].children[0] - dim2idxs = var.children[0].dim2idxs - assert len(var.multiindex) == len(shapes) - shape_of = dict(zip(var.multiindex, shapes)) - else: - raise ValueError("Cannot view {} objects.".format(type(var).__name__)) + variable, dim2idxs, indexes = decompose_variable_view(expression) + assert len(indexes) == len(shapes) + shape_of = dict(zip(indexes, shapes)) dim2idxs_ = [] indices = [] @@ -767,25 +773,15 @@ def reshape(var, *shapes): return ComponentTensor(expr, tuple(indices)) -def view(var, *slices): +def view(expression, *slices): """View a part of a variable. - :arg var: a :py:class:`Variable` + :arg expression: view of a :py:class:`Variable` :arg slices: one slice object for each dimension of the variable. """ - if isinstance(var, Variable): - variable = var - indexes = tuple(Index(extent=extent) for extent in variable.shape) - dim2idxs = tuple((0, ((index, 1),)) for index in indexes) - assert len(indexes) == len(slices) - slice_of = dict(zip(indexes, slices)) - elif isinstance(var, ComponentTensor) and isinstance(var.children[0], FlexiblyIndexed): - variable = var.children[0].children[0] - dim2idxs = var.children[0].dim2idxs - assert len(var.multiindex) == len(slices) - slice_of = dict(zip(var.multiindex, slices)) - else: - raise ValueError("Cannot view {} objects.".format(type(var).__name__)) + variable, dim2idxs, indexes = decompose_variable_view(expression) + assert len(indexes) == len(slices) + slice_of = dict(zip(indexes, slices)) dim2idxs_ = [] indices = [] From c4cad3ea9aea1ff19c974bfa892b0dd7d4c7e0b9 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 12 Dec 2016 12:36:11 +0000 Subject: [PATCH 328/749] add error messages --- gem/optimise.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index 08548f89a..14f9c1f12 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -165,7 +165,7 @@ def child(expression): if len(types) == 1: cls, = types if cls.__front__ or cls.__back__: - raise NotImplementedError + raise NotImplementedError("How to factorise {} expressions?".format(cls.__name__)) assert all(len(e.children) == len(expr.children) for e in expressions) assert len(expr.children) > 0 @@ -173,7 +173,7 @@ def child(expression): for nth_children in zip(*[e.children for e in expressions])]) - raise NotImplementedError + raise NotImplementedError("No rule for factorising expressions of this kind.") def select_expression(expressions, index): From 2708ee1289971e979c764846c322ab8ab0f8beac Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 12 Dec 2016 12:36:23 +0000 Subject: [PATCH 329/749] fix typo --- gem/optimise.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gem/optimise.py b/gem/optimise.py index 14f9c1f12..39aaca6ff 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -231,7 +231,7 @@ def delta_elimination(sum_indices, factors): def sum_factorise(sum_indices, factors): - """Optimise a tensor product throw sum factorisation. + """Optimise a tensor product through sum factorisation. :arg sum_indices: free indices for contractions :arg factors: product factors From cf186b501ad607532515abf871792d92e996ea5e Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 13 Dec 2016 10:23:49 +0000 Subject: [PATCH 330/749] minor simplication to impero_utils.py Remove TODO.md --- gem/impero_utils.py | 47 +++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/gem/impero_utils.py b/gem/impero_utils.py index 72be93d9b..4f58bac14 100644 --- a/gem/impero_utils.py +++ b/gem/impero_utils.py @@ -92,10 +92,10 @@ def compile_gem(return_variables, expressions, prefix_ordering, remove_zeros=Fal tree = make_loop_tree(ops, get_indices) # Collect temporaries - temporaries = collect_temporaries(ops) + temporaries = collect_temporaries(tree) # Determine declarations - declare, indices = place_declarations(ops, tree, temporaries, get_indices) + declare, indices = place_declarations(tree, temporaries, get_indices) # Prepare ImperoC (Impero AST + other data for code generation) return ImperoC(tree, temporaries, declare, indices) @@ -147,18 +147,18 @@ def inline_temporaries(expressions, ops): return [op for op in ops if not (isinstance(op, imp.Evaluate) and op.expression in candidates)] -def collect_temporaries(ops): +def collect_temporaries(tree): """Collects GEM expressions to assign to temporaries from a list of Impero terminals.""" result = [] - for op in ops: + for node in traversal((tree,)): # IndexSum temporaries should be added either at Initialise or # at Accumulate. The difference is only in ordering # (numbering). We chose Accumulate here. - if isinstance(op, imp.Accumulate): - result.append(op.indexsum) - elif isinstance(op, imp.Evaluate): - result.append(op.expression) + if isinstance(node, imp.Accumulate): + result.append(node.indexsum) + elif isinstance(node, imp.Evaluate): + result.append(node.expression) return result @@ -185,10 +185,9 @@ def make_loop_tree(ops, get_indices, level=0): return imp.Block(statements) -def place_declarations(ops, tree, temporaries, get_indices): +def place_declarations(tree, temporaries, get_indices): """Determines where and how to declare temporaries for an Impero AST. - :arg ops: terminals of ``tree`` :arg tree: Impero AST to determine the declarations for :arg temporaries: list of GEM expressions which are assigned to temporaries @@ -200,8 +199,9 @@ def place_declarations(ops, tree, temporaries, get_indices): # Collect the total number of temporary references total_refcount = collections.Counter() - for op in ops: - total_refcount.update(temp_refcount(temporaries_set, op)) + for node in traversal((tree,)): + if isinstance(node, imp.Terminal): + total_refcount.update(temp_refcount(temporaries_set, node)) assert temporaries_set == set(total_refcount) # Result @@ -264,17 +264,18 @@ def recurse_block(expr, loop_indices): # Set in ``declare`` for Impero terminals whether they should # declare the temporary that they are writing to. - for op in ops: - declare[op] = False - if isinstance(op, imp.Evaluate): - e = op.expression - elif isinstance(op, imp.Initialise): - e = op.indexsum - else: - continue - - if len(indices[e]) == 0: - declare[op] = True + for node in traversal((tree,)): + if isinstance(node, imp.Terminal): + declare[node] = False + if isinstance(node, imp.Evaluate): + e = node.expression + elif isinstance(node, imp.Initialise): + e = node.indexsum + else: + continue + + if len(indices[e]) == 0: + declare[node] = True return declare, indices From 3fd6ae515e2e5e5ff1f25c785eb12222630bda5b Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 13 Dec 2016 11:30:35 +0000 Subject: [PATCH 331/749] fix node ordering and temporary numbering --- gem/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gem/node.py b/gem/node.py index a5ed3a699..5adce50fe 100644 --- a/gem/node.py +++ b/gem/node.py @@ -113,7 +113,7 @@ def traversal(expression_dags): while lifo: node = lifo.pop() yield node - for child in node.children: + for child in reversed(node.children): if child not in seen: seen.add(child) lifo.append(child) From 6e3273e1c3ded50b19e921a1357f8b543c364e20 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 12 Jan 2017 15:33:51 +0000 Subject: [PATCH 332/749] fix spectral element wrappers --- finat/__init__.py | 2 +- finat/fiat_elements.py | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/finat/__init__.py b/finat/__init__.py index b9377f20e..eb8a1a518 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -1,6 +1,6 @@ from __future__ import absolute_import, print_function, division -from .fiat_elements import Lagrange, DiscontinuousLagrange, GaussLobatto # noqa: F401 +from .fiat_elements import Lagrange, DiscontinuousLagrange, GaussLobattoLegendre, GaussLegendre # noqa: F401 from .fiat_elements import RaviartThomas, DiscontinuousRaviartThomas # noqa: F401 from .fiat_elements import BrezziDouglasMarini, BrezziDouglasFortinMarini # noqa: F401 from .fiat_elements import Nedelec, NedelecSecondKind, Regge # noqa: F401 diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 4234f0dcc..01df4ddeb 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -87,6 +87,11 @@ def basis_evaluation(self, order, ps, entity=None): return result +class Regge(FiatElementBase): # naturally tensor valued + def __init__(self, cell, degree): + super(Regge, self).__init__(FIAT.Regge(cell, degree)) + + class ScalarFiatElement(FiatElementBase): @property def value_shape(self): @@ -98,19 +103,19 @@ def __init__(self, cell, degree): super(Lagrange, self).__init__(FIAT.Lagrange(cell, degree)) -class Regge(ScalarFiatElement): +class GaussLobattoLegendre(ScalarFiatElement): def __init__(self, cell, degree): - super(Regge, self).__init__(FIAT.Regge(cell, degree)) + super(GaussLobattoLegendre, self).__init__(FIAT.GaussLobattoLegendre(cell, degree)) -class GaussLobatto(ScalarFiatElement): +class DiscontinuousLagrange(ScalarFiatElement): def __init__(self, cell, degree): - super(GaussLobatto, self).__init__(FIAT.GaussLobatto(cell, degree)) + super(DiscontinuousLagrange, self).__init__(FIAT.DiscontinuousLagrange(cell, degree)) -class DiscontinuousLagrange(ScalarFiatElement): +class GaussLegendre(ScalarFiatElement): def __init__(self, cell, degree): - super(DiscontinuousLagrange, self).__init__(FIAT.DiscontinuousLagrange(cell, degree)) + super(GaussLegendre, self).__init__(FIAT.GaussLegendre(cell, degree)) class VectorFiatElement(FiatElementBase): From 641a586b1b51fba641722484401eb6a57b21dc85 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 27 Jan 2017 10:24:25 +0000 Subject: [PATCH 333/749] fix O(n^2) performance bug in the number of temporaries --- gem/impero_utils.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/gem/impero_utils.py b/gem/impero_utils.py index 4f58bac14..db7a9565c 100644 --- a/gem/impero_utils.py +++ b/gem/impero_utils.py @@ -194,15 +194,15 @@ def place_declarations(tree, temporaries, get_indices): :arg get_indices: callable mapping from GEM nodes to an ordering of free indices """ - temporaries_set = set(temporaries) - assert len(temporaries_set) == len(temporaries) + numbering = {t: n for n, t in enumerate(temporaries)} + assert len(numbering) == len(temporaries) # Collect the total number of temporary references total_refcount = collections.Counter() for node in traversal((tree,)): if isinstance(node, imp.Terminal): - total_refcount.update(temp_refcount(temporaries_set, node)) - assert temporaries_set == set(total_refcount) + total_refcount.update(temp_refcount(numbering, node)) + assert set(total_refcount) == set(temporaries) # Result declare = {} @@ -223,7 +223,7 @@ def recurse(expr, loop_indices): @recurse.register(imp.Terminal) def recurse_terminal(expr, loop_indices): - return temp_refcount(temporaries_set, expr) + return temp_refcount(numbering, expr) @recurse.register(imp.For) def recurse_for(expr, loop_indices): @@ -241,7 +241,7 @@ def recurse_block(expr, loop_indices): refcount.update(recurse(statement, loop_indices)) # Visit :class:`collections.Counter` in deterministic order - for e in sorted(refcount.keys(), key=temporaries.index): + for e in sorted(refcount.keys(), key=lambda t: numbering[t]): if refcount[e] == total_refcount[e]: # If all references are within this block, then this # block is the right place to declare the temporary. From 665bae7d79d6be647ca51b828290b38798cc70ac Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Wed, 1 Feb 2017 12:17:21 +0000 Subject: [PATCH 334/749] gem: Simplification in Conditional --- gem/gem.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/gem/gem.py b/gem/gem.py index 7dddb830d..c10981ffa 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -361,12 +361,19 @@ def __init__(self, a, b): class Conditional(Node): __slots__ = ('children', 'shape') - def __init__(self, condition, then, else_): + def __new__(cls, condition, then, else_): assert not condition.shape assert then.shape == else_.shape + # If both branches are the same, just return one of them. In + # particular, this will help constant-fold zeros. + if then == else_: + return then + + self = super(Conditional, cls).__new__(cls) self.children = condition, then, else_ self.shape = then.shape + return self class IndexBase(with_metaclass(ABCMeta)): From 32c22df0ee6b596021b3e3a0edd76328d35a692c Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 15 Dec 2016 09:45:31 +0000 Subject: [PATCH 335/749] enable pickling of GEM expressions --- gem/gem.py | 13 ++++++++++++- gem/node.py | 22 +++++++++++++--------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index c10981ffa..fb20ed5ef 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -59,7 +59,7 @@ def __call__(self, *args, **kwargs): class Node(with_metaclass(NodeMeta, NodeBase)): """Abstract GEM node class.""" - __slots__ = ('free_indices') + __slots__ = ('free_indices',) def is_equal(self, other): """Common subexpression eliminating equality predicate. @@ -419,11 +419,19 @@ def __lt__(self, other): # Allow sorting of free indices in Python 3 return id(self) < id(other) + def __getstate__(self): + return self.name, self.extent, self.count + + def __setstate__(self, state): + self.name, self.extent, self.count = state + class VariableIndex(IndexBase): """An index that is constant during a single execution of the kernel, but whose value is not known at compile time.""" + __slots__ = ('expression',) + def __init__(self, expression): assert isinstance(expression, Node) assert not expression.free_indices @@ -443,6 +451,9 @@ def __ne__(self, other): def __hash__(self): return hash((VariableIndex, self.expression)) + def __reduce__(self): + return VariableIndex, (self.expression,) + class Indexed(Scalar): __slots__ = ('children', 'multiindex') diff --git a/gem/node.py b/gem/node.py index 5adce50fe..6738381bd 100644 --- a/gem/node.py +++ b/gem/node.py @@ -2,7 +2,7 @@ expression DAG languages.""" from __future__ import absolute_import, print_function, division -from six.moves import map +from six.moves import map, zip import collections @@ -32,7 +32,7 @@ class Node(object): # To be (potentially) overridden by derived node classes. __back__ = () - def __getinitargs__(self, children): + def _cons_args(self, children): """Constructs an argument list for the constructor with non-child data from 'self' and children from 'children'. @@ -43,17 +43,21 @@ def __getinitargs__(self, children): return tuple(front_args) + tuple(children) + tuple(back_args) + def __reduce__(self): + # Gold version: + return type(self), self._cons_args(self.children) + def reconstruct(self, *args): """Reconstructs the node with new children from 'args'. Non-child data are copied from 'self'. Returns a new object. """ - return type(self)(*self.__getinitargs__(args)) + return type(self)(*self._cons_args(args)) def __repr__(self): - init_args = self.__getinitargs__(self.children) - return "%s(%s)" % (type(self).__name__, ", ".join(map(repr, init_args))) + cons_args = self._cons_args(self.children) + return "%s(%s)" % (type(self).__name__, ", ".join(map(repr, cons_args))) def __eq__(self, other): """Provides equality testing with quick positive and negative @@ -85,9 +89,9 @@ def is_equal(self, other): """ if type(self) != type(other): return False - self_initargs = self.__getinitargs__(self.children) - other_initargs = other.__getinitargs__(other.children) - return self_initargs == other_initargs + self_consargs = self._cons_args(self.children) + other_consargs = other._cons_args(other.children) + return self_consargs == other_consargs def get_hash(self): """Hash function. @@ -95,7 +99,7 @@ def get_hash(self): This is the method to potentially override in derived classes, not :meth:`__hash__`. """ - return hash((type(self),) + self.__getinitargs__(self.children)) + return hash((type(self),) + self._cons_args(self.children)) def traversal(expression_dags): From a21e7897366d542efc793925cc8ee0f34fda486c Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 15 Dec 2016 10:21:57 +0000 Subject: [PATCH 336/749] add pickling test for "all" protocol versions --- gem/gem.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gem/gem.py b/gem/gem.py index fb20ed5ef..29c5a6eb2 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -451,6 +451,12 @@ def __ne__(self, other): def __hash__(self): return hash((VariableIndex, self.expression)) + def __str__(self): + return str(self.expression) + + def __repr__(self): + return repr(self.expression) + def __reduce__(self): return VariableIndex, (self.expression,) From 710b3d3f678ebbc2d5a1aeef8e0784faa3cedc82 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 3 Feb 2017 14:39:06 +0000 Subject: [PATCH 337/749] make VariableIndex.__repr__ less confusing --- gem/gem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gem/gem.py b/gem/gem.py index 29c5a6eb2..5f0a4ac6e 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -455,7 +455,7 @@ def __str__(self): return str(self.expression) def __repr__(self): - return repr(self.expression) + return "VariableIndex(%r)" % (self.expression,) def __reduce__(self): return VariableIndex, (self.expression,) From 5f2651ef0eb6c2ead153a9b7f8208bef3b3cf162 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 10 Feb 2017 15:53:12 +0000 Subject: [PATCH 338/749] add node allocation API --- finat/fiat_elements.py | 9 +++++++++ finat/finiteelementbase.py | 28 +++++++++++++++++++++++++++- finat/quadrilateral.py | 19 +++++++++++++++++++ finat/tensor_product.py | 30 +++++++++++++++++++++++++++++- finat/tensorfiniteelement.py | 8 ++++++++ 5 files changed, 92 insertions(+), 2 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 01df4ddeb..0ceaa5ece 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -23,6 +23,15 @@ def degree(self): # Requires FIAT.CiarletElement return self._element.degree() + def entity_dofs(self): + return self._element.entity_dofs() + + def entity_closure_dofs(self): + return self._element.entity_closure_dofs() + + def space_dimension(self): + return self._element.space_dimension() + @property def index_shape(self): return (self._element.space_dimension(),) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 6b61a332c..3a32b515f 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -1,9 +1,11 @@ from __future__ import absolute_import, print_function, division -from six import with_metaclass +from six import with_metaclass, iteritems from abc import ABCMeta, abstractproperty, abstractmethod +from itertools import chain import gem +from gem.utils import cached_property class FiniteElementBase(with_metaclass(ABCMeta)): @@ -19,6 +21,30 @@ def degree(self): In the tensor case this is a tuple. ''' + @abstractmethod + def entity_dofs(self): + '''Return the map of topological entities to degrees of + freedom for the finite element.''' + + @cached_property + def _entity_closure_dofs(self): + # Compute the nodes on the closure of each sub_entity. + entity_dofs = self.entity_dofs() + return {dim: {e: list(chain(*[entity_dofs[d][se] + for d, se in sub_entities])) + for e, sub_entities in iteritems(entities)} + for dim, entities in iteritems(self.cell.sub_entities)} + + def entity_closure_dofs(self): + '''Return the map of topological entities to degrees of + freedom on the closure of those entities for the finite + element.''' + return self._entity_closure_dofs + + @abstractmethod + def space_dimension(self): + '''Return the dimension of the finite element space.''' + @abstractproperty def index_shape(self): '''A tuple indicating the number of degrees of freedom in the diff --git a/finat/quadrilateral.py b/finat/quadrilateral.py index c0a582056..5a8863e38 100644 --- a/finat/quadrilateral.py +++ b/finat/quadrilateral.py @@ -1,4 +1,5 @@ from __future__ import absolute_import, print_function, division +from six import iteritems from FIAT.reference_element import FiredrakeQuadrilateral @@ -24,6 +25,24 @@ def cell(self): def degree(self): raise NotImplementedError("Unused property.") + @cached_property + def _entity_dofs(self): + entity_dofs = self.product.entity_dofs() + flat_entity_dofs = {} + flat_entity_dofs[0] = entity_dofs[(0, 0)] + flat_entity_dofs[1] = dict(enumerate( + [v for k, v in sorted(iteritems(entity_dofs[(0, 1)]))] + + [v for k, v in sorted(iteritems(entity_dofs[(1, 0)]))] + )) + flat_entity_dofs[2] = entity_dofs[(1, 1)] + return flat_entity_dofs + + def entity_dofs(self): + return self._entity_dofs + + def space_dimension(self): + return self.product.space_dimension() + def basis_evaluation(self, order, ps, entity=None): """Return code for evaluating the element at known points on the reference element. diff --git a/finat/tensor_product.py b/finat/tensor_product.py index 08d24305e..9d75b3f2d 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -2,7 +2,7 @@ from six.moves import range, zip from functools import reduce -from itertools import chain +from itertools import chain, product import numpy @@ -31,6 +31,34 @@ def cell(self): def degree(self): raise NotImplementedError("Unused property.") + @cached_property + def _entity_dofs(self): + shape = tuple(fe.space_dimension() for fe in self.factors) + entity_dofs = {} + for dim in product(*[fe.cell.get_topology().keys() + for fe in self.factors]): + entity_dofs[dim] = {} + topds = [fe.entity_dofs()[d] + for fe, d in zip(self.factors, dim)] + for tuple_ei in product(*[sorted(topd) for topd in topds]): + tuple_vs = list(product(*[topd[ei] + for topd, ei in zip(topds, tuple_ei)])) + if tuple_vs: + vs = list(numpy.ravel_multi_index(numpy.transpose(tuple_vs), shape)) + else: + vs = [] + entity_dofs[dim][tuple_ei] = vs + # flatten entity numbers + entity_dofs[dim] = dict(enumerate(entity_dofs[dim][key] + for key in sorted(entity_dofs[dim]))) + return entity_dofs + + def entity_dofs(self): + return self._entity_dofs + + def space_dimension(self): + return numpy.prod([fe.space_dimension() for fe in self.factors]) + @property def index_shape(self): return tuple(chain(*[fe.index_shape for fe in self.factors])) diff --git a/finat/tensorfiniteelement.py b/finat/tensorfiniteelement.py index e86631772..4d0fb9233 100644 --- a/finat/tensorfiniteelement.py +++ b/finat/tensorfiniteelement.py @@ -3,6 +3,8 @@ from functools import reduce +import numpy + import gem from finat.finiteelementbase import FiniteElementBase @@ -53,6 +55,12 @@ def cell(self): def degree(self): return self._base_element.degree + def entity_dofs(self): + raise NotImplementedError("No one uses this!") + + def space_dimension(self): + return numpy.prod((self._base_element.space_dimension(),) + self._shape) + @property def index_shape(self): return self._base_element.index_shape + self._shape From 662db375519e62255e613ce5f7bbfc8ede70750c Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 10 Feb 2017 17:20:21 +0000 Subject: [PATCH 339/749] add entity_support_dofs --- finat/finiteelementbase.py | 48 +++++++++++++++++++++++++++++++++++++- finat/quadrilateral.py | 3 ++- finat/tensor_product.py | 2 +- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 3a32b515f..ee1745968 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -1,12 +1,17 @@ from __future__ import absolute_import, print_function, division -from six import with_metaclass, iteritems +from six import with_metaclass, iteritems, itervalues from abc import ABCMeta, abstractproperty, abstractmethod from itertools import chain +import numpy + import gem +from gem.optimise import aggressive_unroll from gem.utils import cached_property +from finat.quadrature import make_quadrature + class FiniteElementBase(with_metaclass(ABCMeta)): @@ -77,3 +82,44 @@ def basis_evaluation(self, order, ps, entity=None): :param ps: the point set object. :param entity: the cell entity on which to tabulate. ''' + + +def entity_support_dofs(elem, entity_dim): + """Return the map of entity id to the degrees of freedom for which + the corresponding basis functions take non-zero values. + + :arg elem: FInAT finite element + :arg entity_dim: Dimension of the cell subentity. + """ + if not hasattr(elem, "_entity_support_dofs"): + elem._entity_support_dofs = {} + cache = elem._entity_support_dofs + try: + return cache[entity_dim] + except KeyError: + pass + + beta = elem.get_indices() + zeta = elem.get_value_indices() + + entity_cell = elem.cell.construct_subelement(entity_dim) + quad = make_quadrature(entity_cell, (2*numpy.array(elem.degree)).tolist()) + + eps = 1.e-8 # Is this a safe value? + + result = {} + for f in elem.entity_dofs()[entity_dim].keys(): + # Tabulate basis functions on the facet + vals, = itervalues(elem.basis_evaluation(0, quad.point_set, entity=(entity_dim, f))) + # Integrate the square of the basis functions on the facet. + ints = gem.IndexSum( + gem.Product(gem.IndexSum(gem.Product(gem.Indexed(vals, beta + zeta), + gem.Indexed(vals, beta + zeta)), zeta), + quad.weight_expression), + quad.point_set.indices + ) + ints = aggressive_unroll(gem.ComponentTensor(ints, beta)).array.flatten() + result[f] = [dof for dof, i in enumerate(ints) if i > eps] + + cache[entity_dim] = result + return result diff --git a/finat/quadrilateral.py b/finat/quadrilateral.py index 5a8863e38..9044e3697 100644 --- a/finat/quadrilateral.py +++ b/finat/quadrilateral.py @@ -23,7 +23,8 @@ def cell(self): @property def degree(self): - raise NotImplementedError("Unused property.") + unique_degree, = set(self.product.degree) + return unique_degree @cached_property def _entity_dofs(self): diff --git a/finat/tensor_product.py b/finat/tensor_product.py index 9d75b3f2d..3c1b8fca3 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -29,7 +29,7 @@ def cell(self): @property def degree(self): - raise NotImplementedError("Unused property.") + return tuple(fe.degree for fe in self.factors) @cached_property def _entity_dofs(self): From 165cf1d321fc05b7518fd7272318f15a617504dd Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 13 Feb 2017 11:26:20 +0000 Subject: [PATCH 340/749] fewer mutations --- finat/tensor_product.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/finat/tensor_product.py b/finat/tensor_product.py index 3c1b8fca3..f6613fa18 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -37,7 +37,7 @@ def _entity_dofs(self): entity_dofs = {} for dim in product(*[fe.cell.get_topology().keys() for fe in self.factors]): - entity_dofs[dim] = {} + dim_dofs = [] topds = [fe.entity_dofs()[d] for fe, d in zip(self.factors, dim)] for tuple_ei in product(*[sorted(topd) for topd in topds]): @@ -45,12 +45,11 @@ def _entity_dofs(self): for topd, ei in zip(topds, tuple_ei)])) if tuple_vs: vs = list(numpy.ravel_multi_index(numpy.transpose(tuple_vs), shape)) + dim_dofs.append((tuple_ei, vs)) else: - vs = [] - entity_dofs[dim][tuple_ei] = vs + dim_dofs.append((tuple_ei, [])) # flatten entity numbers - entity_dofs[dim] = dict(enumerate(entity_dofs[dim][key] - for key in sorted(entity_dofs[dim]))) + entity_dofs[dim] = dict(enumerate(v for k, v in sorted(dim_dofs))) return entity_dofs def entity_dofs(self): From d40c71d77b9b67b4abc44568f601df512b70db3d Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 9 Mar 2017 15:40:21 +0000 Subject: [PATCH 341/749] Generalise IndexSum unrolling --- gem/optimise.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index 39aaca6ff..cc39ae022 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -3,7 +3,7 @@ from __future__ import absolute_import, print_function, division from six import itervalues -from six.moves import map, zip +from six.moves import filter, map, zip from collections import OrderedDict, deque from functools import reduce @@ -372,8 +372,7 @@ def _unroll_indexsum(node, self): @_unroll_indexsum.register(IndexSum) # noqa def _(node, self): - unroll = tuple(index for index in node.multiindex - if index.extent <= self.max_extent) + unroll = tuple(filter(self.predicate, node.multiindex)) if unroll: # Unrolling summand = self(node.children[0]) @@ -388,15 +387,16 @@ def _(node, self): return reuse_if_untouched(node, self) -def unroll_indexsum(expressions, max_extent): +def unroll_indexsum(expressions, predicate): """Unrolls IndexSums below a specified extent. :arg expressions: list of expression DAGs - :arg max_extent: maximum extent for which IndexSums are unrolled + :arg predicate: a predicate function on :py:class:`Index` objects + that tells whether to unroll a particular index :returns: list of expression DAGs with some unrolled IndexSums """ mapper = Memoizer(_unroll_indexsum) - mapper.max_extent = max_extent + mapper.predicate = predicate return list(map(mapper, expressions)) @@ -410,6 +410,6 @@ def aggressive_unroll(expression): expression, = remove_componenttensors((ListTensor(tensor),)) # Unroll summation - expression, = unroll_indexsum((expression,), max_extent=numpy.inf) + expression, = unroll_indexsum((expression,), predicate=lambda index: True) expression, = remove_componenttensors((expression,)) return expression From 82b9f7d14ba7b8528a1e82ef58e3ab433601cd90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Homolya?= Date: Thu, 5 Jan 2017 13:40:02 +0100 Subject: [PATCH 342/749] traverse products and sums --- gem/optimise.py | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index cc39ae022..30cf28e35 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -14,8 +14,8 @@ from gem.node import Memoizer, MemoizerArg, reuse_if_untouched, reuse_if_untouched_arg from gem.gem import (Node, Terminal, Failure, Identity, Literal, Zero, - Product, Sum, Comparison, Conditional, Index, - VariableIndex, Indexed, FlexiblyIndexed, + Product, Sum, Comparison, Conditional, Division, + Index, VariableIndex, Indexed, FlexiblyIndexed, IndexSum, ComponentTensor, ListTensor, Delta, partial_indexed, one) @@ -316,6 +316,48 @@ def contraction(expression): return sum_factorise(*delta_elimination(sum_indices, factors)) +def traverse_product(expression, stop_at=None): + sum_indices = [] + terms = [] + + stack = [expression] + while stack: + expr = stack.pop() + if stop_at is not None and stop_at(expr): + terms.append(expr) + elif isinstance(expr, IndexSum): + stack.append(expr.children[0]) + sum_indices.extend(expr.multiindex) + elif isinstance(expr, Product): + stack.extend(reversed(expr.children)) + elif isinstance(expr, Division): + # Break up products in the dividend, but not in divisor. + dividend, divisor = expr.children + if dividend == one: + terms.append(expr) + else: + terms.append(Division(one, divisor)) + terms.append(dividend) + else: + terms.append(expr) + + return sum_indices, terms + + +def traverse_sum(expression, stop_at=None): + stack = [expression] + result = [] + while stack: + expr = stack.pop() + if stop_at is not None and stop_at(expr): + result.append(expr) + elif isinstance(expr, Sum): + stack.extend(reversed(expr.children)) + else: + result.append(expr) + return result + + @singledispatch def _replace_delta(node, self): raise AssertionError("cannot handle type %s" % type(node)) From 229709e021286fca39aa6f456441565907d194a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Homolya?= Date: Thu, 5 Jan 2017 14:10:29 +0100 Subject: [PATCH 343/749] add generic refactorisation code --- gem/optimise.py | 80 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 4 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index 30cf28e35..39dde041c 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -3,11 +3,11 @@ from __future__ import absolute_import, print_function, division from six import itervalues -from six.moves import filter, map, zip +from six.moves import filter, intern, map, zip -from collections import OrderedDict, deque +from collections import OrderedDict, deque, namedtuple from functools import reduce -from itertools import permutations +from itertools import chain, permutations, product import numpy from singledispatch import singledispatch @@ -275,7 +275,7 @@ def sum_factorise(sum_indices, factors): # If some contraction indices were independent, then we may # still have several terms at this point. - expr = reduce(Product, terms) + expr = reduce(Product, terms, one) flops += (len(terms) - 1) * numpy.prod([i.extent for i in expr.free_indices], dtype=int) if flops < best_flops: @@ -358,6 +358,78 @@ def traverse_sum(expression, stop_at=None): return result +ATOMIC = intern('atomic') +COMPOUND = intern('compound') +OTHER = intern('other') + +Monomial = namedtuple('Monomial', ['sum_indices', 'atomics', 'rest']) + + +class FactorisationError(Exception): + """Raised when factorisation fails to achieve some desired form.""" + pass + + +def collect_monomials(expression, classifier): + def stop_at(expr): + return classifier(expr) == OTHER + common_indices, terms = traverse_product(expression, stop_at=stop_at) + + common_atomics = [] + common_others = [] + compounds = [] + for term in terms: + cls = classifier(term) + if cls == ATOMIC: + common_atomics.append(term) + elif cls == COMPOUND: + compounds.append(term) + elif cls == OTHER: + common_others.append(term) + else: + raise ValueError("Classifier returned illegal value.") + + sums = [] + for expr in compounds: + summands = traverse_sum(expr, stop_at=stop_at) + if len(summands) <= 1: + raise FactorisationError(expr) + sums.append(chain.from_iterable(collect_monomials(summand, classifier) + for summand in summands)) + + unfactored = OrderedDict() + for partials in product(*sums): + all_indices = list(common_indices) + atomics = list(common_atomics) + others = list(common_others) + for monomial in partials: + all_indices.extend(monomial.sum_indices) + atomics.extend(monomial.atomics) + others.append(monomial.rest) + atomic_indices = set().union(*[atomic.free_indices + for atomic in atomics]) + sum_indices = tuple(index for index in all_indices + if index in atomic_indices) + rest_indices = tuple(index for index in all_indices + if index not in atomic_indices) + + # Not really sum factorisation, but rather just an optimised + # way of building a product. + rest = sum_factorise(rest_indices, others) + + monomial = Monomial(sum_indices, tuple(atomics), rest) + key = (frozenset(sum_indices), frozenset(atomics)) + unfactored.setdefault(key, []).append(monomial) + + result = [] + for monomials in itervalues(unfactored): + sum_indices = monomials[0].sum_indices + atomics = monomials[0].atomics + rest = reduce(Sum, [m.rest for m in monomials]) + result.append(Monomial(sum_indices, atomics, rest)) + return result + + @singledispatch def _replace_delta(node, self): raise AssertionError("cannot handle type %s" % type(node)) From 93330536bbc54696f8e05153dbd3cca0f80ccf37 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 24 Jan 2017 14:38:27 +0000 Subject: [PATCH 344/749] not break up ATOMICs --- gem/optimise.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gem/optimise.py b/gem/optimise.py index 39dde041c..51ffafebf 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -372,7 +372,7 @@ class FactorisationError(Exception): def collect_monomials(expression, classifier): def stop_at(expr): - return classifier(expr) == OTHER + return classifier(expr) != COMPOUND common_indices, terms = traverse_product(expression, stop_at=stop_at) common_atomics = [] From c42128b5ae97f8fca8484a75de2404ca0ef5366f Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 24 Jan 2017 14:37:40 +0000 Subject: [PATCH 345/749] add thorough documentation --- gem/optimise.py | 88 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/gem/optimise.py b/gem/optimise.py index 51ffafebf..9ccb901c2 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -317,6 +317,19 @@ def contraction(expression): def traverse_product(expression, stop_at=None): + """Traverses a product tree and collects factors, also descending into + tensor contractions (IndexSum). The nominators of divisions are + also broken up, but not the denominators. + + :arg expression: a GEM expression + :arg stop_at: Optional predicate on GEM expressions. If specified + and returns true for some subexpression, that + subexpression is not broken into further factors + even if it is a product-like expression. + :returns: (sum_indices, terms) + - sum_indices: list of indices to sum over + - terms: list of product terms + """ sum_indices = [] terms = [] @@ -345,6 +358,15 @@ def traverse_product(expression, stop_at=None): def traverse_sum(expression, stop_at=None): + """Traverses a summation tree and collects summands. + + :arg expression: a GEM expression + :arg stop_at: Optional predicate on GEM expressions. If specified + and returns true for some subexpression, that + subexpression is not broken into further summands + even if it is an addition. + :returns: list of summand expressions + """ stack = [expression] result = [] while stack: @@ -358,11 +380,33 @@ def traverse_sum(expression, stop_at=None): return result +# Refactorisation classes + ATOMIC = intern('atomic') +"""Label: the expression need not be broken up into smaller parts""" + COMPOUND = intern('compound') +"""Label: the expression must be broken up into smaller parts""" + OTHER = intern('other') +"""Label: the expression is irrelevant with regards to refactorisation""" + Monomial = namedtuple('Monomial', ['sum_indices', 'atomics', 'rest']) +"""Monomial type, used in the return type of +:py:func:`collect_monomials`. + +- sum_indices: indices to sum over +- atomics: tuple of expressions classified as ATOMIC +- rest: a single expression classified as OTHER + +A :py:class:`Monomial` is a structured description of the expression: + +.. code-block:: python + + IndexSum(reduce(Product, atomics, rest), sum_indices) + +""" class FactorisationError(Exception): @@ -371,7 +415,24 @@ class FactorisationError(Exception): def collect_monomials(expression, classifier): + """Refactorises an expression into a sum-of-products form, using + distributivity rules (i.e. a*(b + c) -> a*b + a*c). Expansion + proceeds until all "compound" expressions are broken up. + + :arg expression: a GEM expression to refactorise + :arg classifier: a function that can classify any GEM expression + as ``ATOMIC``, ``COMPOUND``, or ``OTHER``. This + classification drives the factorisation. + + :returns: list of monomials; each monomial is a summand and a + structured description of a product + + :raises FactorisationError: Failed to break up some "compound" + expressions with expansion. + """ + # Phase 1: Collect and categorise product terms def stop_at(expr): + # Break up compounds only return classifier(expr) != COMPOUND common_indices, terms = traverse_product(expression, stop_at=stop_at) @@ -389,27 +450,51 @@ def stop_at(expr): else: raise ValueError("Classifier returned illegal value.") + # Phase 2: Attempt to break up compound terms into summands sums = [] for expr in compounds: summands = traverse_sum(expr, stop_at=stop_at) if len(summands) <= 1: + # Compound term is not an addition, avoid infinite + # recursion and fail gracefully raising an exception. raise FactorisationError(expr) + # Recurse into each summand, concatenate their results sums.append(chain.from_iterable(collect_monomials(summand, classifier) for summand in summands)) + # Phase 3: Expansion + # + # Each element of ``sums`` is list (representing a sum) of + # monomials corresponding to one compound product term. Expansion + # produces a series (representing a sum) of products of monomials. unfactored = OrderedDict() for partials in product(*sums): + # ``partials`` is a tuple of :py:class:`Monomial`s. Here we + # construct their "product" with the common atomic and other + # factors. + + # Copy common ingredients all_indices = list(common_indices) atomics = list(common_atomics) others = list(common_others) + + # Decompose monomial named tuples for monomial in partials: all_indices.extend(monomial.sum_indices) atomics.extend(monomial.atomics) others.append(monomial.rest) + + # All free indices that appear in atomic terms atomic_indices = set().union(*[atomic.free_indices for atomic in atomics]) + + # Sum indices that appear in atomic terms + # (will go to the result :py:class:`Monomial`) sum_indices = tuple(index for index in all_indices if index in atomic_indices) + + # Sum indices that do not appear in atomic terms + # (can factorise them over atomic terms immediately) rest_indices = tuple(index for index in all_indices if index not in atomic_indices) @@ -421,6 +506,9 @@ def stop_at(expr): key = (frozenset(sum_indices), frozenset(atomics)) unfactored.setdefault(key, []).append(monomial) + # Phase 4: Re-factorise. + # + # Merge ``rest`` for identical ``sum_indices`` and ``atomics``. result = [] for monomials in itervalues(unfactored): sum_indices = monomials[0].sum_indices From 4d2ecb255371d0a525db029a3eef6bdc08554b33 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 24 Jan 2017 14:48:32 +0000 Subject: [PATCH 346/749] deduplicate code --- gem/optimise.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index 9ccb901c2..4b97a7f08 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -5,7 +5,7 @@ from six import itervalues from six.moves import filter, intern, map, zip -from collections import OrderedDict, deque, namedtuple +from collections import OrderedDict, namedtuple from functools import reduce from itertools import chain, permutations, product @@ -298,22 +298,8 @@ def contraction(expression): # Eliminate annoying ComponentTensors expression, = remove_componenttensors([expression]) - # Flatten a product tree - sum_indices = [] - factors = [] - - queue = deque([expression]) - while queue: - expr = queue.popleft() - if isinstance(expr, IndexSum): - queue.append(expr.children[0]) - sum_indices.extend(expr.multiindex) - elif isinstance(expr, Product): - queue.extend(expr.children) - else: - factors.append(expr) - - return sum_factorise(*delta_elimination(sum_indices, factors)) + # Flatten product tree, eliminate deltas, sum factorise + return sum_factorise(*delta_elimination(*traverse_product(expression))) def traverse_product(expression, stop_at=None): From 301fbdc97f6b07e58fa3b8375938cbd2b6a2632a Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 24 Jan 2017 16:30:12 +0000 Subject: [PATCH 347/749] optimise associativity --- gem/optimise.py | 50 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index 4b97a7f08..149dc7bcb 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -7,7 +7,7 @@ from collections import OrderedDict, namedtuple from functools import reduce -from itertools import chain, permutations, product +from itertools import chain, combinations, permutations, product import numpy from singledispatch import singledispatch @@ -230,6 +230,36 @@ def delta_elimination(sum_indices, factors): return sum_indices, [e for e in factors if e != one] +def _associate(factors): + """Apply associativity rules to construct an operation-minimal product tree. + + For best performance give factors that have different set of free indices. + """ + if len(factors) > 32: + # O(N^3) algorithm + raise NotImplementedError("Not expected such a complicated expression!") + + def count(pair): + """Operation count to multiply a pair of GEM expressions""" + a, b = pair + extents = [i.extent for i in set().union(a.free_indices, b.free_indices)] + return numpy.prod(extents, dtype=int) + + factors = list(factors) # copy for in-place modifications + flops = 0 + while len(factors) > 1: + # Greedy algorithm: choose a pair of factors that are the + # cheapest to multiply. + a, b = min(combinations(factors, 2), key=count) + flops += count((a, b)) + # Remove chosen factors, append their product + factors.remove(a) + factors.remove(b) + factors.append(Product(a, b)) + product, = factors + return product, flops + + def sum_factorise(sum_indices, factors): """Optimise a tensor product through sum factorisation. @@ -237,6 +267,10 @@ def sum_factorise(sum_indices, factors): :arg factors: product factors :returns: optimised GEM expression """ + if len(factors) == 0 and len(sum_indices) == 0: + # Empty product + return one + if len(sum_indices) > 5: raise NotImplementedError("Too many indices for sum factorisation!") @@ -260,14 +294,10 @@ def sum_factorise(sum_indices, factors): contract = [t for t in terms if sum_index in t.free_indices] deferred = [t for t in terms if sum_index not in t.free_indices] - # A further optimisation opportunity is to consider - # various ways of building the product tree. - product = reduce(Product, contract) + # Optimise associativity + product, flops_ = _associate(contract) term = IndexSum(product, (sum_index,)) - # For the operation count estimation we assume that no - # operations were saved with the particular product tree - # that we built above. - flops += len(contract) * numpy.prod([i.extent for i in product.free_indices], dtype=int) + flops += flops_ + numpy.prod([i.extent for i in product.free_indices], dtype=int) # Replace the contracted terms with the result of the # contraction. @@ -275,8 +305,8 @@ def sum_factorise(sum_indices, factors): # If some contraction indices were independent, then we may # still have several terms at this point. - expr = reduce(Product, terms, one) - flops += (len(terms) - 1) * numpy.prod([i.extent for i in expr.free_indices], dtype=int) + expr, flops_ = _associate(terms) + flops += flops_ if flops < best_flops: expression = expr From f628a82b6287644b047a3e234394d1638090d684 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 27 Jan 2017 10:03:45 +0000 Subject: [PATCH 348/749] fix division bug in traverse_product --- gem/optimise.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index 149dc7bcb..825363a13 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -365,8 +365,8 @@ def traverse_product(expression, stop_at=None): if dividend == one: terms.append(expr) else: - terms.append(Division(one, divisor)) - terms.append(dividend) + stack.append(Division(one, divisor)) + stack.append(dividend) else: terms.append(expr) From 9f9a9e3af93d85d8b1215d1b6e0d8df081a8233c Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 10 Mar 2017 11:49:54 +0000 Subject: [PATCH 349/749] make monomial collection DAG-aware --- gem/optimise.py | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index 825363a13..da95c69e0 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -430,15 +430,13 @@ class FactorisationError(Exception): pass -def collect_monomials(expression, classifier): +def _collect_monomials(expression, self): """Refactorises an expression into a sum-of-products form, using distributivity rules (i.e. a*(b + c) -> a*b + a*c). Expansion proceeds until all "compound" expressions are broken up. :arg expression: a GEM expression to refactorise - :arg classifier: a function that can classify any GEM expression - as ``ATOMIC``, ``COMPOUND``, or ``OTHER``. This - classification drives the factorisation. + :arg self: function for recursive calls :returns: list of monomials; each monomial is a summand and a structured description of a product @@ -449,14 +447,14 @@ def collect_monomials(expression, classifier): # Phase 1: Collect and categorise product terms def stop_at(expr): # Break up compounds only - return classifier(expr) != COMPOUND + return self.classifier(expr) != COMPOUND common_indices, terms = traverse_product(expression, stop_at=stop_at) common_atomics = [] common_others = [] compounds = [] for term in terms: - cls = classifier(term) + cls = self.classifier(term) if cls == ATOMIC: common_atomics.append(term) elif cls == COMPOUND: @@ -475,8 +473,7 @@ def stop_at(expr): # recursion and fail gracefully raising an exception. raise FactorisationError(expr) # Recurse into each summand, concatenate their results - sums.append(chain.from_iterable(collect_monomials(summand, classifier) - for summand in summands)) + sums.append(chain.from_iterable(map(self, summands))) # Phase 3: Expansion # @@ -534,6 +531,27 @@ def stop_at(expr): return result +def collect_monomials(expression, classifier): + """Refactorises an expression into a sum-of-products form, using + distributivity rules (i.e. a*(b + c) -> a*b + a*c). Expansion + proceeds until all "compound" expressions are broken up. + + :arg expression: a GEM expression to refactorise + :arg classifier: a function that can classify any GEM expression + as ``ATOMIC``, ``COMPOUND``, or ``OTHER``. This + classification drives the factorisation. + + :returns: list of monomials; each monomial is a summand and a + structured description of a product + + :raises FactorisationError: Failed to break up some "compound" + expressions with expansion. + """ + mapper = Memoizer(_collect_monomials) + mapper.classifier = classifier + return mapper(expression) + + @singledispatch def _replace_delta(node, self): raise AssertionError("cannot handle type %s" % type(node)) From edd4eadae461e2b04845b94c9c05d358b68ca75a Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 9 Mar 2017 15:41:18 +0000 Subject: [PATCH 350/749] unroll ListTensors to enable refactorisation --- gem/optimise.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index da95c69e0..234f16a31 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -12,7 +12,7 @@ import numpy from singledispatch import singledispatch -from gem.node import Memoizer, MemoizerArg, reuse_if_untouched, reuse_if_untouched_arg +from gem.node import Memoizer, MemoizerArg, reuse_if_untouched, reuse_if_untouched_arg, traversal from gem.gem import (Node, Terminal, Failure, Identity, Literal, Zero, Product, Sum, Comparison, Conditional, Division, Index, VariableIndex, Indexed, FlexiblyIndexed, @@ -531,25 +531,43 @@ def stop_at(expr): return result -def collect_monomials(expression, classifier): - """Refactorises an expression into a sum-of-products form, using +def collect_monomials(expressions, classifier): + """Refactorises expressions into a sum-of-products form, using distributivity rules (i.e. a*(b + c) -> a*b + a*c). Expansion proceeds until all "compound" expressions are broken up. - :arg expression: a GEM expression to refactorise + :arg expressions: GEM expressions to refactorise :arg classifier: a function that can classify any GEM expression as ``ATOMIC``, ``COMPOUND``, or ``OTHER``. This classification drives the factorisation. - :returns: list of monomials; each monomial is a summand and a + :returns: list of list of monomials; the outer list has one entry + for each expression; each monomial is a summand and a structured description of a product :raises FactorisationError: Failed to break up some "compound" expressions with expansion. """ + # Get ComponentTensors out of the way + expressions = remove_componenttensors(expressions) + + # Get ListTensors out of the way + must_unroll = [] # indices to unroll + for node in traversal(expressions): + if isinstance(node, Indexed): + child, = node.children + if isinstance(child, ListTensor) and classifier(node) == COMPOUND: + must_unroll.extend(node.multiindex) + if must_unroll: + must_unroll = set(must_unroll) + expressions = unroll_indexsum(expressions, + predicate=lambda i: i in must_unroll) + expressions = remove_componenttensors(expressions) + + # Finally, refactorise expressions mapper = Memoizer(_collect_monomials) mapper.classifier = classifier - return mapper(expression) + return list(map(mapper, expressions)) @singledispatch From bc3858a38824d63d09f8c7fa7b23194a3690e0ed Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 10 Mar 2017 14:27:23 +0000 Subject: [PATCH 351/749] speed up and optimise refactorisation --- gem/optimise.py | 134 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 90 insertions(+), 44 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index 234f16a31..b22bfe21d 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -2,12 +2,12 @@ expressions.""" from __future__ import absolute_import, print_function, division -from six import itervalues +from six import iteritems, itervalues from six.moves import filter, intern, map, zip -from collections import OrderedDict, namedtuple +from collections import OrderedDict, defaultdict, namedtuple from functools import reduce -from itertools import chain, combinations, permutations, product +from itertools import combinations, permutations, product import numpy from singledispatch import singledispatch @@ -409,8 +409,8 @@ def traverse_sum(expression, stop_at=None): Monomial = namedtuple('Monomial', ['sum_indices', 'atomics', 'rest']) -"""Monomial type, used in the return type of -:py:func:`collect_monomials`. +"""Monomial type, representation of a tensor product with some +distinguished factors (called atomics). - sum_indices: indices to sum over - atomics: tuple of expressions classified as ATOMIC @@ -425,6 +425,78 @@ def traverse_sum(expression, stop_at=None): """ +class MonomialSum(object): + """Represents a sum of :py:class:`Monomial`s. + + The set of :py:class:`Monomial` summands are represented as a + mapping from a pair of unordered ``sum_indices`` and unordered + ``atomics`` to a ``rest`` GEM expression. This representation + makes it easier to merge similar monomials. + """ + def __init__(self): + # (unordered sum_indices, unordered atomics) -> rest + self.monomials = defaultdict(Zero) + + # We shall retain ordering for deterministic code generation: + # + # (unordered sum_indices, unordered atomics) -> + # (ordered sum_indices, ordered atomics) + self.ordering = OrderedDict() + + def add(self, sum_indices, atomics, rest): + """Updates the :py:class:`MonomialSum` adding a new monomial.""" + sum_indices = tuple(sum_indices) + sum_indices_set = frozenset(sum_indices) + # Sum indices cannot have duplicates + assert len(sum_indices) == len(sum_indices_set) + + atomics = tuple(atomics) + atomics_set = frozenset(atomics) + if len(atomics) != len(atomics_set): + raise NotImplementedError("MonomialSum does not support duplicate atomics") + + assert isinstance(rest, Node) + + key = (sum_indices_set, atomics_set) + self.monomials[key] = Sum(self.monomials[key], rest) + self.ordering.setdefault(key, (sum_indices, atomics)) + + def __iter__(self): + """Iteration yields :py:class:`Monomial` objects""" + for key, (sum_indices, atomics) in iteritems(self.ordering): + rest = self.monomials[key] + yield Monomial(sum_indices, atomics, rest) + + @staticmethod + def sum(*args): + """Sum of multiple :py:class:`MonomialSum`s""" + result = MonomialSum() + for arg in args: + assert isinstance(arg, MonomialSum) + # Optimised implementation: no need to decompose and + # reconstruct key. + for key, rest in iteritems(arg.monomials): + result.monomials[key] = Sum(result.monomials[key], rest) + for key, value in iteritems(arg.ordering): + result.ordering.setdefault(key, value) + return result + + @staticmethod + def product(*args): + """Product of multiple :py:class:`MonomialSum`s""" + result = MonomialSum() + for monomials in product(*args): + sum_indices = [] + atomics = [] + rest = one + for s, a, r in monomials: + sum_indices.extend(s) + atomics.extend(a) + rest = Product(r, rest) + result.add(sum_indices, atomics, rest) + return result + + class FactorisationError(Exception): """Raised when factorisation fails to achieve some desired form.""" pass @@ -438,8 +510,7 @@ def _collect_monomials(expression, self): :arg expression: a GEM expression to refactorise :arg self: function for recursive calls - :returns: list of monomials; each monomial is a summand and a - structured description of a product + :returns: :py:class:`MonomialSum` :raises FactorisationError: Failed to break up some "compound" expressions with expansion. @@ -449,6 +520,7 @@ def stop_at(expr): # Break up compounds only return self.classifier(expr) != COMPOUND common_indices, terms = traverse_product(expression, stop_at=stop_at) + common_indices = tuple(common_indices) common_atomics = [] common_others = [] @@ -463,6 +535,7 @@ def stop_at(expr): common_others.append(term) else: raise ValueError("Classifier returned illegal value.") + common_atomics = tuple(common_atomics) # Phase 2: Attempt to break up compound terms into summands sums = [] @@ -473,29 +546,16 @@ def stop_at(expr): # recursion and fail gracefully raising an exception. raise FactorisationError(expr) # Recurse into each summand, concatenate their results - sums.append(chain.from_iterable(map(self, summands))) + sums.append(MonomialSum.sum(*map(self, summands))) # Phase 3: Expansion # - # Each element of ``sums`` is list (representing a sum) of - # monomials corresponding to one compound product term. Expansion - # produces a series (representing a sum) of products of monomials. - unfactored = OrderedDict() - for partials in product(*sums): - # ``partials`` is a tuple of :py:class:`Monomial`s. Here we - # construct their "product" with the common atomic and other - # factors. - - # Copy common ingredients - all_indices = list(common_indices) - atomics = list(common_atomics) - others = list(common_others) - - # Decompose monomial named tuples - for monomial in partials: - all_indices.extend(monomial.sum_indices) - atomics.extend(monomial.atomics) - others.append(monomial.rest) + # Each element of ``sums`` is a MonomialSum. Expansion produces a + # series (representing a sum) of products of monomials. + result = MonomialSum() + for s, a, r in MonomialSum.product(*sums): + all_indices = common_indices + s + atomics = common_atomics + a # All free indices that appear in atomic terms atomic_indices = set().union(*[atomic.free_indices @@ -513,21 +573,9 @@ def stop_at(expr): # Not really sum factorisation, but rather just an optimised # way of building a product. - rest = sum_factorise(rest_indices, others) + rest = sum_factorise(rest_indices, common_others + [r]) - monomial = Monomial(sum_indices, tuple(atomics), rest) - key = (frozenset(sum_indices), frozenset(atomics)) - unfactored.setdefault(key, []).append(monomial) - - # Phase 4: Re-factorise. - # - # Merge ``rest`` for identical ``sum_indices`` and ``atomics``. - result = [] - for monomials in itervalues(unfactored): - sum_indices = monomials[0].sum_indices - atomics = monomials[0].atomics - rest = reduce(Sum, [m.rest for m in monomials]) - result.append(Monomial(sum_indices, atomics, rest)) + result.add(sum_indices, atomics, rest) return result @@ -541,9 +589,7 @@ def collect_monomials(expressions, classifier): as ``ATOMIC``, ``COMPOUND``, or ``OTHER``. This classification drives the factorisation. - :returns: list of list of monomials; the outer list has one entry - for each expression; each monomial is a summand and a - structured description of a product + :returns: list of :py:class:`MonomialSum`s :raises FactorisationError: Failed to break up some "compound" expressions with expansion. From d47e190917ea8539063899f487908a0b16482788 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 15 Mar 2017 12:09:34 +0000 Subject: [PATCH 352/749] split refactorisation from gem.optimise --- gem/optimise.py | 258 ++++----------------------------------------- gem/refactorise.py | 234 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 253 insertions(+), 239 deletions(-) create mode 100644 gem/refactorise.py diff --git a/gem/optimise.py b/gem/optimise.py index b22bfe21d..c1e42aa40 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -2,17 +2,17 @@ expressions.""" from __future__ import absolute_import, print_function, division -from six import iteritems, itervalues -from six.moves import filter, intern, map, zip +from six import itervalues +from six.moves import filter, map, zip -from collections import OrderedDict, defaultdict, namedtuple +from collections import OrderedDict from functools import reduce -from itertools import combinations, permutations, product +from itertools import combinations, permutations import numpy from singledispatch import singledispatch -from gem.node import Memoizer, MemoizerArg, reuse_if_untouched, reuse_if_untouched_arg, traversal +from gem.node import Memoizer, MemoizerArg, reuse_if_untouched, reuse_if_untouched_arg from gem.gem import (Node, Terminal, Failure, Identity, Literal, Zero, Product, Sum, Comparison, Conditional, Division, Index, VariableIndex, Indexed, FlexiblyIndexed, @@ -230,7 +230,7 @@ def delta_elimination(sum_indices, factors): return sum_indices, [e for e in factors if e != one] -def _associate(factors): +def associate_product(factors): """Apply associativity rules to construct an operation-minimal product tree. For best performance give factors that have different set of free indices. @@ -295,7 +295,7 @@ def sum_factorise(sum_indices, factors): deferred = [t for t in terms if sum_index not in t.free_indices] # Optimise associativity - product, flops_ = _associate(contract) + product, flops_ = associate_product(contract) term = IndexSum(product, (sum_index,)) flops += flops_ + numpy.prod([i.extent for i in product.free_indices], dtype=int) @@ -305,7 +305,7 @@ def sum_factorise(sum_indices, factors): # If some contraction indices were independent, then we may # still have several terms at this point. - expr, flops_ = _associate(terms) + expr, flops_ = associate_product(terms) flops += flops_ if flops < best_flops: @@ -315,23 +315,6 @@ def sum_factorise(sum_indices, factors): return expression -def contraction(expression): - """Optimise the contractions of the tensor product at the root of - the expression, including: - - - IndexSum-Delta cancellation - - Sum factorisation - - This routine was designed with finite element coefficient - evaluation in mind. - """ - # Eliminate annoying ComponentTensors - expression, = remove_componenttensors([expression]) - - # Flatten product tree, eliminate deltas, sum factorise - return sum_factorise(*delta_elimination(*traverse_product(expression))) - - def traverse_product(expression, stop_at=None): """Traverses a product tree and collects factors, also descending into tensor contractions (IndexSum). The nominators of divisions are @@ -396,224 +379,21 @@ def traverse_sum(expression, stop_at=None): return result -# Refactorisation classes - -ATOMIC = intern('atomic') -"""Label: the expression need not be broken up into smaller parts""" - -COMPOUND = intern('compound') -"""Label: the expression must be broken up into smaller parts""" - -OTHER = intern('other') -"""Label: the expression is irrelevant with regards to refactorisation""" - - -Monomial = namedtuple('Monomial', ['sum_indices', 'atomics', 'rest']) -"""Monomial type, representation of a tensor product with some -distinguished factors (called atomics). - -- sum_indices: indices to sum over -- atomics: tuple of expressions classified as ATOMIC -- rest: a single expression classified as OTHER - -A :py:class:`Monomial` is a structured description of the expression: - -.. code-block:: python - - IndexSum(reduce(Product, atomics, rest), sum_indices) - -""" - - -class MonomialSum(object): - """Represents a sum of :py:class:`Monomial`s. - - The set of :py:class:`Monomial` summands are represented as a - mapping from a pair of unordered ``sum_indices`` and unordered - ``atomics`` to a ``rest`` GEM expression. This representation - makes it easier to merge similar monomials. - """ - def __init__(self): - # (unordered sum_indices, unordered atomics) -> rest - self.monomials = defaultdict(Zero) - - # We shall retain ordering for deterministic code generation: - # - # (unordered sum_indices, unordered atomics) -> - # (ordered sum_indices, ordered atomics) - self.ordering = OrderedDict() - - def add(self, sum_indices, atomics, rest): - """Updates the :py:class:`MonomialSum` adding a new monomial.""" - sum_indices = tuple(sum_indices) - sum_indices_set = frozenset(sum_indices) - # Sum indices cannot have duplicates - assert len(sum_indices) == len(sum_indices_set) - - atomics = tuple(atomics) - atomics_set = frozenset(atomics) - if len(atomics) != len(atomics_set): - raise NotImplementedError("MonomialSum does not support duplicate atomics") - - assert isinstance(rest, Node) - - key = (sum_indices_set, atomics_set) - self.monomials[key] = Sum(self.monomials[key], rest) - self.ordering.setdefault(key, (sum_indices, atomics)) - - def __iter__(self): - """Iteration yields :py:class:`Monomial` objects""" - for key, (sum_indices, atomics) in iteritems(self.ordering): - rest = self.monomials[key] - yield Monomial(sum_indices, atomics, rest) - - @staticmethod - def sum(*args): - """Sum of multiple :py:class:`MonomialSum`s""" - result = MonomialSum() - for arg in args: - assert isinstance(arg, MonomialSum) - # Optimised implementation: no need to decompose and - # reconstruct key. - for key, rest in iteritems(arg.monomials): - result.monomials[key] = Sum(result.monomials[key], rest) - for key, value in iteritems(arg.ordering): - result.ordering.setdefault(key, value) - return result - - @staticmethod - def product(*args): - """Product of multiple :py:class:`MonomialSum`s""" - result = MonomialSum() - for monomials in product(*args): - sum_indices = [] - atomics = [] - rest = one - for s, a, r in monomials: - sum_indices.extend(s) - atomics.extend(a) - rest = Product(r, rest) - result.add(sum_indices, atomics, rest) - return result - - -class FactorisationError(Exception): - """Raised when factorisation fails to achieve some desired form.""" - pass - - -def _collect_monomials(expression, self): - """Refactorises an expression into a sum-of-products form, using - distributivity rules (i.e. a*(b + c) -> a*b + a*c). Expansion - proceeds until all "compound" expressions are broken up. - - :arg expression: a GEM expression to refactorise - :arg self: function for recursive calls +def contraction(expression): + """Optimise the contractions of the tensor product at the root of + the expression, including: - :returns: :py:class:`MonomialSum` + - IndexSum-Delta cancellation + - Sum factorisation - :raises FactorisationError: Failed to break up some "compound" - expressions with expansion. + This routine was designed with finite element coefficient + evaluation in mind. """ - # Phase 1: Collect and categorise product terms - def stop_at(expr): - # Break up compounds only - return self.classifier(expr) != COMPOUND - common_indices, terms = traverse_product(expression, stop_at=stop_at) - common_indices = tuple(common_indices) - - common_atomics = [] - common_others = [] - compounds = [] - for term in terms: - cls = self.classifier(term) - if cls == ATOMIC: - common_atomics.append(term) - elif cls == COMPOUND: - compounds.append(term) - elif cls == OTHER: - common_others.append(term) - else: - raise ValueError("Classifier returned illegal value.") - common_atomics = tuple(common_atomics) - - # Phase 2: Attempt to break up compound terms into summands - sums = [] - for expr in compounds: - summands = traverse_sum(expr, stop_at=stop_at) - if len(summands) <= 1: - # Compound term is not an addition, avoid infinite - # recursion and fail gracefully raising an exception. - raise FactorisationError(expr) - # Recurse into each summand, concatenate their results - sums.append(MonomialSum.sum(*map(self, summands))) - - # Phase 3: Expansion - # - # Each element of ``sums`` is a MonomialSum. Expansion produces a - # series (representing a sum) of products of monomials. - result = MonomialSum() - for s, a, r in MonomialSum.product(*sums): - all_indices = common_indices + s - atomics = common_atomics + a - - # All free indices that appear in atomic terms - atomic_indices = set().union(*[atomic.free_indices - for atomic in atomics]) - - # Sum indices that appear in atomic terms - # (will go to the result :py:class:`Monomial`) - sum_indices = tuple(index for index in all_indices - if index in atomic_indices) - - # Sum indices that do not appear in atomic terms - # (can factorise them over atomic terms immediately) - rest_indices = tuple(index for index in all_indices - if index not in atomic_indices) - - # Not really sum factorisation, but rather just an optimised - # way of building a product. - rest = sum_factorise(rest_indices, common_others + [r]) - - result.add(sum_indices, atomics, rest) - return result - - -def collect_monomials(expressions, classifier): - """Refactorises expressions into a sum-of-products form, using - distributivity rules (i.e. a*(b + c) -> a*b + a*c). Expansion - proceeds until all "compound" expressions are broken up. - - :arg expressions: GEM expressions to refactorise - :arg classifier: a function that can classify any GEM expression - as ``ATOMIC``, ``COMPOUND``, or ``OTHER``. This - classification drives the factorisation. - - :returns: list of :py:class:`MonomialSum`s + # Eliminate annoying ComponentTensors + expression, = remove_componenttensors([expression]) - :raises FactorisationError: Failed to break up some "compound" - expressions with expansion. - """ - # Get ComponentTensors out of the way - expressions = remove_componenttensors(expressions) - - # Get ListTensors out of the way - must_unroll = [] # indices to unroll - for node in traversal(expressions): - if isinstance(node, Indexed): - child, = node.children - if isinstance(child, ListTensor) and classifier(node) == COMPOUND: - must_unroll.extend(node.multiindex) - if must_unroll: - must_unroll = set(must_unroll) - expressions = unroll_indexsum(expressions, - predicate=lambda i: i in must_unroll) - expressions = remove_componenttensors(expressions) - - # Finally, refactorise expressions - mapper = Memoizer(_collect_monomials) - mapper.classifier = classifier - return list(map(mapper, expressions)) + # Flatten product tree, eliminate deltas, sum factorise + return sum_factorise(*delta_elimination(*traverse_product(expression))) @singledispatch diff --git a/gem/refactorise.py b/gem/refactorise.py new file mode 100644 index 000000000..93a52075f --- /dev/null +++ b/gem/refactorise.py @@ -0,0 +1,234 @@ +"""Data structures and algorithms for generic expansion and +refactorisation.""" + +from __future__ import absolute_import, print_function, division +from six import iteritems +from six.moves import intern, map + +from collections import OrderedDict, defaultdict, namedtuple +from itertools import product + +from gem.node import Memoizer, traversal +from gem.gem import Node, Zero, Product, Sum, Indexed, ListTensor, one +from gem.optimise import (remove_componenttensors, sum_factorise, + traverse_product, traverse_sum, unroll_indexsum) + + +# Refactorisation labels + +ATOMIC = intern('atomic') +"""Label: the expression need not be broken up into smaller parts""" + +COMPOUND = intern('compound') +"""Label: the expression must be broken up into smaller parts""" + +OTHER = intern('other') +"""Label: the expression is irrelevant with regards to refactorisation""" + + +Monomial = namedtuple('Monomial', ['sum_indices', 'atomics', 'rest']) +"""Monomial type, representation of a tensor product with some +distinguished factors (called atomics). + +- sum_indices: indices to sum over +- atomics: tuple of expressions classified as ATOMIC +- rest: a single expression classified as OTHER + +A :py:class:`Monomial` is a structured description of the expression: + +.. code-block:: python + + IndexSum(reduce(Product, atomics, rest), sum_indices) + +""" + + +class MonomialSum(object): + """Represents a sum of :py:class:`Monomial`s. + + The set of :py:class:`Monomial` summands are represented as a + mapping from a pair of unordered ``sum_indices`` and unordered + ``atomics`` to a ``rest`` GEM expression. This representation + makes it easier to merge similar monomials. + """ + def __init__(self): + # (unordered sum_indices, unordered atomics) -> rest + self.monomials = defaultdict(Zero) + + # We shall retain ordering for deterministic code generation: + # + # (unordered sum_indices, unordered atomics) -> + # (ordered sum_indices, ordered atomics) + self.ordering = OrderedDict() + + def add(self, sum_indices, atomics, rest): + """Updates the :py:class:`MonomialSum` adding a new monomial.""" + sum_indices = tuple(sum_indices) + sum_indices_set = frozenset(sum_indices) + # Sum indices cannot have duplicates + assert len(sum_indices) == len(sum_indices_set) + + atomics = tuple(atomics) + atomics_set = frozenset(atomics) + if len(atomics) != len(atomics_set): + raise NotImplementedError("MonomialSum does not support duplicate atomics") + + assert isinstance(rest, Node) + + key = (sum_indices_set, atomics_set) + self.monomials[key] = Sum(self.monomials[key], rest) + self.ordering.setdefault(key, (sum_indices, atomics)) + + def __iter__(self): + """Iteration yields :py:class:`Monomial` objects""" + for key, (sum_indices, atomics) in iteritems(self.ordering): + rest = self.monomials[key] + yield Monomial(sum_indices, atomics, rest) + + @staticmethod + def sum(*args): + """Sum of multiple :py:class:`MonomialSum`s""" + result = MonomialSum() + for arg in args: + assert isinstance(arg, MonomialSum) + # Optimised implementation: no need to decompose and + # reconstruct key. + for key, rest in iteritems(arg.monomials): + result.monomials[key] = Sum(result.monomials[key], rest) + for key, value in iteritems(arg.ordering): + result.ordering.setdefault(key, value) + return result + + @staticmethod + def product(*args): + """Product of multiple :py:class:`MonomialSum`s""" + result = MonomialSum() + for monomials in product(*args): + sum_indices = [] + atomics = [] + rest = one + for s, a, r in monomials: + sum_indices.extend(s) + atomics.extend(a) + rest = Product(r, rest) + result.add(sum_indices, atomics, rest) + return result + + +class FactorisationError(Exception): + """Raised when factorisation fails to achieve some desired form.""" + pass + + +def _collect_monomials(expression, self): + """Refactorises an expression into a sum-of-products form, using + distributivity rules (i.e. a*(b + c) -> a*b + a*c). Expansion + proceeds until all "compound" expressions are broken up. + + :arg expression: a GEM expression to refactorise + :arg self: function for recursive calls + + :returns: :py:class:`MonomialSum` + + :raises FactorisationError: Failed to break up some "compound" + expressions with expansion. + """ + # Phase 1: Collect and categorise product terms + def stop_at(expr): + # Break up compounds only + return self.classifier(expr) != COMPOUND + common_indices, terms = traverse_product(expression, stop_at=stop_at) + common_indices = tuple(common_indices) + + common_atomics = [] + common_others = [] + compounds = [] + for term in terms: + cls = self.classifier(term) + if cls == ATOMIC: + common_atomics.append(term) + elif cls == COMPOUND: + compounds.append(term) + elif cls == OTHER: + common_others.append(term) + else: + raise ValueError("Classifier returned illegal value.") + common_atomics = tuple(common_atomics) + + # Phase 2: Attempt to break up compound terms into summands + sums = [] + for expr in compounds: + summands = traverse_sum(expr, stop_at=stop_at) + if len(summands) <= 1: + # Compound term is not an addition, avoid infinite + # recursion and fail gracefully raising an exception. + raise FactorisationError(expr) + # Recurse into each summand, concatenate their results + sums.append(MonomialSum.sum(*map(self, summands))) + + # Phase 3: Expansion + # + # Each element of ``sums`` is a MonomialSum. Expansion produces a + # series (representing a sum) of products of monomials. + result = MonomialSum() + for s, a, r in MonomialSum.product(*sums): + all_indices = common_indices + s + atomics = common_atomics + a + + # All free indices that appear in atomic terms + atomic_indices = set().union(*[atomic.free_indices + for atomic in atomics]) + + # Sum indices that appear in atomic terms + # (will go to the result :py:class:`Monomial`) + sum_indices = tuple(index for index in all_indices + if index in atomic_indices) + + # Sum indices that do not appear in atomic terms + # (can factorise them over atomic terms immediately) + rest_indices = tuple(index for index in all_indices + if index not in atomic_indices) + + # Not really sum factorisation, but rather just an optimised + # way of building a product. + rest = sum_factorise(rest_indices, common_others + [r]) + + result.add(sum_indices, atomics, rest) + return result + + +def collect_monomials(expressions, classifier): + """Refactorises expressions into a sum-of-products form, using + distributivity rules (i.e. a*(b + c) -> a*b + a*c). Expansion + proceeds until all "compound" expressions are broken up. + + :arg expressions: GEM expressions to refactorise + :arg classifier: a function that can classify any GEM expression + as ``ATOMIC``, ``COMPOUND``, or ``OTHER``. This + classification drives the factorisation. + + :returns: list of :py:class:`MonomialSum`s + + :raises FactorisationError: Failed to break up some "compound" + expressions with expansion. + """ + # Get ComponentTensors out of the way + expressions = remove_componenttensors(expressions) + + # Get ListTensors out of the way + must_unroll = [] # indices to unroll + for node in traversal(expressions): + if isinstance(node, Indexed): + child, = node.children + if isinstance(child, ListTensor) and classifier(node) == COMPOUND: + must_unroll.extend(node.multiindex) + if must_unroll: + must_unroll = set(must_unroll) + expressions = unroll_indexsum(expressions, + predicate=lambda i: i in must_unroll) + expressions = remove_componenttensors(expressions) + + # Finally, refactorise expressions + mapper = Memoizer(_collect_monomials) + mapper.classifier = classifier + return list(map(mapper, expressions)) From 1e8dfd66464c80d437b116a7f6c8f29b4bd8ff03 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 15 Mar 2017 14:34:29 +0000 Subject: [PATCH 353/749] support duplicate atomics --- gem/refactorise.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/gem/refactorise.py b/gem/refactorise.py index 93a52075f..c9efc979d 100644 --- a/gem/refactorise.py +++ b/gem/refactorise.py @@ -5,7 +5,7 @@ from six import iteritems from six.moves import intern, map -from collections import OrderedDict, defaultdict, namedtuple +from collections import Counter, OrderedDict, defaultdict, namedtuple from itertools import product from gem.node import Memoizer, traversal @@ -69,9 +69,7 @@ def add(self, sum_indices, atomics, rest): assert len(sum_indices) == len(sum_indices_set) atomics = tuple(atomics) - atomics_set = frozenset(atomics) - if len(atomics) != len(atomics_set): - raise NotImplementedError("MonomialSum does not support duplicate atomics") + atomics_set = frozenset(iteritems(Counter(atomics))) assert isinstance(rest, Node) From 423973469295d618121b017e4af55505cf5bdc92 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 15 Mar 2017 17:00:53 +0000 Subject: [PATCH 354/749] rename: cls -> label --- gem/refactorise.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gem/refactorise.py b/gem/refactorise.py index c9efc979d..ec6234a11 100644 --- a/gem/refactorise.py +++ b/gem/refactorise.py @@ -142,12 +142,12 @@ def stop_at(expr): common_others = [] compounds = [] for term in terms: - cls = self.classifier(term) - if cls == ATOMIC: + label = self.classifier(term) + if label == ATOMIC: common_atomics.append(term) - elif cls == COMPOUND: + elif label == COMPOUND: compounds.append(term) - elif cls == OTHER: + elif label == OTHER: common_others.append(term) else: raise ValueError("Classifier returned illegal value.") From d64253362b6fa3056dbd5ffd0fa215ab90b26939 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 23 Mar 2017 13:36:13 +0000 Subject: [PATCH 355/749] change impero_utils.compile_gem API --- gem/impero_utils.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/gem/impero_utils.py b/gem/impero_utils.py index db7a9565c..5beb96196 100644 --- a/gem/impero_utils.py +++ b/gem/impero_utils.py @@ -7,7 +7,7 @@ """ from __future__ import absolute_import, print_function, division -from six.moves import zip +from six.moves import filter import collections import itertools @@ -41,23 +41,22 @@ def preprocess_gem(expressions): return expressions -def compile_gem(return_variables, expressions, prefix_ordering, remove_zeros=False): +def compile_gem(assignments, prefix_ordering, remove_zeros=False): """Compiles GEM to Impero. - :arg return_variables: return variables for each root (type: GEM expressions) - :arg expressions: multi-root expression DAG (type: GEM expressions) + :arg assignments: list of (return variable, expression DAG root) pairs :arg prefix_ordering: outermost loop indices :arg remove_zeros: remove zero assignment to return variables """ # Remove zeros if remove_zeros: - rv = [] - es = [] - for var, expr in zip(return_variables, expressions): - if not isinstance(expr, gem.Zero): - rv.append(var) - es.append(expr) - return_variables, expressions = rv, es + def nonzero(assignment): + variable, expression = assignment + return not isinstance(expression, gem.Zero) + assignments = list(filter(nonzero, assignments)) + + # Just the expressions + expressions = [expression for variable, expression in assignments] # Collect indices in a deterministic order indices = OrderedSet() @@ -79,7 +78,7 @@ def compile_gem(return_variables, expressions, prefix_ordering, remove_zeros=Fal get_indices = lambda expr: apply_ordering(expr.free_indices) # Build operation ordering - ops = scheduling.emit_operations(list(zip(return_variables, expressions)), get_indices) + ops = scheduling.emit_operations(assignments, get_indices) # Empty kernel if len(ops) == 0: From 54feed71db19170e7f8a06931c925bda9880db7e Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 29 Mar 2017 16:33:30 +0100 Subject: [PATCH 356/749] avoid duplicate preprocessing --- gem/impero_utils.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/gem/impero_utils.py b/gem/impero_utils.py index 5beb96196..f524592df 100644 --- a/gem/impero_utils.py +++ b/gem/impero_utils.py @@ -34,10 +34,12 @@ class NoopError(Exception): pass -def preprocess_gem(expressions): +def preprocess_gem(expressions, replace_delta=True, remove_componenttensors=True): """Lower GEM nodes that cannot be translated to C directly.""" - expressions = optimise.replace_delta(expressions) - expressions = optimise.remove_componenttensors(expressions) + if replace_delta: + expressions = optimise.replace_delta(expressions) + if remove_componenttensors: + expressions = optimise.remove_componenttensors(expressions) return expressions From be098061013af3b56ca0e1b57a1a26a2c19cccc2 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 5 Apr 2017 17:44:01 +0100 Subject: [PATCH 357/749] fix simplification with delta elimination --- gem/optimise.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index c1e42aa40..aa0122215 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -226,8 +226,7 @@ def delta_elimination(sum_indices, factors): for f in factors if isinstance(f, Delta) for index in (f.i, f.j) if index in sum_indices] - # Drop ones - return sum_indices, [e for e in factors if e != one] + return sum_indices, factors def associate_product(factors): From 8caa821bfe6d13c6a85afaf9004738c3a24f9b7a Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 7 Apr 2017 19:58:02 +0100 Subject: [PATCH 358/749] add QuadratureElement --- finat/__init__.py | 1 + finat/point_set.py | 14 ++++++- finat/quadrature_element.py | 78 +++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 finat/quadrature_element.py diff --git a/finat/__init__.py b/finat/__init__.py index eb8a1a518..5b273ad87 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -7,4 +7,5 @@ from .tensorfiniteelement import TensorFiniteElement # noqa: F401 from .tensor_product import TensorProductElement # noqa: F401 from .quadrilateral import QuadrilateralElement # noqa: F401 +from .quadrature_element import QuadratureElement # noqa: F401 from . import quadrature # noqa: F401 diff --git a/finat/point_set.py b/finat/point_set.py index 19c47c425..ec73b8aad 100644 --- a/finat/point_set.py +++ b/finat/point_set.py @@ -1,6 +1,6 @@ from __future__ import absolute_import, print_function, division from six import with_metaclass -from six.moves import range +from six.moves import range, zip from abc import ABCMeta, abstractproperty from itertools import chain, product @@ -57,6 +57,12 @@ def indices(self): def expression(self): return gem.partial_indexed(gem.Literal(self.points), self.indices) + def almost_equal(self, other, tolerance=1e-12): + """Approximate numerical equality of point sets""" + return type(self) == type(other) and \ + self.points.shape == other.points.shape and \ + numpy.allclose(self.points, other.points, rtol=0, atol=tolerance) + class TensorPointSet(AbstractPointSet): @@ -80,3 +86,9 @@ def expression(self): for i in range(point_set.dimension): result.append(gem.Indexed(point_set.expression, (i,))) return gem.ListTensor(result) + + def almost_equal(self, other, tolerance=1e-12): + """Approximate numerical equality of point sets""" + return type(self) == type(other) and \ + len(self.factors) == len(other.factors) and \ + all(s.almost_equal(o) for s, o in zip(self.factors, other.factors)) diff --git a/finat/quadrature_element.py b/finat/quadrature_element.py new file mode 100644 index 000000000..9239171e4 --- /dev/null +++ b/finat/quadrature_element.py @@ -0,0 +1,78 @@ +from __future__ import absolute_import, print_function, division +from six import iteritems +from six.moves import range, zip +from functools import reduce + +import numpy + +import gem +from gem.utils import cached_property + +from finat.finiteelementbase import FiniteElementBase +from finat.quadrature import make_quadrature + + +class QuadratureElement(FiniteElementBase): + """A set of quadrature points pretending to be a finite element.""" + + def __init__(self, cell, degree, scheme="default"): + self.cell = cell + self._rule = make_quadrature(cell, degree, scheme) + + @cached_property + def cell(self): + pass # set at initialisation + + @property + def degree(self): + raise NotImplementedError("QuadratureElement does not represent a polynomial space.") + + @cached_property + def _entity_dofs(self): + # Inspired by ffc/quadratureelement.py + entity_dofs = { + {entity: [] for entity in entities} + for dim, entities in iteritems(self.cell.get_topology()) + } + entity_dofs[self.cell.get_dimension()] = {0: list(range(self.space_dimension()))} + return entity_dofs + + def entity_dofs(self): + return self._entity_dofs + + def space_dimension(self): + return numpy.prod(self.index_shape, dtype=int) + + @property + def index_shape(self): + ps = self._rule.point_set + return tuple(index.extent for index in ps.indices) + + @property + def value_shape(self): + return () + + def basis_evaluation(self, order, ps, entity=None): + '''Return code for evaluating the element at known points on the + reference element. + + :param order: return derivatives up to this order. + :param ps: the point set object. + :param entity: the cell entity on which to tabulate. + ''' + if entity is not None and entity != (self.cell.get_dimension(), 0): + raise ValueError('QuadratureElement does not "tabulate" on subentities.') + + if order: + raise ValueError("Derivatives are not defined on a QuadratureElement.") + + if not self._rule.point_set.almost_equal(ps): + raise ValueError("Mismatch of quadrature points!") + + # Return an outer product of identity matrices + multiindex = self.get_indices() + product = reduce(gem.Product, [gem.Delta(q, r) + for q, r in zip(ps.indices, multiindex)]) + + dim = self.cell.get_spatial_dimension() + return {(0,) * dim: gem.ComponentTensor(product, multiindex)} From 7aadd7df60101851e3a2e573efcd493064e07f78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Homolya?= Date: Mon, 10 Apr 2017 10:24:40 +0100 Subject: [PATCH 359/749] Propagate tolerance in TensorPointSet --- finat/point_set.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/finat/point_set.py b/finat/point_set.py index ec73b8aad..b49389229 100644 --- a/finat/point_set.py +++ b/finat/point_set.py @@ -91,4 +91,5 @@ def almost_equal(self, other, tolerance=1e-12): """Approximate numerical equality of point sets""" return type(self) == type(other) and \ len(self.factors) == len(other.factors) and \ - all(s.almost_equal(o) for s, o in zip(self.factors, other.factors)) + all(s.almost_equal(o, tolerance=tolerance) + for s, o in zip(self.factors, other.factors)) From 987fd7da4a621a78367589b69b86dd194ddcf3c2 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 10 Apr 2017 18:30:36 +0100 Subject: [PATCH 360/749] add missing key --- finat/quadrature_element.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/finat/quadrature_element.py b/finat/quadrature_element.py index 9239171e4..a9c59f006 100644 --- a/finat/quadrature_element.py +++ b/finat/quadrature_element.py @@ -30,10 +30,8 @@ def degree(self): @cached_property def _entity_dofs(self): # Inspired by ffc/quadratureelement.py - entity_dofs = { - {entity: [] for entity in entities} - for dim, entities in iteritems(self.cell.get_topology()) - } + entity_dofs = {dim: {entity: [] for entity in entities} + for dim, entities in iteritems(self.cell.get_topology())} entity_dofs[self.cell.get_dimension()] = {0: list(range(self.space_dimension()))} return entity_dofs From 7c697357b655e765ca1f0f39b0a97c8765b446fe Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 3 Mar 2016 18:13:06 +0000 Subject: [PATCH 361/749] support Bessel functions Modified Bessel functions are FEniCS only due to dependency on Boost. --- gem/gem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 5f0a4ac6e..5625f06c1 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -286,12 +286,12 @@ class MathFunction(Scalar): __slots__ = ('name', 'children') __front__ = ('name',) - def __init__(self, name, argument): + def __init__(self, name, *args): assert isinstance(name, str) - assert not argument.shape + assert all(arg.shape == () for arg in args) self.name = name - self.children = argument, + self.children = args class MinValue(Scalar): From 8180e8d8495aa718558e44451ebf9b008f6b15e3 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 25 Apr 2017 17:22:49 +0100 Subject: [PATCH 362/749] add FiatElementBase.point_evaluation --- finat/fiat_elements.py | 114 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 2 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 0ceaa5ece..4d7834c11 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -1,10 +1,18 @@ from __future__ import absolute_import, print_function, division from six import iteritems -from .finiteelementbase import FiniteElementBase +from functools import reduce + +import numpy as np +import sympy as sp +from singledispatch import singledispatch + import FIAT +from FIAT.polynomial_set import mis, form_matrix_product + import gem -import numpy as np + +from finat.finiteelementbase import FiniteElementBase class FiatElementBase(FiniteElementBase): @@ -95,6 +103,66 @@ def basis_evaluation(self, order, ps, entity=None): result[alpha] = expr return result + def point_evaluation(self, order, refcoords, entity=None): + '''Return code for evaluating the element at an arbitrary points on + the reference element. + + :param order: return derivatives up to this order. + :param refcoords: GEM expression representing the coordinates + on the reference entity. Its shape must be + a vector with the correct dimension, its + free indices are arbitrary. + :param entity: the cell entity on which to tabulate. + ''' + assert isinstance(self._element, FIAT.CiarletElement) + cell = self.cell + + if entity is None: + entity = (cell.get_dimension(), 0) + entity_dim, entity_i = entity + + # Spatial dimension of the entity + esd = cell.construct_subelement(entity_dim).get_spatial_dimension() + assert isinstance(refcoords, gem.Node) and refcoords.shape == (esd,) + + # Coordinates on the reference entity (SymPy) + Xi = sp.symbols('X Y Z')[:esd] + # Coordinates on the reference cell + X = cell.get_entity_transform(entity_dim, entity_i)(Xi) + + # Evaluate expansion set at SymPy point + poly_set = self._element.get_nodal_basis() + degree = poly_set.get_embedded_degree() + base_values = poly_set.get_expansion_set().tabulate(degree, [X]) + m = len(base_values) + assert base_values.shape == (m, 1) + + # Convert SymPy expression to GEM + mapper = gem.node.Memoizer(sympy2gem) + mapper.bindings = {s: gem.Indexed(refcoords, (i,)) + for i, s in enumerate(X)} + base_values = gem.ListTensor(list(map(mapper, base_values.flat))) + + # Populate result dict, creating precomputed coefficient + # matrices for each derivative tuple. + result = {} + for i in range(order + 1): + for alpha in mis(cell.get_spatial_dimension(), i): + D = form_matrix_product(poly_set.get_dmats(), alpha) + table = np.dot(poly_set.get_coeffs(), np.transpose(D)) + assert table.shape[-1] == m + beta = tuple(gem.Index() for s in table.shape[:-1]) + k = gem.Index() + result[alpha] = gem.ComponentTensor( + gem.IndexSum( + gem.Product(gem.Indexed(gem.Literal(table), beta + (k,)), + gem.Indexed(base_values, (k,))), + (k,) + ), + beta + ) + return result + class Regge(FiatElementBase): # naturally tensor valued def __init__(self, cell, degree): @@ -161,3 +229,45 @@ def __init__(self, cell, degree): class NedelecSecondKind(VectorFiatElement): def __init__(self, cell, degree): super(NedelecSecondKind, self).__init__(FIAT.NedelecSecondKind(cell, degree)) + + +@singledispatch +def sympy2gem(node, self): + raise AssertionError("sympy node expected, got %s" % type(node)) + + +@sympy2gem.register(sp.Expr) +def sympy2gem_expr(node, self): + raise NotImplementedError("no handler for sympy node type %s" % type(node)) + + +@sympy2gem.register(sp.Add) +def sympy2gem_add(node, self): + return reduce(gem.Sum, map(self, node.args)) + + +@sympy2gem.register(sp.Mul) +def sympy2gem_mul(node, self): + return reduce(gem.Product, map(self, node.args)) + + +@sympy2gem.register(sp.Pow) +def sympy2gem_pow(node, self): + return gem.Power(*map(self, node.args)) + + +@sympy2gem.register(sp.Integer) +# @sympy2gem.register(int) +def sympy2gem_integer(node, self): + return gem.Literal(int(node)) + + +@sympy2gem.register(sp.Float) +# @sympy2gem.register(float) +def sympy2gem_float(node, self): + return gem.Literal(node) + + +@sympy2gem.register(sp.Symbol) +def sympy2gem_symbol(node, self): + return self.bindings[node] From 640b3d610bfe488e346194e3e4b1a9a698ac3610 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 25 Apr 2017 17:24:23 +0100 Subject: [PATCH 363/749] add TensorFiniteElement.point_evaluation --- finat/tensorfiniteelement.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/finat/tensorfiniteelement.py b/finat/tensorfiniteelement.py index 4d0fb9233..1db452c0e 100644 --- a/finat/tensorfiniteelement.py +++ b/finat/tensorfiniteelement.py @@ -97,3 +97,25 @@ def basis_evaluation(self, order, ps, entity=None): scalar_i + tensor_i + scalar_vi + tensor_vi ) return result + + def point_evaluation(self, order, point, entity=None): + # Old basis function and value indices + scalar_i = self._base_element.get_indices() + scalar_vi = self._base_element.get_value_indices() + + # New basis function and value indices + tensor_i = tuple(gem.Index(extent=d) for d in self._shape) + tensor_vi = tuple(gem.Index(extent=d) for d in self._shape) + + # Couple new basis function and value indices + deltas = reduce(gem.Product, (gem.Delta(j, k) + for j, k in zip(tensor_i, tensor_vi))) + + scalar_result = self._base_element.point_evaluation(order, point, entity) + result = {} + for alpha, expr in iteritems(scalar_result): + result[alpha] = gem.ComponentTensor( + gem.Product(deltas, gem.Indexed(expr, scalar_i + scalar_vi)), + scalar_i + tensor_i + scalar_vi + tensor_vi + ) + return result From 0f9b10859c672e36278f7df0e373e95b13cda25f Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 25 Apr 2017 17:56:49 +0100 Subject: [PATCH 364/749] add TensorProductElement.point_evaluation --- finat/tensor_product.py | 64 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/finat/tensor_product.py b/finat/tensor_product.py index f6613fa18..343b84edd 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -120,6 +120,70 @@ def basis_evaluation(self, order, ps, entity=None): ) return result + def point_evaluation(self, order, point, entity=None): + # Default entity + if entity is None: + entity = (self.cell.get_dimension(), 0) + entity_dim, entity_id = entity + + # Factor entity + assert isinstance(entity_dim, tuple) + assert len(entity_dim) == len(self.factors) + + shape = tuple(len(c.get_topology()[d]) + for c, d in zip(self.cell.cells, entity_dim)) + entities = list(zip(entity_dim, numpy.unravel_index(entity_id, shape))) + + # Split point expression + assert len(self.cell.cells) == len(entity_dim) + point_dims = [cell.construct_subelement(dim).get_spatial_dimension() + for cell, dim in zip(self.cell.cells, entity_dim)] + assert isinstance(point, gem.Node) and point.shape == (sum(point_dims),) + slices = TensorProductCell._split_slices(point_dims) + point_factors = [] + for s in slices: + point_factors.append(gem.ListTensor( + [gem.Indexed(point, (i,)) + for i in range(s.start, s.stop)] + )) + + # Subelement results + factor_results = [fe.point_evaluation(order, p_, e) + for fe, p_, e in zip(self.factors, point_factors, entities)] + + # Spatial dimension + dimension = self.cell.get_spatial_dimension() + + # A list of slices that are used to select dimensions + # corresponding to each subelement. + dim_slices = TensorProductCell._split_slices([c.get_spatial_dimension() + for c in self.cell.cells]) + + # A list of multiindices, one multiindex per subelement, each + # multiindex describing the shape of basis functions of the + # subelement. + alphas = [fe.get_indices() for fe in self.factors] + + result = {} + for derivative in range(order + 1): + for Delta in mis(dimension, derivative): + # Split the multiindex for the subelements + deltas = [Delta[s] for s in dim_slices] + # GEM scalars (can have free indices) for collecting + # the contributions from the subelements. + scalars = [] + for fr, delta, alpha in zip(factor_results, deltas, alphas): + # Turn basis shape to free indices, select the + # right derivative entry, and collect the result. + scalars.append(gem.Indexed(fr[delta], alpha)) + # Multiply the values from the subelements and wrap up + # non-point indices into shape. + result[Delta] = gem.ComponentTensor( + reduce(gem.Product, scalars), + tuple(chain(*alphas)) + ) + return result + def factor_point_set(product_cell, product_dim, point_set): """Factors a point set for the product element into a point sets for From 230a1d50ca2943e4aa5a734e5194ab2ad2f19612 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 25 Apr 2017 17:57:01 +0100 Subject: [PATCH 365/749] add QuadrilateralElement.point_evaluation --- finat/quadrilateral.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/finat/quadrilateral.py b/finat/quadrilateral.py index 9044e3697..4746f5ee3 100644 --- a/finat/quadrilateral.py +++ b/finat/quadrilateral.py @@ -75,6 +75,30 @@ def basis_evaluation(self, order, ps, entity=None): return self.product.basis_evaluation(order, ps, product_entity) + def point_evaluation(self, order, point, entity=None): + if entity is None: + entity = (2, 0) + + # Entity is provided in flattened form (d, i) + # We factor the entity and construct an appropriate + # entity id for a TensorProductCell: ((d1, d2), i) + entity_dim, entity_id = entity + if entity_dim == 2: + assert entity_id == 0 + product_entity = ((1, 1), 0) + elif entity_dim == 1: + facets = [((0, 1), 0), + ((0, 1), 1), + ((1, 0), 0), + ((1, 0), 1)] + product_entity = facets[entity_id] + elif entity_dim == 0: + raise NotImplementedError("Not implemented for 0 dimension entities") + else: + raise ValueError("Illegal entity dimension %s" % entity_dim) + + return self.product.point_evaluation(order, point, product_entity) + @property def index_shape(self): return self.product.index_shape From 81d81a82cc3de30900fddab69e22ee92dcdf4a64 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 26 Apr 2017 15:32:30 +0100 Subject: [PATCH 366/749] small typo I think --- finat/fiat_elements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 4d7834c11..5aacf4a8d 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -140,7 +140,7 @@ def point_evaluation(self, order, refcoords, entity=None): # Convert SymPy expression to GEM mapper = gem.node.Memoizer(sympy2gem) mapper.bindings = {s: gem.Indexed(refcoords, (i,)) - for i, s in enumerate(X)} + for i, s in enumerate(Xi)} base_values = gem.ListTensor(list(map(mapper, base_values.flat))) # Populate result dict, creating precomputed coefficient From 1ca2280676555028a94692cf31587a46efbb8139 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 26 Apr 2017 15:38:24 +0100 Subject: [PATCH 367/749] accept non-SymPy literals Required for the FIAT.FiniteElement wrappers. --- finat/fiat_elements.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 5aacf4a8d..d4025309a 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -257,13 +257,13 @@ def sympy2gem_pow(node, self): @sympy2gem.register(sp.Integer) -# @sympy2gem.register(int) +@sympy2gem.register(int) def sympy2gem_integer(node, self): - return gem.Literal(int(node)) + return gem.Literal(node) @sympy2gem.register(sp.Float) -# @sympy2gem.register(float) +@sympy2gem.register(float) def sympy2gem_float(node, self): return gem.Literal(node) From 49fefb52a582e5fde414dea488ba9b4d5bb14c3a Mon Sep 17 00:00:00 2001 From: tj-sun Date: Wed, 3 May 2017 13:47:54 +0100 Subject: [PATCH 368/749] Coffee migration (#98) Migration of factorisation for loop optimisation from COFFEE. This change unconditionally performs argument factorisation on the input expression. --- gem/optimise.py | 83 ++++++++++++++++++++++++++++++++++------------ gem/refactorise.py | 3 ++ gem/utils.py | 17 ++++++++++ 3 files changed, 81 insertions(+), 22 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index aa0122215..28c03a67f 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -2,16 +2,15 @@ expressions.""" from __future__ import absolute_import, print_function, division -from six import itervalues from six.moves import filter, map, zip -from collections import OrderedDict from functools import reduce from itertools import combinations, permutations import numpy from singledispatch import singledispatch +from gem.utils import groupby from gem.node import Memoizer, MemoizerArg, reuse_if_untouched, reuse_if_untouched_arg from gem.gem import (Node, Terminal, Failure, Identity, Literal, Zero, Product, Sum, Comparison, Conditional, Division, @@ -57,6 +56,31 @@ def ffc_rounding(expression, epsilon): return mapper(expression) +@singledispatch +def _replace_division(node, self): + """Replace division with multiplication + + :param node: root of expression + :param self: function for recursive calls + """ + raise AssertionError("cannot handle type %s" % type(node)) + + +_replace_division.register(Node)(reuse_if_untouched) + + +@_replace_division.register(Division) +def _replace_division_division(node, self): + a, b = node.children + return Product(self(a), Division(one, self(b))) + + +def replace_division(expressions): + """Replace divisions with multiplications in expressions""" + mapper = Memoizer(_replace_division) + return list(map(mapper, expressions)) + + @singledispatch def replace_indices(node, self, subst): """Replace free indices in a GEM expression. @@ -229,34 +253,38 @@ def delta_elimination(sum_indices, factors): return sum_indices, factors -def associate_product(factors): - """Apply associativity rules to construct an operation-minimal product tree. +def associate(operator, operands): + """Apply associativity rules to construct an operation-minimal expression tree. For best performance give factors that have different set of free indices. + + :arg operator: associative binary operator + :arg operands: list of operands + + :returns: (reduced expression, # of floating-point operations) """ - if len(factors) > 32: + if len(operands) > 32: # O(N^3) algorithm raise NotImplementedError("Not expected such a complicated expression!") def count(pair): - """Operation count to multiply a pair of GEM expressions""" + """Operation count to reduce a pair of GEM expressions""" a, b = pair extents = [i.extent for i in set().union(a.free_indices, b.free_indices)] return numpy.prod(extents, dtype=int) - factors = list(factors) # copy for in-place modifications flops = 0 - while len(factors) > 1: - # Greedy algorithm: choose a pair of factors that are the - # cheapest to multiply. - a, b = min(combinations(factors, 2), key=count) + while len(operands) > 1: + # Greedy algorithm: choose a pair of operands that are the + # cheapest to reduce. + a, b = min(combinations(operands, 2), key=count) flops += count((a, b)) # Remove chosen factors, append their product - factors.remove(a) - factors.remove(b) - factors.append(Product(a, b)) - product, = factors - return product, flops + operands.remove(a) + operands.remove(b) + operands.append(operator(a, b)) + result, = operands + return result, flops def sum_factorise(sum_indices, factors): @@ -274,10 +302,8 @@ def sum_factorise(sum_indices, factors): raise NotImplementedError("Too many indices for sum factorisation!") # Form groups by free indices - groups = OrderedDict() - for factor in factors: - groups.setdefault(factor.free_indices, []).append(factor) - groups = [reduce(Product, terms) for terms in itervalues(groups)] + groups = groupby(factors, key=lambda f: f.free_indices) + groups = [reduce(Product, terms) for _, terms in groups] # Sum factorisation expression = None @@ -294,7 +320,7 @@ def sum_factorise(sum_indices, factors): deferred = [t for t in terms if sum_index not in t.free_indices] # Optimise associativity - product, flops_ = associate_product(contract) + product, flops_ = associate(Product, contract) term = IndexSum(product, (sum_index,)) flops += flops_ + numpy.prod([i.extent for i in product.free_indices], dtype=int) @@ -304,7 +330,7 @@ def sum_factorise(sum_indices, factors): # If some contraction indices were independent, then we may # still have several terms at this point. - expr, flops_ = associate_product(terms) + expr, flops_ = associate(Product, terms) flops += flops_ if flops < best_flops: @@ -314,6 +340,19 @@ def sum_factorise(sum_indices, factors): return expression +def make_sum(summands): + """Constructs an operation-minimal sum of GEM expressions.""" + groups = groupby(summands, key=lambda f: f.free_indices) + summands = [reduce(Sum, terms) for _, terms in groups] + result, flops = associate(Sum, summands) + return result + + +def make_product(factors, sum_indices=()): + """Constructs an operation-minimal (tensor) product of GEM expressions.""" + return sum_factorise(sum_indices, factors) + + def traverse_product(expression, stop_at=None): """Traverses a product tree and collects factors, also descending into tensor contractions (IndexSum). The nominators of divisions are diff --git a/gem/refactorise.py b/gem/refactorise.py index ec6234a11..4a8e22a7b 100644 --- a/gem/refactorise.py +++ b/gem/refactorise.py @@ -61,6 +61,9 @@ def __init__(self): # (ordered sum_indices, ordered atomics) self.ordering = OrderedDict() + def __len__(self): + return len(self.ordering) + def add(self, sum_indices, atomics, rest): """Updates the :py:class:`MonomialSum` adding a new monomial.""" sum_indices = tuple(sum_indices) diff --git a/gem/utils.py b/gem/utils.py index b2804d16f..cf37a705a 100644 --- a/gem/utils.py +++ b/gem/utils.py @@ -1,4 +1,5 @@ from __future__ import absolute_import, print_function, division +from six import viewitems import collections @@ -58,6 +59,22 @@ def discard(self, value): self._set.discard(value) +def groupby(iterable, key=None): + """Groups objects by their keys. + + :arg iterable: an iterable + :arg key: key function + + :returns: list of (group key, list of group members) pairs + """ + if key is None: + key = lambda x: x + groups = collections.OrderedDict() + for elem in iterable: + groups.setdefault(key(elem), []).append(elem) + return viewitems(groups) + + def make_proxy_class(name, cls): """Constructs a proxy class for a given class. Instance attributes are supposed to be listed e.g. with the unset_attribute decorator, From 3e122aebe44c0ce6c70c283800db001997e91676 Mon Sep 17 00:00:00 2001 From: tj-sun Date: Tue, 9 May 2017 18:37:29 +0100 Subject: [PATCH 369/749] Rewrite Conditional nodes before optimisation (#120) Fixes #119. --- gem/optimise.py | 34 ++++++++++++++++++++++++++++++++++ gem/refactorise.py | 7 ++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/gem/optimise.py b/gem/optimise.py index 28c03a67f..2eaaf8b6d 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -531,3 +531,37 @@ def aggressive_unroll(expression): expression, = unroll_indexsum((expression,), predicate=lambda index: True) expression, = remove_componenttensors((expression,)) return expression + + +@singledispatch +def _expand_conditional(node, self): + raise AssertionError("cannot handle type %s" % type(node)) + + +_expand_conditional.register(Node)(reuse_if_untouched) + + +@_expand_conditional.register(Conditional) +def _expand_conditional_conditional(node, self): + if self.predicate(node): + condition, then, else_ = map(self, node.children) + return Sum(Product(Conditional(condition, one, Zero()), then), + Product(Conditional(condition, Zero(), one), else_)) + else: + return reuse_if_untouched(node, self) + + +def expand_conditional(expressions, predicate): + """Applies the following substitution rule on selected :py:class:`Conditional`s: + + Conditional(a, b, c) => Conditional(a, 1, 0)*b + Conditional(a, 0, 1)*c + + :arg expressions: expression DAG roots + :arg predicate: a predicate function on :py:class:`Conditional`s to determine + whether to apply the substitution rule or not + + :returns: expression DAG roots with some :py:class:`Conditional` nodes expanded + """ + mapper = Memoizer(_expand_conditional) + mapper.predicate = predicate + return list(map(mapper, expressions)) diff --git a/gem/refactorise.py b/gem/refactorise.py index 4a8e22a7b..aaea93f9d 100644 --- a/gem/refactorise.py +++ b/gem/refactorise.py @@ -11,7 +11,8 @@ from gem.node import Memoizer, traversal from gem.gem import Node, Zero, Product, Sum, Indexed, ListTensor, one from gem.optimise import (remove_componenttensors, sum_factorise, - traverse_product, traverse_sum, unroll_indexsum) + traverse_product, traverse_sum, unroll_indexsum, + expand_conditional) # Refactorisation labels @@ -229,6 +230,10 @@ def collect_monomials(expressions, classifier): predicate=lambda i: i in must_unroll) expressions = remove_componenttensors(expressions) + # Expand Conditional nodes which are COMPOUND + conditional_predicate = lambda node: classifier(node) == COMPOUND + expressions = expand_conditional(expressions, conditional_predicate) + # Finally, refactorise expressions mapper = Memoizer(_collect_monomials) mapper.classifier = classifier From b82f124c2ae9aff0e1158df8431ad1f287d128f6 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 26 Apr 2017 16:04:20 +0100 Subject: [PATCH 370/749] implement constant folding for gem.Power --- gem/gem.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gem/gem.py b/gem/gem.py index 5625f06c1..1f74200f0 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -269,7 +269,7 @@ def __new__(cls, base, exponent): assert not base.shape assert not exponent.shape - # Zero folding + # Constant folding if isinstance(base, Zero): if isinstance(exponent, Zero): raise ValueError("cannot solve 0^0") @@ -277,6 +277,9 @@ def __new__(cls, base, exponent): elif isinstance(exponent, Zero): return one + if isinstance(base, Constant) and isinstance(exponent, Constant): + return Literal(base.value ** exponent.value) + self = super(Power, cls).__new__(cls) self.children = base, exponent return self From d3707fc2422c7af8cb19f60a1398f98b17d1cbae Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 10 May 2017 15:07:59 +0100 Subject: [PATCH 371/749] make point_evaluation an obligatory method of finite elements --- finat/finiteelementbase.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index ee1745968..3b7892b5a 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -83,6 +83,19 @@ def basis_evaluation(self, order, ps, entity=None): :param entity: the cell entity on which to tabulate. ''' + @abstractmethod + def point_evaluation(self, order, refcoords, entity=None): + '''Return code for evaluating the element at an arbitrary points on + the reference element. + + :param order: return derivatives up to this order. + :param refcoords: GEM expression representing the coordinates + on the reference entity. Its shape must be + a vector with the correct dimension, its + free indices are arbitrary. + :param entity: the cell entity on which to tabulate. + ''' + def entity_support_dofs(elem, entity_dim): """Return the map of entity id to the degrees of freedom for which From fa06840009065b7a1292a0a055c59b63d238b9ae Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 10 May 2017 15:07:16 +0100 Subject: [PATCH 372/749] move sympy2gem to a separate module --- finat/fiat_elements.py | 46 +------------------------------------ finat/sympy2gem.py | 51 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 45 deletions(-) create mode 100644 finat/sympy2gem.py diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index d4025309a..5c9b88de9 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -1,11 +1,8 @@ from __future__ import absolute_import, print_function, division from six import iteritems -from functools import reduce - import numpy as np import sympy as sp -from singledispatch import singledispatch import FIAT from FIAT.polynomial_set import mis, form_matrix_product @@ -13,6 +10,7 @@ import gem from finat.finiteelementbase import FiniteElementBase +from finat.sympy2gem import sympy2gem class FiatElementBase(FiniteElementBase): @@ -229,45 +227,3 @@ def __init__(self, cell, degree): class NedelecSecondKind(VectorFiatElement): def __init__(self, cell, degree): super(NedelecSecondKind, self).__init__(FIAT.NedelecSecondKind(cell, degree)) - - -@singledispatch -def sympy2gem(node, self): - raise AssertionError("sympy node expected, got %s" % type(node)) - - -@sympy2gem.register(sp.Expr) -def sympy2gem_expr(node, self): - raise NotImplementedError("no handler for sympy node type %s" % type(node)) - - -@sympy2gem.register(sp.Add) -def sympy2gem_add(node, self): - return reduce(gem.Sum, map(self, node.args)) - - -@sympy2gem.register(sp.Mul) -def sympy2gem_mul(node, self): - return reduce(gem.Product, map(self, node.args)) - - -@sympy2gem.register(sp.Pow) -def sympy2gem_pow(node, self): - return gem.Power(*map(self, node.args)) - - -@sympy2gem.register(sp.Integer) -@sympy2gem.register(int) -def sympy2gem_integer(node, self): - return gem.Literal(node) - - -@sympy2gem.register(sp.Float) -@sympy2gem.register(float) -def sympy2gem_float(node, self): - return gem.Literal(node) - - -@sympy2gem.register(sp.Symbol) -def sympy2gem_symbol(node, self): - return self.bindings[node] diff --git a/finat/sympy2gem.py b/finat/sympy2gem.py new file mode 100644 index 000000000..a3d5bb66f --- /dev/null +++ b/finat/sympy2gem.py @@ -0,0 +1,51 @@ +from __future__ import absolute_import, print_function, division +from six.moves import map + +from functools import reduce + +from singledispatch import singledispatch +import sympy + +import gem + + +@singledispatch +def sympy2gem(node, self): + raise AssertionError("sympy node expected, got %s" % type(node)) + + +@sympy2gem.register(sympy.Expr) +def sympy2gem_expr(node, self): + raise NotImplementedError("no handler for sympy node type %s" % type(node)) + + +@sympy2gem.register(sympy.Add) +def sympy2gem_add(node, self): + return reduce(gem.Sum, map(self, node.args)) + + +@sympy2gem.register(sympy.Mul) +def sympy2gem_mul(node, self): + return reduce(gem.Product, map(self, node.args)) + + +@sympy2gem.register(sympy.Pow) +def sympy2gem_pow(node, self): + return gem.Power(*map(self, node.args)) + + +@sympy2gem.register(sympy.Integer) +@sympy2gem.register(int) +def sympy2gem_integer(node, self): + return gem.Literal(node) + + +@sympy2gem.register(sympy.Float) +@sympy2gem.register(float) +def sympy2gem_float(node, self): + return gem.Literal(node) + + +@sympy2gem.register(sympy.Symbol) +def sympy2gem_symbol(node, self): + return self.bindings[node] From 021bbc22caa761ee90b08cfcdd48b604bc344811 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 10 May 2017 16:14:50 +0100 Subject: [PATCH 373/749] implement point evaluation for generic FIAT elements --- finat/fiat_elements.py | 126 +++++++++++++++++++++++++++++------------ 1 file changed, 90 insertions(+), 36 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 5c9b88de9..54928e580 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -3,6 +3,7 @@ import numpy as np import sympy as sp +from singledispatch import singledispatch import FIAT from FIAT.polynomial_set import mis, form_matrix_product @@ -112,54 +113,107 @@ def point_evaluation(self, order, refcoords, entity=None): free indices are arbitrary. :param entity: the cell entity on which to tabulate. ''' - assert isinstance(self._element, FIAT.CiarletElement) - cell = self.cell - if entity is None: - entity = (cell.get_dimension(), 0) + entity = (self.cell.get_dimension(), 0) entity_dim, entity_i = entity # Spatial dimension of the entity - esd = cell.construct_subelement(entity_dim).get_spatial_dimension() + esd = self.cell.construct_subelement(entity_dim).get_spatial_dimension() assert isinstance(refcoords, gem.Node) and refcoords.shape == (esd,) - # Coordinates on the reference entity (SymPy) - Xi = sp.symbols('X Y Z')[:esd] - # Coordinates on the reference cell - X = cell.get_entity_transform(entity_dim, entity_i)(Xi) + # Dispatch on FIAT element class + return point_evaluation(self._element, self, order, refcoords, (entity_dim, entity_i)) + + +@singledispatch +def point_evaluation(element, self, order, refcoords, entity): + raise AssertionError("FIAT element expected!") + + +@point_evaluation.register(FIAT.FiniteElement) +def point_evaluation_generic(element, self, order, refcoords, entity): + # Coordinates on the reference entity (SymPy) + esd, = refcoords.shape + Xi = sp.symbols('X Y Z')[:esd] - # Evaluate expansion set at SymPy point - poly_set = self._element.get_nodal_basis() - degree = poly_set.get_embedded_degree() - base_values = poly_set.get_expansion_set().tabulate(degree, [X]) - m = len(base_values) - assert base_values.shape == (m, 1) + space_dimension = element.space_dimension() + value_size = np.prod(element.value_shape(), dtype=int) + fiat_result = element.tabulate(order, [Xi], entity) + result = {} + for alpha, fiat_table in iteritems(fiat_result): + if isinstance(fiat_table, Exception): + result[alpha] = gem.Failure(self.index_shape + self.value_shape, fiat_table) + continue # Convert SymPy expression to GEM mapper = gem.node.Memoizer(sympy2gem) mapper.bindings = {s: gem.Indexed(refcoords, (i,)) for i, s in enumerate(Xi)} - base_values = gem.ListTensor(list(map(mapper, base_values.flat))) - - # Populate result dict, creating precomputed coefficient - # matrices for each derivative tuple. - result = {} - for i in range(order + 1): - for alpha in mis(cell.get_spatial_dimension(), i): - D = form_matrix_product(poly_set.get_dmats(), alpha) - table = np.dot(poly_set.get_coeffs(), np.transpose(D)) - assert table.shape[-1] == m - beta = tuple(gem.Index() for s in table.shape[:-1]) - k = gem.Index() - result[alpha] = gem.ComponentTensor( - gem.IndexSum( - gem.Product(gem.Indexed(gem.Literal(table), beta + (k,)), - gem.Indexed(base_values, (k,))), - (k,) - ), - beta - ) - return result + gem_table = np.vectorize(mapper)(fiat_table) + + table_roll = gem_table.reshape(space_dimension, value_size).transpose() + + exprs = [] + for table in table_roll: + exprs.append(gem.ListTensor(table.reshape(self.index_shape))) + if self.value_shape: + beta = self.get_indices() + zeta = self.get_value_indices() + result[alpha] = gem.ComponentTensor( + gem.Indexed( + gem.ListTensor(np.array( + [gem.Indexed(expr, beta) for expr in exprs] + ).reshape(self.value_shape)), + zeta), + beta + zeta + ) + else: + expr, = exprs + result[alpha] = expr + return result + + +@point_evaluation.register(FIAT.CiarletElement) +def point_evaluation_ciarlet(element, self, order, refcoords, entity): + # Coordinates on the reference entity (SymPy) + esd, = refcoords.shape + Xi = sp.symbols('X Y Z')[:esd] + + # Coordinates on the reference cell + X = self.cell.get_entity_transform(*entity)(Xi) + + # Evaluate expansion set at SymPy point + poly_set = element.get_nodal_basis() + degree = poly_set.get_embedded_degree() + base_values = poly_set.get_expansion_set().tabulate(degree, [X]) + m = len(base_values) + assert base_values.shape == (m, 1) + + # Convert SymPy expression to GEM + mapper = gem.node.Memoizer(sympy2gem) + mapper.bindings = {s: gem.Indexed(refcoords, (i,)) + for i, s in enumerate(Xi)} + base_values = gem.ListTensor(list(map(mapper, base_values.flat))) + + # Populate result dict, creating precomputed coefficient + # matrices for each derivative tuple. + result = {} + for i in range(order + 1): + for alpha in mis(self.cell.get_spatial_dimension(), i): + D = form_matrix_product(poly_set.get_dmats(), alpha) + table = np.dot(poly_set.get_coeffs(), np.transpose(D)) + assert table.shape[-1] == m + beta = tuple(gem.Index() for s in table.shape[:-1]) + k = gem.Index() + result[alpha] = gem.ComponentTensor( + gem.IndexSum( + gem.Product(gem.Indexed(gem.Literal(table), beta + (k,)), + gem.Indexed(base_values, (k,))), + (k,) + ), + beta + ) + return result class Regge(FiatElementBase): # naturally tensor valued From 745a9de1f67178db85c648209bbefb4dbc9e2e73 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 10 May 2017 17:11:36 +0100 Subject: [PATCH 374/749] deduplicate code --- finat/quadrilateral.py | 69 +++++++++++++---------------------- finat/tensor_product.py | 70 ++++++++++-------------------------- finat/tensorfiniteelement.py | 29 ++++----------- 3 files changed, 51 insertions(+), 117 deletions(-) diff --git a/finat/quadrilateral.py b/finat/quadrilateral.py index 4746f5ee3..394cc1a38 100644 --- a/finat/quadrilateral.py +++ b/finat/quadrilateral.py @@ -52,52 +52,10 @@ def basis_evaluation(self, order, ps, entity=None): :param ps: the point set object. :param entity: the cell entity on which to tabulate. """ - if entity is None: - entity = (2, 0) - - # Entity is provided in flattened form (d, i) - # We factor the entity and construct an appropriate - # entity id for a TensorProductCell: ((d1, d2), i) - entity_dim, entity_id = entity - if entity_dim == 2: - assert entity_id == 0 - product_entity = ((1, 1), 0) - elif entity_dim == 1: - facets = [((0, 1), 0), - ((0, 1), 1), - ((1, 0), 0), - ((1, 0), 1)] - product_entity = facets[entity_id] - elif entity_dim == 0: - raise NotImplementedError("Not implemented for 0 dimension entities") - else: - raise ValueError("Illegal entity dimension %s" % entity_dim) - - return self.product.basis_evaluation(order, ps, product_entity) + return self.product.basis_evaluation(order, ps, productise(entity)) def point_evaluation(self, order, point, entity=None): - if entity is None: - entity = (2, 0) - - # Entity is provided in flattened form (d, i) - # We factor the entity and construct an appropriate - # entity id for a TensorProductCell: ((d1, d2), i) - entity_dim, entity_id = entity - if entity_dim == 2: - assert entity_id == 0 - product_entity = ((1, 1), 0) - elif entity_dim == 1: - facets = [((0, 1), 0), - ((0, 1), 1), - ((1, 0), 0), - ((1, 0), 1)] - product_entity = facets[entity_id] - elif entity_dim == 0: - raise NotImplementedError("Not implemented for 0 dimension entities") - else: - raise ValueError("Illegal entity dimension %s" % entity_dim) - - return self.product.point_evaluation(order, point, product_entity) + return self.product.point_evaluation(order, point, productise(entity)) @property def index_shape(self): @@ -106,3 +64,26 @@ def index_shape(self): @property def value_shape(self): return self.product.value_shape + + +def productise(entity): + if entity is None: + entity = (2, 0) + + # Entity is provided in flattened form (d, i) + # We factor the entity and construct an appropriate + # entity id for a TensorProductCell: ((d1, d2), i) + entity_dim, entity_id = entity + if entity_dim == 2: + assert entity_id == 0 + return ((1, 1), 0) + elif entity_dim == 1: + facets = [((0, 1), 0), + ((0, 1), 1), + ((1, 0), 0), + ((1, 0), 1)] + return facets[entity_id] + elif entity_dim == 0: + raise NotImplementedError("Not implemented for 0 dimension entities") + else: + raise ValueError("Illegal entity dimension %s" % entity_dim) diff --git a/finat/tensor_product.py b/finat/tensor_product.py index 343b84edd..8845ab6d5 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -66,7 +66,7 @@ def index_shape(self): def value_shape(self): return () # TODO: non-scalar factors not supported yet - def basis_evaluation(self, order, ps, entity=None): + def _factor_entity(self, entity): # Default entity if entity is None: entity = (self.cell.get_dimension(), 0) @@ -79,17 +79,15 @@ def basis_evaluation(self, order, ps, entity=None): shape = tuple(len(c.get_topology()[d]) for c, d in zip(self.cell.cells, entity_dim)) entities = list(zip(entity_dim, numpy.unravel_index(entity_id, shape))) + return entities - # Factor point set - ps_factors = factor_point_set(self.cell, entity_dim, ps) - - # Subelement results - factor_results = [fe.basis_evaluation(order, ps_, e) - for fe, ps_, e in zip(self.factors, ps_factors, entities)] - + def _merge_evaluations(self, factor_results): # Spatial dimension dimension = self.cell.get_spatial_dimension() + # Derivative order + order = max(map(sum, chain(*factor_results))) + # A list of slices that are used to select dimensions # corresponding to each subelement. dim_slices = TensorProductCell._split_slices([c.get_spatial_dimension() @@ -120,19 +118,20 @@ def basis_evaluation(self, order, ps, entity=None): ) return result - def point_evaluation(self, order, point, entity=None): - # Default entity - if entity is None: - entity = (self.cell.get_dimension(), 0) - entity_dim, entity_id = entity + def basis_evaluation(self, order, ps, entity=None): + entities = self._factor_entity(entity) + entity_dim, _ = zip(*entities) - # Factor entity - assert isinstance(entity_dim, tuple) - assert len(entity_dim) == len(self.factors) + ps_factors = factor_point_set(self.cell, entity_dim, ps) - shape = tuple(len(c.get_topology()[d]) - for c, d in zip(self.cell.cells, entity_dim)) - entities = list(zip(entity_dim, numpy.unravel_index(entity_id, shape))) + factor_results = [fe.basis_evaluation(order, ps_, e) + for fe, ps_, e in zip(self.factors, ps_factors, entities)] + + return self._merge_evaluations(factor_results) + + def point_evaluation(self, order, point, entity=None): + entities = self._factor_entity(entity) + entity_dim, _ = zip(*entities) # Split point expression assert len(self.cell.cells) == len(entity_dim) @@ -151,38 +150,7 @@ def point_evaluation(self, order, point, entity=None): factor_results = [fe.point_evaluation(order, p_, e) for fe, p_, e in zip(self.factors, point_factors, entities)] - # Spatial dimension - dimension = self.cell.get_spatial_dimension() - - # A list of slices that are used to select dimensions - # corresponding to each subelement. - dim_slices = TensorProductCell._split_slices([c.get_spatial_dimension() - for c in self.cell.cells]) - - # A list of multiindices, one multiindex per subelement, each - # multiindex describing the shape of basis functions of the - # subelement. - alphas = [fe.get_indices() for fe in self.factors] - - result = {} - for derivative in range(order + 1): - for Delta in mis(dimension, derivative): - # Split the multiindex for the subelements - deltas = [Delta[s] for s in dim_slices] - # GEM scalars (can have free indices) for collecting - # the contributions from the subelements. - scalars = [] - for fr, delta, alpha in zip(factor_results, deltas, alphas): - # Turn basis shape to free indices, select the - # right derivative entry, and collect the result. - scalars.append(gem.Indexed(fr[delta], alpha)) - # Multiply the values from the subelements and wrap up - # non-point indices into shape. - result[Delta] = gem.ComponentTensor( - reduce(gem.Product, scalars), - tuple(chain(*alphas)) - ) - return result + return self._merge_evaluations(factor_results) def factor_point_set(product_cell, product_dim, point_set): diff --git a/finat/tensorfiniteelement.py b/finat/tensorfiniteelement.py index 1db452c0e..9e7d9610c 100644 --- a/finat/tensorfiniteelement.py +++ b/finat/tensorfiniteelement.py @@ -77,28 +77,14 @@ def basis_evaluation(self, order, ps, entity=None): \nabla\boldsymbol\phi_{(\epsilon \gamma \zeta) (i \alpha \beta) q} = \delta_{\alpha \epsilon} \deta{\beta \gamma}\nabla\phi_{\zeta i q} """ - # Old basis function and value indices - scalar_i = self._base_element.get_indices() - scalar_vi = self._base_element.get_value_indices() - - # New basis function and value indices - tensor_i = tuple(gem.Index(extent=d) for d in self._shape) - tensor_vi = tuple(gem.Index(extent=d) for d in self._shape) - - # Couple new basis function and value indices - deltas = reduce(gem.Product, (gem.Delta(j, k) - for j, k in zip(tensor_i, tensor_vi))) - - scalar_result = self._base_element.basis_evaluation(order, ps, entity) - result = {} - for alpha, expr in iteritems(scalar_result): - result[alpha] = gem.ComponentTensor( - gem.Product(deltas, gem.Indexed(expr, scalar_i + scalar_vi)), - scalar_i + tensor_i + scalar_vi + tensor_vi - ) - return result + scalar_evaluation = self._base_element.basis_evaluation + return self._tensorise(scalar_evaluation(order, ps, entity)) def point_evaluation(self, order, point, entity=None): + scalar_evaluation = self._base_element.point_evaluation + return self._tensorise(scalar_evaluation(order, point, entity)) + + def _tensorise(self, scalar_evaluation): # Old basis function and value indices scalar_i = self._base_element.get_indices() scalar_vi = self._base_element.get_value_indices() @@ -111,9 +97,8 @@ def point_evaluation(self, order, point, entity=None): deltas = reduce(gem.Product, (gem.Delta(j, k) for j, k in zip(tensor_i, tensor_vi))) - scalar_result = self._base_element.point_evaluation(order, point, entity) result = {} - for alpha, expr in iteritems(scalar_result): + for alpha, expr in iteritems(scalar_evaluation): result[alpha] = gem.ComponentTensor( gem.Product(deltas, gem.Indexed(expr, scalar_i + scalar_vi)), scalar_i + tensor_i + scalar_vi + tensor_vi From 83033d63a32ab64c7dc80e15212f58503389906d Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 11 May 2017 11:53:16 +0100 Subject: [PATCH 375/749] remove self --- finat/fiat_elements.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 54928e580..fc7289186 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -122,16 +122,16 @@ def point_evaluation(self, order, refcoords, entity=None): assert isinstance(refcoords, gem.Node) and refcoords.shape == (esd,) # Dispatch on FIAT element class - return point_evaluation(self._element, self, order, refcoords, (entity_dim, entity_i)) + return point_evaluation(self._element, order, refcoords, (entity_dim, entity_i)) @singledispatch -def point_evaluation(element, self, order, refcoords, entity): +def point_evaluation(element, order, refcoords, entity): raise AssertionError("FIAT element expected!") @point_evaluation.register(FIAT.FiniteElement) -def point_evaluation_generic(element, self, order, refcoords, entity): +def point_evaluation_generic(element, order, refcoords, entity): # Coordinates on the reference entity (SymPy) esd, = refcoords.shape Xi = sp.symbols('X Y Z')[:esd] @@ -142,7 +142,7 @@ def point_evaluation_generic(element, self, order, refcoords, entity): result = {} for alpha, fiat_table in iteritems(fiat_result): if isinstance(fiat_table, Exception): - result[alpha] = gem.Failure(self.index_shape + self.value_shape, fiat_table) + result[alpha] = gem.Failure((space_dimension,) + element.value_shape(), fiat_table) continue # Convert SymPy expression to GEM @@ -155,15 +155,16 @@ def point_evaluation_generic(element, self, order, refcoords, entity): exprs = [] for table in table_roll: - exprs.append(gem.ListTensor(table.reshape(self.index_shape))) - if self.value_shape: - beta = self.get_indices() - zeta = self.get_value_indices() + exprs.append(gem.ListTensor(table.reshape(space_dimension))) + if element.value_shape(): + beta = (gem.Index(extent=space_dimension),) + zeta = tuple(gem.Index(extent=d) + for d in element.value_shape()) result[alpha] = gem.ComponentTensor( gem.Indexed( gem.ListTensor(np.array( [gem.Indexed(expr, beta) for expr in exprs] - ).reshape(self.value_shape)), + ).reshape(element.value_shape())), zeta), beta + zeta ) @@ -174,13 +175,14 @@ def point_evaluation_generic(element, self, order, refcoords, entity): @point_evaluation.register(FIAT.CiarletElement) -def point_evaluation_ciarlet(element, self, order, refcoords, entity): +def point_evaluation_ciarlet(element, order, refcoords, entity): # Coordinates on the reference entity (SymPy) esd, = refcoords.shape Xi = sp.symbols('X Y Z')[:esd] # Coordinates on the reference cell - X = self.cell.get_entity_transform(*entity)(Xi) + cell = element.get_reference_element() + X = cell.get_entity_transform(*entity)(Xi) # Evaluate expansion set at SymPy point poly_set = element.get_nodal_basis() @@ -199,7 +201,7 @@ def point_evaluation_ciarlet(element, self, order, refcoords, entity): # matrices for each derivative tuple. result = {} for i in range(order + 1): - for alpha in mis(self.cell.get_spatial_dimension(), i): + for alpha in mis(cell.get_spatial_dimension(), i): D = form_matrix_product(poly_set.get_dmats(), alpha) table = np.dot(poly_set.get_coeffs(), np.transpose(D)) assert table.shape[-1] == m From 213dbab2842a5d0ff78b003fd6e0808ba3b8a9b8 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 11 May 2017 11:53:29 +0100 Subject: [PATCH 376/749] call it a fiat_element --- finat/fiat_elements.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index fc7289186..0dd99e239 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -126,23 +126,23 @@ def point_evaluation(self, order, refcoords, entity=None): @singledispatch -def point_evaluation(element, order, refcoords, entity): +def point_evaluation(fiat_element, order, refcoords, entity): raise AssertionError("FIAT element expected!") @point_evaluation.register(FIAT.FiniteElement) -def point_evaluation_generic(element, order, refcoords, entity): +def point_evaluation_generic(fiat_element, order, refcoords, entity): # Coordinates on the reference entity (SymPy) esd, = refcoords.shape Xi = sp.symbols('X Y Z')[:esd] - space_dimension = element.space_dimension() - value_size = np.prod(element.value_shape(), dtype=int) - fiat_result = element.tabulate(order, [Xi], entity) + space_dimension = fiat_element.space_dimension() + value_size = np.prod(fiat_element.value_shape(), dtype=int) + fiat_result = fiat_element.tabulate(order, [Xi], entity) result = {} for alpha, fiat_table in iteritems(fiat_result): if isinstance(fiat_table, Exception): - result[alpha] = gem.Failure((space_dimension,) + element.value_shape(), fiat_table) + result[alpha] = gem.Failure((space_dimension,) + fiat_element.value_shape(), fiat_table) continue # Convert SymPy expression to GEM @@ -156,15 +156,15 @@ def point_evaluation_generic(element, order, refcoords, entity): exprs = [] for table in table_roll: exprs.append(gem.ListTensor(table.reshape(space_dimension))) - if element.value_shape(): + if fiat_element.value_shape(): beta = (gem.Index(extent=space_dimension),) zeta = tuple(gem.Index(extent=d) - for d in element.value_shape()) + for d in fiat_element.value_shape()) result[alpha] = gem.ComponentTensor( gem.Indexed( gem.ListTensor(np.array( [gem.Indexed(expr, beta) for expr in exprs] - ).reshape(element.value_shape())), + ).reshape(fiat_element.value_shape())), zeta), beta + zeta ) @@ -175,17 +175,17 @@ def point_evaluation_generic(element, order, refcoords, entity): @point_evaluation.register(FIAT.CiarletElement) -def point_evaluation_ciarlet(element, order, refcoords, entity): +def point_evaluation_ciarlet(fiat_element, order, refcoords, entity): # Coordinates on the reference entity (SymPy) esd, = refcoords.shape Xi = sp.symbols('X Y Z')[:esd] # Coordinates on the reference cell - cell = element.get_reference_element() + cell = fiat_element.get_reference_element() X = cell.get_entity_transform(*entity)(Xi) # Evaluate expansion set at SymPy point - poly_set = element.get_nodal_basis() + poly_set = fiat_element.get_nodal_basis() degree = poly_set.get_embedded_degree() base_values = poly_set.get_expansion_set().tabulate(degree, [X]) m = len(base_values) From 6dfeb8cdd535d7896b9dbe19795e73af0124f51b Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 26 Apr 2017 16:07:12 +0100 Subject: [PATCH 377/749] disable FFC rounding for scalars --- gem/optimise.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gem/optimise.py b/gem/optimise.py index 2eaaf8b6d..461e2b676 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -35,6 +35,8 @@ def literal_rounding(node, self): @literal_rounding.register(Literal) def literal_rounding_literal(node, self): + if not node.shape: + return node # skip scalars table = node.array epsilon = self.epsilon # Mimic the rounding applied at COFFEE formatting, which in turn From 91f080b20029d91034676e9cfd60ceebf841579d Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 26 Apr 2017 16:07:43 +0100 Subject: [PATCH 378/749] WIP: give sum factorisation more time --- gem/optimise.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gem/optimise.py b/gem/optimise.py index 461e2b676..bab9665a9 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -300,7 +300,7 @@ def sum_factorise(sum_indices, factors): # Empty product return one - if len(sum_indices) > 5: + if len(sum_indices) > 6: raise NotImplementedError("Too many indices for sum factorisation!") # Form groups by free indices From 81cef6ac6a9c3dfe9fe8b1c6671b8c57239afad0 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 15 May 2017 14:13:10 +0100 Subject: [PATCH 379/749] add QuadratureElement.point_evaluation --- finat/quadrature_element.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/finat/quadrature_element.py b/finat/quadrature_element.py index a9c59f006..4c5bcce2f 100644 --- a/finat/quadrature_element.py +++ b/finat/quadrature_element.py @@ -74,3 +74,6 @@ def basis_evaluation(self, order, ps, entity=None): dim = self.cell.get_spatial_dimension() return {(0,) * dim: gem.ComponentTensor(product, multiindex)} + + def point_evaluation(self, order, refcoords, entity=None): + raise NotImplementedError("QuadratureElement cannot do point evaluation!") From 666237283078cd72a72683cdcd8b7d393bdd78cb Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 18 May 2017 16:55:49 +0100 Subject: [PATCH 380/749] preserve cellwise constantness at point eval for Ciarlet elements Resolves #30. --- finat/fiat_elements.py | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 0dd99e239..b228792cf 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -190,6 +190,16 @@ def point_evaluation_ciarlet(fiat_element, order, refcoords, entity): base_values = poly_set.get_expansion_set().tabulate(degree, [X]) m = len(base_values) assert base_values.shape == (m, 1) + base_values_sympy = np.array(list(base_values.flat)) + + # Find constant polynomials + def is_const(expr): + try: + float(expr) + return True + except TypeError: + return False + const_mask = np.array(list(map(is_const, base_values_sympy))) # Convert SymPy expression to GEM mapper = gem.node.Memoizer(sympy2gem) @@ -205,16 +215,21 @@ def point_evaluation_ciarlet(fiat_element, order, refcoords, entity): D = form_matrix_product(poly_set.get_dmats(), alpha) table = np.dot(poly_set.get_coeffs(), np.transpose(D)) assert table.shape[-1] == m - beta = tuple(gem.Index() for s in table.shape[:-1]) - k = gem.Index() - result[alpha] = gem.ComponentTensor( - gem.IndexSum( - gem.Product(gem.Indexed(gem.Literal(table), beta + (k,)), - gem.Indexed(base_values, (k,))), - (k,) - ), - beta - ) + zerocols = np.isclose(abs(table).max(axis=tuple(range(table.ndim - 1))), 0.0) + if all(np.logical_or(const_mask, zerocols)): + vals = base_values_sympy[const_mask] + result[alpha] = gem.Literal(table[..., const_mask].dot(vals)) + else: + beta = tuple(gem.Index() for s in table.shape[:-1]) + k = gem.Index() + result[alpha] = gem.ComponentTensor( + gem.IndexSum( + gem.Product(gem.Indexed(gem.Literal(table), beta + (k,)), + gem.Indexed(base_values, (k,))), + (k,) + ), + beta + ) return result From d3eca75d036698be8ca842f8a092d4be5fa667cd Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 18 May 2017 17:14:57 +0100 Subject: [PATCH 381/749] add test case --- test/test_point_evaluation_ciarlet.py | 34 +++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 test/test_point_evaluation_ciarlet.py diff --git a/test/test_point_evaluation_ciarlet.py b/test/test_point_evaluation_ciarlet.py new file mode 100644 index 000000000..61bef4465 --- /dev/null +++ b/test/test_point_evaluation_ciarlet.py @@ -0,0 +1,34 @@ +from __future__ import absolute_import, print_function, division +from six import iteritems + +import pytest + +import FIAT +import finat +import gem + + +@pytest.fixture(params=[1, 2, 3]) +def cell(request): + dim = request.param + return FIAT.ufc_simplex(dim) + + +@pytest.mark.parametrize('degree', [1, 2]) +def test_cellwise_constant(cell, degree): + dim = cell.get_spatial_dimension() + element = finat.Lagrange(cell, degree) + index = gem.Index() + point = gem.partial_indexed(gem.Variable('X', (17, dim)), (index,)) + + order = 2 + for alpha, table in iteritems(element.point_evaluation(order, point)): + if sum(alpha) < degree: + assert table.free_indices == (index,) + else: + assert table.free_indices == () + + +if __name__ == '__main__': + import os + pytest.main(os.path.abspath(__file__)) From c00528b42b1b4d059e13da170b20899a737dbf87 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 19 May 2017 12:09:14 +0100 Subject: [PATCH 382/749] do not skip rounding scalars that come from FIAT --- gem/optimise.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index bab9665a9..cffd4ec4f 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -35,13 +35,11 @@ def literal_rounding(node, self): @literal_rounding.register(Literal) def literal_rounding_literal(node, self): - if not node.shape: - return node # skip scalars table = node.array epsilon = self.epsilon # Mimic the rounding applied at COFFEE formatting, which in turn # mimics FFC formatting. - one_decimal = numpy.round(table, 1) + one_decimal = numpy.asarray(numpy.round(table, 1)) one_decimal[numpy.logical_not(one_decimal)] = 0 # no minus zeros return Literal(numpy.where(abs(table - one_decimal) < epsilon, one_decimal, table)) From 955c8ddd976a16b40667ab1823a80a62bce84751 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 22 May 2017 13:49:52 +0100 Subject: [PATCH 383/749] rename indices when necessary (sum_i a_i)*(sum_i b_i) ===> \sum_{i,i'} a_i*b_{i'} --- gem/optimise.py | 61 +++++++++++++++++++++++++++++++++++++++++++--- gem/refactorise.py | 31 ++++++++++++++++------- 2 files changed, 79 insertions(+), 13 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index cffd4ec4f..249257c13 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -4,7 +4,8 @@ from __future__ import absolute_import, print_function, division from six.moves import filter, map, zip -from functools import reduce +from collections import defaultdict +from functools import partial, reduce from itertools import combinations, permutations import numpy @@ -353,7 +354,53 @@ def make_product(factors, sum_indices=()): return sum_factorise(sum_indices, factors) -def traverse_product(expression, stop_at=None): +def make_rename_map(): + """Creates an rename map for reusing the same index renames.""" + return defaultdict(Index) + + +def make_renamer(rename_map): + """Creates a function for renaming indices when expanding products of + IndexSums, i.e. applying to following rule: + + (sum_i a_i)*(sum_i b_i) ===> \sum_{i,i'} a_i*b_{i'} + + :arg rename_map: An rename map for renaming indices the same way + as functions returned by other calls of this + function. + :returns: A function that takes an iterable of indices to rename, + and returns (renamed indices, applier), where applier is + a function that remap the free indices of GEM + expressions from the old to the new indices. + """ + def _renamer(rename_map, current_set, incoming): + renamed = [] + renames = [] + for i in incoming: + j = i + while j in current_set: + j = rename_map[j] + current_set.add(j) + renamed.append(j) + if i != j: + renames.append((i, j)) + + if renames: + def applier(expr): + pairs = [(i, j) for i, j in renames if i in expr.free_indices] + if pairs: + current, renamed = zip(*pairs) + return Indexed(ComponentTensor(expr, current), renamed) + else: + return expr + else: + applier = lambda expr: expr + + return tuple(renamed), applier + return partial(_renamer, rename_map, set()) + + +def traverse_product(expression, stop_at=None, rename_map=None): """Traverses a product tree and collects factors, also descending into tensor contractions (IndexSum). The nominators of divisions are also broken up, but not the denominators. @@ -363,10 +410,15 @@ def traverse_product(expression, stop_at=None): and returns true for some subexpression, that subexpression is not broken into further factors even if it is a product-like expression. + :arg rename_map: an rename map for consistent index renaming :returns: (sum_indices, terms) - sum_indices: list of indices to sum over - terms: list of product terms """ + if rename_map is None: + rename_map = make_rename_map() + renamer = make_renamer(rename_map) + sum_indices = [] terms = [] @@ -376,8 +428,9 @@ def traverse_product(expression, stop_at=None): if stop_at is not None and stop_at(expr): terms.append(expr) elif isinstance(expr, IndexSum): - stack.append(expr.children[0]) - sum_indices.extend(expr.multiindex) + indices, applier = renamer(expr.multiindex) + sum_indices.extend(indices) + stack.extend(remove_componenttensors(map(applier, expr.children))) elif isinstance(expr, Product): stack.extend(reversed(expr.children)) elif isinstance(expr, Division): diff --git a/gem/refactorise.py b/gem/refactorise.py index aaea93f9d..d3e2d4a71 100644 --- a/gem/refactorise.py +++ b/gem/refactorise.py @@ -12,7 +12,7 @@ from gem.gem import Node, Zero, Product, Sum, Indexed, ListTensor, one from gem.optimise import (remove_componenttensors, sum_factorise, traverse_product, traverse_sum, unroll_indexsum, - expand_conditional) + expand_conditional, make_rename_map, make_renamer) # Refactorisation labels @@ -102,17 +102,25 @@ def sum(*args): return result @staticmethod - def product(*args): + def product(*args, **kwargs): """Product of multiple :py:class:`MonomialSum`s""" + rename_map = kwargs.pop('rename_map', None) + if rename_map is None: + rename_map = make_rename_map() + if kwargs: + raise ValueError("Unrecognised keyword argument: " + kwargs.pop()) + result = MonomialSum() for monomials in product(*args): + renamer = make_renamer(rename_map) sum_indices = [] atomics = [] rest = one for s, a, r in monomials: - sum_indices.extend(s) - atomics.extend(a) - rest = Product(r, rest) + s_, applier = renamer(s) + sum_indices.extend(s_) + atomics.extend(map(applier, a)) + rest = Product(applier(r), rest) result.add(sum_indices, atomics, rest) return result @@ -173,9 +181,13 @@ def stop_at(expr): # Each element of ``sums`` is a MonomialSum. Expansion produces a # series (representing a sum) of products of monomials. result = MonomialSum() - for s, a, r in MonomialSum.product(*sums): - all_indices = common_indices + s - atomics = common_atomics + a + for s, a, r in MonomialSum.product(*sums, rename_map=self.rename_map): + renamer = make_renamer(self.rename_map) + renamer(common_indices) # update current_set + s_, applier = renamer(s) + + all_indices = common_indices + s_ + atomics = common_atomics + tuple(map(applier, a)) # All free indices that appear in atomic terms atomic_indices = set().union(*[atomic.free_indices @@ -193,7 +205,7 @@ def stop_at(expr): # Not really sum factorisation, but rather just an optimised # way of building a product. - rest = sum_factorise(rest_indices, common_others + [r]) + rest = sum_factorise(rest_indices, common_others + [applier(r)]) result.add(sum_indices, atomics, rest) return result @@ -237,4 +249,5 @@ def collect_monomials(expressions, classifier): # Finally, refactorise expressions mapper = Memoizer(_collect_monomials) mapper.classifier = classifier + mapper.rename_map = make_rename_map() return list(map(mapper, expressions)) From cf846d7748fa8235d88cbe493794c06204a19706 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 24 May 2017 17:23:17 +0100 Subject: [PATCH 384/749] fix ListTensor pickling and add test case --- gem/gem.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gem/gem.py b/gem/gem.py index 1f74200f0..20fc7c4bb 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -655,6 +655,9 @@ def children(self): def shape(self): return self.array.shape + def __reduce__(self): + return type(self), (self.array,) + def reconstruct(self, *args): return ListTensor(asarray(args).reshape(self.array.shape)) From 6a37cc9227b3a4b62e875b7b3f0184ebd27c9f78 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 11 Jul 2017 15:36:09 +0100 Subject: [PATCH 385/749] adopt rename: FiredrakeQuadrilateral -> UFCQuadrilateral --- finat/quadrilateral.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/finat/quadrilateral.py b/finat/quadrilateral.py index 394cc1a38..a2ce8043e 100644 --- a/finat/quadrilateral.py +++ b/finat/quadrilateral.py @@ -1,7 +1,7 @@ from __future__ import absolute_import, print_function, division from six import iteritems -from FIAT.reference_element import FiredrakeQuadrilateral +from FIAT.reference_element import UFCQuadrilateral from gem.utils import cached_property @@ -19,7 +19,7 @@ def __init__(self, element): @cached_property def cell(self): - return FiredrakeQuadrilateral() + return UFCQuadrilateral() @property def degree(self): From 1a2b05f49fe65a47a9dcefd78d5f42397c6e5ba4 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 17 Jul 2017 15:04:15 +0100 Subject: [PATCH 386/749] use GEM interpreter instead of aggressive_unroll --- finat/finiteelementbase.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 3b7892b5a..474285fd7 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -7,7 +7,7 @@ import numpy import gem -from gem.optimise import aggressive_unroll +from gem.interpreter import evaluate from gem.utils import cached_property from finat.quadrature import make_quadrature @@ -131,7 +131,9 @@ def entity_support_dofs(elem, entity_dim): quad.weight_expression), quad.point_set.indices ) - ints = aggressive_unroll(gem.ComponentTensor(ints, beta)).array.flatten() + evaluation, = evaluate([gem.ComponentTensor(ints, beta)]) + ints = evaluation.arr.flatten() + assert evaluation.fids == () result[f] = [dof for dof, i in enumerate(ints) if i > eps] cache[entity_dim] = result From 583b6ae62a08d27074b31f0bde28bed2eb5251bc Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 17 Jul 2017 12:18:08 +0100 Subject: [PATCH 387/749] gem.IndexSum switched index -> multiindex --- gem/interpreter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gem/interpreter.py b/gem/interpreter.py index 1ec64391d..f80c799dd 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -274,9 +274,9 @@ def _(e, self): def _(e, self): """Index sums reduce over the given axis.""" val = self(e.children[0]) - idx = val.fids.index(e.index) - return Result(val.arr.sum(axis=idx), - val.fids[:idx] + val.fids[idx+1:]) + idx = tuple(map(val.fids.index, e.multiindex)) + rfids = tuple(fi for fi in val.fids if fi not in e.multiindex) + return Result(val.arr.sum(axis=idx), rfids) @_evaluate.register(gem.ListTensor) # noqa: F811 From 0ddff9f6f5e9cf5e076de6811720360c1bd80407 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 17 Jul 2017 12:19:04 +0100 Subject: [PATCH 388/749] Ellipsis -> slice(None) Ellipsis matches any number of dimensions, slice(None) matches exactly one. --- gem/interpreter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gem/interpreter.py b/gem/interpreter.py index f80c799dd..3b3482c31 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -232,12 +232,12 @@ def _(e, self): idx = [] # First pick up all the existing free indices for _ in val.fids: - idx.append(Ellipsis) + idx.append(slice(None)) # Now grab the shape axes for i in e.multiindex: if isinstance(i, gem.Index): # Free index, want entire extent - idx.append(Ellipsis) + idx.append(slice(None)) elif isinstance(i, gem.VariableIndex): # Variable index, evaluate inner expression result, = self(i.expression) From d202fb751e7c4c2a2ef3cbfcbd57a34aaebbd398 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 17 Jul 2017 12:20:07 +0100 Subject: [PATCH 389/749] performance improvement Python loops are slow. --- gem/interpreter.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/gem/interpreter.py b/gem/interpreter.py index 3b3482c31..02120b781 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -29,6 +29,27 @@ def __init__(self, arr, fids=None): self.arr = arr self.fids = fids if fids is not None else () + def broadcast(self, fids): + """Given some free indices, return a broadcasted array which + contains extra dimensions that correspond to indices in fids + that are not in ``self.fids``. + + Note that inserted dimensions will have length one. + + :arg fids: The free indices for broadcasting. + """ + # Select free indices + axes = tuple(self.fids.index(fi) for fi in fids if fi in self.fids) + assert len(axes) == len(self.fids) + # Add shape + axes += tuple(range(len(self.fids), self.arr.ndim)) + # Move axes, insert extra axes + arr = numpy.transpose(self.arr, axes) + for i, fi in enumerate(fids): + if fi not in self.fids: + arr = numpy.expand_dims(arr, axis=i) + return arr + def filter(self, idx, fids): """Given an index tuple and some free indices, return a "filtered" index tuple which removes entries that correspond @@ -132,8 +153,7 @@ def _(e, self): a, b = [self(o) for o in e.children] result = Result.empty(a, b) fids = result.fids - for idx in numpy.ndindex(result.tshape): - result[idx] = op(a[a.filter(idx, fids)], b[b.filter(idx, fids)]) + result.arr = op(a.broadcast(fids), b.broadcast(fids)) return result From 3e759823625904de738a8df8999a0c8ca1adf117 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 17 Jul 2017 15:02:45 +0100 Subject: [PATCH 390/749] fix ListTensor interpretation --- gem/interpreter.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/gem/interpreter.py b/gem/interpreter.py index 02120b781..2616eca00 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -303,9 +303,14 @@ def _(e, self): def _(e, self): """List tensors just turn into arrays.""" ops = [self(o) for o in e.children] - assert all(ops[0].fids == o.fids for o in ops) - return Result(numpy.asarray([o.arr for o in ops]).reshape(e.shape), - ops[0].fids) + tmp = Result.empty(*ops) + arrs = [] + for o in ops: + arr = numpy.empty(tmp.fshape) + arr[:] = o.broadcast(tmp.fids) + arrs.append(arr) + arrs = numpy.moveaxis(numpy.asarray(arrs), 0, -1).reshape(tmp.fshape + e.shape) + return Result(arrs, tmp.fids) def evaluate(expressions, bindings=None): From 9727f299d26d1b7206db4c0a26d5fb8e33b179dc Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 25 May 2017 12:04:18 +0100 Subject: [PATCH 391/749] add new GEM node: Concatenate --- gem/gem.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 20fc7c4bb..0ccdc1688 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -33,8 +33,8 @@ 'MathFunction', 'MinValue', 'MaxValue', 'Comparison', 'LogicalNot', 'LogicalAnd', 'LogicalOr', 'Conditional', 'Index', 'VariableIndex', 'Indexed', 'ComponentTensor', - 'IndexSum', 'ListTensor', 'Delta', 'index_sum', - 'partial_indexed', 'reshape', 'view'] + 'IndexSum', 'ListTensor', 'Concatenate', 'Delta', + 'index_sum', 'partial_indexed', 'reshape', 'view'] class NodeMeta(type): @@ -677,6 +677,23 @@ def get_hash(self): return hash((type(self), self.shape, self.children)) +class Concatenate(Node): + """Flattens and concatenates GEM expressions by shape. + + Similar to what UFL MixedElement does to value shape. For + example, if children have shapes (2, 2), (), and (3,) then the + concatenated expression has shape (8,). + """ + __slots__ = ('children',) + + def __init__(self, *children): + self.children = children + + @property + def shape(self): + return (sum(numpy.prod(child.shape, dtype=int) for child in self.children),) + + class Delta(Scalar, Terminal): __slots__ = ('i', 'j') __front__ = ('i', 'j') From c6f9e060764989797b2576c670731fc48ef6e2e2 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 18 Jul 2017 15:31:35 +0100 Subject: [PATCH 392/749] remove noqa directives in gem/interpreter.py --- gem/interpreter.py | 60 +++++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/gem/interpreter.py b/gem/interpreter.py index 2616eca00..77ec0d7c0 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -115,20 +115,20 @@ def _evaluate(expression, self): raise ValueError("Unhandled node type %s" % type(expression)) -@_evaluate.register(gem.Zero) # noqa: F811 -def _(e, self): +@_evaluate.register(gem.Zero) +def _evaluate_zero(e, self): """Zeros produce an array of zeros.""" return Result(numpy.zeros(e.shape, dtype=float)) -@_evaluate.register(gem.Constant) # noqa: F811 -def _(e, self): +@_evaluate.register(gem.Constant) +def _evaluate_constant(e, self): """Constants return their array.""" return Result(e.array) -@_evaluate.register(gem.Variable) # noqa: F811 -def _(e, self): +@_evaluate.register(gem.Variable) +def _evaluate_variable(e, self): """Look up variables in the provided bindings.""" try: val = self.bindings[e] @@ -140,11 +140,11 @@ def _(e, self): return Result(val) -@_evaluate.register(gem.Power) # noqa: F811 +@_evaluate.register(gem.Power) @_evaluate.register(gem.Division) @_evaluate.register(gem.Product) @_evaluate.register(gem.Sum) -def _(e, self): +def _evaluate_operator(e, self): op = {gem.Product: operator.mul, gem.Division: operator.div, gem.Sum: operator.add, @@ -157,8 +157,8 @@ def _(e, self): return result -@_evaluate.register(gem.MathFunction) # noqa: F811 -def _(e, self): +@_evaluate.register(gem.MathFunction) +def _evaluate_mathfunction(e, self): ops = [self(o) for o in e.children] result = Result.empty(*ops) names = {"abs": abs, @@ -169,9 +169,9 @@ def _(e, self): return result -@_evaluate.register(gem.MaxValue) # noqa: F811 +@_evaluate.register(gem.MaxValue) @_evaluate.register(gem.MinValue) -def _(e, self): +def _evaluate_minmaxvalue(e, self): ops = [self(o) for o in e.children] result = Result.empty(*ops) op = {gem.MinValue: min, @@ -181,8 +181,8 @@ def _(e, self): return result -@_evaluate.register(gem.Comparison) # noqa: F811 -def _(e, self): +@_evaluate.register(gem.Comparison) +def _evaluate_comparison(e, self): ops = [self(o) for o in e.children] op = {">": operator.gt, ">=": operator.ge, @@ -196,8 +196,8 @@ def _(e, self): return result -@_evaluate.register(gem.LogicalNot) # noqa: F811 -def _(e, self): +@_evaluate.register(gem.LogicalNot) +def _evaluate_logicalnot(e, self): val = self(e.children[0]) assert val.arr.dtype == numpy.dtype("bool") result = Result.empty(val, bool) @@ -206,8 +206,8 @@ def _(e, self): return result -@_evaluate.register(gem.LogicalAnd) # noqa: F811 -def _(e, self): +@_evaluate.register(gem.LogicalAnd) +def _evaluate_logicaland(e, self): a, b = [self(o) for o in e.children] assert a.arr.dtype == numpy.dtype("bool") assert b.arr.dtype == numpy.dtype("bool") @@ -218,8 +218,8 @@ def _(e, self): return result -@_evaluate.register(gem.LogicalOr) # noqa: F811 -def _(e, self): +@_evaluate.register(gem.LogicalOr) +def _evaluate_logicalor(e, self): a, b = [self(o) for o in e.children] assert a.arr.dtype == numpy.dtype("bool") assert b.arr.dtype == numpy.dtype("bool") @@ -230,8 +230,8 @@ def _(e, self): return result -@_evaluate.register(gem.Conditional) # noqa: F811 -def _(e, self): +@_evaluate.register(gem.Conditional) +def _evaluate_conditional(e, self): cond, then, else_ = [self(o) for o in e.children] assert cond.arr.dtype == numpy.dtype("bool") result = Result.empty(cond, then, else_) @@ -243,8 +243,8 @@ def _(e, self): return result -@_evaluate.register(gem.Indexed) # noqa: F811 -def _(e, self): +@_evaluate.register(gem.Indexed) +def _evaluate_indexed(e, self): """Indexing maps shape to free indices""" val = self(e.children[0]) fids = tuple(i for i in e.multiindex if isinstance(i, gem.Index)) @@ -270,8 +270,8 @@ def _(e, self): return Result(val[idx], val.fids + fids) -@_evaluate.register(gem.ComponentTensor) # noqa: F811 -def _(e, self): +@_evaluate.register(gem.ComponentTensor) +def _evaluate_componenttensor(e, self): """Component tensors map free indices to shape.""" val = self(e.children[0]) axes = [] @@ -290,8 +290,8 @@ def _(e, self): tuple(fids)) -@_evaluate.register(gem.IndexSum) # noqa: F811 -def _(e, self): +@_evaluate.register(gem.IndexSum) +def _evaluate_indexsum(e, self): """Index sums reduce over the given axis.""" val = self(e.children[0]) idx = tuple(map(val.fids.index, e.multiindex)) @@ -299,8 +299,8 @@ def _(e, self): return Result(val.arr.sum(axis=idx), rfids) -@_evaluate.register(gem.ListTensor) # noqa: F811 -def _(e, self): +@_evaluate.register(gem.ListTensor) +def _evaluate_listtensor(e, self): """List tensors just turn into arrays.""" ops = [self(o) for o in e.children] tmp = Result.empty(*ops) From 1bfebbe23e91f25e68e50d9b856d3b7493092e1c Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 18 Jul 2017 15:57:40 +0100 Subject: [PATCH 393/749] add Concatenate to the GEM interpreter --- gem/interpreter.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/gem/interpreter.py b/gem/interpreter.py index 77ec0d7c0..9c2556987 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -7,6 +7,7 @@ import numpy import operator import math +from collections import OrderedDict from singledispatch import singledispatch import itertools @@ -313,6 +314,25 @@ def _evaluate_listtensor(e, self): return Result(arrs, tmp.fids) +@_evaluate.register(gem.Concatenate) +def _evaluate_concatenate(e, self): + """Concatenate nodes flatten and concatenate shapes.""" + ops = [self(o) for o in e.children] + fids = tuple(OrderedDict.fromkeys(itertools.chain(*(o.fids for o in ops)))) + fshape = tuple(i.extent for i in fids) + arrs = [] + for o in ops: + # Create temporary with correct shape + arr = numpy.empty(fshape + o.shape) + # Broadcast for extra free indices + arr[:] = o.broadcast(fids) + # Flatten shape + arr = arr.reshape(arr.shape[:arr.ndim-len(o.shape)] + (-1,)) + arrs.append(arr) + arrs = numpy.concatenate(arrs, axis=-1) + return Result(arrs, fids) + + def evaluate(expressions, bindings=None): """Evaluate some GEM expressions given variable bindings. From b7864a0130910a0fb2f3d00a0999111a9c117015 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 19 Jul 2017 11:03:01 +0100 Subject: [PATCH 394/749] fix accidental axis reordering gem.reshape and gem.view ordered the axis of the result according to the indices of the FlexiblyIndexed node, which meant that transposed imposed by a ComponentTensor node are unexpectedly lost. --- gem/gem.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 0ccdc1688..e705fc259 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -799,7 +799,7 @@ def reshape(expression, *shapes): shape_of = dict(zip(indexes, shapes)) dim2idxs_ = [] - indices = [] + indices = [[] for _ in range(len(indexes))] for offset, idxs in dim2idxs: idxs_ = [] for idx in idxs: @@ -811,13 +811,13 @@ def reshape(expression, *shapes): raise ValueError("Shape {} does not match extent {}.".format(shape, dim)) strides = strides_of(shape) for extent, stride_ in zip(shape, strides): - index = Index(extent=extent) - idxs_.append((index, stride_ * stride)) - indices.append(index) + index_ = Index(extent=extent) + idxs_.append((index_, stride_ * stride)) + indices[indexes.index(index)].append(index_) dim2idxs_.append((offset, tuple(idxs_))) expr = FlexiblyIndexed(variable, tuple(dim2idxs_)) - return ComponentTensor(expr, tuple(indices)) + return ComponentTensor(expr, tuple(chain.from_iterable(indices))) def view(expression, *slices): @@ -831,7 +831,7 @@ def view(expression, *slices): slice_of = dict(zip(indexes, slices)) dim2idxs_ = [] - indices = [] + indices = [None] * len(slices) for offset, idxs in dim2idxs: offset_ = offset idxs_ = [] @@ -850,7 +850,7 @@ def view(expression, *slices): offset_ += start * stride extent = 1 + (stop - start - 1) // step index_ = Index(extent=extent) - indices.append(index_) + indices[indexes.index(index)] = index_ idxs_.append((index_, step * stride)) dim2idxs_.append((offset_, tuple(idxs_))) From 6927ea990a8d2d2b3aca472ae00794b59cb88612 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 19 Jul 2017 11:05:59 +0100 Subject: [PATCH 395/749] support ComponentTensor nodes in gem.select_expression --- gem/optimise.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gem/optimise.py b/gem/optimise.py index 249257c13..ef44fcd50 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -187,6 +187,12 @@ def child(expression): if types <= {Literal, Zero, Failure}: return partial_indexed(ListTensor(expressions), (index,)) + if types <= {ComponentTensor, Zero}: + shape, = set(e.shape for e in expressions) + multiindex = tuple(Index(extent=d) for d in shape) + children = remove_componenttensors([Indexed(e, multiindex) for e in expressions]) + return ComponentTensor(_select_expression(children, index), multiindex) + if len(types) == 1: cls, = types if cls.__front__ or cls.__back__: From b0432ac5b44dd41a0c1b04d7d75fa13b116fecfb Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 24 May 2017 17:45:09 +0100 Subject: [PATCH 396/749] add EnrichedElement --- finat/__init__.py | 1 + finat/enriched.py | 103 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 finat/enriched.py diff --git a/finat/__init__.py b/finat/__init__.py index 5b273ad87..97633cae6 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -7,5 +7,6 @@ from .tensorfiniteelement import TensorFiniteElement # noqa: F401 from .tensor_product import TensorProductElement # noqa: F401 from .quadrilateral import QuadrilateralElement # noqa: F401 +from .enriched import EnrichedElement # noqa: F401 from .quadrature_element import QuadratureElement # noqa: F401 from . import quadrature # noqa: F401 diff --git a/finat/enriched.py b/finat/enriched.py new file mode 100644 index 000000000..5b87159f4 --- /dev/null +++ b/finat/enriched.py @@ -0,0 +1,103 @@ +from __future__ import absolute_import, print_function, division +from six.moves import map, zip + +import gem +from gem.utils import cached_property + +from finat.finiteelementbase import FiniteElementBase + + +class EnrichedElement(FiniteElementBase): + """A finite element whose basis functions are the union of the + basis functions of several other finite elements.""" + + def __init__(self, elements): + super(EnrichedElement, self).__init__() + self.elements = tuple(elements) + + @cached_property + def cell(self): + result, = set(elem.cell for elem in self.elements) + return result + + @cached_property + def degree(self): + return tree_map(max, *[elem.degree for elem in self.elements]) + + def entity_dofs(self): + '''Return the map of topological entities to degrees of + freedom for the finite element.''' + from FIAT.mixed import concatenate_entity_dofs + return concatenate_entity_dofs(self.cell, self.elements) + + def space_dimension(self): + '''Return the dimension of the finite element space.''' + return sum(elem.space_dimension() for elem in self.elements) + + @cached_property + def index_shape(self): + return (self.space_dimension(),) + + @cached_property + def value_shape(self): + '''A tuple indicating the shape of the element.''' + shape, = set(elem.value_shape for elem in self.elements) + return shape + + def _compose_evaluations(self, results): + keys, = set(map(frozenset, results)) + + def merge(tables): + tables = tuple(tables) + zeta = self.get_value_indices() + tensors = [] + for elem, table in zip(self.elements, tables): + beta_i = elem.get_indices() + tensors.append(gem.ComponentTensor( + gem.Indexed(table, beta_i + zeta), + beta_i + )) + beta = self.get_indices() + return gem.ComponentTensor( + gem.Indexed(gem.Concatenate(*tensors), beta), + beta + zeta + ) + return {key: merge(result[key] for result in results) + for key in keys} + + def basis_evaluation(self, order, ps, entity=None): + '''Return code for evaluating the element at known points on the + reference element. + + :param order: return derivatives up to this order. + :param ps: the point set object. + :param entity: the cell entity on which to tabulate. + ''' + results = [element.basis_evaluation(order, ps, entity) + for element in self.elements] + return self._compose_evaluations(results) + + def point_evaluation(self, order, refcoords, entity=None): + '''Return code for evaluating the element at an arbitrary points on + the reference element. + + :param order: return derivatives up to this order. + :param refcoords: GEM expression representing the coordinates + on the reference entity. Its shape must be + a vector with the correct dimension, its + free indices are arbitrary. + :param entity: the cell entity on which to tabulate. + ''' + results = [element.point_evaluation(order, refcoords, entity) + for element in self.elements] + return self._compose_evaluations(results) + + +def tree_map(f, *args): + """Like the built-in :py:func:`map`, but applies to a tuple tree.""" + nonleaf, = set(isinstance(arg, tuple) for arg in args) + if nonleaf: + ndim, = set(map(len, args)) + return tuple(tree_map(f, *subargs) for subargs in zip(*args)) + else: + return f(*args) From f4ab9b55072aa6b19bfc8f6466a97295303633b8 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 20 Jul 2017 14:56:10 +0100 Subject: [PATCH 397/749] add comment to dubious line --- finat/enriched.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/enriched.py b/finat/enriched.py index 5b87159f4..bf8cd2c35 100644 --- a/finat/enriched.py +++ b/finat/enriched.py @@ -97,7 +97,7 @@ def tree_map(f, *args): """Like the built-in :py:func:`map`, but applies to a tuple tree.""" nonleaf, = set(isinstance(arg, tuple) for arg in args) if nonleaf: - ndim, = set(map(len, args)) + ndim, = set(map(len, args)) # asserts equal arity of all args return tuple(tree_map(f, *subargs) for subargs in zip(*args)) else: return f(*args) From 13bf2b00cf070bd2a5e882a7fcc600e0721b1c6c Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 20 Jul 2017 17:43:15 +0100 Subject: [PATCH 398/749] retire gem.utils.OrderedSet collections.OrderedDict is good enough. --- gem/impero_utils.py | 7 +++---- gem/utils.py | 36 ------------------------------------ 2 files changed, 3 insertions(+), 40 deletions(-) diff --git a/gem/impero_utils.py b/gem/impero_utils.py index f524592df..f327bf319 100644 --- a/gem/impero_utils.py +++ b/gem/impero_utils.py @@ -15,7 +15,6 @@ from singledispatch import singledispatch from gem.node import traversal, collect_refcount -from gem.utils import OrderedSet from gem import gem, impero as imp, optimise, scheduling @@ -61,17 +60,17 @@ def nonzero(assignment): expressions = [expression for variable, expression in assignments] # Collect indices in a deterministic order - indices = OrderedSet() + indices = collections.OrderedDict() for node in traversal(expressions): if isinstance(node, gem.Indexed): for index in node.multiindex: if isinstance(index, gem.Index): - indices.add(index) + indices.setdefault(index) elif isinstance(node, gem.FlexiblyIndexed): for offset, idxs in node.dim2idxs: for index, stride in idxs: if isinstance(index, gem.Index): - indices.add(index) + indices.setdefault(index) # Build ordered index map index_ordering = make_prefix_ordering(indices, prefix_ordering) diff --git a/gem/utils.py b/gem/utils.py index cf37a705a..0d3c715a2 100644 --- a/gem/utils.py +++ b/gem/utils.py @@ -23,42 +23,6 @@ def __get__(self, obj, cls): return result -class OrderedSet(collections.MutableSet): - """A set that preserves ordering, useful for deterministic code - generation.""" - - def __init__(self, iterable=None): - self._list = list() - self._set = set() - - if iterable is not None: - for item in iterable: - self.add(item) - - def __contains__(self, item): - return item in self._set - - def __iter__(self): - return iter(self._list) - - def __len__(self): - return len(self._list) - - def __repr__(self): - return "OrderedSet({0})".format(self._list) - - def add(self, value): - if value not in self._set: - self._list.append(value) - self._set.add(value) - - def discard(self, value): - # O(n) time complexity: do not use this! - if value in self._set: - self._list.remove(value) - self._set.discard(value) - - def groupby(iterable, key=None): """Groups objects by their keys. From 356e3aea05d50ad0d03379eb5f2ef53c66089086 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 20 Jul 2017 17:53:18 +0100 Subject: [PATCH 399/749] GEM: add .index_ordering() on Indexed and FlexiblyIndexed --- gem/gem.py | 11 +++++++++++ gem/impero_utils.py | 20 +++++++------------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index e705fc259..5611ec556 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -500,6 +500,10 @@ def __new__(cls, aggregate, multiindex): return self + def index_ordering(self): + """Running indices in the order of indexing in this node.""" + return tuple(i for i in self.multiindex if isinstance(i, Index)) + class FlexiblyIndexed(Scalar): """Flexible indexing of :py:class:`Variable`s to implement views and @@ -553,6 +557,13 @@ def __init__(self, variable, dim2idxs): self.dim2idxs = tuple(dim2idxs_) self.free_indices = unique(free_indices) + def index_ordering(self): + """Running indices in the order of indexing in this node.""" + return tuple(index + for offset, idxs in self.dim2idxs + for index, stride in idxs + if isinstance(index, Index)) + class ComponentTensor(Node): __slots__ = ('children', 'multiindex', 'shape') diff --git a/gem/impero_utils.py b/gem/impero_utils.py index f327bf319..2df471697 100644 --- a/gem/impero_utils.py +++ b/gem/impero_utils.py @@ -10,7 +10,7 @@ from six.moves import filter import collections -import itertools +from itertools import chain, groupby from singledispatch import singledispatch @@ -60,17 +60,11 @@ def nonzero(assignment): expressions = [expression for variable, expression in assignments] # Collect indices in a deterministic order - indices = collections.OrderedDict() - for node in traversal(expressions): - if isinstance(node, gem.Indexed): - for index in node.multiindex: - if isinstance(index, gem.Index): - indices.setdefault(index) - elif isinstance(node, gem.FlexiblyIndexed): - for offset, idxs in node.dim2idxs: - for index, stride in idxs: - if isinstance(index, gem.Index): - indices.setdefault(index) + indices = list(collections.OrderedDict.fromkeys(chain.from_iterable( + node.index_ordering() + for node in traversal(expressions) + if isinstance(node, (gem.Indexed, gem.FlexiblyIndexed)) + ))) # Build ordered index map index_ordering = make_prefix_ordering(indices, prefix_ordering) @@ -174,7 +168,7 @@ def make_loop_tree(ops, get_indices, level=0): """ keyfunc = lambda op: op.loop_shape(get_indices)[level:level+1] statements = [] - for first_index, op_group in itertools.groupby(ops, keyfunc): + for first_index, op_group in groupby(ops, keyfunc): if first_index: inner_block = make_loop_tree(op_group, get_indices, level+1) statements.append(imp.For(first_index[0], inner_block)) From 059e807235c801cdb02c9377edfbf0219bb72eaa Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 21 Jul 2017 16:32:23 +0100 Subject: [PATCH 400/749] add unconcatenate tools --- gem/unconcatenate.py | 192 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 gem/unconcatenate.py diff --git a/gem/unconcatenate.py b/gem/unconcatenate.py new file mode 100644 index 000000000..b6cc7217a --- /dev/null +++ b/gem/unconcatenate.py @@ -0,0 +1,192 @@ +"""Utility functions for decomposing Concatenate nodes.""" + +from __future__ import absolute_import, print_function, division +from six.moves import range, zip + +from itertools import chain + +import numpy + +from gem.node import Memoizer, reuse_if_untouched +from gem.gem import (ComponentTensor, Concatenate, FlexiblyIndexed, + Index, Indexed, Node, reshape, view) +from gem.optimise import remove_componenttensors + + +__all__ = ['unconcatenate'] + + +def find_group(expressions): + """Finds a full set of indexed Concatenate nodes with the same + free index, if any such node exists. + + Pre-condition: ComponentTensor nodes surrounding Concatenate nodes + must be removed. + + :arg expressions: a multi-root GEM expression DAG + :returns: a list of GEM nodes, or None + """ + free_indices = set().union(chain(*[e.free_indices for e in expressions])) + + # Result variables + index = None + nodes = [] + + # Sui generis pre-order traversal so that we can avoid going + # unnecessarily deep in the DAG. + seen = set() + lifo = [] + for root in expressions: + if root not in seen: + seen.add(root) + lifo.append(root) + + while lifo: + node = lifo.pop() + if not free_indices.intersection(node.free_indices): + continue + + if isinstance(node, Indexed): + child, = node.children + if isinstance(child, Concatenate): + i, = node.multiindex + assert i in free_indices + if (index or i) == i: + index = i + nodes.append(node) + # Skip adding children + continue + + for child in reversed(node.children): + if child not in seen: + seen.add(child) + lifo.append(child) + + return index and nodes + + +def split_variable(variable_ref, index, multiindices): + """Splits a flexibly indexed variable along a concatenation index. + + :param variable_ref: flexibly indexed variable to split + :param index: :py:class:`Concatenate` index to split along + :param multiindices: one multiindex for each split variable + + :returns: generator of split indexed variables + """ + assert isinstance(variable_ref, FlexiblyIndexed) + other_indices = list(variable_ref.index_ordering()) + other_indices.remove(index) + other_indices = tuple(other_indices) + data = ComponentTensor(variable_ref, (index,) + other_indices) + slices = [slice(None)] * len(other_indices) + shapes = [(other_index.extent,) for other_index in other_indices] + + offset = 0 + for multiindex in multiindices: + shape = tuple(index.extent for index in multiindex) + size = numpy.prod(shape, dtype=int) + slice_ = slice(offset, offset + size) + offset += size + + sub_ref = Indexed(reshape(view(data, slice_, *slices), + shape, *shapes), + multiindex + other_indices) + sub_ref, = remove_componenttensors((sub_ref,)) + yield sub_ref + + +def _replace_node(node, self): + """Replace subexpressions using a given mapping. + + :param node: root of expression + :param self: function for recursive calls + """ + assert isinstance(node, Node) + if self.cut(node): + return node + try: + return self.mapping[node] + except KeyError: + return reuse_if_untouched(node, self) + + +def replace_node(expression, mapping, cut=None): + """Replace subexpressions using a given mapping. + + :param expression: a GEM expression + :param mapping: a :py:class:`dict` containing the substitutions + :param cut: cutting predicate; if returns true, it is assumed that + no replacements would take place in the subexpression. + """ + mapper = Memoizer(_replace_node) + mapper.mapping = mapping + mapper.cut = cut or (lambda node: False) + return mapper(expression) + + +def _unconcatenate(cache, pairs): + # Tail-call recursive core of unconcatenate. + # Assumes that input has already been sanitised. + concat_group = find_group([e for v, e in pairs]) + if concat_group is None: + return pairs + + # Get the index split + concat_ref = next(iter(concat_group)) + assert isinstance(concat_ref, Indexed) + concat_expr, = concat_ref.children + index, = concat_ref.multiindex + assert isinstance(concat_expr, Concatenate) + try: + multiindices = cache[index] + except KeyError: + multiindices = tuple(tuple(Index(extent=d) for d in child.shape) + for child in concat_expr.children) + cache[index] = multiindices + + def cut(node): + """No need to rebuild expression of independent of the + relevant concatenation index.""" + return index not in node.free_indices + + # Build Concatenate node replacement mappings + mappings = [{} for i in range(len(multiindices))] + for concat_ref in concat_group: + concat_expr, = concat_ref.children + for i in range(len(multiindices)): + sub_ref = Indexed(concat_expr.children[i], multiindices[i]) + sub_ref, = remove_componenttensors((sub_ref,)) + mappings[i][concat_ref] = sub_ref + + # Finally, split assignment pairs + split_pairs = [] + for var, expr in pairs: + if index not in var.free_indices: + split_pairs.append((var, expr)) + else: + for v, m in zip(split_variable(var, index, multiindices), mappings): + split_pairs.append((v, replace_node(expr, m, cut))) + + # Run again, there may be other Concatenate groups + return _unconcatenate(cache, split_pairs) + + +def unconcatenate(pairs, cache=None): + """Splits a list of (variable reference, expression) pairs along + :py:class:`Concatenate` nodes embedded in the expressions. + + :param pairs: list of (indexed variable, expression) pairs + :param cache: index splitting cache :py:class:`dict` (optional) + + :returns: list of (indexed variable, expression) pairs + """ + # Set up cache + if cache is None: + cache = {} + + # Eliminate index renaming due to ComponentTensor nodes + exprs = remove_componenttensors([e for v, e in pairs]) + pairs = [(v, e) for (v, _), e in zip(pairs, exprs)] + + return _unconcatenate(cache, pairs) From f4e2cc1a51f83f0e32eea9538711460a565b3085 Mon Sep 17 00:00:00 2001 From: David Ham Date: Thu, 20 Jul 2017 16:05:14 +0100 Subject: [PATCH 401/749] Operator rename --- gem/interpreter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gem/interpreter.py b/gem/interpreter.py index 9c2556987..81a2b0079 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -147,7 +147,7 @@ def _evaluate_variable(e, self): @_evaluate.register(gem.Sum) def _evaluate_operator(e, self): op = {gem.Product: operator.mul, - gem.Division: operator.div, + gem.Division: operator.truediv, gem.Sum: operator.add, gem.Power: operator.pow}[type(e)] From 49cf8e0e3ee2646fe542021b2485524343538e4f Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 25 Jul 2017 17:23:48 +0100 Subject: [PATCH 402/749] easy fixes for PR comments --- gem/gem.py | 4 ++-- gem/unconcatenate.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 5611ec556..0ea5c9646 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -560,8 +560,8 @@ def __init__(self, variable, dim2idxs): def index_ordering(self): """Running indices in the order of indexing in this node.""" return tuple(index - for offset, idxs in self.dim2idxs - for index, stride in idxs + for _, idxs in self.dim2idxs + for index, _ in idxs if isinstance(index, Index)) diff --git a/gem/unconcatenate.py b/gem/unconcatenate.py index b6cc7217a..90205de72 100644 --- a/gem/unconcatenate.py +++ b/gem/unconcatenate.py @@ -173,7 +173,7 @@ def cut(node): def unconcatenate(pairs, cache=None): - """Splits a list of (variable reference, expression) pairs along + """Splits a list of (indexed variable, expression) pairs along :py:class:`Concatenate` nodes embedded in the expressions. :param pairs: list of (indexed variable, expression) pairs From c99fd3ea0bc562a4cc55fcfe120bfec4a72ae22e Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 25 Jul 2017 18:04:57 +0100 Subject: [PATCH 403/749] add code for flattening Concatenate nodes --- gem/unconcatenate.py | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/gem/unconcatenate.py b/gem/unconcatenate.py index 90205de72..0e306dc04 100644 --- a/gem/unconcatenate.py +++ b/gem/unconcatenate.py @@ -1,19 +1,22 @@ """Utility functions for decomposing Concatenate nodes.""" from __future__ import absolute_import, print_function, division -from six.moves import range, zip +from six.moves import map, range, zip from itertools import chain import numpy +from singledispatch import singledispatch from gem.node import Memoizer, reuse_if_untouched from gem.gem import (ComponentTensor, Concatenate, FlexiblyIndexed, - Index, Indexed, Node, reshape, view) + Index, Indexed, Literal, Node, partial_indexed, + reshape, view) from gem.optimise import remove_componenttensors +from gem.interpreter import evaluate -__all__ = ['unconcatenate'] +__all__ = ['flatten', 'unconcatenate'] def find_group(expressions): @@ -190,3 +193,31 @@ def unconcatenate(pairs, cache=None): pairs = [(v, e) for (v, _), e in zip(pairs, exprs)] return _unconcatenate(cache, pairs) + + +@singledispatch +def _flatten(node, self): + """Replace Concatenate nodes with Literal nodes. + + :arg node: root of the expression + :arg self: function for recursive calls + """ + raise AssertionError("cannot handle type %s" % type(node)) + + +_flatten.register(Node)(reuse_if_untouched) + + +@_flatten.register(Concatenate) +def _flatten_concatenate(node, self): + result, = evaluate([node]) + return partial_indexed(Literal(result.arr), result.fids) + + +def flatten(expressions): + """Flatten Concatenate nodes, and destroy the structure they express. + + :arg expressions: a multi-root expression DAG + """ + mapper = Memoizer(_flatten) + return list(map(mapper, expressions)) From c6a1d184b0ed2686c7b3d7346335436a17012cd9 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 26 Jul 2017 14:49:51 +0100 Subject: [PATCH 404/749] add formdegree --- finat/enriched.py | 8 ++++++++ finat/fiat_elements.py | 4 ++++ finat/finiteelementbase.py | 4 ++++ finat/quadrilateral.py | 4 ++++ finat/tensor_product.py | 7 +++++++ finat/tensorfiniteelement.py | 4 ++++ 6 files changed, 31 insertions(+) diff --git a/finat/enriched.py b/finat/enriched.py index bf8cd2c35..eea0ac820 100644 --- a/finat/enriched.py +++ b/finat/enriched.py @@ -24,6 +24,14 @@ def cell(self): def degree(self): return tree_map(max, *[elem.degree for elem in self.elements]) + @cached_property + def formdegree(self): + ks = set(elem.formdegree for elem in self.elements) + if None in ks: + return None + else: + return max(ks) + def entity_dofs(self): '''Return the map of topological entities to degrees of freedom for the finite element.''' diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index b228792cf..b2bd0096b 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -30,6 +30,10 @@ def degree(self): # Requires FIAT.CiarletElement return self._element.degree() + @property + def formdegree(self): + return self._element.get_formdegree() + def entity_dofs(self): return self._element.entity_dofs() diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 474285fd7..a793dd172 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -26,6 +26,10 @@ def degree(self): In the tensor case this is a tuple. ''' + @abstractproperty + def formdegree(self): + '''Degree of the associated form (FEEC)''' + @abstractmethod def entity_dofs(self): '''Return the map of topological entities to degrees of diff --git a/finat/quadrilateral.py b/finat/quadrilateral.py index a2ce8043e..d2ca95ab6 100644 --- a/finat/quadrilateral.py +++ b/finat/quadrilateral.py @@ -26,6 +26,10 @@ def degree(self): unique_degree, = set(self.product.degree) return unique_degree + @property + def formdegree(self): + return self.product.formdegree + @cached_property def _entity_dofs(self): entity_dofs = self.product.entity_dofs() diff --git a/finat/tensor_product.py b/finat/tensor_product.py index 8845ab6d5..98491e93e 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -31,6 +31,13 @@ def cell(self): def degree(self): return tuple(fe.degree for fe in self.factors) + @cached_property + def formdegree(self): + if any(fe.formdegree is None for fe in self.factors): + return None + else: + return sum(fe.formdegree for fe in self.factors) + @cached_property def _entity_dofs(self): shape = tuple(fe.space_dimension() for fe in self.factors) diff --git a/finat/tensorfiniteelement.py b/finat/tensorfiniteelement.py index 9e7d9610c..c996e44c3 100644 --- a/finat/tensorfiniteelement.py +++ b/finat/tensorfiniteelement.py @@ -55,6 +55,10 @@ def cell(self): def degree(self): return self._base_element.degree + @property + def formdegree(self): + return self._base_element.formdegree + def entity_dofs(self): raise NotImplementedError("No one uses this!") From 6249f667b90487d29c74a47512c8594cf906686b Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 26 Jul 2017 15:35:52 +0100 Subject: [PATCH 405/749] add mapping --- finat/enriched.py | 9 +++++++++ finat/fiat_elements.py | 9 +++++++++ finat/finiteelementbase.py | 5 +++++ finat/quadrilateral.py | 4 ++++ finat/tensor_product.py | 10 ++++++++++ finat/tensorfiniteelement.py | 4 ++++ 6 files changed, 41 insertions(+) diff --git a/finat/enriched.py b/finat/enriched.py index eea0ac820..12315f8a8 100644 --- a/finat/enriched.py +++ b/finat/enriched.py @@ -100,6 +100,15 @@ def point_evaluation(self, order, refcoords, entity=None): for element in self.elements] return self._compose_evaluations(results) + @property + def mapping(self): + mappings = set(elem.mapping for elem in self.elements) + if len(mappings) != 1: + return None + else: + result, = mappings + return result + def tree_map(f, *args): """Like the built-in :py:func:`map`, but applies to a tuple tree.""" diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index b2bd0096b..cbe1d9be8 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -128,6 +128,15 @@ def point_evaluation(self, order, refcoords, entity=None): # Dispatch on FIAT element class return point_evaluation(self._element, order, refcoords, (entity_dim, entity_i)) + @property + def mapping(self): + mappings = set(self._element.mapping()) + if len(mappings) != 1: + return None + else: + result, = mappings + return result + @singledispatch def point_evaluation(fiat_element, order, refcoords, entity): diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index a793dd172..a51a13326 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -100,6 +100,11 @@ def point_evaluation(self, order, refcoords, entity=None): :param entity: the cell entity on which to tabulate. ''' + @abstractproperty + def mapping(self): + '''Appropriate mapping from the reference cell to a physical cell for + all basis functions of the finite element.''' + def entity_support_dofs(elem, entity_dim): """Return the map of entity id to the degrees of freedom for which diff --git a/finat/quadrilateral.py b/finat/quadrilateral.py index d2ca95ab6..6f72d101d 100644 --- a/finat/quadrilateral.py +++ b/finat/quadrilateral.py @@ -69,6 +69,10 @@ def index_shape(self): def value_shape(self): return self.product.value_shape + @property + def mapping(self): + return self.product.mapping + def productise(entity): if entity is None: diff --git a/finat/tensor_product.py b/finat/tensor_product.py index 98491e93e..a5ec50316 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -159,6 +159,16 @@ def point_evaluation(self, order, point, entity=None): return self._merge_evaluations(factor_results) + @cached_property + def mapping(self): + mappings = [fe.mapping for fe in self.factors if fe.mapping != "affine"] + if len(mappings) == 0: + return "affine" + elif len(mappings) == 1: + return mappings[0] + else: + return None + def factor_point_set(product_cell, product_dim, point_set): """Factors a point set for the product element into a point sets for diff --git a/finat/tensorfiniteelement.py b/finat/tensorfiniteelement.py index c996e44c3..67d1441af 100644 --- a/finat/tensorfiniteelement.py +++ b/finat/tensorfiniteelement.py @@ -108,3 +108,7 @@ def _tensorise(self, scalar_evaluation): scalar_i + tensor_i + scalar_vi + tensor_vi ) return result + + @property + def mapping(self): + return self._base_element.mapping From 2d2940bd12a3da88221d72545f360c69258de209 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 26 Jul 2017 16:44:26 +0100 Subject: [PATCH 406/749] make TensorProductElement accept one non-scalar factor --- finat/tensor_product.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/finat/tensor_product.py b/finat/tensor_product.py index a5ec50316..251bb4079 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -21,7 +21,14 @@ class TensorProductElement(FiniteElementBase): def __init__(self, factors): super(TensorProductElement, self).__init__() self.factors = tuple(factors) - assert all(fe.value_shape == () for fe in self.factors) + + shapes = [fe.value_shape for fe in self.factors if fe.value_shape != ()] + if len(shapes) == 0: + self._value_shape = () + elif len(shapes) == 1: + self._value_shape = shapes[0] + else: + raise NotImplementedError("Only one nonscalar factor permitted!") @cached_property def cell(self): @@ -71,7 +78,7 @@ def index_shape(self): @property def value_shape(self): - return () # TODO: non-scalar factors not supported yet + return self._value_shape def _factor_entity(self, entity): # Default entity @@ -105,6 +112,10 @@ def _merge_evaluations(self, factor_results): # subelement. alphas = [fe.get_indices() for fe in self.factors] + # A list of multiindices, one multiindex per subelement, each + # multiindex describing the value shape of the subelement. + zetas = [fe.get_value_indices() for fe in self.factors] + result = {} for derivative in range(order + 1): for Delta in mis(dimension, derivative): @@ -113,15 +124,15 @@ def _merge_evaluations(self, factor_results): # GEM scalars (can have free indices) for collecting # the contributions from the subelements. scalars = [] - for fr, delta, alpha in zip(factor_results, deltas, alphas): + for fr, delta, alpha, zeta in zip(factor_results, deltas, alphas, zetas): # Turn basis shape to free indices, select the # right derivative entry, and collect the result. - scalars.append(gem.Indexed(fr[delta], alpha)) + scalars.append(gem.Indexed(fr[delta], alpha + zeta)) # Multiply the values from the subelements and wrap up # non-point indices into shape. result[Delta] = gem.ComponentTensor( reduce(gem.Product, scalars), - tuple(chain(*alphas)) + tuple(chain(*(alphas + zetas))) ) return result From c154ad9b8914e35951fa5e15e8e5ef235a5aebe3 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 26 Jul 2017 16:33:16 +0100 Subject: [PATCH 407/749] add HDivElement --- finat/__init__.py | 1 + finat/hdivcurl.py | 100 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 finat/hdivcurl.py diff --git a/finat/__init__.py b/finat/__init__.py index 97633cae6..10ba398b7 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -8,5 +8,6 @@ from .tensor_product import TensorProductElement # noqa: F401 from .quadrilateral import QuadrilateralElement # noqa: F401 from .enriched import EnrichedElement # noqa: F401 +from .hdivcurl import HDivElement # noqa: F401 from .quadrature_element import QuadratureElement # noqa: F401 from . import quadrature # noqa: F401 diff --git a/finat/hdivcurl.py b/finat/hdivcurl.py new file mode 100644 index 000000000..27e2c94ac --- /dev/null +++ b/finat/hdivcurl.py @@ -0,0 +1,100 @@ +from __future__ import absolute_import, division, print_function + +from six import iteritems + +from FIAT.reference_element import LINE + +import gem +from finat.finiteelementbase import FiniteElementBase +from finat.tensor_product import TensorProductElement + + +class HDivElement(FiniteElementBase): + def __init__(self, wrapped): + assert isinstance(wrapped, TensorProductElement) + if any(fe.formdegree is None for fe in wrapped.factors): + raise ValueError("Form degree of subelement is None, cannot H(div)!") + + formdegree = sum(fe.formdegree for fe in wrapped.factors) + if formdegree != wrapped.cell.get_spatial_dimension() - 1: + raise ValueError("H(div) requires (n-1)-form element!") + + super(HDivElement, self).__init__() + self.wrapped = wrapped + self.transform = select_hdiv_transformer(wrapped) + + @property + def cell(self): + return self.wrapped.cell + + @property + def degree(self): + return self.wrapped.degree + + @property + def formdegree(self): + return self.cell.get_spatial_dimension() - 1 + + def entity_dofs(self): + return self.wrapped.entity_dofs() + + def entity_closure_dofs(self): + return self.wrapped.entity_closure_dofs() + + def space_dimension(self): + return self.wrapped.space_dimension() + + @property + def index_shape(self): + return self.wrapped.index_shape + + @property + def value_shape(self): + return (self.cell.get_spatial_dimension(),) + + def basis_evaluation(self, order, ps, entity=None): + beta = self.get_indices() + zeta = self.get_value_indices() + + def promote(table): + v = gem.partial_indexed(table, beta) + u = gem.ListTensor(self.transform(v)) + return gem.ComponentTensor(gem.Indexed(u, zeta), beta + zeta) + + core_eval = self.wrapped.basis_evaluation(order, ps, entity) + return {alpha: promote(table) + for alpha, table in iteritems(core_eval)} + + def point_evaluation(self, order, refcoords, entity=None): + raise NotImplementedError + + @property + def mapping(self): + return "contravariant piola" + + +def select_hdiv_transformer(element): + # Assume: something x interval + assert len(element.factors) == 2 + assert element.factors[1].cell.get_shape() == LINE + + ks = tuple(fe.formdegree for fe in element.factors) + if ks == (0, 1): + return lambda v: [gem.Product(gem.Literal(-1), v), gem.Zero()] + elif ks == (1, 0): + return lambda v: [gem.Zero(), v] + elif ks == (2, 0): + return lambda v: [gem.Zero(), gem.Zero(), v] + elif ks == (1, 1): + if element.mapping == "contravariant piola": + return lambda v: [gem.Indexed(v, (0,)), + gem.Indexed(v, (1,)), + gem.Zero()] + elif element.mapping == "covariant piola": + return lambda v: [gem.Indexed(v, (1,)), + gem.Product(gem.Literal(-1), gem.Indexed(v, (0,))), + gem.Zero()] + else: + assert False, "Unexpected original mapping!" + else: + assert False, "Unexpected form degree combination!" From b6673783536d0c45a7910d9ca779afbcba3a2f12 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 26 Jul 2017 16:56:47 +0100 Subject: [PATCH 408/749] add HDivElement.point_evaluation --- finat/hdivcurl.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/finat/hdivcurl.py b/finat/hdivcurl.py index 27e2c94ac..b8a9efc2a 100644 --- a/finat/hdivcurl.py +++ b/finat/hdivcurl.py @@ -52,7 +52,7 @@ def index_shape(self): def value_shape(self): return (self.cell.get_spatial_dimension(),) - def basis_evaluation(self, order, ps, entity=None): + def _transform_evaluation(self, core_eval): beta = self.get_indices() zeta = self.get_value_indices() @@ -61,12 +61,16 @@ def promote(table): u = gem.ListTensor(self.transform(v)) return gem.ComponentTensor(gem.Indexed(u, zeta), beta + zeta) - core_eval = self.wrapped.basis_evaluation(order, ps, entity) return {alpha: promote(table) for alpha, table in iteritems(core_eval)} + def basis_evaluation(self, order, ps, entity=None): + core_eval = self.wrapped.basis_evaluation(order, ps, entity) + return self._transform_evaluation(core_eval) + def point_evaluation(self, order, refcoords, entity=None): - raise NotImplementedError + core_eval = self.wrapped.point_evaluation(order, refcoords, entity) + return self._transform_evaluation(core_eval) @property def mapping(self): From 3180c147c22fc5432abc869dbd16e0f77a14410d Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 26 Jul 2017 17:25:02 +0100 Subject: [PATCH 409/749] add HCurlElement --- finat/__init__.py | 2 +- finat/hdivcurl.py | 94 +++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 80 insertions(+), 16 deletions(-) diff --git a/finat/__init__.py b/finat/__init__.py index 10ba398b7..89859ff3d 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -8,6 +8,6 @@ from .tensor_product import TensorProductElement # noqa: F401 from .quadrilateral import QuadrilateralElement # noqa: F401 from .enriched import EnrichedElement # noqa: F401 -from .hdivcurl import HDivElement # noqa: F401 +from .hdivcurl import HCurlElement, HDivElement # noqa: F401 from .quadrature_element import QuadratureElement # noqa: F401 from . import quadrature # noqa: F401 diff --git a/finat/hdivcurl.py b/finat/hdivcurl.py index b8a9efc2a..f1ee2acc8 100644 --- a/finat/hdivcurl.py +++ b/finat/hdivcurl.py @@ -9,19 +9,13 @@ from finat.tensor_product import TensorProductElement -class HDivElement(FiniteElementBase): - def __init__(self, wrapped): - assert isinstance(wrapped, TensorProductElement) - if any(fe.formdegree is None for fe in wrapped.factors): - raise ValueError("Form degree of subelement is None, cannot H(div)!") +class WrapperElementBase(FiniteElementBase): + """Common base class for H(div) and H(curl) element wrappers.""" - formdegree = sum(fe.formdegree for fe in wrapped.factors) - if formdegree != wrapped.cell.get_spatial_dimension() - 1: - raise ValueError("H(div) requires (n-1)-form element!") - - super(HDivElement, self).__init__() + def __init__(self, wrapped, transform): + super(WrapperElementBase, self).__init__() self.wrapped = wrapped - self.transform = select_hdiv_transformer(wrapped) + self.transform = transform @property def cell(self): @@ -31,10 +25,6 @@ def cell(self): def degree(self): return self.wrapped.degree - @property - def formdegree(self): - return self.cell.get_spatial_dimension() - 1 - def entity_dofs(self): return self.wrapped.entity_dofs() @@ -72,11 +62,55 @@ def point_evaluation(self, order, refcoords, entity=None): core_eval = self.wrapped.point_evaluation(order, refcoords, entity) return self._transform_evaluation(core_eval) + +class HDivElement(WrapperElementBase): + """H(div) wrapper element for tensor product elements.""" + + def __init__(self, wrapped): + assert isinstance(wrapped, TensorProductElement) + if any(fe.formdegree is None for fe in wrapped.factors): + raise ValueError("Form degree of subelement is None, cannot H(div)!") + + formdegree = sum(fe.formdegree for fe in wrapped.factors) + if formdegree != wrapped.cell.get_spatial_dimension() - 1: + raise ValueError("H(div) requires (n-1)-form element!") + + transform = select_hdiv_transformer(wrapped) + super(HDivElement, self).__init__(wrapped, transform) + + @property + def formdegree(self): + return self.cell.get_spatial_dimension() - 1 + @property def mapping(self): return "contravariant piola" +class HCurlElement(WrapperElementBase): + """H(curl) wrapper element for tensor product elements.""" + + def __init__(self, wrapped): + assert isinstance(wrapped, TensorProductElement) + if any(fe.formdegree is None for fe in wrapped.factors): + raise ValueError("Form degree of subelement is None, cannot H(curl)!") + + formdegree = sum(fe.formdegree for fe in wrapped.factors) + if formdegree != 1: + raise ValueError("H(curl) requires 1-form element!") + + transform = select_hcurl_transformer(wrapped) + super(HCurlElement, self).__init__(wrapped, transform) + + @property + def formdegree(self): + return 1 + + @property + def mapping(self): + return "covariant piola" + + def select_hdiv_transformer(element): # Assume: something x interval assert len(element.factors) == 2 @@ -102,3 +136,33 @@ def select_hdiv_transformer(element): assert False, "Unexpected original mapping!" else: assert False, "Unexpected form degree combination!" + + +def select_hcurl_transformer(element): + # Assume: something x interval + assert len(element.factors) == 2 + assert element.factors[1].cell.get_shape() == LINE + + dim = element.cell.get_spatial_dimension() + ks = tuple(fe.formdegree for fe in element.factors) + if element.mapping == "affine": + if ks == (1, 0): + # Can only be 2D + return lambda v: [v, gem.Zero()] + elif ks == (0, 1): + # Can be any spatial dimension + return lambda v: [gem.Zero()] * (dim - 1) + [v] + else: + assert False + elif element.mapping == "covariant piola": + # Second factor must be continuous interval + return lambda v: [gem.Indexed(v, (0,)), + gem.Indexed(v, (1,)), + gem.Zero()] + elif element.mapping == "contravariant piola": + # Second factor must be continuous interval + return lambda v: [gem.Product(gem.Literal(-1), gem.Indexed(v, (1,))), + gem.Indexed(v, (0,)), + gem.Zero()] + else: + assert False, "Unexpected original mapping!" From 133caccc73b3e63a0f9b53a0250422e73c3a664e Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 27 Jul 2017 11:36:02 +0100 Subject: [PATCH 410/749] sum factorise H(div) / H(curl) coefficient evaluation --- gem/optimise.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index ef44fcd50..10cbd0a19 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -2,9 +2,9 @@ expressions.""" from __future__ import absolute_import, print_function, division -from six.moves import filter, map, zip +from six.moves import filter, map, zip, zip_longest -from collections import defaultdict +from collections import OrderedDict, defaultdict from functools import partial, reduce from itertools import combinations, permutations @@ -12,7 +12,8 @@ from singledispatch import singledispatch from gem.utils import groupby -from gem.node import Memoizer, MemoizerArg, reuse_if_untouched, reuse_if_untouched_arg +from gem.node import (Memoizer, MemoizerArg, reuse_if_untouched, + reuse_if_untouched_arg, traversal) from gem.gem import (Node, Terminal, Failure, Identity, Literal, Zero, Product, Sum, Comparison, Conditional, Division, Index, VariableIndex, Indexed, FlexiblyIndexed, @@ -490,7 +491,29 @@ def contraction(expression): expression, = remove_componenttensors([expression]) # Flatten product tree, eliminate deltas, sum factorise - return sum_factorise(*delta_elimination(*traverse_product(expression))) + def rebuild(expression): + return sum_factorise(*delta_elimination(*traverse_product(expression))) + + # ListTensor free indices + lt_fis = OrderedDict() + for node in traversal((expression,)): + if isinstance(node, Indexed): + child, = node.children + if isinstance(child, ListTensor): + lt_fis.update(zip_longest(node.multiindex, ())) + lt_fis = tuple(index for index in lt_fis if index in expression.free_indices) + + if lt_fis: + # Rebuild each split component + tensor = ComponentTensor(expression, lt_fis) + entries = [Indexed(tensor, zeta) for zeta in numpy.ndindex(tensor.shape)] + entries = remove_componenttensors(entries) + return Indexed(ListTensor( + numpy.array(list(map(rebuild, entries))).reshape(tensor.shape) + ), lt_fis) + else: + # Rebuild whole expression at once + return rebuild(expression) @singledispatch From 116559cb11d80f62cf4a16b086cdb1f97aec8918 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 27 Jul 2017 14:55:11 +0100 Subject: [PATCH 411/749] add more comments --- finat/hdivcurl.py | 81 +++++++++++++++++++++++++++++++---------------- 1 file changed, 54 insertions(+), 27 deletions(-) diff --git a/finat/hdivcurl.py b/finat/hdivcurl.py index f1ee2acc8..5a98e57a0 100644 --- a/finat/hdivcurl.py +++ b/finat/hdivcurl.py @@ -12,31 +12,38 @@ class WrapperElementBase(FiniteElementBase): """Common base class for H(div) and H(curl) element wrappers.""" - def __init__(self, wrapped, transform): + def __init__(self, wrappee, transform): super(WrapperElementBase, self).__init__() - self.wrapped = wrapped + self.wrappee = wrappee + """An appropriate tensor product FInAT element whose basis + functions are mapped to produce an H(div) or H(curl) + conforming element.""" + self.transform = transform + """A transformation applied on the scalar/vector values of the + wrapped element to produce an H(div) or H(curl) conforming + element.""" @property def cell(self): - return self.wrapped.cell + return self.wrappee.cell @property def degree(self): - return self.wrapped.degree + return self.wrappee.degree def entity_dofs(self): - return self.wrapped.entity_dofs() + return self.wrappee.entity_dofs() def entity_closure_dofs(self): - return self.wrapped.entity_closure_dofs() + return self.wrappee.entity_closure_dofs() def space_dimension(self): - return self.wrapped.space_dimension() + return self.wrappee.space_dimension() @property def index_shape(self): - return self.wrapped.index_shape + return self.wrappee.index_shape @property def value_shape(self): @@ -55,28 +62,28 @@ def promote(table): for alpha, table in iteritems(core_eval)} def basis_evaluation(self, order, ps, entity=None): - core_eval = self.wrapped.basis_evaluation(order, ps, entity) + core_eval = self.wrappee.basis_evaluation(order, ps, entity) return self._transform_evaluation(core_eval) def point_evaluation(self, order, refcoords, entity=None): - core_eval = self.wrapped.point_evaluation(order, refcoords, entity) + core_eval = self.wrappee.point_evaluation(order, refcoords, entity) return self._transform_evaluation(core_eval) class HDivElement(WrapperElementBase): """H(div) wrapper element for tensor product elements.""" - def __init__(self, wrapped): - assert isinstance(wrapped, TensorProductElement) - if any(fe.formdegree is None for fe in wrapped.factors): + def __init__(self, wrappee): + assert isinstance(wrappee, TensorProductElement) + if any(fe.formdegree is None for fe in wrappee.factors): raise ValueError("Form degree of subelement is None, cannot H(div)!") - formdegree = sum(fe.formdegree for fe in wrapped.factors) - if formdegree != wrapped.cell.get_spatial_dimension() - 1: + formdegree = sum(fe.formdegree for fe in wrappee.factors) + if formdegree != wrappee.cell.get_spatial_dimension() - 1: raise ValueError("H(div) requires (n-1)-form element!") - transform = select_hdiv_transformer(wrapped) - super(HDivElement, self).__init__(wrapped, transform) + transform = select_hdiv_transformer(wrappee) + super(HDivElement, self).__init__(wrappee, transform) @property def formdegree(self): @@ -90,17 +97,17 @@ def mapping(self): class HCurlElement(WrapperElementBase): """H(curl) wrapper element for tensor product elements.""" - def __init__(self, wrapped): - assert isinstance(wrapped, TensorProductElement) - if any(fe.formdegree is None for fe in wrapped.factors): + def __init__(self, wrappee): + assert isinstance(wrappee, TensorProductElement) + if any(fe.formdegree is None for fe in wrappee.factors): raise ValueError("Form degree of subelement is None, cannot H(curl)!") - formdegree = sum(fe.formdegree for fe in wrapped.factors) + formdegree = sum(fe.formdegree for fe in wrappee.factors) if formdegree != 1: raise ValueError("H(curl) requires 1-form element!") - transform = select_hcurl_transformer(wrapped) - super(HCurlElement, self).__init__(wrapped, transform) + transform = select_hcurl_transformer(wrappee) + super(HCurlElement, self).__init__(wrappee, transform) @property def formdegree(self): @@ -116,19 +123,32 @@ def select_hdiv_transformer(element): assert len(element.factors) == 2 assert element.factors[1].cell.get_shape() == LINE + # Globally consistent edge orientations of the reference + # quadrilateral: rightward horizontally, upward vertically. + # Their rotation by 90 degrees anticlockwise is interpreted as the + # positive direction for normal vectors. ks = tuple(fe.formdegree for fe in element.factors) if ks == (0, 1): + # Make the scalar value the leftward-pointing normal on the + # y-aligned edges. return lambda v: [gem.Product(gem.Literal(-1), v), gem.Zero()] elif ks == (1, 0): + # Make the scalar value the upward-pointing normal on the + # x-aligned edges. return lambda v: [gem.Zero(), v] elif ks == (2, 0): + # Same for 3D, so z-plane. return lambda v: [gem.Zero(), gem.Zero(), v] elif ks == (1, 1): if element.mapping == "contravariant piola": + # Pad the 2-vector normal on the "base" cell into a + # 3-vector, maintaining direction. return lambda v: [gem.Indexed(v, (0,)), gem.Indexed(v, (1,)), gem.Zero()] elif element.mapping == "covariant piola": + # Rotate the 2-vector tangential component on the "base" + # cell 90 degrees anticlockwise into a 3-vector and pad. return lambda v: [gem.Indexed(v, (1,)), gem.Product(gem.Literal(-1), gem.Indexed(v, (0,))), gem.Zero()] @@ -143,24 +163,31 @@ def select_hcurl_transformer(element): assert len(element.factors) == 2 assert element.factors[1].cell.get_shape() == LINE + # Globally consistent edge orientations of the reference + # quadrilateral: rightward horizontally, upward vertically. + # Tangential vectors interpret these as the positive direction. dim = element.cell.get_spatial_dimension() ks = tuple(fe.formdegree for fe in element.factors) if element.mapping == "affine": if ks == (1, 0): - # Can only be 2D + # Can only be 2D. Make the scalar value the + # rightward-pointing tangential on the x-aligned edges. return lambda v: [v, gem.Zero()] elif ks == (0, 1): - # Can be any spatial dimension + # Can be any spatial dimension. Make the scalar value the + # upward-pointing tangential. return lambda v: [gem.Zero()] * (dim - 1) + [v] else: assert False elif element.mapping == "covariant piola": - # Second factor must be continuous interval + # Second factor must be continuous interval. Just padding. return lambda v: [gem.Indexed(v, (0,)), gem.Indexed(v, (1,)), gem.Zero()] elif element.mapping == "contravariant piola": - # Second factor must be continuous interval + # Second factor must be continuous interval. Rotate the + # 2-vector tangential component on the "base" cell 90 degrees + # clockwise into a 3-vector and pad. return lambda v: [gem.Product(gem.Literal(-1), gem.Indexed(v, (1,))), gem.Indexed(v, (0,)), gem.Zero()] From 90b9d642fb3864eb53f385eaefebd06882368883 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 27 Jul 2017 17:01:10 +0100 Subject: [PATCH 412/749] add much description with a little example --- gem/unconcatenate.py | 52 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/gem/unconcatenate.py b/gem/unconcatenate.py index 0e306dc04..975b74c63 100644 --- a/gem/unconcatenate.py +++ b/gem/unconcatenate.py @@ -1,4 +1,54 @@ -"""Utility functions for decomposing Concatenate nodes.""" +"""Utility functions for decomposing Concatenate nodes. + +The exported functions are flatten and unconcatenate. +- flatten: destroys the structure preserved within Concatenate nodes, + essentially reducing FInAT provided tabulations to what + FIAT could have provided, so old code can continue to work. +- unconcatenate: split up (variable, expression) pairs along + Concatenate nodes, thus recovering the structure + within them, yet eliminating the Concatenate nodes. + +Let us see an example on unconcatenate. Let us consider the form + + div(v) * dx + +where v is an RTCF7 test function. This means that the assembled +local vector has 8 * 7 + 7 * 8 = 112 entries. So the compilation of +the form starts with a single assignment pair [(v, e)]. v is now the +indexed return variable, something equivalent to + + Indexed(Variable('A', (112,)), (j,)) + +where j is the basis function index of the argument. e is just a GEM +quadrature expression with j as its only free index. This will +contain the tabulation of the RTCF7 element, which will cause +something like + + C_j := Indexed(Concatenate(A, B), (j,)) + +to appear as a subexpression in e. unconcatenate splits e along C_j +into e_1 and e_2 such that + + e_1 := e /. C_j -> A_{ja1,ja2}, and + e_2 := e /. C_j -> B_{jb1,jb2}. + +The split indices ja1, ja2, jb1, and jb2 have extents 8, 7, 7, and 8 +respectively (see the RTCF7 element construction above). So the +result of unconcatenate will be the list of pairs + + [(v_1, e_2), (v_2, e_2)] + +where v_1 is the first 56 entries of v, reshaped as an 8 x 7 matrix, +indexed with (ja1, ja2), and similarly, v_2 is the second 56 entries +of v, reshaped as a 7 x 8 matrix, indexed with (jb1, jb2). + +The unconcatenated form allows for sum factorisation of tensor product +elements as usual. This pair splitting is also applicable to +coefficient evaluation: take the local basis function coefficients as +the variable, the FInAT tabulation of the element as the expression, +and apply "matrix-vector multifunction" for each pair after +unconcatenation, and then add up the results. +""" from __future__ import absolute_import, print_function, division from six.moves import map, range, zip From c798c57a94e96ef60639e58ae28d274907e4902e Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 27 Jul 2017 17:41:52 +0100 Subject: [PATCH 413/749] add missing .mapping and .formdegree to QuadratureElement --- finat/quadrature_element.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/finat/quadrature_element.py b/finat/quadrature_element.py index 4c5bcce2f..7c802b4ce 100644 --- a/finat/quadrature_element.py +++ b/finat/quadrature_element.py @@ -27,6 +27,10 @@ def cell(self): def degree(self): raise NotImplementedError("QuadratureElement does not represent a polynomial space.") + @property + def formdegree(self): + return None + @cached_property def _entity_dofs(self): # Inspired by ffc/quadratureelement.py @@ -77,3 +81,7 @@ def basis_evaluation(self, order, ps, entity=None): def point_evaluation(self, order, refcoords, entity=None): raise NotImplementedError("QuadratureElement cannot do point evaluation!") + + @property + def mapping(self): + return "affine" From d1274bb99baacf452930ad2232a11163e5f62bf8 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 28 Jul 2017 10:22:07 +0100 Subject: [PATCH 414/749] a little more explanation --- gem/optimise.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index 10cbd0a19..11979fe14 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -494,8 +494,13 @@ def contraction(expression): def rebuild(expression): return sum_factorise(*delta_elimination(*traverse_product(expression))) - # ListTensor free indices - lt_fis = OrderedDict() + # Sometimes the value shape is composed as a ListTensor, which + # could get in the way of decomposing factors. In particular, + # this is the case for H(div) and H(curl) conforming tensor + # product elements. So if ListTensors are used, they are pulled + # out to be outermost, so we can straightforwardly factorise each + # of its entries. + lt_fis = OrderedDict() # ListTensor free indices for node in traversal((expression,)): if isinstance(node, Indexed): child, = node.children From 3b5a5888d31077349cddedd90e773c7a2bb4ce54 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 14 Jul 2017 12:38:57 +0100 Subject: [PATCH 415/749] add GL and GLL point set types --- finat/point_set.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/finat/point_set.py b/finat/point_set.py index b49389229..bfea7d24e 100644 --- a/finat/point_set.py +++ b/finat/point_set.py @@ -64,6 +64,26 @@ def almost_equal(self, other, tolerance=1e-12): numpy.allclose(self.points, other.points, rtol=0, atol=tolerance) +class GaussLegendrePointSet(PointSet): + """Gauss-Legendre quadrature points on the interval. + + This facilitates implementing discontinuous spectral elements. + """ + def __init__(self, points): + super(GaussLegendrePointSet, self).__init__(points) + assert self.points.shape[1] == 1 + + +class GaussLobattoLegendrePointSet(PointSet): + """Gauss-Lobatto-Legendre quadrature points on the interval. + + This facilitates implementing continuous spectral elements. + """ + def __init__(self, points): + super(GaussLobattoLegendrePointSet, self).__init__(points) + assert self.points.shape[1] == 1 + + class TensorPointSet(AbstractPointSet): def __init__(self, factors): From 0e17288461cf9e771fa9a348c05245f4fe3b0f98 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 28 Jul 2017 22:31:31 +0100 Subject: [PATCH 416/749] move GL and GLL elements to spectral.py --- finat/__init__.py | 3 ++- finat/fiat_elements.py | 10 ---------- finat/spectral.py | 21 +++++++++++++++++++++ 3 files changed, 23 insertions(+), 11 deletions(-) create mode 100644 finat/spectral.py diff --git a/finat/__init__.py b/finat/__init__.py index 89859ff3d..e21089d10 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -1,9 +1,10 @@ from __future__ import absolute_import, print_function, division -from .fiat_elements import Lagrange, DiscontinuousLagrange, GaussLobattoLegendre, GaussLegendre # noqa: F401 +from .fiat_elements import Lagrange, DiscontinuousLagrange # noqa: F401 from .fiat_elements import RaviartThomas, DiscontinuousRaviartThomas # noqa: F401 from .fiat_elements import BrezziDouglasMarini, BrezziDouglasFortinMarini # noqa: F401 from .fiat_elements import Nedelec, NedelecSecondKind, Regge # noqa: F401 +from .spectral import GaussLobattoLegendre, GaussLegendre # noqa: F401 from .tensorfiniteelement import TensorFiniteElement # noqa: F401 from .tensor_product import TensorProductElement # noqa: F401 from .quadrilateral import QuadrilateralElement # noqa: F401 diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index cbe1d9be8..0ffc5c3b0 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -262,21 +262,11 @@ def __init__(self, cell, degree): super(Lagrange, self).__init__(FIAT.Lagrange(cell, degree)) -class GaussLobattoLegendre(ScalarFiatElement): - def __init__(self, cell, degree): - super(GaussLobattoLegendre, self).__init__(FIAT.GaussLobattoLegendre(cell, degree)) - - class DiscontinuousLagrange(ScalarFiatElement): def __init__(self, cell, degree): super(DiscontinuousLagrange, self).__init__(FIAT.DiscontinuousLagrange(cell, degree)) -class GaussLegendre(ScalarFiatElement): - def __init__(self, cell, degree): - super(GaussLegendre, self).__init__(FIAT.GaussLegendre(cell, degree)) - - class VectorFiatElement(FiatElementBase): @property def value_shape(self): diff --git a/finat/spectral.py b/finat/spectral.py new file mode 100644 index 000000000..b91e9d93c --- /dev/null +++ b/finat/spectral.py @@ -0,0 +1,21 @@ +from __future__ import absolute_import, print_function, division + +import FIAT + +from finat.fiat_elements import ScalarFiatElement + + +class GaussLobattoLegendre(ScalarFiatElement): + """1D continuous element with nodes at the Gauss-Lobatto points.""" + + def __init__(self, cell, degree): + fiat_element = FIAT.GaussLobattoLegendre(cell, degree) + super(GaussLobattoLegendre, self).__init__(fiat_element) + + +class GaussLegendre(ScalarFiatElement): + """1D discontinuous element with nodes at the Gauss-Legendre points.""" + + def __init__(self, cell, degree): + fiat_element = FIAT.GaussLegendre(cell, degree) + super(GaussLegendre, self).__init__(fiat_element) From 3c8bef019da7c8dba5d82aba3672a07f3ab71d6c Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 28 Jul 2017 22:41:36 +0100 Subject: [PATCH 417/749] optimise GL/GLL zeroth derivative if matching points --- finat/spectral.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/finat/spectral.py b/finat/spectral.py index b91e9d93c..c6dff1109 100644 --- a/finat/spectral.py +++ b/finat/spectral.py @@ -2,7 +2,10 @@ import FIAT +import gem + from finat.fiat_elements import ScalarFiatElement +from finat.point_set import GaussLobattoLegendrePointSet, GaussLegendrePointSet class GaussLobattoLegendre(ScalarFiatElement): @@ -12,6 +15,26 @@ def __init__(self, cell, degree): fiat_element = FIAT.GaussLobattoLegendre(cell, degree) super(GaussLobattoLegendre, self).__init__(fiat_element) + def basis_evaluation(self, order, ps, entity=None): + '''Return code for evaluating the element at known points on the + reference element. + + :param order: return derivatives up to this order. + :param ps: the point set. + :param entity: the cell entity on which to tabulate. + ''' + result = super(GaussLobattoLegendre, self).basis_evaluation(order, ps, entity) + cell_dimension = self.cell.get_dimension() + if entity is None or entity == (cell_dimension, 0): # on cell interior + space_dim = self.space_dimension() + if isinstance(ps, GaussLobattoLegendrePointSet) and len(ps.points) == space_dim: + # Bingo: evaluation points match node locations! + spatial_dim = self.cell.get_spatial_dimension() + q, = ps.indices + r, = self.get_indices() + result[(0,) * spatial_dim] = gem.ComponentTensor(gem.Delta(q, r), (r,)) + return result + class GaussLegendre(ScalarFiatElement): """1D discontinuous element with nodes at the Gauss-Legendre points.""" @@ -19,3 +42,23 @@ class GaussLegendre(ScalarFiatElement): def __init__(self, cell, degree): fiat_element = FIAT.GaussLegendre(cell, degree) super(GaussLegendre, self).__init__(fiat_element) + + def basis_evaluation(self, order, ps, entity=None): + '''Return code for evaluating the element at known points on the + reference element. + + :param order: return derivatives up to this order. + :param ps: the point set. + :param entity: the cell entity on which to tabulate. + ''' + result = super(GaussLegendre, self).basis_evaluation(order, ps, entity) + cell_dimension = self.cell.get_dimension() + if entity is None or entity == (cell_dimension, 0): # on cell interior + space_dim = self.space_dimension() + if isinstance(ps, GaussLegendrePointSet) and len(ps.points) == space_dim: + # Bingo: evaluation points match node locations! + spatial_dim = self.cell.get_spatial_dimension() + q, = ps.indices + r, = self.get_indices() + result[(0,) * spatial_dim] = gem.ComponentTensor(gem.Delta(q, r), (r,)) + return result From fcccb99e284d9aaffc1b903e2de40e447b99610c Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 28 Jul 2017 22:46:17 +0100 Subject: [PATCH 418/749] refactor generic QuadratureRule Now goes with any labelled flat point set. --- finat/quadrature.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/finat/quadrature.py b/finat/quadrature.py index fea017c43..1f6b5aec9 100644 --- a/finat/quadrature.py +++ b/finat/quadrature.py @@ -48,7 +48,7 @@ def make_quadrature(ref_el, degree, scheme="default"): raise ValueError("Need positive degree, not %d" % degree) fiat_rule = fiat_scheme(ref_el, degree, scheme) - return QuadratureRule(fiat_rule.get_points(), fiat_rule.get_weights()) + return QuadratureRule(PointSet(fiat_rule.get_points()), fiat_rule.get_weights()) class AbstractQuadratureRule(with_metaclass(ABCMeta)): @@ -68,16 +68,16 @@ def weight_expression(self): class QuadratureRule(AbstractQuadratureRule): """Generic quadrature rule with no internal structure.""" - def __init__(self, points, weights): + def __init__(self, point_set, weights): weights = numpy.asarray(weights) - assert len(points) == len(weights) + assert len(point_set.points) == len(weights) - self._points = numpy.asarray(points) + self.point_set = point_set self.weights = numpy.asarray(weights) @cached_property def point_set(self): - return PointSet(self._points) + pass # set at initialisation @cached_property def weight_expression(self): From f01ea6c37fc9b5ab11ee9be952370cbffab95194 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 31 Jul 2017 12:37:06 +0100 Subject: [PATCH 419/749] add FEniCS-style MixedElement --- finat/__init__.py | 1 + finat/mixed.py | 97 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 finat/mixed.py diff --git a/finat/__init__.py b/finat/__init__.py index 89859ff3d..b57a488f3 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -9,5 +9,6 @@ from .quadrilateral import QuadrilateralElement # noqa: F401 from .enriched import EnrichedElement # noqa: F401 from .hdivcurl import HCurlElement, HDivElement # noqa: F401 +from .mixed import MixedElement # noqa: F401 from .quadrature_element import QuadratureElement # noqa: F401 from . import quadrature # noqa: F401 diff --git a/finat/mixed.py b/finat/mixed.py new file mode 100644 index 000000000..7d2a4c38b --- /dev/null +++ b/finat/mixed.py @@ -0,0 +1,97 @@ +from __future__ import absolute_import, print_function, division +from six import iteritems +from six.moves import zip + +import numpy + +import gem + +from finat.finiteelementbase import FiniteElementBase +from finat.enriched import EnrichedElement + + +def MixedElement(elements): + """Constructor function for FEniCS-style mixed elements. + + Implements mixed element using :py:class:`EnrichedElement` and + value shape transformations with :py:class:`MixedSubElement`. + """ + sizes = [numpy.prod(element.value_shape, dtype=int) + for element in elements] + offsets = [int(offset) for offset in numpy.cumsum([0] + sizes)] + total_size = offsets.pop() + return EnrichedElement([MixedSubElement(element, total_size, offset) + for offset, element in zip(offsets, elements)]) + + +class MixedSubElement(FiniteElementBase): + """Element wrapper that flattens value shape and places the flattened + vector in a longer vector of zeros.""" + + def __init__(self, element, size, offset): + assert 0 <= offset <= size + assert offset + numpy.prod(element.value_shape, dtype=int) <= size + + super(MixedSubElement, self).__init__() + self.element = element + self.size = size + self.offset = offset + + @property + def cell(self): + return self.element.cell + + @property + def degree(self): + return self.element.degree + + @property + def formdegree(self): + return self.element.formdegree + + def entity_dofs(self): + return self.element.entity_dofs() + + def entity_closure_dofs(self): + return self.element.entity_closure_dofs() + + def space_dimension(self): + return self.element.space_dimension() + + @property + def index_shape(self): + return self.element.index_shape + + @property + def value_shape(self): + return (self.size,) + + def _transform(self, v): + u = [gem.Zero()] * self.size + for j, zeta in enumerate(numpy.ndindex(self.element.value_shape)): + u[self.offset + j] = gem.Indexed(v, zeta) + return u + + def _transform_evaluation(self, core_eval): + beta = self.get_indices() + zeta = self.get_value_indices() + + def promote(table): + v = gem.partial_indexed(table, beta) + u = gem.ListTensor(self._transform(v)) + return gem.ComponentTensor(gem.Indexed(u, zeta), beta + zeta) + + return {alpha: promote(table) + for alpha, table in iteritems(core_eval)} + + def basis_evaluation(self, order, ps, entity=None): + core_eval = self.element.basis_evaluation(order, ps, entity) + return self._transform_evaluation(core_eval) + + def point_evaluation(self, order, refcoords, entity=None): + core_eval = self.element.point_evaluation(order, refcoords, entity) + return self._transform_evaluation(core_eval) + + @property + def mapping(self): + return self.element.mapping From 88a04a2e62fdb523cd9e8a344e900595868bdf69 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 31 Jul 2017 14:14:49 +0100 Subject: [PATCH 420/749] make TensorFiniteElement transposable --- finat/tensorfiniteelement.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/finat/tensorfiniteelement.py b/finat/tensorfiniteelement.py index 67d1441af..668052a17 100644 --- a/finat/tensorfiniteelement.py +++ b/finat/tensorfiniteelement.py @@ -12,7 +12,7 @@ class TensorFiniteElement(FiniteElementBase): - def __init__(self, element, shape): + def __init__(self, element, shape, transpose=False): # TODO: Update docstring for arbitrary rank! r"""A Finite element whose basis functions have the form: @@ -29,6 +29,8 @@ def __init__(self, element, shape): :param element: The scalar finite element. :param shape: The geometric shape of the tensor element. + :param transpose: Tensor indices come before scalar basis + function indices (boolean). :math:`\boldsymbol\phi_{i\alpha\beta}` is, of course, tensor-valued. If we subscript the vector-value with :math:`\gamma\epsilon` then we can write: @@ -41,6 +43,7 @@ def __init__(self, element, shape): super(TensorFiniteElement, self).__init__() self._base_element = element self._shape = shape + self._transpose = transpose @property def base_element(self): @@ -63,15 +66,18 @@ def entity_dofs(self): raise NotImplementedError("No one uses this!") def space_dimension(self): - return numpy.prod((self._base_element.space_dimension(),) + self._shape) + return int(numpy.prod(self.index_shape)) @property def index_shape(self): - return self._base_element.index_shape + self._shape + if self._transpose: + return self._shape + self._base_element.index_shape + else: + return self._base_element.index_shape + self._shape @property def value_shape(self): - return self._base_element.value_shape + self._shape + return self._shape + self._base_element.value_shape def basis_evaluation(self, order, ps, entity=None): r"""Produce the recipe for basis function evaluation at a set of points :math:`q`: @@ -101,11 +107,16 @@ def _tensorise(self, scalar_evaluation): deltas = reduce(gem.Product, (gem.Delta(j, k) for j, k in zip(tensor_i, tensor_vi))) + if self._transpose: + index_ordering = tensor_i + scalar_i + tensor_vi + scalar_vi + else: + index_ordering = scalar_i + tensor_i + tensor_vi + scalar_vi + result = {} for alpha, expr in iteritems(scalar_evaluation): result[alpha] = gem.ComponentTensor( gem.Product(deltas, gem.Indexed(expr, scalar_i + scalar_vi)), - scalar_i + tensor_i + scalar_vi + tensor_vi + index_ordering ) return result From 947d135dc15910aee5016201a6cc394ccb8a9326 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 28 Jul 2017 22:57:36 +0100 Subject: [PATCH 421/749] correctly label FIAT line quadrature as Gauss-Legendre --- finat/quadrature.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/finat/quadrature.py b/finat/quadrature.py index 1f6b5aec9..4a4ffbe96 100644 --- a/finat/quadrature.py +++ b/finat/quadrature.py @@ -9,11 +9,11 @@ import gem from gem.utils import cached_property -from FIAT.reference_element import QUADRILATERAL, TENSORPRODUCT -# from FIAT.quadrature import compute_gauss_jacobi_rule as gauss_jacobi_rule +from FIAT.reference_element import LINE, QUADRILATERAL, TENSORPRODUCT +from FIAT.quadrature import GaussLegendreQuadratureLineRule from FIAT.quadrature_schemes import create_quadrature as fiat_scheme -from finat.point_set import PointSet, TensorPointSet +from finat.point_set import PointSet, GaussLegendrePointSet, TensorPointSet def make_quadrature(ref_el, degree, scheme="default"): @@ -47,6 +47,16 @@ def make_quadrature(ref_el, degree, scheme="default"): if degree < 0: raise ValueError("Need positive degree, not %d" % degree) + if ref_el.get_shape() == LINE: + # FIAT uses Gauss-Legendre line quadature, however, since we + # symbolically label it as such, we wish not to risk attaching + # the wrong label in case FIAT changes. So we explicitly ask + # for Gauss-Legendre line quadature. + num_points = (degree + 1 + 1) // 2 # exact integration + fiat_rule = GaussLegendreQuadratureLineRule(ref_el, num_points) + point_set = GaussLegendrePointSet(fiat_rule.get_points()) + return QuadratureRule(point_set, fiat_rule.get_weights()) + fiat_rule = fiat_scheme(ref_el, degree, scheme) return QuadratureRule(PointSet(fiat_rule.get_points()), fiat_rule.get_weights()) From a32071169c338fc15bc8c2b18f516379e2f0bcf1 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 1 Aug 2017 16:56:47 +0100 Subject: [PATCH 422/749] delay index substitution in delta_elimination --- gem/optimise.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index 11979fe14..bca31c4fc 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -242,6 +242,11 @@ def delta_elimination(sum_indices, factors): """ sum_indices = list(sum_indices) # copy for modification + def substitute(expression, from_, to_): + if from_ in expression.free_indices: + expression = Indexed(ComponentTensor(expression, (from_,)), (to_,)) + return expression + delta_queue = [(f, index) for f in factors if isinstance(f, Delta) for index in (f.i, f.j) if index in sum_indices] @@ -251,8 +256,7 @@ def delta_elimination(sum_indices, factors): sum_indices.remove(from_) - mapper = MemoizerArg(filtered_replace_indices) - factors = [mapper(e, ((from_, to_),)) for e in factors] + factors = [substitute(f, from_, to_) for f in factors] delta_queue = [(f, index) for f in factors if isinstance(f, Delta) @@ -492,7 +496,9 @@ def contraction(expression): # Flatten product tree, eliminate deltas, sum factorise def rebuild(expression): - return sum_factorise(*delta_elimination(*traverse_product(expression))) + sum_indices, factors = delta_elimination(*traverse_product(expression)) + factors = remove_componenttensors(factors) + return sum_factorise(sum_indices, factors) # Sometimes the value shape is composed as a ListTensor, which # could get in the way of decomposing factors. In particular, From 602fd028bbee03312e42385fc0d9f4902383d32a Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 2 Aug 2017 10:41:23 +0100 Subject: [PATCH 423/749] make docstring slightly better --- finat/tensorfiniteelement.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/finat/tensorfiniteelement.py b/finat/tensorfiniteelement.py index 668052a17..f6c9f6033 100644 --- a/finat/tensorfiniteelement.py +++ b/finat/tensorfiniteelement.py @@ -29,8 +29,11 @@ def __init__(self, element, shape, transpose=False): :param element: The scalar finite element. :param shape: The geometric shape of the tensor element. - :param transpose: Tensor indices come before scalar basis - function indices (boolean). + :param transpose: Changes the DoF ordering from the + Firedrake-style XYZ XYZ XYZ XYZ to the + FEniCS-style XXXX YYYY ZZZZ. That is, + tensor shape indices come before the scalar + basis function indices when transpose=True. :math:`\boldsymbol\phi_{i\alpha\beta}` is, of course, tensor-valued. If we subscript the vector-value with :math:`\gamma\epsilon` then we can write: From 0d076ecfec61b006675e02b7bea3820609f7f53e Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 31 Jul 2017 12:42:11 +0100 Subject: [PATCH 424/749] GEM: simplify Concatenate of Zero nodes --- gem/gem.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/gem/gem.py b/gem/gem.py index 0ea5c9646..00362eaa7 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -697,8 +697,14 @@ class Concatenate(Node): """ __slots__ = ('children',) - def __init__(self, *children): + def __new__(cls, *children): + if all(isinstance(child, Zero) for child in children): + size = sum(numpy.prod(child.shape, dtype=int) for child in children) + return Zero((size,)) + + self = super(Concatenate, cls).__new__(cls) self.children = children + return self @property def shape(self): From eb7701e107524ef831a65dc812c3dfcae70803f0 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 2 Aug 2017 14:42:14 +0100 Subject: [PATCH 425/749] obsessive casting to int --- gem/gem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 00362eaa7..3b5c997f8 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -699,7 +699,7 @@ class Concatenate(Node): def __new__(cls, *children): if all(isinstance(child, Zero) for child in children): - size = sum(numpy.prod(child.shape, dtype=int) for child in children) + size = int(sum(numpy.prod(child.shape, dtype=int) for child in children)) return Zero((size,)) self = super(Concatenate, cls).__new__(cls) @@ -708,7 +708,7 @@ def __new__(cls, *children): @property def shape(self): - return (sum(numpy.prod(child.shape, dtype=int) for child in self.children),) + return (int(sum(numpy.prod(child.shape, dtype=int) for child in self.children)),) class Delta(Scalar, Terminal): From 166f9ab3ed458da728002c8c42766098c9eb627e Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 2 Aug 2017 15:21:30 +0100 Subject: [PATCH 426/749] move COFFEE algorithm from TSFC to GEM Two modes share this: 'coffee' and 'spectral'. --- gem/coffee.py | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 gem/coffee.py diff --git a/gem/coffee.py b/gem/coffee.py new file mode 100644 index 000000000..c47eb5196 --- /dev/null +++ b/gem/coffee.py @@ -0,0 +1,202 @@ +"""This module contains an implementation of the COFFEE optimisation +algorithm operating on a GEM representation. + +This file is NOT for code generation as a COFFEE AST. +""" + +from __future__ import absolute_import, print_function, division +from six.moves import map, range + +from collections import OrderedDict +import itertools +import logging + +import numpy + +from gem.gem import IndexSum, one +from gem.optimise import make_sum, make_product +from gem.refactorise import Monomial +from gem.utils import groupby + + +try: + from firedrake import Citations + Citations().register("Luporini2016") +except ImportError: + pass + + +__all__ = ['optimise_monomial_sum'] + + +def monomial_sum_to_expression(monomial_sum): + """Convert a monomial sum to a GEM expression. + + :arg monomial_sum: an iterable of :class:`Monomial`s + + :returns: GEM expression + """ + indexsums = [] # The result is summation of indexsums + # Group monomials according to their sum indices + groups = groupby(monomial_sum, key=lambda m: frozenset(m.sum_indices)) + # Create IndexSum's from each monomial group + for _, monomials in groups: + sum_indices = monomials[0].sum_indices + products = [make_product(monomial.atomics + (monomial.rest,)) for monomial in monomials] + indexsums.append(IndexSum(make_sum(products), sum_indices)) + return make_sum(indexsums) + + +def index_extent(factor, argument_indices): + """Compute the product of the extents of argument indices of a GEM expression + + :arg factor: GEM expression + :arg argument_indices: set of argument indices + + :returns: product of extents of argument indices + """ + return numpy.prod([i.extent for i in factor.free_indices if i in argument_indices]) + + +def find_optimal_atomics(monomials, argument_indices): + """Find optimal atomic common subexpressions, which produce least number of + terms in the resultant IndexSum when factorised. + + :arg monomials: A list of :class:`Monomial`s, all of which should have + the same sum indices + :arg argument_indices: tuple of argument indices + + :returns: list of atomic GEM expressions + """ + atomics = tuple(OrderedDict.fromkeys(itertools.chain(*(monomial.atomics for monomial in monomials)))) + + def cost(solution): + extent = sum(map(lambda atomic: index_extent(atomic, argument_indices), solution)) + # Prefer shorter solutions, but larger extents + return (len(solution), -extent) + + optimal_solution = set(atomics) # pessimal but feasible solution + solution = set() + + max_it = 1 << 12 + it = iter(range(max_it)) + + def solve(idx): + while idx < len(monomials) and solution.intersection(monomials[idx].atomics): + idx += 1 + + if idx < len(monomials): + if len(solution) < len(optimal_solution): + for atomic in monomials[idx].atomics: + solution.add(atomic) + solve(idx + 1) + solution.remove(atomic) + else: + if cost(solution) < cost(optimal_solution): + optimal_solution.clear() + optimal_solution.update(solution) + next(it) + + try: + solve(0) + except StopIteration: + logger = logging.getLogger('tsfc') + logger.warning("Solution to ILP problem may not be optimal: search " + "interrupted after examining %d solutions.", max_it) + + return tuple(atomic for atomic in atomics if atomic in optimal_solution) + + +def factorise_atomics(monomials, optimal_atomics, argument_indices): + """Group and factorise monomials using a list of atomics as common + subexpressions. Create new monomials for each group and optimise them recursively. + + :arg monomials: an iterable of :class:`Monomial`s, all of which should have + the same sum indices + :arg optimal_atomics: list of tuples of atomics to be used as common subexpression + :arg argument_indices: tuple of argument indices + + :returns: an iterable of :class:`Monomials`s after factorisation + """ + if not optimal_atomics or len(monomials) <= 1: + return monomials + + # Group monomials with respect to each optimal atomic + def group_key(monomial): + for oa in optimal_atomics: + if oa in monomial.atomics: + return oa + assert False, "Expect at least one optimal atomic per monomial." + factor_group = groupby(monomials, key=group_key) + + # We should not drop monomials + assert sum(len(ms) for _, ms in factor_group) == len(monomials) + + sum_indices = next(iter(monomials)).sum_indices + new_monomials = [] + for oa, monomials in factor_group: + # Create new MonomialSum for the factorised out terms + sub_monomials = [] + for monomial in monomials: + atomics = list(monomial.atomics) + atomics.remove(oa) # remove common factor + sub_monomials.append(Monomial((), tuple(atomics), monomial.rest)) + # Continue to factorise the remaining expression + sub_monomials = optimise_monomials(sub_monomials, argument_indices) + if len(sub_monomials) == 1: + # Factorised part is a product, we add back the common atomics then + # add to new MonomialSum directly rather than forming a product node + # Retaining the monomial structure enables applying associativity + # when forming GEM nodes later. + sub_monomial, = sub_monomials + new_monomials.append( + Monomial(sum_indices, (oa,) + sub_monomial.atomics, sub_monomial.rest)) + else: + # Factorised part is a summation, we need to create a new GEM node + # and multiply with the common factor + node = monomial_sum_to_expression(sub_monomials) + # If the free indices of the new node intersect with argument indices, + # add to the new monomial as `atomic`, otherwise add as `rest`. + # Note: we might want to continue to factorise with the new atomics + # by running optimise_monoials twice. + if set(argument_indices) & set(node.free_indices): + new_monomials.append(Monomial(sum_indices, (oa, node), one)) + else: + new_monomials.append(Monomial(sum_indices, (oa, ), node)) + return new_monomials + + +def optimise_monomial_sum(monomial_sum, argument_indices): + """Choose optimal common atomic subexpressions and factorise a + :class:`MonomialSum` object to create a GEM expression. + + :arg monomial_sum: a :class:`MonomialSum` object + :arg argument_indices: tuple of argument indices + + :returns: factorised GEM expression + """ + groups = groupby(monomial_sum, key=lambda m: frozenset(m.sum_indices)) + new_monomials = [] + for _, monomials in groups: + new_monomials.extend(optimise_monomials(monomials, argument_indices)) + return monomial_sum_to_expression(new_monomials) + + +def optimise_monomials(monomials, argument_indices): + """Choose optimal common atomic subexpressions and factorise an iterable + of monomials. + + :arg monomials: a list of :class:`Monomial`s, all of which should have + the same sum indices + :arg argument_indices: tuple of argument indices + + :returns: an iterable of factorised :class:`Monomials`s + """ + assert len(set(frozenset(m.sum_indices) for m in monomials)) <= 1,\ + "All monomials required to have same sum indices for factorisation" + + result = [m for m in monomials if not m.atomics] # skipped monomials + active_monomials = [m for m in monomials if m.atomics] + optimal_atomics = find_optimal_atomics(active_monomials, argument_indices) + result += factorise_atomics(active_monomials, optimal_atomics, argument_indices) + return result From 318b05d86fa68c1db748ab7cc522843c76abc22d Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 2 Aug 2017 15:23:53 +0100 Subject: [PATCH 427/749] rename argument indices to linear indices --- gem/coffee.py | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/gem/coffee.py b/gem/coffee.py index c47eb5196..6415e28df 100644 --- a/gem/coffee.py +++ b/gem/coffee.py @@ -47,31 +47,31 @@ def monomial_sum_to_expression(monomial_sum): return make_sum(indexsums) -def index_extent(factor, argument_indices): - """Compute the product of the extents of argument indices of a GEM expression +def index_extent(factor, linear_indices): + """Compute the product of the extents of linear indices of a GEM expression :arg factor: GEM expression - :arg argument_indices: set of argument indices + :arg linear_indices: set of linear indices - :returns: product of extents of argument indices + :returns: product of extents of linear indices """ - return numpy.prod([i.extent for i in factor.free_indices if i in argument_indices]) + return numpy.prod([i.extent for i in factor.free_indices if i in linear_indices]) -def find_optimal_atomics(monomials, argument_indices): +def find_optimal_atomics(monomials, linear_indices): """Find optimal atomic common subexpressions, which produce least number of terms in the resultant IndexSum when factorised. :arg monomials: A list of :class:`Monomial`s, all of which should have the same sum indices - :arg argument_indices: tuple of argument indices + :arg linear_indices: tuple of linear indices :returns: list of atomic GEM expressions """ atomics = tuple(OrderedDict.fromkeys(itertools.chain(*(monomial.atomics for monomial in monomials)))) def cost(solution): - extent = sum(map(lambda atomic: index_extent(atomic, argument_indices), solution)) + extent = sum(map(lambda atomic: index_extent(atomic, linear_indices), solution)) # Prefer shorter solutions, but larger extents return (len(solution), -extent) @@ -107,14 +107,14 @@ def solve(idx): return tuple(atomic for atomic in atomics if atomic in optimal_solution) -def factorise_atomics(monomials, optimal_atomics, argument_indices): +def factorise_atomics(monomials, optimal_atomics, linear_indices): """Group and factorise monomials using a list of atomics as common subexpressions. Create new monomials for each group and optimise them recursively. :arg monomials: an iterable of :class:`Monomial`s, all of which should have the same sum indices :arg optimal_atomics: list of tuples of atomics to be used as common subexpression - :arg argument_indices: tuple of argument indices + :arg linear_indices: tuple of linear indices :returns: an iterable of :class:`Monomials`s after factorisation """ @@ -142,7 +142,7 @@ def group_key(monomial): atomics.remove(oa) # remove common factor sub_monomials.append(Monomial((), tuple(atomics), monomial.rest)) # Continue to factorise the remaining expression - sub_monomials = optimise_monomials(sub_monomials, argument_indices) + sub_monomials = optimise_monomials(sub_monomials, linear_indices) if len(sub_monomials) == 1: # Factorised part is a product, we add back the common atomics then # add to new MonomialSum directly rather than forming a product node @@ -155,40 +155,40 @@ def group_key(monomial): # Factorised part is a summation, we need to create a new GEM node # and multiply with the common factor node = monomial_sum_to_expression(sub_monomials) - # If the free indices of the new node intersect with argument indices, + # If the free indices of the new node intersect with linear indices, # add to the new monomial as `atomic`, otherwise add as `rest`. # Note: we might want to continue to factorise with the new atomics # by running optimise_monoials twice. - if set(argument_indices) & set(node.free_indices): + if set(linear_indices) & set(node.free_indices): new_monomials.append(Monomial(sum_indices, (oa, node), one)) else: new_monomials.append(Monomial(sum_indices, (oa, ), node)) return new_monomials -def optimise_monomial_sum(monomial_sum, argument_indices): +def optimise_monomial_sum(monomial_sum, linear_indices): """Choose optimal common atomic subexpressions and factorise a :class:`MonomialSum` object to create a GEM expression. :arg monomial_sum: a :class:`MonomialSum` object - :arg argument_indices: tuple of argument indices + :arg linear_indices: tuple of linear indices :returns: factorised GEM expression """ groups = groupby(monomial_sum, key=lambda m: frozenset(m.sum_indices)) new_monomials = [] for _, monomials in groups: - new_monomials.extend(optimise_monomials(monomials, argument_indices)) + new_monomials.extend(optimise_monomials(monomials, linear_indices)) return monomial_sum_to_expression(new_monomials) -def optimise_monomials(monomials, argument_indices): +def optimise_monomials(monomials, linear_indices): """Choose optimal common atomic subexpressions and factorise an iterable of monomials. :arg monomials: a list of :class:`Monomial`s, all of which should have the same sum indices - :arg argument_indices: tuple of argument indices + :arg linear_indices: tuple of linear indices :returns: an iterable of factorised :class:`Monomials`s """ @@ -197,6 +197,6 @@ def optimise_monomials(monomials, argument_indices): result = [m for m in monomials if not m.atomics] # skipped monomials active_monomials = [m for m in monomials if m.atomics] - optimal_atomics = find_optimal_atomics(active_monomials, argument_indices) - result += factorise_atomics(active_monomials, optimal_atomics, argument_indices) + optimal_atomics = find_optimal_atomics(active_monomials, linear_indices) + result += factorise_atomics(active_monomials, optimal_atomics, linear_indices) return result From b402081ca837a5e5715e1e3b9e4450346ad4ea8d Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 2 Aug 2017 16:26:09 +0100 Subject: [PATCH 428/749] fix previous bug --- gem/optimise.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index bca31c4fc..d19006dec 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -243,9 +243,13 @@ def delta_elimination(sum_indices, factors): sum_indices = list(sum_indices) # copy for modification def substitute(expression, from_, to_): - if from_ in expression.free_indices: - expression = Indexed(ComponentTensor(expression, (from_,)), (to_,)) - return expression + if from_ not in expression.free_indices: + return expression + elif isinstance(expression, Delta): + mapper = MemoizerArg(filtered_replace_indices) + return mapper(expression, ((from_, to_),)) + else: + return Indexed(ComponentTensor(expression, (from_,)), (to_,)) delta_queue = [(f, index) for f in factors if isinstance(f, Delta) From 61517092a90152c0d733297935521c64a7002eb0 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Wed, 3 May 2017 10:49:21 +0100 Subject: [PATCH 429/749] Basis evaluation on dim-0 entities on quads --- finat/quadrilateral.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/quadrilateral.py b/finat/quadrilateral.py index 6f72d101d..1ca882509 100644 --- a/finat/quadrilateral.py +++ b/finat/quadrilateral.py @@ -92,6 +92,6 @@ def productise(entity): ((1, 0), 1)] return facets[entity_id] elif entity_dim == 0: - raise NotImplementedError("Not implemented for 0 dimension entities") + return ((0, 0), entity_id) else: raise ValueError("Illegal entity dimension %s" % entity_dim) From 763168c54626f5f461588c34b56e6ed03ab4e372 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Wed, 3 May 2017 10:51:21 +0100 Subject: [PATCH 430/749] Add entity_support_dofs method to FiniteElementBase --- finat/finiteelementbase.py | 76 ++++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index a51a13326..b5c6f3183 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -50,6 +50,43 @@ def entity_closure_dofs(self): element.''' return self._entity_closure_dofs + @cached_property + def _entity_support_dofs(self): + esd = {} + for entity_dim in self.cell.sub_entities.keys(): + beta = self.get_indices() + zeta = self.get_value_indices() + + entity_cell = self.cell.construct_subelement(entity_dim) + quad = make_quadrature(entity_cell, (2*numpy.array(self.degree)).tolist()) + + eps = 1.e-8 # Is this a safe value? + + result = {} + for f in self.entity_dofs()[entity_dim].keys(): + # Tabulate basis functions on the facet + vals, = itervalues(self.basis_evaluation(0, quad.point_set, entity=(entity_dim, f))) + # Integrate the square of the basis functions on the facet. + ints = gem.IndexSum( + gem.Product(gem.IndexSum(gem.Product(gem.Indexed(vals, beta + zeta), + gem.Indexed(vals, beta + zeta)), zeta), + quad.weight_expression), + quad.point_set.indices + ) + evaluation, = evaluate([gem.ComponentTensor(ints, beta)]) + ints = evaluation.arr.flatten() + assert evaluation.fids == () + result[f] = [dof for dof, i in enumerate(ints) if i > eps] + + esd[entity_dim] = result + return esd + + def entity_support_dofs(self): + '''Return the map of topological entities to degrees of + freedom that have non-zero support on those entities for the + finite element.''' + return self._entity_support_dofs + @abstractmethod def space_dimension(self): '''Return the dimension of the finite element space.''' @@ -107,43 +144,10 @@ def mapping(self): def entity_support_dofs(elem, entity_dim): - """Return the map of entity id to the degrees of freedom for which + '''Return the map of entity id to the degrees of freedom for which the corresponding basis functions take non-zero values. :arg elem: FInAT finite element :arg entity_dim: Dimension of the cell subentity. - """ - if not hasattr(elem, "_entity_support_dofs"): - elem._entity_support_dofs = {} - cache = elem._entity_support_dofs - try: - return cache[entity_dim] - except KeyError: - pass - - beta = elem.get_indices() - zeta = elem.get_value_indices() - - entity_cell = elem.cell.construct_subelement(entity_dim) - quad = make_quadrature(entity_cell, (2*numpy.array(elem.degree)).tolist()) - - eps = 1.e-8 # Is this a safe value? - - result = {} - for f in elem.entity_dofs()[entity_dim].keys(): - # Tabulate basis functions on the facet - vals, = itervalues(elem.basis_evaluation(0, quad.point_set, entity=(entity_dim, f))) - # Integrate the square of the basis functions on the facet. - ints = gem.IndexSum( - gem.Product(gem.IndexSum(gem.Product(gem.Indexed(vals, beta + zeta), - gem.Indexed(vals, beta + zeta)), zeta), - quad.weight_expression), - quad.point_set.indices - ) - evaluation, = evaluate([gem.ComponentTensor(ints, beta)]) - ints = evaluation.arr.flatten() - assert evaluation.fids == () - result[f] = [dof for dof, i in enumerate(ints) if i > eps] - - cache[entity_dim] = result - return result + ''' + return elem.entity_support_dofs()[entity_dim] From 3b6f95324739a4060a1fd8aa3aa87f1289cf60ec Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Thu, 3 Aug 2017 17:04:51 +0100 Subject: [PATCH 431/749] Handle Failure nodes in the interpreter Map to an appropriately shaped array of NaNs. Necessary if anyone ever asks for entity_support_dofs on a trace element. --- gem/interpreter.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gem/interpreter.py b/gem/interpreter.py index 81a2b0079..7d64b8048 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -122,6 +122,12 @@ def _evaluate_zero(e, self): return Result(numpy.zeros(e.shape, dtype=float)) +@_evaluate.register(gem.Failure) +def _evaluate_failure(e, self): + """Failure nodes produce NaNs.""" + return Result(numpy.full(e.shape, numpy.nan, dtype=float)) + + @_evaluate.register(gem.Constant) def _evaluate_constant(e, self): """Constants return their array.""" From b6c74bfdf23214876be71b343d93d56269a780ba Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 4 Aug 2017 11:51:46 +0100 Subject: [PATCH 432/749] Revert "Merge pull request #38 from FInAT/esd-method" This reverts commit 0428fca47be9a4a6a48d88b2fd24fccaccefcb81, reversing changes made to 2d03d202c35fc100e5aff110d6bbc0529ceef12b. --- finat/finiteelementbase.py | 76 ++++++++++++++++++-------------------- finat/quadrilateral.py | 2 +- 2 files changed, 37 insertions(+), 41 deletions(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index b5c6f3183..a51a13326 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -50,43 +50,6 @@ def entity_closure_dofs(self): element.''' return self._entity_closure_dofs - @cached_property - def _entity_support_dofs(self): - esd = {} - for entity_dim in self.cell.sub_entities.keys(): - beta = self.get_indices() - zeta = self.get_value_indices() - - entity_cell = self.cell.construct_subelement(entity_dim) - quad = make_quadrature(entity_cell, (2*numpy.array(self.degree)).tolist()) - - eps = 1.e-8 # Is this a safe value? - - result = {} - for f in self.entity_dofs()[entity_dim].keys(): - # Tabulate basis functions on the facet - vals, = itervalues(self.basis_evaluation(0, quad.point_set, entity=(entity_dim, f))) - # Integrate the square of the basis functions on the facet. - ints = gem.IndexSum( - gem.Product(gem.IndexSum(gem.Product(gem.Indexed(vals, beta + zeta), - gem.Indexed(vals, beta + zeta)), zeta), - quad.weight_expression), - quad.point_set.indices - ) - evaluation, = evaluate([gem.ComponentTensor(ints, beta)]) - ints = evaluation.arr.flatten() - assert evaluation.fids == () - result[f] = [dof for dof, i in enumerate(ints) if i > eps] - - esd[entity_dim] = result - return esd - - def entity_support_dofs(self): - '''Return the map of topological entities to degrees of - freedom that have non-zero support on those entities for the - finite element.''' - return self._entity_support_dofs - @abstractmethod def space_dimension(self): '''Return the dimension of the finite element space.''' @@ -144,10 +107,43 @@ def mapping(self): def entity_support_dofs(elem, entity_dim): - '''Return the map of entity id to the degrees of freedom for which + """Return the map of entity id to the degrees of freedom for which the corresponding basis functions take non-zero values. :arg elem: FInAT finite element :arg entity_dim: Dimension of the cell subentity. - ''' - return elem.entity_support_dofs()[entity_dim] + """ + if not hasattr(elem, "_entity_support_dofs"): + elem._entity_support_dofs = {} + cache = elem._entity_support_dofs + try: + return cache[entity_dim] + except KeyError: + pass + + beta = elem.get_indices() + zeta = elem.get_value_indices() + + entity_cell = elem.cell.construct_subelement(entity_dim) + quad = make_quadrature(entity_cell, (2*numpy.array(elem.degree)).tolist()) + + eps = 1.e-8 # Is this a safe value? + + result = {} + for f in elem.entity_dofs()[entity_dim].keys(): + # Tabulate basis functions on the facet + vals, = itervalues(elem.basis_evaluation(0, quad.point_set, entity=(entity_dim, f))) + # Integrate the square of the basis functions on the facet. + ints = gem.IndexSum( + gem.Product(gem.IndexSum(gem.Product(gem.Indexed(vals, beta + zeta), + gem.Indexed(vals, beta + zeta)), zeta), + quad.weight_expression), + quad.point_set.indices + ) + evaluation, = evaluate([gem.ComponentTensor(ints, beta)]) + ints = evaluation.arr.flatten() + assert evaluation.fids == () + result[f] = [dof for dof, i in enumerate(ints) if i > eps] + + cache[entity_dim] = result + return result diff --git a/finat/quadrilateral.py b/finat/quadrilateral.py index 1ca882509..6f72d101d 100644 --- a/finat/quadrilateral.py +++ b/finat/quadrilateral.py @@ -92,6 +92,6 @@ def productise(entity): ((1, 0), 1)] return facets[entity_id] elif entity_dim == 0: - return ((0, 0), entity_id) + raise NotImplementedError("Not implemented for 0 dimension entities") else: raise ValueError("Illegal entity dimension %s" % entity_dim) From f8eef148a3e062497252442aa3a82ac56d2fceab Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Fri, 4 Aug 2017 16:12:41 +0100 Subject: [PATCH 433/749] Revert "Revert "Merge pull request #38 from FInAT/esd-method"" This reverts commit b6c74bfdf23214876be71b343d93d56269a780ba. --- finat/finiteelementbase.py | 76 ++++++++++++++++++++------------------ finat/quadrilateral.py | 2 +- 2 files changed, 41 insertions(+), 37 deletions(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index a51a13326..b5c6f3183 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -50,6 +50,43 @@ def entity_closure_dofs(self): element.''' return self._entity_closure_dofs + @cached_property + def _entity_support_dofs(self): + esd = {} + for entity_dim in self.cell.sub_entities.keys(): + beta = self.get_indices() + zeta = self.get_value_indices() + + entity_cell = self.cell.construct_subelement(entity_dim) + quad = make_quadrature(entity_cell, (2*numpy.array(self.degree)).tolist()) + + eps = 1.e-8 # Is this a safe value? + + result = {} + for f in self.entity_dofs()[entity_dim].keys(): + # Tabulate basis functions on the facet + vals, = itervalues(self.basis_evaluation(0, quad.point_set, entity=(entity_dim, f))) + # Integrate the square of the basis functions on the facet. + ints = gem.IndexSum( + gem.Product(gem.IndexSum(gem.Product(gem.Indexed(vals, beta + zeta), + gem.Indexed(vals, beta + zeta)), zeta), + quad.weight_expression), + quad.point_set.indices + ) + evaluation, = evaluate([gem.ComponentTensor(ints, beta)]) + ints = evaluation.arr.flatten() + assert evaluation.fids == () + result[f] = [dof for dof, i in enumerate(ints) if i > eps] + + esd[entity_dim] = result + return esd + + def entity_support_dofs(self): + '''Return the map of topological entities to degrees of + freedom that have non-zero support on those entities for the + finite element.''' + return self._entity_support_dofs + @abstractmethod def space_dimension(self): '''Return the dimension of the finite element space.''' @@ -107,43 +144,10 @@ def mapping(self): def entity_support_dofs(elem, entity_dim): - """Return the map of entity id to the degrees of freedom for which + '''Return the map of entity id to the degrees of freedom for which the corresponding basis functions take non-zero values. :arg elem: FInAT finite element :arg entity_dim: Dimension of the cell subentity. - """ - if not hasattr(elem, "_entity_support_dofs"): - elem._entity_support_dofs = {} - cache = elem._entity_support_dofs - try: - return cache[entity_dim] - except KeyError: - pass - - beta = elem.get_indices() - zeta = elem.get_value_indices() - - entity_cell = elem.cell.construct_subelement(entity_dim) - quad = make_quadrature(entity_cell, (2*numpy.array(elem.degree)).tolist()) - - eps = 1.e-8 # Is this a safe value? - - result = {} - for f in elem.entity_dofs()[entity_dim].keys(): - # Tabulate basis functions on the facet - vals, = itervalues(elem.basis_evaluation(0, quad.point_set, entity=(entity_dim, f))) - # Integrate the square of the basis functions on the facet. - ints = gem.IndexSum( - gem.Product(gem.IndexSum(gem.Product(gem.Indexed(vals, beta + zeta), - gem.Indexed(vals, beta + zeta)), zeta), - quad.weight_expression), - quad.point_set.indices - ) - evaluation, = evaluate([gem.ComponentTensor(ints, beta)]) - ints = evaluation.arr.flatten() - assert evaluation.fids == () - result[f] = [dof for dof, i in enumerate(ints) if i > eps] - - cache[entity_dim] = result - return result + ''' + return elem.entity_support_dofs()[entity_dim] diff --git a/finat/quadrilateral.py b/finat/quadrilateral.py index 6f72d101d..1ca882509 100644 --- a/finat/quadrilateral.py +++ b/finat/quadrilateral.py @@ -92,6 +92,6 @@ def productise(entity): ((1, 0), 1)] return facets[entity_id] elif entity_dim == 0: - raise NotImplementedError("Not implemented for 0 dimension entities") + return ((0, 0), entity_id) else: raise ValueError("Illegal entity dimension %s" % entity_dim) From 17fafed79239529e1b2b7e528d39925c9dac768c Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 7 Aug 2017 14:40:33 +0100 Subject: [PATCH 434/749] add Chris and DiscontinuousChris elements on intervals --- finat/__init__.py | 1 + finat/chris.py | 79 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 finat/chris.py diff --git a/finat/__init__.py b/finat/__init__.py index 177038df9..fa9b50f3e 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -12,4 +12,5 @@ from .hdivcurl import HCurlElement, HDivElement # noqa: F401 from .mixed import MixedElement # noqa: F401 from .quadrature_element import QuadratureElement # noqa: F401 +from .chris import Chris, DiscontinuousChris # noqa: F401 from . import quadrature # noqa: F401 diff --git a/finat/chris.py b/finat/chris.py new file mode 100644 index 000000000..e614a12b6 --- /dev/null +++ b/finat/chris.py @@ -0,0 +1,79 @@ +from __future__ import absolute_import, print_function, division + +from FIAT.polynomial_set import mis +from FIAT.reference_element import LINE + +import gem +from gem.utils import cached_property + +from finat.finiteelementbase import FiniteElementBase + + +class Chris(FiniteElementBase): + + def __init__(self, cell, degree, shift_axes): + assert cell.get_shape() == LINE + self.cell = cell + self.degree = degree + self.shift_axes = shift_axes + + @cached_property + def cell(self): + pass # set at initialization + + @cached_property + def degree(self): + pass # set at initialization + + @property + def formdegree(self): + return 0 + + def entity_dofs(self): + raise NotImplementedError + + def space_dimension(self): + return self.degree + 1 + + def basis_evaluation(self, order, ps, entity=None): + """Return code for evaluating the element at known points on the + reference element. + + :param order: return derivatives up to this order. + :param ps: the point set object. + :param entity: the cell entity on which to tabulate. + """ + # Spatial dimension + dimension = self.cell.get_spatial_dimension() + + # Shape of the tabulation matrix + shape = tuple(index.extent for index in ps.indices) + self.index_shape + self.value_shape + + result = {} + for derivative in range(order + 1): + for alpha in mis(dimension, derivative): + name = "chris{}d{}sa{}".format(self.degree, ''.join(map(str, alpha)), self.shift_axes) + result[alpha] = gem.partial_indexed(gem.Variable(name, shape), ps.indices) + return result + + def point_evaluation(self, order, point, entity=None): + raise NotImplementedError + + @property + def index_shape(self): + return (self.space_dimension(),) + + @property + def value_shape(self): + return () + + @property + def mapping(self): + return "affine" + + +class DiscontinuousChris(Chris): + + @property + def formdegree(self): + return self.cell.get_spatial_dimension() From 3f311d7d1fa2210be4b1615e09d2667342031c4d Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 7 Aug 2017 15:20:12 +0100 Subject: [PATCH 435/749] make discontinuous elements distinct --- finat/chris.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/finat/chris.py b/finat/chris.py index e614a12b6..64697f42c 100644 --- a/finat/chris.py +++ b/finat/chris.py @@ -11,6 +11,8 @@ class Chris(FiniteElementBase): + suffix = "" + def __init__(self, cell, degree, shift_axes): assert cell.get_shape() == LINE self.cell = cell @@ -52,7 +54,7 @@ def basis_evaluation(self, order, ps, entity=None): result = {} for derivative in range(order + 1): for alpha in mis(dimension, derivative): - name = "chris{}d{}sa{}".format(self.degree, ''.join(map(str, alpha)), self.shift_axes) + name = "chris{}d{}sa{}{}".format(self.degree, ''.join(map(str, alpha)), self.shift_axes, self.suffix) result[alpha] = gem.partial_indexed(gem.Variable(name, shape), ps.indices) return result @@ -74,6 +76,8 @@ def mapping(self): class DiscontinuousChris(Chris): + suffix = "_disc" + @property def formdegree(self): return self.cell.get_spatial_dimension() From 8a3f3331831d1e4682e2e5401b1ead2e0f73b251 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 8 Aug 2017 16:21:07 +0100 Subject: [PATCH 436/749] remove outdated docstring snippet --- gem/utils.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gem/utils.py b/gem/utils.py index 0d3c715a2..5cd96cc06 100644 --- a/gem/utils.py +++ b/gem/utils.py @@ -40,9 +40,7 @@ def groupby(iterable, key=None): def make_proxy_class(name, cls): - """Constructs a proxy class for a given class. Instance attributes - are supposed to be listed e.g. with the unset_attribute decorator, - so that this function find them and create wrappers for them. + """Constructs a proxy class for a given class. :arg name: name of the new proxy class :arg cls: the wrapee class to create a proxy for From e6190d771c2f82fad71cbf98ae190cd888ace2dd Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 8 Aug 2017 16:22:57 +0100 Subject: [PATCH 437/749] add a library implementation of dynamic scoping --- gem/utils.py | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/gem/utils.py b/gem/utils.py index 5cd96cc06..1d7b62cc6 100644 --- a/gem/utils.py +++ b/gem/utils.py @@ -58,3 +58,54 @@ def getter(self): if not attr.startswith('_'): dct[attr] = make_proxy_property(attr) return type(name, (), dct) + + +# Implementation of dynamically scoped variables in Python. +class UnsetVariableError(LookupError): + pass + + +_unset = object() + + +class DynamicallyScoped(object): + """A dynamically scoped variable.""" + + def __init__(self, default_value=_unset): + if default_value is _unset: + self._head = None + else: + self._head = (default_value, None) + + def let(self, value): + return _LetBlock(self, value) + + @property + def value(self): + if self._head is None: + raise UnsetVariableError("Dynamically scoped variable not set.") + result, tail = self._head + return result + + +class _LetBlock(object): + """Context manager representing a dynamic scope.""" + + def __init__(self, variable, value): + self.variable = variable + self.value = value + self.state = None + + def __enter__(self): + assert self.state is None + value = self.value + tail = self.variable._head + scope = (value, tail) + self.variable._head = scope + self.state = scope + + def __exit__(self, exc_type, exc_value, traceback): + variable = self.variable + assert self.state is variable._head + value, variable._head = variable._head + self.state = None From 98754ecd210b03eef671d076f3de91f43a5b5e25 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 9 Aug 2017 12:28:16 +0100 Subject: [PATCH 438/749] wrap more FIAT elements as FInAT elements --- finat/__init__.py | 5 ++++- finat/fiat_elements.py | 20 ++++++++++++++++++++ finat/trace.py | 10 ++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 finat/trace.py diff --git a/finat/__init__.py b/finat/__init__.py index 177038df9..8a74046de 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -1,9 +1,12 @@ from __future__ import absolute_import, print_function, division +from .fiat_elements import Bubble, CrouzeixRaviart, DiscontinuousTaylor # noqa: F401 from .fiat_elements import Lagrange, DiscontinuousLagrange # noqa: F401 from .fiat_elements import RaviartThomas, DiscontinuousRaviartThomas # noqa: F401 from .fiat_elements import BrezziDouglasMarini, BrezziDouglasFortinMarini # noqa: F401 -from .fiat_elements import Nedelec, NedelecSecondKind, Regge # noqa: F401 +from .fiat_elements import Nedelec, NedelecSecondKind # noqa: F401 +from .fiat_elements import HellanHerrmannJohnson, Regge # noqa: F401 +from .trace import HDivTrace # noqa: F401 from .spectral import GaussLobattoLegendre, GaussLegendre # noqa: F401 from .tensorfiniteelement import TensorFiniteElement # noqa: F401 from .tensor_product import TensorProductElement # noqa: F401 diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 0ffc5c3b0..e2195a8a8 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -251,12 +251,27 @@ def __init__(self, cell, degree): super(Regge, self).__init__(FIAT.Regge(cell, degree)) +class HellanHerrmannJohnson(FiatElementBase): # symmetric matrix valued + def __init__(self, cell, degree): + super(HellanHerrmannJohnson, self).__init__(FIAT.HellanHerrmannJohnson(cell, degree)) + + class ScalarFiatElement(FiatElementBase): @property def value_shape(self): return () +class Bubble(ScalarFiatElement): + def __init__(self, cell, degree): + super(Bubble, self).__init__(FIAT.Bubble(cell, degree)) + + +class CrouzeixRaviart(ScalarFiatElement): + def __init__(self, cell, degree): + super(CrouzeixRaviart, self).__init__(FIAT.CrouzeixRaviart(cell, degree)) + + class Lagrange(ScalarFiatElement): def __init__(self, cell, degree): super(Lagrange, self).__init__(FIAT.Lagrange(cell, degree)) @@ -267,6 +282,11 @@ def __init__(self, cell, degree): super(DiscontinuousLagrange, self).__init__(FIAT.DiscontinuousLagrange(cell, degree)) +class DiscontinuousTaylor(ScalarFiatElement): + def __init__(self, cell, degree): + super(DiscontinuousTaylor, self).__init__(FIAT.DiscontinuousTaylor(cell, degree)) + + class VectorFiatElement(FiatElementBase): @property def value_shape(self): diff --git a/finat/trace.py b/finat/trace.py new file mode 100644 index 000000000..8275d7c76 --- /dev/null +++ b/finat/trace.py @@ -0,0 +1,10 @@ +from __future__ import absolute_import, print_function, division + +import FIAT + +from finat.fiat_elements import ScalarFiatElement + + +class HDivTrace(ScalarFiatElement): + def __init__(self, cell, degree): + super(HDivTrace, self).__init__(FIAT.HDivTrace(cell, degree)) From 2c5af805e6ee0e43a8ce06bd1fcab8b3dd0ceff5 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 9 Aug 2017 14:55:58 +0100 Subject: [PATCH 439/749] rename: Chris -> RuntimeTabulated --- finat/__init__.py | 2 +- finat/{chris.py => runtime_tabulated.py} | 41 +++++++++++++----------- 2 files changed, 24 insertions(+), 19 deletions(-) rename finat/{chris.py => runtime_tabulated.py} (60%) diff --git a/finat/__init__.py b/finat/__init__.py index fa9b50f3e..db5ef5c2e 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -12,5 +12,5 @@ from .hdivcurl import HCurlElement, HDivElement # noqa: F401 from .mixed import MixedElement # noqa: F401 from .quadrature_element import QuadratureElement # noqa: F401 -from .chris import Chris, DiscontinuousChris # noqa: F401 +from .runtime_tabulated import RuntimeTabulated # noqa: F401 from . import quadrature # noqa: F401 diff --git a/finat/chris.py b/finat/runtime_tabulated.py similarity index 60% rename from finat/chris.py rename to finat/runtime_tabulated.py index 64697f42c..e142a0468 100644 --- a/finat/chris.py +++ b/finat/runtime_tabulated.py @@ -9,15 +9,21 @@ from finat.finiteelementbase import FiniteElementBase -class Chris(FiniteElementBase): +class RuntimeTabulated(FiniteElementBase): - suffix = "" + def __init__(self, cell, degree, variant=None, shift_axes=0, continuous=True): + if cell.get_shape() != LINE: + raise NotImplementedError("Runtime tabulated elements limited to 1D.") + + assert isinstance(variant, str) + assert isinstance(shift_axes, int) and 0 <= shift_axes + assert isinstance(continuous, bool) - def __init__(self, cell, degree, shift_axes): - assert cell.get_shape() == LINE self.cell = cell self.degree = degree + self.variant = variant self.shift_axes = shift_axes + self.continuous = continuous @cached_property def cell(self): @@ -27,12 +33,15 @@ def cell(self): def degree(self): pass # set at initialization - @property + @cached_property def formdegree(self): - return 0 + if self.continuous: + return 0 + else: + return self.cell.get_spatial_dimension() def entity_dofs(self): - raise NotImplementedError + raise NotImplementedError("I cannot tell where my DoFs are... :-/") def space_dimension(self): return self.degree + 1 @@ -54,12 +63,17 @@ def basis_evaluation(self, order, ps, entity=None): result = {} for derivative in range(order + 1): for alpha in mis(dimension, derivative): - name = "chris{}d{}sa{}{}".format(self.degree, ''.join(map(str, alpha)), self.shift_axes, self.suffix) + name = str.format("rt_{}{}d{}sa{}{}", + self.variant, + self.degree, + ''.join(map(str, alpha)), + self.shift_axes, + 'c' if self.continuous else 'd') result[alpha] = gem.partial_indexed(gem.Variable(name, shape), ps.indices) return result def point_evaluation(self, order, point, entity=None): - raise NotImplementedError + raise NotImplementedError("Point evaluation supported for runtime tabulated elements") @property def index_shape(self): @@ -72,12 +86,3 @@ def value_shape(self): @property def mapping(self): return "affine" - - -class DiscontinuousChris(Chris): - - suffix = "_disc" - - @property - def formdegree(self): - return self.cell.get_spatial_dimension() From 2cf4918daefebde382bfb1e970bafae7dc43348e Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 9 Aug 2017 16:01:44 +0100 Subject: [PATCH 440/749] rename: FiatElementBase -> FiatElement --- finat/fiat_elements.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index e2195a8a8..816d6f27f 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -14,11 +14,11 @@ from finat.sympy2gem import sympy2gem -class FiatElementBase(FiniteElementBase): +class FiatElement(FiniteElementBase): """Base class for finite elements for which the tabulation is provided by FIAT.""" def __init__(self, fiat_element): - super(FiatElementBase, self).__init__() + super(FiatElement, self).__init__() self._element = fiat_element @property @@ -246,17 +246,17 @@ def is_const(expr): return result -class Regge(FiatElementBase): # naturally tensor valued +class Regge(FiatElement): # naturally tensor valued def __init__(self, cell, degree): super(Regge, self).__init__(FIAT.Regge(cell, degree)) -class HellanHerrmannJohnson(FiatElementBase): # symmetric matrix valued +class HellanHerrmannJohnson(FiatElement): # symmetric matrix valued def __init__(self, cell, degree): super(HellanHerrmannJohnson, self).__init__(FIAT.HellanHerrmannJohnson(cell, degree)) -class ScalarFiatElement(FiatElementBase): +class ScalarFiatElement(FiatElement): @property def value_shape(self): return () @@ -287,7 +287,7 @@ def __init__(self, cell, degree): super(DiscontinuousTaylor, self).__init__(FIAT.DiscontinuousTaylor(cell, degree)) -class VectorFiatElement(FiatElementBase): +class VectorFiatElement(FiatElement): @property def value_shape(self): return (self.cell.get_spatial_dimension(),) From 5fb7d59962994afa49825afea8a85f5d7e6e6f8b Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 10 Aug 2017 10:09:22 +0100 Subject: [PATCH 441/749] add restriction --- finat/runtime_tabulated.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/finat/runtime_tabulated.py b/finat/runtime_tabulated.py index e142a0468..33f91aa7c 100644 --- a/finat/runtime_tabulated.py +++ b/finat/runtime_tabulated.py @@ -11,18 +11,20 @@ class RuntimeTabulated(FiniteElementBase): - def __init__(self, cell, degree, variant=None, shift_axes=0, continuous=True): + def __init__(self, cell, degree, variant=None, shift_axes=0, restriction=None, continuous=True): if cell.get_shape() != LINE: raise NotImplementedError("Runtime tabulated elements limited to 1D.") assert isinstance(variant, str) assert isinstance(shift_axes, int) and 0 <= shift_axes assert isinstance(continuous, bool) + assert restriction in [None, '+', '-'] self.cell = cell self.degree = degree self.variant = variant self.shift_axes = shift_axes + self.restriction = restriction self.continuous = continuous @cached_property @@ -63,12 +65,15 @@ def basis_evaluation(self, order, ps, entity=None): result = {} for derivative in range(order + 1): for alpha in mis(dimension, derivative): - name = str.format("rt_{}{}d{}sa{}{}", + name = str.format("rt_{}{}d{}sa{}{}{}", self.variant, self.degree, ''.join(map(str, alpha)), self.shift_axes, - 'c' if self.continuous else 'd') + 'c' if self.continuous else 'd', + {None: "", + '+': "_p", + '-': "_m"}[self.restriction]) result[alpha] = gem.partial_indexed(gem.Variable(name, shape), ps.indices) return result From f2c264ce4bc0871b42c0022600746f0fefbcdef7 Mon Sep 17 00:00:00 2001 From: Chris Eldred Date: Thu, 10 Aug 2017 10:12:55 +0100 Subject: [PATCH 442/749] Changed runtime tabulated element metadata --- finat/runtime_tabulated.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/runtime_tabulated.py b/finat/runtime_tabulated.py index 33f91aa7c..af0a52ef7 100644 --- a/finat/runtime_tabulated.py +++ b/finat/runtime_tabulated.py @@ -65,7 +65,7 @@ def basis_evaluation(self, order, ps, entity=None): result = {} for derivative in range(order + 1): for alpha in mis(dimension, derivative): - name = str.format("rt_{}{}d{}sa{}{}{}", + name = str.format("rt_{}_{}_{}_{}_{}_{}", self.variant, self.degree, ''.join(map(str, alpha)), From 7f9997442d11d894687828d4ebf7770c079d75ae Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 10 Aug 2017 10:29:43 +0100 Subject: [PATCH 443/749] add DiscontinuousElement --- finat/__init__.py | 1 + finat/discontinuous.py | 59 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 finat/discontinuous.py diff --git a/finat/__init__.py b/finat/__init__.py index 8a74046de..7e0e4a9d0 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -11,6 +11,7 @@ from .tensorfiniteelement import TensorFiniteElement # noqa: F401 from .tensor_product import TensorProductElement # noqa: F401 from .quadrilateral import QuadrilateralElement # noqa: F401 +from .discontinuous import DiscontinuousElement # noqa: F401 from .enriched import EnrichedElement # noqa: F401 from .hdivcurl import HCurlElement, HDivElement # noqa: F401 from .mixed import MixedElement # noqa: F401 diff --git a/finat/discontinuous.py b/finat/discontinuous.py new file mode 100644 index 000000000..2f41c586e --- /dev/null +++ b/finat/discontinuous.py @@ -0,0 +1,59 @@ +from __future__ import absolute_import, print_function, division +from six import iteritems + +from gem.utils import cached_property + +from finat.finiteelementbase import FiniteElementBase + + +class DiscontinuousElement(FiniteElementBase): + """Element wrapper that makes a FInAT element discontinuous.""" + + def __init__(self, element): + super(DiscontinuousElement, self).__init__() + self.element = element + + @property + def cell(self): + return self.element.cell + + @property + def degree(self): + return self.element.degree + + @cached_property + def formdegree(self): + # Always discontinuous! + return self.element.cell.get_spatial_dimension() + + @cached_property + def _entity_dofs(self): + result = {dim: {i: [] for i in entities} + for dim, entities in iteritems(self.cell.get_topology())} + cell_dimension = self.cell.get_dimension() + result[cell_dimension][0].extend(range(self.space_dimension())) + return result + + def entity_dofs(self): + return self._entity_dofs + + def space_dimension(self): + return self.element.space_dimension() + + @property + def index_shape(self): + return self.element.index_shape + + @property + def value_shape(self): + return self.element.value_shape + + def basis_evaluation(self, order, ps, entity=None): + return self.element.basis_evaluation(order, ps, entity) + + def point_evaluation(self, order, refcoords, entity=None): + return self.element.point_evaluation(order, refcoords, entity) + + @property + def mapping(self): + return self.element.mapping From 311c8657ea5fc118ee51cb4e89b23766bd27b1e9 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Thu, 10 Aug 2017 10:36:57 +0100 Subject: [PATCH 444/749] impero: Swap order of processing in preprocess_gem We should remove component tensors before simplifying deltas. --- gem/impero_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gem/impero_utils.py b/gem/impero_utils.py index 2df471697..43e739be1 100644 --- a/gem/impero_utils.py +++ b/gem/impero_utils.py @@ -35,10 +35,10 @@ class NoopError(Exception): def preprocess_gem(expressions, replace_delta=True, remove_componenttensors=True): """Lower GEM nodes that cannot be translated to C directly.""" - if replace_delta: - expressions = optimise.replace_delta(expressions) if remove_componenttensors: expressions = optimise.remove_componenttensors(expressions) + if replace_delta: + expressions = optimise.replace_delta(expressions) return expressions From 40259748ed7a4281f2998b76896830a8e974800d Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 10 Aug 2017 10:48:25 +0100 Subject: [PATCH 445/749] drop DiscontinuousRaviartThomas wrapper --- finat/__init__.py | 3 +-- finat/fiat_elements.py | 5 ----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/finat/__init__.py b/finat/__init__.py index 7e0e4a9d0..a7f1845c4 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -2,9 +2,8 @@ from .fiat_elements import Bubble, CrouzeixRaviart, DiscontinuousTaylor # noqa: F401 from .fiat_elements import Lagrange, DiscontinuousLagrange # noqa: F401 -from .fiat_elements import RaviartThomas, DiscontinuousRaviartThomas # noqa: F401 from .fiat_elements import BrezziDouglasMarini, BrezziDouglasFortinMarini # noqa: F401 -from .fiat_elements import Nedelec, NedelecSecondKind # noqa: F401 +from .fiat_elements import Nedelec, NedelecSecondKind, RaviartThomas # noqa: F401 from .fiat_elements import HellanHerrmannJohnson, Regge # noqa: F401 from .trace import HDivTrace # noqa: F401 from .spectral import GaussLobattoLegendre, GaussLegendre # noqa: F401 diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 816d6f27f..01cfd6d4a 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -298,11 +298,6 @@ def __init__(self, cell, degree): super(RaviartThomas, self).__init__(FIAT.RaviartThomas(cell, degree)) -class DiscontinuousRaviartThomas(VectorFiatElement): - def __init__(self, cell, degree): - super(DiscontinuousRaviartThomas, self).__init__(FIAT.DiscontinuousRaviartThomas(cell, degree)) - - class BrezziDouglasMarini(VectorFiatElement): def __init__(self, cell, degree): super(BrezziDouglasMarini, self).__init__(FIAT.BrezziDouglasMarini(cell, degree)) From 4c9fc1b7ecc3a9786cde78a525c0805cd6a8d2d1 Mon Sep 17 00:00:00 2001 From: Chris Eldred Date: Thu, 10 Aug 2017 10:50:18 +0100 Subject: [PATCH 446/749] Further runtime tabulated element metadata changes --- finat/runtime_tabulated.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/finat/runtime_tabulated.py b/finat/runtime_tabulated.py index af0a52ef7..877d9753d 100644 --- a/finat/runtime_tabulated.py +++ b/finat/runtime_tabulated.py @@ -72,8 +72,8 @@ def basis_evaluation(self, order, ps, entity=None): self.shift_axes, 'c' if self.continuous else 'd', {None: "", - '+': "_p", - '-': "_m"}[self.restriction]) + '+': "p", + '-': "m"}[self.restriction]) result[alpha] = gem.partial_indexed(gem.Variable(name, shape), ps.indices) return result From d0d15892f938e0f00e3813bc0f362aec78325866 Mon Sep 17 00:00:00 2001 From: Chris Eldred Date: Thu, 10 Aug 2017 11:47:46 +0100 Subject: [PATCH 447/749] fix point evaluation error --- finat/runtime_tabulated.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/runtime_tabulated.py b/finat/runtime_tabulated.py index 877d9753d..a366ba15d 100644 --- a/finat/runtime_tabulated.py +++ b/finat/runtime_tabulated.py @@ -78,7 +78,7 @@ def basis_evaluation(self, order, ps, entity=None): return result def point_evaluation(self, order, point, entity=None): - raise NotImplementedError("Point evaluation supported for runtime tabulated elements") + raise NotImplementedError("Point evaluation not supported for runtime tabulated elements") @property def index_shape(self): From cd9fde90b5df88172848c890f2e0c2b8d6b3dca9 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 15 Aug 2017 15:19:51 +0100 Subject: [PATCH 448/749] handle Delta nodes in the GEM interpreter --- gem/interpreter.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/gem/interpreter.py b/gem/interpreter.py index 7d64b8048..a34ff5770 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -12,6 +12,7 @@ import itertools from gem import gem, node +from gem.optimise import replace_delta __all__ = ("evaluate", ) @@ -134,6 +135,13 @@ def _evaluate_constant(e, self): return Result(e.array) +@_evaluate.register(gem.Delta) +def _evaluate_delta(e, self): + """Lower delta and evaluate.""" + e, = replace_delta((e,)) + return self(e) + + @_evaluate.register(gem.Variable) def _evaluate_variable(e, self): """Look up variables in the provided bindings.""" From 637b393965ac6ce194c3b3820212ebaf791214b6 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 21 Aug 2017 12:20:16 +0100 Subject: [PATCH 449/749] add docstring and comments --- finat/runtime_tabulated.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/finat/runtime_tabulated.py b/finat/runtime_tabulated.py index a366ba15d..c2d5b6f7b 100644 --- a/finat/runtime_tabulated.py +++ b/finat/runtime_tabulated.py @@ -10,11 +10,30 @@ class RuntimeTabulated(FiniteElementBase): - - def __init__(self, cell, degree, variant=None, shift_axes=0, restriction=None, continuous=True): + """Element placeholder for tabulations provided at run time through a + kernel argument. + + Used by Themis. + """ + + def __init__(self, cell, degree, variant=None, shift_axes=0, + restriction=None, continuous=True): + """Construct a runtime tabulated element. + + :arg cell: reference cell + :arg degree: polynomial degree (int) + :arg variant: variant string of the UFL element + :arg shift_axes: first dimension + :arg restriction: None for single-cell integrals, '+' or '-' + for interior facet integrals depending on + which we need the tabulation on + :arg continuous: continuous or discontinuous element? + """ + # Currently only interval elements are accepted. if cell.get_shape() != LINE: raise NotImplementedError("Runtime tabulated elements limited to 1D.") + # Sanity check assert isinstance(variant, str) assert isinstance(shift_axes, int) and 0 <= shift_axes assert isinstance(continuous, bool) From 765b348aadca17fa538851c613b3094f8a207558 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 7 Sep 2017 17:39:48 +0100 Subject: [PATCH 450/749] add PointSingleton --- finat/point_set.py | 21 +++++++++++++++++++++ finat/tensor_product.py | 20 ++++++++++++-------- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/finat/point_set.py b/finat/point_set.py index bfea7d24e..f89c7f133 100644 --- a/finat/point_set.py +++ b/finat/point_set.py @@ -37,6 +37,27 @@ def expression(self): ``self.indices`` and shape (point dimension,).""" +class PointSingleton(AbstractPointSet): + """Just a single point.""" + + def __init__(self, point): + point = numpy.asarray(point) + assert len(point.shape) == 1 + self.point = point + + @property + def points(self): + return self.point.reshape(1, -1) + + @property + def indices(self): + return () + + @cached_property + def expression(self): + return gem.Literal(self.point) + + class PointSet(AbstractPointSet): """A basic point set with no internal structure.""" diff --git a/finat/tensor_product.py b/finat/tensor_product.py index 251bb4079..5d7e2d72a 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -13,7 +13,7 @@ from gem.utils import cached_property from finat.finiteelementbase import FiniteElementBase -from finat.point_set import PointSet, TensorPointSet +from finat.point_set import PointSingleton, PointSet, TensorPointSet class TensorProductElement(FiniteElementBase): @@ -199,17 +199,21 @@ def factor_point_set(product_cell, product_dim, point_set): assert all(ps.dimension == dim for ps, dim in zip(point_set.factors, point_dims)) return point_set.factors + + # Split the point coordinates along the point dimensions + # required by the subelements. + assert point_set.dimension == sum(point_dims) + slices = TensorProductCell._split_slices(point_dims) + + if isinstance(point_set, PointSingleton): + return [PointSingleton(point_set.point[s]) for s in slices] elif isinstance(point_set, PointSet): - # Split the point coordinates along the point dimensions - # required by the subelements, but use the same point index - # for the new point sets. - assert point_set.dimension == sum(point_dims) - slices = TensorProductCell._split_slices(point_dims) + # Use the same point index for the new point sets. result = [] for s in slices: ps = PointSet(point_set.points[:, s]) ps.indices = point_set.indices result.append(ps) return result - else: - raise NotImplementedError("How to tabulate TensorProductElement on %s?" % (type(point_set).__name__,)) + + raise NotImplementedError("How to tabulate TensorProductElement on %s?" % (type(point_set).__name__,)) From eedc816a678da2599b1290a1111d6f394890772e Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 1 Sep 2017 17:32:00 +0100 Subject: [PATCH 451/749] WIP --- finat/fiat_elements.py | 2 +- finat/finiteelementbase.py | 2 +- finat/hermite.py | 59 ++++++++++++++++++++++++++++++++++++ finat/tensorfiniteelement.py | 4 +-- 4 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 finat/hermite.py diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 01cfd6d4a..a16426506 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -51,7 +51,7 @@ def index_shape(self): def value_shape(self): return self._element.value_shape() - def basis_evaluation(self, order, ps, entity=None): + def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): '''Return code for evaluating the element at known points on the reference element. diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index b5c6f3183..41f3ce434 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -115,7 +115,7 @@ def get_value_indices(self): return tuple(gem.Index(extent=d) for d in self.value_shape) @abstractmethod - def basis_evaluation(self, order, ps, entity=None): + def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): '''Return code for evaluating the element at known points on the reference element. diff --git a/finat/hermite.py b/finat/hermite.py new file mode 100644 index 000000000..43c6cab12 --- /dev/null +++ b/finat/hermite.py @@ -0,0 +1,59 @@ +from __future__ import absolute_import, print_function, division +from six import iteritems, with_metaclass + +from abc import ABCMeta, abstractmethod + +import numpy + +import FIAT + +import gem + +from finat.fiat_elements import ScalarFiatElement + + +class PhysicalGeometry(with_metaclass(ABCMeta)): + @abstractmethod + def jacobian_at(self, point): + pass + + +class CubicHermite(ScalarFiatElement): + def __init__(self, cell): + super(CubicHermite, self).__init__(FIAT.CubicHermite(cell)) + + def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): + assert coordinate_mapping is not None + JA, JB, JC = [coordinate_mapping.jacobian_at(vertex) + for vertex in self.cell.get_vertices()] + + def n(J): + assert J.shape == (2, 2) + return numpy.array( + [[gem.Indexed(J, (0, 0)), + gem.Indexed(J, (0, 1))], + [gem.Indexed(J, (1, 0)), + gem.Indexed(J, (1, 1))]] + ) + M = numpy.eye(10, dtype=object) + for multiindex in numpy.ndindex(M.shape): + M[multiindex] = gem.Literal(M[multiindex]) + M[1:3, 1:3] = n(JA) + M[4:6, 4:6] = n(JB) + M[7:9, 7:9] = n(JC) + M = gem.ListTensor(M) + + def matvec(table): + i = gem.Index() + j = gem.Index() + return gem.ComponentTensor(gem.IndexSum(gem.Product(gem.Indexed(M, (i, j)), + gem.Indexed(table, (j,))), + (j,)), + (i,)) + + result = super(CubicHermite, self).basis_evaluation(order, ps, entity=entity) + return {alpha: matvec(table) + for alpha, table in iteritems(result)} + + def point_evaluation(self, order, refcoords, entity=None): + raise NotImplementedError # TODO: think about it later! diff --git a/finat/tensorfiniteelement.py b/finat/tensorfiniteelement.py index f6c9f6033..1c3e74833 100644 --- a/finat/tensorfiniteelement.py +++ b/finat/tensorfiniteelement.py @@ -82,7 +82,7 @@ def index_shape(self): def value_shape(self): return self._shape + self._base_element.value_shape - def basis_evaluation(self, order, ps, entity=None): + def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): r"""Produce the recipe for basis function evaluation at a set of points :math:`q`: .. math:: @@ -91,7 +91,7 @@ def basis_evaluation(self, order, ps, entity=None): \nabla\boldsymbol\phi_{(\epsilon \gamma \zeta) (i \alpha \beta) q} = \delta_{\alpha \epsilon} \deta{\beta \gamma}\nabla\phi_{\zeta i q} """ scalar_evaluation = self._base_element.basis_evaluation - return self._tensorise(scalar_evaluation(order, ps, entity)) + return self._tensorise(scalar_evaluation(order, ps, entity, coordinate_mapping=coordinate_mapping)) def point_evaluation(self, order, point, entity=None): scalar_evaluation = self._base_element.point_evaluation From 731707b1325cf7a9765221b0d4e44829f8bd4958 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 27 Sep 2017 16:12:42 +0100 Subject: [PATCH 452/749] let all integral types be accepted as indices --- gem/gem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gem/gem.py b/gem/gem.py index 3b5c997f8..30602ef5a 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -21,6 +21,7 @@ from abc import ABCMeta from itertools import chain from operator import attrgetter +from numbers import Integral import numpy from numpy import asarray @@ -384,7 +385,7 @@ class IndexBase(with_metaclass(ABCMeta)): pass -IndexBase.register(int) +IndexBase.register(Integral) class Index(IndexBase): From 37b868dab08c96454c5e53228bcac86ca3e57338 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Wed, 27 Sep 2017 16:43:26 +0100 Subject: [PATCH 453/749] better isinstance(index, int) fix --- gem/gem.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gem/gem.py b/gem/gem.py index 30602ef5a..978f17ce7 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -385,7 +385,7 @@ class IndexBase(with_metaclass(ABCMeta)): pass -IndexBase.register(Integral) +IndexBase.register(int) class Index(IndexBase): @@ -470,6 +470,10 @@ class Indexed(Scalar): __back__ = ('multiindex',) def __new__(cls, aggregate, multiindex): + # Accept numpy or any integer, but cast to int. + multiindex = tuple(int(i) if isinstance(i, Integral) else i + for i in multiindex) + # Set index extents from shape assert len(aggregate.shape) == len(multiindex) for index, extent in zip(multiindex, aggregate.shape): From 5da0fb5dec01fed24d4e6b567aeb4f506aeef5ae Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Thu, 19 Oct 2017 16:26:00 +0100 Subject: [PATCH 454/749] Fix excessive memory consumption in entity_support_dofs Previously, computation of entity_support_dofs did not exploit tensor product structure, and so at high order, required allocation of temporaries of size O(p^2d). Fix this by computing the support dofs on the element factors and productising them. --- finat/quadrilateral.py | 25 +++++++++++++-------- finat/tensor_product.py | 49 ++++++++++++++++++++++++++--------------- 2 files changed, 47 insertions(+), 27 deletions(-) diff --git a/finat/quadrilateral.py b/finat/quadrilateral.py index 1ca882509..97cc816a8 100644 --- a/finat/quadrilateral.py +++ b/finat/quadrilateral.py @@ -32,15 +32,11 @@ def formdegree(self): @cached_property def _entity_dofs(self): - entity_dofs = self.product.entity_dofs() - flat_entity_dofs = {} - flat_entity_dofs[0] = entity_dofs[(0, 0)] - flat_entity_dofs[1] = dict(enumerate( - [v for k, v in sorted(iteritems(entity_dofs[(0, 1)]))] + - [v for k, v in sorted(iteritems(entity_dofs[(1, 0)]))] - )) - flat_entity_dofs[2] = entity_dofs[(1, 1)] - return flat_entity_dofs + return flatten(self.product.entity_dofs()) + + @cached_property + def _entity_support_dofs(self): + return flatten(self.product.entity_support_dofs()) def entity_dofs(self): return self._entity_dofs @@ -74,6 +70,17 @@ def mapping(self): return self.product.mapping +def flatten(dofs): + flat_dofs = {} + flat_dofs[0] = dofs[(0, 0)] + flat_dofs[1] = dict(enumerate( + [v for k, v in sorted(iteritems(dofs[(0, 1)]))] + + [v for k, v in sorted(iteritems(dofs[(1, 0)]))] + )) + flat_dofs[2] = dofs[(1, 1)] + return flat_dofs + + def productise(entity): if entity is None: entity = (2, 0) diff --git a/finat/tensor_product.py b/finat/tensor_product.py index 5d7e2d72a..f95eaa2be 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -3,6 +3,7 @@ from functools import reduce from itertools import chain, product +from operator import methodcaller import numpy @@ -47,24 +48,11 @@ def formdegree(self): @cached_property def _entity_dofs(self): - shape = tuple(fe.space_dimension() for fe in self.factors) - entity_dofs = {} - for dim in product(*[fe.cell.get_topology().keys() - for fe in self.factors]): - dim_dofs = [] - topds = [fe.entity_dofs()[d] - for fe, d in zip(self.factors, dim)] - for tuple_ei in product(*[sorted(topd) for topd in topds]): - tuple_vs = list(product(*[topd[ei] - for topd, ei in zip(topds, tuple_ei)])) - if tuple_vs: - vs = list(numpy.ravel_multi_index(numpy.transpose(tuple_vs), shape)) - dim_dofs.append((tuple_ei, vs)) - else: - dim_dofs.append((tuple_ei, [])) - # flatten entity numbers - entity_dofs[dim] = dict(enumerate(v for k, v in sorted(dim_dofs))) - return entity_dofs + return productise(self.factors, methodcaller("entity_dofs")) + + @cached_property + def _entity_support_dofs(self): + return productise(self.factors, methodcaller("entity_support_dofs")) def entity_dofs(self): return self._entity_dofs @@ -181,6 +169,31 @@ def mapping(self): return None +def productise(factors, method): + '''Tensor product the dict mapping topological entities to dofs across factors. + + :arg factors: element factors. + :arg method: instance method to call on each factor to get dofs.''' + shape = tuple(fe.space_dimension() for fe in factors) + dofs = {} + for dim in product(*[fe.cell.get_topology().keys() + for fe in factors]): + dim_dofs = [] + topds = [method(fe)[d] + for fe, d in zip(factors, dim)] + for tuple_ei in product(*[sorted(topd) for topd in topds]): + tuple_vs = list(product(*[topd[ei] + for topd, ei in zip(topds, tuple_ei)])) + if tuple_vs: + vs = list(numpy.ravel_multi_index(numpy.transpose(tuple_vs), shape)) + dim_dofs.append((tuple_ei, vs)) + else: + dim_dofs.append((tuple_ei, [])) + # flatten entity numbers + dofs[dim] = dict(enumerate(v for k, v in sorted(dim_dofs))) + return dofs + + def factor_point_set(product_cell, product_dim, point_set): """Factors a point set for the product element into a point sets for each subelement. From 77ae653a4d8dffe9fd142a93c192d36cc5fa5abf Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Mon, 18 Sep 2017 11:39:32 +0100 Subject: [PATCH 455/749] Fix https://bitbucket.org/fenics-project/fiat/issues/21 --- finat/finiteelementbase.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index b5c6f3183..8441360fb 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -39,8 +39,8 @@ def entity_dofs(self): def _entity_closure_dofs(self): # Compute the nodes on the closure of each sub_entity. entity_dofs = self.entity_dofs() - return {dim: {e: list(chain(*[entity_dofs[d][se] - for d, se in sub_entities])) + return {dim: {e: sorted(chain(*[entity_dofs[d][se] + for d, se in sub_entities])) for e, sub_entities in iteritems(entities)} for dim, entities in iteritems(self.cell.sub_entities)} From 16c5c0efca60dab34f5b9e2901d527045d535837 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 20 Oct 2017 10:54:45 +0100 Subject: [PATCH 456/749] fix entity_support_dofs performance for more elements --- finat/enriched.py | 36 ++++++++++++++++++++++++++++++++++-- finat/hdivcurl.py | 3 +++ finat/mixed.py | 3 +++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/finat/enriched.py b/finat/enriched.py index 12315f8a8..60f6eb5b8 100644 --- a/finat/enriched.py +++ b/finat/enriched.py @@ -1,6 +1,12 @@ from __future__ import absolute_import, print_function, division +from six import iteritems from six.moves import map, zip +from functools import partial +from operator import add, methodcaller + +import numpy + import gem from gem.utils import cached_property @@ -35,8 +41,13 @@ def formdegree(self): def entity_dofs(self): '''Return the map of topological entities to degrees of freedom for the finite element.''' - from FIAT.mixed import concatenate_entity_dofs - return concatenate_entity_dofs(self.cell, self.elements) + return concatenate_entity_dofs(self.cell, self.elements, + methodcaller("entity_dofs")) + + @cached_property + def _entity_support_dofs(self): + return concatenate_entity_dofs(self.cell, self.elements, + methodcaller("entity_support_dofs")) def space_dimension(self): '''Return the dimension of the finite element space.''' @@ -118,3 +129,24 @@ def tree_map(f, *args): return tuple(tree_map(f, *subargs) for subargs in zip(*args)) else: return f(*args) + + +def concatenate_entity_dofs(ref_el, elements, method): + """Combine the entity DoFs from a list of elements into a combined + dict containing the information for the concatenated DoFs of all + the elements. + + :arg ref_el: the reference cell + :arg elements: subelement whose DoFs are concatenated + :arg method: method to obtain the entity DoFs dict + :returns: concatenated entity DoFs dict + """ + entity_dofs = {dim: {i: [] for i in entities} + for dim, entities in iteritems(ref_el.get_topology())} + offsets = numpy.cumsum([0] + list(e.space_dimension() + for e in elements), dtype=int) + for i, d in enumerate(map(method, elements)): + for dim, dofs in iteritems(d): + for ent, off in iteritems(dofs): + entity_dofs[dim][ent] += list(map(partial(add, offsets[i]), off)) + return entity_dofs diff --git a/finat/hdivcurl.py b/finat/hdivcurl.py index 5a98e57a0..1fb69bb00 100644 --- a/finat/hdivcurl.py +++ b/finat/hdivcurl.py @@ -38,6 +38,9 @@ def entity_dofs(self): def entity_closure_dofs(self): return self.wrappee.entity_closure_dofs() + def entity_support_dofs(self): + return self.wrappee.entity_support_dofs() + def space_dimension(self): return self.wrappee.space_dimension() diff --git a/finat/mixed.py b/finat/mixed.py index 7d2a4c38b..d19f92271 100644 --- a/finat/mixed.py +++ b/finat/mixed.py @@ -55,6 +55,9 @@ def entity_dofs(self): def entity_closure_dofs(self): return self.element.entity_closure_dofs() + def entity_support_dofs(self): + return self.element.entity_support_dofs() + def space_dimension(self): return self.element.space_dimension() From 5f021cf6f52e3d215ae15af462ae2ebd75170e42 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Fri, 10 Nov 2017 10:47:29 +0000 Subject: [PATCH 457/749] Migrate citations import to firedrake_citations --- gem/coffee.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gem/coffee.py b/gem/coffee.py index 6415e28df..f34326c30 100644 --- a/gem/coffee.py +++ b/gem/coffee.py @@ -20,7 +20,7 @@ try: - from firedrake import Citations + from firedrake_citations import Citations Citations().register("Luporini2016") except ImportError: pass From d39f01069eaecb0f9ba36fa50236a1d8fe6691fe Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Fri, 10 Nov 2017 17:55:38 +0000 Subject: [PATCH 458/749] Citation registration only in pick_mode Also register Kirby & Logg 2006 in tensor mode. --- gem/coffee.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/gem/coffee.py b/gem/coffee.py index f34326c30..c1afb7743 100644 --- a/gem/coffee.py +++ b/gem/coffee.py @@ -19,13 +19,6 @@ from gem.utils import groupby -try: - from firedrake_citations import Citations - Citations().register("Luporini2016") -except ImportError: - pass - - __all__ = ['optimise_monomial_sum'] From e1af0172c424da3fd5185bfb9a99215143c21807 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 7 Dec 2017 10:52:16 +0000 Subject: [PATCH 459/749] remove six --- finat/discontinuous.py | 3 +-- finat/enriched.py | 8 +++----- finat/fiat_elements.py | 5 ++--- finat/finiteelementbase.py | 9 ++++----- finat/hdivcurl.py | 4 +--- finat/mixed.py | 4 +--- finat/point_set.py | 4 +--- finat/quadrature.py | 3 +-- finat/quadrature_element.py | 4 +--- finat/quadrilateral.py | 5 ++--- finat/sympy2gem.py | 1 - finat/tensor_product.py | 1 - finat/tensorfiniteelement.py | 3 +-- test/test_point_evaluation_ciarlet.py | 3 +-- 14 files changed, 19 insertions(+), 38 deletions(-) diff --git a/finat/discontinuous.py b/finat/discontinuous.py index 2f41c586e..e4399911c 100644 --- a/finat/discontinuous.py +++ b/finat/discontinuous.py @@ -1,5 +1,4 @@ from __future__ import absolute_import, print_function, division -from six import iteritems from gem.utils import cached_property @@ -29,7 +28,7 @@ def formdegree(self): @cached_property def _entity_dofs(self): result = {dim: {i: [] for i in entities} - for dim, entities in iteritems(self.cell.get_topology())} + for dim, entities in self.cell.get_topology().items()} cell_dimension = self.cell.get_dimension() result[cell_dimension][0].extend(range(self.space_dimension())) return result diff --git a/finat/enriched.py b/finat/enriched.py index 60f6eb5b8..ab5cb2563 100644 --- a/finat/enriched.py +++ b/finat/enriched.py @@ -1,6 +1,4 @@ from __future__ import absolute_import, print_function, division -from six import iteritems -from six.moves import map, zip from functools import partial from operator import add, methodcaller @@ -142,11 +140,11 @@ def concatenate_entity_dofs(ref_el, elements, method): :returns: concatenated entity DoFs dict """ entity_dofs = {dim: {i: [] for i in entities} - for dim, entities in iteritems(ref_el.get_topology())} + for dim, entities in ref_el.get_topology().items()} offsets = numpy.cumsum([0] + list(e.space_dimension() for e in elements), dtype=int) for i, d in enumerate(map(method, elements)): - for dim, dofs in iteritems(d): - for ent, off in iteritems(dofs): + for dim, dofs in d.items(): + for ent, off in dofs.items(): entity_dofs[dim][ent] += list(map(partial(add, offsets[i]), off)) return entity_dofs diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 01cfd6d4a..80babbcec 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -1,5 +1,4 @@ from __future__ import absolute_import, print_function, division -from six import iteritems import numpy as np import sympy as sp @@ -63,7 +62,7 @@ def basis_evaluation(self, order, ps, entity=None): value_size = np.prod(self._element.value_shape(), dtype=int) fiat_result = self._element.tabulate(order, ps.points, entity) result = {} - for alpha, fiat_table in iteritems(fiat_result): + for alpha, fiat_table in fiat_result.items(): if isinstance(fiat_table, Exception): result[alpha] = gem.Failure(self.index_shape + self.value_shape, fiat_table) continue @@ -153,7 +152,7 @@ def point_evaluation_generic(fiat_element, order, refcoords, entity): value_size = np.prod(fiat_element.value_shape(), dtype=int) fiat_result = fiat_element.tabulate(order, [Xi], entity) result = {} - for alpha, fiat_table in iteritems(fiat_result): + for alpha, fiat_table in fiat_result.items(): if isinstance(fiat_table, Exception): result[alpha] = gem.Failure((space_dimension,) + fiat_element.value_shape(), fiat_table) continue diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 8441360fb..a429de64e 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -1,5 +1,4 @@ from __future__ import absolute_import, print_function, division -from six import with_metaclass, iteritems, itervalues from abc import ABCMeta, abstractproperty, abstractmethod from itertools import chain @@ -13,7 +12,7 @@ from finat.quadrature import make_quadrature -class FiniteElementBase(with_metaclass(ABCMeta)): +class FiniteElementBase(metaclass=ABCMeta): @abstractproperty def cell(self): @@ -41,8 +40,8 @@ def _entity_closure_dofs(self): entity_dofs = self.entity_dofs() return {dim: {e: sorted(chain(*[entity_dofs[d][se] for d, se in sub_entities])) - for e, sub_entities in iteritems(entities)} - for dim, entities in iteritems(self.cell.sub_entities)} + for e, sub_entities in entities.items()} + for dim, entities in self.cell.sub_entities.items()} def entity_closure_dofs(self): '''Return the map of topological entities to degrees of @@ -65,7 +64,7 @@ def _entity_support_dofs(self): result = {} for f in self.entity_dofs()[entity_dim].keys(): # Tabulate basis functions on the facet - vals, = itervalues(self.basis_evaluation(0, quad.point_set, entity=(entity_dim, f))) + vals, = self.basis_evaluation(0, quad.point_set, entity=(entity_dim, f)).values() # Integrate the square of the basis functions on the facet. ints = gem.IndexSum( gem.Product(gem.IndexSum(gem.Product(gem.Indexed(vals, beta + zeta), diff --git a/finat/hdivcurl.py b/finat/hdivcurl.py index 1fb69bb00..28a5184df 100644 --- a/finat/hdivcurl.py +++ b/finat/hdivcurl.py @@ -1,7 +1,5 @@ from __future__ import absolute_import, division, print_function -from six import iteritems - from FIAT.reference_element import LINE import gem @@ -62,7 +60,7 @@ def promote(table): return gem.ComponentTensor(gem.Indexed(u, zeta), beta + zeta) return {alpha: promote(table) - for alpha, table in iteritems(core_eval)} + for alpha, table in core_eval.items()} def basis_evaluation(self, order, ps, entity=None): core_eval = self.wrappee.basis_evaluation(order, ps, entity) diff --git a/finat/mixed.py b/finat/mixed.py index d19f92271..edb128a2b 100644 --- a/finat/mixed.py +++ b/finat/mixed.py @@ -1,6 +1,4 @@ from __future__ import absolute_import, print_function, division -from six import iteritems -from six.moves import zip import numpy @@ -85,7 +83,7 @@ def promote(table): return gem.ComponentTensor(gem.Indexed(u, zeta), beta + zeta) return {alpha: promote(table) - for alpha, table in iteritems(core_eval)} + for alpha, table in core_eval.items()} def basis_evaluation(self, order, ps, entity=None): core_eval = self.element.basis_evaluation(order, ps, entity) diff --git a/finat/point_set.py b/finat/point_set.py index f89c7f133..95b334233 100644 --- a/finat/point_set.py +++ b/finat/point_set.py @@ -1,6 +1,4 @@ from __future__ import absolute_import, print_function, division -from six import with_metaclass -from six.moves import range, zip from abc import ABCMeta, abstractproperty from itertools import chain, product @@ -11,7 +9,7 @@ from gem.utils import cached_property -class AbstractPointSet(with_metaclass(ABCMeta)): +class AbstractPointSet(metaclass=ABCMeta): """A way of specifying a known set of points, perhaps with some (tensor) structure.""" diff --git a/finat/quadrature.py b/finat/quadrature.py index 4a4ffbe96..268e213df 100644 --- a/finat/quadrature.py +++ b/finat/quadrature.py @@ -1,5 +1,4 @@ from __future__ import absolute_import, print_function, division -from six import with_metaclass from abc import ABCMeta, abstractproperty from functools import reduce @@ -61,7 +60,7 @@ def make_quadrature(ref_el, degree, scheme="default"): return QuadratureRule(PointSet(fiat_rule.get_points()), fiat_rule.get_weights()) -class AbstractQuadratureRule(with_metaclass(ABCMeta)): +class AbstractQuadratureRule(metaclass=ABCMeta): """Abstract class representing a quadrature rule as point set and a corresponding set of weights.""" diff --git a/finat/quadrature_element.py b/finat/quadrature_element.py index 7c802b4ce..9d26db174 100644 --- a/finat/quadrature_element.py +++ b/finat/quadrature_element.py @@ -1,6 +1,4 @@ from __future__ import absolute_import, print_function, division -from six import iteritems -from six.moves import range, zip from functools import reduce import numpy @@ -35,7 +33,7 @@ def formdegree(self): def _entity_dofs(self): # Inspired by ffc/quadratureelement.py entity_dofs = {dim: {entity: [] for entity in entities} - for dim, entities in iteritems(self.cell.get_topology())} + for dim, entities in self.cell.get_topology().items()} entity_dofs[self.cell.get_dimension()] = {0: list(range(self.space_dimension()))} return entity_dofs diff --git a/finat/quadrilateral.py b/finat/quadrilateral.py index 97cc816a8..098dadccd 100644 --- a/finat/quadrilateral.py +++ b/finat/quadrilateral.py @@ -1,5 +1,4 @@ from __future__ import absolute_import, print_function, division -from six import iteritems from FIAT.reference_element import UFCQuadrilateral @@ -74,8 +73,8 @@ def flatten(dofs): flat_dofs = {} flat_dofs[0] = dofs[(0, 0)] flat_dofs[1] = dict(enumerate( - [v for k, v in sorted(iteritems(dofs[(0, 1)]))] + - [v for k, v in sorted(iteritems(dofs[(1, 0)]))] + [v for k, v in sorted(dofs[(0, 1)].items())] + + [v for k, v in sorted(dofs[(1, 0)].items())] )) flat_dofs[2] = dofs[(1, 1)] return flat_dofs diff --git a/finat/sympy2gem.py b/finat/sympy2gem.py index a3d5bb66f..18f8cbc4b 100644 --- a/finat/sympy2gem.py +++ b/finat/sympy2gem.py @@ -1,5 +1,4 @@ from __future__ import absolute_import, print_function, division -from six.moves import map from functools import reduce diff --git a/finat/tensor_product.py b/finat/tensor_product.py index f95eaa2be..88a8cb18a 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -1,5 +1,4 @@ from __future__ import absolute_import, print_function, division -from six.moves import range, zip from functools import reduce from itertools import chain, product diff --git a/finat/tensorfiniteelement.py b/finat/tensorfiniteelement.py index f6c9f6033..f464d66a3 100644 --- a/finat/tensorfiniteelement.py +++ b/finat/tensorfiniteelement.py @@ -1,5 +1,4 @@ from __future__ import absolute_import, print_function, division -from six import iteritems from functools import reduce @@ -116,7 +115,7 @@ def _tensorise(self, scalar_evaluation): index_ordering = scalar_i + tensor_i + tensor_vi + scalar_vi result = {} - for alpha, expr in iteritems(scalar_evaluation): + for alpha, expr in scalar_evaluation.items(): result[alpha] = gem.ComponentTensor( gem.Product(deltas, gem.Indexed(expr, scalar_i + scalar_vi)), index_ordering diff --git a/test/test_point_evaluation_ciarlet.py b/test/test_point_evaluation_ciarlet.py index 61bef4465..db1225161 100644 --- a/test/test_point_evaluation_ciarlet.py +++ b/test/test_point_evaluation_ciarlet.py @@ -1,5 +1,4 @@ from __future__ import absolute_import, print_function, division -from six import iteritems import pytest @@ -22,7 +21,7 @@ def test_cellwise_constant(cell, degree): point = gem.partial_indexed(gem.Variable('X', (17, dim)), (index,)) order = 2 - for alpha, table in iteritems(element.point_evaluation(order, point)): + for alpha, table in element.point_evaluation(order, point).items(): if sum(alpha) < degree: assert table.free_indices == (index,) else: From 2bd31590a1714476895a036bf20ea128ae54740c Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 7 Dec 2017 10:53:45 +0000 Subject: [PATCH 460/749] remove singledispatch package dependency --- finat/fiat_elements.py | 2 +- finat/sympy2gem.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 80babbcec..c9cd0169d 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -2,7 +2,7 @@ import numpy as np import sympy as sp -from singledispatch import singledispatch +from functools import singledispatch import FIAT from FIAT.polynomial_set import mis, form_matrix_product diff --git a/finat/sympy2gem.py b/finat/sympy2gem.py index 18f8cbc4b..62e9b4ae9 100644 --- a/finat/sympy2gem.py +++ b/finat/sympy2gem.py @@ -1,8 +1,7 @@ from __future__ import absolute_import, print_function, division -from functools import reduce +from functools import singledispatch, reduce -from singledispatch import singledispatch import sympy import gem From dacbef40206c191305aa491e052ad6ce265672d1 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 7 Dec 2017 10:59:33 +0000 Subject: [PATCH 461/749] remove future imports --- finat/__init__.py | 2 -- finat/discontinuous.py | 2 -- finat/enriched.py | 2 -- finat/fiat_elements.py | 2 -- finat/finiteelementbase.py | 2 -- finat/hdivcurl.py | 2 -- finat/mixed.py | 2 -- finat/point_set.py | 2 -- finat/quadrature.py | 2 -- finat/quadrature_element.py | 1 - finat/quadrilateral.py | 2 -- finat/runtime_tabulated.py | 2 -- finat/spectral.py | 2 -- finat/sympy2gem.py | 2 -- finat/tensor_product.py | 2 -- finat/tensorfiniteelement.py | 2 -- finat/trace.py | 2 -- test/test_point_evaluation_ciarlet.py | 2 -- 18 files changed, 35 deletions(-) diff --git a/finat/__init__.py b/finat/__init__.py index 09962bbe9..f0c01fa68 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, print_function, division - from .fiat_elements import Bubble, CrouzeixRaviart, DiscontinuousTaylor # noqa: F401 from .fiat_elements import Lagrange, DiscontinuousLagrange # noqa: F401 from .fiat_elements import BrezziDouglasMarini, BrezziDouglasFortinMarini # noqa: F401 diff --git a/finat/discontinuous.py b/finat/discontinuous.py index e4399911c..e8309df63 100644 --- a/finat/discontinuous.py +++ b/finat/discontinuous.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, print_function, division - from gem.utils import cached_property from finat.finiteelementbase import FiniteElementBase diff --git a/finat/enriched.py b/finat/enriched.py index ab5cb2563..4563ebc61 100644 --- a/finat/enriched.py +++ b/finat/enriched.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, print_function, division - from functools import partial from operator import add, methodcaller diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index c9cd0169d..f74eedf7b 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, print_function, division - import numpy as np import sympy as sp from functools import singledispatch diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index a429de64e..658ec8d5e 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, print_function, division - from abc import ABCMeta, abstractproperty, abstractmethod from itertools import chain diff --git a/finat/hdivcurl.py b/finat/hdivcurl.py index 28a5184df..5027b4a6f 100644 --- a/finat/hdivcurl.py +++ b/finat/hdivcurl.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, division, print_function - from FIAT.reference_element import LINE import gem diff --git a/finat/mixed.py b/finat/mixed.py index edb128a2b..baf04d988 100644 --- a/finat/mixed.py +++ b/finat/mixed.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, print_function, division - import numpy import gem diff --git a/finat/point_set.py b/finat/point_set.py index 95b334233..ffc754a8e 100644 --- a/finat/point_set.py +++ b/finat/point_set.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, print_function, division - from abc import ABCMeta, abstractproperty from itertools import chain, product diff --git a/finat/quadrature.py b/finat/quadrature.py index 268e213df..85b95758d 100644 --- a/finat/quadrature.py +++ b/finat/quadrature.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, print_function, division - from abc import ABCMeta, abstractproperty from functools import reduce diff --git a/finat/quadrature_element.py b/finat/quadrature_element.py index 9d26db174..db68ff682 100644 --- a/finat/quadrature_element.py +++ b/finat/quadrature_element.py @@ -1,4 +1,3 @@ -from __future__ import absolute_import, print_function, division from functools import reduce import numpy diff --git a/finat/quadrilateral.py b/finat/quadrilateral.py index 098dadccd..f0db0a850 100644 --- a/finat/quadrilateral.py +++ b/finat/quadrilateral.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, print_function, division - from FIAT.reference_element import UFCQuadrilateral from gem.utils import cached_property diff --git a/finat/runtime_tabulated.py b/finat/runtime_tabulated.py index c2d5b6f7b..5b3d71c02 100644 --- a/finat/runtime_tabulated.py +++ b/finat/runtime_tabulated.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, print_function, division - from FIAT.polynomial_set import mis from FIAT.reference_element import LINE diff --git a/finat/spectral.py b/finat/spectral.py index c6dff1109..8b8d70efc 100644 --- a/finat/spectral.py +++ b/finat/spectral.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, print_function, division - import FIAT import gem diff --git a/finat/sympy2gem.py b/finat/sympy2gem.py index 62e9b4ae9..889a643a6 100644 --- a/finat/sympy2gem.py +++ b/finat/sympy2gem.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, print_function, division - from functools import singledispatch, reduce import sympy diff --git a/finat/tensor_product.py b/finat/tensor_product.py index 88a8cb18a..95b2c4d5f 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, print_function, division - from functools import reduce from itertools import chain, product from operator import methodcaller diff --git a/finat/tensorfiniteelement.py b/finat/tensorfiniteelement.py index f464d66a3..8ea7a3e11 100644 --- a/finat/tensorfiniteelement.py +++ b/finat/tensorfiniteelement.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, print_function, division - from functools import reduce import numpy diff --git a/finat/trace.py b/finat/trace.py index 8275d7c76..9b621c81e 100644 --- a/finat/trace.py +++ b/finat/trace.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, print_function, division - import FIAT from finat.fiat_elements import ScalarFiatElement diff --git a/test/test_point_evaluation_ciarlet.py b/test/test_point_evaluation_ciarlet.py index db1225161..5e13d918b 100644 --- a/test/test_point_evaluation_ciarlet.py +++ b/test/test_point_evaluation_ciarlet.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, print_function, division - import pytest import FIAT From ccdbb4def54b24cd0acc77d270c7e75fc0cf8233 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 7 Dec 2017 11:22:42 +0000 Subject: [PATCH 462/749] remove six dependency --- gem/coffee.py | 1 - gem/gem.py | 6 ++---- gem/impero.py | 3 +-- gem/impero_utils.py | 1 - gem/interpreter.py | 1 - gem/node.py | 1 - gem/optimise.py | 3 +-- gem/refactorise.py | 11 +++++------ gem/unconcatenate.py | 1 - gem/utils.py | 3 +-- 10 files changed, 10 insertions(+), 21 deletions(-) diff --git a/gem/coffee.py b/gem/coffee.py index f34326c30..585934099 100644 --- a/gem/coffee.py +++ b/gem/coffee.py @@ -5,7 +5,6 @@ """ from __future__ import absolute_import, print_function, division -from six.moves import map, range from collections import OrderedDict import itertools diff --git a/gem/gem.py b/gem/gem.py index 978f17ce7..c2c2f6357 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -15,8 +15,6 @@ """ from __future__ import absolute_import, print_function, division -from six import with_metaclass -from six.moves import range, zip from abc import ABCMeta from itertools import chain @@ -57,7 +55,7 @@ def __call__(self, *args, **kwargs): return obj -class Node(with_metaclass(NodeMeta, NodeBase)): +class Node(NodeBase, metaclass=NodeMeta): """Abstract GEM node class.""" __slots__ = ('free_indices',) @@ -380,7 +378,7 @@ def __new__(cls, condition, then, else_): return self -class IndexBase(with_metaclass(ABCMeta)): +class IndexBase(metaclass=ABCMeta): """Abstract base class for indices.""" pass diff --git a/gem/impero.py b/gem/impero.py index 3eee267e1..ec4b4de89 100644 --- a/gem/impero.py +++ b/gem/impero.py @@ -11,7 +11,6 @@ """ from __future__ import absolute_import, print_function, division -from six import with_metaclass from abc import ABCMeta, abstractmethod @@ -24,7 +23,7 @@ class Node(NodeBase): __slots__ = () -class Terminal(with_metaclass(ABCMeta, Node)): +class Terminal(Node, metaclass=ABCMeta): """Abstract class for terminal Impero nodes""" __slots__ = () diff --git a/gem/impero_utils.py b/gem/impero_utils.py index 43e739be1..a2e288dc0 100644 --- a/gem/impero_utils.py +++ b/gem/impero_utils.py @@ -7,7 +7,6 @@ """ from __future__ import absolute_import, print_function, division -from six.moves import filter import collections from itertools import chain, groupby diff --git a/gem/interpreter.py b/gem/interpreter.py index a34ff5770..191834d78 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -2,7 +2,6 @@ An interpreter for GEM trees. """ from __future__ import absolute_import, print_function, division -from six.moves import map import numpy import operator diff --git a/gem/node.py b/gem/node.py index 6738381bd..45871875e 100644 --- a/gem/node.py +++ b/gem/node.py @@ -2,7 +2,6 @@ expression DAG languages.""" from __future__ import absolute_import, print_function, division -from six.moves import map, zip import collections diff --git a/gem/optimise.py b/gem/optimise.py index d19006dec..4ed78a5a0 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -2,11 +2,10 @@ expressions.""" from __future__ import absolute_import, print_function, division -from six.moves import filter, map, zip, zip_longest from collections import OrderedDict, defaultdict from functools import partial, reduce -from itertools import combinations, permutations +from itertools import combinations, permutations, zip_longest import numpy from singledispatch import singledispatch diff --git a/gem/refactorise.py b/gem/refactorise.py index d3e2d4a71..df9fc3d78 100644 --- a/gem/refactorise.py +++ b/gem/refactorise.py @@ -2,11 +2,10 @@ refactorisation.""" from __future__ import absolute_import, print_function, division -from six import iteritems -from six.moves import intern, map from collections import Counter, OrderedDict, defaultdict, namedtuple from itertools import product +from sys import intern from gem.node import Memoizer, traversal from gem.gem import Node, Zero, Product, Sum, Indexed, ListTensor, one @@ -73,7 +72,7 @@ def add(self, sum_indices, atomics, rest): assert len(sum_indices) == len(sum_indices_set) atomics = tuple(atomics) - atomics_set = frozenset(iteritems(Counter(atomics))) + atomics_set = frozenset(Counter(atomics).items()) assert isinstance(rest, Node) @@ -83,7 +82,7 @@ def add(self, sum_indices, atomics, rest): def __iter__(self): """Iteration yields :py:class:`Monomial` objects""" - for key, (sum_indices, atomics) in iteritems(self.ordering): + for key, (sum_indices, atomics) in self.ordering.items(): rest = self.monomials[key] yield Monomial(sum_indices, atomics, rest) @@ -95,9 +94,9 @@ def sum(*args): assert isinstance(arg, MonomialSum) # Optimised implementation: no need to decompose and # reconstruct key. - for key, rest in iteritems(arg.monomials): + for key, rest in arg.monomials.items(): result.monomials[key] = Sum(result.monomials[key], rest) - for key, value in iteritems(arg.ordering): + for key, value in arg.ordering.items(): result.ordering.setdefault(key, value) return result diff --git a/gem/unconcatenate.py b/gem/unconcatenate.py index 975b74c63..423e317dc 100644 --- a/gem/unconcatenate.py +++ b/gem/unconcatenate.py @@ -51,7 +51,6 @@ """ from __future__ import absolute_import, print_function, division -from six.moves import map, range, zip from itertools import chain diff --git a/gem/utils.py b/gem/utils.py index 1d7b62cc6..faba69d34 100644 --- a/gem/utils.py +++ b/gem/utils.py @@ -1,5 +1,4 @@ from __future__ import absolute_import, print_function, division -from six import viewitems import collections @@ -36,7 +35,7 @@ def groupby(iterable, key=None): groups = collections.OrderedDict() for elem in iterable: groups.setdefault(key(elem), []).append(elem) - return viewitems(groups) + return groups.items() def make_proxy_class(name, cls): From 354d51cc5f3280cc88311e8b724423024d21678c Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 7 Dec 2017 11:30:33 +0000 Subject: [PATCH 463/749] remove singledispatch package dependency --- gem/impero_utils.py | 3 +-- gem/interpreter.py | 2 +- gem/optimise.py | 3 +-- gem/unconcatenate.py | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/gem/impero_utils.py b/gem/impero_utils.py index a2e288dc0..378f2648e 100644 --- a/gem/impero_utils.py +++ b/gem/impero_utils.py @@ -9,10 +9,9 @@ from __future__ import absolute_import, print_function, division import collections +from functools import singledispatch from itertools import chain, groupby -from singledispatch import singledispatch - from gem.node import traversal, collect_refcount from gem import gem, impero as imp, optimise, scheduling diff --git a/gem/interpreter.py b/gem/interpreter.py index 191834d78..950b5a73c 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -7,7 +7,7 @@ import operator import math from collections import OrderedDict -from singledispatch import singledispatch +from functools import singledispatch import itertools from gem import gem, node diff --git a/gem/optimise.py b/gem/optimise.py index 4ed78a5a0..3ac4d1d85 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -4,11 +4,10 @@ from __future__ import absolute_import, print_function, division from collections import OrderedDict, defaultdict -from functools import partial, reduce +from functools import singledispatch, partial, reduce from itertools import combinations, permutations, zip_longest import numpy -from singledispatch import singledispatch from gem.utils import groupby from gem.node import (Memoizer, MemoizerArg, reuse_if_untouched, diff --git a/gem/unconcatenate.py b/gem/unconcatenate.py index 423e317dc..01b392211 100644 --- a/gem/unconcatenate.py +++ b/gem/unconcatenate.py @@ -52,10 +52,10 @@ from __future__ import absolute_import, print_function, division +from functools import singledispatch from itertools import chain import numpy -from singledispatch import singledispatch from gem.node import Memoizer, reuse_if_untouched from gem.gem import (ComponentTensor, Concatenate, FlexiblyIndexed, From 9d3c61a194c5bbb7805742b1f75d81972a6ce6b2 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 7 Dec 2017 11:38:46 +0000 Subject: [PATCH 464/749] remove future imports --- gem/__init__.py | 2 -- gem/coffee.py | 2 -- gem/gem.py | 2 -- gem/impero.py | 2 -- gem/impero_utils.py | 2 -- gem/interpreter.py | 2 -- gem/node.py | 2 -- gem/optimise.py | 2 -- gem/refactorise.py | 2 -- gem/scheduling.py | 2 -- gem/unconcatenate.py | 2 -- gem/utils.py | 2 -- 12 files changed, 24 deletions(-) diff --git a/gem/__init__.py b/gem/__init__.py index af1b768cc..f1e772037 100644 --- a/gem/__init__.py +++ b/gem/__init__.py @@ -1,4 +1,2 @@ -from __future__ import absolute_import, print_function, division - from gem.gem import * # noqa from gem.optimise import select_expression # noqa diff --git a/gem/coffee.py b/gem/coffee.py index 585934099..4c48b2fe4 100644 --- a/gem/coffee.py +++ b/gem/coffee.py @@ -4,8 +4,6 @@ This file is NOT for code generation as a COFFEE AST. """ -from __future__ import absolute_import, print_function, division - from collections import OrderedDict import itertools import logging diff --git a/gem/gem.py b/gem/gem.py index c2c2f6357..d5870ac9f 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -14,8 +14,6 @@ indices. """ -from __future__ import absolute_import, print_function, division - from abc import ABCMeta from itertools import chain from operator import attrgetter diff --git a/gem/impero.py b/gem/impero.py index ec4b4de89..c909e1bfc 100644 --- a/gem/impero.py +++ b/gem/impero.py @@ -10,8 +10,6 @@ (Command?) after clicking on them. """ -from __future__ import absolute_import, print_function, division - from abc import ABCMeta, abstractmethod from gem.node import Node as NodeBase diff --git a/gem/impero_utils.py b/gem/impero_utils.py index 378f2648e..e56359752 100644 --- a/gem/impero_utils.py +++ b/gem/impero_utils.py @@ -6,8 +6,6 @@ C code or a COFFEE AST. """ -from __future__ import absolute_import, print_function, division - import collections from functools import singledispatch from itertools import chain, groupby diff --git a/gem/interpreter.py b/gem/interpreter.py index 950b5a73c..a7e2e7243 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -1,8 +1,6 @@ """ An interpreter for GEM trees. """ -from __future__ import absolute_import, print_function, division - import numpy import operator import math diff --git a/gem/node.py b/gem/node.py index 45871875e..8345a0d62 100644 --- a/gem/node.py +++ b/gem/node.py @@ -1,8 +1,6 @@ """Generic abstract node class and utility functions for creating expression DAG languages.""" -from __future__ import absolute_import, print_function, division - import collections diff --git a/gem/optimise.py b/gem/optimise.py index 3ac4d1d85..ebfffeee1 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -1,8 +1,6 @@ """A set of routines implementing various transformations on GEM expressions.""" -from __future__ import absolute_import, print_function, division - from collections import OrderedDict, defaultdict from functools import singledispatch, partial, reduce from itertools import combinations, permutations, zip_longest diff --git a/gem/refactorise.py b/gem/refactorise.py index df9fc3d78..1c65d11dd 100644 --- a/gem/refactorise.py +++ b/gem/refactorise.py @@ -1,8 +1,6 @@ """Data structures and algorithms for generic expansion and refactorisation.""" -from __future__ import absolute_import, print_function, division - from collections import Counter, OrderedDict, defaultdict, namedtuple from itertools import product from sys import intern diff --git a/gem/scheduling.py b/gem/scheduling.py index 767abce4b..1fbb563c6 100644 --- a/gem/scheduling.py +++ b/gem/scheduling.py @@ -1,8 +1,6 @@ """Schedules operations to evaluate a multi-root expression DAG, forming an ordered list of Impero terminals.""" -from __future__ import absolute_import, print_function, division - import collections import functools diff --git a/gem/unconcatenate.py b/gem/unconcatenate.py index 01b392211..ce6e30b3c 100644 --- a/gem/unconcatenate.py +++ b/gem/unconcatenate.py @@ -50,8 +50,6 @@ unconcatenation, and then add up the results. """ -from __future__ import absolute_import, print_function, division - from functools import singledispatch from itertools import chain diff --git a/gem/utils.py b/gem/utils.py index faba69d34..12e0e0f6c 100644 --- a/gem/utils.py +++ b/gem/utils.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, print_function, division - import collections From 665949edb43091c87942a27d7764ffe489ec1e41 Mon Sep 17 00:00:00 2001 From: NicholasBermuda Date: Sun, 28 May 2017 17:19:17 +0100 Subject: [PATCH 465/749] added Conj to interpreter --- gem/interpreter.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/gem/interpreter.py b/gem/interpreter.py index a7e2e7243..94054b6c0 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -168,6 +168,15 @@ def _evaluate_operator(e, self): result.arr = op(a.broadcast(fids), b.broadcast(fids)) return result +@_evaluate.register(gem.Conj) # noqa: F811 # i don't know what this is for? +def _(e, self): + ops = [self(o) for o in e.children] + + result = Result.empty(*ops) + for idx in numpy.ndindex(result.tshape): + result[idx] = [o[o.filter(idx,result.fids)].conjugate() for o in ops] + return result + @_evaluate.register(gem.MathFunction) def _evaluate_mathfunction(e, self): From ea95fb5b3275e4e235d9178e8f1a1fe7b4eeb346 Mon Sep 17 00:00:00 2001 From: NicholasBermuda Date: Sun, 28 May 2017 17:21:39 +0100 Subject: [PATCH 466/749] added Conj node --- gem/gem.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index d5870ac9f..1d068c931 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -26,7 +26,7 @@ __all__ = ['Node', 'Identity', 'Literal', 'Zero', 'Failure', - 'Variable', 'Sum', 'Product', 'Division', 'Power', + 'Variable', 'Sum', 'Product', 'Division', 'Power', 'Conj', 'MathFunction', 'MinValue', 'MaxValue', 'Comparison', 'LogicalNot', 'LogicalAnd', 'LogicalOr', 'Conditional', 'Index', 'VariableIndex', 'Indexed', 'ComponentTensor', @@ -105,7 +105,7 @@ class Constant(Terminal): Convention: - array: numpy array of values - - value: float value (scalars only) + - value: complex value (scalars only) """ __slots__ = () @@ -158,7 +158,7 @@ def __new__(cls, array): return super(Literal, cls).__new__(cls) def __init__(self, array): - self.array = asarray(array, dtype=float) + self.array = asarray(array, dtype='complex128') def is_equal(self, other): if type(self) != type(other): @@ -172,7 +172,7 @@ def get_hash(self): @property def value(self): - return float(self.array) + return self.array @property def shape(self): @@ -282,6 +282,20 @@ def __new__(cls, base, exponent): return self +class Conj(Scalar): + __slots__ = ('children',) + + def __new__(cls, a): + assert not a.shape + + if isinstance(a, Constant): + return Literal(a.value.conjugate()) + + self = super(Conj, cls).__new__(cls) + self.children = a + return self + + class MathFunction(Scalar): __slots__ = ('name', 'children') __front__ = ('name',) From 36edf9f03001d2f716a8efbf3f2941495c2bd8f9 Mon Sep 17 00:00:00 2001 From: NicholasBermuda Date: Fri, 16 Jun 2017 10:16:33 +0100 Subject: [PATCH 467/749] tidy up style --- gem/interpreter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gem/interpreter.py b/gem/interpreter.py index 94054b6c0..1d83dd084 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -168,7 +168,8 @@ def _evaluate_operator(e, self): result.arr = op(a.broadcast(fids), b.broadcast(fids)) return result -@_evaluate.register(gem.Conj) # noqa: F811 # i don't know what this is for? + +@_evaluate.register(gem.Conj) # noqa: F811 def _(e, self): ops = [self(o) for o in e.children] From 676ab4954f3bac882aebc6c2eb1afe19f1f0a27b Mon Sep 17 00:00:00 2001 From: NicholasBermuda Date: Mon, 19 Jun 2017 12:29:47 +0100 Subject: [PATCH 468/749] change conj to MathFunction --- gem/interpreter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gem/interpreter.py b/gem/interpreter.py index 1d83dd084..922003960 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -184,7 +184,8 @@ def _evaluate_mathfunction(e, self): ops = [self(o) for o in e.children] result = Result.empty(*ops) names = {"abs": abs, - "log": math.log} + "log": math.log, + "conj": complex.conjugate} op = names[e.name] for idx in numpy.ndindex(result.tshape): result[idx] = op(*(o[o.filter(idx, result.fids)] for o in ops)) From 320c21a4d5ef51c0147affad13c61c8afd85f885 Mon Sep 17 00:00:00 2001 From: NicholasBermuda Date: Wed, 21 Jun 2017 16:37:04 +0100 Subject: [PATCH 469/749] add real and imag nodes to gem --- gem/gem.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/gem/gem.py b/gem/gem.py index 1d068c931..213cb3f0b 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -25,7 +25,7 @@ from gem.node import Node as NodeBase -__all__ = ['Node', 'Identity', 'Literal', 'Zero', 'Failure', +__all__ = ['Node', 'Identity', 'Literal', 'Zero', 'Failure', 'Real', 'Imag', 'Variable', 'Sum', 'Product', 'Division', 'Power', 'Conj', 'MathFunction', 'MinValue', 'MaxValue', 'Comparison', 'LogicalNot', 'LogicalAnd', 'LogicalOr', 'Conditional', @@ -296,6 +296,34 @@ def __new__(cls, a): return self +class Real(Scalar): + __slots__ = ('children',) + + def __new__(cls, a): + assert not a.shape + + if isinstance(a, Constant): + return Literal(a.value.real) + + self = super(Real, cls).__new__(cls) + self.children = a + return self + + +class Imag(Scalar): + __slots__ = ('children',) + + def __new__(cls, a): + assert not a.shape + + if isinstance(a, Constant): + return Literal(a.value.imag) + + self = super(Imag, cls).__new__(cls) + self.children = a + return self + + class MathFunction(Scalar): __slots__ = ('name', 'children') __front__ = ('name',) From 7792944f2ee4c7a76d7bff932e460c05581d925f Mon Sep 17 00:00:00 2001 From: NicholasBermuda Date: Wed, 21 Jun 2017 16:39:29 +0100 Subject: [PATCH 470/749] real and imag evaluations, move conjugate to mathfunction --- gem/interpreter.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/gem/interpreter.py b/gem/interpreter.py index 922003960..6ab26bcc4 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -169,13 +169,23 @@ def _evaluate_operator(e, self): return result -@_evaluate.register(gem.Conj) # noqa: F811 +@_evaluate.register(gem.Real) # noqa: F811 def _(e, self): ops = [self(o) for o in e.children] result = Result.empty(*ops) for idx in numpy.ndindex(result.tshape): - result[idx] = [o[o.filter(idx,result.fids)].conjugate() for o in ops] + result[idx] = [o[o.filter(idx,result.fids)].real for o in ops] + return result + + +@_evaluate.register(gem.Imag) # noqa: F811 +def _(e, self): + ops = [self(o) for o in e.children] + + result = Result.empty(*ops) + for idx in numpy.ndindex(result.tshape): + result[idx] = [o[o.filter(idx,result.fids)].imag for o in ops] return result From 97134b95591f94f88aeb87da1ee5560a98b6b4ef Mon Sep 17 00:00:00 2001 From: NicholasBermuda Date: Mon, 26 Jun 2017 16:46:29 +0100 Subject: [PATCH 471/749] make conj like real and imag --- gem/interpreter.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/gem/interpreter.py b/gem/interpreter.py index 6ab26bcc4..01ce4c291 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -169,6 +169,16 @@ def _evaluate_operator(e, self): return result +@_evaluate.register(gem.Conj) # noqa: F811 +def _(e, self): + ops = [self(o) for o in e.children] + + result = Result.empty(*ops) + for idx in numpy.ndindex(result.tshape): + result[idx] = [o[o.filter(idx,result.fids)].conjugate() for o in ops] + return result + + @_evaluate.register(gem.Real) # noqa: F811 def _(e, self): ops = [self(o) for o in e.children] @@ -194,8 +204,7 @@ def _evaluate_mathfunction(e, self): ops = [self(o) for o in e.children] result = Result.empty(*ops) names = {"abs": abs, - "log": math.log, - "conj": complex.conjugate} + "log": math.log} op = names[e.name] for idx in numpy.ndindex(result.tshape): result[idx] = op(*(o[o.filter(idx, result.fids)] for o in ops)) From 7fc397672afb109f8d7080bc719cd6d4d24c6c7a Mon Sep 17 00:00:00 2001 From: NicholasBermuda Date: Tue, 27 Jun 2017 20:01:19 +0100 Subject: [PATCH 472/749] make sure to import the right functions --- gem/gem.py | 70 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 213cb3f0b..4a30bd05d 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -25,8 +25,8 @@ from gem.node import Node as NodeBase -__all__ = ['Node', 'Identity', 'Literal', 'Zero', 'Failure', 'Real', 'Imag', - 'Variable', 'Sum', 'Product', 'Division', 'Power', 'Conj', +__all__ = ['Node', 'Identity', 'Literal', 'Zero', 'Failure', 'ComplexPartsFunction', + 'Variable', 'Sum', 'Product', 'Division', 'Power', 'MathFunction', 'MinValue', 'MaxValue', 'Comparison', 'LogicalNot', 'LogicalAnd', 'LogicalOr', 'Conditional', 'Index', 'VariableIndex', 'Indexed', 'ComponentTensor', @@ -282,46 +282,46 @@ def __new__(cls, base, exponent): return self -class Conj(Scalar): - __slots__ = ('children',) +# class Conj(Scalar): +# __slots__ = ('children',) - def __new__(cls, a): - assert not a.shape +# def __new__(cls, a): +# assert not a.shape - if isinstance(a, Constant): - return Literal(a.value.conjugate()) +# if isinstance(a, Constant): +# return Literal(a.value.conjugate()) - self = super(Conj, cls).__new__(cls) - self.children = a - return self +# self = super(Conj, cls).__new__(cls) +# self.children = a +# return self -class Real(Scalar): - __slots__ = ('children',) +# class Real(Scalar): +# __slots__ = ('children',) - def __new__(cls, a): - assert not a.shape +# def __new__(cls, a): +# assert not a.shape - if isinstance(a, Constant): - return Literal(a.value.real) +# if isinstance(a, Constant): +# return Literal(a.value.real) - self = super(Real, cls).__new__(cls) - self.children = a - return self +# self = super(Real, cls).__new__(cls) +# self.children = a +# return self -class Imag(Scalar): - __slots__ = ('children',) +# class Imag(Scalar): +# __slots__ = ('children',) - def __new__(cls, a): - assert not a.shape +# def __new__(cls, a): +# assert not a.shape - if isinstance(a, Constant): - return Literal(a.value.imag) +# if isinstance(a, Constant): +# return Literal(a.value.imag) - self = super(Imag, cls).__new__(cls) - self.children = a - return self +# self = super(Imag, cls).__new__(cls) +# self.children = a +# return self class MathFunction(Scalar): @@ -336,6 +336,18 @@ def __init__(self, name, *args): self.children = args +class ComplexPartsFunction(Scalar): + __slots__ = ('name', 'children') + __front__ = ('name',) + + def __init__(self, name, *args): + assert isinstance(name, str) + assert all(arg.shape == () for arg in args) + + self.name = name + self.children = args + + class MinValue(Scalar): __slots__ = ('children',) From cfb03814ddf3a9998813414ae5063082a307f129 Mon Sep 17 00:00:00 2001 From: NicholasBermuda Date: Tue, 27 Jun 2017 20:02:57 +0100 Subject: [PATCH 473/749] new complex parts function --- gem/interpreter.py | 53 +++++++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/gem/interpreter.py b/gem/interpreter.py index 01ce4c291..41ecc830e 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -169,33 +169,48 @@ def _evaluate_operator(e, self): return result -@_evaluate.register(gem.Conj) # noqa: F811 -def _(e, self): - ops = [self(o) for o in e.children] +# @_evaluate.register(gem.Conj) # noqa: F811 +# def _(e, self): +# ops = [self(o) for o in e.children] - result = Result.empty(*ops) - for idx in numpy.ndindex(result.tshape): - result[idx] = [o[o.filter(idx,result.fids)].conjugate() for o in ops] - return result +# result = Result.empty(*ops) +# for idx in numpy.ndindex(result.tshape): +# result[idx] = [o[o.filter(idx,result.fids)].conjugate() for o in ops] +# return result -@_evaluate.register(gem.Real) # noqa: F811 -def _(e, self): - ops = [self(o) for o in e.children] +# @_evaluate.register(gem.Real) # noqa: F811 +# def _(e, self): +# ops = [self(o) for o in e.children] - result = Result.empty(*ops) - for idx in numpy.ndindex(result.tshape): - result[idx] = [o[o.filter(idx,result.fids)].real for o in ops] - return result +# result = Result.empty(*ops) +# for idx in numpy.ndindex(result.tshape): +# result[idx] = [o[o.filter(idx,result.fids)].real for o in ops] +# return result + + +# @_evaluate.register(gem.Imag) # noqa: F811 +# def _(e, self): +# ops = [self(o) for o in e.children] +# result = Result.empty(*ops) +# for idx in numpy.ndindex(result.tshape): +# result[idx] = [o[o.filter(idx,result.fids)].imag for o in ops] +# return result -@_evaluate.register(gem.Imag) # noqa: F811 + +@_evaluate.register(gem.ComplexPartsFunction) # noqa: F811 def _(e, self): ops = [self(o) for o in e.children] - - result = Result.empty(*ops) - for idx in numpy.ndindex(result.tshape): - result[idx] = [o[o.filter(idx,result.fids)].imag for o in ops] + if e.name is 'imag': + for idx in numpy.ndindex(result.tshape): + result[idx] = [o[o.filter(idx,result.fids)].imag for o in ops] + elif e.name is 'conj': + for idx in numpy.ndindex(result.tshape): + result[idx] = [o[o.filter(idx,result.fids)].conjugate() for o in ops] + else: + for idx in numpy.ndindex(result.tshape): + result[idx] = [o[o.filter(idx,result.fids)].real for o in ops] return result From a0bbf5d21bbc56b514d65afaae47bacedce4430f Mon Sep 17 00:00:00 2001 From: NicholasBermuda Date: Thu, 29 Jun 2017 19:41:25 +0100 Subject: [PATCH 474/749] more changes in type checking --- gem/gem.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gem/gem.py b/gem/gem.py index 4a30bd05d..1fa4d159a 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -24,6 +24,8 @@ from gem.node import Node as NodeBase +from tsfc.parameters import numpy_type + __all__ = ['Node', 'Identity', 'Literal', 'Zero', 'Failure', 'ComplexPartsFunction', 'Variable', 'Sum', 'Product', 'Division', 'Power', @@ -158,7 +160,7 @@ def __new__(cls, array): return super(Literal, cls).__new__(cls) def __init__(self, array): - self.array = asarray(array, dtype='complex128') + self.array = asarray(array, dtype=numpy_type()) # import numpy_type() def is_equal(self, other): if type(self) != type(other): From 4a3fe2b4a3fd6445fb909104e3e786512462fd5b Mon Sep 17 00:00:00 2001 From: NicholasBermuda Date: Sun, 16 Jul 2017 19:02:44 +0100 Subject: [PATCH 475/749] python whitespace sux --- gem/gem.py | 46 ++-------------------------------------------- 1 file changed, 2 insertions(+), 44 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 1fa4d159a..30495e98f 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -27,8 +27,8 @@ from tsfc.parameters import numpy_type -__all__ = ['Node', 'Identity', 'Literal', 'Zero', 'Failure', 'ComplexPartsFunction', - 'Variable', 'Sum', 'Product', 'Division', 'Power', +__all__ = ['Node', 'Identity', 'Literal', 'Zero', 'Failure', 'ComplexPartsFunction', + 'Variable', 'Sum', 'Product', 'Division', 'Power', 'MathFunction', 'MinValue', 'MaxValue', 'Comparison', 'LogicalNot', 'LogicalAnd', 'LogicalOr', 'Conditional', 'Index', 'VariableIndex', 'Indexed', 'ComponentTensor', @@ -284,48 +284,6 @@ def __new__(cls, base, exponent): return self -# class Conj(Scalar): -# __slots__ = ('children',) - -# def __new__(cls, a): -# assert not a.shape - -# if isinstance(a, Constant): -# return Literal(a.value.conjugate()) - -# self = super(Conj, cls).__new__(cls) -# self.children = a -# return self - - -# class Real(Scalar): -# __slots__ = ('children',) - -# def __new__(cls, a): -# assert not a.shape - -# if isinstance(a, Constant): -# return Literal(a.value.real) - -# self = super(Real, cls).__new__(cls) -# self.children = a -# return self - - -# class Imag(Scalar): -# __slots__ = ('children',) - -# def __new__(cls, a): -# assert not a.shape - -# if isinstance(a, Constant): -# return Literal(a.value.imag) - -# self = super(Imag, cls).__new__(cls) -# self.children = a -# return self - - class MathFunction(Scalar): __slots__ = ('name', 'children') __front__ = ('name',) From dd97723bc847502bac63e18684396fd36722ef42 Mon Sep 17 00:00:00 2001 From: NicholasBermuda Date: Sun, 16 Jul 2017 19:03:29 +0100 Subject: [PATCH 476/749] remove old unused test code --- gem/interpreter.py | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/gem/interpreter.py b/gem/interpreter.py index 41ecc830e..736cdb82f 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -169,36 +169,6 @@ def _evaluate_operator(e, self): return result -# @_evaluate.register(gem.Conj) # noqa: F811 -# def _(e, self): -# ops = [self(o) for o in e.children] - -# result = Result.empty(*ops) -# for idx in numpy.ndindex(result.tshape): -# result[idx] = [o[o.filter(idx,result.fids)].conjugate() for o in ops] -# return result - - -# @_evaluate.register(gem.Real) # noqa: F811 -# def _(e, self): -# ops = [self(o) for o in e.children] - -# result = Result.empty(*ops) -# for idx in numpy.ndindex(result.tshape): -# result[idx] = [o[o.filter(idx,result.fids)].real for o in ops] -# return result - - -# @_evaluate.register(gem.Imag) # noqa: F811 -# def _(e, self): -# ops = [self(o) for o in e.children] - -# result = Result.empty(*ops) -# for idx in numpy.ndindex(result.tshape): -# result[idx] = [o[o.filter(idx,result.fids)].imag for o in ops] -# return result - - @_evaluate.register(gem.ComplexPartsFunction) # noqa: F811 def _(e, self): ops = [self(o) for o in e.children] From d9af36337c26dcd1ab46649afb374dbfe631bc96 Mon Sep 17 00:00:00 2001 From: NicholasBermuda Date: Sun, 16 Jul 2017 19:41:03 +0100 Subject: [PATCH 477/749] gonna need to refactor numpy_type --- gem/gem.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 30495e98f..60162f633 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -24,8 +24,6 @@ from gem.node import Node as NodeBase -from tsfc.parameters import numpy_type - __all__ = ['Node', 'Identity', 'Literal', 'Zero', 'Failure', 'ComplexPartsFunction', 'Variable', 'Sum', 'Product', 'Division', 'Power', @@ -160,7 +158,8 @@ def __new__(cls, array): return super(Literal, cls).__new__(cls) def __init__(self, array): - self.array = asarray(array, dtype=numpy_type()) # import numpy_type() + from tsfc.parameters import numpy_type + self.array = asarray(array, dtype=numpy_type()) def is_equal(self, other): if type(self) != type(other): From be91df633694fbc5c6c61c09466440599e7ee0eb Mon Sep 17 00:00:00 2001 From: NicholasBermuda Date: Mon, 17 Jul 2017 13:21:47 +0100 Subject: [PATCH 478/749] make sure tsfc in the right mode and avoid Literaly dtype problem for now --- gem/gem.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 60162f633..b06d27fa8 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -158,8 +158,10 @@ def __new__(cls, array): return super(Literal, cls).__new__(cls) def __init__(self, array): - from tsfc.parameters import numpy_type - self.array = asarray(array, dtype=numpy_type()) + # right now we use complex128, but maybe in the future we will determine this per user + # maybe at install time we'll have a switch to determine this? dtype = float or complex depending on mode + # from tsfc.parameters import numpy_type + self.array = asarray(array, dtype=complex) def is_equal(self, other): if type(self) != type(other): From 52af63b063f0a4f6a2172cd66921e075ae89cdc2 Mon Sep 17 00:00:00 2001 From: NicholasBermuda Date: Mon, 17 Jul 2017 14:07:29 +0100 Subject: [PATCH 479/749] new type handling --- gem/gem.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index b06d27fa8..bd03f4f97 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -158,10 +158,7 @@ def __new__(cls, array): return super(Literal, cls).__new__(cls) def __init__(self, array): - # right now we use complex128, but maybe in the future we will determine this per user - # maybe at install time we'll have a switch to determine this? dtype = float or complex depending on mode - # from tsfc.parameters import numpy_type - self.array = asarray(array, dtype=complex) + self.array = asarray(array, dtype=float) if asarray(array).dtype is numpy.dtype('int') else asarray(array) def is_equal(self, other): if type(self) != type(other): From 23b37b490e283eb53e4acce389672f66a5443324 Mon Sep 17 00:00:00 2001 From: NicholasBermuda Date: Wed, 16 Aug 2017 22:11:35 +0100 Subject: [PATCH 480/749] move complex fxns to mathfunction --- gem/gem.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index bd03f4f97..17938a44e 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -25,7 +25,7 @@ from gem.node import Node as NodeBase -__all__ = ['Node', 'Identity', 'Literal', 'Zero', 'Failure', 'ComplexPartsFunction', +__all__ = ['Node', 'Identity', 'Literal', 'Zero', 'Failure', 'Variable', 'Sum', 'Product', 'Division', 'Power', 'MathFunction', 'MinValue', 'MaxValue', 'Comparison', 'LogicalNot', 'LogicalAnd', 'LogicalOr', 'Conditional', @@ -294,18 +294,6 @@ def __init__(self, name, *args): self.children = args -class ComplexPartsFunction(Scalar): - __slots__ = ('name', 'children') - __front__ = ('name',) - - def __init__(self, name, *args): - assert isinstance(name, str) - assert all(arg.shape == () for arg in args) - - self.name = name - self.children = args - - class MinValue(Scalar): __slots__ = ('children',) From 531f85e2d1c9d9f511c6a220a110609c8c0ddd3a Mon Sep 17 00:00:00 2001 From: NicholasBermuda Date: Wed, 16 Aug 2017 22:14:46 +0100 Subject: [PATCH 481/749] remove last traces of complexpartsfunction --- gem/interpreter.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/gem/interpreter.py b/gem/interpreter.py index 736cdb82f..eb2c79d73 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -169,18 +169,25 @@ def _evaluate_operator(e, self): return result -@_evaluate.register(gem.ComplexPartsFunction) # noqa: F811 +@_evaluate.register(gem.MathFunction) # noqa: F811 def _(e, self): ops = [self(o) for o in e.children] + result = Result.empty(*ops) + names = {"abs": abs, + "log": math.log} + op = names[e.name] if e.name is 'imag': for idx in numpy.ndindex(result.tshape): result[idx] = [o[o.filter(idx,result.fids)].imag for o in ops] elif e.name is 'conj': for idx in numpy.ndindex(result.tshape): result[idx] = [o[o.filter(idx,result.fids)].conjugate() for o in ops] + elif e.name is 'real': + for idx in numpy.ndindex(result.tshape): + result[idx] = [o[o.filter(idx,result.fids)].real for o in ops] else: for idx in numpy.ndindex(result.tshape): - result[idx] = [o[o.filter(idx,result.fids)].real for o in ops] + result[idx] = op(*(o[o.filter(idx, result.fids)] for o in ops)) return result From 191a0f60a99cc37a11d4d827c3a2961d0f81c671 Mon Sep 17 00:00:00 2001 From: NicholasBermuda Date: Thu, 17 Aug 2017 22:00:07 +0100 Subject: [PATCH 482/749] add zero simplification for conj, real, imag in gem --- gem/gem.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/gem/gem.py b/gem/gem.py index 17938a44e..04be5e33b 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -286,12 +286,18 @@ class MathFunction(Scalar): __slots__ = ('name', 'children') __front__ = ('name',) - def __init__(self, name, *args): + def __new__(cls, name, *args): assert isinstance(name, str) assert all(arg.shape == () for arg in args) + if name in {'conj', 'real', 'imag'}: + if isinstance(args[0], Zero): + return args[0] + + self = super(MathFunction, cls).__new__(cls) self.name = name self.children = args + return self class MinValue(Scalar): From e637e25b2fbcec3202ceb5076be07d0fbb99f8e2 Mon Sep 17 00:00:00 2001 From: David Ham Date: Tue, 16 Jan 2018 11:08:02 +0000 Subject: [PATCH 483/749] Flake8 --- gem/interpreter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gem/interpreter.py b/gem/interpreter.py index eb2c79d73..40f8a47e6 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -178,13 +178,13 @@ def _(e, self): op = names[e.name] if e.name is 'imag': for idx in numpy.ndindex(result.tshape): - result[idx] = [o[o.filter(idx,result.fids)].imag for o in ops] + result[idx] = [o[o.filter(idx, result.fids)].imag for o in ops] elif e.name is 'conj': for idx in numpy.ndindex(result.tshape): - result[idx] = [o[o.filter(idx,result.fids)].conjugate() for o in ops] + result[idx] = [o[o.filter(idx, result.fids)].conjugate() for o in ops] elif e.name is 'real': for idx in numpy.ndindex(result.tshape): - result[idx] = [o[o.filter(idx,result.fids)].real for o in ops] + result[idx] = [o[o.filter(idx, result.fids)].real for o in ops] else: for idx in numpy.ndindex(result.tshape): result[idx] = op(*(o[o.filter(idx, result.fids)] for o in ops)) From 15e286615e11b020488bda6ebd01f49d8d0b3caf Mon Sep 17 00:00:00 2001 From: David Ham Date: Sat, 24 Feb 2018 12:39:14 +0000 Subject: [PATCH 484/749] Clean up complex code --- gem/gem.py | 17 ++++++++++++----- gem/interpreter.py | 16 ++-------------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 04be5e33b..e63095cf6 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -105,7 +105,7 @@ class Constant(Terminal): Convention: - array: numpy array of values - - value: complex value (scalars only) + - value: float or complex value (scalars only) """ __slots__ = () @@ -158,7 +158,10 @@ def __new__(cls, array): return super(Literal, cls).__new__(cls) def __init__(self, array): - self.array = asarray(array, dtype=float) if asarray(array).dtype is numpy.dtype('int') else asarray(array) + try: + array = asarray(array, dtype=float) + except TypeError: + array = asarray(array, dtype=complex) def is_equal(self, other): if type(self) != type(other): @@ -172,7 +175,10 @@ def get_hash(self): @property def value(self): - return self.array + try: + return float(array) + except TypeError: + return complex(array) @property def shape(self): @@ -291,8 +297,9 @@ def __new__(cls, name, *args): assert all(arg.shape == () for arg in args) if name in {'conj', 'real', 'imag'}: - if isinstance(args[0], Zero): - return args[0] + arg, = args + if isinstance(arg, Zero): + return arg self = super(MathFunction, cls).__new__(cls) self.name = name diff --git a/gem/interpreter.py b/gem/interpreter.py index 40f8a47e6..f06143b55 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -169,8 +169,8 @@ def _evaluate_operator(e, self): return result -@_evaluate.register(gem.MathFunction) # noqa: F811 -def _(e, self): +@_evaluate.register(gem.MathFunction) +def _evaluate_mathfunction(e, self): ops = [self(o) for o in e.children] result = Result.empty(*ops) names = {"abs": abs, @@ -191,18 +191,6 @@ def _(e, self): return result -@_evaluate.register(gem.MathFunction) -def _evaluate_mathfunction(e, self): - ops = [self(o) for o in e.children] - result = Result.empty(*ops) - names = {"abs": abs, - "log": math.log} - op = names[e.name] - for idx in numpy.ndindex(result.tshape): - result[idx] = op(*(o[o.filter(idx, result.fids)] for o in ops)) - return result - - @_evaluate.register(gem.MaxValue) @_evaluate.register(gem.MinValue) def _evaluate_minmaxvalue(e, self): From d7d701a2248a7d0b8510f3542a2d7aea0ae441b6 Mon Sep 17 00:00:00 2001 From: David Ham Date: Sat, 24 Feb 2018 13:56:12 +0000 Subject: [PATCH 485/749] Trivial OO errors --- gem/gem.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index e63095cf6..cc5ce7e19 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -159,9 +159,9 @@ def __new__(cls, array): def __init__(self, array): try: - array = asarray(array, dtype=float) + self.array = asarray(array, dtype=float) except TypeError: - array = asarray(array, dtype=complex) + self.array = asarray(array, dtype=complex) def is_equal(self, other): if type(self) != type(other): @@ -176,9 +176,9 @@ def get_hash(self): @property def value(self): try: - return float(array) + return float(self.array) except TypeError: - return complex(array) + return complex(self.array) @property def shape(self): From 56dd1c376ac74b28b9e5bde6444ec2027c0fe863 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Fri, 16 Mar 2018 15:07:56 -0500 Subject: [PATCH 486/749] moved PhysicalGeometryABC into its own file. --- finat/hermite.py | 10 +--------- finat/physical_geometry.py | 9 +++++++++ 2 files changed, 10 insertions(+), 9 deletions(-) create mode 100644 finat/physical_geometry.py diff --git a/finat/hermite.py b/finat/hermite.py index 43c6cab12..94727182e 100644 --- a/finat/hermite.py +++ b/finat/hermite.py @@ -1,7 +1,5 @@ from __future__ import absolute_import, print_function, division -from six import iteritems, with_metaclass - -from abc import ABCMeta, abstractmethod +from six import iteritems import numpy @@ -12,12 +10,6 @@ from finat.fiat_elements import ScalarFiatElement -class PhysicalGeometry(with_metaclass(ABCMeta)): - @abstractmethod - def jacobian_at(self, point): - pass - - class CubicHermite(ScalarFiatElement): def __init__(self, cell): super(CubicHermite, self).__init__(FIAT.CubicHermite(cell)) diff --git a/finat/physical_geometry.py b/finat/physical_geometry.py new file mode 100644 index 000000000..b53471a83 --- /dev/null +++ b/finat/physical_geometry.py @@ -0,0 +1,9 @@ +from six import with_metaclass + +from abc import ABCMeta, abstractmethod + + +class PhysicalGeometry(with_metaclass(ABCMeta)): + @abstractmethod + def jacobian_at(self, point): + pass From 8f4b1fe04c248410d7afd135052c9bafe5e329ef Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Fri, 16 Mar 2018 16:55:43 -0500 Subject: [PATCH 487/749] Update cubic hermite to work in n dimensions. To do: higher degree --- finat/hermite.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/finat/hermite.py b/finat/hermite.py index 94727182e..638dfa976 100644 --- a/finat/hermite.py +++ b/finat/hermite.py @@ -16,23 +16,29 @@ def __init__(self, cell): def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): assert coordinate_mapping is not None - JA, JB, JC = [coordinate_mapping.jacobian_at(vertex) - for vertex in self.cell.get_vertices()] + Js = [coordinate_mapping.jacobian_at(vertex) + for vertex in self.cell.get_vertices()] + + d = self.cell.get_dimension() + numbf = self.space_dimension() def n(J): - assert J.shape == (2, 2) + assert J.shape == (d, d) return numpy.array( - [[gem.Indexed(J, (0, 0)), - gem.Indexed(J, (0, 1))], - [gem.Indexed(J, (1, 0)), - gem.Indexed(J, (1, 1))]] - ) - M = numpy.eye(10, dtype=object) + [[gem.Indexed(J, (i, j)) for j in range(d)] + for i in range(d)]) + + M = numpy.eye(numbf, dtype=object) + for multiindex in numpy.ndindex(M.shape): M[multiindex] = gem.Literal(M[multiindex]) - M[1:3, 1:3] = n(JA) - M[4:6, 4:6] = n(JB) - M[7:9, 7:9] = n(JC) + + cur = 0 + for i in range(d+1): + cur += 1 # skip the vertex + M[cur:cur+d, cur:cur+d] = n(Js[i]) + cur += d + M = gem.ListTensor(M) def matvec(table): From f7d5f27c0307027ec25e4314e3234551e09ddc58 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Tue, 29 Aug 2017 11:40:06 +0100 Subject: [PATCH 488/749] generalise QuadrilateralElement -> FlattenedDimensions --- finat/__init__.py | 2 +- finat/cube.py | 83 +++++++++++++++++++++++++++++++++ finat/quadrilateral.py | 101 ----------------------------------------- 3 files changed, 84 insertions(+), 102 deletions(-) create mode 100644 finat/cube.py delete mode 100644 finat/quadrilateral.py diff --git a/finat/__init__.py b/finat/__init__.py index f0c01fa68..cef0fbaca 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -7,7 +7,7 @@ from .spectral import GaussLobattoLegendre, GaussLegendre # noqa: F401 from .tensorfiniteelement import TensorFiniteElement # noqa: F401 from .tensor_product import TensorProductElement # noqa: F401 -from .quadrilateral import QuadrilateralElement # noqa: F401 +from .cube import FlattenedDimensions # noqa: F401 from .discontinuous import DiscontinuousElement # noqa: F401 from .enriched import EnrichedElement # noqa: F401 from .hdivcurl import HCurlElement, HDivElement # noqa: F401 diff --git a/finat/cube.py b/finat/cube.py new file mode 100644 index 000000000..c1284b27f --- /dev/null +++ b/finat/cube.py @@ -0,0 +1,83 @@ +from __future__ import absolute_import, print_function, division + +from FIAT.reference_element import UFCHexahedron, UFCQuadrilateral +from FIAT.reference_element import compute_unflattening_map, flatten_entities + +from gem.utils import cached_property + +from finat.finiteelementbase import FiniteElementBase + + +class FlattenedDimensions(FiniteElementBase): + """Class for elements on quadrilaterals and hexahedra. Wraps a tensor + product element on a tensor product cell, and flattens its entity + dimensions.""" + + def __init__(self, element): + super(FlattenedDimensions, self).__init__() + self.product = element + self._unflatten = compute_unflattening_map(element.cell.get_topology()) + + @cached_property + def cell(self): + dim = self.product.cell.get_spatial_dimension() + if dim == 2: + return UFCQuadrilateral() + elif dim == 3: + return UFCHexahedron() + else: + raise NotImplementedError("Cannot guess cell for spatial dimension %s" % dim) + + @property + def degree(self): + unique_degree, = set(self.product.degree) + return unique_degree + + @property + def formdegree(self): + return self.product.formdegree + + @cached_property + def _entity_dofs(self): + return flatten_entities(self.product.entity_dofs()) + + @cached_property + def _entity_support_dofs(self): + return flatten_entities(self.product.entity_support_dofs()) + + def entity_dofs(self): + return self._entity_dofs + + def space_dimension(self): + return self.product.space_dimension() + + def basis_evaluation(self, order, ps, entity=None): + """Return code for evaluating the element at known points on the + reference element. + + :param order: return derivatives up to this order. + :param ps: the point set object. + :param entity: the cell entity on which to tabulate. + """ + if entity is None: + entity = (self.cell.get_spatial_dimension(), 0) + + return self.product.basis_evaluation(order, ps, self._unflatten[entity]) + + def point_evaluation(self, order, point, entity=None): + if entity is None: + entity = (self.cell.get_spatial_dimension(), 0) + + return self.product.point_evaluation(order, point, self._unflatten[entity]) + + @property + def index_shape(self): + return self.product.index_shape + + @property + def value_shape(self): + return self.product.value_shape + + @property + def mapping(self): + return self.product.mapping diff --git a/finat/quadrilateral.py b/finat/quadrilateral.py deleted file mode 100644 index f0db0a850..000000000 --- a/finat/quadrilateral.py +++ /dev/null @@ -1,101 +0,0 @@ -from FIAT.reference_element import UFCQuadrilateral - -from gem.utils import cached_property - -from finat.finiteelementbase import FiniteElementBase - - -class QuadrilateralElement(FiniteElementBase): - """Class for elements on quadrilaterals. Wraps a tensor product - element on an interval x interval cell, but appears on a - quadrilateral cell to the outside world.""" - - def __init__(self, element): - super(QuadrilateralElement, self).__init__() - self.product = element - - @cached_property - def cell(self): - return UFCQuadrilateral() - - @property - def degree(self): - unique_degree, = set(self.product.degree) - return unique_degree - - @property - def formdegree(self): - return self.product.formdegree - - @cached_property - def _entity_dofs(self): - return flatten(self.product.entity_dofs()) - - @cached_property - def _entity_support_dofs(self): - return flatten(self.product.entity_support_dofs()) - - def entity_dofs(self): - return self._entity_dofs - - def space_dimension(self): - return self.product.space_dimension() - - def basis_evaluation(self, order, ps, entity=None): - """Return code for evaluating the element at known points on the - reference element. - - :param order: return derivatives up to this order. - :param ps: the point set object. - :param entity: the cell entity on which to tabulate. - """ - return self.product.basis_evaluation(order, ps, productise(entity)) - - def point_evaluation(self, order, point, entity=None): - return self.product.point_evaluation(order, point, productise(entity)) - - @property - def index_shape(self): - return self.product.index_shape - - @property - def value_shape(self): - return self.product.value_shape - - @property - def mapping(self): - return self.product.mapping - - -def flatten(dofs): - flat_dofs = {} - flat_dofs[0] = dofs[(0, 0)] - flat_dofs[1] = dict(enumerate( - [v for k, v in sorted(dofs[(0, 1)].items())] + - [v for k, v in sorted(dofs[(1, 0)].items())] - )) - flat_dofs[2] = dofs[(1, 1)] - return flat_dofs - - -def productise(entity): - if entity is None: - entity = (2, 0) - - # Entity is provided in flattened form (d, i) - # We factor the entity and construct an appropriate - # entity id for a TensorProductCell: ((d1, d2), i) - entity_dim, entity_id = entity - if entity_dim == 2: - assert entity_id == 0 - return ((1, 1), 0) - elif entity_dim == 1: - facets = [((0, 1), 0), - ((0, 1), 1), - ((1, 0), 0), - ((1, 0), 1)] - return facets[entity_id] - elif entity_dim == 0: - return ((0, 0), entity_id) - else: - raise ValueError("Illegal entity dimension %s" % entity_dim) From 2c768daf037be2a2de00e63a1021172a9be49c68 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 26 Feb 2016 10:59:03 +0000 Subject: [PATCH 489/749] implement post-order traversal Inspired by UFL unique_post_traversal. --- gem/node.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/gem/node.py b/gem/node.py index 8345a0d62..1af62a635 100644 --- a/gem/node.py +++ b/gem/node.py @@ -99,7 +99,7 @@ def get_hash(self): return hash((type(self),) + self._cons_args(self.children)) -def traversal(expression_dags): +def pre_traversal(expression_dags): """Pre-order traversal of the nodes of expression DAGs.""" seen = set() lifo = [] @@ -120,6 +120,35 @@ def traversal(expression_dags): lifo.append(child) +def post_traversal(expression_dags): + """Post-order traversal of the nodes of expression DAGs.""" + seen = set() + lifo = [] + # Some roots might be same, but they must be visited only once. + # Keep the original ordering of roots, for deterministic code + # generation. + for root in expression_dags: + if root not in seen: + seen.add(root) + lifo.append((root, list(root.children))) + + while lifo: + node, deps = lifo[-1] + for i, dep in enumerate(deps): + if dep is not None and dep not in seen: + lifo.append((dep, list(dep.children))) + deps[i] = None + break + else: + yield node + seen.add(node) + lifo.pop() + + +# Default to the more efficient pre-order traversal +traversal = pre_traversal + + def collect_refcount(expression_dags): """Collects reference counts for a multi-root expression DAG.""" result = collections.Counter(expression_dags) From 584671034fc896499336f8b9b1095d8145a65dcb Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 26 Feb 2016 15:17:12 +0000 Subject: [PATCH 490/749] WIP: pretty-printing GEM --- gem/pprint.py | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 gem/pprint.py diff --git a/gem/pprint.py b/gem/pprint.py new file mode 100644 index 000000000..cc2a75506 --- /dev/null +++ b/gem/pprint.py @@ -0,0 +1,114 @@ +"""Pretty-printing GEM expressions.""" + +from __future__ import absolute_import +from __future__ import print_function + +import collections +import itertools + +from singledispatch import singledispatch + +from gem import gem +from gem.node import collect_refcount, post_traversal + + +def pprint(expression_dags): + refcount = collect_refcount(expression_dags) + + index_counter = itertools.count() + index_names = collections.defaultdict(lambda: "i_%d" % (1 + next(index_counter))) + + def inlinable(node): + return not (not isinstance(node, gem.Variable) and (node.shape or (refcount[node] > 1 and not isinstance(node, (gem.Literal, gem.Zero, gem.Indexed))))) + + i = 1 + stringified = {} + for node in post_traversal(expression_dags): + string = stringify(node, stringified, index_names) + if inlinable(node): + stringified[node] = string + else: + name = "$%d" % i + print(make_decl(node, name, index_names) + ' = ' + string) + stringified[node] = make_ref(node, name, index_names) + i += 1 + + for i, root in enumerate(expression_dags): + print(make_decl(root, "#%d" % (i + 1), index_names) + ' = ' + stringified[root]) + + +def make_decl(node, name, index_names): + if node.shape: + name += '[' + ','.join(map(repr, node.shape)) + ']' + if node.free_indices: + name += '{' + ','.join(index_names[i] for i in node.free_indices) + '}' + return name + + +def make_ref(node, name, index_names): + if node.free_indices: + name += '{' + ','.join(index_names[i] for i in node.free_indices) + '}' + return name + + +@singledispatch +def stringify(node, stringified, index_names): + raise AssertionError("GEM node expected") + + +@stringify.register(gem.Node) +def stringify_node(node, stringified, index_names): + front_args = [repr(getattr(node, name)) for name in node.__front__] + back_args = [repr(getattr(node, name)) for name in node.__back__] + children = [stringified[child] for child in node.children] + return "%s(%s)" % (type(node).__name__, ", ".join(front_args + children + back_args)) + + +@stringify.register(gem.Zero) +def stringify_zero(node, stringified, index_names): + assert not node.shape + return repr(0) + + +@stringify.register(gem.Literal) +def stringify_literal(node, stringified, index_names): + if node.shape: + return repr(node.array.tolist()) + else: + return "%g" % node.value + + +@stringify.register(gem.Variable) +def stringify_variable(node, stringified, index_names): + return node.name + + +@stringify.register(gem.ListTensor) +def stringify_listtensor(node, stringified, index_names): + def recurse_rank(array): + if len(array.shape) > 1: + return '[' + ', '.join(map(recurse_rank, array)) + ']' + else: + return '[' + ', '.join(stringified[item] for item in array) + ']' + + return recurse_rank(node.array) + + +@stringify.register(gem.Indexed) +def stringify_indexed(node, stringified, index_names): + child, = node.children + result = stringified[child] + if child.free_indices: + result += '{' + ','.join(index_names[i] for i in child.free_indices) + '}' + result += '[' + ','.join(index_names[i] for i in node.multiindex) + ']' + return result + + +@stringify.register(gem.IndexSum) +def stringify_indexsum(node, stringified, index_names): + return u'\u03A3_{' + index_names[node.index] + '}(' + stringified[node.children[0]] + ')' + + +@stringify.register(gem.ComponentTensor) +def stringify_componenttensor(node, stringified, index_names): + return stringified[node.children[0]] + '|' + ','.join(index_names[i] for i in node.multiindex) From 300cccac33b1ec16cb2670de1b2939a79b7f1111 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 15 Dec 2016 14:44:23 +0000 Subject: [PATCH 491/749] proper pretty-printing context --- gem/pprint.py | 199 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 145 insertions(+), 54 deletions(-) diff --git a/gem/pprint.py b/gem/pprint.py index cc2a75506..ed5a4b22e 100644 --- a/gem/pprint.py +++ b/gem/pprint.py @@ -1,9 +1,8 @@ """Pretty-printing GEM expressions.""" -from __future__ import absolute_import -from __future__ import print_function +from __future__ import absolute_import, print_function, division -import collections +from collections import defaultdict import itertools from singledispatch import singledispatch @@ -12,103 +11,195 @@ from gem.node import collect_refcount, post_traversal -def pprint(expression_dags): - refcount = collect_refcount(expression_dags) +class Context(object): + def __init__(self): + expr_counter = itertools.count(1) + self.expr_name = defaultdict(lambda: "${}".format(next(expr_counter))) + index_counter = itertools.count(1) + self.index_name = defaultdict(lambda: "i_{}".format(next(index_counter))) + self.index_names = set() + + def force_expression(self, expr): + assert isinstance(expr, gem.Node) + return self.expr_name[expr] + + def expression(self, expr): + assert isinstance(expr, gem.Node) + return self.expr_name.get(expr) + + def index(self, index): + assert isinstance(index, gem.Index) + if index.name is None: + name = self.index_name[index] + elif index.name not in self.index_names: + name = index.name + self.index_name[index] = name + else: + name_ = index.name + for i in itertools.count(1): + name = "{}~{}".format(name_, i) + if name not in self.index_names: + break + self.index_names.add(name) + return name + - index_counter = itertools.count() - index_names = collections.defaultdict(lambda: "i_%d" % (1 + next(index_counter))) +global_context = Context() + + +def pprint(expression_dags, context=global_context): + refcount = collect_refcount(expression_dags) - def inlinable(node): - return not (not isinstance(node, gem.Variable) and (node.shape or (refcount[node] > 1 and not isinstance(node, (gem.Literal, gem.Zero, gem.Indexed))))) + def force(node): + if isinstance(node, gem.Variable): + return False + if node.shape: + return True + if isinstance(node, (gem.Constant, gem.Indexed, gem.FlexiblyIndexed)): + return False + return refcount[node] > 1 - i = 1 - stringified = {} for node in post_traversal(expression_dags): - string = stringify(node, stringified, index_names) - if inlinable(node): - stringified[node] = string - else: - name = "$%d" % i - print(make_decl(node, name, index_names) + ' = ' + string) - stringified[node] = make_ref(node, name, index_names) - i += 1 + if force(node): + context.force_expression(node) + + name = context.expression(node) + if name is not None: + print(make_decl(node, name, context), '=', to_str(node, context, top=True)) for i, root in enumerate(expression_dags): - print(make_decl(root, "#%d" % (i + 1), index_names) + ' = ' + stringified[root]) + print(make_decl(root, "#%d" % (i + 1), context), '=', to_str(root, context)) -def make_decl(node, name, index_names): +def make_decl(node, name, ctx): + result = name if node.shape: - name += '[' + ','.join(map(repr, node.shape)) + ']' + result += '[' + ','.join(map(repr, node.shape)) + ']' if node.free_indices: - name += '{' + ','.join(index_names[i] for i in node.free_indices) + '}' - return name + result += '{' + ','.join(map(ctx.index, node.free_indices)) + '}' + return result -def make_ref(node, name, index_names): - if node.free_indices: - name += '{' + ','.join(index_names[i] for i in node.free_indices) + '}' - return name +def to_str(expr, ctx, top=False): + if not top and ctx.expression(expr): + result = ctx.expression(expr) + if expr.free_indices: + result += '{' + ','.join(map(ctx.index, expr.free_indices)) + '}' + return result + else: + return _to_str(expr, ctx) @singledispatch -def stringify(node, stringified, index_names): +def _to_str(node, ctx): raise AssertionError("GEM node expected") -@stringify.register(gem.Node) -def stringify_node(node, stringified, index_names): +@_to_str.register(gem.Node) +def _to_str_node(node, ctx): front_args = [repr(getattr(node, name)) for name in node.__front__] back_args = [repr(getattr(node, name)) for name in node.__back__] - children = [stringified[child] for child in node.children] + children = [to_str(child, ctx) for child in node.children] return "%s(%s)" % (type(node).__name__, ", ".join(front_args + children + back_args)) -@stringify.register(gem.Zero) -def stringify_zero(node, stringified, index_names): +@_to_str.register(gem.Zero) +def _to_str_zero(node, ctx): assert not node.shape - return repr(0) + return "%g" % node.value -@stringify.register(gem.Literal) -def stringify_literal(node, stringified, index_names): +@_to_str.register(gem.Literal) +def _to_str_literal(node, ctx): if node.shape: return repr(node.array.tolist()) else: return "%g" % node.value -@stringify.register(gem.Variable) -def stringify_variable(node, stringified, index_names): +@_to_str.register(gem.Variable) +def _to_str_variable(node, ctx): return node.name -@stringify.register(gem.ListTensor) -def stringify_listtensor(node, stringified, index_names): +@_to_str.register(gem.ListTensor) +def _to_str_listtensor(node, ctx): def recurse_rank(array): if len(array.shape) > 1: return '[' + ', '.join(map(recurse_rank, array)) + ']' else: - return '[' + ', '.join(stringified[item] for item in array) + ']' + return '[' + ', '.join(to_str(item, ctx) for item in array) + ']' return recurse_rank(node.array) -@stringify.register(gem.Indexed) -def stringify_indexed(node, stringified, index_names): +@_to_str.register(gem.Indexed) +def _to_str_indexed(node, ctx): + child, = node.children + result = to_str(child, ctx) + # if child.free_indices: + # result += '{' + ','.join(index_names[i] for i in child.free_indices) + '}' + dimensions = [] + for index in node.multiindex: + if isinstance(index, gem.Index): + dimensions.append(ctx.index(index)) + elif isinstance(index, int): + dimensions.append(str(index)) + else: + assert False + result += '[' + ','.join(dimensions) + ']' + return result + + +@_to_str.register(gem.FlexiblyIndexed) +def _to_str_flexiblyindexed(node, ctx): child, = node.children - result = stringified[child] - if child.free_indices: - result += '{' + ','.join(index_names[i] for i in child.free_indices) + '}' - result += '[' + ','.join(index_names[i] for i in node.multiindex) + ']' + result = to_str(child, ctx) + dimensions = [] + for offset, idxs in node.dim2idxs: + parts = [] + if offset: + parts.append(str(offset)) + for index, stride in idxs: + index_name = ctx.index(index) + assert stride + if stride == 1: + parts.append(index_name) + else: + parts.append(index_name + "*" + str(stride)) + if parts: + dimensions.append(' + '.join(parts)) + else: + dimensions.append('0') + if dimensions: + result += '[' + ','.join(dimensions) + ']' return result -@stringify.register(gem.IndexSum) -def stringify_indexsum(node, stringified, index_names): - return u'\u03A3_{' + index_names[node.index] + '}(' + stringified[node.children[0]] + ')' +@_to_str.register(gem.IndexSum) +def _to_str_indexsum(node, ctx): + index, = node.multiindex + return u'\u03A3_{' + ctx.index(index) + '}(' + to_str(node.children[0], ctx) + ')' + +@_to_str.register(gem.ComponentTensor) +def _to_str_componenttensor(node, ctx): + return to_str(node.children[0], ctx) + '|' + ','.join(ctx.index(i) for i in node.multiindex) -@stringify.register(gem.ComponentTensor) -def stringify_componenttensor(node, stringified, index_names): - return stringified[node.children[0]] + '|' + ','.join(index_names[i] for i in node.multiindex) + +@_to_str.register(gem.Sum) +def _to_str_sum(node, ctx): + children = [to_str(child, ctx) for child in node.children] + return "(" + " + ".join(children) + ")" + + +@_to_str.register(gem.Product) +def _to_str_product(node, ctx): + children = [to_str(child, ctx) for child in node.children] + return "(" + "*".join(children) + ")" + + +@_to_str.register(gem.MathFunction) +def _to_str_mathfunction(node, ctx): + child, = node.children + return node.name + "(" + to_str(child, ctx) + ")" From cbbbe61723752e7ff3b571e35444f7a45ce9edc7 Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Thu, 15 Dec 2016 15:14:55 +0000 Subject: [PATCH 492/749] better parenthesis Precedence levels hardcoded, ugh. --- gem/pprint.py | 55 +++++++++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/gem/pprint.py b/gem/pprint.py index ed5a4b22e..ec4f5b6c2 100644 --- a/gem/pprint.py +++ b/gem/pprint.py @@ -68,7 +68,8 @@ def force(node): print(make_decl(node, name, context), '=', to_str(node, context, top=True)) for i, root in enumerate(expression_dags): - print(make_decl(root, "#%d" % (i + 1), context), '=', to_str(root, context)) + name = "#%d" % (i + 1) + print(make_decl(root, name, context), '=', to_str(root, context)) def make_decl(node, name, ctx): @@ -80,23 +81,23 @@ def make_decl(node, name, ctx): return result -def to_str(expr, ctx, top=False): +def to_str(expr, ctx, prec=None, top=False): if not top and ctx.expression(expr): result = ctx.expression(expr) if expr.free_indices: result += '{' + ','.join(map(ctx.index, expr.free_indices)) + '}' return result else: - return _to_str(expr, ctx) + return _to_str(expr, ctx, prec=prec) @singledispatch -def _to_str(node, ctx): +def _to_str(node, ctx, prec): raise AssertionError("GEM node expected") @_to_str.register(gem.Node) -def _to_str_node(node, ctx): +def _to_str_node(node, ctx, prec): front_args = [repr(getattr(node, name)) for name in node.__front__] back_args = [repr(getattr(node, name)) for name in node.__back__] children = [to_str(child, ctx) for child in node.children] @@ -104,13 +105,13 @@ def _to_str_node(node, ctx): @_to_str.register(gem.Zero) -def _to_str_zero(node, ctx): +def _to_str_zero(node, ctx, prec): assert not node.shape return "%g" % node.value @_to_str.register(gem.Literal) -def _to_str_literal(node, ctx): +def _to_str_literal(node, ctx, prec): if node.shape: return repr(node.array.tolist()) else: @@ -118,12 +119,12 @@ def _to_str_literal(node, ctx): @_to_str.register(gem.Variable) -def _to_str_variable(node, ctx): +def _to_str_variable(node, ctx, prec): return node.name @_to_str.register(gem.ListTensor) -def _to_str_listtensor(node, ctx): +def _to_str_listtensor(node, ctx, prec): def recurse_rank(array): if len(array.shape) > 1: return '[' + ', '.join(map(recurse_rank, array)) + ']' @@ -134,11 +135,9 @@ def recurse_rank(array): @_to_str.register(gem.Indexed) -def _to_str_indexed(node, ctx): +def _to_str_indexed(node, ctx, prec): child, = node.children result = to_str(child, ctx) - # if child.free_indices: - # result += '{' + ','.join(index_names[i] for i in child.free_indices) + '}' dimensions = [] for index in node.multiindex: if isinstance(index, gem.Index): @@ -152,7 +151,7 @@ def _to_str_indexed(node, ctx): @_to_str.register(gem.FlexiblyIndexed) -def _to_str_flexiblyindexed(node, ctx): +def _to_str_flexiblyindexed(node, ctx, prec): child, = node.children result = to_str(child, ctx) dimensions = [] @@ -177,29 +176,37 @@ def _to_str_flexiblyindexed(node, ctx): @_to_str.register(gem.IndexSum) -def _to_str_indexsum(node, ctx): - index, = node.multiindex - return u'\u03A3_{' + ctx.index(index) + '}(' + to_str(node.children[0], ctx) + ')' +def _to_str_indexsum(node, ctx, prec): + result = 'Sum_{' + ','.join(map(ctx.index, node.multiindex)) + '} ' + to_str(node.children[0], ctx, prec=2) + if prec is not None and prec > 2: + result = '({})'.format(result) + return result @_to_str.register(gem.ComponentTensor) -def _to_str_componenttensor(node, ctx): +def _to_str_componenttensor(node, ctx, prec): return to_str(node.children[0], ctx) + '|' + ','.join(ctx.index(i) for i in node.multiindex) @_to_str.register(gem.Sum) -def _to_str_sum(node, ctx): - children = [to_str(child, ctx) for child in node.children] - return "(" + " + ".join(children) + ")" +def _to_str_sum(node, ctx, prec): + children = [to_str(child, ctx, prec=1) for child in node.children] + result = " + ".join(children) + if prec is not None and prec > 1: + result = "({})".format(result) + return result @_to_str.register(gem.Product) -def _to_str_product(node, ctx): - children = [to_str(child, ctx) for child in node.children] - return "(" + "*".join(children) + ")" +def _to_str_product(node, ctx, prec): + children = [to_str(child, ctx, prec=3) for child in node.children] + result = "*".join(children) + if prec is not None and prec > 3: + result = "({})".format(result) + return result @_to_str.register(gem.MathFunction) -def _to_str_mathfunction(node, ctx): +def _to_str_mathfunction(node, ctx, prec): child, = node.children return node.name + "(" + to_str(child, ctx) + ")" From 9496962720128859dd5c776a8205e4f6288cccbd Mon Sep 17 00:00:00 2001 From: Miklos Homolya Date: Fri, 16 Dec 2016 16:19:14 +0000 Subject: [PATCH 493/749] implement VariableIndex --- gem/pprint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gem/pprint.py b/gem/pprint.py index ec4f5b6c2..da1599d40 100644 --- a/gem/pprint.py +++ b/gem/pprint.py @@ -145,7 +145,7 @@ def _to_str_indexed(node, ctx, prec): elif isinstance(index, int): dimensions.append(str(index)) else: - assert False + dimensions.append(to_str(index.expression, ctx)) result += '[' + ','.join(dimensions) + ']' return result From 089eeec327f22548c1100f42cf3502a488dbc483 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Thu, 3 May 2018 15:08:57 +0100 Subject: [PATCH 494/749] Remove future imports --- gem/pprint.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/gem/pprint.py b/gem/pprint.py index da1599d40..9c2451235 100644 --- a/gem/pprint.py +++ b/gem/pprint.py @@ -1,11 +1,8 @@ """Pretty-printing GEM expressions.""" - -from __future__ import absolute_import, print_function, division - from collections import defaultdict import itertools -from singledispatch import singledispatch +from functools import singledispatch from gem import gem from gem.node import collect_refcount, post_traversal From e465ec41b749f0cffe5f422d4884c675b7d2dca1 Mon Sep 17 00:00:00 2001 From: Patrick Farrell Date: Fri, 4 May 2018 12:21:06 +0100 Subject: [PATCH 495/749] Add support for FIAT.FacetBubble --- finat/__init__.py | 1 + finat/fiat_elements.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/finat/__init__.py b/finat/__init__.py index f0c01fa68..cf7f585fc 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -3,6 +3,7 @@ from .fiat_elements import BrezziDouglasMarini, BrezziDouglasFortinMarini # noqa: F401 from .fiat_elements import Nedelec, NedelecSecondKind, RaviartThomas # noqa: F401 from .fiat_elements import HellanHerrmannJohnson, Regge # noqa: F401 +from .fiat_elements import FacetBubble # noqa: F401 from .trace import HDivTrace # noqa: F401 from .spectral import GaussLobattoLegendre, GaussLegendre # noqa: F401 from .tensorfiniteelement import TensorFiniteElement # noqa: F401 diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index f74eedf7b..a11db5930 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -264,6 +264,11 @@ def __init__(self, cell, degree): super(Bubble, self).__init__(FIAT.Bubble(cell, degree)) +class FacetBubble(ScalarFiatElement): + def __init__(self, cell, degree): + super(FacetBubble, self).__init__(FIAT.FacetBubble(cell, degree)) + + class CrouzeixRaviart(ScalarFiatElement): def __init__(self, cell, degree): super(CrouzeixRaviart, self).__init__(FIAT.CrouzeixRaviart(cell, degree)) From 6cc3ef958c8e2b22d4b0662f8b7a90b95e16e64f Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Fri, 8 Jun 2018 10:30:17 -0500 Subject: [PATCH 496/749] add broken morley --- finat/morley.py | 63 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 finat/morley.py diff --git a/finat/morley.py b/finat/morley.py new file mode 100644 index 000000000..f7a09b0f2 --- /dev/null +++ b/finat/morley.py @@ -0,0 +1,63 @@ +from __future__ import absolute_import, print_function, division +from six import iteritems + +import numpy + +import FIAT + +import gem + +from finat.fiat_elements import ScalarFiatElement + + +class Morley(ScalarFiatElement): + def __init__(self, cell): + super(Morley, self).__init__(FIAT.Morley(cell)) + + def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): + assert coordinate_mapping is not None + + # Jacobians at cell midpoints + mps = [self.cell.make_points(1, i, 2)[-1] for i in range(3)] + Js = [coordinate_mapping.jacobian_at(mp) for mp in mps] + + # how to get expr for length of local edge i? + elens = [coordinate_mapping.edge_length(i) for i in range(3)] + relens = [coordinate_mapping.ref_edge_tangent(i) for i in range(3)] + + d = 2 + numbf = 6 + + M = numpy.eye(numbf, dtype=object) + + def n(J): + assert J.shape == (d, d) + return numpy.array( + [[gem.Indexed(J, (i, j)) for j in range(d)] + for i in range(d)]) + + for multiindex in numpy.ndindex(M.shape): + M[multiindex] = gem.Literal(M[multiindex]) + + B111 + + # Now put the values into M! + + + M = gem.ListTensor(M) + + def matvec(table): + i = gem.Index() + j = gem.Index() + return gem.ComponentTensor( + gem.IndexSum(gem.Product(gem.Indexed(M, (i, j)), + gem.Indexed(table, (j,))), + (j,)), + (i,)) + + result = super(Morley, self).basis_evaluation(order, ps, entity=entity) + return {alpha: matvec(table) + for alpha, table in iteritems(result)} + + def point_evaluation(self, order, refcoords, entity=None): + raise NotImplementedError # TODO: think about it later! From 709f9c90bb33d0321e25d75a55ead6d48591d9bf Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Fri, 8 Jun 2018 18:23:38 +0100 Subject: [PATCH 497/749] Perhaps Morley is right? --- finat/morley.py | 82 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 56 insertions(+), 26 deletions(-) diff --git a/finat/morley.py b/finat/morley.py index f7a09b0f2..48abda16e 100644 --- a/finat/morley.py +++ b/finat/morley.py @@ -17,32 +17,62 @@ def __init__(self, cell): def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): assert coordinate_mapping is not None - # Jacobians at cell midpoints - mps = [self.cell.make_points(1, i, 2)[-1] for i in range(3)] - Js = [coordinate_mapping.jacobian_at(mp) for mp in mps] - - # how to get expr for length of local edge i? - elens = [coordinate_mapping.edge_length(i) for i in range(3)] - relens = [coordinate_mapping.ref_edge_tangent(i) for i in range(3)] - - d = 2 - numbf = 6 - - M = numpy.eye(numbf, dtype=object) - - def n(J): - assert J.shape == (d, d) - return numpy.array( - [[gem.Indexed(J, (i, j)) for j in range(d)] - for i in range(d)]) - - for multiindex in numpy.ndindex(M.shape): - M[multiindex] = gem.Literal(M[multiindex]) - - B111 - - # Now put the values into M! - + # Jacobians at edge midpoints + J = coordinate_mapping.jacobian_at([1/3, 1/3]) + + rns = [coordinate_mapping.reference_normal(i) for i in range(3)] + pns = [coordinate_mapping.physical_normal(i) for i in range(3)] + + rts = [coordinate_mapping.reference_tangent(i) for i in range(3)] + pts = [coordinate_mapping.physical_tangent(i) for i in range(3)] + + pel = coordinate_mapping.physical_edge_lengths() + + # B11 = rns[i, 0]*(pns[i, 0]*J[0,0] + pts[i, 0]*J[1, 0]) + rts[i, 0]*(pns[i, 0]*J[0, 1] + pts[i, 0]*J[1,1]) + + # B12 = rns[i, 0]*(pns[i, 1]*J[0,0] + pts[i, 1]*J[1, 0]) + rts[i, 1]*(pns[i, 1]*J[0, 1] + pts[i, 1]*J[1,1]) + + B11 = [gem.Sum(gem.Product(gem.Indexed(rns[i], (0, )), + gem.Sum(gem.Product(gem.Indexed(pns[i], (0, )), + gem.Indexed(J, (0, 0))), + gem.Product(gem.Indexed(pts[i], (0, )), + gem.Indexed(J, (1, 0))))), + gem.Product(gem.Indexed(rts[i], (0, )), + gem.Sum(gem.Product(gem.Indexed(pns[i], (0, )), + gem.Indexed(J, (0, 1))), + gem.Product(gem.Indexed(pts[i], (0, )), + gem.Indexed(J, (1, 1)))))) + for i in range(3)] + + B12 = [gem.Sum(gem.Product(gem.Indexed(rns[i], (0, )), + gem.Sum(gem.Product(gem.Indexed(pns[i], (1, )), + gem.Indexed(J, (0, 0))), + gem.Product(gem.Indexed(pts[i], (1, )), + gem.Indexed(J, (1, 0))))), + gem.Product(gem.Indexed(rts[i], (0, )), + gem.Sum(gem.Product(gem.Indexed(pns[i], (1, )), + gem.Indexed(J, (0, 1))), + gem.Product(gem.Indexed(pts[i], (1, )), + gem.Indexed(J, (1, 1)))))) + for i in range(3)] + + V = numpy.eye(6, dtype=object) + for multiindex in numpy.ndindex(V.shape): + V[multiindex] = gem.Literal(V[multiindex]) + + for i in range(3): + V[i + 3, i + 3] = B11[i] + + V[3, 1] = gem.Division(gem.Product(gem.Literal(-1), B12[0]), gem.Indexed(pel, (0, ))) + V[3, 2] = gem.Division(B12[0], gem.Indexed(pel, (0, ))) + + V[4, 0] = gem.Division(gem.Product(gem.Literal(-1), B12[1]), gem.Indexed(pel, (1, ))) + V[4, 2] = gem.Division(B12[1], gem.Indexed(pel, (1, ))) + + V[5, 0] = gem.Division(gem.Product(gem.Literal(-1), B12[2]), gem.Indexed(pel, (2, ))) + V[5, 1] = gem.Division(B12[2], gem.Indexed(pel, (2, ))) + + M = V.T M = gem.ListTensor(M) From 1b52a4763d413efd37167fb6178da486325eaca4 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Mon, 18 Jun 2018 12:45:33 -0500 Subject: [PATCH 498/749] Fixes for Morley element --- finat/morley.py | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/finat/morley.py b/finat/morley.py index 48abda16e..93d7059cb 100644 --- a/finat/morley.py +++ b/finat/morley.py @@ -21,10 +21,10 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): J = coordinate_mapping.jacobian_at([1/3, 1/3]) rns = [coordinate_mapping.reference_normal(i) for i in range(3)] - pns = [coordinate_mapping.physical_normal(i) for i in range(3)] + pns = coordinate_mapping.physical_normals() rts = [coordinate_mapping.reference_tangent(i) for i in range(3)] - pts = [coordinate_mapping.physical_tangent(i) for i in range(3)] + pts = coordinate_mapping.physical_tangents() pel = coordinate_mapping.physical_edge_lengths() @@ -33,26 +33,26 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): # B12 = rns[i, 0]*(pns[i, 1]*J[0,0] + pts[i, 1]*J[1, 0]) + rts[i, 1]*(pns[i, 1]*J[0, 1] + pts[i, 1]*J[1,1]) B11 = [gem.Sum(gem.Product(gem.Indexed(rns[i], (0, )), - gem.Sum(gem.Product(gem.Indexed(pns[i], (0, )), + gem.Sum(gem.Product(gem.Indexed(pns, (i, 0)), gem.Indexed(J, (0, 0))), - gem.Product(gem.Indexed(pts[i], (0, )), + gem.Product(gem.Indexed(pns, (i, 1)), gem.Indexed(J, (1, 0))))), - gem.Product(gem.Indexed(rts[i], (0, )), - gem.Sum(gem.Product(gem.Indexed(pns[i], (0, )), + gem.Product(gem.Indexed(rns[i], (1, )), + gem.Sum(gem.Product(gem.Indexed(pns, (i, 0)), gem.Indexed(J, (0, 1))), - gem.Product(gem.Indexed(pts[i], (0, )), + gem.Product(gem.Indexed(pns, (i, 1)), gem.Indexed(J, (1, 1)))))) for i in range(3)] B12 = [gem.Sum(gem.Product(gem.Indexed(rns[i], (0, )), - gem.Sum(gem.Product(gem.Indexed(pns[i], (1, )), + gem.Sum(gem.Product(gem.Indexed(pts, (i, 0)), gem.Indexed(J, (0, 0))), - gem.Product(gem.Indexed(pts[i], (1, )), + gem.Product(gem.Indexed(pts, (i, 1)), gem.Indexed(J, (1, 0))))), - gem.Product(gem.Indexed(rts[i], (0, )), - gem.Sum(gem.Product(gem.Indexed(pns[i], (1, )), + gem.Product(gem.Indexed(rns[i], (1, )), + gem.Sum(gem.Product(gem.Indexed(pts, (i, 0)), gem.Indexed(J, (0, 1))), - gem.Product(gem.Indexed(pts[i], (1, )), + gem.Product(gem.Indexed(pts, (i, 1)), gem.Indexed(J, (1, 1)))))) for i in range(3)] @@ -63,14 +63,9 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): for i in range(3): V[i + 3, i + 3] = B11[i] - V[3, 1] = gem.Division(gem.Product(gem.Literal(-1), B12[0]), gem.Indexed(pel, (0, ))) - V[3, 2] = gem.Division(B12[0], gem.Indexed(pel, (0, ))) - - V[4, 0] = gem.Division(gem.Product(gem.Literal(-1), B12[1]), gem.Indexed(pel, (1, ))) - V[4, 2] = gem.Division(B12[1], gem.Indexed(pel, (1, ))) - - V[5, 0] = gem.Division(gem.Product(gem.Literal(-1), B12[2]), gem.Indexed(pel, (2, ))) - V[5, 1] = gem.Division(B12[2], gem.Indexed(pel, (2, ))) + for i, c in enumerate([(1, 2), (0, 2), (0, 1)]): + V[3+i, c[0]] = gem.Division(gem.Product(gem.Literal(-1), B12[i]), gem.Indexed(pel, (i, ))) + V[3+i, c[1]] = gem.Division(B12[i], gem.Indexed(pel, (i, ))) M = V.T From 26287785f6847c24fea85ecfc5e22ef95ca6c907 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Wed, 20 Jun 2018 13:24:58 -0500 Subject: [PATCH 499/749] Add Argyris, fix lint for Morley --- finat/argyris.py | 157 +++++++++++++++++++++++++++++++++++++++++++++++ finat/morley.py | 1 - 2 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 finat/argyris.py diff --git a/finat/argyris.py b/finat/argyris.py new file mode 100644 index 000000000..ac42b38e2 --- /dev/null +++ b/finat/argyris.py @@ -0,0 +1,157 @@ +from __future__ import absolute_import, print_function, division +from six import iteritems + +import numpy + +import FIAT + +import gem + +from finat.fiat_elements import ScalarFiatElement + + +class Argyris(ScalarFiatElement): + def __init__(self, cell): + super(Argyris, self).__init__(FIAT.QuinticArgyris(cell)) + + def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): + assert coordinate_mapping is not None + + # Jacobians at edge midpoints + J = coordinate_mapping.jacobian_at([1/3, 1/3]) + + rns = [coordinate_mapping.reference_normal(i) for i in range(3)] + pns = coordinate_mapping.physical_normals() + + pts = coordinate_mapping.physical_tangents() + + pel = coordinate_mapping.physical_edge_lengths() + + V = numpy.zeros((21, 21), dtype=object) + + from gem import Product, Literal, Division, Sum, Indexed, Power + + for multiindex in numpy.ndindex(V.shape): + V[multiindex] = Literal(V[multiindex]) + + for v in range(3): + s = 6*v + V[s, s] = Literal(1) + for i in range(2): + for j in range(2): + V[s+1+i, s+1+j] = Indexed(J, (j, i)) + V[s+3, s+3] = Power(Indexed(J, (0, 0)), Literal(2)) + V[s+3, s+4] = Product(Literal(2), + Product(Indexed(J, (0, 0)), + Indexed(J, (1, 0)))) + V[s+3, s+5] = Power(Indexed(J, (1, 0)), Literal(2)) + V[s+4, s+3] = Product(Indexed(J, (0, 0)), + Indexed(J, (0, 1))) + V[s+4, s+4] = Sum(Product(Indexed(J, (0, 0)), + Indexed(J, (1, 1))), + Product(Indexed(J, (1, 0)), + Indexed(J, (0, 1)))) + V[s+4, s+5] = Product(Indexed(J, (1, 0)), + Indexed(J, (1, 1))) + V[s+5, s+3] = Power(Indexed(J, (0, 1)), Literal(2)) + V[s+5, s+4] = Product(Literal(2), + Product(Indexed(J, (0, 1)), + Indexed(J, (1, 1)))) + V[s+5, s+5] = Power(Indexed(J, (1, 1)), Literal(2)) + + for e in range(3): + v0id, v1id = [i for i in range(3) if i != e] + + # nhat . J^{-T} . t + foo = Sum(Product(Indexed(rns[e], (0,)), + Sum(Product(Indexed(J, (0, 0)), + Indexed(pts, (e, 0))), + Product(Indexed(J, (1, 0)), + Indexed(pts, (e, 1))))), + Product(Indexed(rns[e], (1,)), + Sum(Product(Indexed(J, (0, 1)), + Indexed(pts, (e, 0))), + Product(Indexed(J, (1, 1)), + Indexed(pts, (e, 1)))))) + + # vertex points + V[18+e, 6*v0id] = Division( + Product(Literal(-1), Product(Literal(15), foo)), + Product(Literal(8), Indexed(pel, (e,)))) + V[18+e, 6*v1id] = Division( + Product(Literal(15), foo), + Product(Literal(8), Indexed(pel, (e,)))) + + # vertex derivatives + for i in (0, 1): + V[18+e, 6*v0id+1+i] = Division( + Product(Literal(-1), + Product(Literal(7), + Product(foo, + Indexed(pts, (e, i))))), + Literal(16)) + V[18+e, 6*v1id+1+i] = V[18+e, 6*v0id+1+i] + + # second derivatives + tau = [Power(Indexed(pts, (e, 0)), Literal(2)), + Product(Literal(2), + Product(Indexed(pts, (e, 0)), + Indexed(pts, (e, 1)))), + Power(Indexed(pts, (e, 1)), Literal(2))] + + for i in (0, 1, 2): + V[18+e, 6*v0id+3+i] = Division( + Product(Literal(-1), + Product(Indexed(pel, (e,)), + Product(foo, tau[i]))), + Literal(32)) + V[18+e, 6*v1id+3+i] = Division( + Product(Indexed(pel, (e,)), + Product(foo, tau[i])), + Literal(32)) + + V[18+e, 18+e] = Sum(Product(Indexed(rns[e], (0,)), + Sum(Product(Indexed(J, (0, 0)), + Indexed(pns, (e, 0))), + Product(Indexed(J, (1, 0)), + Indexed(pns, (e, 1))))), + Product(Indexed(rns[e], (1,)), + Sum(Product(Indexed(J, (0, 1)), + Indexed(pns, (e, 0))), + Product(Indexed(J, (1, 1)), + Indexed(pns, (e, 1)))))) + + Sum( + Product( + Indexed(rns[e], (0,)), + Sum( + Product(Indexed(J, (0, 0)), + Indexed(pns, (e, 0))), + Product(Indexed(J, (1, 0)), + Indexed(pns, (e, 1))))), + Product( + Indexed(rns[e], (1,)), + Sum( + Product(Indexed(J, (0, 1)), + Indexed(pns, (e, 0))), + Product(Indexed(J, (1, 1)), + Indexed(pns, (e, 1)))))) + + M = V.T + M = gem.ListTensor(M) + + def matvec(table): + i = gem.Index() + j = gem.Index() + return gem.ComponentTensor( + gem.IndexSum(gem.Product(gem.Indexed(M, (i, j)), + gem.Indexed(table, (j,))), + (j,)), + (i,)) + + result = super(Argyris, self).basis_evaluation(order, ps, entity=entity) + return {alpha: matvec(table) + for alpha, table in iteritems(result)} + + def point_evaluation(self, order, refcoords, entity=None): + raise NotImplementedError # TODO: think about it later! diff --git a/finat/morley.py b/finat/morley.py index 93d7059cb..8da9d788c 100644 --- a/finat/morley.py +++ b/finat/morley.py @@ -23,7 +23,6 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): rns = [coordinate_mapping.reference_normal(i) for i in range(3)] pns = coordinate_mapping.physical_normals() - rts = [coordinate_mapping.reference_tangent(i) for i in range(3)] pts = coordinate_mapping.physical_tangents() pel = coordinate_mapping.physical_edge_lengths() From 7c338295ddd177a9e85b47b6e6ed9e9e05442c57 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Thu, 21 Jun 2018 16:17:59 -0500 Subject: [PATCH 500/749] First stab at Bell. --- finat/bell.py | 153 +++++++++++++++++++++++++++++++++++++++++ finat/fiat_elements.py | 4 +- 2 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 finat/bell.py diff --git a/finat/bell.py b/finat/bell.py new file mode 100644 index 000000000..b6b31a340 --- /dev/null +++ b/finat/bell.py @@ -0,0 +1,153 @@ +from __future__ import absolute_import, print_function, division +from six import iteritems + +import numpy + +import FIAT + +import gem + +from finat.fiat_elements import ScalarFiatElement + + +class Bell(ScalarFiatElement): + def __init__(self, cell): + super(Bell, self).__init__(FIAT.Bell(cell)) + + def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): + assert coordinate_mapping is not None + + # Jacobians at edge midpoints + J = coordinate_mapping.jacobian_at([1/3, 1/3]) + + rns = [coordinate_mapping.reference_normal(i) for i in range(3)] + + pts = coordinate_mapping.physical_tangents() + + pel = coordinate_mapping.physical_edge_lengths() + + V = numpy.zeros((21, 18), dtype=object) + + from gem import Product, Literal, Division, Sum, Indexed, Power + + for multiindex in numpy.ndindex(V.shape): + V[multiindex] = Literal(V[multiindex]) + + for v in range(3): + s = 6*v + V[s, s] = Literal(1) + for i in range(2): + for j in range(2): + V[s+1+i, s+1+j] = Indexed(J, (j, i)) + V[s+3, s+3] = Power(Indexed(J, (0, 0)), Literal(2)) + V[s+3, s+4] = Product(Literal(2), + Product(Indexed(J, (0, 0)), + Indexed(J, (1, 0)))) + V[s+3, s+5] = Power(Indexed(J, (1, 0)), Literal(2)) + V[s+4, s+3] = Product(Indexed(J, (0, 0)), + Indexed(J, (0, 1))) + V[s+4, s+4] = Sum(Product(Indexed(J, (0, 0)), + Indexed(J, (1, 1))), + Product(Indexed(J, (1, 0)), + Indexed(J, (0, 1)))) + V[s+4, s+5] = Product(Indexed(J, (1, 0)), + Indexed(J, (1, 1))) + V[s+5, s+3] = Power(Indexed(J, (0, 1)), Literal(2)) + V[s+5, s+4] = Product(Literal(2), + Product(Indexed(J, (0, 1)), + Indexed(J, (1, 1)))) + V[s+5, s+5] = Power(Indexed(J, (1, 1)), Literal(2)) + + for e in range(3): + v0id, v1id = [i for i in range(3) if i != e] + + # nhat . J^{-T} . t + foo = Sum(Product(Indexed(rns[e], (0,)), + Sum(Product(Indexed(J, (0, 0)), + Indexed(pts, (e, 0))), + Product(Indexed(J, (1, 0)), + Indexed(pts, (e, 1))))), + Product(Indexed(rns[e], (1,)), + Sum(Product(Indexed(J, (0, 1)), + Indexed(pts, (e, 0))), + Product(Indexed(J, (1, 1)), + Indexed(pts, (e, 1)))))) + + # vertex points + V[18+e, 6*v0id] = Division( + Product(Literal(-1), foo), + Product(Literal(21), Indexed(pel, (e,)))) + V[18+e, 6*v1id] = Division( + foo, + Product(Literal(21), Indexed(pel, (e,)))) + + # vertex derivatives + for i in (0, 1): + V[18+e, 6*v0id+1+i] = Division( + Product(Literal(-1), + Product(foo, + Indexed(pts, (e, i)))), + Literal(42)) + V[18+e, 6*v1id+1+i] = V[18+e, 6*v0id+1+i] + + # second derivatives + tau = [Power(Indexed(pts, (e, 0)), Literal(2)), + Product(Literal(2), + Product(Indexed(pts, (e, 0)), + Indexed(pts, (e, 1)))), + Power(Indexed(pts, (e, 1)), Literal(2))] + + for i in (0, 1, 2): + V[18+e, 6*v0id+3+i] = Division( + Product(Literal(-1), + Product(Indexed(pel, (e,)), + Product(foo, tau[i]))), + Literal(252)) + V[18+e, 6*v1id+3+i] = Division( + Product(Indexed(pel, (e,)), + Product(foo, tau[i])), + Literal(252)) + + M = V.T + M = gem.ListTensor(M) + + def matvec(table): + i = gem.Index() + j = gem.Index() + return gem.ComponentTensor( + gem.IndexSum(gem.Product(gem.Indexed(M, (i, j)), + gem.Indexed(table, (j,))), + (j,)), + (i,)) + + result = super(Bell, self).basis_evaluation(order, ps, entity=entity) + + # print(result[(0,0)].shape) + + results = {alpha: matvec(table) + for alpha, table in iteritems(result)} + + # print(results[(0,0)].shape) + return results + + # This wipes out the edge dofs. FIAT gives a 21 DOF element + # because we need some extra functions to help with transforming + # under the edge constraint. However, we only have an 18 DOF + # element. + + def entity_dofs(self): + return {0: {0: range(6), + 1: range(6, 12), + 2: range(12, 18)}, + 1: {0: [], 1: [], 2: []}, + 2: {0: []}} + + @property + def index_shape(self): + return (18,) + + def space_dimension(self): + return 18 + + def point_evaluation(self, order, refcoords, entity=None): + raise NotImplementedError # TODO: think about it later! diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 735c59ca7..5b1bb4969 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -75,8 +75,10 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): if derivative < self.degree: point_indices = ps.indices point_shape = tuple(index.extent for index in point_indices) + + indshp = (self._element.space_dimension(),) exprs.append(gem.partial_indexed( - gem.Literal(table.reshape(point_shape + self.index_shape)), + gem.Literal(table.reshape(point_shape + indshp)), point_indices )) elif derivative == self.degree: From 55fb9dad6dc504c9d0f99ac07ebcbfe062ad4414 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Mon, 25 Jun 2018 15:48:50 +0100 Subject: [PATCH 501/749] Exploit sparsity in physically mapped elements --- finat/argyris.py | 4 +++- finat/bell.py | 13 +++++-------- finat/hermite.py | 11 +++++++---- finat/morley.py | 4 +++- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/finat/argyris.py b/finat/argyris.py index ac42b38e2..dfd76254a 100644 --- a/finat/argyris.py +++ b/finat/argyris.py @@ -143,11 +143,13 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): def matvec(table): i = gem.Index() j = gem.Index() - return gem.ComponentTensor( + val = gem.ComponentTensor( gem.IndexSum(gem.Product(gem.Indexed(M, (i, j)), gem.Indexed(table, (j,))), (j,)), (i,)) + # Eliminate zeros + return gem.optimise.aggressive_unroll(val) result = super(Argyris, self).basis_evaluation(order, ps, entity=entity) return {alpha: matvec(table) diff --git a/finat/bell.py b/finat/bell.py index b6b31a340..56646c570 100644 --- a/finat/bell.py +++ b/finat/bell.py @@ -114,21 +114,18 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): def matvec(table): i = gem.Index() j = gem.Index() - return gem.ComponentTensor( + val = gem.ComponentTensor( gem.IndexSum(gem.Product(gem.Indexed(M, (i, j)), gem.Indexed(table, (j,))), (j,)), (i,)) + # Eliminate zeros + return gem.optimise.aggressive_unroll(val) result = super(Bell, self).basis_evaluation(order, ps, entity=entity) - # print(result[(0,0)].shape) - - results = {alpha: matvec(table) - for alpha, table in iteritems(result)} - - # print(results[(0,0)].shape) - return results + return {alpha: matvec(table) + for alpha, table in iteritems(result)} # This wipes out the edge dofs. FIAT gives a 21 DOF element # because we need some extra functions to help with transforming diff --git a/finat/hermite.py b/finat/hermite.py index 638dfa976..8341c8baf 100644 --- a/finat/hermite.py +++ b/finat/hermite.py @@ -44,10 +44,13 @@ def n(J): def matvec(table): i = gem.Index() j = gem.Index() - return gem.ComponentTensor(gem.IndexSum(gem.Product(gem.Indexed(M, (i, j)), - gem.Indexed(table, (j,))), - (j,)), - (i,)) + val = gem.ComponentTensor( + gem.IndexSum(gem.Product(gem.Indexed(M, (i, j)), + gem.Indexed(table, (j,))), + (j,)), + (i,)) + # Eliminate zeros + return gem.optimise.aggressive_unroll(val) result = super(CubicHermite, self).basis_evaluation(order, ps, entity=entity) return {alpha: matvec(table) diff --git a/finat/morley.py b/finat/morley.py index 8da9d788c..b517bb088 100644 --- a/finat/morley.py +++ b/finat/morley.py @@ -73,11 +73,13 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): def matvec(table): i = gem.Index() j = gem.Index() - return gem.ComponentTensor( + val = gem.ComponentTensor( gem.IndexSum(gem.Product(gem.Indexed(M, (i, j)), gem.Indexed(table, (j,))), (j,)), (i,)) + # Eliminate zeros + return gem.optimise.aggressive_unroll(val) result = super(Morley, self).basis_evaluation(order, ps, entity=entity) return {alpha: matvec(table) From 3c4a95aaa02645c57aec9c764d034a85ce1ac50f Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Mon, 25 Jun 2018 15:50:05 +0100 Subject: [PATCH 502/749] Desix zany element files --- finat/argyris.py | 5 +---- finat/bell.py | 5 +---- finat/hermite.py | 5 +---- finat/morley.py | 5 +---- 4 files changed, 4 insertions(+), 16 deletions(-) diff --git a/finat/argyris.py b/finat/argyris.py index dfd76254a..0b491a1c3 100644 --- a/finat/argyris.py +++ b/finat/argyris.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import, print_function, division -from six import iteritems - import numpy import FIAT @@ -153,7 +150,7 @@ def matvec(table): result = super(Argyris, self).basis_evaluation(order, ps, entity=entity) return {alpha: matvec(table) - for alpha, table in iteritems(result)} + for alpha, table in result.items()} def point_evaluation(self, order, refcoords, entity=None): raise NotImplementedError # TODO: think about it later! diff --git a/finat/bell.py b/finat/bell.py index 56646c570..c5f9f07a4 100644 --- a/finat/bell.py +++ b/finat/bell.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import, print_function, division -from six import iteritems - import numpy import FIAT @@ -125,7 +122,7 @@ def matvec(table): result = super(Bell, self).basis_evaluation(order, ps, entity=entity) return {alpha: matvec(table) - for alpha, table in iteritems(result)} + for alpha, table in result.items()} # This wipes out the edge dofs. FIAT gives a 21 DOF element # because we need some extra functions to help with transforming diff --git a/finat/hermite.py b/finat/hermite.py index 8341c8baf..26f84a508 100644 --- a/finat/hermite.py +++ b/finat/hermite.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import, print_function, division -from six import iteritems - import numpy import FIAT @@ -54,7 +51,7 @@ def matvec(table): result = super(CubicHermite, self).basis_evaluation(order, ps, entity=entity) return {alpha: matvec(table) - for alpha, table in iteritems(result)} + for alpha, table in result.items()} def point_evaluation(self, order, refcoords, entity=None): raise NotImplementedError # TODO: think about it later! diff --git a/finat/morley.py b/finat/morley.py index b517bb088..e0cfcdc0d 100644 --- a/finat/morley.py +++ b/finat/morley.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import, print_function, division -from six import iteritems - import numpy import FIAT @@ -83,7 +80,7 @@ def matvec(table): result = super(Morley, self).basis_evaluation(order, ps, entity=entity) return {alpha: matvec(table) - for alpha, table in iteritems(result)} + for alpha, table in result.items()} def point_evaluation(self, order, refcoords, entity=None): raise NotImplementedError # TODO: think about it later! From a15710bd1b260a4aa808701e189f3526e4ce7c93 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Mon, 25 Jun 2018 16:15:09 +0100 Subject: [PATCH 503/749] Refactor common code into PhysicallMappedElement --- finat/argyris.py | 34 +++------------- finat/bell.py | 36 +++-------------- finat/hermite.py | 35 ++++------------ finat/morley.py | 81 ++++++++++++++------------------------ finat/physically_mapped.py | 38 ++++++++++++++++++ 5 files changed, 88 insertions(+), 136 deletions(-) create mode 100644 finat/physically_mapped.py diff --git a/finat/argyris.py b/finat/argyris.py index 0b491a1c3..0a7f45c6f 100644 --- a/finat/argyris.py +++ b/finat/argyris.py @@ -2,18 +2,17 @@ import FIAT -import gem +from gem import Division, Indexed, Literal, ListTensor, Power, Product, Sum from finat.fiat_elements import ScalarFiatElement +from finat.physically_mapped import PhysicallyMappedElement -class Argyris(ScalarFiatElement): +class Argyris(PhysicallyMappedElement, ScalarFiatElement): def __init__(self, cell): - super(Argyris, self).__init__(FIAT.QuinticArgyris(cell)) - - def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): - assert coordinate_mapping is not None + super().__init__(FIAT.QuinticArgyris(cell)) + def basis_transformation(self, coordinate_mapping): # Jacobians at edge midpoints J = coordinate_mapping.jacobian_at([1/3, 1/3]) @@ -26,8 +25,6 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): V = numpy.zeros((21, 21), dtype=object) - from gem import Product, Literal, Division, Sum, Indexed, Power - for multiindex in numpy.ndindex(V.shape): V[multiindex] = Literal(V[multiindex]) @@ -134,23 +131,4 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): Product(Indexed(J, (1, 1)), Indexed(pns, (e, 1)))))) - M = V.T - M = gem.ListTensor(M) - - def matvec(table): - i = gem.Index() - j = gem.Index() - val = gem.ComponentTensor( - gem.IndexSum(gem.Product(gem.Indexed(M, (i, j)), - gem.Indexed(table, (j,))), - (j,)), - (i,)) - # Eliminate zeros - return gem.optimise.aggressive_unroll(val) - - result = super(Argyris, self).basis_evaluation(order, ps, entity=entity) - return {alpha: matvec(table) - for alpha, table in result.items()} - - def point_evaluation(self, order, refcoords, entity=None): - raise NotImplementedError # TODO: think about it later! + return ListTensor(V.T) diff --git a/finat/bell.py b/finat/bell.py index c5f9f07a4..3546db183 100644 --- a/finat/bell.py +++ b/finat/bell.py @@ -2,18 +2,17 @@ import FIAT -import gem +from gem import Division, Indexed, Literal, ListTensor, Power, Product, Sum from finat.fiat_elements import ScalarFiatElement +from finat.physically_mapped import PhysicallyMappedElement -class Bell(ScalarFiatElement): +class Bell(PhysicallyMappedElement, ScalarFiatElement): def __init__(self, cell): - super(Bell, self).__init__(FIAT.Bell(cell)) - - def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): - assert coordinate_mapping is not None + super().__init__(FIAT.Bell(cell)) + def basis_transformation(self, coordinate_mapping): # Jacobians at edge midpoints J = coordinate_mapping.jacobian_at([1/3, 1/3]) @@ -25,8 +24,6 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): V = numpy.zeros((21, 18), dtype=object) - from gem import Product, Literal, Division, Sum, Indexed, Power - for multiindex in numpy.ndindex(V.shape): V[multiindex] = Literal(V[multiindex]) @@ -105,30 +102,12 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): Product(foo, tau[i])), Literal(252)) - M = V.T - M = gem.ListTensor(M) - - def matvec(table): - i = gem.Index() - j = gem.Index() - val = gem.ComponentTensor( - gem.IndexSum(gem.Product(gem.Indexed(M, (i, j)), - gem.Indexed(table, (j,))), - (j,)), - (i,)) - # Eliminate zeros - return gem.optimise.aggressive_unroll(val) - - result = super(Bell, self).basis_evaluation(order, ps, entity=entity) - - return {alpha: matvec(table) - for alpha, table in result.items()} + return ListTensor(V.T) # This wipes out the edge dofs. FIAT gives a 21 DOF element # because we need some extra functions to help with transforming # under the edge constraint. However, we only have an 18 DOF # element. - def entity_dofs(self): return {0: {0: range(6), 1: range(6, 12), @@ -142,6 +121,3 @@ def index_shape(self): def space_dimension(self): return 18 - - def point_evaluation(self, order, refcoords, entity=None): - raise NotImplementedError # TODO: think about it later! diff --git a/finat/hermite.py b/finat/hermite.py index 26f84a508..f180657cd 100644 --- a/finat/hermite.py +++ b/finat/hermite.py @@ -1,18 +1,17 @@ import numpy import FIAT - -import gem +from gem import Indexed, Literal, ListTensor from finat.fiat_elements import ScalarFiatElement +from finat.physically_mapped import PhysicallyMappedElement -class CubicHermite(ScalarFiatElement): +class CubicHermite(PhysicallyMappedElement, ScalarFiatElement): def __init__(self, cell): - super(CubicHermite, self).__init__(FIAT.CubicHermite(cell)) + super().__init__(FIAT.CubicHermite(cell)) - def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): - assert coordinate_mapping is not None + def basis_transformation(self, coordinate_mapping): Js = [coordinate_mapping.jacobian_at(vertex) for vertex in self.cell.get_vertices()] @@ -22,13 +21,13 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): def n(J): assert J.shape == (d, d) return numpy.array( - [[gem.Indexed(J, (i, j)) for j in range(d)] + [[Indexed(J, (i, j)) for j in range(d)] for i in range(d)]) M = numpy.eye(numbf, dtype=object) for multiindex in numpy.ndindex(M.shape): - M[multiindex] = gem.Literal(M[multiindex]) + M[multiindex] = Literal(M[multiindex]) cur = 0 for i in range(d+1): @@ -36,22 +35,4 @@ def n(J): M[cur:cur+d, cur:cur+d] = n(Js[i]) cur += d - M = gem.ListTensor(M) - - def matvec(table): - i = gem.Index() - j = gem.Index() - val = gem.ComponentTensor( - gem.IndexSum(gem.Product(gem.Indexed(M, (i, j)), - gem.Indexed(table, (j,))), - (j,)), - (i,)) - # Eliminate zeros - return gem.optimise.aggressive_unroll(val) - - result = super(CubicHermite, self).basis_evaluation(order, ps, entity=entity) - return {alpha: matvec(table) - for alpha, table in result.items()} - - def point_evaluation(self, order, refcoords, entity=None): - raise NotImplementedError # TODO: think about it later! + return ListTensor(M) diff --git a/finat/morley.py b/finat/morley.py index e0cfcdc0d..c193185b1 100644 --- a/finat/morley.py +++ b/finat/morley.py @@ -2,18 +2,17 @@ import FIAT -import gem +from gem import Division, Indexed, Literal, ListTensor, Product, Sum from finat.fiat_elements import ScalarFiatElement +from finat.physically_mapped import PhysicallyMappedElement -class Morley(ScalarFiatElement): +class Morley(PhysicallyMappedElement, ScalarFiatElement): def __init__(self, cell): - super(Morley, self).__init__(FIAT.Morley(cell)) - - def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): - assert coordinate_mapping is not None + super().__init__(FIAT.Morley(cell)) + def basis_transformation(self, coordinate_mapping): # Jacobians at edge midpoints J = coordinate_mapping.jacobian_at([1/3, 1/3]) @@ -28,59 +27,39 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): # B12 = rns[i, 0]*(pns[i, 1]*J[0,0] + pts[i, 1]*J[1, 0]) + rts[i, 1]*(pns[i, 1]*J[0, 1] + pts[i, 1]*J[1,1]) - B11 = [gem.Sum(gem.Product(gem.Indexed(rns[i], (0, )), - gem.Sum(gem.Product(gem.Indexed(pns, (i, 0)), - gem.Indexed(J, (0, 0))), - gem.Product(gem.Indexed(pns, (i, 1)), - gem.Indexed(J, (1, 0))))), - gem.Product(gem.Indexed(rns[i], (1, )), - gem.Sum(gem.Product(gem.Indexed(pns, (i, 0)), - gem.Indexed(J, (0, 1))), - gem.Product(gem.Indexed(pns, (i, 1)), - gem.Indexed(J, (1, 1)))))) + B11 = [Sum(Product(Indexed(rns[i], (0, )), + Sum(Product(Indexed(pns, (i, 0)), + Indexed(J, (0, 0))), + Product(Indexed(pns, (i, 1)), + Indexed(J, (1, 0))))), + Product(Indexed(rns[i], (1, )), + Sum(Product(Indexed(pns, (i, 0)), + Indexed(J, (0, 1))), + Product(Indexed(pns, (i, 1)), + Indexed(J, (1, 1)))))) for i in range(3)] - B12 = [gem.Sum(gem.Product(gem.Indexed(rns[i], (0, )), - gem.Sum(gem.Product(gem.Indexed(pts, (i, 0)), - gem.Indexed(J, (0, 0))), - gem.Product(gem.Indexed(pts, (i, 1)), - gem.Indexed(J, (1, 0))))), - gem.Product(gem.Indexed(rns[i], (1, )), - gem.Sum(gem.Product(gem.Indexed(pts, (i, 0)), - gem.Indexed(J, (0, 1))), - gem.Product(gem.Indexed(pts, (i, 1)), - gem.Indexed(J, (1, 1)))))) + B12 = [Sum(Product(Indexed(rns[i], (0, )), + Sum(Product(Indexed(pts, (i, 0)), + Indexed(J, (0, 0))), + Product(Indexed(pts, (i, 1)), + Indexed(J, (1, 0))))), + Product(Indexed(rns[i], (1, )), + Sum(Product(Indexed(pts, (i, 0)), + Indexed(J, (0, 1))), + Product(Indexed(pts, (i, 1)), + Indexed(J, (1, 1)))))) for i in range(3)] V = numpy.eye(6, dtype=object) for multiindex in numpy.ndindex(V.shape): - V[multiindex] = gem.Literal(V[multiindex]) + V[multiindex] = Literal(V[multiindex]) for i in range(3): V[i + 3, i + 3] = B11[i] for i, c in enumerate([(1, 2), (0, 2), (0, 1)]): - V[3+i, c[0]] = gem.Division(gem.Product(gem.Literal(-1), B12[i]), gem.Indexed(pel, (i, ))) - V[3+i, c[1]] = gem.Division(B12[i], gem.Indexed(pel, (i, ))) - - M = V.T - - M = gem.ListTensor(M) - - def matvec(table): - i = gem.Index() - j = gem.Index() - val = gem.ComponentTensor( - gem.IndexSum(gem.Product(gem.Indexed(M, (i, j)), - gem.Indexed(table, (j,))), - (j,)), - (i,)) - # Eliminate zeros - return gem.optimise.aggressive_unroll(val) - - result = super(Morley, self).basis_evaluation(order, ps, entity=entity) - return {alpha: matvec(table) - for alpha, table in result.items()} - - def point_evaluation(self, order, refcoords, entity=None): - raise NotImplementedError # TODO: think about it later! + V[3+i, c[0]] = Division(Product(Literal(-1), B12[i]), Indexed(pel, (i, ))) + V[3+i, c[1]] = Division(B12[i], Indexed(pel, (i, ))) + + return ListTensor(V.T) diff --git a/finat/physically_mapped.py b/finat/physically_mapped.py new file mode 100644 index 000000000..90da190b5 --- /dev/null +++ b/finat/physically_mapped.py @@ -0,0 +1,38 @@ +import gem +from abc import ABCMeta, abstractmethod + + +class PhysicallyMappedElement(metaclass=ABCMeta): + """A mixin that applies a "physical" transformation to tabulated + basis functions.""" + + @abstractmethod + def basis_transformation(self, coordinate_mapping): + """Transformation matrix for the basis functions. + + :arg coordinate_mapping: Object providing physical geometry.""" + pass + + def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): + assert coordinate_mapping is not None + + M = self.basis_transformation(coordinate_mapping) + + def matvec(table): + i = gem.Index() + j = gem.Index() + val = gem.ComponentTensor( + gem.IndexSum(gem.Product(gem.Indexed(M, (i, j)), + gem.Indexed(table, (j,))), + (j,)), + (i,)) + # Eliminate zeros + return gem.optimise.aggressive_unroll(val) + + result = super().basis_evaluation(order, ps, entity=entity) + + return {alpha: matvec(table) + for alpha, table in result.items()} + + def point_evaluation(self, order, refcoords, entity=None): + raise NotImplementedError("TODO: not yet thought about it") From 8d447f3f2d85562c34dec5141317b6806d43eea0 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Mon, 25 Jun 2018 16:28:27 +0100 Subject: [PATCH 504/749] Hook general approach paper into Citations infrastructure --- finat/physically_mapped.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/finat/physically_mapped.py b/finat/physically_mapped.py index 90da190b5..43dfb0e3c 100644 --- a/finat/physically_mapped.py +++ b/finat/physically_mapped.py @@ -1,11 +1,35 @@ import gem from abc import ABCMeta, abstractmethod +try: + from firedrake_citations import Citations + Citations().add("Kirby2018zany", """ +@Article{Kirby2018zany, + author = {Robert C. Kirby}, + title = {A general approach to transforming finite elements}, + journal = {SMAI Journal of Computational Mathematics}, + year = 2018, + volume = 4, + pages = {197-224}, + doi = {10.5802/smai-jcm.33}, + archiveprefix ={arXiv}, + eprint = {1706.09017}, + primaryclass = {math.NA} +} +""") +except ImportError: + Citations = None + class PhysicallyMappedElement(metaclass=ABCMeta): """A mixin that applies a "physical" transformation to tabulated basis functions.""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if Citations is not None: + Citations().register("Kirby2018zany") + @abstractmethod def basis_transformation(self, coordinate_mapping): """Transformation matrix for the basis functions. From 5084d369faa50789a9cff3b59a7893fe850af285 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Tue, 26 Jun 2018 16:29:49 +0100 Subject: [PATCH 505/749] More minor refactoring for zany elements Import them into the global namespace, and make all PhysicalGeometry methods return GEM expressions. --- finat/__init__.py | 4 +++ finat/argyris.py | 14 +++++----- finat/bell.py | 10 ++++--- finat/hermite.py | 6 +++-- finat/morley.py | 14 +++++----- finat/physically_mapped.py | 54 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 84 insertions(+), 18 deletions(-) diff --git a/finat/__init__.py b/finat/__init__.py index 34f1b5bae..44620a120 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -4,6 +4,10 @@ from .fiat_elements import Nedelec, NedelecSecondKind, RaviartThomas # noqa: F401 from .fiat_elements import HellanHerrmannJohnson, Regge # noqa: F401 from .fiat_elements import FacetBubble # noqa: F401 +from .argyris import Argyris # noqa: F401 +from .bell import Bell # noqa: F401 +from .hermite import Hermite # noqa: F401 +from .morley import Morley # noqa: F401 from .trace import HDivTrace # noqa: F401 from .spectral import GaussLobattoLegendre, GaussLegendre # noqa: F401 from .tensorfiniteelement import TensorFiniteElement # noqa: F401 diff --git a/finat/argyris.py b/finat/argyris.py index 0a7f45c6f..110ab46fb 100644 --- a/finat/argyris.py +++ b/finat/argyris.py @@ -9,14 +9,16 @@ class Argyris(PhysicallyMappedElement, ScalarFiatElement): - def __init__(self, cell): + def __init__(self, cell, degree): + if degree != 5: + raise ValueError("Degree must be 5 for Argyris element") super().__init__(FIAT.QuinticArgyris(cell)) def basis_transformation(self, coordinate_mapping): # Jacobians at edge midpoints J = coordinate_mapping.jacobian_at([1/3, 1/3]) - rns = [coordinate_mapping.reference_normal(i) for i in range(3)] + rns = coordinate_mapping.reference_normals() pns = coordinate_mapping.physical_normals() pts = coordinate_mapping.physical_tangents() @@ -57,12 +59,12 @@ def basis_transformation(self, coordinate_mapping): v0id, v1id = [i for i in range(3) if i != e] # nhat . J^{-T} . t - foo = Sum(Product(Indexed(rns[e], (0,)), + foo = Sum(Product(Indexed(rns, (e, 0)), Sum(Product(Indexed(J, (0, 0)), Indexed(pts, (e, 0))), Product(Indexed(J, (1, 0)), Indexed(pts, (e, 1))))), - Product(Indexed(rns[e], (1,)), + Product(Indexed(rns, (e, 1)), Sum(Product(Indexed(J, (0, 1)), Indexed(pts, (e, 0))), Product(Indexed(J, (1, 1)), @@ -104,12 +106,12 @@ def basis_transformation(self, coordinate_mapping): Product(foo, tau[i])), Literal(32)) - V[18+e, 18+e] = Sum(Product(Indexed(rns[e], (0,)), + V[18+e, 18+e] = Sum(Product(Indexed(rns, (e, 0)), Sum(Product(Indexed(J, (0, 0)), Indexed(pns, (e, 0))), Product(Indexed(J, (1, 0)), Indexed(pns, (e, 1))))), - Product(Indexed(rns[e], (1,)), + Product(Indexed(rns, (e, 1)), Sum(Product(Indexed(J, (0, 1)), Indexed(pns, (e, 0))), Product(Indexed(J, (1, 1)), diff --git a/finat/bell.py b/finat/bell.py index 3546db183..1eacab10f 100644 --- a/finat/bell.py +++ b/finat/bell.py @@ -9,14 +9,16 @@ class Bell(PhysicallyMappedElement, ScalarFiatElement): - def __init__(self, cell): + def __init__(self, cell, degree): + if degree != 5: + raise ValueError("Degree must be 3 for Bell element") super().__init__(FIAT.Bell(cell)) def basis_transformation(self, coordinate_mapping): # Jacobians at edge midpoints J = coordinate_mapping.jacobian_at([1/3, 1/3]) - rns = [coordinate_mapping.reference_normal(i) for i in range(3)] + rns = coordinate_mapping.reference_normals() pts = coordinate_mapping.physical_tangents() @@ -56,12 +58,12 @@ def basis_transformation(self, coordinate_mapping): v0id, v1id = [i for i in range(3) if i != e] # nhat . J^{-T} . t - foo = Sum(Product(Indexed(rns[e], (0,)), + foo = Sum(Product(Indexed(rns, (e, 0)), Sum(Product(Indexed(J, (0, 0)), Indexed(pts, (e, 0))), Product(Indexed(J, (1, 0)), Indexed(pts, (e, 1))))), - Product(Indexed(rns[e], (1,)), + Product(Indexed(rns, (e, 1)), Sum(Product(Indexed(J, (0, 1)), Indexed(pts, (e, 0))), Product(Indexed(J, (1, 1)), diff --git a/finat/hermite.py b/finat/hermite.py index f180657cd..5cf44f6d1 100644 --- a/finat/hermite.py +++ b/finat/hermite.py @@ -7,8 +7,10 @@ from finat.physically_mapped import PhysicallyMappedElement -class CubicHermite(PhysicallyMappedElement, ScalarFiatElement): - def __init__(self, cell): +class Hermite(PhysicallyMappedElement, ScalarFiatElement): + def __init__(self, cell, degree): + if degree != 3: + raise ValueError("Degree must be 3 for Hermite element") super().__init__(FIAT.CubicHermite(cell)) def basis_transformation(self, coordinate_mapping): diff --git a/finat/morley.py b/finat/morley.py index c193185b1..f5df38ff5 100644 --- a/finat/morley.py +++ b/finat/morley.py @@ -9,14 +9,16 @@ class Morley(PhysicallyMappedElement, ScalarFiatElement): - def __init__(self, cell): + def __init__(self, cell, degree): + if degree != 2: + raise ValueError("Degree must be 2 for Morley element") super().__init__(FIAT.Morley(cell)) def basis_transformation(self, coordinate_mapping): # Jacobians at edge midpoints J = coordinate_mapping.jacobian_at([1/3, 1/3]) - rns = [coordinate_mapping.reference_normal(i) for i in range(3)] + rns = coordinate_mapping.reference_normals() pns = coordinate_mapping.physical_normals() pts = coordinate_mapping.physical_tangents() @@ -27,24 +29,24 @@ def basis_transformation(self, coordinate_mapping): # B12 = rns[i, 0]*(pns[i, 1]*J[0,0] + pts[i, 1]*J[1, 0]) + rts[i, 1]*(pns[i, 1]*J[0, 1] + pts[i, 1]*J[1,1]) - B11 = [Sum(Product(Indexed(rns[i], (0, )), + B11 = [Sum(Product(Indexed(rns, (i, 0)), Sum(Product(Indexed(pns, (i, 0)), Indexed(J, (0, 0))), Product(Indexed(pns, (i, 1)), Indexed(J, (1, 0))))), - Product(Indexed(rns[i], (1, )), + Product(Indexed(rns, (i, 1)), Sum(Product(Indexed(pns, (i, 0)), Indexed(J, (0, 1))), Product(Indexed(pns, (i, 1)), Indexed(J, (1, 1)))))) for i in range(3)] - B12 = [Sum(Product(Indexed(rns[i], (0, )), + B12 = [Sum(Product(Indexed(rns, (i, 0)), Sum(Product(Indexed(pts, (i, 0)), Indexed(J, (0, 0))), Product(Indexed(pts, (i, 1)), Indexed(J, (1, 0))))), - Product(Indexed(rns[i], (1, )), + Product(Indexed(rns, (i, 1)), Sum(Product(Indexed(pts, (i, 0)), Indexed(J, (0, 1))), Product(Indexed(pts, (i, 1)), diff --git a/finat/physically_mapped.py b/finat/physically_mapped.py index 43dfb0e3c..d4eda331f 100644 --- a/finat/physically_mapped.py +++ b/finat/physically_mapped.py @@ -60,3 +60,57 @@ def matvec(table): def point_evaluation(self, order, refcoords, entity=None): raise NotImplementedError("TODO: not yet thought about it") + + +class PhysicalGeometry(metaclass=ABCMeta): + + @abstractmethod + def jacobian_at(self, point): + """The jacobian of the physical coordinates at a point. + + :arg point: The point in reference space to evaluate the Jacobian. + :returns: A GEM expression for the Jacobian, shape (gdim, tdim). + """ + pass + + @abstractmethod + def reference_normals(self): + """The (unit) reference cell normals for each facet. + + :returns: A GEM expression for the normal to each + facet (numbered according to FIAT conventions), shape + (nfacet, tdim). + """ + pass + + @abstractmethod + def physical_normals(self): + """The (unit) physical cell normals for each facet. + + :returns: A GEM expression for the normal to each + facet (numbered according to FIAT conventions). These are + all computed by a clockwise rotation of the physical + tangents, shape (nfacet, gdim). + """ + pass + + @abstractmethod + def physical_tangents(self): + """The (unit) physical cell tangents on each facet. + + :returns: A GEM expression for the tangent to each + facet (numbered according to FIAT conventions). These + always point from low to high numbered local vertex, shape + (nfacet, gdim). + """ + pass + + @abstractmethod + def physical_edge_lengths(self): + """The length of each edge of the physical cell. + + :returns: A GEM expression for the length of each + edge (numbered according to FIAT conventions), shape + (nfacet, ). + """ + pass From 54cf8ac500ca90baf3656e7b224d5da2b39903fb Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Tue, 26 Jun 2018 17:40:15 +0100 Subject: [PATCH 506/749] Remove unneeded physical_geometry.py Helper abstract class now lives in physically_mapped. --- finat/physical_geometry.py | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 finat/physical_geometry.py diff --git a/finat/physical_geometry.py b/finat/physical_geometry.py deleted file mode 100644 index b53471a83..000000000 --- a/finat/physical_geometry.py +++ /dev/null @@ -1,9 +0,0 @@ -from six import with_metaclass - -from abc import ABCMeta, abstractmethod - - -class PhysicalGeometry(with_metaclass(ABCMeta)): - @abstractmethod - def jacobian_at(self, point): - pass From d66b7f8b31f8f3c0d8f5b362dbcafdcb8bd33d98 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Tue, 26 Jun 2018 17:40:37 +0100 Subject: [PATCH 507/749] Accept coordinate_mapping for all basis_evaluation Dropped on the floor for many cases. --- finat/cube.py | 2 +- finat/discontinuous.py | 4 ++-- finat/enriched.py | 4 ++-- finat/finiteelementbase.py | 3 +++ finat/hdivcurl.py | 2 +- finat/mixed.py | 4 ++-- finat/quadrature_element.py | 2 +- finat/runtime_tabulated.py | 2 +- finat/spectral.py | 6 ++++-- finat/tensor_product.py | 2 +- 10 files changed, 18 insertions(+), 13 deletions(-) diff --git a/finat/cube.py b/finat/cube.py index c1284b27f..cab199c75 100644 --- a/finat/cube.py +++ b/finat/cube.py @@ -51,7 +51,7 @@ def entity_dofs(self): def space_dimension(self): return self.product.space_dimension() - def basis_evaluation(self, order, ps, entity=None): + def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): """Return code for evaluating the element at known points on the reference element. diff --git a/finat/discontinuous.py b/finat/discontinuous.py index e8309df63..69b6d5fc8 100644 --- a/finat/discontinuous.py +++ b/finat/discontinuous.py @@ -45,8 +45,8 @@ def index_shape(self): def value_shape(self): return self.element.value_shape - def basis_evaluation(self, order, ps, entity=None): - return self.element.basis_evaluation(order, ps, entity) + def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): + return self.element.basis_evaluation(order, ps, entity, coordinate_mapping=coordinate_mapping) def point_evaluation(self, order, refcoords, entity=None): return self.element.point_evaluation(order, refcoords, entity) diff --git a/finat/enriched.py b/finat/enriched.py index 4563ebc61..c4940298b 100644 --- a/finat/enriched.py +++ b/finat/enriched.py @@ -80,7 +80,7 @@ def merge(tables): return {key: merge(result[key] for result in results) for key in keys} - def basis_evaluation(self, order, ps, entity=None): + def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): '''Return code for evaluating the element at known points on the reference element. @@ -88,7 +88,7 @@ def basis_evaluation(self, order, ps, entity=None): :param ps: the point set object. :param entity: the cell entity on which to tabulate. ''' - results = [element.basis_evaluation(order, ps, entity) + results = [element.basis_evaluation(order, ps, entity, coordinate_mapping=coordinate_mapping) for element in self.elements] return self._compose_evaluations(results) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 27c821d63..8e00500a9 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -119,6 +119,9 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): :param order: return derivatives up to this order. :param ps: the point set object. :param entity: the cell entity on which to tabulate. + :param coordinate_mapping: a + :class:`~.physically_mapped.PhysicalGeometry` object that + provides physical geometry callbacks (may be None). ''' @abstractmethod diff --git a/finat/hdivcurl.py b/finat/hdivcurl.py index 5027b4a6f..61bb80429 100644 --- a/finat/hdivcurl.py +++ b/finat/hdivcurl.py @@ -60,7 +60,7 @@ def promote(table): return {alpha: promote(table) for alpha, table in core_eval.items()} - def basis_evaluation(self, order, ps, entity=None): + def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): core_eval = self.wrappee.basis_evaluation(order, ps, entity) return self._transform_evaluation(core_eval) diff --git a/finat/mixed.py b/finat/mixed.py index baf04d988..099613956 100644 --- a/finat/mixed.py +++ b/finat/mixed.py @@ -83,8 +83,8 @@ def promote(table): return {alpha: promote(table) for alpha, table in core_eval.items()} - def basis_evaluation(self, order, ps, entity=None): - core_eval = self.element.basis_evaluation(order, ps, entity) + def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): + core_eval = self.element.basis_evaluation(order, ps, entity, coordinate_mapping=coordinate_mapping) return self._transform_evaluation(core_eval) def point_evaluation(self, order, refcoords, entity=None): diff --git a/finat/quadrature_element.py b/finat/quadrature_element.py index db68ff682..1e9c4d0ba 100644 --- a/finat/quadrature_element.py +++ b/finat/quadrature_element.py @@ -51,7 +51,7 @@ def index_shape(self): def value_shape(self): return () - def basis_evaluation(self, order, ps, entity=None): + def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): '''Return code for evaluating the element at known points on the reference element. diff --git a/finat/runtime_tabulated.py b/finat/runtime_tabulated.py index 5b3d71c02..cc629fb43 100644 --- a/finat/runtime_tabulated.py +++ b/finat/runtime_tabulated.py @@ -65,7 +65,7 @@ def entity_dofs(self): def space_dimension(self): return self.degree + 1 - def basis_evaluation(self, order, ps, entity=None): + def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): """Return code for evaluating the element at known points on the reference element. diff --git a/finat/spectral.py b/finat/spectral.py index 8b8d70efc..292db805c 100644 --- a/finat/spectral.py +++ b/finat/spectral.py @@ -13,7 +13,7 @@ def __init__(self, cell, degree): fiat_element = FIAT.GaussLobattoLegendre(cell, degree) super(GaussLobattoLegendre, self).__init__(fiat_element) - def basis_evaluation(self, order, ps, entity=None): + def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): '''Return code for evaluating the element at known points on the reference element. @@ -21,6 +21,7 @@ def basis_evaluation(self, order, ps, entity=None): :param ps: the point set. :param entity: the cell entity on which to tabulate. ''' + assert coordinate_mapping is None result = super(GaussLobattoLegendre, self).basis_evaluation(order, ps, entity) cell_dimension = self.cell.get_dimension() if entity is None or entity == (cell_dimension, 0): # on cell interior @@ -41,7 +42,7 @@ def __init__(self, cell, degree): fiat_element = FIAT.GaussLegendre(cell, degree) super(GaussLegendre, self).__init__(fiat_element) - def basis_evaluation(self, order, ps, entity=None): + def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): '''Return code for evaluating the element at known points on the reference element. @@ -49,6 +50,7 @@ def basis_evaluation(self, order, ps, entity=None): :param ps: the point set. :param entity: the cell entity on which to tabulate. ''' + assert coordinate_mapping is None result = super(GaussLegendre, self).basis_evaluation(order, ps, entity) cell_dimension = self.cell.get_dimension() if entity is None or entity == (cell_dimension, 0): # on cell interior diff --git a/finat/tensor_product.py b/finat/tensor_product.py index 95b2c4d5f..59a29e980 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -121,7 +121,7 @@ def _merge_evaluations(self, factor_results): ) return result - def basis_evaluation(self, order, ps, entity=None): + def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): entities = self._factor_entity(entity) entity_dim, _ = zip(*entities) From 82d892688c275f842ccfbcf525a8c1a3c54d9274 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Tue, 26 Jun 2018 11:56:30 -0500 Subject: [PATCH 508/749] Make new elements top-level importable --- finat/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/finat/__init__.py b/finat/__init__.py index 34f1b5bae..4ba868522 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -4,6 +4,9 @@ from .fiat_elements import Nedelec, NedelecSecondKind, RaviartThomas # noqa: F401 from .fiat_elements import HellanHerrmannJohnson, Regge # noqa: F401 from .fiat_elements import FacetBubble # noqa: F401 +from .morley import Morley # noqa: F401 +from .argyris import Argyris # noqa: F401 +from .hermite import CubicHermite # noqa: F401 from .trace import HDivTrace # noqa: F401 from .spectral import GaussLobattoLegendre, GaussLegendre # noqa: F401 from .tensorfiniteelement import TensorFiniteElement # noqa: F401 From b50a3b5a998e82ff4e41d8a6cd1b40baee9fdd9e Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Tue, 26 Jun 2018 18:27:04 +0100 Subject: [PATCH 509/749] References for Argyris, Bell, Hermite, Morley Should go through and add these for the other element types too! --- finat/argyris.py | 4 ++- finat/bell.py | 4 ++- finat/hermite.py | 4 ++- finat/morley.py | 4 ++- finat/physically_mapped.py | 52 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 64 insertions(+), 4 deletions(-) diff --git a/finat/argyris.py b/finat/argyris.py index 110ab46fb..9db9b7dcf 100644 --- a/finat/argyris.py +++ b/finat/argyris.py @@ -5,13 +5,15 @@ from gem import Division, Indexed, Literal, ListTensor, Power, Product, Sum from finat.fiat_elements import ScalarFiatElement -from finat.physically_mapped import PhysicallyMappedElement +from finat.physically_mapped import PhysicallyMappedElement, Citations class Argyris(PhysicallyMappedElement, ScalarFiatElement): def __init__(self, cell, degree): if degree != 5: raise ValueError("Degree must be 5 for Argyris element") + if Citations is not None: + Citations().register("Argyris1968") super().__init__(FIAT.QuinticArgyris(cell)) def basis_transformation(self, coordinate_mapping): diff --git a/finat/bell.py b/finat/bell.py index 1eacab10f..b140e9286 100644 --- a/finat/bell.py +++ b/finat/bell.py @@ -5,13 +5,15 @@ from gem import Division, Indexed, Literal, ListTensor, Power, Product, Sum from finat.fiat_elements import ScalarFiatElement -from finat.physically_mapped import PhysicallyMappedElement +from finat.physically_mapped import PhysicallyMappedElement, Citations class Bell(PhysicallyMappedElement, ScalarFiatElement): def __init__(self, cell, degree): if degree != 5: raise ValueError("Degree must be 3 for Bell element") + if Citations is not None: + Citations().register("Bell1969") super().__init__(FIAT.Bell(cell)) def basis_transformation(self, coordinate_mapping): diff --git a/finat/hermite.py b/finat/hermite.py index 5cf44f6d1..94d779047 100644 --- a/finat/hermite.py +++ b/finat/hermite.py @@ -4,13 +4,15 @@ from gem import Indexed, Literal, ListTensor from finat.fiat_elements import ScalarFiatElement -from finat.physically_mapped import PhysicallyMappedElement +from finat.physically_mapped import PhysicallyMappedElement, Citations class Hermite(PhysicallyMappedElement, ScalarFiatElement): def __init__(self, cell, degree): if degree != 3: raise ValueError("Degree must be 3 for Hermite element") + if Citations is not None: + Citations().register("Ciarlet1972") super().__init__(FIAT.CubicHermite(cell)) def basis_transformation(self, coordinate_mapping): diff --git a/finat/morley.py b/finat/morley.py index f5df38ff5..6931c5327 100644 --- a/finat/morley.py +++ b/finat/morley.py @@ -5,13 +5,15 @@ from gem import Division, Indexed, Literal, ListTensor, Product, Sum from finat.fiat_elements import ScalarFiatElement -from finat.physically_mapped import PhysicallyMappedElement +from finat.physically_mapped import PhysicallyMappedElement, Citations class Morley(PhysicallyMappedElement, ScalarFiatElement): def __init__(self, cell, degree): if degree != 2: raise ValueError("Degree must be 2 for Morley element") + if Citations is not None: + Citations().register("Morley1971") super().__init__(FIAT.Morley(cell)) def basis_transformation(self, coordinate_mapping): diff --git a/finat/physically_mapped.py b/finat/physically_mapped.py index d4eda331f..c261a7eb7 100644 --- a/finat/physically_mapped.py +++ b/finat/physically_mapped.py @@ -16,6 +16,58 @@ eprint = {1706.09017}, primaryclass = {math.NA} } +""") + Citations().add("Argyris1968", """ +@Article{Argyris1968, + author = {J. H. Argyris and I. Fried and D. W. Scharpf}, + title = {{The TUBA family of plate elements for the matrix + displacement method}}, + journal = {The Aeronautical Journal}, + year = 1968, + volume = 72, + pages = {701-709}, + doi = {10.1017/S000192400008489X} +} +""") + Citations().add("Bell1969", """ +@Article{Bell1969, + author = {Kolbein Bell}, + title = {A refined triangular plate bending finite element}, + journal = {International Journal for Numerical Methods in + Engineering}, + year = 1969, + volume = 1, + number = 1, + pages = {101-122}, + doi = {10.1002/nme.1620010108} +} +""") + Citations().add("Ciarlet1972", """ +@Article{Ciarlet1972, + author = {P. G. Ciarlet and P. A. Raviart}, + title = {{General Lagrange and Hermite interpolation in + $\mathbb{R}^n$ with applications to finite element + methods}}, + journal = {Archive for Rational Mechanics and Analysis}, + year = 1972, + volume = 46, + number = 3, + pages = {177-199}, + doi = {10.1007/BF0025245} +} +""") + Citations().add("Morley1971", """ +@Article{Morley1971, + author = {L. S. D. Morley}, + title = {The constant-moment plate-bending element}, + journal = {The Journal of Strain Analysis for Engineering + Design}, + year = 1971, + volume = 6, + number = 1, + pages = {20-24}, + doi = {10.1243/03093247V061020} +} """) except ImportError: Citations = None From b05039d607f3c890c0ae3ecea7bcf6728790f1e4 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Tue, 26 Jun 2018 12:53:50 -0500 Subject: [PATCH 510/749] Fix bug in Argyris that appeared after refactoring. --- finat/argyris.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/finat/argyris.py b/finat/argyris.py index 110ab46fb..ab6301176 100644 --- a/finat/argyris.py +++ b/finat/argyris.py @@ -119,14 +119,14 @@ def basis_transformation(self, coordinate_mapping): Sum( Product( - Indexed(rns[e], (0,)), + Indexed(rns, (e, 0)), Sum( Product(Indexed(J, (0, 0)), Indexed(pns, (e, 0))), Product(Indexed(J, (1, 0)), Indexed(pns, (e, 1))))), Product( - Indexed(rns[e], (1,)), + Indexed(rns, (e, 1)), Sum( Product(Indexed(J, (0, 1)), Indexed(pns, (e, 0))), From 626f97bd9eec027afbd30c8a29cba8a3f7e128f0 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Wed, 27 Jun 2018 11:35:42 +0100 Subject: [PATCH 511/749] Add cell_size method to PhysicalGeometry --- finat/physically_mapped.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/finat/physically_mapped.py b/finat/physically_mapped.py index c261a7eb7..dbfde1fa3 100644 --- a/finat/physically_mapped.py +++ b/finat/physically_mapped.py @@ -116,6 +116,13 @@ def point_evaluation(self, order, refcoords, entity=None): class PhysicalGeometry(metaclass=ABCMeta): + @abstractmethod + def cell_size(self): + """The cell size at each vertex. + + :returns: A GEM expression for the cell size, shape (nvertex, ). + """ + @abstractmethod def jacobian_at(self, point): """The jacobian of the physical coordinates at a point. @@ -123,7 +130,6 @@ def jacobian_at(self, point): :arg point: The point in reference space to evaluate the Jacobian. :returns: A GEM expression for the Jacobian, shape (gdim, tdim). """ - pass @abstractmethod def reference_normals(self): @@ -133,7 +139,6 @@ def reference_normals(self): facet (numbered according to FIAT conventions), shape (nfacet, tdim). """ - pass @abstractmethod def physical_normals(self): @@ -144,7 +149,6 @@ def physical_normals(self): all computed by a clockwise rotation of the physical tangents, shape (nfacet, gdim). """ - pass @abstractmethod def physical_tangents(self): @@ -155,7 +159,6 @@ def physical_tangents(self): always point from low to high numbered local vertex, shape (nfacet, gdim). """ - pass @abstractmethod def physical_edge_lengths(self): @@ -165,4 +168,3 @@ def physical_edge_lengths(self): edge (numbered according to FIAT conventions), shape (nfacet, ). """ - pass From 807729474691a29b334e5aa586079d74199b74f4 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Wed, 27 Jun 2018 09:45:49 -0500 Subject: [PATCH 512/749] Rescale dofs for zany elements to improve conditioning --- finat/argyris.py | 36 +++++++++++++++++++++--------------- finat/bell.py | 13 +++++++++++++ finat/hermite.py | 10 ++++++++-- finat/morley.py | 11 +++++++---- 4 files changed, 49 insertions(+), 21 deletions(-) diff --git a/finat/argyris.py b/finat/argyris.py index 84cca9e14..4cdadb63a 100644 --- a/finat/argyris.py +++ b/finat/argyris.py @@ -119,20 +119,26 @@ def basis_transformation(self, coordinate_mapping): Product(Indexed(J, (1, 1)), Indexed(pns, (e, 1)))))) - Sum( - Product( - Indexed(rns, (e, 0)), - Sum( - Product(Indexed(J, (0, 0)), - Indexed(pns, (e, 0))), - Product(Indexed(J, (1, 0)), - Indexed(pns, (e, 1))))), - Product( - Indexed(rns, (e, 1)), - Sum( - Product(Indexed(J, (0, 1)), - Indexed(pns, (e, 0))), - Product(Indexed(J, (1, 1)), - Indexed(pns, (e, 1)))))) + # Patch up conditioning + h = coordinate_mapping.cell_size() + + for v in range(3): + for k in range(2): + for i in range(21): + V[i, 6*v+1+k] = Division(V[i, 6*v+1+k], + Indexed(h, (v,))) + for k in range(3): + for i in range(21): + V[i, 6*v+3+k] = Division(V[i, 6*v+3+k], + Power(Indexed(h, (v,)), + Literal(2))) + for e in range(3): + v0id, v1id = [i for i in range(3) if i != e] + for i in range(21): + V[i, 18+e] = Division(V[i, 18+e], + Division( + Sum(Indexed(h, (v0id,)), + Indexed(h, (v1id,))), + Literal(2))) return ListTensor(V.T) diff --git a/finat/bell.py b/finat/bell.py index b140e9286..3f8d6d93e 100644 --- a/finat/bell.py +++ b/finat/bell.py @@ -106,6 +106,19 @@ def basis_transformation(self, coordinate_mapping): Product(foo, tau[i])), Literal(252)) + h = coordinate_mapping.cell_size() + + for v in range(3): + for k in range(2): + for i in range(21): + V[i, 6*v+1+k] = Division(V[i, 6*v+1+k], + Indexed(h, (v,))) + for k in range(3): + for i in range(21): + V[i, 6*v+3+k] = Division(V[i, 6*v+3+k], + Power(Indexed(h, (v,)), + Literal(2))) + return ListTensor(V.T) # This wipes out the edge dofs. FIAT gives a 21 DOF element diff --git a/finat/hermite.py b/finat/hermite.py index 94d779047..9db8e8ece 100644 --- a/finat/hermite.py +++ b/finat/hermite.py @@ -1,7 +1,7 @@ import numpy import FIAT -from gem import Indexed, Literal, ListTensor +from gem import Indexed, Literal, ListTensor, Division from finat.fiat_elements import ScalarFiatElement from finat.physically_mapped import PhysicallyMappedElement, Citations @@ -19,6 +19,8 @@ def basis_transformation(self, coordinate_mapping): Js = [coordinate_mapping.jacobian_at(vertex) for vertex in self.cell.get_vertices()] + h = coordinate_mapping.cell_size() + d = self.cell.get_dimension() numbf = self.space_dimension() @@ -36,7 +38,11 @@ def n(J): cur = 0 for i in range(d+1): cur += 1 # skip the vertex - M[cur:cur+d, cur:cur+d] = n(Js[i]) + nJsi = n(Js[i]) + for j in range(d): + for k in range(d): + M[cur+j, cur+k] = Division(nJsi[j, k], + Indexed(h, (i,))) cur += d return ListTensor(M) diff --git a/finat/morley.py b/finat/morley.py index 6931c5327..2dca7ce23 100644 --- a/finat/morley.py +++ b/finat/morley.py @@ -27,10 +27,6 @@ def basis_transformation(self, coordinate_mapping): pel = coordinate_mapping.physical_edge_lengths() - # B11 = rns[i, 0]*(pns[i, 0]*J[0,0] + pts[i, 0]*J[1, 0]) + rts[i, 0]*(pns[i, 0]*J[0, 1] + pts[i, 0]*J[1,1]) - - # B12 = rns[i, 0]*(pns[i, 1]*J[0,0] + pts[i, 1]*J[1, 0]) + rts[i, 1]*(pns[i, 1]*J[0, 1] + pts[i, 1]*J[1,1]) - B11 = [Sum(Product(Indexed(rns, (i, 0)), Sum(Product(Indexed(pns, (i, 0)), Indexed(J, (0, 0))), @@ -66,4 +62,11 @@ def basis_transformation(self, coordinate_mapping): V[3+i, c[0]] = Division(Product(Literal(-1), B12[i]), Indexed(pel, (i, ))) V[3+i, c[1]] = Division(B12[i], Indexed(pel, (i, ))) + # diagonal post-scaling to patch up conditioning + h = coordinate_mapping.cell_size() + + for j in range(3): + for i in range(6): + V[i, 3+j] = Division(V[i, 3+j], Indexed(h, (j,))) + return ListTensor(V.T) From c3931264e51cf4e00ac60ee180bd58e5851c9db6 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Wed, 27 Jun 2018 17:28:54 +0100 Subject: [PATCH 513/749] optimise: fix select_expression for case where some indices are fixed The shape of an indexed expression is only determined by its free indices. --- gem/optimise.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gem/optimise.py b/gem/optimise.py index ebfffeee1..21f4966e5 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -172,7 +172,8 @@ def _select_expression(expressions, index): types = set(map(type, expressions)) if types <= {Indexed, Zero}: multiindex, = set(e.multiindex for e in expressions if isinstance(e, Indexed)) - shape = tuple(i.extent for i in multiindex) + # Shape only determined by free indices + shape = tuple(i.extent for i in multiindex if isinstance(i, Index)) def child(expression): if isinstance(expression, Indexed): From 09721876a00a34b37570364e4f9ac125e3abb1e9 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Wed, 4 Jul 2018 09:39:34 +0100 Subject: [PATCH 514/749] Add comment about index_shape in fiat element Why might it be that self.space_dimension() != element.space_dimension()? --- finat/fiat_elements.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 5b1bb4969..d2c9ae297 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -60,6 +60,14 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): value_size = np.prod(self._element.value_shape(), dtype=int) fiat_result = self._element.tabulate(order, ps.points, entity) result = {} + # In almost all cases, we have + # self.space_dimension() == self._element.space_dimension() + # But for Bell, FIAT reports 21 basis functions, + # but FInAT only 18 (because there are actually 18 + # basis functions, and the additional 3 are for + # dealing with transformations between physical + # and reference space). + index_shape = (self._element.space_dimension(),) for alpha, fiat_table in fiat_result.items(): if isinstance(fiat_table, Exception): result[alpha] = gem.Failure(self.index_shape + self.value_shape, fiat_table) @@ -76,9 +84,8 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): point_indices = ps.indices point_shape = tuple(index.extent for index in point_indices) - indshp = (self._element.space_dimension(),) exprs.append(gem.partial_indexed( - gem.Literal(table.reshape(point_shape + indshp)), + gem.Literal(table.reshape(point_shape + index_shape)), point_indices )) elif derivative == self.degree: From 33057c29d187f633146cdd113a0a7c7e4c1f2092 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Wed, 4 Jul 2018 10:01:09 +0100 Subject: [PATCH 515/749] gem: Add some minimal sugar Support +, -, *, /, and __getitem__ magic methods. Fixes #168. --- gem/gem.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/gem/gem.py b/gem/gem.py index d5870ac9f..ee8268a55 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -70,6 +70,29 @@ def is_equal(self, other): self.children = other.children return result + def __getitem__(self, indices): + try: + indices = tuple(indices) + except TypeError: + indices = (indices, ) + return Indexed(self, indices) + + def __add__(self, other): + assert isinstance(other, Node) + return Sum(self, other) + + def __sub__(self, other): + assert isinstance(other, Node) + return Sum(self, Product(Literal(-1), other)) + + def __mul__(self, other): + assert isinstance(other, Node) + return Product(self, other) + + def __truediv__(self, other): + assert isinstance(other, Node) + return Division(self, other) + class Terminal(Node): """Abstract class for terminal GEM nodes.""" From 0cf4b37908a678d229ad6c3818cbbea6d97dce9c Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Wed, 4 Jul 2018 12:03:20 +0100 Subject: [PATCH 516/749] A little sweeter --- gem/gem.py | 54 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index ee8268a55..ffcb64939 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -17,7 +17,7 @@ from abc import ABCMeta from itertools import chain from operator import attrgetter -from numbers import Integral +from numbers import Integral, Number import numpy from numpy import asarray @@ -31,7 +31,8 @@ 'LogicalNot', 'LogicalAnd', 'LogicalOr', 'Conditional', 'Index', 'VariableIndex', 'Indexed', 'ComponentTensor', 'IndexSum', 'ListTensor', 'Concatenate', 'Delta', - 'index_sum', 'partial_indexed', 'reshape', 'view'] + 'index_sum', 'partial_indexed', 'reshape', 'view', + 'indices', 'as_gem'] class NodeMeta(type): @@ -78,20 +79,28 @@ def __getitem__(self, indices): return Indexed(self, indices) def __add__(self, other): - assert isinstance(other, Node) - return Sum(self, other) + return Sum(self, as_gem(other)) + + def __radd__(self, other): + return as_gem(other).__add__(self) def __sub__(self, other): - assert isinstance(other, Node) - return Sum(self, Product(Literal(-1), other)) + return Sum(self, Product(Literal(-1), as_gem(other))) + + def __rsub__(self, other): + return as_gem(other).__sub__(self) def __mul__(self, other): - assert isinstance(other, Node) - return Product(self, other) + return Product(self, as_gem(other)) + + def __rmul__(self, other): + return as_gem(other).__mul__(self) def __truediv__(self, other): - assert isinstance(other, Node) - return Division(self, other) + return Division(self, as_gem(other)) + + def __rtruediv__(self, other): + return as_gem(other).__truediv__(self) class Terminal(Node): @@ -901,3 +910,28 @@ def view(expression, *slices): # Static one object for quicker constant folding one = Literal(1) + + +# Syntax sugar +def indices(n): + """Make some :class:`Index` objects. + + :arg n: The number of indices to make. + :returns: A tuple of `n` :class:`Index` objects. + """ + return tuple(Index() for _ in range(n)) + + +def as_gem(expr): + """Attempt to convert an expression into GEM. + + :arg expr: The expression. + :returns: A GEM representation of the expression. + :raises ValueError: if conversion was not possible. + """ + if isinstance(expr, Node): + return expr + elif isinstance(expr, Number): + return Literal(expr) + else: + raise ValueError("Do not know how to convert %r to GEM" % expr) From d909bada7f87686e0a27600427ed2fd3692a7ab8 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Thu, 5 Jul 2018 14:16:36 +0100 Subject: [PATCH 517/749] bell: Don't put an iterator in entity_dofs --- finat/bell.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/finat/bell.py b/finat/bell.py index 3f8d6d93e..43fcfcfa8 100644 --- a/finat/bell.py +++ b/finat/bell.py @@ -126,9 +126,9 @@ def basis_transformation(self, coordinate_mapping): # under the edge constraint. However, we only have an 18 DOF # element. def entity_dofs(self): - return {0: {0: range(6), - 1: range(6, 12), - 2: range(12, 18)}, + return {0: {0: list(range(6)), + 1: list(range(6, 12)), + 2: list(range(12, 18))}, 1: {0: [], 1: [], 2: []}, 2: {0: []}} From 63f413f66d6f9658b007440801ac010ddd651c24 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Thu, 5 Jul 2018 17:10:20 +0100 Subject: [PATCH 518/749] Use new gem sugar to prettyify physically mapped basis transformation --- finat/argyris.py | 104 ++++++++++--------------------------- finat/bell.py | 83 ++++++++--------------------- finat/hermite.py | 13 ++--- finat/morley.py | 37 +++---------- finat/physically_mapped.py | 9 +--- 5 files changed, 63 insertions(+), 183 deletions(-) diff --git a/finat/argyris.py b/finat/argyris.py index 4cdadb63a..820c2c805 100644 --- a/finat/argyris.py +++ b/finat/argyris.py @@ -2,7 +2,7 @@ import FIAT -from gem import Division, Indexed, Literal, ListTensor, Power, Product, Sum +from gem import Literal, ListTensor from finat.fiat_elements import ScalarFiatElement from finat.physically_mapped import PhysicallyMappedElement, Citations @@ -37,87 +37,44 @@ def basis_transformation(self, coordinate_mapping): V[s, s] = Literal(1) for i in range(2): for j in range(2): - V[s+1+i, s+1+j] = Indexed(J, (j, i)) - V[s+3, s+3] = Power(Indexed(J, (0, 0)), Literal(2)) - V[s+3, s+4] = Product(Literal(2), - Product(Indexed(J, (0, 0)), - Indexed(J, (1, 0)))) - V[s+3, s+5] = Power(Indexed(J, (1, 0)), Literal(2)) - V[s+4, s+3] = Product(Indexed(J, (0, 0)), - Indexed(J, (0, 1))) - V[s+4, s+4] = Sum(Product(Indexed(J, (0, 0)), - Indexed(J, (1, 1))), - Product(Indexed(J, (1, 0)), - Indexed(J, (0, 1)))) - V[s+4, s+5] = Product(Indexed(J, (1, 0)), - Indexed(J, (1, 1))) - V[s+5, s+3] = Power(Indexed(J, (0, 1)), Literal(2)) - V[s+5, s+4] = Product(Literal(2), - Product(Indexed(J, (0, 1)), - Indexed(J, (1, 1)))) - V[s+5, s+5] = Power(Indexed(J, (1, 1)), Literal(2)) + V[s+1+i, s+1+j] = J[j, i] + V[s+3, s+3] = J[0, 0]*J[0, 0] + V[s+3, s+4] = 2*J[0, 0]*J[1, 0] + V[s+3, s+5] = J[1, 0]*J[1, 0] + V[s+4, s+3] = J[0, 0]*J[0, 1] + V[s+4, s+4] = J[0, 0]*J[1, 1] + J[1, 0]*J[0, 1] + V[s+4, s+5] = J[1, 0]*J[1, 1] + V[s+5, s+3] = J[0, 1]*J[0, 1] + V[s+5, s+4] = 2*J[0, 1]*J[1, 1] + V[s+5, s+5] = J[1, 1]*J[1, 1] for e in range(3): v0id, v1id = [i for i in range(3) if i != e] # nhat . J^{-T} . t - foo = Sum(Product(Indexed(rns, (e, 0)), - Sum(Product(Indexed(J, (0, 0)), - Indexed(pts, (e, 0))), - Product(Indexed(J, (1, 0)), - Indexed(pts, (e, 1))))), - Product(Indexed(rns, (e, 1)), - Sum(Product(Indexed(J, (0, 1)), - Indexed(pts, (e, 0))), - Product(Indexed(J, (1, 1)), - Indexed(pts, (e, 1)))))) + foo = (rns[e, 0]*(J[0, 0]*pts[e, 0] + J[1, 0]*pts[e, 1]) + + rns[e, 1]*(J[0, 1]*pts[e, 0] + J[1, 1]*pts[e, 1])) # vertex points - V[18+e, 6*v0id] = Division( - Product(Literal(-1), Product(Literal(15), foo)), - Product(Literal(8), Indexed(pel, (e,)))) - V[18+e, 6*v1id] = Division( - Product(Literal(15), foo), - Product(Literal(8), Indexed(pel, (e,)))) + V[18+e, 6*v0id] = -15/8 * (foo / pel[e]) + V[18+e, 6*v1id] = 15/8 * (foo / pel[e]) # vertex derivatives for i in (0, 1): - V[18+e, 6*v0id+1+i] = Division( - Product(Literal(-1), - Product(Literal(7), - Product(foo, - Indexed(pts, (e, i))))), - Literal(16)) + V[18+e, 6*v0id+1+i] = -7/16*foo*pts[e, i] V[18+e, 6*v1id+1+i] = V[18+e, 6*v0id+1+i] # second derivatives - tau = [Power(Indexed(pts, (e, 0)), Literal(2)), - Product(Literal(2), - Product(Indexed(pts, (e, 0)), - Indexed(pts, (e, 1)))), - Power(Indexed(pts, (e, 1)), Literal(2))] + tau = [pts[e, 0]*pts[e, 0], + 2*pts[e, 0]*pts[e, 1], + pts[e, 1]*pts[e, 1]] for i in (0, 1, 2): - V[18+e, 6*v0id+3+i] = Division( - Product(Literal(-1), - Product(Indexed(pel, (e,)), - Product(foo, tau[i]))), - Literal(32)) - V[18+e, 6*v1id+3+i] = Division( - Product(Indexed(pel, (e,)), - Product(foo, tau[i])), - Literal(32)) - - V[18+e, 18+e] = Sum(Product(Indexed(rns, (e, 0)), - Sum(Product(Indexed(J, (0, 0)), - Indexed(pns, (e, 0))), - Product(Indexed(J, (1, 0)), - Indexed(pns, (e, 1))))), - Product(Indexed(rns, (e, 1)), - Sum(Product(Indexed(J, (0, 1)), - Indexed(pns, (e, 0))), - Product(Indexed(J, (1, 1)), - Indexed(pns, (e, 1)))))) + V[18+e, 6*v0id+3+i] = -1/32 * (pel[e]*foo*tau[i]) + V[18+e, 6*v1id+3+i] = 1/32 * (pel[e]*foo*tau[i]) + + V[18+e, 18+e] = (rns[e, 0]*(J[0, 0]*pns[e, 0] + J[1, 0]*pns[e, 1]) + + rns[e, 1]*(J[0, 1]*pns[e, 0] + J[1, 1]*pns[e, 1])) # Patch up conditioning h = coordinate_mapping.cell_size() @@ -125,20 +82,13 @@ def basis_transformation(self, coordinate_mapping): for v in range(3): for k in range(2): for i in range(21): - V[i, 6*v+1+k] = Division(V[i, 6*v+1+k], - Indexed(h, (v,))) + V[i, 6*v+1+k] = V[i, 6*v+1+k] / h[v] for k in range(3): for i in range(21): - V[i, 6*v+3+k] = Division(V[i, 6*v+3+k], - Power(Indexed(h, (v,)), - Literal(2))) + V[i, 6*v+3+k] = V[i, 6*v+3+k] / (h[v]*h[v]) for e in range(3): v0id, v1id = [i for i in range(3) if i != e] for i in range(21): - V[i, 18+e] = Division(V[i, 18+e], - Division( - Sum(Indexed(h, (v0id,)), - Indexed(h, (v1id,))), - Literal(2))) + V[i, 18+e] = 2*V[i, 18+e] / (h[v0id] + h[v1id]) return ListTensor(V.T) diff --git a/finat/bell.py b/finat/bell.py index 43fcfcfa8..9e0fbd03c 100644 --- a/finat/bell.py +++ b/finat/bell.py @@ -2,7 +2,7 @@ import FIAT -from gem import Division, Indexed, Literal, ListTensor, Power, Product, Sum +from gem import Literal, ListTensor from finat.fiat_elements import ScalarFiatElement from finat.physically_mapped import PhysicallyMappedElement, Citations @@ -36,88 +36,51 @@ def basis_transformation(self, coordinate_mapping): V[s, s] = Literal(1) for i in range(2): for j in range(2): - V[s+1+i, s+1+j] = Indexed(J, (j, i)) - V[s+3, s+3] = Power(Indexed(J, (0, 0)), Literal(2)) - V[s+3, s+4] = Product(Literal(2), - Product(Indexed(J, (0, 0)), - Indexed(J, (1, 0)))) - V[s+3, s+5] = Power(Indexed(J, (1, 0)), Literal(2)) - V[s+4, s+3] = Product(Indexed(J, (0, 0)), - Indexed(J, (0, 1))) - V[s+4, s+4] = Sum(Product(Indexed(J, (0, 0)), - Indexed(J, (1, 1))), - Product(Indexed(J, (1, 0)), - Indexed(J, (0, 1)))) - V[s+4, s+5] = Product(Indexed(J, (1, 0)), - Indexed(J, (1, 1))) - V[s+5, s+3] = Power(Indexed(J, (0, 1)), Literal(2)) - V[s+5, s+4] = Product(Literal(2), - Product(Indexed(J, (0, 1)), - Indexed(J, (1, 1)))) - V[s+5, s+5] = Power(Indexed(J, (1, 1)), Literal(2)) + V[s+1+i, s+1+j] = J[j, i] + V[s+3, s+3] = J[0, 0]*J[0, 0] + V[s+3, s+4] = 2*J[0, 0]*J[1, 0] + V[s+3, s+5] = J[1, 0]*J[1, 0] + V[s+4, s+3] = J[0, 0]*J[0, 1] + V[s+4, s+4] = J[0, 0]*J[1, 1] + J[1, 0]*J[0, 1] + V[s+4, s+5] = J[1, 0]*J[1, 1] + V[s+5, s+3] = J[0, 1]*J[0, 1] + V[s+5, s+4] = 2*J[0, 1]*J[1, 1] + V[s+5, s+5] = J[1, 1]*J[1, 1] for e in range(3): v0id, v1id = [i for i in range(3) if i != e] # nhat . J^{-T} . t - foo = Sum(Product(Indexed(rns, (e, 0)), - Sum(Product(Indexed(J, (0, 0)), - Indexed(pts, (e, 0))), - Product(Indexed(J, (1, 0)), - Indexed(pts, (e, 1))))), - Product(Indexed(rns, (e, 1)), - Sum(Product(Indexed(J, (0, 1)), - Indexed(pts, (e, 0))), - Product(Indexed(J, (1, 1)), - Indexed(pts, (e, 1)))))) + foo = (rns[e, 0]*(J[0, 0]*pts[e, 0] + J[1, 0]*pts[e, 1]) + + rns[e, 1]*(J[0, 1]*pts[e, 0] + J[1, 1]*pts[e, 1])) # vertex points - V[18+e, 6*v0id] = Division( - Product(Literal(-1), foo), - Product(Literal(21), Indexed(pel, (e,)))) - V[18+e, 6*v1id] = Division( - foo, - Product(Literal(21), Indexed(pel, (e,)))) + V[18+e, 6*v0id] = -1/21 * (foo / pel[e]) + V[18+e, 6*v1id] = 1/21 * (foo / pel[e]) # vertex derivatives for i in (0, 1): - V[18+e, 6*v0id+1+i] = Division( - Product(Literal(-1), - Product(foo, - Indexed(pts, (e, i)))), - Literal(42)) + V[18+e, 6*v0id+1+i] = -1/42*foo*pts[e, i] V[18+e, 6*v1id+1+i] = V[18+e, 6*v0id+1+i] # second derivatives - tau = [Power(Indexed(pts, (e, 0)), Literal(2)), - Product(Literal(2), - Product(Indexed(pts, (e, 0)), - Indexed(pts, (e, 1)))), - Power(Indexed(pts, (e, 1)), Literal(2))] + tau = [pts[e, 0]*pts[e, 0], + 2*pts[e, 0]*pts[e, 1], + pts[e, 1]*pts[e, 1]] for i in (0, 1, 2): - V[18+e, 6*v0id+3+i] = Division( - Product(Literal(-1), - Product(Indexed(pel, (e,)), - Product(foo, tau[i]))), - Literal(252)) - V[18+e, 6*v1id+3+i] = Division( - Product(Indexed(pel, (e,)), - Product(foo, tau[i])), - Literal(252)) + V[18+e, 6*v0id+3+i] = -1/252 * (pel[e]*foo*tau[i]) + V[18+e, 6*v1id+3+i] = 1/252 * (pel[e]*foo*tau[i]) h = coordinate_mapping.cell_size() for v in range(3): for k in range(2): for i in range(21): - V[i, 6*v+1+k] = Division(V[i, 6*v+1+k], - Indexed(h, (v,))) + V[i, 6*v+1+k] = V[i, 6*v+1+k] / h[v] for k in range(3): for i in range(21): - V[i, 6*v+3+k] = Division(V[i, 6*v+3+k], - Power(Indexed(h, (v,)), - Literal(2))) + V[i, 6*v+3+k] = V[i, 6*v+3+k] / (h[v]*h[v]) return ListTensor(V.T) diff --git a/finat/hermite.py b/finat/hermite.py index 9db8e8ece..6f453cda5 100644 --- a/finat/hermite.py +++ b/finat/hermite.py @@ -1,7 +1,7 @@ import numpy import FIAT -from gem import Indexed, Literal, ListTensor, Division +from gem import Literal, ListTensor from finat.fiat_elements import ScalarFiatElement from finat.physically_mapped import PhysicallyMappedElement, Citations @@ -24,12 +24,6 @@ def basis_transformation(self, coordinate_mapping): d = self.cell.get_dimension() numbf = self.space_dimension() - def n(J): - assert J.shape == (d, d) - return numpy.array( - [[Indexed(J, (i, j)) for j in range(d)] - for i in range(d)]) - M = numpy.eye(numbf, dtype=object) for multiindex in numpy.ndindex(M.shape): @@ -38,11 +32,10 @@ def n(J): cur = 0 for i in range(d+1): cur += 1 # skip the vertex - nJsi = n(Js[i]) + J = Js[i] for j in range(d): for k in range(d): - M[cur+j, cur+k] = Division(nJsi[j, k], - Indexed(h, (i,))) + M[cur+j, cur+k] = J[j, k] / h[i] cur += d return ListTensor(M) diff --git a/finat/morley.py b/finat/morley.py index 2dca7ce23..e8e4b0df9 100644 --- a/finat/morley.py +++ b/finat/morley.py @@ -2,7 +2,7 @@ import FIAT -from gem import Division, Indexed, Literal, ListTensor, Product, Sum +from gem import Literal, ListTensor from finat.fiat_elements import ScalarFiatElement from finat.physically_mapped import PhysicallyMappedElement, Citations @@ -27,46 +27,25 @@ def basis_transformation(self, coordinate_mapping): pel = coordinate_mapping.physical_edge_lengths() - B11 = [Sum(Product(Indexed(rns, (i, 0)), - Sum(Product(Indexed(pns, (i, 0)), - Indexed(J, (0, 0))), - Product(Indexed(pns, (i, 1)), - Indexed(J, (1, 0))))), - Product(Indexed(rns, (i, 1)), - Sum(Product(Indexed(pns, (i, 0)), - Indexed(J, (0, 1))), - Product(Indexed(pns, (i, 1)), - Indexed(J, (1, 1)))))) - for i in range(3)] - - B12 = [Sum(Product(Indexed(rns, (i, 0)), - Sum(Product(Indexed(pts, (i, 0)), - Indexed(J, (0, 0))), - Product(Indexed(pts, (i, 1)), - Indexed(J, (1, 0))))), - Product(Indexed(rns, (i, 1)), - Sum(Product(Indexed(pts, (i, 0)), - Indexed(J, (0, 1))), - Product(Indexed(pts, (i, 1)), - Indexed(J, (1, 1)))))) - for i in range(3)] - V = numpy.eye(6, dtype=object) for multiindex in numpy.ndindex(V.shape): V[multiindex] = Literal(V[multiindex]) for i in range(3): - V[i + 3, i + 3] = B11[i] + V[i+3, i+3] = (rns[i, 0]*(pns[i, 0]*J[0, 0] + pns[i, 1]*J[1, 0]) + + rns[i, 1]*(pns[i, 0]*J[0, 1] + pns[i, 1]*J[1, 1])) for i, c in enumerate([(1, 2), (0, 2), (0, 1)]): - V[3+i, c[0]] = Division(Product(Literal(-1), B12[i]), Indexed(pel, (i, ))) - V[3+i, c[1]] = Division(B12[i], Indexed(pel, (i, ))) + B12 = (rns[i, 0]*(pts[i, 0]*J[0, 0] + pts[i, 1]*J[1, 0]) + + rns[i, 1]*(pts[i, 0]*J[0, 1] + pts[i, 1]*J[1, 1])) + V[3+i, c[0]] = -1*B12 / pel[i] + V[3+i, c[1]] = B12 / pel[i] # diagonal post-scaling to patch up conditioning h = coordinate_mapping.cell_size() for j in range(3): for i in range(6): - V[i, 3+j] = Division(V[i, 3+j], Indexed(h, (j,))) + V[i, 3+j] = V[i, 3+j] / h[j] return ListTensor(V.T) diff --git a/finat/physically_mapped.py b/finat/physically_mapped.py index dbfde1fa3..fc1282b65 100644 --- a/finat/physically_mapped.py +++ b/finat/physically_mapped.py @@ -95,13 +95,8 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): M = self.basis_transformation(coordinate_mapping) def matvec(table): - i = gem.Index() - j = gem.Index() - val = gem.ComponentTensor( - gem.IndexSum(gem.Product(gem.Indexed(M, (i, j)), - gem.Indexed(table, (j,))), - (j,)), - (i,)) + i, j = gem.indices(2) + val = gem.ComponentTensor(gem.IndexSum(M[i, j]*table[j], (j,)), (i,)) # Eliminate zeros return gem.optimise.aggressive_unroll(val) From 11fb5645f63c72a671bf309d7b22538f48e14018 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Homolya?= Date: Fri, 13 Jul 2018 21:53:49 +0200 Subject: [PATCH 519/749] simplify interpreter --- gem/interpreter.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/gem/interpreter.py b/gem/interpreter.py index f06143b55..880ce491d 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -173,21 +173,16 @@ def _evaluate_operator(e, self): def _evaluate_mathfunction(e, self): ops = [self(o) for o in e.children] result = Result.empty(*ops) - names = {"abs": abs, - "log": math.log} + names = { + "abs": abs, + "log": math.log, + "real": operator.attrgetter("real"), + "imag": operator.attrgetter("imag"), + "conj": operator.methodcaller("conjugate"), + } op = names[e.name] - if e.name is 'imag': - for idx in numpy.ndindex(result.tshape): - result[idx] = [o[o.filter(idx, result.fids)].imag for o in ops] - elif e.name is 'conj': - for idx in numpy.ndindex(result.tshape): - result[idx] = [o[o.filter(idx, result.fids)].conjugate() for o in ops] - elif e.name is 'real': - for idx in numpy.ndindex(result.tshape): - result[idx] = [o[o.filter(idx, result.fids)].real for o in ops] - else: - for idx in numpy.ndindex(result.tshape): - result[idx] = op(*(o[o.filter(idx, result.fids)] for o in ops)) + for idx in numpy.ndindex(result.tshape): + result[idx] = op(*(o[o.filter(idx, result.fids)] for o in ops)) return result From 296063a815a784af284fc21476135b55411092b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Homolya?= Date: Fri, 13 Jul 2018 22:42:29 +0200 Subject: [PATCH 520/749] use complex-capable numpy.log --- gem/interpreter.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gem/interpreter.py b/gem/interpreter.py index 880ce491d..246870617 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -3,7 +3,6 @@ """ import numpy import operator -import math from collections import OrderedDict from functools import singledispatch import itertools @@ -175,7 +174,7 @@ def _evaluate_mathfunction(e, self): result = Result.empty(*ops) names = { "abs": abs, - "log": math.log, + "log": numpy.log, "real": operator.attrgetter("real"), "imag": operator.attrgetter("imag"), "conj": operator.methodcaller("conjugate"), From 036a5768a100e281de0c93189beed7fa7e026777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Homolya?= Date: Wed, 25 Jul 2018 11:05:19 +0100 Subject: [PATCH 521/749] eliminate FutureWarning --- gem/interpreter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gem/interpreter.py b/gem/interpreter.py index a7e2e7243..a95c3d9bd 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -60,7 +60,7 @@ def filter(self, idx, fids): return tuple(idx[fids.index(i)] for i in self.fids) + idx[len(fids):] def __getitem__(self, idx): - return self.arr[idx] + return self.arr[tuple(idx)] def __setitem__(self, idx, val): self.arr[idx] = val From 5ddd342b624a2512d434ac2b18ccd482a13e63a1 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Tue, 14 Aug 2018 13:47:10 -0500 Subject: [PATCH 522/749] Bug fix for dof re-scaling in Morley --- finat/morley.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/finat/morley.py b/finat/morley.py index e8e4b0df9..b8fc329b3 100644 --- a/finat/morley.py +++ b/finat/morley.py @@ -44,8 +44,9 @@ def basis_transformation(self, coordinate_mapping): # diagonal post-scaling to patch up conditioning h = coordinate_mapping.cell_size() - for j in range(3): + for e in range(3): + v0id, v1id = [i for i in range(3) if i != e] for i in range(6): - V[i, 3+j] = V[i, 3+j] / h[j] + V[i, 3+e] = 2*V[i, 3+e] / (h[v0id] + h[v1id]) return ListTensor(V.T) From c4cafd6fb53ab1f69da820a2c5bee4dde1d4dd8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Homolya?= Date: Thu, 25 Oct 2018 09:19:30 +0200 Subject: [PATCH 523/749] fix new flake8 rules --- gem/optimise.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index 21f4966e5..7716bc083 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -372,10 +372,10 @@ def make_rename_map(): def make_renamer(rename_map): - """Creates a function for renaming indices when expanding products of + r"""Creates a function for renaming indices when expanding products of IndexSums, i.e. applying to following rule: - (sum_i a_i)*(sum_i b_i) ===> \sum_{i,i'} a_i*b_{i'} + (\sum_i a_i)*(\sum_i b_i) ===> \sum_{i,i'} a_i*b_{i'} :arg rename_map: An rename map for renaming indices the same way as functions returned by other calls of this From 26862fb210996dd8c0625ce9ed2c4aed84277466 Mon Sep 17 00:00:00 2001 From: cyruscycheng21 Date: Wed, 13 Feb 2019 13:22:33 +0000 Subject: [PATCH 524/749] Added new class DP --- finat/fiat_elements.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index d2c9ae297..0d9c6b03c 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -292,6 +292,10 @@ class DiscontinuousLagrange(ScalarFiatElement): def __init__(self, cell, degree): super(DiscontinuousLagrange, self).__init__(FIAT.DiscontinuousLagrange(cell, degree)) +class DP(ScalarFiatElement): + def __init(self, cell, degree): + super(DP, self).__init__(FIAT.DP(cell, degree)) + class DiscontinuousTaylor(ScalarFiatElement): def __init__(self, cell, degree): From 8ceca8b1964ad71ce835b047e8ac655e618482b8 Mon Sep 17 00:00:00 2001 From: cyruscycheng21 Date: Thu, 14 Feb 2019 10:19:29 +0000 Subject: [PATCH 525/749] Added DPC to init file --- finat/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/finat/__init__.py b/finat/__init__.py index 44620a120..a6faf39b2 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -1,5 +1,6 @@ from .fiat_elements import Bubble, CrouzeixRaviart, DiscontinuousTaylor # noqa: F401 from .fiat_elements import Lagrange, DiscontinuousLagrange # noqa: F401 +from .fiat_elements import DPC from .fiat_elements import BrezziDouglasMarini, BrezziDouglasFortinMarini # noqa: F401 from .fiat_elements import Nedelec, NedelecSecondKind, RaviartThomas # noqa: F401 from .fiat_elements import HellanHerrmannJohnson, Regge # noqa: F401 From 24f6069e8454c6866df63257cd5cad3c0a2d703b Mon Sep 17 00:00:00 2001 From: cyruscycheng21 Date: Thu, 14 Feb 2019 10:22:33 +0000 Subject: [PATCH 526/749] Added DPC as a class --- finat/fiat_elements.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 0d9c6b03c..46f65fdc4 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -292,9 +292,9 @@ class DiscontinuousLagrange(ScalarFiatElement): def __init__(self, cell, degree): super(DiscontinuousLagrange, self).__init__(FIAT.DiscontinuousLagrange(cell, degree)) -class DP(ScalarFiatElement): - def __init(self, cell, degree): - super(DP, self).__init__(FIAT.DP(cell, degree)) +class DPC(ScalarFiatElement): + def __init__(self, cell, degree): + super(DPC, self).__init__(FIAT.DPC(cell, degree)) class DiscontinuousTaylor(ScalarFiatElement): From a4043a190a2aca7e4ecadeb4212810e200dc563d Mon Sep 17 00:00:00 2001 From: cyruscycheng21 Date: Tue, 19 Feb 2019 18:33:00 +0000 Subject: [PATCH 527/749] flake8 compliant --- finat/__init__.py | 2 +- finat/fiat_elements.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/finat/__init__.py b/finat/__init__.py index a6faf39b2..e4096533e 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -1,6 +1,6 @@ from .fiat_elements import Bubble, CrouzeixRaviart, DiscontinuousTaylor # noqa: F401 from .fiat_elements import Lagrange, DiscontinuousLagrange # noqa: F401 -from .fiat_elements import DPC +from .fiat_elements import DPC # noqa: F401 from .fiat_elements import BrezziDouglasMarini, BrezziDouglasFortinMarini # noqa: F401 from .fiat_elements import Nedelec, NedelecSecondKind, RaviartThomas # noqa: F401 from .fiat_elements import HellanHerrmannJohnson, Regge # noqa: F401 diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 46f65fdc4..8b08d2515 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -292,6 +292,7 @@ class DiscontinuousLagrange(ScalarFiatElement): def __init__(self, cell, degree): super(DiscontinuousLagrange, self).__init__(FIAT.DiscontinuousLagrange(cell, degree)) + class DPC(ScalarFiatElement): def __init__(self, cell, degree): super(DPC, self).__init__(FIAT.DPC(cell, degree)) From 51f444c21f168c92d13802d7e9919b69ac2d2104 Mon Sep 17 00:00:00 2001 From: David Ham Date: Thu, 21 Feb 2019 09:19:14 +0000 Subject: [PATCH 528/749] fix flake8 brain damage --- finat/argyris.py | 8 ++++---- finat/bell.py | 4 ++-- finat/morley.py | 8 ++++---- finat/physically_mapped.py | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/finat/argyris.py b/finat/argyris.py index 820c2c805..38713d947 100644 --- a/finat/argyris.py +++ b/finat/argyris.py @@ -52,8 +52,8 @@ def basis_transformation(self, coordinate_mapping): v0id, v1id = [i for i in range(3) if i != e] # nhat . J^{-T} . t - foo = (rns[e, 0]*(J[0, 0]*pts[e, 0] + J[1, 0]*pts[e, 1]) + - rns[e, 1]*(J[0, 1]*pts[e, 0] + J[1, 1]*pts[e, 1])) + foo = (rns[e, 0]*(J[0, 0]*pts[e, 0] + J[1, 0]*pts[e, 1]) + + rns[e, 1]*(J[0, 1]*pts[e, 0] + J[1, 1]*pts[e, 1])) # vertex points V[18+e, 6*v0id] = -15/8 * (foo / pel[e]) @@ -73,8 +73,8 @@ def basis_transformation(self, coordinate_mapping): V[18+e, 6*v0id+3+i] = -1/32 * (pel[e]*foo*tau[i]) V[18+e, 6*v1id+3+i] = 1/32 * (pel[e]*foo*tau[i]) - V[18+e, 18+e] = (rns[e, 0]*(J[0, 0]*pns[e, 0] + J[1, 0]*pns[e, 1]) + - rns[e, 1]*(J[0, 1]*pns[e, 0] + J[1, 1]*pns[e, 1])) + V[18+e, 18+e] = (rns[e, 0]*(J[0, 0]*pns[e, 0] + J[1, 0]*pns[e, 1]) + + rns[e, 1]*(J[0, 1]*pns[e, 0] + J[1, 1]*pns[e, 1])) # Patch up conditioning h = coordinate_mapping.cell_size() diff --git a/finat/bell.py b/finat/bell.py index 9e0fbd03c..561d6be35 100644 --- a/finat/bell.py +++ b/finat/bell.py @@ -51,8 +51,8 @@ def basis_transformation(self, coordinate_mapping): v0id, v1id = [i for i in range(3) if i != e] # nhat . J^{-T} . t - foo = (rns[e, 0]*(J[0, 0]*pts[e, 0] + J[1, 0]*pts[e, 1]) + - rns[e, 1]*(J[0, 1]*pts[e, 0] + J[1, 1]*pts[e, 1])) + foo = (rns[e, 0]*(J[0, 0]*pts[e, 0] + J[1, 0]*pts[e, 1]) + + rns[e, 1]*(J[0, 1]*pts[e, 0] + J[1, 1]*pts[e, 1])) # vertex points V[18+e, 6*v0id] = -1/21 * (foo / pel[e]) diff --git a/finat/morley.py b/finat/morley.py index b8fc329b3..73e060192 100644 --- a/finat/morley.py +++ b/finat/morley.py @@ -32,12 +32,12 @@ def basis_transformation(self, coordinate_mapping): V[multiindex] = Literal(V[multiindex]) for i in range(3): - V[i+3, i+3] = (rns[i, 0]*(pns[i, 0]*J[0, 0] + pns[i, 1]*J[1, 0]) + - rns[i, 1]*(pns[i, 0]*J[0, 1] + pns[i, 1]*J[1, 1])) + V[i+3, i+3] = (rns[i, 0]*(pns[i, 0]*J[0, 0] + pns[i, 1]*J[1, 0]) + + rns[i, 1]*(pns[i, 0]*J[0, 1] + pns[i, 1]*J[1, 1])) for i, c in enumerate([(1, 2), (0, 2), (0, 1)]): - B12 = (rns[i, 0]*(pts[i, 0]*J[0, 0] + pts[i, 1]*J[1, 0]) + - rns[i, 1]*(pts[i, 0]*J[0, 1] + pts[i, 1]*J[1, 1])) + B12 = (rns[i, 0]*(pts[i, 0]*J[0, 0] + pts[i, 1]*J[1, 0]) + + rns[i, 1]*(pts[i, 0]*J[0, 1] + pts[i, 1]*J[1, 1])) V[3+i, c[0]] = -1*B12 / pel[i] V[3+i, c[1]] = B12 / pel[i] diff --git a/finat/physically_mapped.py b/finat/physically_mapped.py index fc1282b65..f34d45571 100644 --- a/finat/physically_mapped.py +++ b/finat/physically_mapped.py @@ -42,7 +42,7 @@ doi = {10.1002/nme.1620010108} } """) - Citations().add("Ciarlet1972", """ + Citations().add("Ciarlet1972", r""" @Article{Ciarlet1972, author = {P. G. Ciarlet and P. A. Raviart}, title = {{General Lagrange and Hermite interpolation in From ef276ffe46786ce85a276fec72db4f1c1adb709d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Homolya?= Date: Thu, 28 Feb 2019 11:15:14 +0100 Subject: [PATCH 529/749] [gem] restrict Conditional nodes to scalar shape --- gem/gem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gem/gem.py b/gem/gem.py index 1b889435b..9f6f3752a 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -408,7 +408,7 @@ class Conditional(Node): def __new__(cls, condition, then, else_): assert not condition.shape - assert then.shape == else_.shape + assert then.shape == else_.shape == () # If both branches are the same, just return one of them. In # particular, this will help constant-fold zeros. From 92a29ecadfb3ffbf070bba980a9e54235ce19ed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Homolya?= Date: Fri, 1 Mar 2019 10:07:25 +0100 Subject: [PATCH 530/749] Fix refactorisation of Conditional nodes Resolves #183. --- gem/refactorise.py | 45 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/gem/refactorise.py b/gem/refactorise.py index 1c65d11dd..f83bbfd84 100644 --- a/gem/refactorise.py +++ b/gem/refactorise.py @@ -2,14 +2,16 @@ refactorisation.""" from collections import Counter, OrderedDict, defaultdict, namedtuple +from functools import singledispatch from itertools import product from sys import intern from gem.node import Memoizer, traversal -from gem.gem import Node, Zero, Product, Sum, Indexed, ListTensor, one +from gem.gem import (Node, Conditional, Zero, Product, Sum, Indexed, + ListTensor, one) from gem.optimise import (remove_componenttensors, sum_factorise, traverse_product, traverse_sum, unroll_indexsum, - expand_conditional, make_rename_map, make_renamer) + make_rename_map, make_renamer) # Refactorisation labels @@ -127,6 +129,7 @@ class FactorisationError(Exception): pass +@singledispatch def _collect_monomials(expression, self): """Refactorises an expression into a sum-of-products form, using distributivity rules (i.e. a*(b + c) -> a*b + a*c). Expansion @@ -166,7 +169,7 @@ def stop_at(expr): sums = [] for expr in compounds: summands = traverse_sum(expr, stop_at=stop_at) - if len(summands) <= 1: + if len(summands) <= 1 and not isinstance(expr, Conditional): # Compound term is not an addition, avoid infinite # recursion and fail gracefully raising an exception. raise FactorisationError(expr) @@ -208,6 +211,38 @@ def stop_at(expr): return result +@_collect_monomials.register(Conditional) +def _collect_monomials_conditional(expression, self): + """Refactorises a conditional expression into a sum-of-products form, + pulling only "atomics" out of conditional expressions. + + :arg expression: a GEM expression to refactorise + :arg self: function for recursive calls + + :returns: :py:class:`MonomialSum` + """ + condition, then, else_ = expression.children + # Recursively refactorise both branches to `MonomialSum`s + then_ms = self(then) + else_ms = self(else_) + + result = MonomialSum() + # For each set of atomics, create a new Conditional node. Atomics + # are considered safe to be pulled out of conditionals, but other + # expressions remain inside conditional branches. + zero = Zero() + for k in then_ms.monomials.keys() | else_ms.monomials.keys(): + _then = then_ms.monomials.get(k, zero) + _else = else_ms.monomials.get(k, zero) + result.monomials[k] = Conditional(condition, _then, _else) + + # Construct a deterministic ordering + result.ordering = then_ms.ordering.copy() + for k, v in else_ms.ordering.items(): + result.ordering.setdefault(k, v) + return result + + def collect_monomials(expressions, classifier): """Refactorises expressions into a sum-of-products form, using distributivity rules (i.e. a*(b + c) -> a*b + a*c). Expansion @@ -239,10 +274,6 @@ def collect_monomials(expressions, classifier): predicate=lambda i: i in must_unroll) expressions = remove_componenttensors(expressions) - # Expand Conditional nodes which are COMPOUND - conditional_predicate = lambda node: classifier(node) == COMPOUND - expressions = expand_conditional(expressions, conditional_predicate) - # Finally, refactorise expressions mapper = Memoizer(_collect_monomials) mapper.classifier = classifier From ae96fbf617d858555cd9e872e850452100295914 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Homolya?= Date: Fri, 1 Mar 2019 10:08:18 +0100 Subject: [PATCH 531/749] remove unsafe conditional substitution --- gem/optimise.py | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index 7716bc083..f55d3353c 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -625,37 +625,3 @@ def aggressive_unroll(expression): expression, = unroll_indexsum((expression,), predicate=lambda index: True) expression, = remove_componenttensors((expression,)) return expression - - -@singledispatch -def _expand_conditional(node, self): - raise AssertionError("cannot handle type %s" % type(node)) - - -_expand_conditional.register(Node)(reuse_if_untouched) - - -@_expand_conditional.register(Conditional) -def _expand_conditional_conditional(node, self): - if self.predicate(node): - condition, then, else_ = map(self, node.children) - return Sum(Product(Conditional(condition, one, Zero()), then), - Product(Conditional(condition, Zero(), one), else_)) - else: - return reuse_if_untouched(node, self) - - -def expand_conditional(expressions, predicate): - """Applies the following substitution rule on selected :py:class:`Conditional`s: - - Conditional(a, b, c) => Conditional(a, 1, 0)*b + Conditional(a, 0, 1)*c - - :arg expressions: expression DAG roots - :arg predicate: a predicate function on :py:class:`Conditional`s to determine - whether to apply the substitution rule or not - - :returns: expression DAG roots with some :py:class:`Conditional` nodes expanded - """ - mapper = Memoizer(_expand_conditional) - mapper.predicate = predicate - return list(map(mapper, expressions)) From eddff1e2e9b599b000f52ba023e011ab88149220 Mon Sep 17 00:00:00 2001 From: cyruscycheng21 Date: Tue, 5 Mar 2019 22:08:58 +0000 Subject: [PATCH 532/749] flake8 complaint --- finat/argyris.py | 8 ++++---- finat/bell.py | 4 ++-- finat/morley.py | 8 ++++---- finat/physically_mapped.py | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/finat/argyris.py b/finat/argyris.py index 820c2c805..38713d947 100644 --- a/finat/argyris.py +++ b/finat/argyris.py @@ -52,8 +52,8 @@ def basis_transformation(self, coordinate_mapping): v0id, v1id = [i for i in range(3) if i != e] # nhat . J^{-T} . t - foo = (rns[e, 0]*(J[0, 0]*pts[e, 0] + J[1, 0]*pts[e, 1]) + - rns[e, 1]*(J[0, 1]*pts[e, 0] + J[1, 1]*pts[e, 1])) + foo = (rns[e, 0]*(J[0, 0]*pts[e, 0] + J[1, 0]*pts[e, 1]) + + rns[e, 1]*(J[0, 1]*pts[e, 0] + J[1, 1]*pts[e, 1])) # vertex points V[18+e, 6*v0id] = -15/8 * (foo / pel[e]) @@ -73,8 +73,8 @@ def basis_transformation(self, coordinate_mapping): V[18+e, 6*v0id+3+i] = -1/32 * (pel[e]*foo*tau[i]) V[18+e, 6*v1id+3+i] = 1/32 * (pel[e]*foo*tau[i]) - V[18+e, 18+e] = (rns[e, 0]*(J[0, 0]*pns[e, 0] + J[1, 0]*pns[e, 1]) + - rns[e, 1]*(J[0, 1]*pns[e, 0] + J[1, 1]*pns[e, 1])) + V[18+e, 18+e] = (rns[e, 0]*(J[0, 0]*pns[e, 0] + J[1, 0]*pns[e, 1]) + + rns[e, 1]*(J[0, 1]*pns[e, 0] + J[1, 1]*pns[e, 1])) # Patch up conditioning h = coordinate_mapping.cell_size() diff --git a/finat/bell.py b/finat/bell.py index 9e0fbd03c..561d6be35 100644 --- a/finat/bell.py +++ b/finat/bell.py @@ -51,8 +51,8 @@ def basis_transformation(self, coordinate_mapping): v0id, v1id = [i for i in range(3) if i != e] # nhat . J^{-T} . t - foo = (rns[e, 0]*(J[0, 0]*pts[e, 0] + J[1, 0]*pts[e, 1]) + - rns[e, 1]*(J[0, 1]*pts[e, 0] + J[1, 1]*pts[e, 1])) + foo = (rns[e, 0]*(J[0, 0]*pts[e, 0] + J[1, 0]*pts[e, 1]) + + rns[e, 1]*(J[0, 1]*pts[e, 0] + J[1, 1]*pts[e, 1])) # vertex points V[18+e, 6*v0id] = -1/21 * (foo / pel[e]) diff --git a/finat/morley.py b/finat/morley.py index b8fc329b3..73e060192 100644 --- a/finat/morley.py +++ b/finat/morley.py @@ -32,12 +32,12 @@ def basis_transformation(self, coordinate_mapping): V[multiindex] = Literal(V[multiindex]) for i in range(3): - V[i+3, i+3] = (rns[i, 0]*(pns[i, 0]*J[0, 0] + pns[i, 1]*J[1, 0]) + - rns[i, 1]*(pns[i, 0]*J[0, 1] + pns[i, 1]*J[1, 1])) + V[i+3, i+3] = (rns[i, 0]*(pns[i, 0]*J[0, 0] + pns[i, 1]*J[1, 0]) + + rns[i, 1]*(pns[i, 0]*J[0, 1] + pns[i, 1]*J[1, 1])) for i, c in enumerate([(1, 2), (0, 2), (0, 1)]): - B12 = (rns[i, 0]*(pts[i, 0]*J[0, 0] + pts[i, 1]*J[1, 0]) + - rns[i, 1]*(pts[i, 0]*J[0, 1] + pts[i, 1]*J[1, 1])) + B12 = (rns[i, 0]*(pts[i, 0]*J[0, 0] + pts[i, 1]*J[1, 0]) + + rns[i, 1]*(pts[i, 0]*J[0, 1] + pts[i, 1]*J[1, 1])) V[3+i, c[0]] = -1*B12 / pel[i] V[3+i, c[1]] = B12 / pel[i] diff --git a/finat/physically_mapped.py b/finat/physically_mapped.py index fc1282b65..f34d45571 100644 --- a/finat/physically_mapped.py +++ b/finat/physically_mapped.py @@ -42,7 +42,7 @@ doi = {10.1002/nme.1620010108} } """) - Citations().add("Ciarlet1972", """ + Citations().add("Ciarlet1972", r""" @Article{Ciarlet1972, author = {P. G. Ciarlet and P. A. Raviart}, title = {{General Lagrange and Hermite interpolation in From e3eead3e6cf19cb3bd0402f0320006be0fcaaaf6 Mon Sep 17 00:00:00 2001 From: cyruscycheng21 Date: Fri, 29 Mar 2019 15:17:59 +0000 Subject: [PATCH 533/749] registered serendipity element --- finat/__init__.py | 3 ++- finat/fiat_elements.py | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/finat/__init__.py b/finat/__init__.py index e4096533e..b112746c3 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -1,6 +1,6 @@ from .fiat_elements import Bubble, CrouzeixRaviart, DiscontinuousTaylor # noqa: F401 from .fiat_elements import Lagrange, DiscontinuousLagrange # noqa: F401 -from .fiat_elements import DPC # noqa: F401 +from .fiat_elements import DPC, Serendipity # noqa: F401 from .fiat_elements import BrezziDouglasMarini, BrezziDouglasFortinMarini # noqa: F401 from .fiat_elements import Nedelec, NedelecSecondKind, RaviartThomas # noqa: F401 from .fiat_elements import HellanHerrmannJohnson, Regge # noqa: F401 @@ -9,6 +9,7 @@ from .bell import Bell # noqa: F401 from .hermite import Hermite # noqa: F401 from .morley import Morley # noqa: F401 +#from .serendipity import Serendipity # noqa: F401 from .trace import HDivTrace # noqa: F401 from .spectral import GaussLobattoLegendre, GaussLegendre # noqa: F401 from .tensorfiniteelement import TensorFiniteElement # noqa: F401 diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 8b08d2515..5cb680534 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -293,6 +293,11 @@ def __init__(self, cell, degree): super(DiscontinuousLagrange, self).__init__(FIAT.DiscontinuousLagrange(cell, degree)) +class Serendipity(ScalarFiatElement): + def __init__(self, cell, degree): + super(Serendipity, self).__init__(FIAT.Serendipity(cell, degree)) + + class DPC(ScalarFiatElement): def __init__(self, cell, degree): super(DPC, self).__init__(FIAT.DPC(cell, degree)) From 5a1f7203deb71d8e6fcf43e91eaf310d76924b19 Mon Sep 17 00:00:00 2001 From: cyruscycheng21 Date: Fri, 12 Apr 2019 18:48:33 +0100 Subject: [PATCH 534/749] removed derivative = degree test --- finat/fiat_elements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 5cb680534..83f2ed37a 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -90,7 +90,7 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): )) elif derivative == self.degree: # Make sure numerics satisfies theory - assert np.allclose(table, table.mean(axis=0, keepdims=True)) + # assert np.allclose(table, table.mean(axis=0, keepdims=True)) exprs.append(gem.Literal(table[0])) else: # Make sure numerics satisfies theory From f82cc857e9255e66869d26072fa9af533f2494f4 Mon Sep 17 00:00:00 2001 From: cyruscycheng21 Date: Mon, 29 Apr 2019 12:21:38 +0100 Subject: [PATCH 535/749] flake8 compliant --- finat/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/finat/__init__.py b/finat/__init__.py index b112746c3..5d19003e6 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -9,7 +9,6 @@ from .bell import Bell # noqa: F401 from .hermite import Hermite # noqa: F401 from .morley import Morley # noqa: F401 -#from .serendipity import Serendipity # noqa: F401 from .trace import HDivTrace # noqa: F401 from .spectral import GaussLobattoLegendre, GaussLegendre # noqa: F401 from .tensorfiniteelement import TensorFiniteElement # noqa: F401 From 2ddb210ca5044be567af273a5686d0b2a4d3316f Mon Sep 17 00:00:00 2001 From: cyruscycheng21 Date: Mon, 29 Apr 2019 13:31:43 +0100 Subject: [PATCH 536/749] removed assertion --- finat/fiat_elements.py | 1 - 1 file changed, 1 deletion(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 83f2ed37a..b230d788c 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -90,7 +90,6 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): )) elif derivative == self.degree: # Make sure numerics satisfies theory - # assert np.allclose(table, table.mean(axis=0, keepdims=True)) exprs.append(gem.Literal(table[0])) else: # Make sure numerics satisfies theory From f7318be8155dab26fa3998031267698e7e84cf01 Mon Sep 17 00:00:00 2001 From: David Ham Date: Wed, 1 May 2019 10:46:49 +0100 Subject: [PATCH 537/749] Remove apparently spurious asserts --- finat/spectral.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/finat/spectral.py b/finat/spectral.py index 292db805c..933c00ff5 100644 --- a/finat/spectral.py +++ b/finat/spectral.py @@ -21,7 +21,7 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): :param ps: the point set. :param entity: the cell entity on which to tabulate. ''' - assert coordinate_mapping is None + result = super(GaussLobattoLegendre, self).basis_evaluation(order, ps, entity) cell_dimension = self.cell.get_dimension() if entity is None or entity == (cell_dimension, 0): # on cell interior @@ -50,6 +50,7 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): :param ps: the point set. :param entity: the cell entity on which to tabulate. ''' + assert coordinate_mapping is None result = super(GaussLegendre, self).basis_evaluation(order, ps, entity) cell_dimension = self.cell.get_dimension() From 70e4e42cb75160ee0eab3a89617dfe1610459e4d Mon Sep 17 00:00:00 2001 From: David Ham Date: Thu, 2 May 2019 13:50:04 +0100 Subject: [PATCH 538/749] Remove overenthusiastic assert --- finat/spectral.py | 1 - 1 file changed, 1 deletion(-) diff --git a/finat/spectral.py b/finat/spectral.py index 933c00ff5..f8fec09b8 100644 --- a/finat/spectral.py +++ b/finat/spectral.py @@ -51,7 +51,6 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): :param entity: the cell entity on which to tabulate. ''' - assert coordinate_mapping is None result = super(GaussLegendre, self).basis_evaluation(order, ps, entity) cell_dimension = self.cell.get_dimension() if entity is None or entity == (cell_dimension, 0): # on cell interior From bcdfe754c7f3044229630e1836ffdcdf9a032661 Mon Sep 17 00:00:00 2001 From: cyruscycheng21 Date: Tue, 6 Aug 2019 10:32:00 +0100 Subject: [PATCH 539/749] register bdmc element --- finat/__init__.py | 2 +- finat/fiat_elements.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/finat/__init__.py b/finat/__init__.py index 5d19003e6..cc54566ef 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -1,6 +1,6 @@ from .fiat_elements import Bubble, CrouzeixRaviart, DiscontinuousTaylor # noqa: F401 from .fiat_elements import Lagrange, DiscontinuousLagrange # noqa: F401 -from .fiat_elements import DPC, Serendipity # noqa: F401 +from .fiat_elements import DPC, Serendipity, BrezziDouglasMariniCubeEdge, BrezziDouglasMariniCubeFace # noqa: F401 from .fiat_elements import BrezziDouglasMarini, BrezziDouglasFortinMarini # noqa: F401 from .fiat_elements import Nedelec, NedelecSecondKind, RaviartThomas # noqa: F401 from .fiat_elements import HellanHerrmannJohnson, Regge # noqa: F401 diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index b230d788c..28001fb6c 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -323,6 +323,16 @@ def __init__(self, cell, degree): super(BrezziDouglasMarini, self).__init__(FIAT.BrezziDouglasMarini(cell, degree)) +class BrezziDouglasMariniCubeEdge(VectorFiatElement): + def __init__(self, cell, degree): + super(BrezziDouglasMariniCubeEdge, self).__init__(FIAT.BrezziDouglasMariniCubeEdge(cell, degree)) + + +class BrezziDouglasMariniCubeFace(VectorFiatElement): + def __init__(self, cell, degree): + super(BrezziDouglasMariniCubeFace, self).__init__(FIAT.BrezziDouglasMariniCubeFace(cell, degree)) + + class BrezziDouglasFortinMarini(VectorFiatElement): def __init__(self, cell, degree): super(BrezziDouglasFortinMarini, self).__init__(FIAT.BrezziDouglasFortinMarini(cell, degree)) From 9df23af25a856440447f375ecc7fdbb58988b6c1 Mon Sep 17 00:00:00 2001 From: Patrick Farrell Date: Tue, 20 Aug 2019 20:20:42 -0230 Subject: [PATCH 540/749] First stab at Arnold-Awanou-Winther element (2D only, not dealt with linearity of sigma_nn on edges yet) --- finat/__init__.py | 1 + finat/aaw.py | 55 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 finat/aaw.py diff --git a/finat/__init__.py b/finat/__init__.py index 5d19003e6..f6d374de6 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -9,6 +9,7 @@ from .bell import Bell # noqa: F401 from .hermite import Hermite # noqa: F401 from .morley import Morley # noqa: F401 +from .aaw import ArnoldAwanouWinther # noqa: F401 from .trace import HDivTrace # noqa: F401 from .spectral import GaussLobattoLegendre, GaussLegendre # noqa: F401 from .tensorfiniteelement import TensorFiniteElement # noqa: F401 diff --git a/finat/aaw.py b/finat/aaw.py new file mode 100644 index 000000000..64117af42 --- /dev/null +++ b/finat/aaw.py @@ -0,0 +1,55 @@ +import numpy + +import FIAT + +from gem import Literal, ListTensor + +from finat.fiat_elements import FiatElement +from finat.physically_mapped import PhysicallyMappedElement, Citations + + +class ArnoldAwanouWinther(FiatElement): + def __init__(self, cell, degree): + super(ArnoldAwanouWinther, self).__init__(FIAT.ArnoldAwanouWinther(cell, degree)) + + + +""" +class ArnoldAwanouWinther(PhysicallyMappedElement, FiatElement): + def __init__(self, cell, degree): + if degree != 2: + raise ValueError("Degree must be 2 for Arnold-Awanou-Winther element") + + super().__init__(FIAT.ArnoldAwanouWinther(cell, degree)) + + def basis_transformation(self, coordinate_mapping): + V = numpy.zeros((18, 15), dtype=object) + + for i in range(18): + for j in range(15): + V[i, j] = Literal(0) + + for i in range(15): + V[i, i] = Literal(1) + + return ListTensor(V.T) + + # This wipes out the constraint dofs. FIAT gives a 18 DOF element + # because we need some extra functions to enforce that sigma_nn + # is linear on each edge. + # However, we only have a 15 DOF element. + def entity_dofs(self): + return {0: {0: [], + 1: [], + 2: []}, + 1: {0: [0, 1, 2, 3], 1: [4, 5, 6, 7], 2: [8, 9, 10, 11]}, + 2: {0: [12, 13, 14]}} + + @property + def index_shape(self): + import ipdb; ipdb.set_trace() + return (15,) + + def space_dimension(self): + return 15 +""" From 6cbe318a73dfa7f46608af00a89fdc1f9920b759 Mon Sep 17 00:00:00 2001 From: Patrick Farrell Date: Tue, 20 Aug 2019 20:44:46 -0230 Subject: [PATCH 541/749] GEM is hard to debug. --- finat/aaw.py | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/finat/aaw.py b/finat/aaw.py index 64117af42..af10144d9 100644 --- a/finat/aaw.py +++ b/finat/aaw.py @@ -8,36 +8,23 @@ from finat.physically_mapped import PhysicallyMappedElement, Citations -class ArnoldAwanouWinther(FiatElement): - def __init__(self, cell, degree): - super(ArnoldAwanouWinther, self).__init__(FIAT.ArnoldAwanouWinther(cell, degree)) - - - -""" class ArnoldAwanouWinther(PhysicallyMappedElement, FiatElement): def __init__(self, cell, degree): - if degree != 2: - raise ValueError("Degree must be 2 for Arnold-Awanou-Winther element") + super(ArnoldAwanouWinther, self).__init__(FIAT.ArnoldAwanouWinther(cell, degree)) - super().__init__(FIAT.ArnoldAwanouWinther(cell, degree)) def basis_transformation(self, coordinate_mapping): V = numpy.zeros((18, 15), dtype=object) - for i in range(18): - for j in range(15): - V[i, j] = Literal(0) + for multiindex in numpy.ndindex(V.shape): + V[multiindex] = Literal(V[multiindex]) for i in range(15): V[i, i] = Literal(1) return ListTensor(V.T) - # This wipes out the constraint dofs. FIAT gives a 18 DOF element - # because we need some extra functions to enforce that sigma_nn - # is linear on each edge. - # However, we only have a 15 DOF element. + def entity_dofs(self): return {0: {0: [], 1: [], @@ -45,11 +32,11 @@ def entity_dofs(self): 1: {0: [0, 1, 2, 3], 1: [4, 5, 6, 7], 2: [8, 9, 10, 11]}, 2: {0: [12, 13, 14]}} + @property def index_shape(self): - import ipdb; ipdb.set_trace() return (15,) + def space_dimension(self): return 15 -""" From 6ac647343526f15671dbc39ad4a05a3e42c36877 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Wed, 21 Aug 2019 18:18:23 +0100 Subject: [PATCH 542/749] Handle elements with value_shape in physically-mapped transformations --- finat/fiat_elements.py | 4 +++- finat/physically_mapped.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index b230d788c..fd3c5f978 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -96,7 +96,9 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): assert np.allclose(table, 0.0) exprs.append(gem.Zero(self.index_shape)) if self.value_shape: - beta = self.get_indices() + # As above, this extent may be different from that advertised by the finat element. + assert len(self.get_indices()) == 1, "Was not expecting more than one index" + beta = (gem.Index(extent=self._element.space_dimension()), ) zeta = self.get_value_indices() result[alpha] = gem.ComponentTensor( gem.Indexed( diff --git a/finat/physically_mapped.py b/finat/physically_mapped.py index f34d45571..cbfd5917d 100644 --- a/finat/physically_mapped.py +++ b/finat/physically_mapped.py @@ -96,7 +96,9 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): def matvec(table): i, j = gem.indices(2) - val = gem.ComponentTensor(gem.IndexSum(M[i, j]*table[j], (j,)), (i,)) + value_indices = self.get_value_indices() + table = gem.Indexed(table, (j, ) + value_indices) + val = gem.ComponentTensor(gem.IndexSum(M[i, j]*table, (j,)), (i,) + value_indices) # Eliminate zeros return gem.optimise.aggressive_unroll(val) From cace96b3b158e266e41f77bd64ccb88435596d85 Mon Sep 17 00:00:00 2001 From: Patrick Farrell Date: Wed, 2 Oct 2019 16:47:56 +0100 Subject: [PATCH 543/749] A first stab at transformation, not working --- finat/aaw.py | 33 ++++++++++++++++++++++++++++++++- finat/physically_mapped.py | 8 ++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/finat/aaw.py b/finat/aaw.py index af10144d9..b8a24b184 100644 --- a/finat/aaw.py +++ b/finat/aaw.py @@ -19,7 +19,38 @@ def basis_transformation(self, coordinate_mapping): for multiindex in numpy.ndindex(V.shape): V[multiindex] = Literal(V[multiindex]) - for i in range(15): + for i in range(0, 12, 2): + V[i, i] = Literal(1) + + + detJ = coordinate_mapping.detJ_at([1/3, 1/3]) + J = coordinate_mapping.jacobian_at([1/3, 1/3]) + J_np = numpy.array([[J[0, 0], J[0, 1]], + [J[1, 0], J[1, 1]]]) + JTJ = J_np.T @ J_np + rts = coordinate_mapping.reference_edge_tangents() + rns = coordinate_mapping.reference_normals() + + for e in range(3): + # update rows 1,3 for edge 0, + # rows 5,7 for edge 1, + # rows 9, 11 for edge 2. + + # Compute alpha and beta for the edge. + Ghat = numpy.array([[rns[e, 0], rts[e, 0]], + [rns[e, 1], rts[e, 1]]]) + that = numpy.array([rts[e, 0], rts[e, 1]]) + (alpha, beta) = Ghat @ JTJ @ that / detJ + + # Stuff into the right rows and columns. + (idx1, idx2) = (4*e + 1, 4*e + 3) + V[idx1, idx1-1] = Literal(-1) * alpha / beta + V[idx1, idx1] = Literal(1) / beta + V[idx2, idx2-1] = Literal(-1) * alpha / beta + V[idx2, idx2] = Literal(1) / beta + + # internal dofs + for i in range(12, 15): V[i, i] = Literal(1) return ListTensor(V.T) diff --git a/finat/physically_mapped.py b/finat/physically_mapped.py index cbfd5917d..eed976070 100644 --- a/finat/physically_mapped.py +++ b/finat/physically_mapped.py @@ -128,6 +128,14 @@ def jacobian_at(self, point): :returns: A GEM expression for the Jacobian, shape (gdim, tdim). """ + @abstractmethod + def detJ_at(self, point): + """The determinant of the jacobian of the physical coordinates at a point. + + :arg point: The point in reference space to evaluate the Jacobian determinant. + :returns: A GEM expression for the Jacobian determinant. + """ + @abstractmethod def reference_normals(self): """The (unit) reference cell normals for each facet. From 88fbfacabea89dae6bb15ce7168eff43d0df6e77 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Wed, 2 Oct 2019 16:02:13 -0500 Subject: [PATCH 544/749] seems to work --- finat/aaw.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/finat/aaw.py b/finat/aaw.py index b8a24b184..07d44d1dd 100644 --- a/finat/aaw.py +++ b/finat/aaw.py @@ -22,25 +22,24 @@ def basis_transformation(self, coordinate_mapping): for i in range(0, 12, 2): V[i, i] = Literal(1) + T = self.cell + + # This bypasses the GEM wrapper. + that = numpy.array([T.compute_normalized_edge_tangent(i) for i in range(3)]) + nhat = numpy.array([T.compute_normal(i) for i in range(3)]) detJ = coordinate_mapping.detJ_at([1/3, 1/3]) J = coordinate_mapping.jacobian_at([1/3, 1/3]) J_np = numpy.array([[J[0, 0], J[0, 1]], [J[1, 0], J[1, 1]]]) JTJ = J_np.T @ J_np - rts = coordinate_mapping.reference_edge_tangents() - rns = coordinate_mapping.reference_normals() for e in range(3): - # update rows 1,3 for edge 0, - # rows 5,7 for edge 1, - # rows 9, 11 for edge 2. - + # Compute alpha and beta for the edge. - Ghat = numpy.array([[rns[e, 0], rts[e, 0]], - [rns[e, 1], rts[e, 1]]]) - that = numpy.array([rts[e, 0], rts[e, 1]]) - (alpha, beta) = Ghat @ JTJ @ that / detJ + Ghat = numpy.array([nhat[e, :], that[e, :]]) + + (alpha, beta) = Ghat @ JTJ @ that[e,:] / detJ # Stuff into the right rows and columns. (idx1, idx2) = (4*e + 1, 4*e + 3) From 7c80b663c276101cd610c0c65b3ddf9f22b14734 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Wed, 20 Nov 2019 14:40:44 -0600 Subject: [PATCH 545/749] relable variable in AAW, add (nonworking) MTW --- finat/__init__.py | 1 + finat/aaw.py | 4 +-- finat/mtw.py | 67 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 finat/mtw.py diff --git a/finat/__init__.py b/finat/__init__.py index f6d374de6..2b973e383 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -8,6 +8,7 @@ from .argyris import Argyris # noqa: F401 from .bell import Bell # noqa: F401 from .hermite import Hermite # noqa: F401 +from .mtw import MardalTaiWinther # noqa: F401 from .morley import Morley # noqa: F401 from .aaw import ArnoldAwanouWinther # noqa: F401 from .trace import HDivTrace # noqa: F401 diff --git a/finat/aaw.py b/finat/aaw.py index 07d44d1dd..c03e582d5 100644 --- a/finat/aaw.py +++ b/finat/aaw.py @@ -37,9 +37,9 @@ def basis_transformation(self, coordinate_mapping): for e in range(3): # Compute alpha and beta for the edge. - Ghat = numpy.array([nhat[e, :], that[e, :]]) + Ghat_T = numpy.array([nhat[e, :], that[e, :]]) - (alpha, beta) = Ghat @ JTJ @ that[e,:] / detJ + (alpha, beta) = Ghat_T @ JTJ @ that[e,:] / detJ # Stuff into the right rows and columns. (idx1, idx2) = (4*e + 1, 4*e + 3) diff --git a/finat/mtw.py b/finat/mtw.py new file mode 100644 index 000000000..45830db5a --- /dev/null +++ b/finat/mtw.py @@ -0,0 +1,67 @@ +import numpy + +import FIAT + +from gem import Literal, ListTensor + +from finat.fiat_elements import FiatElement +from finat.physically_mapped import PhysicallyMappedElement, Citations + + +class MardalTaiWinther(PhysicallyMappedElement, FiatElement): + def __init__(self, cell, degree): + super(MardalTaiWinther, self).__init__(FIAT.MardalTaiWinther(cell, degree)) + + + def basis_transformation(self, coordinate_mapping): + V = numpy.zeros((20, 9), dtype=object) + + for multiindex in numpy.ndindex(V.shape): + V[multiindex] = Literal(V[multiindex]) + + for i in range(0, 9, 3): + V[i, i] = Literal(1) + V[i+2, i+2] = Literal(1) + + T = self.cell + + # This bypasses the GEM wrapper. + that = numpy.array([T.compute_normalized_edge_tangent(i) for i in range(3)]) + nhat = numpy.array([T.compute_normal(i) for i in range(3)]) + + detJ = coordinate_mapping.detJ_at([1/3, 1/3]) + J = coordinate_mapping.jacobian_at([1/3, 1/3]) + J_np = numpy.array([[J[0, 0], J[0, 1]], + [J[1, 0], J[1, 1]]]) + JTJ = J_np.T @ J_np + + for e in range(3): + + # Compute alpha and beta for the edge. + Ghat_T = numpy.array([nhat[e, :], that[e, :]]) + + (alpha, beta) = Ghat_T @ JTJ @ that[e,:] / detJ + + # Stuff into the right rows and columns. + idx = 3*e + 1 + V[idx, idx-1] = Literal(-1) * alpha / beta + V[idx, idx] = Literal(1) / beta + + return ListTensor(V.T) + + + def entity_dofs(self): + return {0: {0: [], + 1: [], + 2: []}, + 1: {0: [0, 1, 2], 1: [3, 4, 5], 2: [6, 7, 8]}, + 2: {0: []}} + + + @property + def index_shape(self): + return (9,) + + + def space_dimension(self): + return 9 From cd644a09209785512ff46b5428f73a56cf10b4af Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Tue, 4 Feb 2020 16:22:55 +0000 Subject: [PATCH 546/749] Add TOMS zany element paper to citations list --- finat/physically_mapped.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/finat/physically_mapped.py b/finat/physically_mapped.py index f34d45571..39c60261e 100644 --- a/finat/physically_mapped.py +++ b/finat/physically_mapped.py @@ -17,6 +17,21 @@ primaryclass = {math.NA} } """) + Citations().add("Kirby2019zany", """ +@Article{Kirby:2019, + author = {Robert C. Kirby and Lawrence Mitchell}, + title = {Code generation for generally mapped finite + elements}, + journal = {ACM Transactions on Mathematical Software}, + year = 2019, + volume = 45, + number = 41, + pages = {41:1--41:23}, + doi = {10.1145/3361745}, + archiveprefix ={arXiv}, + eprint = {1808.05513}, + primaryclass = {cs.MS} +}""") Citations().add("Argyris1968", """ @Article{Argyris1968, author = {J. H. Argyris and I. Fried and D. W. Scharpf}, @@ -81,6 +96,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if Citations is not None: Citations().register("Kirby2018zany") + Citations().register("Kirby2019zany") @abstractmethod def basis_transformation(self, coordinate_mapping): From 5169762c2d74e3ae4c8939d1db70753d1c7aa3f8 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Wed, 5 Feb 2020 16:57:19 -0600 Subject: [PATCH 547/749] WIP: direct serendipity --- finat/__init__.py | 1 + finat/direct_serendipity.py | 176 ++++++++++++++++++++++++++++++++++++ finat/physically_mapped.py | 28 +++++- finat/sympy2gem.py | 5 + 4 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 finat/direct_serendipity.py diff --git a/finat/__init__.py b/finat/__init__.py index 5d19003e6..3cd7bbca3 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -10,6 +10,7 @@ from .hermite import Hermite # noqa: F401 from .morley import Morley # noqa: F401 from .trace import HDivTrace # noqa: F401 +from .direct_serendipity import DirectSerendipity # noqa: F401 from .spectral import GaussLobattoLegendre, GaussLegendre # noqa: F401 from .tensorfiniteelement import TensorFiniteElement # noqa: F401 from .tensor_product import TensorProductElement # noqa: F401 diff --git a/finat/direct_serendipity.py b/finat/direct_serendipity.py new file mode 100644 index 000000000..528924811 --- /dev/null +++ b/finat/direct_serendipity.py @@ -0,0 +1,176 @@ +import numpy + +import finat +from finat.finiteelementbase import FiniteElementBase +from finat.physically_mapped import DirectlyDefinedElement, Citations +from FIAT.reference_element import UFCQuadrilateral +from FIAT.polynomial_set import mis + +import gem +import sympy +from finat.sympy2gem import sympy2gem + +class DirectSerendipity(DirectlyDefinedElement, FiniteElementBase): + def __init__(self, cell, degree): + assert isinstance(cell, UFCQuadrilateral) + assert degree == 1 + self._cell = cell + self._degree = degree + self.space_dim = 4 if degree == 1 else (self.degree+1)*(self.degree+2)/2 + 2 + + @property + def cell(self): + return self._cell + + @property + def degree(self): + return self._degree + + @property + def formdegree(self): + return 0 + + def entity_dofs(self): + return {0: {i: [i] for i in range(4)}, + 1: {i: [] for i in range(4)}, + 2: {0: []}} + + def space_dimension(self): + return self.space_dim + + @property + def index_shape(self): + return (self.space_dimension(),) + + @property + def value_shape(self): + return () + + def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): + '''Return code for evaluating the element at known points on the + reference element. + + :param order: return derivatives up to this order. + :param ps: the point set. + :param entity: the cell entity on which to tabulate. + ''' + + ct = self.cell.topology + + # # Build everything in sympy + # vs, xx, phis = ds1_sympy(ct) + + # and convert -- all this can be used for each derivative! + phys_verts = coordinate_mapping.physical_vertices() + psflat = finat.point_set.PointSet(ps.points) + print(psflat.indices) + phys_points = coordinate_mapping.physical_points(psflat) + print(phys_points.free_indices) + + + # repl = {vs[i, j]: gem.Indexed(phys_verts, (i, j)) for i in range(4) for j in range(2)} + + # repl.update({s: gem.Indexed(phys_points, (i,)) + # for i, s in enumerate(xx)}) + + # mapper = gem.node.Memoizer(sympy2gem) + # mapper.bindings = repl + + # print(phys_points.free_indices) + + + # # foo = list(map(mapper, phis)) + # # bar = gem.ListTensor(foo) + # # print(bar.free_indices) + # # print(phys_points.free_indices) + + # result[(0, 0)] = None + + # # for i in range(order+1): + # # alphas = mis(2, i) + + # # for alpha in alphas: + # # dphis = [phi.diff(*tuple(zip(xx, alpha))) for phi in phis] + # # foo = gem.ListTensor(list(map(mapper, dphis))) + + # # result[alpha] = gem.ComponentTensor(gem.Indexed(foo, + + return result + + def point_evaluation(self, order, refcoords, entity=None): + 1/0 + + + def mapping(self): + 1/0 + +def ds1_sympy(ct): + vs = numpy.asarray(list(zip(sympy.symbols('x:4'), sympy.symbols('y:4')))) + xx = numpy.asarray(sympy.symbols("x,y")) + + ts = numpy.zeros((4, 2), dtype=object) + for e in range(4): + v0id, v1id = ct[1][e][:] + for j in range(2): + ts[e, :] = vs[v1id, :] - vs[v0id, :] + + ns = numpy.zeros((4, 2), dtype=object) + for e in (0, 3): + ns[e, 0] = -ts[e, 1] + ns[e, 1] = ts[e, 0] + + for e in (1, 2): + ns[e, 0] = ts[e, 1] + ns[e, 1] = -ts[e, 0] + + def xysub(x, y): + return {x[0]: y[0], x[1]: y[1]} + + + xstars = numpy.zeros((4, 2), dtype=object) + for e in range(4): + v0id, v1id = ct[1][e][:] + xstars[e, :] = (vs[v0id, :] + vs[v1id])/2 + + lams = [(xx-xstars[i, :]) @ ns[i, :] for i in range(4)] + + RV = (lams[0] - lams[1]) / (lams[0] + lams[1]) + RH = (lams[2] - lams[3]) / (lams[2] + lams[3]) + Rs = [RV, RH] + + xis = [] + for e in range(4): + dct = xysub(xx, xstars[e, :]) + i = 2*((3-e)//2) + j = i + 1 + xi = lams[i] * lams[j] * (1+ (-1)**(e+1) * Rs[e//2]) / lams[i].subs(dct) / lams[j].subs(dct) / 2 + xis.append(xi) + + d = xysub(xx, vs[0, :]) + r = lams[1] * lams[3] / lams[1].subs(d) / lams[3].subs(d) + d = xysub(xx, vs[2, :]) + r -= lams[0] * lams[3] / lams[0].subs(d) / lams[3].subs(d) + d = xysub(xx, vs[3, :]) + r += lams[0] * lams[2] / lams[0].subs(d) / lams[2].subs(d) + d = xysub(xx, vs[1, :]) + r -= lams[1] * lams[2] / lams[1].subs(d) / lams[2].subs(d) + R = r - sum([r.subs(xysub(xx, xstars[i, :])) * xis[i] for i in range(4)]) + + n03 = numpy.array([[0, -1], [1, 0]]) @ (vs[3, :] - vs[0, :]) + lam03 = (xx - vs[0, :]) @ n03 + n12 = numpy.array([[0, -1], [1, 0]]) @ (vs[2, :] - vs[1, :]) + lam12 = (xx - vs[2, :]) @ n12 + + phi0tilde = lam12 - lam12.subs({xx[0]: vs[3, 0], xx[1]: vs[3, 1]}) * (1 + R) / 2 + phi1tilde = lam03 - lam03.subs({xx[0]: vs[2, 0], xx[1]: vs[2, 1]}) * (1 - R) / 2 + phi2tilde = lam03 - lam03.subs({xx[0]: vs[1, 0], xx[1]: vs[1, 1]}) * (1 - R) / 2 + phi3tilde = lam12 - lam12.subs({xx[0]: vs[0, 0], xx[1]: vs[0, 1]}) * (1 + R) / 2 + + phis = [] + for i, phitilde in enumerate([phi0tilde, phi1tilde, phi2tilde, phi3tilde]): + phi = phitilde / phitilde.subs({xx[0]: vs[i, 0], xx[1]: vs[i, 1]}) + phis.append(phi) + + return vs, xx, numpy.asarray(phis) + + diff --git a/finat/physically_mapped.py b/finat/physically_mapped.py index f34d45571..518780a83 100644 --- a/finat/physically_mapped.py +++ b/finat/physically_mapped.py @@ -73,7 +73,13 @@ Citations = None -class PhysicallyMappedElement(metaclass=ABCMeta): +class NeedsCoordinateMappingElement(metaclass=ABCMeta): + """Abstract class for elements that require physical information + either to map or construct their basis functions.""" + pass + + +class PhysicallyMappedElement(NeedsCoordinateMappingElement): """A mixin that applies a "physical" transformation to tabulated basis functions.""" @@ -109,6 +115,12 @@ def point_evaluation(self, order, refcoords, entity=None): raise NotImplementedError("TODO: not yet thought about it") +class DirectlyDefinedElement(NeedsCoordinateMappingElement): + """Base class for directly defined elements such as direct + serendipity that bypass a coordinate mapping.""" + pass + + class PhysicalGeometry(metaclass=ABCMeta): @abstractmethod @@ -163,3 +175,17 @@ def physical_edge_lengths(self): edge (numbered according to FIAT conventions), shape (nfacet, ). """ + + @abstractmethod + def physical_points(self, pt_set): + """Maps reference element points to GEM for the physical coordinates + + :returns a GEM expression for the physical locations of the points + """ + + @abstractmethod + def physical_vertices(self): + """Physical locations of the cell vertices. + + :returns a GEM expression for the physical vertices.""" + diff --git a/finat/sympy2gem.py b/finat/sympy2gem.py index 889a643a6..0d71734bd 100644 --- a/finat/sympy2gem.py +++ b/finat/sympy2gem.py @@ -45,3 +45,8 @@ def sympy2gem_float(node, self): @sympy2gem.register(sympy.Symbol) def sympy2gem_symbol(node, self): return self.bindings[node] + + +@sympy2gem.register(sympy.Rational) +def sympy2gem_rational(node, self): + return gem.Literal(node.numerator()) / gem.Literal(node.denominator()) From 6de92c9c5338d83aa7507373df199df14fdb26ce Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Fri, 7 Feb 2020 12:57:10 -0600 Subject: [PATCH 548/749] Work on direct serendipity. --- finat/direct_serendipity.py | 145 +++++++++++++++++++++++-------- finat/ds_sympy.py | 164 ++++++++++++++++++++++++++++++++++++ finat/point_set.py | 41 +++++++++ finat/tensor_product.py | 3 +- 4 files changed, 317 insertions(+), 36 deletions(-) create mode 100644 finat/ds_sympy.py diff --git a/finat/direct_serendipity.py b/finat/direct_serendipity.py index 528924811..1830c5424 100644 --- a/finat/direct_serendipity.py +++ b/finat/direct_serendipity.py @@ -10,10 +10,12 @@ import sympy from finat.sympy2gem import sympy2gem + + class DirectSerendipity(DirectlyDefinedElement, FiniteElementBase): def __init__(self, cell, degree): assert isinstance(cell, UFCQuadrilateral) - assert degree == 1 + assert degree == 1 or degree == 2 self._cell = cell self._degree = degree self.space_dim = 4 if degree == 1 else (self.degree+1)*(self.degree+2)/2 + 2 @@ -31,9 +33,14 @@ def formdegree(self): return 0 def entity_dofs(self): - return {0: {i: [i] for i in range(4)}, - 1: {i: [] for i in range(4)}, - 2: {0: []}} + if self.degree == 1: + return {0: {i: [i] for i in range(4)}, + 1: {i: [] for i in range(4)}, + 2: {0: []}} + else: + return {0: {i: [i] for i in range(4)}, + 1: {i: [i+4] for i in range(4)}, + 2: {0: []}} def space_dimension(self): return self.space_dim @@ -54,56 +61,50 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): :param ps: the point set. :param entity: the cell entity on which to tabulate. ''' - ct = self.cell.topology - # # Build everything in sympy - # vs, xx, phis = ds1_sympy(ct) + # Build everything in sympy + if self.degree == 1: + vs, xx, phis = ds1_sympy(ct) + elif self.degree == 2: + vs, xx, phis = ds2_sympy(ct) + # and convert -- all this can be used for each derivative! phys_verts = coordinate_mapping.physical_vertices() - psflat = finat.point_set.PointSet(ps.points) - print(psflat.indices) - phys_points = coordinate_mapping.physical_points(psflat) - print(phys_points.free_indices) - - - # repl = {vs[i, j]: gem.Indexed(phys_verts, (i, j)) for i in range(4) for j in range(2)} - - # repl.update({s: gem.Indexed(phys_points, (i,)) - # for i, s in enumerate(xx)}) - # mapper = gem.node.Memoizer(sympy2gem) - # mapper.bindings = repl - - # print(phys_points.free_indices) + if entity is not None and entity[0] < self.cell.get_spatial_dimension(): + fps = finat.point_set.FacetMappedPointSet(self.cell, entity, ps) + phys_points = gem.partial_indexed(coordinate_mapping.physical_points(fps), ps.indices) + else: + phys_points = gem.partial_indexed(coordinate_mapping.physical_points(ps), + ps.indices) - # # foo = list(map(mapper, phis)) - # # bar = gem.ListTensor(foo) - # # print(bar.free_indices) - # # print(phys_points.free_indices) - - # result[(0, 0)] = None + repl = {vs[i, j]: gem.Indexed(phys_verts, (i, j)) for i in range(4) for j in range(2)} + + repl.update({s: gem.Indexed(phys_points, (i,)) + for i, s in enumerate(xx)}) - # # for i in range(order+1): - # # alphas = mis(2, i) + mapper = gem.node.Memoizer(sympy2gem) + mapper.bindings = repl - # # for alpha in alphas: - # # dphis = [phi.diff(*tuple(zip(xx, alpha))) for phi in phis] - # # foo = gem.ListTensor(list(map(mapper, dphis))) - - # # result[alpha] = gem.ComponentTensor(gem.Indexed(foo, + result = {} + for i in range(order+1): + alphas = mis(2, i) + for alpha in alphas: + dphis = [phi.diff(*tuple(zip(xx, alpha))) for phi in phis] + result[alpha] = gem.ListTensor(list(map(mapper, dphis))) return result def point_evaluation(self, order, refcoords, entity=None): 1/0 - def mapping(self): 1/0 + def ds1_sympy(ct): vs = numpy.asarray(list(zip(sympy.symbols('x:4'), sympy.symbols('y:4')))) xx = numpy.asarray(sympy.symbols("x,y")) @@ -173,4 +174,78 @@ def xysub(x, y): return vs, xx, numpy.asarray(phis) + +def ds2_sympy(ct): + vs = numpy.asarray(list(zip(sympy.symbols('x:4'), sympy.symbols('y:4')))) + xx = numpy.asarray(sympy.symbols("x,y")) + + ts = numpy.zeros((4, 2), dtype=object) + for e in range(4): + v0id, v1id = ct[1][e][:] + for j in range(2): + ts[e, :] = vs[v1id, :] - vs[v0id, :] + + ns = numpy.zeros((4, 2), dtype=object) + for e in (0, 3): + ns[e, 0] = -ts[e, 1] + ns[e, 1] = ts[e, 0] + + for e in (1, 2): + ns[e, 0] = ts[e, 1] + ns[e, 1] = -ts[e, 0] + + def xysub(x, y): + return {x[0]: y[0], x[1]: y[1]} + + xstars = numpy.zeros((4, 2), dtype=object) + for e in range(4): + v0id, v1id = ct[1][e][:] + xstars[e, :] = (vs[v0id, :] + vs[v1id])/2 + + lams = [(xx-xstars[i, :]) @ ns[i, :] for i in range(4)] + + RV = (lams[0] - lams[1]) / (lams[0] + lams[1]) + RH = (lams[2] - lams[3]) / (lams[2] + lams[3]) + + vert_phis = [] + xx2xstars = [xysub(xx, xstars[i]) for i in range(4)] + v_phitildes = [(lams[1] * lams[3] + - lams[3].subs(xx2xstars[2]) + / lams[0].subs(xx2xstars[2]) + * lams[0] * lams[1] * (1-RH) / 2 + - lams[1].subs(xx2xstars[0]) + / lams[2].subs(xx2xstars[0]) + * lams[2] * lams[3] * (1-RV) / 2), + (lams[1] * lams[2] + - lams[2].subs(xx2xstars[3]) + / lams[0].subs(xx2xstars[3]) + * lams[0] * lams[1] * (1+RH) / 2 + - lams[1].subs(xx2xstars[0]) + / lams[3].subs(xx2xstars[0]) + * lams[2] * lams[3] * (1-RV) / 2), + (lams[0] * lams[3] + - lams[3].subs(xx2xstars[2]) + / lams[1].subs(xx2xstars[2]) + * lams[0] * lams[1] * (1-RH) / 2 + - lams[0].subs(xx2xstars[1]) + / lams[2].subs(xx2xstars[1]) + * lams[2] * lams[3] * (1+RV) / 2), + (lams[0] * lams[2] + - lams[2].subs(xx2xstars[3]) + / lams[1].subs(xx2xstars[3]) + * lams[0] * lams[1] * (1+RH) / 2 + - lams[0].subs(xx2xstars[1]) + / lams[3].subs(xx2xstars[1]) + * lams[2] * lams[3] * (1+RV) / 2)] + phis_v = [phitilde_v / phitilde_v.subs(xysub(xx, vs[i, :])) + for i, phitilde_v in enumerate(v_phitildes)] + e_phitildes = [lams[2] * lams[3] * (1-RV) / 2, + lams[2] * lams[3] * (1+RV) / 2, + lams[0] * lams[1] * (1-RH) / 2, + lams[0] * lams[1] * (1+RH) / 2] + + phis_e = [ephi / ephi.subs(xx2xstars[i]) for i, ephi in enumerate(e_phitildes)] + + + return vs, xx, numpy.asarray(phis_v + phis_e) diff --git a/finat/ds_sympy.py b/finat/ds_sympy.py new file mode 100644 index 000000000..c46db561f --- /dev/null +++ b/finat/ds_sympy.py @@ -0,0 +1,164 @@ +import sympy +import numpy +import FIAT + +u = FIAT.ufc_cell("quadrilateral") +ct = u.topology + +# symbols x00, x01, x11, etc for physical vertex coords +vs = numpy.asarray(list(zip(sympy.symbols('x:4'), sympy.symbols('y:4')))) +xx = numpy.asarray(sympy.symbols("x,y")) + +#phys_verts = numpy.array(u.vertices) +phys_verts = numpy.array([[0.0, 0.0], [0.0, 1.0], [2.0, 0.0], [1.5, 3.0]]) +sdict = {vs[i, j]: phys_verts[i, j] for i in range(4) for j in range(2)} + +ts = numpy.zeros((4, 2), dtype=object) +for e in range(4): + v0id, v1id = ct[1][e][:] + for j in range(2): + ts[e, :] = vs[v1id, :] - vs[v0id, :] + + +ns = numpy.zeros((4, 2), dtype=object) +for e in (0, 3): + ns[e, 0] = -ts[e, 1] + ns[e, 1] = ts[e, 0] + +for e in (1, 2): + ns[e, 0] = ts[e, 1] + ns[e, 1] = -ts[e, 0] + + +def xysub(x, y): + return {x[0]: y[0], x[1]: y[1]} + +# midpoints of each edge +xstars = numpy.zeros((4, 2), dtype=object) +for e in range(4): + v0id, v1id = ct[1][e][:] + xstars[e, :] = (vs[v0id, :] + vs[v1id])/2 + +lams = [(xx-xstars[i, :]) @ ns[i, :] for i in range(4)] + +RV = (lams[0] - lams[1]) / (lams[0] + lams[1]) +RH = (lams[2] - lams[3]) / (lams[2] + lams[3]) +Rs = [RV, RH] + +xis = [] +sgn = -1 +for e in range(4): + dct = xysub(xx, xstars[e, :]) + i = 2*((3-e)//2) + j = i + 1 + xi = lams[i] * lams[j] * (1+ (-1)**(e+1) * Rs[e//2]) / lams[i].subs(dct) / lams[j].subs(dct) / 2 + xis.append(xi) + + +#sympy.plotting.plot3d(xis[3].subs(sdict), (xx[0], 0, 1), (xx[1], 0, 1)) + +# These check out! +# for xi in xis: +# for e in range(4): +# print(xi.subs(xysub(xx, phys_verts[e, :])).subs(sdict)) +# for e in range(4): +# print(xi.subs(xysub(xx, xstars[e, :])).subs(sdict)) +# print() + + +d = xysub(xx, vs[0, :]) + +r = lams[1] * lams[3] / lams[1].subs(d) / lams[3].subs(d) + +d = xysub(xx, vs[2, :]) + +r -= lams[0] * lams[3] / lams[0].subs(d) / lams[3].subs(d) + +d = xysub(xx, vs[3, :]) + +r += lams[0] * lams[2] / lams[0].subs(d) / lams[2].subs(d) + +d = xysub(xx, vs[1, :]) + +r -= lams[1] * lams[2] / lams[1].subs(d) / lams[2].subs(d) + +R = r - sum([r.subs(xysub(xx, xstars[i, :])) * xis[i] for i in range(4)]) + + +# for e in range(4): +# print(R.subs(sdict).subs({xx[0]: phys_verts[e, 0], xx[1]: phys_verts[e, 1]})) +# for e in range(4): +# print(R.subs({xx[0]: xstars[e, 0], xx[1]: xstars[e, 1]}).subs(sdict)) + + +n03 = numpy.array([[0, -1], [1, 0]]) @ (vs[3, :] - vs[0, :]) +lam03 = (xx - vs[0, :]) @ n03 + +n12 = numpy.array([[0, -1], [1, 0]]) @ (vs[2, :] - vs[1, :]) +lam12 = (xx - vs[2, :]) @ n12 + + +def ds1(): + phi0tilde = lam12 - lam12.subs({xx[0]: vs[3, 0], xx[1]: vs[3, 1]}) * (1 + R) / 2 + phi1tilde = lam03 - lam03.subs({xx[0]: vs[2, 0], xx[1]: vs[2, 1]}) * (1 - R) / 2 + phi2tilde = lam03 - lam03.subs({xx[0]: vs[1, 0], xx[1]: vs[1, 1]}) * (1 - R) / 2 + phi3tilde = lam12 - lam12.subs({xx[0]: vs[0, 0], xx[1]: vs[0, 1]}) * (1 + R) / 2 + + phis = [] + for i, phitilde in enumerate([phi0tilde, phi1tilde, phi2tilde, phi3tilde]): + phi = phitilde / phitilde.subs({xx[0]: vs[i, 0], xx[1]: vs[i, 1]}) + phis.append(phi) + + phis = numpy.asarray(phis) + return phis + +def ds2_sympy(): + xx2xstars = [xysub(xx, xstars[i]) for i in range(4)] + v_phitildes = [(lams[1] * lams[3] + - lams[3].subs(xx2xstars[2]) + / lams[0].subs(xx2xstars[2]) + * lams[0] * lams[1] * (1-RH) / 2 + - lams[1].subs(xx2xstars[0]) + / lams[2].subs(xx2xstars[0]) + * lams[2] * lams[3] * (1-RV) / 2), + (lams[1] * lams[2] + - lams[2].subs(xx2xstars[3]) + / lams[0].subs(xx2xstars[3]) + * lams[0] * lams[1] * (1+RH) / 2 + - lams[1].subs(xx2xstars[0]) + / lams[3].subs(xx2xstars[0]) + * lams[2] * lams[3] * (1-RV) / 2), + (lams[0] * lams[3] + - lams[3].subs(xx2xstars[2]) + / lams[1].subs(xx2xstars[2]) + * lams[0] * lams[1] * (1-RH) / 2 + - lams[0].subs(xx2xstars[1]) + / lams[2].subs(xx2xstars[1]) + * lams[2] * lams[3] * (1+RV) / 2), + (lams[0] * lams[2] + - lams[2].subs(xx2xstars[3]) + / lams[1].subs(xx2xstars[3]) + * lams[0] * lams[1] * (1+RH) / 2 + - lams[0].subs(xx2xstars[1]) + / lams[3].subs(xx2xstars[1]) + * lams[2] * lams[3] * (1+RV) / 2)] + phis_v = [phitilde_v / phitilde_v.subs(xysub(xx, vs[i, :])) + for i, phitilde_v in enumerate(v_phitildes)] + + e_phitildes = [lams[2] * lams[3] * (1-RV) / 2, + lams[2] * lams[3] * (1+RV) / 2, + lams[0] * lams[1] * (1-RH) / 2, + lams[0] * lams[1] * (1+RH) / 2] + + phis_e = [ephi / ephi.subs(xx2xstars[i]) for i, ephi in enumerate(e_phitildes)] + + return numpy.asarray(phis_v + phis_e) + + +for phi in ds2_sympy(): + for e in range(4): + print(phi.subs(sdict).subs({xx[0]: phys_verts[e, 0], xx[1]: phys_verts[e, 1]})) + for e in range(4): + print(phi.subs({xx[0]: xstars[e, 0], xx[1]: xstars[e, 1]}).subs(sdict)) + print() + diff --git a/finat/point_set.py b/finat/point_set.py index ffc754a8e..30f1789e0 100644 --- a/finat/point_set.py +++ b/finat/point_set.py @@ -130,3 +130,44 @@ def almost_equal(self, other, tolerance=1e-12): len(self.factors) == len(other.factors) and \ all(s.almost_equal(o, tolerance=tolerance) for s, o in zip(self.factors, other.factors)) + + +class FacetMappedPointSet(AbstractPointSet): + def __init__(self, fiat_cell, entity, entity_ps): + (edim, eno) = entity + assert edim < fiat_cell.get_spatial_dimension() + assert entity_ps.dimension == edim + self.fiat_cell = fiat_cell + self.entity = entity + self.entity_ps = entity_ps + self.xfrm = fiat_cell.get_entity_transform(*entity) + + @property + def dimension(self): + return self.fiat_cell.get_spatial_dimension() + + @property + def points(self): + epts = self.entity_ps.points + xfrm = self.fiat_cell + return tuple([tuple(self.xfrm(p)) for p in epts]) + + @property + def indices(self): + return self.entity_ps.indices + + @property + def expression(self): + import sympy + Xi = sympy.symbols('s0 s1 s2')[:self.entity_ps.dimension] + S = self.cell.get_entity_transform(*entity)(Xi) + from_facet_mapper = gem.node.Memoizer(sympy2gem) + from_facet_mapper.bindings = {Xi[i]: gem.Indexed(ps.expression, (i,)) + for i in range(ps.dimension)} + + ref_cell_points = gem.ListTensor([from_facet_mapper(Si) for Si in S]) + + return gem.partial_indexed(ref_cell_points, self.indices) + + + diff --git a/finat/tensor_product.py b/finat/tensor_product.py index 59a29e980..82a905b22 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -11,7 +11,7 @@ from gem.utils import cached_property from finat.finiteelementbase import FiniteElementBase -from finat.point_set import PointSingleton, PointSet, TensorPointSet +from finat.point_set import PointSingleton, PointSet, TensorPointSet, FacetMappedPointSet class TensorProductElement(FiniteElementBase): @@ -226,4 +226,5 @@ def factor_point_set(product_cell, product_dim, point_set): result.append(ps) return result + raise NotImplementedError("How to tabulate TensorProductElement on %s?" % (type(point_set).__name__,)) From a998daa208f3c19bb4372e243dd5b96d31a921f8 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Wed, 5 Feb 2020 16:57:19 -0600 Subject: [PATCH 549/749] WIP: direct serendipity --- finat/__init__.py | 1 + finat/direct_serendipity.py | 176 ++++++++++++++++++++++++++++++++++++ finat/physically_mapped.py | 28 +++++- finat/sympy2gem.py | 5 + 4 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 finat/direct_serendipity.py diff --git a/finat/__init__.py b/finat/__init__.py index 5d19003e6..2a4535fed 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -10,6 +10,7 @@ from .hermite import Hermite # noqa: F401 from .morley import Morley # noqa: F401 from .trace import HDivTrace # noqa: F401 +from .direct_serendipity import DirectSerendipity # noqa: F401 from .spectral import GaussLobattoLegendre, GaussLegendre # noqa: F401 from .tensorfiniteelement import TensorFiniteElement # noqa: F401 from .tensor_product import TensorProductElement # noqa: F401 diff --git a/finat/direct_serendipity.py b/finat/direct_serendipity.py new file mode 100644 index 000000000..528924811 --- /dev/null +++ b/finat/direct_serendipity.py @@ -0,0 +1,176 @@ +import numpy + +import finat +from finat.finiteelementbase import FiniteElementBase +from finat.physically_mapped import DirectlyDefinedElement, Citations +from FIAT.reference_element import UFCQuadrilateral +from FIAT.polynomial_set import mis + +import gem +import sympy +from finat.sympy2gem import sympy2gem + +class DirectSerendipity(DirectlyDefinedElement, FiniteElementBase): + def __init__(self, cell, degree): + assert isinstance(cell, UFCQuadrilateral) + assert degree == 1 + self._cell = cell + self._degree = degree + self.space_dim = 4 if degree == 1 else (self.degree+1)*(self.degree+2)/2 + 2 + + @property + def cell(self): + return self._cell + + @property + def degree(self): + return self._degree + + @property + def formdegree(self): + return 0 + + def entity_dofs(self): + return {0: {i: [i] for i in range(4)}, + 1: {i: [] for i in range(4)}, + 2: {0: []}} + + def space_dimension(self): + return self.space_dim + + @property + def index_shape(self): + return (self.space_dimension(),) + + @property + def value_shape(self): + return () + + def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): + '''Return code for evaluating the element at known points on the + reference element. + + :param order: return derivatives up to this order. + :param ps: the point set. + :param entity: the cell entity on which to tabulate. + ''' + + ct = self.cell.topology + + # # Build everything in sympy + # vs, xx, phis = ds1_sympy(ct) + + # and convert -- all this can be used for each derivative! + phys_verts = coordinate_mapping.physical_vertices() + psflat = finat.point_set.PointSet(ps.points) + print(psflat.indices) + phys_points = coordinate_mapping.physical_points(psflat) + print(phys_points.free_indices) + + + # repl = {vs[i, j]: gem.Indexed(phys_verts, (i, j)) for i in range(4) for j in range(2)} + + # repl.update({s: gem.Indexed(phys_points, (i,)) + # for i, s in enumerate(xx)}) + + # mapper = gem.node.Memoizer(sympy2gem) + # mapper.bindings = repl + + # print(phys_points.free_indices) + + + # # foo = list(map(mapper, phis)) + # # bar = gem.ListTensor(foo) + # # print(bar.free_indices) + # # print(phys_points.free_indices) + + # result[(0, 0)] = None + + # # for i in range(order+1): + # # alphas = mis(2, i) + + # # for alpha in alphas: + # # dphis = [phi.diff(*tuple(zip(xx, alpha))) for phi in phis] + # # foo = gem.ListTensor(list(map(mapper, dphis))) + + # # result[alpha] = gem.ComponentTensor(gem.Indexed(foo, + + return result + + def point_evaluation(self, order, refcoords, entity=None): + 1/0 + + + def mapping(self): + 1/0 + +def ds1_sympy(ct): + vs = numpy.asarray(list(zip(sympy.symbols('x:4'), sympy.symbols('y:4')))) + xx = numpy.asarray(sympy.symbols("x,y")) + + ts = numpy.zeros((4, 2), dtype=object) + for e in range(4): + v0id, v1id = ct[1][e][:] + for j in range(2): + ts[e, :] = vs[v1id, :] - vs[v0id, :] + + ns = numpy.zeros((4, 2), dtype=object) + for e in (0, 3): + ns[e, 0] = -ts[e, 1] + ns[e, 1] = ts[e, 0] + + for e in (1, 2): + ns[e, 0] = ts[e, 1] + ns[e, 1] = -ts[e, 0] + + def xysub(x, y): + return {x[0]: y[0], x[1]: y[1]} + + + xstars = numpy.zeros((4, 2), dtype=object) + for e in range(4): + v0id, v1id = ct[1][e][:] + xstars[e, :] = (vs[v0id, :] + vs[v1id])/2 + + lams = [(xx-xstars[i, :]) @ ns[i, :] for i in range(4)] + + RV = (lams[0] - lams[1]) / (lams[0] + lams[1]) + RH = (lams[2] - lams[3]) / (lams[2] + lams[3]) + Rs = [RV, RH] + + xis = [] + for e in range(4): + dct = xysub(xx, xstars[e, :]) + i = 2*((3-e)//2) + j = i + 1 + xi = lams[i] * lams[j] * (1+ (-1)**(e+1) * Rs[e//2]) / lams[i].subs(dct) / lams[j].subs(dct) / 2 + xis.append(xi) + + d = xysub(xx, vs[0, :]) + r = lams[1] * lams[3] / lams[1].subs(d) / lams[3].subs(d) + d = xysub(xx, vs[2, :]) + r -= lams[0] * lams[3] / lams[0].subs(d) / lams[3].subs(d) + d = xysub(xx, vs[3, :]) + r += lams[0] * lams[2] / lams[0].subs(d) / lams[2].subs(d) + d = xysub(xx, vs[1, :]) + r -= lams[1] * lams[2] / lams[1].subs(d) / lams[2].subs(d) + R = r - sum([r.subs(xysub(xx, xstars[i, :])) * xis[i] for i in range(4)]) + + n03 = numpy.array([[0, -1], [1, 0]]) @ (vs[3, :] - vs[0, :]) + lam03 = (xx - vs[0, :]) @ n03 + n12 = numpy.array([[0, -1], [1, 0]]) @ (vs[2, :] - vs[1, :]) + lam12 = (xx - vs[2, :]) @ n12 + + phi0tilde = lam12 - lam12.subs({xx[0]: vs[3, 0], xx[1]: vs[3, 1]}) * (1 + R) / 2 + phi1tilde = lam03 - lam03.subs({xx[0]: vs[2, 0], xx[1]: vs[2, 1]}) * (1 - R) / 2 + phi2tilde = lam03 - lam03.subs({xx[0]: vs[1, 0], xx[1]: vs[1, 1]}) * (1 - R) / 2 + phi3tilde = lam12 - lam12.subs({xx[0]: vs[0, 0], xx[1]: vs[0, 1]}) * (1 + R) / 2 + + phis = [] + for i, phitilde in enumerate([phi0tilde, phi1tilde, phi2tilde, phi3tilde]): + phi = phitilde / phitilde.subs({xx[0]: vs[i, 0], xx[1]: vs[i, 1]}) + phis.append(phi) + + return vs, xx, numpy.asarray(phis) + + diff --git a/finat/physically_mapped.py b/finat/physically_mapped.py index f34d45571..518780a83 100644 --- a/finat/physically_mapped.py +++ b/finat/physically_mapped.py @@ -73,7 +73,13 @@ Citations = None -class PhysicallyMappedElement(metaclass=ABCMeta): +class NeedsCoordinateMappingElement(metaclass=ABCMeta): + """Abstract class for elements that require physical information + either to map or construct their basis functions.""" + pass + + +class PhysicallyMappedElement(NeedsCoordinateMappingElement): """A mixin that applies a "physical" transformation to tabulated basis functions.""" @@ -109,6 +115,12 @@ def point_evaluation(self, order, refcoords, entity=None): raise NotImplementedError("TODO: not yet thought about it") +class DirectlyDefinedElement(NeedsCoordinateMappingElement): + """Base class for directly defined elements such as direct + serendipity that bypass a coordinate mapping.""" + pass + + class PhysicalGeometry(metaclass=ABCMeta): @abstractmethod @@ -163,3 +175,17 @@ def physical_edge_lengths(self): edge (numbered according to FIAT conventions), shape (nfacet, ). """ + + @abstractmethod + def physical_points(self, pt_set): + """Maps reference element points to GEM for the physical coordinates + + :returns a GEM expression for the physical locations of the points + """ + + @abstractmethod + def physical_vertices(self): + """Physical locations of the cell vertices. + + :returns a GEM expression for the physical vertices.""" + diff --git a/finat/sympy2gem.py b/finat/sympy2gem.py index 889a643a6..0d71734bd 100644 --- a/finat/sympy2gem.py +++ b/finat/sympy2gem.py @@ -45,3 +45,8 @@ def sympy2gem_float(node, self): @sympy2gem.register(sympy.Symbol) def sympy2gem_symbol(node, self): return self.bindings[node] + + +@sympy2gem.register(sympy.Rational) +def sympy2gem_rational(node, self): + return gem.Literal(node.numerator()) / gem.Literal(node.denominator()) From 96471df10a17b2600b98620388ad92d87294035d Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Fri, 7 Feb 2020 12:57:10 -0600 Subject: [PATCH 550/749] Work on direct serendipity. --- finat/direct_serendipity.py | 145 +++++++++++++++++++++++-------- finat/ds_sympy.py | 164 ++++++++++++++++++++++++++++++++++++ finat/point_set.py | 41 +++++++++ 3 files changed, 315 insertions(+), 35 deletions(-) create mode 100644 finat/ds_sympy.py diff --git a/finat/direct_serendipity.py b/finat/direct_serendipity.py index 528924811..1830c5424 100644 --- a/finat/direct_serendipity.py +++ b/finat/direct_serendipity.py @@ -10,10 +10,12 @@ import sympy from finat.sympy2gem import sympy2gem + + class DirectSerendipity(DirectlyDefinedElement, FiniteElementBase): def __init__(self, cell, degree): assert isinstance(cell, UFCQuadrilateral) - assert degree == 1 + assert degree == 1 or degree == 2 self._cell = cell self._degree = degree self.space_dim = 4 if degree == 1 else (self.degree+1)*(self.degree+2)/2 + 2 @@ -31,9 +33,14 @@ def formdegree(self): return 0 def entity_dofs(self): - return {0: {i: [i] for i in range(4)}, - 1: {i: [] for i in range(4)}, - 2: {0: []}} + if self.degree == 1: + return {0: {i: [i] for i in range(4)}, + 1: {i: [] for i in range(4)}, + 2: {0: []}} + else: + return {0: {i: [i] for i in range(4)}, + 1: {i: [i+4] for i in range(4)}, + 2: {0: []}} def space_dimension(self): return self.space_dim @@ -54,56 +61,50 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): :param ps: the point set. :param entity: the cell entity on which to tabulate. ''' - ct = self.cell.topology - # # Build everything in sympy - # vs, xx, phis = ds1_sympy(ct) + # Build everything in sympy + if self.degree == 1: + vs, xx, phis = ds1_sympy(ct) + elif self.degree == 2: + vs, xx, phis = ds2_sympy(ct) + # and convert -- all this can be used for each derivative! phys_verts = coordinate_mapping.physical_vertices() - psflat = finat.point_set.PointSet(ps.points) - print(psflat.indices) - phys_points = coordinate_mapping.physical_points(psflat) - print(phys_points.free_indices) - - - # repl = {vs[i, j]: gem.Indexed(phys_verts, (i, j)) for i in range(4) for j in range(2)} - - # repl.update({s: gem.Indexed(phys_points, (i,)) - # for i, s in enumerate(xx)}) - # mapper = gem.node.Memoizer(sympy2gem) - # mapper.bindings = repl - - # print(phys_points.free_indices) + if entity is not None and entity[0] < self.cell.get_spatial_dimension(): + fps = finat.point_set.FacetMappedPointSet(self.cell, entity, ps) + phys_points = gem.partial_indexed(coordinate_mapping.physical_points(fps), ps.indices) + else: + phys_points = gem.partial_indexed(coordinate_mapping.physical_points(ps), + ps.indices) - # # foo = list(map(mapper, phis)) - # # bar = gem.ListTensor(foo) - # # print(bar.free_indices) - # # print(phys_points.free_indices) - - # result[(0, 0)] = None + repl = {vs[i, j]: gem.Indexed(phys_verts, (i, j)) for i in range(4) for j in range(2)} + + repl.update({s: gem.Indexed(phys_points, (i,)) + for i, s in enumerate(xx)}) - # # for i in range(order+1): - # # alphas = mis(2, i) + mapper = gem.node.Memoizer(sympy2gem) + mapper.bindings = repl - # # for alpha in alphas: - # # dphis = [phi.diff(*tuple(zip(xx, alpha))) for phi in phis] - # # foo = gem.ListTensor(list(map(mapper, dphis))) - - # # result[alpha] = gem.ComponentTensor(gem.Indexed(foo, + result = {} + for i in range(order+1): + alphas = mis(2, i) + for alpha in alphas: + dphis = [phi.diff(*tuple(zip(xx, alpha))) for phi in phis] + result[alpha] = gem.ListTensor(list(map(mapper, dphis))) return result def point_evaluation(self, order, refcoords, entity=None): 1/0 - def mapping(self): 1/0 + def ds1_sympy(ct): vs = numpy.asarray(list(zip(sympy.symbols('x:4'), sympy.symbols('y:4')))) xx = numpy.asarray(sympy.symbols("x,y")) @@ -173,4 +174,78 @@ def xysub(x, y): return vs, xx, numpy.asarray(phis) + +def ds2_sympy(ct): + vs = numpy.asarray(list(zip(sympy.symbols('x:4'), sympy.symbols('y:4')))) + xx = numpy.asarray(sympy.symbols("x,y")) + + ts = numpy.zeros((4, 2), dtype=object) + for e in range(4): + v0id, v1id = ct[1][e][:] + for j in range(2): + ts[e, :] = vs[v1id, :] - vs[v0id, :] + + ns = numpy.zeros((4, 2), dtype=object) + for e in (0, 3): + ns[e, 0] = -ts[e, 1] + ns[e, 1] = ts[e, 0] + + for e in (1, 2): + ns[e, 0] = ts[e, 1] + ns[e, 1] = -ts[e, 0] + + def xysub(x, y): + return {x[0]: y[0], x[1]: y[1]} + + xstars = numpy.zeros((4, 2), dtype=object) + for e in range(4): + v0id, v1id = ct[1][e][:] + xstars[e, :] = (vs[v0id, :] + vs[v1id])/2 + + lams = [(xx-xstars[i, :]) @ ns[i, :] for i in range(4)] + + RV = (lams[0] - lams[1]) / (lams[0] + lams[1]) + RH = (lams[2] - lams[3]) / (lams[2] + lams[3]) + + vert_phis = [] + xx2xstars = [xysub(xx, xstars[i]) for i in range(4)] + v_phitildes = [(lams[1] * lams[3] + - lams[3].subs(xx2xstars[2]) + / lams[0].subs(xx2xstars[2]) + * lams[0] * lams[1] * (1-RH) / 2 + - lams[1].subs(xx2xstars[0]) + / lams[2].subs(xx2xstars[0]) + * lams[2] * lams[3] * (1-RV) / 2), + (lams[1] * lams[2] + - lams[2].subs(xx2xstars[3]) + / lams[0].subs(xx2xstars[3]) + * lams[0] * lams[1] * (1+RH) / 2 + - lams[1].subs(xx2xstars[0]) + / lams[3].subs(xx2xstars[0]) + * lams[2] * lams[3] * (1-RV) / 2), + (lams[0] * lams[3] + - lams[3].subs(xx2xstars[2]) + / lams[1].subs(xx2xstars[2]) + * lams[0] * lams[1] * (1-RH) / 2 + - lams[0].subs(xx2xstars[1]) + / lams[2].subs(xx2xstars[1]) + * lams[2] * lams[3] * (1+RV) / 2), + (lams[0] * lams[2] + - lams[2].subs(xx2xstars[3]) + / lams[1].subs(xx2xstars[3]) + * lams[0] * lams[1] * (1+RH) / 2 + - lams[0].subs(xx2xstars[1]) + / lams[3].subs(xx2xstars[1]) + * lams[2] * lams[3] * (1+RV) / 2)] + phis_v = [phitilde_v / phitilde_v.subs(xysub(xx, vs[i, :])) + for i, phitilde_v in enumerate(v_phitildes)] + e_phitildes = [lams[2] * lams[3] * (1-RV) / 2, + lams[2] * lams[3] * (1+RV) / 2, + lams[0] * lams[1] * (1-RH) / 2, + lams[0] * lams[1] * (1+RH) / 2] + + phis_e = [ephi / ephi.subs(xx2xstars[i]) for i, ephi in enumerate(e_phitildes)] + + + return vs, xx, numpy.asarray(phis_v + phis_e) diff --git a/finat/ds_sympy.py b/finat/ds_sympy.py new file mode 100644 index 000000000..c46db561f --- /dev/null +++ b/finat/ds_sympy.py @@ -0,0 +1,164 @@ +import sympy +import numpy +import FIAT + +u = FIAT.ufc_cell("quadrilateral") +ct = u.topology + +# symbols x00, x01, x11, etc for physical vertex coords +vs = numpy.asarray(list(zip(sympy.symbols('x:4'), sympy.symbols('y:4')))) +xx = numpy.asarray(sympy.symbols("x,y")) + +#phys_verts = numpy.array(u.vertices) +phys_verts = numpy.array([[0.0, 0.0], [0.0, 1.0], [2.0, 0.0], [1.5, 3.0]]) +sdict = {vs[i, j]: phys_verts[i, j] for i in range(4) for j in range(2)} + +ts = numpy.zeros((4, 2), dtype=object) +for e in range(4): + v0id, v1id = ct[1][e][:] + for j in range(2): + ts[e, :] = vs[v1id, :] - vs[v0id, :] + + +ns = numpy.zeros((4, 2), dtype=object) +for e in (0, 3): + ns[e, 0] = -ts[e, 1] + ns[e, 1] = ts[e, 0] + +for e in (1, 2): + ns[e, 0] = ts[e, 1] + ns[e, 1] = -ts[e, 0] + + +def xysub(x, y): + return {x[0]: y[0], x[1]: y[1]} + +# midpoints of each edge +xstars = numpy.zeros((4, 2), dtype=object) +for e in range(4): + v0id, v1id = ct[1][e][:] + xstars[e, :] = (vs[v0id, :] + vs[v1id])/2 + +lams = [(xx-xstars[i, :]) @ ns[i, :] for i in range(4)] + +RV = (lams[0] - lams[1]) / (lams[0] + lams[1]) +RH = (lams[2] - lams[3]) / (lams[2] + lams[3]) +Rs = [RV, RH] + +xis = [] +sgn = -1 +for e in range(4): + dct = xysub(xx, xstars[e, :]) + i = 2*((3-e)//2) + j = i + 1 + xi = lams[i] * lams[j] * (1+ (-1)**(e+1) * Rs[e//2]) / lams[i].subs(dct) / lams[j].subs(dct) / 2 + xis.append(xi) + + +#sympy.plotting.plot3d(xis[3].subs(sdict), (xx[0], 0, 1), (xx[1], 0, 1)) + +# These check out! +# for xi in xis: +# for e in range(4): +# print(xi.subs(xysub(xx, phys_verts[e, :])).subs(sdict)) +# for e in range(4): +# print(xi.subs(xysub(xx, xstars[e, :])).subs(sdict)) +# print() + + +d = xysub(xx, vs[0, :]) + +r = lams[1] * lams[3] / lams[1].subs(d) / lams[3].subs(d) + +d = xysub(xx, vs[2, :]) + +r -= lams[0] * lams[3] / lams[0].subs(d) / lams[3].subs(d) + +d = xysub(xx, vs[3, :]) + +r += lams[0] * lams[2] / lams[0].subs(d) / lams[2].subs(d) + +d = xysub(xx, vs[1, :]) + +r -= lams[1] * lams[2] / lams[1].subs(d) / lams[2].subs(d) + +R = r - sum([r.subs(xysub(xx, xstars[i, :])) * xis[i] for i in range(4)]) + + +# for e in range(4): +# print(R.subs(sdict).subs({xx[0]: phys_verts[e, 0], xx[1]: phys_verts[e, 1]})) +# for e in range(4): +# print(R.subs({xx[0]: xstars[e, 0], xx[1]: xstars[e, 1]}).subs(sdict)) + + +n03 = numpy.array([[0, -1], [1, 0]]) @ (vs[3, :] - vs[0, :]) +lam03 = (xx - vs[0, :]) @ n03 + +n12 = numpy.array([[0, -1], [1, 0]]) @ (vs[2, :] - vs[1, :]) +lam12 = (xx - vs[2, :]) @ n12 + + +def ds1(): + phi0tilde = lam12 - lam12.subs({xx[0]: vs[3, 0], xx[1]: vs[3, 1]}) * (1 + R) / 2 + phi1tilde = lam03 - lam03.subs({xx[0]: vs[2, 0], xx[1]: vs[2, 1]}) * (1 - R) / 2 + phi2tilde = lam03 - lam03.subs({xx[0]: vs[1, 0], xx[1]: vs[1, 1]}) * (1 - R) / 2 + phi3tilde = lam12 - lam12.subs({xx[0]: vs[0, 0], xx[1]: vs[0, 1]}) * (1 + R) / 2 + + phis = [] + for i, phitilde in enumerate([phi0tilde, phi1tilde, phi2tilde, phi3tilde]): + phi = phitilde / phitilde.subs({xx[0]: vs[i, 0], xx[1]: vs[i, 1]}) + phis.append(phi) + + phis = numpy.asarray(phis) + return phis + +def ds2_sympy(): + xx2xstars = [xysub(xx, xstars[i]) for i in range(4)] + v_phitildes = [(lams[1] * lams[3] + - lams[3].subs(xx2xstars[2]) + / lams[0].subs(xx2xstars[2]) + * lams[0] * lams[1] * (1-RH) / 2 + - lams[1].subs(xx2xstars[0]) + / lams[2].subs(xx2xstars[0]) + * lams[2] * lams[3] * (1-RV) / 2), + (lams[1] * lams[2] + - lams[2].subs(xx2xstars[3]) + / lams[0].subs(xx2xstars[3]) + * lams[0] * lams[1] * (1+RH) / 2 + - lams[1].subs(xx2xstars[0]) + / lams[3].subs(xx2xstars[0]) + * lams[2] * lams[3] * (1-RV) / 2), + (lams[0] * lams[3] + - lams[3].subs(xx2xstars[2]) + / lams[1].subs(xx2xstars[2]) + * lams[0] * lams[1] * (1-RH) / 2 + - lams[0].subs(xx2xstars[1]) + / lams[2].subs(xx2xstars[1]) + * lams[2] * lams[3] * (1+RV) / 2), + (lams[0] * lams[2] + - lams[2].subs(xx2xstars[3]) + / lams[1].subs(xx2xstars[3]) + * lams[0] * lams[1] * (1+RH) / 2 + - lams[0].subs(xx2xstars[1]) + / lams[3].subs(xx2xstars[1]) + * lams[2] * lams[3] * (1+RV) / 2)] + phis_v = [phitilde_v / phitilde_v.subs(xysub(xx, vs[i, :])) + for i, phitilde_v in enumerate(v_phitildes)] + + e_phitildes = [lams[2] * lams[3] * (1-RV) / 2, + lams[2] * lams[3] * (1+RV) / 2, + lams[0] * lams[1] * (1-RH) / 2, + lams[0] * lams[1] * (1+RH) / 2] + + phis_e = [ephi / ephi.subs(xx2xstars[i]) for i, ephi in enumerate(e_phitildes)] + + return numpy.asarray(phis_v + phis_e) + + +for phi in ds2_sympy(): + for e in range(4): + print(phi.subs(sdict).subs({xx[0]: phys_verts[e, 0], xx[1]: phys_verts[e, 1]})) + for e in range(4): + print(phi.subs({xx[0]: xstars[e, 0], xx[1]: xstars[e, 1]}).subs(sdict)) + print() + diff --git a/finat/point_set.py b/finat/point_set.py index ffc754a8e..30f1789e0 100644 --- a/finat/point_set.py +++ b/finat/point_set.py @@ -130,3 +130,44 @@ def almost_equal(self, other, tolerance=1e-12): len(self.factors) == len(other.factors) and \ all(s.almost_equal(o, tolerance=tolerance) for s, o in zip(self.factors, other.factors)) + + +class FacetMappedPointSet(AbstractPointSet): + def __init__(self, fiat_cell, entity, entity_ps): + (edim, eno) = entity + assert edim < fiat_cell.get_spatial_dimension() + assert entity_ps.dimension == edim + self.fiat_cell = fiat_cell + self.entity = entity + self.entity_ps = entity_ps + self.xfrm = fiat_cell.get_entity_transform(*entity) + + @property + def dimension(self): + return self.fiat_cell.get_spatial_dimension() + + @property + def points(self): + epts = self.entity_ps.points + xfrm = self.fiat_cell + return tuple([tuple(self.xfrm(p)) for p in epts]) + + @property + def indices(self): + return self.entity_ps.indices + + @property + def expression(self): + import sympy + Xi = sympy.symbols('s0 s1 s2')[:self.entity_ps.dimension] + S = self.cell.get_entity_transform(*entity)(Xi) + from_facet_mapper = gem.node.Memoizer(sympy2gem) + from_facet_mapper.bindings = {Xi[i]: gem.Indexed(ps.expression, (i,)) + for i in range(ps.dimension)} + + ref_cell_points = gem.ListTensor([from_facet_mapper(Si) for Si in S]) + + return gem.partial_indexed(ref_cell_points, self.indices) + + + From c00815e21bf5e8eb01749006f8c7d9a3b87671a4 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Sat, 8 Feb 2020 13:01:51 +0000 Subject: [PATCH 551/749] Complete docstring for physical_points Add entity argument, to be used when pushing forward points defined on subentities. --- finat/physically_mapped.py | 21 +++++++++++-------- finat/point_set.py | 41 -------------------------------------- 2 files changed, 13 insertions(+), 49 deletions(-) diff --git a/finat/physically_mapped.py b/finat/physically_mapped.py index 518780a83..42f48ee8a 100644 --- a/finat/physically_mapped.py +++ b/finat/physically_mapped.py @@ -77,7 +77,7 @@ class NeedsCoordinateMappingElement(metaclass=ABCMeta): """Abstract class for elements that require physical information either to map or construct their basis functions.""" pass - + class PhysicallyMappedElement(NeedsCoordinateMappingElement): """A mixin that applies a "physical" transformation to tabulated @@ -119,7 +119,7 @@ class DirectlyDefinedElement(NeedsCoordinateMappingElement): """Base class for directly defined elements such as direct serendipity that bypass a coordinate mapping.""" pass - + class PhysicalGeometry(metaclass=ABCMeta): @@ -134,7 +134,8 @@ def cell_size(self): def jacobian_at(self, point): """The jacobian of the physical coordinates at a point. - :arg point: The point in reference space to evaluate the Jacobian. + :arg point: The point in reference space (on the cell) to + evaluate the Jacobian. :returns: A GEM expression for the Jacobian, shape (gdim, tdim). """ @@ -177,15 +178,19 @@ def physical_edge_lengths(self): """ @abstractmethod - def physical_points(self, pt_set): + def physical_points(self, point_set, entity=None): """Maps reference element points to GEM for the physical coordinates - - :returns a GEM expression for the physical locations of the points + + :arg point_set: A point_set on the reference cell to push forward to physical space. + :arg entity: Reference cell entity on which the point set is + defined (for example if it is a point set on a facet). + :returns: a GEM expression for the physical locations of the + points, shape (gdim, ) with free indices of the point_set. """ @abstractmethod def physical_vertices(self): """Physical locations of the cell vertices. - :returns a GEM expression for the physical vertices.""" - + :returns: a GEM expression for the physical vertices, shape + (gdim, ).""" diff --git a/finat/point_set.py b/finat/point_set.py index 30f1789e0..ffc754a8e 100644 --- a/finat/point_set.py +++ b/finat/point_set.py @@ -130,44 +130,3 @@ def almost_equal(self, other, tolerance=1e-12): len(self.factors) == len(other.factors) and \ all(s.almost_equal(o, tolerance=tolerance) for s, o in zip(self.factors, other.factors)) - - -class FacetMappedPointSet(AbstractPointSet): - def __init__(self, fiat_cell, entity, entity_ps): - (edim, eno) = entity - assert edim < fiat_cell.get_spatial_dimension() - assert entity_ps.dimension == edim - self.fiat_cell = fiat_cell - self.entity = entity - self.entity_ps = entity_ps - self.xfrm = fiat_cell.get_entity_transform(*entity) - - @property - def dimension(self): - return self.fiat_cell.get_spatial_dimension() - - @property - def points(self): - epts = self.entity_ps.points - xfrm = self.fiat_cell - return tuple([tuple(self.xfrm(p)) for p in epts]) - - @property - def indices(self): - return self.entity_ps.indices - - @property - def expression(self): - import sympy - Xi = sympy.symbols('s0 s1 s2')[:self.entity_ps.dimension] - S = self.cell.get_entity_transform(*entity)(Xi) - from_facet_mapper = gem.node.Memoizer(sympy2gem) - from_facet_mapper.bindings = {Xi[i]: gem.Indexed(ps.expression, (i,)) - for i in range(ps.dimension)} - - ref_cell_points = gem.ListTensor([from_facet_mapper(Si) for Si in S]) - - return gem.partial_indexed(ref_cell_points, self.indices) - - - From 8b4b31629a25a597e046e9fdee2d51e374ae6275 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Sat, 8 Feb 2020 13:02:53 +0000 Subject: [PATCH 552/749] Clean up direct serendipity --- finat/direct_serendipity.py | 40 +++++++++++++------------------------ 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/finat/direct_serendipity.py b/finat/direct_serendipity.py index 1830c5424..fb64799a8 100644 --- a/finat/direct_serendipity.py +++ b/finat/direct_serendipity.py @@ -1,8 +1,7 @@ import numpy -import finat from finat.finiteelementbase import FiniteElementBase -from finat.physically_mapped import DirectlyDefinedElement, Citations +from finat.physically_mapped import DirectlyDefinedElement from FIAT.reference_element import UFCQuadrilateral from FIAT.polynomial_set import mis @@ -11,7 +10,6 @@ from finat.sympy2gem import sympy2gem - class DirectSerendipity(DirectlyDefinedElement, FiniteElementBase): def __init__(self, cell, degree): assert isinstance(cell, UFCQuadrilateral) @@ -31,7 +29,7 @@ def degree(self): @property def formdegree(self): return 0 - + def entity_dofs(self): if self.degree == 1: return {0: {i: [i] for i in range(4)}, @@ -69,23 +67,16 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): elif self.degree == 2: vs, xx, phis = ds2_sympy(ct) - # and convert -- all this can be used for each derivative! phys_verts = coordinate_mapping.physical_vertices() - if entity is not None and entity[0] < self.cell.get_spatial_dimension(): - fps = finat.point_set.FacetMappedPointSet(self.cell, entity, ps) - phys_points = gem.partial_indexed(coordinate_mapping.physical_points(fps), ps.indices) - else: - phys_points = gem.partial_indexed(coordinate_mapping.physical_points(ps), - ps.indices) - - - repl = {vs[i, j]: gem.Indexed(phys_verts, (i, j)) for i in range(4) for j in range(2)} - - repl.update({s: gem.Indexed(phys_points, (i,)) - for i, s in enumerate(xx)}) - + phys_points = gem.partial_indexed(coordinate_mapping.physical_points(ps, entity=entity), + ps.indices) + + repl = {vs[i, j]: phys_verts[i, j] for i in range(4) for j in range(2)} + + repl.update({s: phys_points[i] for i, s in enumerate(xx)}) + mapper = gem.node.Memoizer(sympy2gem) mapper.bindings = repl @@ -97,12 +88,12 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): result[alpha] = gem.ListTensor(list(map(mapper, dphis))) return result - + def point_evaluation(self, order, refcoords, entity=None): - 1/0 + raise NotImplementedError("Not done yet, sorry!") def mapping(self): - 1/0 + return "physical" def ds1_sympy(ct): @@ -127,7 +118,6 @@ def ds1_sympy(ct): def xysub(x, y): return {x[0]: y[0], x[1]: y[1]} - xstars = numpy.zeros((4, 2), dtype=object) for e in range(4): v0id, v1id = ct[1][e][:] @@ -144,7 +134,7 @@ def xysub(x, y): dct = xysub(xx, xstars[e, :]) i = 2*((3-e)//2) j = i + 1 - xi = lams[i] * lams[j] * (1+ (-1)**(e+1) * Rs[e//2]) / lams[i].subs(dct) / lams[j].subs(dct) / 2 + xi = lams[i] * lams[j] * (1 + (-1)**(e+1) * Rs[e//2]) / lams[i].subs(dct) / lams[j].subs(dct) / 2 xis.append(xi) d = xysub(xx, vs[0, :]) @@ -207,7 +197,6 @@ def xysub(x, y): RV = (lams[0] - lams[1]) / (lams[0] + lams[1]) RH = (lams[2] - lams[3]) / (lams[2] + lams[3]) - vert_phis = [] xx2xstars = [xysub(xx, xstars[i]) for i in range(4)] v_phitildes = [(lams[1] * lams[3] - lams[3].subs(xx2xstars[2]) @@ -239,7 +228,7 @@ def xysub(x, y): * lams[2] * lams[3] * (1+RV) / 2)] phis_v = [phitilde_v / phitilde_v.subs(xysub(xx, vs[i, :])) for i, phitilde_v in enumerate(v_phitildes)] - + e_phitildes = [lams[2] * lams[3] * (1-RV) / 2, lams[2] * lams[3] * (1+RV) / 2, lams[0] * lams[1] * (1-RH) / 2, @@ -247,5 +236,4 @@ def xysub(x, y): phis_e = [ephi / ephi.subs(xx2xstars[i]) for i, ephi in enumerate(e_phitildes)] - return vs, xx, numpy.asarray(phis_v + phis_e) From d6506388424c1a8b80038246716d5d9413512fa4 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Sat, 8 Feb 2020 22:00:24 -0600 Subject: [PATCH 553/749] Fix dS2 --- finat/direct_serendipity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/direct_serendipity.py b/finat/direct_serendipity.py index 1830c5424..556745d8f 100644 --- a/finat/direct_serendipity.py +++ b/finat/direct_serendipity.py @@ -18,7 +18,7 @@ def __init__(self, cell, degree): assert degree == 1 or degree == 2 self._cell = cell self._degree = degree - self.space_dim = 4 if degree == 1 else (self.degree+1)*(self.degree+2)/2 + 2 + self.space_dim = 4 if degree == 1 else (self.degree+1)*(self.degree+2)//2 + 2 @property def cell(self): From 777cb644ecc33a20aa3519f0abbdf48805e773e8 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Thu, 27 Feb 2020 11:10:26 -0600 Subject: [PATCH 554/749] Boilerplate for trimmed serendipity --- finat/__init__.py | 1 + finat/fiat_elements.py | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/finat/__init__.py b/finat/__init__.py index 5d19003e6..610af719e 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -1,6 +1,7 @@ from .fiat_elements import Bubble, CrouzeixRaviart, DiscontinuousTaylor # noqa: F401 from .fiat_elements import Lagrange, DiscontinuousLagrange # noqa: F401 from .fiat_elements import DPC, Serendipity # noqa: F401 +from .fiat_elements import TrimmedSerendipityFace, TrimmedSerendipityEdge # noqa: F401 from .fiat_elements import BrezziDouglasMarini, BrezziDouglasFortinMarini # noqa: F401 from .fiat_elements import Nedelec, NedelecSecondKind, RaviartThomas # noqa: F401 from .fiat_elements import HellanHerrmannJohnson, Regge # noqa: F401 diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index b230d788c..0313b43fe 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -318,6 +318,16 @@ def __init__(self, cell, degree): super(RaviartThomas, self).__init__(FIAT.RaviartThomas(cell, degree)) +class TrimmedSerendipityFace(VectorFiatElement): + def __init__(self, cell, degree): + super(TrimmedSerendipity, self).__init__(FIAT.TrimmedSerendipityFace(cell, degree)) + + +class TrimmedSerendipityEdge(VectorFiatElement): + def __init__(self, cell, degree): + super(TrimmedSerendipity, self).__init__(FIAT.TrimmedSerendipityEdge(cell, degree)) + + class BrezziDouglasMarini(VectorFiatElement): def __init__(self, cell, degree): super(BrezziDouglasMarini, self).__init__(FIAT.BrezziDouglasMarini(cell, degree)) From 8ac9f5a82ba3239afb1dd3972e4b162ef6d8f5d1 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Thu, 27 Feb 2020 12:43:24 -0600 Subject: [PATCH 555/749] Fix inheritance --- finat/fiat_elements.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 0313b43fe..3f6081a39 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -320,12 +320,12 @@ def __init__(self, cell, degree): class TrimmedSerendipityFace(VectorFiatElement): def __init__(self, cell, degree): - super(TrimmedSerendipity, self).__init__(FIAT.TrimmedSerendipityFace(cell, degree)) + super(TrimmedSerendipityFace, self).__init__(FIAT.TrimmedSerendipityFace(cell, degree)) class TrimmedSerendipityEdge(VectorFiatElement): def __init__(self, cell, degree): - super(TrimmedSerendipity, self).__init__(FIAT.TrimmedSerendipityEdge(cell, degree)) + super(TrimmedSerendipityEdge, self).__init__(FIAT.TrimmedSerendipityEdge(cell, degree)) class BrezziDouglasMarini(VectorFiatElement): From 9b3aa876d4d098b371a507492ab2262575e16dc7 Mon Sep 17 00:00:00 2001 From: Justincrum Date: Thu, 26 Mar 2020 21:11:18 -0700 Subject: [PATCH 556/749] working on plumbing--added TrimmedSerendipityCurl3 --- finat/fiat_elements.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 3f6081a39..7bc0aeefa 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -327,6 +327,9 @@ class TrimmedSerendipityEdge(VectorFiatElement): def __init__(self, cell, degree): super(TrimmedSerendipityEdge, self).__init__(FIAT.TrimmedSerendipityEdge(cell, degree)) +class TrimmedSerendipityCurl3(VectorFiatElement): + def __init__(self, cell, degree): + super(TrimmedSerendipityCurl3, self).__init__(FIAT.TrimmedSerendipityCurl3(cell, degree)) class BrezziDouglasMarini(VectorFiatElement): def __init__(self, cell, degree): From e722b0e1c330c8af69fcdf57dcee00e6a1594145 Mon Sep 17 00:00:00 2001 From: Justincrum Date: Wed, 1 Apr 2020 14:20:46 -0700 Subject: [PATCH 557/749] Plumbing changes for Sminus.py to work in 3d. --- finat/fiat_elements.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 7bc0aeefa..25640ffa8 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -327,10 +327,6 @@ class TrimmedSerendipityEdge(VectorFiatElement): def __init__(self, cell, degree): super(TrimmedSerendipityEdge, self).__init__(FIAT.TrimmedSerendipityEdge(cell, degree)) -class TrimmedSerendipityCurl3(VectorFiatElement): - def __init__(self, cell, degree): - super(TrimmedSerendipityCurl3, self).__init__(FIAT.TrimmedSerendipityCurl3(cell, degree)) - class BrezziDouglasMarini(VectorFiatElement): def __init__(self, cell, degree): super(BrezziDouglasMarini, self).__init__(FIAT.BrezziDouglasMarini(cell, degree)) From 220e91ea73ba4add9a095910e06ec8253ce54315 Mon Sep 17 00:00:00 2001 From: Francis Aznaran Date: Sat, 4 Apr 2020 12:24:04 +0100 Subject: [PATCH 558/749] First attempt at the basis transformation for conforming Arnold-Winther. --- finat/__init__.py | 3 +- finat/aaw.py | 72 -------------------------- finat/aw.py | 127 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 73 deletions(-) delete mode 100644 finat/aaw.py create mode 100644 finat/aw.py diff --git a/finat/__init__.py b/finat/__init__.py index 2b973e383..26bb2becf 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -10,7 +10,8 @@ from .hermite import Hermite # noqa: F401 from .mtw import MardalTaiWinther # noqa: F401 from .morley import Morley # noqa: F401 -from .aaw import ArnoldAwanouWinther # noqa: F401 +from .aw import ArnoldWinther # noqa: F401 +from .aw import ArnoldWintherNC # noqa: F401 from .trace import HDivTrace # noqa: F401 from .spectral import GaussLobattoLegendre, GaussLegendre # noqa: F401 from .tensorfiniteelement import TensorFiniteElement # noqa: F401 diff --git a/finat/aaw.py b/finat/aaw.py deleted file mode 100644 index c03e582d5..000000000 --- a/finat/aaw.py +++ /dev/null @@ -1,72 +0,0 @@ -import numpy - -import FIAT - -from gem import Literal, ListTensor - -from finat.fiat_elements import FiatElement -from finat.physically_mapped import PhysicallyMappedElement, Citations - - -class ArnoldAwanouWinther(PhysicallyMappedElement, FiatElement): - def __init__(self, cell, degree): - super(ArnoldAwanouWinther, self).__init__(FIAT.ArnoldAwanouWinther(cell, degree)) - - - def basis_transformation(self, coordinate_mapping): - V = numpy.zeros((18, 15), dtype=object) - - for multiindex in numpy.ndindex(V.shape): - V[multiindex] = Literal(V[multiindex]) - - for i in range(0, 12, 2): - V[i, i] = Literal(1) - - T = self.cell - - # This bypasses the GEM wrapper. - that = numpy.array([T.compute_normalized_edge_tangent(i) for i in range(3)]) - nhat = numpy.array([T.compute_normal(i) for i in range(3)]) - - detJ = coordinate_mapping.detJ_at([1/3, 1/3]) - J = coordinate_mapping.jacobian_at([1/3, 1/3]) - J_np = numpy.array([[J[0, 0], J[0, 1]], - [J[1, 0], J[1, 1]]]) - JTJ = J_np.T @ J_np - - for e in range(3): - - # Compute alpha and beta for the edge. - Ghat_T = numpy.array([nhat[e, :], that[e, :]]) - - (alpha, beta) = Ghat_T @ JTJ @ that[e,:] / detJ - - # Stuff into the right rows and columns. - (idx1, idx2) = (4*e + 1, 4*e + 3) - V[idx1, idx1-1] = Literal(-1) * alpha / beta - V[idx1, idx1] = Literal(1) / beta - V[idx2, idx2-1] = Literal(-1) * alpha / beta - V[idx2, idx2] = Literal(1) / beta - - # internal dofs - for i in range(12, 15): - V[i, i] = Literal(1) - - return ListTensor(V.T) - - - def entity_dofs(self): - return {0: {0: [], - 1: [], - 2: []}, - 1: {0: [0, 1, 2, 3], 1: [4, 5, 6, 7], 2: [8, 9, 10, 11]}, - 2: {0: [12, 13, 14]}} - - - @property - def index_shape(self): - return (15,) - - - def space_dimension(self): - return 15 diff --git a/finat/aw.py b/finat/aw.py new file mode 100644 index 000000000..6ffe05c50 --- /dev/null +++ b/finat/aw.py @@ -0,0 +1,127 @@ +"""Implementation of the Arnold-Winther finite elements.""" +import numpy + +import FIAT + +from gem import Literal, ListTensor + +from finat.fiat_elements import FiatElement +from finat.physically_mapped import PhysicallyMappedElement, Citations + + +class ArnoldWintherNC(PhysicallyMappedElement, FiatElement): + def __init__(self, cell, degree): + super(ArnoldWintherNC, self).__init__(FIAT.ArnoldWintherNC(cell, degree)) + + @staticmethod + def basis_transformation(self, coordinate_mapping): + """Note, the extra 3 dofs which are removed here + correspond to the constraints.""" + V = numpy.zeros((18, 15), dtype=object) + + for multiindex in numpy.ndindex(V.shape): + V[multiindex] = Literal(V[multiindex]) + + for i in range(0, 12, 2): + V[i, i] = Literal(1) + + T = self.cell + + # This bypasses the GEM wrapper. + that = numpy.array([T.compute_normalized_edge_tangent(i) for i in range(3)]) + nhat = numpy.array([T.compute_normal(i) for i in range(3)]) + + detJ = coordinate_mapping.detJ_at([1/3, 1/3]) + J = coordinate_mapping.jacobian_at([1/3, 1/3]) + J_np = numpy.array([[J[0, 0], J[0, 1]], + [J[1, 0], J[1, 1]]]) + JTJ = J_np.T @ J_np + + for e in range(3): + + # Compute alpha and beta for the edge. + Ghat_T = numpy.array([nhat[e, :], that[e, :]]) + + (alpha, beta) = Ghat_T @ JTJ @ that[e,:] / detJ + + # Stuff into the right rows and columns. + (idx1, idx2) = (4*e + 1, 4*e + 3) + V[idx1, idx1-1] = Literal(-1) * alpha / beta + V[idx1, idx1] = Literal(1) / beta + V[idx2, idx2-1] = Literal(-1) * alpha / beta + V[idx2, idx2] = Literal(1) / beta + + # internal dofs + for i in range(12, 15): + V[i, i] = Literal(1) + + return ListTensor(V.T) + + + def entity_dofs(self): + return {0: {0: [], + 1: [], + 2: []}, + 1: {0: [0, 1, 2, 3], 1: [4, 5, 6, 7], 2: [8, 9, 10, 11]}, + 2: {0: [12, 13, 14]}} + + + @property + def index_shape(self): + return (15,) + + + def space_dimension(self): + return 15 + +class ArnoldWinther(PhysicallyMappedElement, FiatElement): + def __init__(self, cell, degree): + super(ArnoldWinther, self).__init__(FIAT.ArnoldWinther(cell, degree)) + + def basis_transformation(self, coordinate_mapping): + """The extra 6 dofs removed here correspond to + the constraints.""" + V = numpy.zeros((30, 24), dtype=object) + + # The edge and internal dofs are as for the + # nonconforming element. + V[9:24, 9:24] = (ArnoldWintherNC.basis_transformation(self, coordinate_mapping)).T + + # vertex dofs + # TODO: find a succinct expression for W in terms of J. + J = coordinate_mapping.jacobian_at([1/3, 1/3]) + detJ = coordinate_mapping.detJ_at([1/3, 1/3]) + + W = numpy.zeros((3,3), dtype=object) + W[0, 0] = J[0, 0]*J[0, 0] + W[0, 1] = 2*J[0, 0]*J[0, 1] + W[0, 2] = J[0, 1]*J[0, 1] + W[1, 0] = J[0, 0]*J[1, 0] + W[1, 1] = J[0, 0]*J[1, 1] + J[0, 1]*J[1, 0] + W[1, 2] = J[0, 1]*J[1, 1] + W[2, 0] = J[1, 0]*J[1, 0] + W[2, 1] = 2*J[1, 0]*J[1, 1] + W[2, 2] = J[1, 1]*J[1, 1] + W = W / detJ + + # Put into the right rows and columns. + V[0:3, 0:3] = V[3:6, 3:6] = V[6:9, 6:9] = W + + return ListTensor(V.T) + + + def entity_dofs(self): + return {0: {0: [0, 1, 2], + 1: [3, 4, 5], + 2: [6, 7, 8]}, + 1: {0: [9, 10, 11, 12], 1: [13, 14, 15, 16], 2: [17, 18, 19, 20]}, + 2: {0: [21, 22, 23]}} + + + @property + def index_shape(self): + return (24,) + + + def space_dimension(self): + return 24 From 4027892f39634a6d64233ebaf21f6baf3bb9e35e Mon Sep 17 00:00:00 2001 From: Francis Aznaran Date: Wed, 8 Apr 2020 11:11:40 +0100 Subject: [PATCH 559/749] Corrected the call of AWc basis_transformation of that of AWnc. --- finat/aw.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/finat/aw.py b/finat/aw.py index 6ffe05c50..747dc4526 100644 --- a/finat/aw.py +++ b/finat/aw.py @@ -14,7 +14,7 @@ def __init__(self, cell, degree): super(ArnoldWintherNC, self).__init__(FIAT.ArnoldWintherNC(cell, degree)) @staticmethod - def basis_transformation(self, coordinate_mapping): + def basis_transformation(self, coordinate_mapping, as_numpy=False): """Note, the extra 3 dofs which are removed here correspond to the constraints.""" V = numpy.zeros((18, 15), dtype=object) @@ -55,7 +55,10 @@ def basis_transformation(self, coordinate_mapping): for i in range(12, 15): V[i, i] = Literal(1) - return ListTensor(V.T) + if as_numpy: + return V.T + else: + return ListTensor(V.T) def entity_dofs(self): @@ -85,7 +88,7 @@ def basis_transformation(self, coordinate_mapping): # The edge and internal dofs are as for the # nonconforming element. - V[9:24, 9:24] = (ArnoldWintherNC.basis_transformation(self, coordinate_mapping)).T + V[9:24, 9:24] = (ArnoldWintherNC.basis_transformation(self, coordinate_mapping, True)).T # vertex dofs # TODO: find a succinct expression for W in terms of J. From 99a43a5b866d0b05c54aff28cc29a3edf434a1ca Mon Sep 17 00:00:00 2001 From: Patrick Farrell Date: Wed, 8 Apr 2020 15:11:01 +0100 Subject: [PATCH 560/749] Small edits --- finat/aw.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/finat/aw.py b/finat/aw.py index 747dc4526..f52782fdf 100644 --- a/finat/aw.py +++ b/finat/aw.py @@ -13,7 +13,6 @@ class ArnoldWintherNC(PhysicallyMappedElement, FiatElement): def __init__(self, cell, degree): super(ArnoldWintherNC, self).__init__(FIAT.ArnoldWintherNC(cell, degree)) - @staticmethod def basis_transformation(self, coordinate_mapping, as_numpy=False): """Note, the extra 3 dofs which are removed here correspond to the constraints.""" @@ -38,7 +37,7 @@ def basis_transformation(self, coordinate_mapping, as_numpy=False): JTJ = J_np.T @ J_np for e in range(3): - + # Compute alpha and beta for the edge. Ghat_T = numpy.array([nhat[e, :], that[e, :]]) @@ -88,7 +87,7 @@ def basis_transformation(self, coordinate_mapping): # The edge and internal dofs are as for the # nonconforming element. - V[9:24, 9:24] = (ArnoldWintherNC.basis_transformation(self, coordinate_mapping, True)).T + V[9:24, 9:24] = (ArnoldWintherNC(self.cell, self.degree).basis_transformation(self, coordinate_mapping, True)).T # vertex dofs # TODO: find a succinct expression for W in terms of J. From d6618fe066890407d11331e2b9279ca33eddc29d Mon Sep 17 00:00:00 2001 From: Justincrum Date: Tue, 14 Apr 2020 11:26:03 -0700 Subject: [PATCH 561/749] Plumbing for SminusDiv.py --- finat/__init__.py | 1 + finat/fiat_elements.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/finat/__init__.py b/finat/__init__.py index 610af719e..25dacc799 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -2,6 +2,7 @@ from .fiat_elements import Lagrange, DiscontinuousLagrange # noqa: F401 from .fiat_elements import DPC, Serendipity # noqa: F401 from .fiat_elements import TrimmedSerendipityFace, TrimmedSerendipityEdge # noqa: F401 +from .fiat_elements import TrimmedSerendipityDiv #noqa: F401 from .fiat_elements import BrezziDouglasMarini, BrezziDouglasFortinMarini # noqa: F401 from .fiat_elements import Nedelec, NedelecSecondKind, RaviartThomas # noqa: F401 from .fiat_elements import HellanHerrmannJohnson, Regge # noqa: F401 diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 25640ffa8..af3fb402e 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -323,10 +323,16 @@ def __init__(self, cell, degree): super(TrimmedSerendipityFace, self).__init__(FIAT.TrimmedSerendipityFace(cell, degree)) +class TrimmedSerendipityDiv(VectorFiatElement): + def __init__(self, cell, degree): + super(TrimmedSerendipityDiv, self).__init__(FIAT.TrimmedSerendipityDiv(cell, degree)) + + class TrimmedSerendipityEdge(VectorFiatElement): def __init__(self, cell, degree): super(TrimmedSerendipityEdge, self).__init__(FIAT.TrimmedSerendipityEdge(cell, degree)) + class BrezziDouglasMarini(VectorFiatElement): def __init__(self, cell, degree): super(BrezziDouglasMarini, self).__init__(FIAT.BrezziDouglasMarini(cell, degree)) From fbaf67a176291ac1ebd9d27c4444111846d1bafc Mon Sep 17 00:00:00 2001 From: Sophia Vorderwuelbecke Date: Thu, 30 Apr 2020 15:02:39 +0100 Subject: [PATCH 562/749] gem: New Inverse and Solve nodes These will be used for shape-based operations on tensors to allow compilation of Slate via GEM. Additionally, expose FlexiblyIndexed in the public API. --- gem/gem.py | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/gem/gem.py b/gem/gem.py index 9f6f3752a..feb3fcb28 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -32,7 +32,8 @@ 'Index', 'VariableIndex', 'Indexed', 'ComponentTensor', 'IndexSum', 'ListTensor', 'Concatenate', 'Delta', 'index_sum', 'partial_indexed', 'reshape', 'view', - 'indices', 'as_gem'] + 'indices', 'as_gem', 'FlexiblyIndexed', + 'Inverse', 'Solve'] class NodeMeta(type): @@ -782,6 +783,44 @@ def __new__(cls, i, j): return self +class Inverse(Node): + """The inverse of a square matrix.""" + __slots__ = ('children', 'shape') + + def __new__(cls, tensor): + assert len(tensor.shape) == 2 + assert tensor.shape[0] == tensor.shape[1] + + # Invert 1x1 matrix + if tensor.shape == (1, 1): + multiindex = (Index(), Index()) + return ComponentTensor(Division(one, Indexed(tensor, multiindex)), multiindex) + + self = super(Inverse, cls).__new__(cls) + self.children = (tensor,) + self.shape = tensor.shape + + return self + + +class Solve(Node): + """Solution of a square matrix equation with (potentially) multiple right hand sides. + + Represents the X obtained by solving AX = B. + """ + __slots__ = ('children', 'shape') + + def __init__(self, A, B): + # Shape requirements + assert B.shape + assert len(A.shape) == 2 + assert A.shape[0] == A.shape[1] + assert A.shape[0] == B.shape[0] + + self.children = (A, B) + self.shape = A.shape[1:] + B.shape[1:] + + def unique(indices): """Sorts free indices and eliminates duplicates. From 82ed173ea911f9ee4c27af2fdc50e6fa8f76d095 Mon Sep 17 00:00:00 2001 From: Sophia Vorderwuelbecke Date: Thu, 30 Apr 2020 15:03:19 +0100 Subject: [PATCH 563/749] gem: Allow FlexiblyIndexed to apply to any shaped thing --- gem/gem.py | 4 ++-- gem/optimise.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index feb3fcb28..4021e9625 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -562,7 +562,7 @@ class FlexiblyIndexed(Scalar): def __init__(self, variable, dim2idxs): """Construct a flexibly indexed node. - :arg variable: a :py:class:`Variable` + :arg variable: a node that has a shape :arg dim2idxs: describes the mapping of indices For example, if ``variable`` is rank two, and ``dim2idxs`` is @@ -574,7 +574,7 @@ def __init__(self, variable, dim2idxs): variable[1 + i*12 + j*4 + k][0] """ - assert isinstance(variable, Variable) + assert variable.shape assert len(variable.shape) == len(dim2idxs) dim2idxs_ = [] diff --git a/gem/optimise.py b/gem/optimise.py index f55d3353c..70ce294b0 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -10,7 +10,7 @@ from gem.utils import groupby from gem.node import (Memoizer, MemoizerArg, reuse_if_untouched, reuse_if_untouched_arg, traversal) -from gem.gem import (Node, Terminal, Failure, Identity, Literal, Zero, +from gem.gem import (Node, Failure, Identity, Literal, Zero, Product, Sum, Comparison, Conditional, Division, Index, VariableIndex, Indexed, FlexiblyIndexed, IndexSum, ComponentTensor, ListTensor, Delta, @@ -128,7 +128,6 @@ def replace_indices_indexed(node, self, subst): @replace_indices.register(FlexiblyIndexed) def replace_indices_flexiblyindexed(node, self, subst): child, = node.children - assert isinstance(child, Terminal) assert not child.free_indices substitute = dict(subst) From 47e2357c2569e2ad1b095b6e9f95fdb4ef2c9631 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Thu, 30 Apr 2020 16:57:10 +0100 Subject: [PATCH 564/749] gem: Avoid complex warnings when initialising Literal array --- gem/gem.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 4021e9625..f0140f611 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -191,10 +191,11 @@ def __new__(cls, array): return super(Literal, cls).__new__(cls) def __init__(self, array): + array = asarray(array) try: - self.array = asarray(array, dtype=float) + self.array = array.astype(float, casting="safe") except TypeError: - self.array = asarray(array, dtype=complex) + self.array = array.astype(complex) def is_equal(self, other): if type(self) != type(other): @@ -208,10 +209,8 @@ def get_hash(self): @property def value(self): - try: - return float(self.array) - except TypeError: - return complex(self.array) + assert self.shape == () + return self.array.dtype.type(self.array) @property def shape(self): From 5b60d8046f25f53026fd0c6f37e9108e26b0aeeb Mon Sep 17 00:00:00 2001 From: Francis Aznaran Date: Tue, 12 May 2020 17:31:15 +0100 Subject: [PATCH 565/749] AWc had been failing to chop off the AWnc constraint dofs. --- finat/aw.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/finat/aw.py b/finat/aw.py index f52782fdf..01ecca44a 100644 --- a/finat/aw.py +++ b/finat/aw.py @@ -87,7 +87,10 @@ def basis_transformation(self, coordinate_mapping): # The edge and internal dofs are as for the # nonconforming element. - V[9:24, 9:24] = (ArnoldWintherNC(self.cell, self.degree).basis_transformation(self, coordinate_mapping, True)).T + full_AWnc_dofs = (ArnoldWintherNC(self.cell, self.degree-1).basis_transformation(coordinate_mapping, True)).T + edge_and_internal = full_AWnc_dofs[:15,:] + V[9:24, 9:24] = edge_and_internal + #V[9:24, 9:24] = (ArnoldWintherNC(self.cell, self.degree-1).basis_transformation(coordinate_mapping, True)).T # vertex dofs # TODO: find a succinct expression for W in terms of J. From afcf70a050bb3af23a44e0f335eedc5b0185867a Mon Sep 17 00:00:00 2001 From: Francis Aznaran Date: Tue, 12 May 2020 18:00:36 +0100 Subject: [PATCH 566/749] AWc now gets constructed, but diverges. --- finat/aw.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/finat/aw.py b/finat/aw.py index 01ecca44a..4043d9f48 100644 --- a/finat/aw.py +++ b/finat/aw.py @@ -85,6 +85,9 @@ def basis_transformation(self, coordinate_mapping): the constraints.""" V = numpy.zeros((30, 24), dtype=object) + for multiindex in numpy.ndindex(V.shape): + V[multiindex] = Literal(V[multiindex]) + # The edge and internal dofs are as for the # nonconforming element. full_AWnc_dofs = (ArnoldWintherNC(self.cell, self.degree-1).basis_transformation(coordinate_mapping, True)).T From 387609001531c0c828c25420b03779708e49ef48 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Thu, 30 Apr 2020 17:03:40 +0100 Subject: [PATCH 567/749] gem: Teach refactoriser about conj/real/imag Fixes #166. --- gem/refactorise.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/gem/refactorise.py b/gem/refactorise.py index f83bbfd84..2ca6e4cc0 100644 --- a/gem/refactorise.py +++ b/gem/refactorise.py @@ -8,7 +8,7 @@ from gem.node import Memoizer, traversal from gem.gem import (Node, Conditional, Zero, Product, Sum, Indexed, - ListTensor, one) + ListTensor, one, MathFunction) from gem.optimise import (remove_componenttensors, sum_factorise, traverse_product, traverse_sum, unroll_indexsum, make_rename_map, make_renamer) @@ -169,7 +169,7 @@ def stop_at(expr): sums = [] for expr in compounds: summands = traverse_sum(expr, stop_at=stop_at) - if len(summands) <= 1 and not isinstance(expr, Conditional): + if len(summands) <= 1 and not isinstance(expr, (Conditional, MathFunction)): # Compound term is not an addition, avoid infinite # recursion and fail gracefully raising an exception. raise FactorisationError(expr) @@ -211,6 +211,29 @@ def stop_at(expr): return result +@_collect_monomials.register(MathFunction) +def _collect_monomials_mathfunction(expression, self): + name = expression.name + if name in {"conj", "real", "imag"}: + # These are allowed to be applied to arguments, and hence must + # be dealt with specially. Just push the function onto each + # entry in the monomialsum of the child. + # NOTE: This presently assumes that the "atomics" part of a + # MonomialSum are real. This is true for the coffee, tensor, + # spectral modes: the atomics are indexed tabulation matrices + # (which are guaranteed real). + # If the classifier puts (potentially) complex expressions in + # atomics, then this code needs fixed. + child_ms, = map(self, expression.children) + result = MonomialSum() + for k, v in child_ms.monomials.items(): + result.monomials[k] = MathFunction(name, v) + result.ordering = child_ms.ordering.copy() + return result + else: + return _collect_monomials.dispatch(MathFunction.mro()[1])(expression, self) + + @_collect_monomials.register(Conditional) def _collect_monomials_conditional(expression, self): """Refactorises a conditional expression into a sum-of-products form, From 9fbef27a47b473e1f5ef14fbc23036cb6f536492 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Thu, 14 May 2020 11:43:04 -0500 Subject: [PATCH 568/749] WIP: aw.py is still wrong. --- finat/aw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/aw.py b/finat/aw.py index 4043d9f48..46cc0ea01 100644 --- a/finat/aw.py +++ b/finat/aw.py @@ -110,7 +110,7 @@ def basis_transformation(self, coordinate_mapping): W[2, 0] = J[1, 0]*J[1, 0] W[2, 1] = 2*J[1, 0]*J[1, 1] W[2, 2] = J[1, 1]*J[1, 1] - W = W / detJ + W = W / detJ / detJ # Put into the right rows and columns. V[0:3, 0:3] = V[3:6, 3:6] = V[6:9, 6:9] = W From dc21ed853c9f96c61af60db046b3e3ede352df9f Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Thu, 14 May 2020 16:28:02 -0500 Subject: [PATCH 569/749] WIP: AW elements --- finat/aw.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/finat/aw.py b/finat/aw.py index 46cc0ea01..101b9a260 100644 --- a/finat/aw.py +++ b/finat/aw.py @@ -72,10 +72,10 @@ def entity_dofs(self): def index_shape(self): return (15,) - def space_dimension(self): return 15 + class ArnoldWinther(PhysicallyMappedElement, FiatElement): def __init__(self, cell, degree): super(ArnoldWinther, self).__init__(FIAT.ArnoldWinther(cell, degree)) @@ -110,7 +110,7 @@ def basis_transformation(self, coordinate_mapping): W[2, 0] = J[1, 0]*J[1, 0] W[2, 1] = 2*J[1, 0]*J[1, 1] W[2, 2] = J[1, 1]*J[1, 1] - W = W / detJ / detJ + W = W / detJ * detJ # Put into the right rows and columns. V[0:3, 0:3] = V[3:6, 3:6] = V[6:9, 6:9] = W From e1c96f6ca1328b65304b369e9bbfb0d7bc92201e Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Mon, 18 May 2020 21:10:26 -0500 Subject: [PATCH 570/749] update scaling in W for aWc --- finat/aw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/aw.py b/finat/aw.py index 101b9a260..397a2365f 100644 --- a/finat/aw.py +++ b/finat/aw.py @@ -110,7 +110,7 @@ def basis_transformation(self, coordinate_mapping): W[2, 0] = J[1, 0]*J[1, 0] W[2, 1] = 2*J[1, 0]*J[1, 1] W[2, 2] = J[1, 1]*J[1, 1] - W = W / detJ * detJ + W = W / detJ / detJ # Put into the right rows and columns. V[0:3, 0:3] = V[3:6, 3:6] = V[6:9, 6:9] = W From aade71d1cd474a12ccc41125c07c1fa3a8b001a5 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Sat, 9 May 2020 18:45:00 +0100 Subject: [PATCH 571/749] impero: Add option to avoid ReturnAccumulate nodes Will be used in new pointwise expressions compiler. This avoids a case where an indexsum accumulates directly into an output variable. This is only safe if the output tensor is zero on entry (which need not be the case for pointwise expressions). --- gem/impero_utils.py | 9 +++++++-- gem/scheduling.py | 8 ++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/gem/impero_utils.py b/gem/impero_utils.py index e56359752..3e87bdf58 100644 --- a/gem/impero_utils.py +++ b/gem/impero_utils.py @@ -38,12 +38,17 @@ def preprocess_gem(expressions, replace_delta=True, remove_componenttensors=True return expressions -def compile_gem(assignments, prefix_ordering, remove_zeros=False): +def compile_gem(assignments, prefix_ordering, remove_zeros=False, + emit_return_accumulate=True): """Compiles GEM to Impero. :arg assignments: list of (return variable, expression DAG root) pairs :arg prefix_ordering: outermost loop indices :arg remove_zeros: remove zero assignment to return variables + :arg emit_return_accumulate: emit ReturnAccumulate nodes (see + :func:`~.scheduling.emit_operations`)? If False, + split into Accumulate/Return pairs. Set to False if the + output tensor of kernels is not guaranteed to be zero on entry. """ # Remove zeros if remove_zeros: @@ -69,7 +74,7 @@ def nonzero(assignment): get_indices = lambda expr: apply_ordering(expr.free_indices) # Build operation ordering - ops = scheduling.emit_operations(assignments, get_indices) + ops = scheduling.emit_operations(assignments, get_indices, emit_return_accumulate) # Empty kernel if len(ops) == 0: diff --git a/gem/scheduling.py b/gem/scheduling.py index 1fbb563c6..831ee048b 100644 --- a/gem/scheduling.py +++ b/gem/scheduling.py @@ -141,7 +141,7 @@ def handle(ops, push, decref, node): raise AssertionError("no handler for node type %s" % type(node)) -def emit_operations(assignments, get_indices): +def emit_operations(assignments, get_indices, emit_return_accumulate=True): """Makes an ordering of operations to evaluate a multi-root expression DAG. @@ -150,6 +150,9 @@ def emit_operations(assignments, get_indices): upon execution. :arg get_indices: mapping from GEM nodes to an ordering of free indices + :arg emit_return_accumulate: emit ReturnAccumulate nodes? Set to + False if the output variables are not guaranteed + zero on entry to the kernel. :returns: list of Impero terminals correctly ordered to evaluate the assignments """ @@ -159,7 +162,8 @@ def emit_operations(assignments, get_indices): # Stage return operations staging = [] for variable, expression in assignments: - if refcount[expression] == 1 and isinstance(expression, gem.IndexSum) \ + if emit_return_accumulate and \ + refcount[expression] == 1 and isinstance(expression, gem.IndexSum) \ and set(variable.free_indices) == set(expression.free_indices): staging.append(impero.ReturnAccumulate(variable, expression)) refcount[expression] -= 1 From fd14cbfbbd1d3850cef001a26ed3c7398a9e7f4e Mon Sep 17 00:00:00 2001 From: Sophia Vorderwuelbecke Date: Mon, 11 May 2020 15:01:37 +0100 Subject: [PATCH 572/749] Allow for ComponentTensor(NotAFlexiblyIndexed()) and Inverse/Solve in decompose_variable_view, so that this can be reused for generating Slate blocks. --- gem/gem.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index f0140f611..358123485 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -874,11 +874,17 @@ def strides_of(shape): def decompose_variable_view(expression): - """Extract ComponentTensor + FlexiblyIndexed view onto a variable.""" - if isinstance(expression, Variable): + """Extract information from a shaped node. + Decompose ComponentTensor + FlexiblyIndexed.""" + if (isinstance(expression, (Variable, Inverse, Solve))): variable = expression indexes = tuple(Index(extent=extent) for extent in expression.shape) dim2idxs = tuple((0, ((index, 1),)) for index in indexes) + elif (isinstance(expression, ComponentTensor) and + not isinstance(expression.children[0], FlexiblyIndexed)): + variable = expression + indexes = expression.multiindex + dim2idxs = tuple((0, ((index, 1),)) for index in indexes) elif isinstance(expression, ComponentTensor) and isinstance(expression.children[0], FlexiblyIndexed): variable = expression.children[0].children[0] indexes = expression.multiindex @@ -922,10 +928,10 @@ def reshape(expression, *shapes): def view(expression, *slices): - """View a part of a variable. + """View a part of a shaped object. - :arg expression: view of a :py:class:`Variable` - :arg slices: one slice object for each dimension of the variable. + :arg expression: a node that has a shape + :arg slices: one slice object for each dimension of the expression. """ variable, dim2idxs, indexes = decompose_variable_view(expression) assert len(indexes) == len(slices) From aa8ac4fba449c6e989537d51c0a86f63fcf0497f Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Wed, 27 May 2020 16:35:41 -0500 Subject: [PATCH 573/749] Update scaling in AWnc element --- finat/aw.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/finat/aw.py b/finat/aw.py index 397a2365f..34998a5ad 100644 --- a/finat/aw.py +++ b/finat/aw.py @@ -59,7 +59,6 @@ def basis_transformation(self, coordinate_mapping, as_numpy=False): else: return ListTensor(V.T) - def entity_dofs(self): return {0: {0: [], 1: [], @@ -110,7 +109,7 @@ def basis_transformation(self, coordinate_mapping): W[2, 0] = J[1, 0]*J[1, 0] W[2, 1] = 2*J[1, 0]*J[1, 1] W[2, 2] = J[1, 1]*J[1, 1] - W = W / detJ / detJ + W = W # Put into the right rows and columns. V[0:3, 0:3] = V[3:6, 3:6] = V[6:9, 6:9] = W From ca0244a521da308e989551f3f1428ef213cf6c7f Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Fri, 29 May 2020 13:31:08 -0500 Subject: [PATCH 574/749] WIP AW --- finat/aw.py | 49 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/finat/aw.py b/finat/aw.py index 34998a5ad..0c3d7d811 100644 --- a/finat/aw.py +++ b/finat/aw.py @@ -87,14 +87,10 @@ def basis_transformation(self, coordinate_mapping): for multiindex in numpy.ndindex(V.shape): V[multiindex] = Literal(V[multiindex]) - # The edge and internal dofs are as for the - # nonconforming element. - full_AWnc_dofs = (ArnoldWintherNC(self.cell, self.degree-1).basis_transformation(coordinate_mapping, True)).T - edge_and_internal = full_AWnc_dofs[:15,:] - V[9:24, 9:24] = edge_and_internal - #V[9:24, 9:24] = (ArnoldWintherNC(self.cell, self.degree-1).basis_transformation(coordinate_mapping, True)).T - - # vertex dofs + for i in range(24): + V[i, i] = Literal(1) + return ListTensor(V.T) + # TODO: find a succinct expression for W in terms of J. J = coordinate_mapping.jacobian_at([1/3, 1/3]) detJ = coordinate_mapping.detJ_at([1/3, 1/3]) @@ -109,11 +105,46 @@ def basis_transformation(self, coordinate_mapping): W[2, 0] = J[1, 0]*J[1, 0] W[2, 1] = 2*J[1, 0]*J[1, 1] W[2, 2] = J[1, 1]*J[1, 1] - W = W + W = W / (detJ * detJ) # Put into the right rows and columns. V[0:3, 0:3] = V[3:6, 3:6] = V[6:9, 6:9] = W + # edge dofs, 4 per edge + for i in range(9, 21, 2): + V[i, i] = Literal(1) + + T = self.cell + + # This bypasses the GEM wrapper. + that = numpy.array([T.compute_normalized_edge_tangent(i) for i in range(3)]) + nhat = numpy.array([T.compute_normal(i) for i in range(3)]) + + detJ = coordinate_mapping.detJ_at([1/3, 1/3]) + J = coordinate_mapping.jacobian_at([1/3, 1/3]) + J_np = numpy.array([[J[0, 0], J[0, 1]], + [J[1, 0], J[1, 1]]]) + JTJ = J_np.T @ J_np + + for e in range(3): + + # Compute alpha and beta for the edge. + Ghat_T = numpy.array([nhat[e, :], that[e, :]]) + + (alpha, beta) = Ghat_T @ JTJ @ that[e,:] / detJ + + # Stuff into the right rows and columns. + (idx1, idx2) = (9 + 4*e + 1, 9 + 4*e + 3) + V[idx1, idx1-1] = Literal(-1) * alpha / beta + V[idx1, idx1] = Literal(1) / beta + V[idx2, idx2-1] = Literal(-1) * alpha / beta + V[idx2, idx2] = Literal(1) / beta + + # internal dofs + for i in range(21, 24): + V[i, i] = Literal(1) + + return ListTensor(V.T) From dd85cfdd8123b31b09f8a178ba4d031a4f08138e Mon Sep 17 00:00:00 2001 From: Francis Aznaran Date: Tue, 2 Jun 2020 18:25:51 +0100 Subject: [PATCH 575/749] The AWc transformation now seems to be working. --- finat/aw.py | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/finat/aw.py b/finat/aw.py index 0c3d7d811..c8814a2fa 100644 --- a/finat/aw.py +++ b/finat/aw.py @@ -87,15 +87,17 @@ def basis_transformation(self, coordinate_mapping): for multiindex in numpy.ndindex(V.shape): V[multiindex] = Literal(V[multiindex]) - for i in range(24): - V[i, i] = Literal(1) - return ListTensor(V.T) + #for i in range(24): + # V[i, i] = Literal(1) + #return ListTensor(V.T) # TODO: find a succinct expression for W in terms of J. J = coordinate_mapping.jacobian_at([1/3, 1/3]) + #J = numpy.linalg.inv(J) detJ = coordinate_mapping.detJ_at([1/3, 1/3]) - + W = numpy.zeros((3,3), dtype=object) + """ W[0, 0] = J[0, 0]*J[0, 0] W[0, 1] = 2*J[0, 0]*J[0, 1] W[0, 2] = J[0, 1]*J[0, 1] @@ -105,11 +107,26 @@ def basis_transformation(self, coordinate_mapping): W[2, 0] = J[1, 0]*J[1, 0] W[2, 1] = 2*J[1, 0]*J[1, 1] W[2, 2] = J[1, 1]*J[1, 1] - W = W / (detJ * detJ) - + W_check = W / (detJ * detJ) + """ + W[0, 0] = J[1,1]*J[1,1] + W[0, 1] = -2*J[1, 1]*J[0, 1] + W[0, 2] = J[0, 1]*J[0, 1] + W[1, 0] = -1*J[1, 1]*J[1, 0] + W[1, 1] = J[1, 1]*J[0, 0] + J[0, 1]*J[1, 0] + W[1, 2] = -1*J[0, 1]*J[0, 0] + W[2, 0] = J[1, 0]*J[1, 0] + W[2, 1] = -2*J[1, 0]*J[0, 0] + W[2, 2] = J[0, 0]*J[0, 0] + W_check = W # Put into the right rows and columns. - V[0:3, 0:3] = V[3:6, 3:6] = V[6:9, 6:9] = W - + V[0:3, 0:3] = V[3:6, 3:6] = V[6:9, 6:9] = W_check + + """ + for i in range(9): + V[i, i] = Literal(1) + """ + #J = numpy.linalg.inv(J) # edge dofs, 4 per edge for i in range(9, 21, 2): V[i, i] = Literal(1) @@ -125,7 +142,7 @@ def basis_transformation(self, coordinate_mapping): J_np = numpy.array([[J[0, 0], J[0, 1]], [J[1, 0], J[1, 1]]]) JTJ = J_np.T @ J_np - + for e in range(3): # Compute alpha and beta for the edge. @@ -141,8 +158,9 @@ def basis_transformation(self, coordinate_mapping): V[idx2, idx2] = Literal(1) / beta # internal dofs - for i in range(21, 24): - V[i, i] = Literal(1) + #for i in range(21, 24): + # V[i, i] = Literal(1) + V[21: 24, 21:24] = W_check return ListTensor(V.T) From 5476708644d41adccdb97d8a13a4e2f9801d3776 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Tue, 2 Jun 2020 16:38:23 -0500 Subject: [PATCH 576/749] Plausible rescaling of aw dofs --- finat/aw.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/finat/aw.py b/finat/aw.py index c8814a2fa..91ec00949 100644 --- a/finat/aw.py +++ b/finat/aw.py @@ -118,7 +118,7 @@ def basis_transformation(self, coordinate_mapping): W[2, 0] = J[1, 0]*J[1, 0] W[2, 1] = -2*J[1, 0]*J[0, 0] W[2, 2] = J[0, 0]*J[0, 0] - W_check = W + W_check = W # Put into the right rows and columns. V[0:3, 0:3] = V[3:6, 3:6] = V[6:9, 6:9] = W_check @@ -157,12 +157,29 @@ def basis_transformation(self, coordinate_mapping): V[idx2, idx2-1] = Literal(-1) * alpha / beta V[idx2, idx2] = Literal(1) / beta - # internal dofs + # internal dofs (AWnc has good conditioning, so leave this alone) #for i in range(21, 24): # V[i, i] = Literal(1) V[21: 24, 21:24] = W_check - + h = coordinate_mapping.cell_size() + for v in range(3): + for c in range(3): + for i in range(30): + V[i, 3*v+c] = V[i, 3*v+c] / h[v] / h[v] + + # for e in range(3): + # v0id, v1id = [i for i in range(3) if i != e] + # he = (h[v0id] + h[v1id]) / 2 + # for j in range(4): + # for i in range(30): + # V[i, 9+4*e+j] = V[i, 9+4*e+j] / h[e] + + # hc = (h[0] + h[1] + h[2]) / 3 + # for j in range(3): + # for i in range(30): + # V[i, 21 + j] = V[i, 21 + j] / hc + return ListTensor(V.T) From a1ec31e520ecda730c15b5cd3592a5c0ff7de073 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Tue, 2 Jun 2020 17:53:00 -0500 Subject: [PATCH 577/749] Update AW dof scaling to improve conditioning --- finat/aw.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/finat/aw.py b/finat/aw.py index 91ec00949..9bce6af7f 100644 --- a/finat/aw.py +++ b/finat/aw.py @@ -158,27 +158,27 @@ def basis_transformation(self, coordinate_mapping): V[idx2, idx2] = Literal(1) / beta # internal dofs (AWnc has good conditioning, so leave this alone) - #for i in range(21, 24): - # V[i, i] = Literal(1) - V[21: 24, 21:24] = W_check + for i in range(21, 24): + V[i, i] = Literal(1) + #V[21:24, 21:24] = W_check h = coordinate_mapping.cell_size() for v in range(3): for c in range(3): for i in range(30): - V[i, 3*v+c] = V[i, 3*v+c] / h[v] / h[v] - - # for e in range(3): - # v0id, v1id = [i for i in range(3) if i != e] - # he = (h[v0id] + h[v1id]) / 2 - # for j in range(4): - # for i in range(30): - # V[i, 9+4*e+j] = V[i, 9+4*e+j] / h[e] - - # hc = (h[0] + h[1] + h[2]) / 3 - # for j in range(3): - # for i in range(30): - # V[i, 21 + j] = V[i, 21 + j] / hc + V[i, 3*v+c] = V[i, 3*v+c] + + for e in range(3): + v0id, v1id = [i for i in range(3) if i != e] + he = (h[v0id] + h[v1id]) / 2 + for j in range(4): + for i in range(30): + V[i, 9+4*e+j] = V[i, 9+4*e+j] * h[e] * h[e] + + hc = (h[0] + h[1] + h[2]) / 3 + for j in range(3): + for i in range(30): + V[i, 21 + j] = V[i, 21 + j] * hc * hc return ListTensor(V.T) From bf9b644b8d11d67f2e9cc3cf83db9f6067659c8c Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Mon, 22 Jun 2020 11:33:40 -0500 Subject: [PATCH 578/749] Fix vertex transformations in AW --- finat/aw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/aw.py b/finat/aw.py index 9bce6af7f..b81529117 100644 --- a/finat/aw.py +++ b/finat/aw.py @@ -120,7 +120,7 @@ def basis_transformation(self, coordinate_mapping): W[2, 2] = J[0, 0]*J[0, 0] W_check = W # Put into the right rows and columns. - V[0:3, 0:3] = V[3:6, 3:6] = V[6:9, 6:9] = W_check + V[0:3, 0:3] = V[3:6, 3:6] = V[6:9, 6:9] = W_check / detJ / detJ """ for i in range(9): From 5c3b55bdad2d27ca9c4cb5b00cea37eeedce97b2 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Mon, 22 Jun 2020 14:01:02 -0500 Subject: [PATCH 579/749] put back AW --- finat/aw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/aw.py b/finat/aw.py index b81529117..b4e7ce349 100644 --- a/finat/aw.py +++ b/finat/aw.py @@ -120,7 +120,7 @@ def basis_transformation(self, coordinate_mapping): W[2, 2] = J[0, 0]*J[0, 0] W_check = W # Put into the right rows and columns. - V[0:3, 0:3] = V[3:6, 3:6] = V[6:9, 6:9] = W_check / detJ / detJ + V[0:3, 0:3] = V[3:6, 3:6] = V[6:9, 6:9] = W_check """ for i in range(9): From e615fcff1d4e33fdc0049b34057cce90a958504e Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Tue, 23 Jun 2020 10:00:40 -0500 Subject: [PATCH 580/749] WIP: general order --- finat/direct_serendipity.py | 57 +++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/finat/direct_serendipity.py b/finat/direct_serendipity.py index 03ba0f4e5..ecac50b4a 100644 --- a/finat/direct_serendipity.py +++ b/finat/direct_serendipity.py @@ -1,8 +1,7 @@ import numpy -import finat from finat.finiteelementbase import FiniteElementBase -from finat.physically_mapped import DirectlyDefinedElement, Citations +from finat.physically_mapped import DirectlyDefinedElement from FIAT.reference_element import UFCQuadrilateral from FIAT.polynomial_set import mis @@ -97,6 +96,10 @@ def mapping(self): return "physical" +def xysub(x, y): + return {x[0]: y[0], x[1]: y[1]} + + def ds1_sympy(ct): vs = numpy.asarray(list(zip(sympy.symbols('x:4'), sympy.symbols('y:4')))) xx = numpy.asarray(sympy.symbols("x,y")) @@ -116,9 +119,6 @@ def ds1_sympy(ct): ns[e, 0] = ts[e, 1] ns[e, 1] = -ts[e, 0] - def xysub(x, y): - return {x[0]: y[0], x[1]: y[1]} - xstars = numpy.zeros((4, 2), dtype=object) for e in range(4): v0id, v1id = ct[1][e][:] @@ -185,9 +185,6 @@ def ds2_sympy(ct): ns[e, 0] = ts[e, 1] ns[e, 1] = -ts[e, 0] - def xysub(x, y): - return {x[0]: y[0], x[1]: y[1]} - xstars = numpy.zeros((4, 2), dtype=object) for e in range(4): v0id, v1id = ct[1][e][:] @@ -238,3 +235,47 @@ def xysub(x, y): phis_e = [ephi / ephi.subs(xx2xstars[i]) for i, ephi in enumerate(e_phitildes)] return vs, xx, numpy.asarray(phis_v + phis_e) + + +def dsr_sympy(ct, r): + vs = numpy.asarray(list(zip(sympy.symbols('x:4'), sympy.symbols('y:4')))) + xx = numpy.asarray(sympy.symbols("x,y")) + + ts = numpy.zeros((4, 2), dtype=object) + for e in range(4): + v0id, v1id = ct[1][e][:] + for j in range(2): + ts[e, :] = vs[v1id, :] - vs[v0id, :] + + ns = numpy.zeros((4, 2), dtype=object) + for e in (0, 3): + ns[e, 0] = -ts[e, 1] + ns[e, 1] = ts[e, 0] + + for e in (1, 2): + ns[e, 0] = ts[e, 1] + ns[e, 1] = -ts[e, 0] + + xstars = numpy.zeros((4, 2), dtype=object) + for e in range(4): + v0id, v1id = ct[1][e][:] + xstars[e, :] = (vs[v0id, :] + vs[v1id])/2 + + lams = [(xx-xstars[i, :]) @ ns[i, :] for i in range(4)] + + RV = (lams[0] - lams[1]) / (lams[0] + lams[1]) + RH = (lams[2] - lams[3]) / (lams[2] + lams[3]) + + xx2xstars = [xysub(xx, xstars[i]) for i in range(4)] + + # interior bfs: monomials for now + bubble = np.prod(lams) + rm4 = r - 4 + + interior_bfs = [] + for deg in range(rm4 + 1): + for i in range(deg + 1): + interior_bfs.append.append(xx[0]**(deg-i) * xx[1]**i * bubble) + + + From 784ef0079ec316188211091453abb8046a0ef0df Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Fri, 26 Jun 2020 12:25:13 -0500 Subject: [PATCH 581/749] Generalize direct serendipity to general order --- finat/direct_serendipity.py | 270 ++++++++++++++++++++++++------------ 1 file changed, 180 insertions(+), 90 deletions(-) diff --git a/finat/direct_serendipity.py b/finat/direct_serendipity.py index ecac50b4a..f3d10ae7f 100644 --- a/finat/direct_serendipity.py +++ b/finat/direct_serendipity.py @@ -13,7 +13,7 @@ class DirectSerendipity(DirectlyDefinedElement, FiniteElementBase): def __init__(self, cell, degree): assert isinstance(cell, UFCQuadrilateral) - assert degree == 1 or degree == 2 + # assert degree == 1 or degree == 2 self._cell = cell self._degree = degree self.space_dim = 4 if degree == 1 else (self.degree+1)*(self.degree+2)//2 + 2 @@ -35,10 +35,17 @@ def entity_dofs(self): return {0: {i: [i] for i in range(4)}, 1: {i: [] for i in range(4)}, 2: {0: []}} - else: + elif self.degree == 2: return {0: {i: [i] for i in range(4)}, 1: {i: [i+4] for i in range(4)}, 2: {0: []}} + else: + return {0: {i: [i] for i in range(4)}, + 1: {i: list(range(4 + i * (self.degree-1), + 4 + (i + 1) * (self.degree-1))) + for i in range(4)}, + 2: {0: list(range(4 + 4 * (self.degree - 1), + self.space_dim))}} def space_dimension(self): return self.space_dim @@ -64,14 +71,15 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): # Build everything in sympy if self.degree == 1: vs, xx, phis = ds1_sympy(ct) - elif self.degree == 2: - vs, xx, phis = ds2_sympy(ct) + else: + vs, xx, phis = dsr_sympy(ct, self.degree) # and convert -- all this can be used for each derivative! phys_verts = coordinate_mapping.physical_vertices() - phys_points = gem.partial_indexed(coordinate_mapping.physical_points(ps, entity=entity), - ps.indices) + phys_points = gem.partial_indexed( + coordinate_mapping.physical_points(ps, entity=entity), + ps.indices) repl = {vs[i, j]: phys_verts[i, j] for i in range(4) for j in range(2)} @@ -166,86 +174,36 @@ def ds1_sympy(ct): return vs, xx, numpy.asarray(phis) -def ds2_sympy(ct): - vs = numpy.asarray(list(zip(sympy.symbols('x:4'), sympy.symbols('y:4')))) - xx = numpy.asarray(sympy.symbols("x,y")) +def newton_dd(nds, fs): + n = len(nds) + mat = numpy.zeros((n, n), dtype=object) + mat[:, 0] = fs[:] + for j in range(1, n): + for i in range(n-j): + mat[i, j] = (mat[i+1, j-1] - mat[i, j-1]) / (nds[i+j] - nds[i]) + return mat[0, :] - ts = numpy.zeros((4, 2), dtype=object) - for e in range(4): - v0id, v1id = ct[1][e][:] - for j in range(2): - ts[e, :] = vs[v1id, :] - vs[v0id, :] - ns = numpy.zeros((4, 2), dtype=object) - for e in (0, 3): - ns[e, 0] = -ts[e, 1] - ns[e, 1] = ts[e, 0] +# Horner evaluation of polynomial in symbolic form +def newton_poly(nds, fs, xsym): + coeffs = newton_dd(nds, fs) + result = coeffs[-1] + n = len(coeffs) + for i in range(n-2, -1, -1): + result = result * (xsym - nds[i]) + coeffs[i] + return result - for e in (1, 2): - ns[e, 0] = ts[e, 1] - ns[e, 1] = -ts[e, 0] - - xstars = numpy.zeros((4, 2), dtype=object) - for e in range(4): - v0id, v1id = ct[1][e][:] - xstars[e, :] = (vs[v0id, :] + vs[v1id])/2 - - lams = [(xx-xstars[i, :]) @ ns[i, :] for i in range(4)] - - RV = (lams[0] - lams[1]) / (lams[0] + lams[1]) - RH = (lams[2] - lams[3]) / (lams[2] + lams[3]) - xx2xstars = [xysub(xx, xstars[i]) for i in range(4)] - v_phitildes = [(lams[1] * lams[3] - - lams[3].subs(xx2xstars[2]) - / lams[0].subs(xx2xstars[2]) - * lams[0] * lams[1] * (1-RH) / 2 - - lams[1].subs(xx2xstars[0]) - / lams[2].subs(xx2xstars[0]) - * lams[2] * lams[3] * (1-RV) / 2), - (lams[1] * lams[2] - - lams[2].subs(xx2xstars[3]) - / lams[0].subs(xx2xstars[3]) - * lams[0] * lams[1] * (1+RH) / 2 - - lams[1].subs(xx2xstars[0]) - / lams[3].subs(xx2xstars[0]) - * lams[2] * lams[3] * (1-RV) / 2), - (lams[0] * lams[3] - - lams[3].subs(xx2xstars[2]) - / lams[1].subs(xx2xstars[2]) - * lams[0] * lams[1] * (1-RH) / 2 - - lams[0].subs(xx2xstars[1]) - / lams[2].subs(xx2xstars[1]) - * lams[2] * lams[3] * (1+RV) / 2), - (lams[0] * lams[2] - - lams[2].subs(xx2xstars[3]) - / lams[1].subs(xx2xstars[3]) - * lams[0] * lams[1] * (1+RH) / 2 - - lams[0].subs(xx2xstars[1]) - / lams[3].subs(xx2xstars[1]) - * lams[2] * lams[3] * (1+RV) / 2)] - phis_v = [phitilde_v / phitilde_v.subs(xysub(xx, vs[i, :])) - for i, phitilde_v in enumerate(v_phitildes)] - - e_phitildes = [lams[2] * lams[3] * (1-RV) / 2, - lams[2] * lams[3] * (1+RV) / 2, - lams[0] * lams[1] * (1-RH) / 2, - lams[0] * lams[1] * (1+RH) / 2] - - phis_e = [ephi / ephi.subs(xx2xstars[i]) for i, ephi in enumerate(e_phitildes)] - - return vs, xx, numpy.asarray(phis_v + phis_e) - - -def dsr_sympy(ct, r): - vs = numpy.asarray(list(zip(sympy.symbols('x:4'), sympy.symbols('y:4')))) +def dsr_sympy(ct, r, vs=None): + if vs is None: + vs = numpy.asarray(list(zip(sympy.symbols('x:4'), + sympy.symbols('y:4')))) xx = numpy.asarray(sympy.symbols("x,y")) ts = numpy.zeros((4, 2), dtype=object) for e in range(4): v0id, v1id = ct[1][e][:] - for j in range(2): - ts[e, :] = vs[v1id, :] - vs[v0id, :] + ts[e, :] = vs[v1id, :] - vs[v0id, :] ns = numpy.zeros((4, 2), dtype=object) for e in (0, 3): @@ -256,6 +214,7 @@ def dsr_sympy(ct, r): ns[e, 0] = ts[e, 1] ns[e, 1] = -ts[e, 0] + # midpoints of each edge xstars = numpy.zeros((4, 2), dtype=object) for e in range(4): v0id, v1id = ct[1][e][:] @@ -263,19 +222,150 @@ def dsr_sympy(ct, r): lams = [(xx-xstars[i, :]) @ ns[i, :] for i in range(4)] + # # internal functions + bubble = numpy.prod(lams) + + if r < 4: + internal_bfs = [] + internal_nodes = [] + elif r == 4: # Just one point + xbar = sum(vs[i, 0] for i in range(4)) / 4 + ybar = sum(vs[i, 1] for i in range(4)) / 4 + internal_bfs = [bubble / bubble.subs(xysub(xx, (xbar, ybar)))] + internal_nodes = [(xbar, ybar)] + else: # build a lattice inside the quad + dx0 = (vs[1, :] - vs[0, :]) / (r-2) + dx1 = (vs[2, :] - vs[0, :]) / (r-2) + + internal_nodes = [vs[0, :] + dx0 * i + dx1 * j + for i in range(1, r-2) + for j in range(1, r-1-i)] + + mons = [xx[0] ** i * xx[1] ** j + for i in range(r-3) for j in range(r-3-i)] + + V = sympy.Matrix([[mon.subs(xysub(xx, nd)) for mon in mons] + for nd in internal_nodes]) + Vinv = V.inv() + nmon = len(mons) + + internal_bfs = [] + for j in range(nmon): + preibf = bubble * sum(Vinv[i, j] * mons[i] for i in range(nmon)) + internal_bfs.append(preibf + / preibf.subs(xysub(xx, internal_nodes[j]))) + RV = (lams[0] - lams[1]) / (lams[0] + lams[1]) RH = (lams[2] - lams[3]) / (lams[2] + lams[3]) - xx2xstars = [xysub(xx, xstars[i]) for i in range(4)] - - # interior bfs: monomials for now - bubble = np.prod(lams) - rm4 = r - 4 - - interior_bfs = [] - for deg in range(rm4 + 1): - for i in range(deg + 1): - interior_bfs.append.append(xx[0]**(deg-i) * xx[1]**i * bubble) - - - + # R for each edge (1 on edge, zero on opposite + Rs = [(1 - RV) / 2, (1 + RV) / 2, (1 - RH) / 2, (1 + RH) / 2] + + nodes1d = [sympy.Rational(i, r) for i in range(1, r)] + + s = sympy.Symbol('s') + + # for each edge: + # I need its adjacent two edges + # and its opposite edge + # and its "tunnel R" RH or RV + opposite_edges = {0: 1, 1: 0, 2: 3, 3: 2} + adjacent_edges = {0: (2, 3), 1: (2, 3), 2: (0, 1), 3: (0, 1)} + tunnel_R_edges = {0: RH, 1: RH, 2: RV, 3: RV} + + edge_nodes = [] + for ed in range(4): + ((v0x, v0y), (v1x, v1y)) = vs[ct[1][ed], :] + delx = v1x - v0x + dely = v1y - v0y + edge_nodes.append([(v0x+nd*delx, v0y+nd*dely) for nd in nodes1d]) + + # subtracts off the value of function at internal nodes times those + # internal basis functions + def nodalize(f): + foo = f + for (bf, nd) in zip(internal_bfs, internal_nodes): + foo = foo - f.subs(xx, nd) * bf + return foo + + edge_bfs = [] + if r == 2: + for ed in range(4): + lamadj0 = lams[adjacent_edges[ed][0]] + lamadj1 = lams[adjacent_edges[ed][1]] + ephi = lamadj0 * lamadj1 * Rs[ed] + phi = nodalize(ephi) / ephi.subs(xysub(xx, xstars[ed])) + edge_bfs.append([phi]) + else: + for ed in range(4): + ((v0x, v0y), (v1x, v1y)) = vs[ct[1][ed], :] + Rcur = tunnel_R_edges[ed] + lam_op = lams[opposite_edges[ed]] + + edge_bfs_cur = [] + + for i in range(len(nodes1d)): + # strike out i:th node + idcs = [j for j in range(len(nodes1d)) if i != j] + nodes1d_cur = [nodes1d[j] for j in idcs] + edge_nodes_cur = [edge_nodes[ed][j] + for j in idcs] + + # construct the 1d interpolation with remaining nodes + pvals = [] + for nd in edge_nodes_cur: + sub = xysub(xx, nd) + pval_cur = (-1 * Rcur.subs(sub)**(r-2) + / lam_op.subs(sub)) + pvals.append(pval_cur) + + ptilde = newton_poly(nodes1d_cur, pvals, s) + xt = xx @ ts[ed] + vt0 = numpy.asarray((v0x, v0y)) @ ts[ed] + vt1 = numpy.asarray((v1x, v1y)) @ ts[ed] + p = ptilde.subs({s: (xt-vt0) / (vt1-vt0)}) + + prebf = (lams[adjacent_edges[ed][0]] + * lams[adjacent_edges[ed][1]] + * (lams[opposite_edges[ed]] * p + + Rcur**(r-2) * Rs[ed])) + + bfcur = (nodalize(prebf) + / prebf.subs(xysub(xx, edge_nodes[ed][i]))) + edge_bfs_cur.append(bfcur) + + edge_bfs.append(edge_bfs_cur) + + # vertex basis functions + vertex_to_adj_edges = {0: (0, 2), 1: (0, 3), 2: (1, 2), 3: (1, 3)} + vertex_to_off_edges = {0: (1, 3), 1: (1, 2), 2: (0, 3), 3: (0, 2)} + vertex_bfs = [] + for v in range(4): + ed0, ed1 = vertex_to_off_edges[v] + lam0 = lams[ed0] + lam1 = lams[ed1] + + prebf = lam0 * lam1 + + # subtract off edge values + for adj_ed in vertex_to_adj_edges[v]: + edge_nodes_cur = edge_nodes[adj_ed] + edge_bfs_cur = edge_bfs[adj_ed] + for k, (nd, edbf) in enumerate(zip(edge_nodes_cur, edge_bfs_cur)): + sb = xysub(xx, nd) + prebf -= lam0.subs(sb) * lam1.subs(sb) * edbf + + bf = nodalize(prebf) / prebf.subs(xysub(xx, vs[v, :])) + vertex_bfs.append(bf) + + bfs = vertex_bfs + for edbfs in edge_bfs: + bfs.extend(edbfs) + bfs.extend(internal_bfs) + + nds = [tuple(vs[i, :]) for i in range(4)] + for ends in edge_nodes: + nds.extend(ends) + nds.extend(internal_nodes) + + return vs, xx, numpy.asarray(bfs) From cc0a1299e0be2a94beded62a7ccb0c9d1cceaac9 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Fri, 26 Jun 2020 12:26:12 -0500 Subject: [PATCH 582/749] Remove unneeded file --- finat/ds_sympy.py | 164 ---------------------------------------------- 1 file changed, 164 deletions(-) delete mode 100644 finat/ds_sympy.py diff --git a/finat/ds_sympy.py b/finat/ds_sympy.py deleted file mode 100644 index c46db561f..000000000 --- a/finat/ds_sympy.py +++ /dev/null @@ -1,164 +0,0 @@ -import sympy -import numpy -import FIAT - -u = FIAT.ufc_cell("quadrilateral") -ct = u.topology - -# symbols x00, x01, x11, etc for physical vertex coords -vs = numpy.asarray(list(zip(sympy.symbols('x:4'), sympy.symbols('y:4')))) -xx = numpy.asarray(sympy.symbols("x,y")) - -#phys_verts = numpy.array(u.vertices) -phys_verts = numpy.array([[0.0, 0.0], [0.0, 1.0], [2.0, 0.0], [1.5, 3.0]]) -sdict = {vs[i, j]: phys_verts[i, j] for i in range(4) for j in range(2)} - -ts = numpy.zeros((4, 2), dtype=object) -for e in range(4): - v0id, v1id = ct[1][e][:] - for j in range(2): - ts[e, :] = vs[v1id, :] - vs[v0id, :] - - -ns = numpy.zeros((4, 2), dtype=object) -for e in (0, 3): - ns[e, 0] = -ts[e, 1] - ns[e, 1] = ts[e, 0] - -for e in (1, 2): - ns[e, 0] = ts[e, 1] - ns[e, 1] = -ts[e, 0] - - -def xysub(x, y): - return {x[0]: y[0], x[1]: y[1]} - -# midpoints of each edge -xstars = numpy.zeros((4, 2), dtype=object) -for e in range(4): - v0id, v1id = ct[1][e][:] - xstars[e, :] = (vs[v0id, :] + vs[v1id])/2 - -lams = [(xx-xstars[i, :]) @ ns[i, :] for i in range(4)] - -RV = (lams[0] - lams[1]) / (lams[0] + lams[1]) -RH = (lams[2] - lams[3]) / (lams[2] + lams[3]) -Rs = [RV, RH] - -xis = [] -sgn = -1 -for e in range(4): - dct = xysub(xx, xstars[e, :]) - i = 2*((3-e)//2) - j = i + 1 - xi = lams[i] * lams[j] * (1+ (-1)**(e+1) * Rs[e//2]) / lams[i].subs(dct) / lams[j].subs(dct) / 2 - xis.append(xi) - - -#sympy.plotting.plot3d(xis[3].subs(sdict), (xx[0], 0, 1), (xx[1], 0, 1)) - -# These check out! -# for xi in xis: -# for e in range(4): -# print(xi.subs(xysub(xx, phys_verts[e, :])).subs(sdict)) -# for e in range(4): -# print(xi.subs(xysub(xx, xstars[e, :])).subs(sdict)) -# print() - - -d = xysub(xx, vs[0, :]) - -r = lams[1] * lams[3] / lams[1].subs(d) / lams[3].subs(d) - -d = xysub(xx, vs[2, :]) - -r -= lams[0] * lams[3] / lams[0].subs(d) / lams[3].subs(d) - -d = xysub(xx, vs[3, :]) - -r += lams[0] * lams[2] / lams[0].subs(d) / lams[2].subs(d) - -d = xysub(xx, vs[1, :]) - -r -= lams[1] * lams[2] / lams[1].subs(d) / lams[2].subs(d) - -R = r - sum([r.subs(xysub(xx, xstars[i, :])) * xis[i] for i in range(4)]) - - -# for e in range(4): -# print(R.subs(sdict).subs({xx[0]: phys_verts[e, 0], xx[1]: phys_verts[e, 1]})) -# for e in range(4): -# print(R.subs({xx[0]: xstars[e, 0], xx[1]: xstars[e, 1]}).subs(sdict)) - - -n03 = numpy.array([[0, -1], [1, 0]]) @ (vs[3, :] - vs[0, :]) -lam03 = (xx - vs[0, :]) @ n03 - -n12 = numpy.array([[0, -1], [1, 0]]) @ (vs[2, :] - vs[1, :]) -lam12 = (xx - vs[2, :]) @ n12 - - -def ds1(): - phi0tilde = lam12 - lam12.subs({xx[0]: vs[3, 0], xx[1]: vs[3, 1]}) * (1 + R) / 2 - phi1tilde = lam03 - lam03.subs({xx[0]: vs[2, 0], xx[1]: vs[2, 1]}) * (1 - R) / 2 - phi2tilde = lam03 - lam03.subs({xx[0]: vs[1, 0], xx[1]: vs[1, 1]}) * (1 - R) / 2 - phi3tilde = lam12 - lam12.subs({xx[0]: vs[0, 0], xx[1]: vs[0, 1]}) * (1 + R) / 2 - - phis = [] - for i, phitilde in enumerate([phi0tilde, phi1tilde, phi2tilde, phi3tilde]): - phi = phitilde / phitilde.subs({xx[0]: vs[i, 0], xx[1]: vs[i, 1]}) - phis.append(phi) - - phis = numpy.asarray(phis) - return phis - -def ds2_sympy(): - xx2xstars = [xysub(xx, xstars[i]) for i in range(4)] - v_phitildes = [(lams[1] * lams[3] - - lams[3].subs(xx2xstars[2]) - / lams[0].subs(xx2xstars[2]) - * lams[0] * lams[1] * (1-RH) / 2 - - lams[1].subs(xx2xstars[0]) - / lams[2].subs(xx2xstars[0]) - * lams[2] * lams[3] * (1-RV) / 2), - (lams[1] * lams[2] - - lams[2].subs(xx2xstars[3]) - / lams[0].subs(xx2xstars[3]) - * lams[0] * lams[1] * (1+RH) / 2 - - lams[1].subs(xx2xstars[0]) - / lams[3].subs(xx2xstars[0]) - * lams[2] * lams[3] * (1-RV) / 2), - (lams[0] * lams[3] - - lams[3].subs(xx2xstars[2]) - / lams[1].subs(xx2xstars[2]) - * lams[0] * lams[1] * (1-RH) / 2 - - lams[0].subs(xx2xstars[1]) - / lams[2].subs(xx2xstars[1]) - * lams[2] * lams[3] * (1+RV) / 2), - (lams[0] * lams[2] - - lams[2].subs(xx2xstars[3]) - / lams[1].subs(xx2xstars[3]) - * lams[0] * lams[1] * (1+RH) / 2 - - lams[0].subs(xx2xstars[1]) - / lams[3].subs(xx2xstars[1]) - * lams[2] * lams[3] * (1+RV) / 2)] - phis_v = [phitilde_v / phitilde_v.subs(xysub(xx, vs[i, :])) - for i, phitilde_v in enumerate(v_phitildes)] - - e_phitildes = [lams[2] * lams[3] * (1-RV) / 2, - lams[2] * lams[3] * (1+RV) / 2, - lams[0] * lams[1] * (1-RH) / 2, - lams[0] * lams[1] * (1+RH) / 2] - - phis_e = [ephi / ephi.subs(xx2xstars[i]) for i, ephi in enumerate(e_phitildes)] - - return numpy.asarray(phis_v + phis_e) - - -for phi in ds2_sympy(): - for e in range(4): - print(phi.subs(sdict).subs({xx[0]: phys_verts[e, 0], xx[1]: phys_verts[e, 1]})) - for e in range(4): - print(phi.subs({xx[0]: xstars[e, 0], xx[1]: xstars[e, 1]}).subs(sdict)) - print() - From 75056d4465455fb74caca1934635104f7a7e938d Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Mon, 29 Jun 2020 09:31:50 -0500 Subject: [PATCH 583/749] Clean up some flake8 --- finat/point_set.py | 41 ----------------------------------------- finat/tensor_product.py | 3 +-- 2 files changed, 1 insertion(+), 43 deletions(-) diff --git a/finat/point_set.py b/finat/point_set.py index 30f1789e0..ffc754a8e 100644 --- a/finat/point_set.py +++ b/finat/point_set.py @@ -130,44 +130,3 @@ def almost_equal(self, other, tolerance=1e-12): len(self.factors) == len(other.factors) and \ all(s.almost_equal(o, tolerance=tolerance) for s, o in zip(self.factors, other.factors)) - - -class FacetMappedPointSet(AbstractPointSet): - def __init__(self, fiat_cell, entity, entity_ps): - (edim, eno) = entity - assert edim < fiat_cell.get_spatial_dimension() - assert entity_ps.dimension == edim - self.fiat_cell = fiat_cell - self.entity = entity - self.entity_ps = entity_ps - self.xfrm = fiat_cell.get_entity_transform(*entity) - - @property - def dimension(self): - return self.fiat_cell.get_spatial_dimension() - - @property - def points(self): - epts = self.entity_ps.points - xfrm = self.fiat_cell - return tuple([tuple(self.xfrm(p)) for p in epts]) - - @property - def indices(self): - return self.entity_ps.indices - - @property - def expression(self): - import sympy - Xi = sympy.symbols('s0 s1 s2')[:self.entity_ps.dimension] - S = self.cell.get_entity_transform(*entity)(Xi) - from_facet_mapper = gem.node.Memoizer(sympy2gem) - from_facet_mapper.bindings = {Xi[i]: gem.Indexed(ps.expression, (i,)) - for i in range(ps.dimension)} - - ref_cell_points = gem.ListTensor([from_facet_mapper(Si) for Si in S]) - - return gem.partial_indexed(ref_cell_points, self.indices) - - - diff --git a/finat/tensor_product.py b/finat/tensor_product.py index 82a905b22..59a29e980 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -11,7 +11,7 @@ from gem.utils import cached_property from finat.finiteelementbase import FiniteElementBase -from finat.point_set import PointSingleton, PointSet, TensorPointSet, FacetMappedPointSet +from finat.point_set import PointSingleton, PointSet, TensorPointSet class TensorProductElement(FiniteElementBase): @@ -226,5 +226,4 @@ def factor_point_set(product_cell, product_dim, point_set): result.append(ps) return result - raise NotImplementedError("How to tabulate TensorProductElement on %s?" % (type(point_set).__name__,)) From 11459091bb3a2aa0c2ab85980a59acc966fb0781 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Mon, 29 Jun 2020 15:34:16 -0500 Subject: [PATCH 584/749] Address most of Lawrence's comments. Need to figure out geometric_dimension still. --- finat/direct_serendipity.py | 58 +++++++++++++++++++++++++++++-------- finat/physically_mapped.py | 12 ++++++-- finat/sympy2gem.py | 2 +- 3 files changed, 57 insertions(+), 15 deletions(-) diff --git a/finat/direct_serendipity.py b/finat/direct_serendipity.py index f3d10ae7f..1a1b879e5 100644 --- a/finat/direct_serendipity.py +++ b/finat/direct_serendipity.py @@ -1,7 +1,7 @@ import numpy from finat.finiteelementbase import FiniteElementBase -from finat.physically_mapped import DirectlyDefinedElement +from finat.physically_mapped import DirectlyDefinedElement, Citations from FIAT.reference_element import UFCQuadrilateral from FIAT.polynomial_set import mis @@ -12,11 +12,14 @@ class DirectSerendipity(DirectlyDefinedElement, FiniteElementBase): def __init__(self, cell, degree): + if Citations is not None: + Citations().register("Arbogast2017") + + # These elements only known currently on quads assert isinstance(cell, UFCQuadrilateral) - # assert degree == 1 or degree == 2 + self._cell = cell self._degree = degree - self.space_dim = 4 if degree == 1 else (self.degree+1)*(self.degree+2)//2 + 2 @property def cell(self): @@ -45,10 +48,10 @@ def entity_dofs(self): 4 + (i + 1) * (self.degree-1))) for i in range(4)}, 2: {0: list(range(4 + 4 * (self.degree - 1), - self.space_dim))}} + self.space_dimension()))}} def space_dimension(self): - return self.space_dim + return 4 if self.degree == 1 else (self.degree+1)*(self.degree+2)//2 + 2 @property def index_shape(self): @@ -109,6 +112,13 @@ def xysub(x, y): def ds1_sympy(ct): + """Constructs lowest-order case of Arbogast's directly defined C^0 serendipity + elements, which are a special case. + :param ct: The cell topology of the reference quadrilateral. + :returns: a 3-tuple containing symbols for the physical cell coordinates and the + physical cell independent variables (e.g. "x" and "y") and a list + of the four basis functions. + """ vs = numpy.asarray(list(zip(sympy.symbols('x:4'), sympy.symbols('y:4')))) xx = numpy.asarray(sympy.symbols("x,y")) @@ -175,6 +185,8 @@ def ds1_sympy(ct): def newton_dd(nds, fs): + """Constructs Newt's divided differences for the input arrays, which may include + symoblic values.""" n = len(nds) mat = numpy.zeros((n, n), dtype=object) mat[:, 0] = fs[:] @@ -184,8 +196,10 @@ def newton_dd(nds, fs): return mat[0, :] -# Horner evaluation of polynomial in symbolic form def newton_poly(nds, fs, xsym): + """Constructs Lagrange interpolating polynomial passing through + x values nds and y values fs. Returns a a symbolic object in terms + of independent variable xsym.""" coeffs = newton_dd(nds, fs) result = coeffs[-1] n = len(coeffs) @@ -195,6 +209,14 @@ def newton_poly(nds, fs, xsym): def dsr_sympy(ct, r, vs=None): + """Constructs higher-order (>= 2) case of Arbogast's directly defined C^0 serendipity + elements, which include all polynomials of degree r plus a couple of rational + functions. + :param ct: The cell topology of the reference quadrilateral. + :returns: a 3-tuple containing symbols for the physical cell coordinates and the + physical cell independent variables (e.g. "x" and "y") and a list + of the four basis functions. + """ if vs is None: vs = numpy.asarray(list(zip(sympy.symbols('x:4'), sympy.symbols('y:4')))) @@ -269,10 +291,19 @@ def dsr_sympy(ct, r, vs=None): # I need its adjacent two edges # and its opposite edge # and its "tunnel R" RH or RV - opposite_edges = {0: 1, 1: 0, 2: 3, 3: 2} - adjacent_edges = {0: (2, 3), 1: (2, 3), 2: (0, 1), 3: (0, 1)} - tunnel_R_edges = {0: RH, 1: RH, 2: RV, 3: RV} - + # This is very 2d specific. + opposite_edges = {e: [eother for eother in ct[1] + if set(ct[1][e]).intersection(ct[1][eother]) == set()][0] + for e in ct[1]} + adjacent_edges = {e: tuple(sorted([eother for eother in ct[1] + if eother != e + and set(ct[1][e]).intersection(ct[1][eother]) + != set()])) + for e in ct[1]} + # adjacent_edges = {0: (2, 3), 1: (2, 3), 2: (0, 1), 3: (0, 1)} + + ae = adjacent_edges + tunnel_R_edges = {e: ((ae[e][0] - ae[e][1]) / (ae[e][0] + ae[e][1]))} edge_nodes = [] for ed in range(4): ((v0x, v0y), (v1x, v1y)) = vs[ct[1][ed], :] @@ -337,8 +368,11 @@ def nodalize(f): edge_bfs.append(edge_bfs_cur) # vertex basis functions - vertex_to_adj_edges = {0: (0, 2), 1: (0, 3), 2: (1, 2), 3: (1, 3)} - vertex_to_off_edges = {0: (1, 3), 1: (1, 2), 2: (0, 3), 3: (0, 2)} + vertex_to_adj_edges = {i: tuple([e for e in ct[1] if i in ct[1][e]]) + for i in ct[0]} + vertex_to_off_edges = {i: tuple([e for e in ct[1] if i not in ct[1][e]]) + for i in ct[0]} + vertex_bfs = [] for v in range(4): ed0, ed1 = vertex_to_off_edges[v] diff --git a/finat/physically_mapped.py b/finat/physically_mapped.py index 5d53f8c88..84555f891 100644 --- a/finat/physically_mapped.py +++ b/finat/physically_mapped.py @@ -83,6 +83,14 @@ pages = {20-24}, doi = {10.1243/03093247V061020} } +""") + Citations().add("Arbogast2017", """ +@techreport{Arbogast2017, + title={Direct serendipity finite elements on convex quadrilaterals}, + author={Arbogast, T and Tao, Z}, + year={2017}, + institution={Tech. Rep. ICES REPORT 17-28, Institute for Computational Engineering and Sciences} +} """) except ImportError: Citations = None @@ -199,9 +207,9 @@ def physical_points(self, point_set, entity=None): :arg point_set: A point_set on the reference cell to push forward to physical space. :arg entity: Reference cell entity on which the point set is - defined (for example if it is a point set on a facet). + defined (for example if it is a point set on a facet). :returns: a GEM expression for the physical locations of the - points, shape (gdim, ) with free indices of the point_set. + points, shape (gdim, ) with free indices of the point_set. """ @abstractmethod diff --git a/finat/sympy2gem.py b/finat/sympy2gem.py index 0d71734bd..c33a173c6 100644 --- a/finat/sympy2gem.py +++ b/finat/sympy2gem.py @@ -49,4 +49,4 @@ def sympy2gem_symbol(node, self): @sympy2gem.register(sympy.Rational) def sympy2gem_rational(node, self): - return gem.Literal(node.numerator()) / gem.Literal(node.denominator()) + return gem.Division(self(node.numerator()), self(node.denominator())) From 700f167fac3d24477b05a8c199483ec6c3cb6f13 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Wed, 1 Jul 2020 18:40:25 +0100 Subject: [PATCH 585/749] enriched: Return subelement for single argument Catches a case where someone built an enriched element with only a single subelement, which subsequently dies in tree_map(max, ...) because that implicitly relies on there being at least two arguments (such that mapping max works). Fixes firedrakeproject/firedrake#1762. --- finat/enriched.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/finat/enriched.py b/finat/enriched.py index c4940298b..da0e7d107 100644 --- a/finat/enriched.py +++ b/finat/enriched.py @@ -13,9 +13,13 @@ class EnrichedElement(FiniteElementBase): """A finite element whose basis functions are the union of the basis functions of several other finite elements.""" - def __init__(self, elements): - super(EnrichedElement, self).__init__() - self.elements = tuple(elements) + def __new__(cls, elements): + if len(elements) == 1: + return elements[0] + else: + self = super().__new__(cls) + self.elements = tuple(elements) + return self @cached_property def cell(self): From 48e89261c3f9e2965fbf370053f8c7530f7c07b7 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Thu, 2 Jul 2020 13:31:09 -0500 Subject: [PATCH 586/749] fix a bug --- finat/direct_serendipity.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/finat/direct_serendipity.py b/finat/direct_serendipity.py index 1a1b879e5..1ad0839d5 100644 --- a/finat/direct_serendipity.py +++ b/finat/direct_serendipity.py @@ -303,7 +303,9 @@ def dsr_sympy(ct, r, vs=None): # adjacent_edges = {0: (2, 3), 1: (2, 3), 2: (0, 1), 3: (0, 1)} ae = adjacent_edges - tunnel_R_edges = {e: ((ae[e][0] - ae[e][1]) / (ae[e][0] + ae[e][1]))} + tunnel_R_edges = {e: ((lams[ae[e][0]] - lams[ae[e][1]]) + / (lams[ae[e][0]] + lams[ae[e][1]])) + for e in range(4)} edge_nodes = [] for ed in range(4): ((v0x, v0y), (v1x, v1y)) = vs[ct[1][ed], :] @@ -403,3 +405,10 @@ def nodalize(f): nds.extend(internal_nodes) return vs, xx, numpy.asarray(bfs) + + +def ds_sympy(ct, r, vs=None): + if r == 1: + return ds1_sympy(ct, r, vs) + else: + return dsr_sympy(ct, r, vs) From 6a0645053b5ce6a62f3a2f2ff2601747a29266ad Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Fri, 3 Jul 2020 09:51:05 -0500 Subject: [PATCH 587/749] Add helper function; fix typos --- finat/direct_serendipity.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/finat/direct_serendipity.py b/finat/direct_serendipity.py index 1ad0839d5..60ef9fa24 100644 --- a/finat/direct_serendipity.py +++ b/finat/direct_serendipity.py @@ -72,6 +72,7 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): ct = self.cell.topology # Build everything in sympy + vs, xx, phis = ds_sympy(ct, self.degree) if self.degree == 1: vs, xx, phis = ds1_sympy(ct) else: @@ -185,8 +186,8 @@ def ds1_sympy(ct): def newton_dd(nds, fs): - """Constructs Newt's divided differences for the input arrays, which may include - symoblic values.""" + """Constructs Newton's divided differences for the input arrays, + which may include symbolic values.""" n = len(nds) mat = numpy.zeros((n, n), dtype=object) mat[:, 0] = fs[:] @@ -220,6 +221,8 @@ def dsr_sympy(ct, r, vs=None): if vs is None: vs = numpy.asarray(list(zip(sympy.symbols('x:4'), sympy.symbols('y:4')))) + else: + vs = numpy.asarray(vs) xx = numpy.asarray(sympy.symbols("x,y")) ts = numpy.zeros((4, 2), dtype=object) From fc549941df24ce46dae5d6e75640e04ec9be08da Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Thu, 9 Jul 2020 16:39:02 -0500 Subject: [PATCH 588/749] Small fix to flake8 & a big --- finat/direct_serendipity.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/finat/direct_serendipity.py b/finat/direct_serendipity.py index 60ef9fa24..80645e0f7 100644 --- a/finat/direct_serendipity.py +++ b/finat/direct_serendipity.py @@ -112,7 +112,7 @@ def xysub(x, y): return {x[0]: y[0], x[1]: y[1]} -def ds1_sympy(ct): +def ds1_sympy(ct, vs=None): """Constructs lowest-order case of Arbogast's directly defined C^0 serendipity elements, which are a special case. :param ct: The cell topology of the reference quadrilateral. @@ -120,7 +120,12 @@ def ds1_sympy(ct): physical cell independent variables (e.g. "x" and "y") and a list of the four basis functions. """ - vs = numpy.asarray(list(zip(sympy.symbols('x:4'), sympy.symbols('y:4')))) + if vs is None: + vs = numpy.asarray(list(zip(sympy.symbols('x:4'), + sympy.symbols('y:4')))) + else: + vs = numpy.asarray(vs) + xx = numpy.asarray(sympy.symbols("x,y")) ts = numpy.zeros((4, 2), dtype=object) @@ -412,6 +417,6 @@ def nodalize(f): def ds_sympy(ct, r, vs=None): if r == 1: - return ds1_sympy(ct, r, vs) + return ds1_sympy(ct, vs) else: return dsr_sympy(ct, r, vs) From 3e0a757db60c453b416529c1a1f41df2a711c67a Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Fri, 10 Jul 2020 11:26:32 -0500 Subject: [PATCH 589/749] Add a test for the direct serendipity --- test/test_direct_serendipity.py | 91 +++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 test/test_direct_serendipity.py diff --git a/test/test_direct_serendipity.py b/test/test_direct_serendipity.py new file mode 100644 index 000000000..ebc6c58d8 --- /dev/null +++ b/test/test_direct_serendipity.py @@ -0,0 +1,91 @@ +import pytest +import FIAT +import finat +import gem +import numpy as np +from finat.physically_mapped import PhysicalGeometry + + +class MyMapping(PhysicalGeometry): + def __init__(self, cell, verts): + # cell is reference cell, verts is physical vertices + self.verts = np.asarray(verts) + + def cell_size(self): + return gem.Literal(np.full(self.cell.num_vertices(), 1, dtype=np.int32)) + + def jacobian_at(self, point): + raise NotImplementedError + + def reference_normals(self): + raise NotImplementedError + + def physical_normals(self): + raise NotImplementedError + + def physical_tangents(self): + raise NotImplementedError + + def physical_edge_lengths(self): + raise NotImplementedError + + def physical_points(self, ps, entity=None): + assert entity is None + prefs = ps.points + pvs = self.verts + pps = np.zeros(prefs.shape, dtype=float) + for i in range(pps.shape[0]): + pps[i, :] = (pvs[0, :] * (1-prefs[i, 0]) * (1-prefs[i, 1]) + + pvs[1, :] * (1-prefs[i, 0]) * prefs[i, 1] + + pvs[2, :] * prefs[i, 0] * (1-prefs[i, 1]) + + pvs[3, :] * prefs[i, 0] * prefs[i, 1]) + pps_gem = np.zeros(pps.shape, dtype=object) + for alpha in np.ndindex(pps.shape): + pps_gem[alpha] = gem.Literal(pps[alpha]) + return gem.ListTensor(pps_gem) + + def physical_vertices(self): + pvs = np.zeros(self.verts.shape, dtype=object) + for alpha in np.ndindex(pvs.shape): + pvs[alpha] = gem.Literal(self.verts[alpha]) + return gem.ListTensor(pvs) + + +def get_pts(cell, deg): + assert cell.shape == 11 # quadrilateral + L = cell.construct_subelement(1) + vs = np.asarray(cell.vertices) + pts = [pt for pt in cell.vertices] + Lpts = FIAT.reference_element.make_lattice(L.vertices, deg, 1) + for e in cell.topology[1]: + Fmap = cell.get_entity_transform(1, e) + epts = [tuple(Fmap(pt)) for pt in Lpts] + pts.extend(epts) + if deg > 3: + dx0 = (vs[1, :] - vs[0, :]) / (deg-2) + dx1 = (vs[2, :] - vs[0, :]) / (deg-2) + + internal_nodes = [tuple(vs[0, :] + dx0 * i + dx1 * j) + for i in range(1, deg-2) + for j in range(1, deg-1-i)] + pts.extend(internal_nodes) + return pts + + +@pytest.mark.parametrize('degree', [1, 2, 3, 4]) +def test_kronecker(degree): + cell = FIAT.ufc_cell("quadrilateral") + element = finat.DirectSerendipity(cell, degree) + pts = finat.point_set.PointSet(get_pts(cell, degree)) + vrts = np.asarray(((0.0, 0.0), (1.0, 0.0), (0.1, 1.1), (0.95, 1.01))) + mppng = MyMapping(cell, vrts) + z = tuple([0] * cell.get_spatial_dimension()) + vals = element.basis_evaluation(0, pts, coordinate_mapping=mppng)[z] + from gem.interpreter import evaluate + numvals = evaluate([vals])[0].arr + assert np.allclose(numvals, np.eye(*numvals.shape)) + + +if __name__ == "__main__": + import os + pytest.main(os.path.abspath(__file__)) From 563f9d985dd56aa95c0118075e06aaf4e00d3d24 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Fri, 10 Jul 2020 11:40:44 -0500 Subject: [PATCH 590/749] see if this helps travis --- finat/direct_serendipity.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/finat/direct_serendipity.py b/finat/direct_serendipity.py index 80645e0f7..0e18b291b 100644 --- a/finat/direct_serendipity.py +++ b/finat/direct_serendipity.py @@ -148,7 +148,12 @@ def ds1_sympy(ct, vs=None): v0id, v1id = ct[1][e][:] xstars[e, :] = (vs[v0id, :] + vs[v1id])/2 - lams = [(xx-xstars[i, :]) @ ns[i, :] for i in range(4)] + lams = [] + for i in range(4): + lam = ((xx[0] - xstars[i, 0]) * ns[i, 0] + + (xx[1] - xstars[i, 1]) * ns[i, 1]) + lams.append(lam) + #lams = [(xx-xstars[i, :]) @ ns[i, :] for i in range(4)] RV = (lams[0] - lams[1]) / (lams[0] + lams[1]) RH = (lams[2] - lams[3]) / (lams[2] + lams[3]) From ac8db54acdb254dd302e2bea55e0978d27bfd978 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Fri, 10 Jul 2020 12:15:07 -0500 Subject: [PATCH 591/749] address miklos' concerns on test --- test/test_direct_serendipity.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_direct_serendipity.py b/test/test_direct_serendipity.py index ebc6c58d8..643aba1de 100644 --- a/test/test_direct_serendipity.py +++ b/test/test_direct_serendipity.py @@ -10,6 +10,7 @@ class MyMapping(PhysicalGeometry): def __init__(self, cell, verts): # cell is reference cell, verts is physical vertices self.verts = np.asarray(verts) + self.cell = cell def cell_size(self): return gem.Literal(np.full(self.cell.num_vertices(), 1, dtype=np.int32)) @@ -52,7 +53,7 @@ def physical_vertices(self): def get_pts(cell, deg): - assert cell.shape == 11 # quadrilateral + assert cell.shape == FIAT.reference_element.QUADRILATERAL L = cell.construct_subelement(1) vs = np.asarray(cell.vertices) pts = [pt for pt in cell.vertices] From b4167b98ab3a2e9cb1098376ac95494d6eb0eb94 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Fri, 10 Jul 2020 12:18:57 -0500 Subject: [PATCH 592/749] see if travis like .dot instead of @ --- finat/direct_serendipity.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/finat/direct_serendipity.py b/finat/direct_serendipity.py index 0e18b291b..e429d80ea 100644 --- a/finat/direct_serendipity.py +++ b/finat/direct_serendipity.py @@ -148,12 +148,7 @@ def ds1_sympy(ct, vs=None): v0id, v1id = ct[1][e][:] xstars[e, :] = (vs[v0id, :] + vs[v1id])/2 - lams = [] - for i in range(4): - lam = ((xx[0] - xstars[i, 0]) * ns[i, 0] - + (xx[1] - xstars[i, 1]) * ns[i, 1]) - lams.append(lam) - #lams = [(xx-xstars[i, :]) @ ns[i, :] for i in range(4)] + lams = [numpy.dot(xx-xstars[i, :], ns[i, :]) for i in range(4)] RV = (lams[0] - lams[1]) / (lams[0] + lams[1]) RH = (lams[2] - lams[3]) / (lams[2] + lams[3]) From 9000a24fb8375e77ec52530d8d84e27fbb613ef7 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Fri, 10 Jul 2020 12:24:07 -0500 Subject: [PATCH 593/749] simplify mapping class via gem.Literal --- test/test_direct_serendipity.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/test/test_direct_serendipity.py b/test/test_direct_serendipity.py index 643aba1de..196978c87 100644 --- a/test/test_direct_serendipity.py +++ b/test/test_direct_serendipity.py @@ -13,7 +13,7 @@ def __init__(self, cell, verts): self.cell = cell def cell_size(self): - return gem.Literal(np.full(self.cell.num_vertices(), 1, dtype=np.int32)) + raise NotImplementedError def jacobian_at(self, point): raise NotImplementedError @@ -40,16 +40,10 @@ def physical_points(self, ps, entity=None): + pvs[1, :] * (1-prefs[i, 0]) * prefs[i, 1] + pvs[2, :] * prefs[i, 0] * (1-prefs[i, 1]) + pvs[3, :] * prefs[i, 0] * prefs[i, 1]) - pps_gem = np.zeros(pps.shape, dtype=object) - for alpha in np.ndindex(pps.shape): - pps_gem[alpha] = gem.Literal(pps[alpha]) - return gem.ListTensor(pps_gem) + return gem.Literal(pps) def physical_vertices(self): - pvs = np.zeros(self.verts.shape, dtype=object) - for alpha in np.ndindex(pvs.shape): - pvs[alpha] = gem.Literal(self.verts[alpha]) - return gem.ListTensor(pvs) + return gem.Literal(self.verts) def get_pts(cell, deg): From 0b17b8a08a8474c81303934034c426a592519b04 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Fri, 10 Jul 2020 12:59:38 -0500 Subject: [PATCH 594/749] Put back --- finat/direct_serendipity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/direct_serendipity.py b/finat/direct_serendipity.py index e429d80ea..80645e0f7 100644 --- a/finat/direct_serendipity.py +++ b/finat/direct_serendipity.py @@ -148,7 +148,7 @@ def ds1_sympy(ct, vs=None): v0id, v1id = ct[1][e][:] xstars[e, :] = (vs[v0id, :] + vs[v1id])/2 - lams = [numpy.dot(xx-xstars[i, :], ns[i, :]) for i in range(4)] + lams = [(xx-xstars[i, :]) @ ns[i, :] for i in range(4)] RV = (lams[0] - lams[1]) / (lams[0] + lams[1]) RH = (lams[2] - lams[3]) / (lams[2] + lams[3]) From 72559c9e72f3dcea56dcaf0d3d708de50c570f6d Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Fri, 10 Jul 2020 13:01:00 -0500 Subject: [PATCH 595/749] Cultic deprogramming --- test/test_direct_serendipity.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/test_direct_serendipity.py b/test/test_direct_serendipity.py index 196978c87..09ab8b36f 100644 --- a/test/test_direct_serendipity.py +++ b/test/test_direct_serendipity.py @@ -79,8 +79,3 @@ def test_kronecker(degree): from gem.interpreter import evaluate numvals = evaluate([vals])[0].arr assert np.allclose(numvals, np.eye(*numvals.shape)) - - -if __name__ == "__main__": - import os - pytest.main(os.path.abspath(__file__)) From a49b61d4f9010537318eb91389323421ecec96fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Homolya?= Date: Fri, 10 Jul 2020 20:56:22 +0200 Subject: [PATCH 596/749] Do not cast to real Cast triggers warning for FInAT/FInAT#58. --- gem/interpreter.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/gem/interpreter.py b/gem/interpreter.py index 7d1fe2806..13eeb44a2 100644 --- a/gem/interpreter.py +++ b/gem/interpreter.py @@ -320,11 +320,7 @@ def _evaluate_listtensor(e, self): """List tensors just turn into arrays.""" ops = [self(o) for o in e.children] tmp = Result.empty(*ops) - arrs = [] - for o in ops: - arr = numpy.empty(tmp.fshape) - arr[:] = o.broadcast(tmp.fids) - arrs.append(arr) + arrs = [numpy.broadcast_to(o.broadcast(tmp.fids), tmp.fshape) for o in ops] arrs = numpy.moveaxis(numpy.asarray(arrs), 0, -1).reshape(tmp.fshape + e.shape) return Result(arrs, tmp.fids) From b13fdd6cbc51a391c7d84784f01b58bbf294c9bd Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Fri, 10 Jul 2020 15:22:15 -0500 Subject: [PATCH 597/749] Add a test for Morley using the gem interpreter. --- test/test_morley.py | 82 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 test/test_morley.py diff --git a/test/test_morley.py b/test/test_morley.py new file mode 100644 index 000000000..f1ed85311 --- /dev/null +++ b/test/test_morley.py @@ -0,0 +1,82 @@ +import pytest +import FIAT +import finat +import gem +import numpy as np +from finat.physically_mapped import PhysicalGeometry +from gem.interpreter import evaluate + + +class MyMapping(PhysicalGeometry): + def __init__(self, ref_cell, phys_cell): + self.ref_cell = ref_cell + self.phys_cell = phys_cell + + self.A, self.b = FIAT.reference_element.make_affine_mapping( + self.ref_cell.vertices, + self.phys_cell.vertices) + + def cell_size(self): + # Firedrake interprets this as 2x the circumradius + # cs = (np.prod([self.phys_cell.volume_of_subcomplex(1, i) + # for i in range(3)]) + # / 2.0 / self.phys_cell.volume()) + # return np.asarray([cs for _ in range(3)]) + # Currently, just return 1 so we can compare FIAT dofs + # to transformed dofs. + + return np.ones((3,)) + + def detJ_at(self, point): + return self.A + + def jacobian_at(self, point): + return self.A + + def reference_normals(self): + return gem.Literal( + np.asarray([self.ref_cell.compute_normal(i) + for i in range(3)])) + + def physical_normals(self): + return gem.Literal( + np.asarray([self.phys_cell.compute_normal(i) + for i in range(3)])) + + def physical_tangents(self): + return gem.Literal( + np.asarray([self.phys_cell.compute_normalized_edge_tangent(i) + for i in range(3)])) + + def physical_edge_lengths(self): + return gem.Literal( + np.asarray([self.phys_cell.volume_of_subcomplex(1, i) + for i in range(3)])) + + def physical_points(self, ps, entity=None): + prefs = ps.points + A, b = self.A, self.b + return gem.Literal(np.asarray([A @ x + b for x in prefs])) + + def physical_vertices(self): + return gem.Literal(self.phys_cell.verts) + + +def test_morley(): + ref_cell = FIAT.ufc_simplex(2) + ref_element = finat.Morley(ref_cell, 2) + ref_pts = finat.point_set.PointSet(ref_cell.make_points(2, 0, 4)) + + phys_cell = FIAT.ufc_simplex(2) + phys_cell.vertices = ((0.0, 0.1), (1.17, -0.09), (0.15, 1.84)) + + mppng = MyMapping(ref_cell, phys_cell) + z = (0, 0) + finat_vals_gem = ref_element.basis_evaluation(0, ref_pts, coordinate_mapping=mppng)[z] + finat_vals = evaluate([finat_vals_gem])[0].arr + + phys_cell_FIAT = FIAT.Morley(phys_cell) + phys_points = phys_cell.make_points(2, 0, 4) + phys_vals = phys_cell_FIAT.tabulate(0, phys_points)[z] + + assert np.allclose(finat_vals, phys_vals.T) From e4c326d0bb032df4a62a24ca877af609543d39ab Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Fri, 10 Jul 2020 15:28:38 -0500 Subject: [PATCH 598/749] Refactor FIAT-based physical mapping into a separate file to facilitate reuse --- test/fiat_mapping.py | 59 ++++++++++++++++++++++++++++++++++++++++++++ test/test_morley.py | 58 +------------------------------------------ 2 files changed, 60 insertions(+), 57 deletions(-) create mode 100644 test/fiat_mapping.py diff --git a/test/fiat_mapping.py b/test/fiat_mapping.py new file mode 100644 index 000000000..bfcaf18fc --- /dev/null +++ b/test/fiat_mapping.py @@ -0,0 +1,59 @@ +import FIAT +import numpy as np +import gem +from finat.physically_mapped import PhysicalGeometry + + +class MyMapping(PhysicalGeometry): + def __init__(self, ref_cell, phys_cell): + self.ref_cell = ref_cell + self.phys_cell = phys_cell + + self.A, self.b = FIAT.reference_element.make_affine_mapping( + self.ref_cell.vertices, + self.phys_cell.vertices) + + def cell_size(self): + # Firedrake interprets this as 2x the circumradius + # cs = (np.prod([self.phys_cell.volume_of_subcomplex(1, i) + # for i in range(3)]) + # / 2.0 / self.phys_cell.volume()) + # return np.asarray([cs for _ in range(3)]) + # Currently, just return 1 so we can compare FIAT dofs + # to transformed dofs. + + return np.ones((3,)) + + def detJ_at(self, point): + return self.A + + def jacobian_at(self, point): + return self.A + + def reference_normals(self): + return gem.Literal( + np.asarray([self.ref_cell.compute_normal(i) + for i in range(3)])) + + def physical_normals(self): + return gem.Literal( + np.asarray([self.phys_cell.compute_normal(i) + for i in range(3)])) + + def physical_tangents(self): + return gem.Literal( + np.asarray([self.phys_cell.compute_normalized_edge_tangent(i) + for i in range(3)])) + + def physical_edge_lengths(self): + return gem.Literal( + np.asarray([self.phys_cell.volume_of_subcomplex(1, i) + for i in range(3)])) + + def physical_points(self, ps, entity=None): + prefs = ps.points + A, b = self.A, self.b + return gem.Literal(np.asarray([A @ x + b for x in prefs])) + + def physical_vertices(self): + return gem.Literal(self.phys_cell.verts) diff --git a/test/test_morley.py b/test/test_morley.py index f1ed85311..d80336f36 100644 --- a/test/test_morley.py +++ b/test/test_morley.py @@ -1,65 +1,9 @@ import pytest import FIAT import finat -import gem import numpy as np -from finat.physically_mapped import PhysicalGeometry from gem.interpreter import evaluate - - -class MyMapping(PhysicalGeometry): - def __init__(self, ref_cell, phys_cell): - self.ref_cell = ref_cell - self.phys_cell = phys_cell - - self.A, self.b = FIAT.reference_element.make_affine_mapping( - self.ref_cell.vertices, - self.phys_cell.vertices) - - def cell_size(self): - # Firedrake interprets this as 2x the circumradius - # cs = (np.prod([self.phys_cell.volume_of_subcomplex(1, i) - # for i in range(3)]) - # / 2.0 / self.phys_cell.volume()) - # return np.asarray([cs for _ in range(3)]) - # Currently, just return 1 so we can compare FIAT dofs - # to transformed dofs. - - return np.ones((3,)) - - def detJ_at(self, point): - return self.A - - def jacobian_at(self, point): - return self.A - - def reference_normals(self): - return gem.Literal( - np.asarray([self.ref_cell.compute_normal(i) - for i in range(3)])) - - def physical_normals(self): - return gem.Literal( - np.asarray([self.phys_cell.compute_normal(i) - for i in range(3)])) - - def physical_tangents(self): - return gem.Literal( - np.asarray([self.phys_cell.compute_normalized_edge_tangent(i) - for i in range(3)])) - - def physical_edge_lengths(self): - return gem.Literal( - np.asarray([self.phys_cell.volume_of_subcomplex(1, i) - for i in range(3)])) - - def physical_points(self, ps, entity=None): - prefs = ps.points - A, b = self.A, self.b - return gem.Literal(np.asarray([A @ x + b for x in prefs])) - - def physical_vertices(self): - return gem.Literal(self.phys_cell.verts) +from fiat_mapping import MyMapping def test_morley(): From 6173e08fd452a7d438a8c349eb95761b950f715c Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Fri, 10 Jul 2020 15:34:05 -0500 Subject: [PATCH 599/749] Wrap some stuff into gem: --- test/fiat_mapping.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/fiat_mapping.py b/test/fiat_mapping.py index bfcaf18fc..2a744019e 100644 --- a/test/fiat_mapping.py +++ b/test/fiat_mapping.py @@ -25,10 +25,10 @@ def cell_size(self): return np.ones((3,)) def detJ_at(self, point): - return self.A + return gem.Literal(np.linalg.det(self.A)) def jacobian_at(self, point): - return self.A + return gem.Literal(self.A) def reference_normals(self): return gem.Literal( From 64dddd0f33bebcac6e7a78566253d1ff887632ca Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Fri, 10 Jul 2020 15:41:59 -0500 Subject: [PATCH 600/749] Add (broken) AWc test for debugging purposes --- test/test_awc.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 test/test_awc.py diff --git a/test/test_awc.py b/test/test_awc.py new file mode 100644 index 000000000..e443df56f --- /dev/null +++ b/test/test_awc.py @@ -0,0 +1,27 @@ +import pytest +import FIAT +import finat +import numpy as np +from gem.interpreter import evaluate +from fiat_mapping import MyMapping + + +def test_morley(): + ref_cell = FIAT.ufc_simplex(2) + ref_element = finat.ArnoldWinther(ref_cell, 3) + ref_pts = finat.point_set.PointSet(ref_cell.make_points(2, 0, 3)) + + phys_cell = FIAT.ufc_simplex(2) + phys_cell.vertices = ((0.0, 0.1), (1.17, -0.09), (0.15, 1.84)) + + mppng = MyMapping(ref_cell, phys_cell) + z = (0, 0) + finat_vals_gem = ref_element.basis_evaluation(0, ref_pts, coordinate_mapping=mppng)[z] + finat_vals = evaluate([finat_vals_gem])[0].arr + + phys_cell_FIAT = FIAT.ArnoldWinther(phys_cell, 3) + phys_points = phys_cell.make_points(2, 0, 3) + phys_vals = phys_cell_FIAT.tabulate(0, phys_points)[z] + phys_vals = phys_vals[:24].transpose((3, 0, 1, 2)) + + assert(np.allclose(finat_vals, phys_vals)) From 3b0b193c3f21c76d34767ea866d873dd75b1871c Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Fri, 10 Jul 2020 16:28:56 -0500 Subject: [PATCH 601/749] WIP: redo AW test to include Piola --- test/test_awc.py | 41 ++++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/test/test_awc.py b/test/test_awc.py index e443df56f..1f0178b78 100644 --- a/test/test_awc.py +++ b/test/test_awc.py @@ -6,22 +6,41 @@ from fiat_mapping import MyMapping -def test_morley(): +def test_awc(): ref_cell = FIAT.ufc_simplex(2) - ref_element = finat.ArnoldWinther(ref_cell, 3) - ref_pts = finat.point_set.PointSet(ref_cell.make_points(2, 0, 3)) + ref_element = FIAT.ArnoldWinther(ref_cell, 3) + ref_el_finat = finat.ArnoldWinther(ref_cell, 3) + ref_pts = ref_cell.make_points(2, 0, 3) + ref_vals = ref_element.tabulate(0, ref_pts)[0, 0] + print(ref_vals.shape) phys_cell = FIAT.ufc_simplex(2) phys_cell.vertices = ((0.0, 0.1), (1.17, -0.09), (0.15, 1.84)) + phys_element = FIAT.ArnoldWinther(phys_cell, 3) + phys_pts = phys_cell.make_points(2, 0, 3) + phys_vals = phys_element.tabulate(0, phys_pts)[0, 0] + # Piola map the reference elements + J, b = FIAT.reference_element.make_affine_mapping(ref_cell.vertices, + phys_cell.vertices) + detJ = np.linalg.det(J) + + ref_vals_piola = np.zeros(ref_vals.shape) + for i in range(ref_vals.shape[0]): + for k in range(ref_vals.shape[3]): + ref_vals_piola[i, :, :, k] = \ + J @ ref_vals[i, :, :, k] @ J / detJ**2 + + # Zany map the results mppng = MyMapping(ref_cell, phys_cell) - z = (0, 0) - finat_vals_gem = ref_element.basis_evaluation(0, ref_pts, coordinate_mapping=mppng)[z] - finat_vals = evaluate([finat_vals_gem])[0].arr + Mgem = ref_el_finat.basis_transformation(mppng) + M = evaluate([Mgem])[0].arr + ref_vals_zany = np.zeros(ref_vals_piola.shape) + for k in range(ref_vals_zany.shape[3]): + for ell1 in range(2): + for ell2 in range(2): + ref_vals_zany[:, ell1, ell2, k] = \ + M @ ref_vals_piola[:, ell1, ell2, k] - phys_cell_FIAT = FIAT.ArnoldWinther(phys_cell, 3) - phys_points = phys_cell.make_points(2, 0, 3) - phys_vals = phys_cell_FIAT.tabulate(0, phys_points)[z] - phys_vals = phys_vals[:24].transpose((3, 0, 1, 2)) + assert np.allclose(ref_vals_zany, phys_vals) - assert(np.allclose(finat_vals, phys_vals)) From d2099ad6933412bdc7db1c9733bc13f82b796c82 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Fri, 10 Jul 2020 16:32:09 -0500 Subject: [PATCH 602/749] Test now runs but fails. To debug later --- test/test_awc.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/test_awc.py b/test/test_awc.py index 1f0178b78..2098b78ee 100644 --- a/test/test_awc.py +++ b/test/test_awc.py @@ -12,7 +12,6 @@ def test_awc(): ref_el_finat = finat.ArnoldWinther(ref_cell, 3) ref_pts = ref_cell.make_points(2, 0, 3) ref_vals = ref_element.tabulate(0, ref_pts)[0, 0] - print(ref_vals.shape) phys_cell = FIAT.ufc_simplex(2) phys_cell.vertices = ((0.0, 0.1), (1.17, -0.09), (0.15, 1.84)) @@ -35,12 +34,14 @@ def test_awc(): mppng = MyMapping(ref_cell, phys_cell) Mgem = ref_el_finat.basis_transformation(mppng) M = evaluate([Mgem])[0].arr - ref_vals_zany = np.zeros(ref_vals_piola.shape) + print(M.shape) + print(ref_vals_piola.shape) + ref_vals_zany = np.zeros((24, 2, 2, len(phys_pts))) for k in range(ref_vals_zany.shape[3]): for ell1 in range(2): for ell2 in range(2): ref_vals_zany[:, ell1, ell2, k] = \ M @ ref_vals_piola[:, ell1, ell2, k] - assert np.allclose(ref_vals_zany, phys_vals) + assert np.allclose(ref_vals_zany, phys_vals[:24]) From eca17f8433449db03f2fb3a900bd81ce3dd56599 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Mon, 13 Jul 2020 11:54:21 -0500 Subject: [PATCH 603/749] Update test for AW --- test/test_aw.py | 98 ++++++++++++++++++++++++++++++++++++++++++++++++ test/test_awc.py | 47 ----------------------- 2 files changed, 98 insertions(+), 47 deletions(-) create mode 100644 test/test_aw.py delete mode 100644 test/test_awc.py diff --git a/test/test_aw.py b/test/test_aw.py new file mode 100644 index 000000000..090060f04 --- /dev/null +++ b/test/test_aw.py @@ -0,0 +1,98 @@ +import pytest +import FIAT +import finat +import numpy as np +from gem.interpreter import evaluate +from fiat_mapping import MyMapping + + +def test_awnc(): + ref_cell = FIAT.ufc_simplex(2) + ref_el_finat = finat.ArnoldWintherNC(ref_cell, 2) + ref_element = ref_el_finat._element + ref_pts = ref_cell.make_points(2, 0, 3) + ref_vals = ref_element.tabulate(0, ref_pts)[0, 0] + + phys_cell = FIAT.ufc_simplex(2) + phys_cell.vertices = ((0.0, 0.0), (2.0, 0.1), (0.0, 1.0)) + phys_element = ref_element.__class__(phys_cell, 2) + + phys_pts = phys_cell.make_points(2, 0, 3) + phys_vals = phys_element.tabulate(0, phys_pts)[0, 0] + + # Piola map the reference elements + J, b = FIAT.reference_element.make_affine_mapping(ref_cell.vertices, + phys_cell.vertices) + detJ = np.linalg.det(J) + + ref_vals_piola = np.zeros(ref_vals.shape) + for i in range(ref_vals.shape[0]): + for k in range(ref_vals.shape[3]): + ref_vals_piola[i, :, :, k] = \ + J @ ref_vals[i, :, :, k] @ J.T / detJ**2 + + # Zany map the results + mppng = MyMapping(ref_cell, phys_cell) + Mgem = ref_el_finat.basis_transformation(mppng) + M = evaluate([Mgem])[0].arr + # print(M) + # print(ref_vals_piola.shape) + ref_vals_zany = np.zeros((15, 2, 2, len(phys_pts))) + for k in range(ref_vals_zany.shape[3]): + for ell1 in range(2): + for ell2 in range(2): + ref_vals_zany[:, ell1, ell2, k] = \ + M @ ref_vals_piola[:, ell1, ell2, k] + + # print(np.linalg.norm(ref_vals_zany[:2, :, :, 0] + # - phys_vals[:2, :, :, 0])) + # print(np.linalg.norm(ref_vals_zany[2:4, :, :, 0] + # - phys_vals[2:4, :, :, 0])) + assert np.allclose(ref_vals_zany[:12], phys_vals[:12]) + + +def test_awc(): + ref_cell = FIAT.ufc_simplex(2) + ref_element = FIAT.ArnoldWinther(ref_cell, 3) + ref_el_finat = finat.ArnoldWinther(ref_cell, 3) + ref_pts = ref_cell.make_points(2, 0, 3) + ref_vals = ref_element.tabulate(0, ref_pts)[0, 0] + + phys_cell = FIAT.ufc_simplex(2) + phys_cell.vertices = ((0.0, 0.0), (1.0, 0.1), (0.0, 2.0)) + phys_element = FIAT.ArnoldWinther(phys_cell, 3) + phys_pts = phys_cell.make_points(2, 0, 3) + phys_vals = phys_element.tabulate(0, phys_pts)[0, 0] + + # Piola map the reference elements + J, b = FIAT.reference_element.make_affine_mapping(ref_cell.vertices, + phys_cell.vertices) + detJ = np.linalg.det(J) + + ref_vals_piola = np.zeros(ref_vals.shape) + for i in range(ref_vals.shape[0]): + for k in range(ref_vals.shape[3]): + ref_vals_piola[i, :, :, k] = \ + J @ ref_vals[i, :, :, k] @ J.T / detJ**2 + + # Zany map the results + mppng = MyMapping(ref_cell, phys_cell) + Mgem = ref_el_finat.basis_transformation(mppng) + M = evaluate([Mgem])[0].arr + # print(M) + # print(ref_vals_piola.shape) + ref_vals_zany = np.zeros((24, 2, 2, len(phys_pts))) + for k in range(ref_vals_zany.shape[3]): + for ell1 in range(2): + for ell2 in range(2): + ref_vals_zany[:, ell1, ell2, k] = \ + M @ ref_vals_piola[:, ell1, ell2, k] + + print() + print(np.linalg.norm(ref_vals_zany[:9, :, :, 0] + - phys_vals[:9, :, :, 0])) + print(np.linalg.norm(ref_vals_zany[9:21, :, :, 0] + - phys_vals[9:21, :, :, 0])) + # print(np.linalg.norm(ref_vals_zany[21:24, :, :, 0] + # - phys_vals[21:24, :, :, 0])) + assert np.allclose(ref_vals_zany[:21], phys_vals[:21]) diff --git a/test/test_awc.py b/test/test_awc.py deleted file mode 100644 index 2098b78ee..000000000 --- a/test/test_awc.py +++ /dev/null @@ -1,47 +0,0 @@ -import pytest -import FIAT -import finat -import numpy as np -from gem.interpreter import evaluate -from fiat_mapping import MyMapping - - -def test_awc(): - ref_cell = FIAT.ufc_simplex(2) - ref_element = FIAT.ArnoldWinther(ref_cell, 3) - ref_el_finat = finat.ArnoldWinther(ref_cell, 3) - ref_pts = ref_cell.make_points(2, 0, 3) - ref_vals = ref_element.tabulate(0, ref_pts)[0, 0] - - phys_cell = FIAT.ufc_simplex(2) - phys_cell.vertices = ((0.0, 0.1), (1.17, -0.09), (0.15, 1.84)) - phys_element = FIAT.ArnoldWinther(phys_cell, 3) - phys_pts = phys_cell.make_points(2, 0, 3) - phys_vals = phys_element.tabulate(0, phys_pts)[0, 0] - - # Piola map the reference elements - J, b = FIAT.reference_element.make_affine_mapping(ref_cell.vertices, - phys_cell.vertices) - detJ = np.linalg.det(J) - - ref_vals_piola = np.zeros(ref_vals.shape) - for i in range(ref_vals.shape[0]): - for k in range(ref_vals.shape[3]): - ref_vals_piola[i, :, :, k] = \ - J @ ref_vals[i, :, :, k] @ J / detJ**2 - - # Zany map the results - mppng = MyMapping(ref_cell, phys_cell) - Mgem = ref_el_finat.basis_transformation(mppng) - M = evaluate([Mgem])[0].arr - print(M.shape) - print(ref_vals_piola.shape) - ref_vals_zany = np.zeros((24, 2, 2, len(phys_pts))) - for k in range(ref_vals_zany.shape[3]): - for ell1 in range(2): - for ell2 in range(2): - ref_vals_zany[:, ell1, ell2, k] = \ - M @ ref_vals_piola[:, ell1, ell2, k] - - assert np.allclose(ref_vals_zany, phys_vals[:24]) - From 1e62cd78a1168b36b9851dec200b9fcc3eed86d9 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Mon, 13 Jul 2020 13:26:20 -0500 Subject: [PATCH 604/749] Clean up test --- test/test_aw.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/test/test_aw.py b/test/test_aw.py index 090060f04..e49756264 100644 --- a/test/test_aw.py +++ b/test/test_aw.py @@ -44,10 +44,6 @@ def test_awnc(): ref_vals_zany[:, ell1, ell2, k] = \ M @ ref_vals_piola[:, ell1, ell2, k] - # print(np.linalg.norm(ref_vals_zany[:2, :, :, 0] - # - phys_vals[:2, :, :, 0])) - # print(np.linalg.norm(ref_vals_zany[2:4, :, :, 0] - # - phys_vals[2:4, :, :, 0])) assert np.allclose(ref_vals_zany[:12], phys_vals[:12]) @@ -59,7 +55,8 @@ def test_awc(): ref_vals = ref_element.tabulate(0, ref_pts)[0, 0] phys_cell = FIAT.ufc_simplex(2) - phys_cell.vertices = ((0.0, 0.0), (1.0, 0.1), (0.0, 2.0)) + pvs = np.array(((0.0, 0.0), (1.0, 0.1), (0.0, 2.0))) + phys_cell.vertices = pvs * 0.5 phys_element = FIAT.ArnoldWinther(phys_cell, 3) phys_pts = phys_cell.make_points(2, 0, 3) phys_vals = phys_element.tabulate(0, phys_pts)[0, 0] @@ -88,11 +85,13 @@ def test_awc(): ref_vals_zany[:, ell1, ell2, k] = \ M @ ref_vals_piola[:, ell1, ell2, k] - print() - print(np.linalg.norm(ref_vals_zany[:9, :, :, 0] - - phys_vals[:9, :, :, 0])) - print(np.linalg.norm(ref_vals_zany[9:21, :, :, 0] - - phys_vals[9:21, :, :, 0])) - # print(np.linalg.norm(ref_vals_zany[21:24, :, :, 0] - # - phys_vals[21:24, :, :, 0])) + # Q = FIAT.make_quadrature(phys_cell, 6) + # vals = phys_element.tabulate(0, Q.pts)[(0, 0)] + # print() + # for i in (0, 9, 21): + # result = 0.0 + # for k in range(len(Q.wts)): + # result += Q.wts[k] * (vals[i, :, :, k] ** 2).sum() + # print(np.sqrt(result)) + assert np.allclose(ref_vals_zany[:21], phys_vals[:21]) From e9c7228205ab395dea5b85d7725ce32efe98add7 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Mon, 13 Jul 2020 13:26:53 -0500 Subject: [PATCH 605/749] Turn of doff scaling --- finat/aw.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/finat/aw.py b/finat/aw.py index b4e7ce349..111044fd3 100644 --- a/finat/aw.py +++ b/finat/aw.py @@ -159,26 +159,22 @@ def basis_transformation(self, coordinate_mapping): # internal dofs (AWnc has good conditioning, so leave this alone) for i in range(21, 24): - V[i, i] = Literal(1) + V[i, i] = Literal(1) #V[21:24, 21:24] = W_check - h = coordinate_mapping.cell_size() - for v in range(3): - for c in range(3): - for i in range(30): - V[i, 3*v+c] = V[i, 3*v+c] + # h = coordinate_mapping.cell_size() + # for v in range(3): + # for c in range(3): + # for i in range(30): + # V[i, 3*v+c] = V[i, 3*v+c] / h[v] / h[v] + + # for e in range(3): + # v0id, v1id = [i for i in range(3) if i != e] + # he = (h[v0id] + h[v1id]) / 2 + # for j in range(4): + # for i in range(30): + # V[i, 9+4*e+j] = V[i, 9+4*e+j] * h[e] * h[e] - for e in range(3): - v0id, v1id = [i for i in range(3) if i != e] - he = (h[v0id] + h[v1id]) / 2 - for j in range(4): - for i in range(30): - V[i, 9+4*e+j] = V[i, 9+4*e+j] * h[e] * h[e] - - hc = (h[0] + h[1] + h[2]) / 3 - for j in range(3): - for i in range(30): - V[i, 21 + j] = V[i, 21 + j] * hc * hc return ListTensor(V.T) From bd31eb0a007f509bc7842a26c016ce31d4097aac Mon Sep 17 00:00:00 2001 From: FabianL1908 Date: Thu, 7 May 2020 19:40:38 +0200 Subject: [PATCH 606/749] Support variant when creating RT/BDM/Nedelec elements --- finat/fiat_elements.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index b230d788c..bdac53293 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -314,13 +314,13 @@ def value_shape(self): class RaviartThomas(VectorFiatElement): - def __init__(self, cell, degree): - super(RaviartThomas, self).__init__(FIAT.RaviartThomas(cell, degree)) + def __init__(self, cell, degree, variant=None): + super(RaviartThomas, self).__init__(FIAT.RaviartThomas(cell, degree, variant=variant)) class BrezziDouglasMarini(VectorFiatElement): - def __init__(self, cell, degree): - super(BrezziDouglasMarini, self).__init__(FIAT.BrezziDouglasMarini(cell, degree)) + def __init__(self, cell, degree, variant=None): + super(BrezziDouglasMarini, self).__init__(FIAT.BrezziDouglasMarini(cell, degree, variant=variant)) class BrezziDouglasFortinMarini(VectorFiatElement): @@ -329,10 +329,10 @@ def __init__(self, cell, degree): class Nedelec(VectorFiatElement): - def __init__(self, cell, degree): - super(Nedelec, self).__init__(FIAT.Nedelec(cell, degree)) + def __init__(self, cell, degree, variant=None): + super(Nedelec, self).__init__(FIAT.Nedelec(cell, degree, variant=variant)) class NedelecSecondKind(VectorFiatElement): - def __init__(self, cell, degree): - super(NedelecSecondKind, self).__init__(FIAT.NedelecSecondKind(cell, degree)) + def __init__(self, cell, degree, variant=None): + super(NedelecSecondKind, self).__init__(FIAT.NedelecSecondKind(cell, degree, variant=variant)) From f2f6fd5dbb7d91b263d495d0cd8d6ef885d17ccc Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Thu, 16 Jul 2020 16:19:02 -0500 Subject: [PATCH 607/749] fix entity_closure_dofs --- finat/mtw.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/finat/mtw.py b/finat/mtw.py index 45830db5a..27f474594 100644 --- a/finat/mtw.py +++ b/finat/mtw.py @@ -49,7 +49,6 @@ def basis_transformation(self, coordinate_mapping): return ListTensor(V.T) - def entity_dofs(self): return {0: {0: [], 1: [], @@ -57,6 +56,13 @@ def entity_dofs(self): 1: {0: [0, 1, 2], 1: [3, 4, 5], 2: [6, 7, 8]}, 2: {0: []}} + def entity_closure_dofs(self): + return {0: {0: [], + 1: [], + 2: []}, + 1: {0: [0, 1, 2], 1: [3, 4, 5], 2: [6, 7, 8]}, + 2: {0: [0, 1, 2, 3, 4, 5, 6, 7, 8]}} + @property def index_shape(self): From 8e9f1d275b63a3409c080c9f029cf094093cb88d Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Wed, 5 Aug 2020 11:15:11 -0500 Subject: [PATCH 608/749] Fix bug in higher-order elements --- finat/direct_serendipity.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/finat/direct_serendipity.py b/finat/direct_serendipity.py index 80645e0f7..3b749478b 100644 --- a/finat/direct_serendipity.py +++ b/finat/direct_serendipity.py @@ -223,7 +223,7 @@ def dsr_sympy(ct, r, vs=None): physical cell independent variables (e.g. "x" and "y") and a list of the four basis functions. """ - if vs is None: + if vs is None: # do vertices symbolically vs = numpy.asarray(list(zip(sympy.symbols('x:4'), sympy.symbols('y:4')))) else: @@ -324,10 +324,9 @@ def dsr_sympy(ct, r, vs=None): # subtracts off the value of function at internal nodes times those # internal basis functions def nodalize(f): - foo = f - for (bf, nd) in zip(internal_bfs, internal_nodes): - foo = foo - f.subs(xx, nd) * bf - return foo + #return f + return f - sum(f.subs(xysub(xx, nd)) * bf + for bf, nd in zip(internal_bfs, internal_nodes)) edge_bfs = [] if r == 2: @@ -371,8 +370,10 @@ def nodalize(f): * (lams[opposite_edges[ed]] * p + Rcur**(r-2) * Rs[ed])) - bfcur = (nodalize(prebf) - / prebf.subs(xysub(xx, edge_nodes[ed][i]))) + prebf = nodalize(prebf) + bfcur = prebf / prebf.subs(xysub(xx, edge_nodes[ed][i])) + # bfcur = (nodalize(prebf) + # / prebf.subs(xysub(xx, edge_nodes[ed][i]))) edge_bfs_cur.append(bfcur) edge_bfs.append(edge_bfs_cur) From e91ea1cdeeb2fe9e11dd48a72de58e39ec360461 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Wed, 5 Aug 2020 12:17:26 -0500 Subject: [PATCH 609/749] Remove comments/flake8 --- finat/direct_serendipity.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/finat/direct_serendipity.py b/finat/direct_serendipity.py index 3b749478b..b7d9006b3 100644 --- a/finat/direct_serendipity.py +++ b/finat/direct_serendipity.py @@ -324,7 +324,6 @@ def dsr_sympy(ct, r, vs=None): # subtracts off the value of function at internal nodes times those # internal basis functions def nodalize(f): - #return f return f - sum(f.subs(xysub(xx, nd)) * bf for bf, nd in zip(internal_bfs, internal_nodes)) @@ -372,8 +371,6 @@ def nodalize(f): prebf = nodalize(prebf) bfcur = prebf / prebf.subs(xysub(xx, edge_nodes[ed][i])) - # bfcur = (nodalize(prebf) - # / prebf.subs(xysub(xx, edge_nodes[ed][i]))) edge_bfs_cur.append(bfcur) edge_bfs.append(edge_bfs_cur) From 75f5f058013b04199d38c5e4bbce2f6c0d6f7157 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Wed, 5 Aug 2020 12:20:25 -0500 Subject: [PATCH 610/749] Another cleanup, including comments --- finat/direct_serendipity.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/finat/direct_serendipity.py b/finat/direct_serendipity.py index b7d9006b3..70df1b8c3 100644 --- a/finat/direct_serendipity.py +++ b/finat/direct_serendipity.py @@ -116,6 +116,8 @@ def ds1_sympy(ct, vs=None): """Constructs lowest-order case of Arbogast's directly defined C^0 serendipity elements, which are a special case. :param ct: The cell topology of the reference quadrilateral. + :param vs: (Optional) coordinates of cell on which to construct the basis. + If it is None, this function constructs symbols for the vertices. :returns: a 3-tuple containing symbols for the physical cell coordinates and the physical cell independent variables (e.g. "x" and "y") and a list of the four basis functions. @@ -219,6 +221,8 @@ def dsr_sympy(ct, r, vs=None): elements, which include all polynomials of degree r plus a couple of rational functions. :param ct: The cell topology of the reference quadrilateral. + :param vs: (Optional) coordinates of cell on which to construct the basis. + If it is None, this function constructs symbols for the vertices. :returns: a 3-tuple containing symbols for the physical cell coordinates and the physical cell independent variables (e.g. "x" and "y") and a list of the four basis functions. @@ -308,7 +312,6 @@ def dsr_sympy(ct, r, vs=None): and set(ct[1][e]).intersection(ct[1][eother]) != set()])) for e in ct[1]} - # adjacent_edges = {0: (2, 3), 1: (2, 3), 2: (0, 1), 3: (0, 1)} ae = adjacent_edges tunnel_R_edges = {e: ((lams[ae[e][0]] - lams[ae[e][1]]) @@ -414,6 +417,15 @@ def nodalize(f): def ds_sympy(ct, r, vs=None): + """Symbolically Constructs Arbogast's directly defined C^0 serendipity elements, + which include all polynomials of degree r plus a couple of rational functions. + :param ct: The cell topology of the reference quadrilateral. + :param vs: (Optional) coordinates of cell on which to construct the basis. + If it is None, this function constructs symbols for the vertices. + :returns: a 3-tuple containing symbols for the physical cell coordinates and the + physical cell independent variables (e.g. "x" and "y") and a list + of the four basis functions. + """ if r == 1: return ds1_sympy(ct, vs) else: From d6dec6ba06b924b96d5f9f441777ea48ab7e34f2 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Tue, 11 Aug 2020 14:59:12 -0500 Subject: [PATCH 611/749] Add KMV elements (WIP) --- finat/__init__.py | 1 + finat/fiat_elements.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/finat/__init__.py b/finat/__init__.py index 5d19003e6..cf34c2ada 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -5,6 +5,7 @@ from .fiat_elements import Nedelec, NedelecSecondKind, RaviartThomas # noqa: F401 from .fiat_elements import HellanHerrmannJohnson, Regge # noqa: F401 from .fiat_elements import FacetBubble # noqa: F401 +from .fiat_elements import KongMulderVeldhuizen # noqa: F401 from .argyris import Argyris # noqa: F401 from .bell import Bell # noqa: F401 from .hermite import Hermite # noqa: F401 diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index bdac53293..0fe1027ea 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -287,6 +287,11 @@ def __init__(self, cell, degree): super(Lagrange, self).__init__(FIAT.Lagrange(cell, degree)) +class KongMulderVeldhuizen(ScalarFiatElement): + def __init__(self, cell, degree): + super(KongMulderVeldhuizen, self).__init__(FIAT.KongMulderVeldhuizen(cell, degree)) + + class DiscontinuousLagrange(ScalarFiatElement): def __init__(self, cell, degree): super(DiscontinuousLagrange, self).__init__(FIAT.DiscontinuousLagrange(cell, degree)) From 0b3519d27e49d6bb827755571939da47ad287ecc Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Thu, 13 Aug 2020 11:15:50 +0100 Subject: [PATCH 612/749] Get docs building without warnings --- docs/source/_themes/finat/static/fenics.css_t | 2 - .../source/_themes/finat/static/fenics.css_t~ | 672 ------------------ docs/source/conf.py | 155 ++-- docs/source/index.rst | 1 + finat/finiteelementbase.py | 2 +- 5 files changed, 83 insertions(+), 749 deletions(-) delete mode 100644 docs/source/_themes/finat/static/fenics.css_t~ diff --git a/docs/source/_themes/finat/static/fenics.css_t b/docs/source/_themes/finat/static/fenics.css_t index 6c6e55de8..87f75eba2 100644 --- a/docs/source/_themes/finat/static/fenics.css_t +++ b/docs/source/_themes/finat/static/fenics.css_t @@ -305,8 +305,6 @@ pre, code { background-color: #fafafa; padding: 10px; margin: 0; - border-top: 2px solid #c6c9cb; - border-bottom: 2px solid #c6c9cb; } tt { diff --git a/docs/source/_themes/finat/static/fenics.css_t~ b/docs/source/_themes/finat/static/fenics.css_t~ deleted file mode 100644 index ccb3eb39c..000000000 --- a/docs/source/_themes/finat/static/fenics.css_t~ +++ /dev/null @@ -1,672 +0,0 @@ -/* - * fenics.css_t - * ~~~~~~~~~~~~ - * - * Sphinx stylesheet -- FEniCS theme. - * This is a modified version of the pylons theme. - * - * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -@import url("basic.css"); -@import url("nobile-fontfacekit/stylesheet.css"); -@import url("neuton-fontfacekit/stylesheet.css"); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: "Nobile", sans-serif; - font-size: 14px; - background-color: #ffffff; - color: #ffffff; - margin: 0; padding: 0; -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - width: 950px; - margin-left: auto; - margin-right: auto; -} - -hr { - border: 1px solid #B1B4B6; -} - -div.document { - background-color: #fff; -} - -div.header{ - width:100%; - height:230px; - background: #f98131 url(headerbg.png) repeat-x 0 top; -} - -div.header-small{ - width:100%; - height:60px; - background: #f98131 url(headerbg.png) repeat-x 0 top; - border-bottom: 2px solid #ffffff; -} - -div.logo { - text-align: center; - padding-top: 50px; -} - -div.logo-small { - text-align: left; - padding: 10px 0 0 250px; -} - -div.body { - background-color: #ffffff; - color: #3E4349; - padding: 0 30px 30px 30px; - font-size: 14px; - overflow: auto; -} - -div.footer { - color: #aaa; - width: 100%; - padding: 13px 0; - text-align: center; - font-size: 13px; - background: #000; - clear:both; -} - -div.footer a { - color: #aaa; - text-decoration: none; -} - -div.footer a:hover { - color: #fff; -} - -div.related { - line-height: 30px; - color: #373839; - font-size: 12px; - background-color: #eee; -} - -div.related a { - color: #1b61d6; -} - -div.related ul { - padding-left: 240px; -} - -/* -- sidebars -------------------------------------------------------------- */ - -div.sidebar { - margin: 0 0 0.5em 1em; - border: 2px solid #8c1b1e; - background-color: #eee; - width: 300px; - float: right; - border-right-style: none; - border-left-style: none; - padding: 0px 0px; -} - -div.commit { - clear: both; - padding: 4px; -} - -div.commit.even { - background-color: #ddd; -} - -img.commit-author-img { - height: 48px; - width: 48px; - float: left; - margin-right: 10px; - display: none; -} - -a.commit-link { - font-weight: bold; - padding-bottom: 4px; -} - -pre.commit { - clear: both; - font-size: 12px; -} - -p.sidebar-title { - font-weight: bold; -} - -div.youtube{ - text-align: center; -} - -/* -- body styles ----------------------------------------------------------- */ - -a, a .pre { - color: #8c1b1e; - text-decoration: none; - word-wrap: break-word; -} - -a:hover, a:hover .pre { - text-decoration: underline; -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: "NeutonRegular", sans-serif; - background-color: #ffffff; - font-weight: normal; - color: #222; - margin: 30px 0px 10px 0px; - padding: 5px 0; -} - -div.body h1 { - font-size: 2.2em; - line-height: 1; - margin-bottom: 0.4em; -} -div.body h2 { - font-size: 1.6em; - line-height: 1; - margin-bottom: 0.6em; -} -div.body h3 { - font-size: 1.2em; - line-height: 1; - margin-bottom: 0.8em; -} -div.body h4 { - font-size: 0.96em; - line-height: 1; - margin-bottom: 1.0em; -} -div.body h5 { - font-size: 0.8em; - font-weight: bold; - margin-bottom: 1.4em; -} -div.body h6 { - font-size: 0.6em; - font-weight: bold; - margin-bottom: 1.6em; -} - -a.headerlink { - color: #1b61d6; - font-size: 12px; - padding: 0 4px 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - text-decoration: underline; -} - -div.body p, div.body dd, div.body li { - line-height: 1.5em; -} - -div.admonition p.admonition-title + p { - display: inline; -} - -div.highlight{ - background-color: white; -} - -div.note { - border: 2px solid #7a9eec; - border-right-style: none; - border-left-style: none; - padding: 10px 20px 10px 60px; - background: #e1ecfe url(dialog-note.png) no-repeat 10px 8px; -} - -#firedrake-package #id1 { - display: none; -} - -#firedrake-package h2 { - background-color: #fafafa; - border: 2px solid #8c1b1e; - border-right-style: none; - border-left-style: none; - padding: 10px 20px 10px 60px; -} - -#firedrake-package div.section>dl { - border: 2px solid #ddd; - border-right-style: none; - border-bottom-style: none; -} - -div.seealso { - background: #fff6bf url(dialog-seealso.png) no-repeat 10px 8px; - border: 2px solid #ffd324; - border-left-style: none; - border-right-style: none; - padding: 10px 20px 10px 60px; -} - -div.topic { - background: #eeeeee; - border: 2px solid #C6C9CB; - padding: 10px 20px; - border-right-style: none; - border-left-style: none; -} - -div.warning { - background: #fbe3e4 url(dialog-warning.png) no-repeat 10px 8px; - border: 2px solid #fbc2c4; - border-right-style: none; - border-left-style: none; - padding: 10px 20px 10px 60px; -} - -p.admonition-title { - display: none; -} - -p.admonition-title:after { - content: ":"; -} - -pre, code { - font-family: Consolas, "andale mono", "lucida console", monospace; - font-size: 14px; - line-height: 17px; - background-color: #fafafa; - padding: 10px; - margin: 0; - border-top: 2px solid #c6c9cb; - border-bottom: 2px solid #c6c9cb; -} - -tt { - background-color: transparent; - color: #222; - font-size: 14px; - font-family: Consolas, "andale mono", "lucida console", monospace; -} - -h2 tt.py-mod { - font-size: 1em; -} - -.viewcode-back { - font-family: "Nobile", sans-serif; -} - -div.viewcode-block:target { - background-color: #fff6bf; - border: 2px solid #ffd324; - border-left-style: none; - border-right-style: none; - padding: 10px 20px; -} - -table.imagegrid { - width: 100%; -} - -table.imagegrid td{ - text-align: center; -} - -table.docutils td { - padding: 0.5em; -} - -table.highlighttable { - width: 100%; - clear: both; -} - -table.highlighttable td { - padding: 0; -} - -a em.std-term { - color: #007f00; -} - -a:hover em.std-term { - text-decoration: underline; -} - -.download { - font-family: "Nobile", sans-serif; - font-weight: normal; - font-style: normal; -} - -tt.xref { - font-weight: normal; - font-style: normal; -} - -#access { - background: #000; - display: block; - float: left; - width: 100%; -} -#access .menu-header, -div.menu { - font-size: 13px; - margin-right: 20px; - width: 100%; -} -#access .menu-header ul, -div.menu ul { - float:right; - margin-right:10px; - list-style: none; - margin: 0; -} -#access .menu-header li, -div.menu li { - float: left; - position: relative; -} -#access a { - color: #aaa; - display: block; - line-height: 38px; - padding: 0 10px; - text-decoration: none; -} -#access ul ul { - box-shadow: 0px 3px 3px rgba(0,0,0,0.2); - -moz-box-shadow: 0px 3px 3px rgba(0,0,0,0.2); - -webkit-box-shadow: 0px 3px 3px rgba(0,0,0,0.2); - display: none; - position: absolute; - top: 38px; - left: 0; - float: left; - width: 180px; - z-index: 99999; -} -#access ul ul li { - min-width: 180px; -} -#access ul ul ul { - left: 100%; - top: 0; -} -#access ul ul a { - background: #333; - line-height: 1em; - padding: 10px; - width: 160px; - height: auto; -} -#access li:hover > a, -#access ul ul :hover > a { - background: #333; - color: #fff; -} -#access ul li:hover > ul { - display: block; -} -#access ul li.current_page_item > a, -#access ul li.current-menu-ancestor > a, -#access ul li.current-menu-item > a, -#access ul li.current-menu-parent > a { - color: #fff; -} -* html #access ul li.current_page_item a, -* html #access ul li.current-menu-ancestor a, -* html #access ul li.current-menu-item a, -* html #access ul li.current-menu-parent a, -* html #access ul li a:hover { - color: #fff; -} - -.wrapper { - margin: 0 auto; - width: 900px; -} - -/* =Leader and Front Page Styles --------------------------------------------------------------- */ - -#leader { - border-bottom:1px solid #ccc; - padding:30px 0 40px 0; -} -#leader-container { - margin:0 auto; - overflow:hidden; - position:relative; - width:870px; -} -#leader .entry-title { - font-size:40px; - line-height:45px; - margin-top:-8px; - padding:0 0 14px 0; -} -#leader .entry-title span { - font-family:Georgia,serif; - font-weight:normal; - font-style:italic; -} -.single #leader .entry-title { - width:652px; -} -#leader .entry-meta { - position:absolute; - top:15px; - left:690px; -} - -#container, -#content { - margin:0; - padding:0; - width:100%; -} -#container { - margin-top:-21px; -} - -ul#recent-items{ -padding-left: 1em; -} - -#sub-feature { - font-size:13px; - line-height:18px; - position:relative; - overflow:hidden; -} -#sub-feature p { - margin:0 0 18px 0; -} - -#sub-feature h3 img { - position:absolute; - top:3px; - right:0; -} -.block { - float:left; - width:400px; -} -#front-block-1 { - margin-right:20px; -} - -.block .avatar { - float:left; - margin:.55em 5px 0 0; -} -.block .avatar-84 { - margin:.25em 10px 0 0; -} - -.block ul { - border-top:1px solid #ccc; - list-style:none; - margin:0; -} -.block ul li { - display:inline; -} -.block ul li a { - border-bottom:1px solid #ccc; - color:#667; - display:block; - padding:6px 0; - text-decoration:none; -} -.block ul li a:hover, -.block ul li a:active { - background:#fafafa; - color: #FF4B33; -} -.page .entry-content, -.single .entry-content { - padding-top:0; -} - -/* =Global Elements --------------------------------------------------------------- */ - -#buttons { - padding:.75em 0; -} -a.button { - border:1px solid #ccc; - -webkit-border-radius: .7em; - -moz-border-radius: .7em; - border-radius: .7em; - color:#667; - font-size:13px; - margin:0 10px 0 0; - padding:.75em 1.25em; - text-decoration:none; -} -a.button:hover, -a.button:active { - color: #FF4B33; -} - -.footer-nav { - text-align:left; - height:9em; -} -.footer-nav h4 { - margin-left: 2em; - margin-top: 0; - margin-bottom: 0; -} -div.span-6 { - display:inline; - float:left; - margin-right:10px; - margin-left: 20px; -} -div.last { - float:right; -} - -.footer-nav ul li{ - padding: 0.2em; -} - -.search { -margin-left:20px; -margin-top: 1em; -} - -@media print { -div.wrapper { -display: none; -} -} - -.mugshot -{ - padding-right: 1em; - height: 19em; - padding-left: 1em; -} - -.mugshot img -{ - width: 100px; - height: 150px; - border: solid 1px #667; -} - -.app { - height: 20em; - margin-right: 1em; -} - -#the-firedrake-team td { - text-align: center; -} - -/* Hack to disable the travis feed logo on the obtaining pyop2 page. */ -img[alt="build status"] { - display: none; -} - -/* Style elements controlling the RSS feeds */ - -/*Container*/ -/*Header*/ -#header .feed_title { - margin:10px 0px 10px 0px; - /*padding:10px 5px 50px 5px;*/ - font-weight:bold; - font-size:16px; -} - -.feed_item { - margin:5px 0px 5px 0px; - background: #eee; - color: #3E4349; -} - -.feed_item_date { - font-size: 12px; -} - -/* Equispace the logos. */ -#firedrake-is-supported-by tr { - width: 100%; -} - -/* Equispace the logos. */ -#firedrake-is-supported-by td { - width: 25%; - text-align: center; -} diff --git a/docs/source/conf.py b/docs/source/conf.py index ae90613e2..90f399c58 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -15,50 +15,50 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.mathjax', - 'sphinx.ext.viewcode', + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.mathjax", + "sphinx.ext.viewcode", ] # Both the class’ and the __init__ method’s docstring are concatenated and # inserted into the class definition -autoclass_content = 'both' +autoclass_content = "both" # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'FInAT' -copyright = u'2014, David A. Ham and Robert C. Kirby' +project = u"FInAT" +copyright = u"2014--2020, David A. Ham, Robert C. Kirby and others" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '0.1' +version = "0.1" # The full version, including alpha/beta/rc tags. -release = '0.1' +release = "0.1" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -69,9 +69,9 @@ # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -79,170 +79,172 @@ # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'finat' +html_theme = "finat" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -html_theme_path = ['_themes'] +html_theme_path = ["_themes"] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +# html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' -#html_search_language = 'en' +# html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value -#html_search_options = {'type': 'default'} +# html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' +# html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = 'FInATdoc' +htmlhelp_basename = "FInATdoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). - 'papersize': 'a4paper', - + "papersize": "a4paper", # The font size ('10pt', '11pt' or '12pt'). - #'pointsize': '10pt', - + # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. - #'preamble': '', - + # 'preamble': '', # Latex figure (float) alignment - #'figure_align': 'htbp', + # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ('index', 'FInAT.tex', u'FInAT Documentation', - u'David A. Ham and Robert C. Kirby', 'manual'), + ( + "index", + "FInAT.tex", + u"FInAT Documentation", + u"David A. Ham and Robert C. Kirby", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output --------------------------------------- @@ -250,12 +252,11 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'finat', u'FInAT Documentation', - [u'David A. Ham and Robert C. Kirby'], 1) + ("index", "finat", u"FInAT Documentation", [u"David A. Ham and Robert C. Kirby"], 1) ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -264,23 +265,29 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'FInAT', u'FInAT Documentation', - u'David A. Ham and Robert C. Kirby', 'FInAT', 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "FInAT", + u"FInAT Documentation", + u"David A. Ham and Robert C. Kirby", + "FInAT", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'http://docs.python.org/': None} +intersphinx_mapping = {"https://docs.python.org/3/": None} diff --git a/docs/source/index.rst b/docs/source/index.rst index ab78c49d9..cef474f5d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -11,6 +11,7 @@ Contents: .. toctree:: :maxdepth: 2 + finat Indices and tables diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 8e00500a9..98d1d89f8 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -120,7 +120,7 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): :param ps: the point set object. :param entity: the cell entity on which to tabulate. :param coordinate_mapping: a - :class:`~.physically_mapped.PhysicalGeometry` object that + :class:`~.physically_mapped.PhysicalGeometry` object that provides physical geometry callbacks (may be None). ''' From 4a9277c8aa1aebf0941d0888a3b11088080ca4d1 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Mon, 10 Aug 2020 16:22:07 +0100 Subject: [PATCH 613/749] enriched: accept generator in constructor --- finat/enriched.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/finat/enriched.py b/finat/enriched.py index da0e7d107..370642115 100644 --- a/finat/enriched.py +++ b/finat/enriched.py @@ -14,11 +14,12 @@ class EnrichedElement(FiniteElementBase): basis functions of several other finite elements.""" def __new__(cls, elements): + elements = tuple(elements) if len(elements) == 1: return elements[0] else: self = super().__new__(cls) - self.elements = tuple(elements) + self.elements = elements return self @cached_property From 2feaf4b5f7c3902b48ca4cc724e5babc15cbd30b Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Fri, 24 Jul 2020 16:43:31 +0100 Subject: [PATCH 614/749] Implement RestrictedElement Defers to FIAT for simplex cells and unpacks and restricts the rest of the element algebra recursively. Doesn't handle PhysicallyMapped elements. --- finat/__init__.py | 1 + finat/restricted.py | 260 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100644 finat/restricted.py diff --git a/finat/__init__.py b/finat/__init__.py index 5d19003e6..01685e159 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -19,5 +19,6 @@ from .hdivcurl import HCurlElement, HDivElement # noqa: F401 from .mixed import MixedElement # noqa: F401 from .quadrature_element import QuadratureElement # noqa: F401 +from .restricted import RestrictedElement # noqa: F401 from .runtime_tabulated import RuntimeTabulated # noqa: F401 from . import quadrature # noqa: F401 diff --git a/finat/restricted.py b/finat/restricted.py new file mode 100644 index 000000000..92f294b51 --- /dev/null +++ b/finat/restricted.py @@ -0,0 +1,260 @@ +from functools import singledispatch +from itertools import chain + +import FIAT +from FIAT.polynomial_set import mis + +import finat +from finat.fiat_elements import FiatElement +from finat.physically_mapped import PhysicallyMappedElement + + +# Sentinel for when restricted element is empty +null_element = object() + + +@singledispatch +def restrict(element, domain, take_closure): + """Restrict an element to a given subentity. + + :arg element: The element to restrict. + :arg domain: The subentity to restrict to. + :arg take_closure: Gather dofs in closure of the subentities? + Ignored for "interior" domain. + + :raises NotImplementedError: If we don't know how to restrict this + element. + :raises ValueError: If the restricted element is empty. + :returns: A new finat element.""" + return NotImplementedError(f"Don't know how to restrict element of type {type(element)}") + + +@restrict.register(FiatElement) +def restrict_fiat(element, domain, take_closure): + try: + return FiatElement(FIAT.RestrictedElement(element._element, restriction_domain=domain)) + except ValueError: + return null_element + + +@restrict.register(PhysicallyMappedElement) +def restrict_physically_mapped(element, domain, take_closure): + raise NotImplementedError("Can't restrict Physically Mapped things") + + +@restrict.register(finat.FlattenedDimensions) +def restrict_flattened_dimensions(element, domain, take_closure): + restricted = restrict(element.product, domain, take_closure) + if restricted is null_element: + return null_element + else: + return finat.FlattenedDimensions(restricted) + + +@restrict.register(finat.DiscontinuousElement) +def restrict_discontinuous(element, domain, take_closure): + if domain == "interior": + return element + else: + return null_element + + +@restrict.register(finat.EnrichedElement) +def restrict_enriched(element, domain, take_closure): + if all(isinstance(e, finat.mixed.MixedSubElement) for e in element.elements): + # Mixed is handled by Enriched + MixedSubElement, we must + # restrict the subelements here because the transformation is + # nonlocal. + elements = tuple(restrict(e.element, domain, take_closure) for + e in element.elements) + reconstruct = finat.mixed.MixedElement + elif not any(isinstance(e, finat.mixed.MixedSubElement) for e in element.elements): + elements = tuple(restrict(e, domain, take_closure) + for e in element.elements) + reconstruct = finat.EnrichedElement + else: + raise NotImplementedError("Not expecting enriched with mixture of MixedSubElement and others") + + elements = tuple(e for e in elements if e is not null_element) + if elements: + return reconstruct(elements) + else: + return null_element + + +@restrict.register(finat.HCurlElement) +def restrict_hcurl(element, domain, take_closure): + restricted = restrict(element.wrappee, domain, take_closure) + if restricted is null_element: + return null_element + else: + if isinstance(restricted, finat.EnrichedElement): + return finat.EnrichedElement(finat.HCurlElement(e) + for e in restricted.elements) + else: + return finat.HCurlElement(restricted) + + +@restrict.register(finat.HDivElement) +def restrict_hdiv(element, domain, take_closure): + restricted = restrict(element.wrappee, domain, take_closure) + if restricted is null_element: + return null_element + else: + if isinstance(restricted, finat.EnrichedElement): + return finat.EnrichedElement(finat.HDivElement(e) + for e in restricted.elements) + else: + return finat.HDivElement(restricted) + + +@restrict.register(finat.mixed.MixedSubElement) +def restrict_mixed(element, domain, take_closure): + raise AssertionError("Was expecting this to be handled inside EnrichedElement restriction") + + +@restrict.register(finat.GaussLobattoLegendre) +def restrict_gll(element, domain, take_closure): + try: + return FiatElement(FIAT.RestrictedElement(element._element, restriction_domain=domain)) + except ValueError: + return null_element + + +@restrict.register(finat.GaussLegendre) +def restrict_gl(element, domain, take_closure): + if domain == "interior": + return element + else: + return null_element + + +def r_to_codim(restriction, dim): + if restriction == "interior": + return 0 + elif restriction == "facet": + return 1 + elif restriction == "face": + return dim - 2 + elif restriction == "edge": + return dim - 1 + elif restriction == "vertex": + return dim + else: + raise ValueError + + +def codim_to_r(codim, dim): + d = dim - codim + if codim == 0: + return "interior" + elif codim == 1: + return "facet" + elif d == 0: + return "vertex" + elif d == 1: + return "edge" + elif d == 2: + return "face" + else: + raise ValueError + + +@restrict.register(finat.TensorProductElement) +def restrict_tpe(element, domain, take_closure): + # The restriction of a TPE to a codim subentity is the direct sum + # of TPEs where the factors have been restricted in such a way + # that the sum of those restrictions is codim. + # + # For example, to restrict an interval x interval to edges (codim 1) + # we construct + # + # R(I, 0)⊗R(I, 1) ⊕ R(I, 1)⊗R(I, 0) + # + # If take_closure is true, the restriction wants to select dofs on + # entities with dim >= codim >= 1 (for the edge example) + # so we get + # + # R(I, 0)⊗R(I, 1) ⊕ R(I, 1)⊗R(I, 0) ⊕ R(I, 0)⊗R(I, 0) + factors = element.factors + dimension = element.cell.get_spatial_dimension() + + # Figure out which codim entity we're selecting + codim = r_to_codim(domain, dimension) + # And the range of codims. + upper = 1 + (dimension + if (take_closure and domain != "interior") + else codim) + # restrictions on each factor taken from n-tuple that sums to the + # target codim (as long as the codim <= dim_factor) + restrictions = tuple(candidate + for candidate in chain(*(mis(len(factors), c) + for c in range(codim, upper))) + if all(d <= factor.cell.get_dimension() + for d, factor in zip(candidate, factors))) + elements = [] + for decomposition in restrictions: + # Recurse, but don't take closure in recursion (since we + # handled it already). + new_factors = tuple( + restrict(factor, codim_to_r(codim, factor.cell.get_dimension()), + take_closure=False) + for factor, codim in zip(factors, decomposition)) + # If one of the factors was empty then the whole TPE is empty, + # so skip. + if all(f is not null_element for f in new_factors): + elements.append(finat.TensorProductElement(new_factors)) + if elements: + return finat.EnrichedElement(elements) + else: + return null_element + + +@restrict.register(finat.TensorFiniteElement) +def restrict_tfe(element, domain, take_closure): + restricted = restrict(element._base_element, domain, take_closure) + if restricted is null_element: + return null_element + else: + return finat.TensorFiniteElement(restricted, element._shape, element._transpose) + + +@restrict.register(finat.HDivTrace) +def restrict_hdivtrace(element, domain, take_closure): + try: + return FiatElement(FIAT.RestrictedElement(element._element, restriction_domain=domain)) + except ValueError: + return null_element + + +def RestrictedElement(element, restriction_domain, *, indices=None): + """Construct a restricted element. + + :arg element: The element to restrict. + :arg restriction_domain: Which entities to restrict to. + :arg indices: Indices of basis functions to select (not supported) + :returns: A new element. + + .. note:: + + A restriction domain of "interior" means to select the dofs on + the cell, all other domains (e.g. "face", "edge") select dofs + in the closure of the entity. + + .. warning:: + + The returned element *may not* be interpolatory. That is, the + dual basis (if implemented) might not be nodal to the primal + basis. Assembly still works (``basis_evaluation`` is fine), but + interpolation may produce bad results. + + Restrictions of FIAT-implemented CiarletElements are always + nodal. + """ + if indices is not None: + raise NotImplementedError("Only done for topological restrictions") + assert restriction_domain is not None + restricted = restrict(element, restriction_domain, take_closure=True) + if restricted is null_element: + raise ValueError("Restricted element is empty") + return restricted From ce6b006286833660e8f2bba42c0378f58bf29adc Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Fri, 14 Aug 2020 18:38:14 +0100 Subject: [PATCH 615/749] Add tests for restricted elements Ensure that the tabulation of a restricted element is the restriction of the tabulation of the original element. --- test/test_restriction.py | 135 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 test/test_restriction.py diff --git a/test/test_restriction.py b/test/test_restriction.py new file mode 100644 index 000000000..bd224fdc5 --- /dev/null +++ b/test/test_restriction.py @@ -0,0 +1,135 @@ +import FIAT +import finat +import numpy +import pytest +from finat.point_set import PointSet +from finat.restricted import r_to_codim +from gem.interpreter import evaluate + + +def tabulate(element, ps): + tabulation, = element.basis_evaluation(0, ps).values() + result, = evaluate([tabulation]) + # Singleton point + shape = (int(numpy.prod(element.index_shape)), ) + element.value_shape + return result.arr.reshape(*shape) + + +def which_dofs(element, restricted): + edofs = element.entity_dofs() + rdofs = restricted.entity_dofs() + keep_e = [] + keep_r = [] + for k in edofs.keys(): + for e, indices in edofs[k].items(): + if rdofs[k][e]: + assert len(rdofs[k][e]) == len(indices) + keep_e.extend(indices) + keep_r.extend(rdofs[k][e]) + return keep_e, keep_r + + +@pytest.fixture(params=["vertex", "edge", "facet", "interior"], scope="module") +def restriction(request): + return request.param + + +@pytest.fixture(params=["tet", "quad"], scope="module") +def cell(request): + return request.param + + +@pytest.fixture +def ps(cell): + if cell == "tet": + return PointSet([[1/3, 1/4, 1/5]]) + elif cell == "quad": + return PointSet([[1/3, 1/4]]) + + +@pytest.fixture(scope="module") +def scalar_element(cell): + if cell == "tet": + return finat.Lagrange(FIAT.reference_element.UFCTetrahedron(), 4) + elif cell == "quad": + interval = FIAT.reference_element.UFCInterval() + return finat.FlattenedDimensions( + finat.TensorProductElement([ + finat.GaussLobattoLegendre(interval, 3), + finat.GaussLobattoLegendre(interval, 3)] + ) + ) + + +@pytest.fixture(scope="module") +def hdiv_element(cell): + if cell == "tet": + return finat.RaviartThomas(FIAT.reference_element.UFCTetrahedron(), 3, variant="integral(3)") + elif cell == "quad": + interval = FIAT.reference_element.UFCInterval() + return finat.FlattenedDimensions( + finat.EnrichedElement([ + finat.HDivElement( + finat.TensorProductElement([ + finat.GaussLobattoLegendre(interval, 3), + finat.GaussLegendre(interval, 3)])), + finat.HDivElement( + finat.TensorProductElement([ + finat.GaussLegendre(interval, 3), + finat.GaussLobattoLegendre(interval, 3)])) + ])) + + +@pytest.fixture(scope="module") +def hcurl_element(cell): + if cell == "tet": + return finat.Nedelec(FIAT.reference_element.UFCTetrahedron(), 3, variant="integral(3)") + elif cell == "quad": + interval = FIAT.reference_element.UFCInterval() + return finat.FlattenedDimensions( + finat.EnrichedElement([ + finat.HCurlElement( + finat.TensorProductElement([ + finat.GaussLobattoLegendre(interval, 3), + finat.GaussLegendre(interval, 3)])), + finat.HCurlElement( + finat.TensorProductElement([ + finat.GaussLegendre(interval, 3), + finat.GaussLobattoLegendre(interval, 3)])) + ])) + + +def run_restriction(element, restriction, ps): + try: + restricted = finat.RestrictedElement(element, restriction) + except ValueError: + # No dofs. + # Check that the original element had no dofs in all the relevant slots. + dim = element.cell.get_spatial_dimension() + lo_codim = r_to_codim(restriction, dim) + hi_codim = (lo_codim if restriction == "interior" else dim) + edofs = element.entity_dofs() + for entity_dim, dof_numbering in edofs.items(): + try: + entity_codim = dim - sum(entity_dim) + except TypeError: + entity_codim = dim - entity_dim + if lo_codim <= entity_codim <= hi_codim: + assert all(len(i) == 0 for i in dof_numbering.values()) + else: + e = tabulate(element, ps) + r = tabulate(restricted, ps) + keep_e, keep_r = which_dofs(element, restricted) + assert numpy.allclose(e[keep_e, ...], r[keep_r, ...]) + + +def test_scalar_restriction(scalar_element, restriction, ps): + run_restriction(scalar_element, restriction, ps) + + +def test_hdiv_restriction(hdiv_element, restriction, ps): + run_restriction(hdiv_element, restriction, ps) + + +def test_hcurl_restriction(hcurl_element, restriction, ps): + run_restriction(hcurl_element, restriction, ps) From 3d77f8562e108b9979a45f7dccc876d26f076e0b Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Tue, 18 Aug 2020 13:50:00 -0500 Subject: [PATCH 616/749] add citations, fix flake8 --- finat/aw.py | 68 +++++++++----------------------------- finat/mtw.py | 10 +++--- finat/physically_mapped.py | 37 +++++++++++++++++++++ 3 files changed, 56 insertions(+), 59 deletions(-) diff --git a/finat/aw.py b/finat/aw.py index 111044fd3..cea70ce52 100644 --- a/finat/aw.py +++ b/finat/aw.py @@ -11,6 +11,8 @@ class ArnoldWintherNC(PhysicallyMappedElement, FiatElement): def __init__(self, cell, degree): + if Citations is not None: + Citations().register("Arnold2003") super(ArnoldWintherNC, self).__init__(FIAT.ArnoldWintherNC(cell, degree)) def basis_transformation(self, coordinate_mapping, as_numpy=False): @@ -41,7 +43,7 @@ def basis_transformation(self, coordinate_mapping, as_numpy=False): # Compute alpha and beta for the edge. Ghat_T = numpy.array([nhat[e, :], that[e, :]]) - (alpha, beta) = Ghat_T @ JTJ @ that[e,:] / detJ + (alpha, beta) = Ghat_T @ JTJ @ that[e, :] / detJ # Stuff into the right rows and columns. (idx1, idx2) = (4*e + 1, 4*e + 3) @@ -54,7 +56,7 @@ def basis_transformation(self, coordinate_mapping, as_numpy=False): for i in range(12, 15): V[i, i] = Literal(1) - if as_numpy: + if as_numpy: return V.T else: return ListTensor(V.T) @@ -66,7 +68,6 @@ def entity_dofs(self): 1: {0: [0, 1, 2, 3], 1: [4, 5, 6, 7], 2: [8, 9, 10, 11]}, 2: {0: [12, 13, 14]}} - @property def index_shape(self): return (15,) @@ -77,6 +78,8 @@ def space_dimension(self): class ArnoldWinther(PhysicallyMappedElement, FiatElement): def __init__(self, cell, degree): + if Citations is not None: + Citations().register("Arnold2002") super(ArnoldWinther, self).__init__(FIAT.ArnoldWinther(cell, degree)) def basis_transformation(self, coordinate_mapping): @@ -87,29 +90,12 @@ def basis_transformation(self, coordinate_mapping): for multiindex in numpy.ndindex(V.shape): V[multiindex] = Literal(V[multiindex]) - #for i in range(24): - # V[i, i] = Literal(1) - #return ListTensor(V.T) - # TODO: find a succinct expression for W in terms of J. J = coordinate_mapping.jacobian_at([1/3, 1/3]) - #J = numpy.linalg.inv(J) detJ = coordinate_mapping.detJ_at([1/3, 1/3]) - - W = numpy.zeros((3,3), dtype=object) - """ - W[0, 0] = J[0, 0]*J[0, 0] - W[0, 1] = 2*J[0, 0]*J[0, 1] - W[0, 2] = J[0, 1]*J[0, 1] - W[1, 0] = J[0, 0]*J[1, 0] - W[1, 1] = J[0, 0]*J[1, 1] + J[0, 1]*J[1, 0] - W[1, 2] = J[0, 1]*J[1, 1] - W[2, 0] = J[1, 0]*J[1, 0] - W[2, 1] = 2*J[1, 0]*J[1, 1] - W[2, 2] = J[1, 1]*J[1, 1] - W_check = W / (detJ * detJ) - """ - W[0, 0] = J[1,1]*J[1,1] + + W = numpy.zeros((3, 3), dtype=object) + W[0, 0] = J[1, 1]*J[1, 1] W[0, 1] = -2*J[1, 1]*J[0, 1] W[0, 2] = J[0, 1]*J[0, 1] W[1, 0] = -1*J[1, 1]*J[1, 0] @@ -118,16 +104,10 @@ def basis_transformation(self, coordinate_mapping): W[2, 0] = J[1, 0]*J[1, 0] W[2, 1] = -2*J[1, 0]*J[0, 0] W[2, 2] = J[0, 0]*J[0, 0] - W_check = W + W_check = W # Put into the right rows and columns. - V[0:3, 0:3] = V[3:6, 3:6] = V[6:9, 6:9] = W_check - - """ - for i in range(9): - V[i, i] = Literal(1) - """ - #J = numpy.linalg.inv(J) - # edge dofs, 4 per edge + V[0:3, 0:3] = V[3:6, 3:6] = V[6:9, 6:9] = W_check + for i in range(9, 21, 2): V[i, i] = Literal(1) @@ -142,13 +122,13 @@ def basis_transformation(self, coordinate_mapping): J_np = numpy.array([[J[0, 0], J[0, 1]], [J[1, 0], J[1, 1]]]) JTJ = J_np.T @ J_np - + for e in range(3): # Compute alpha and beta for the edge. Ghat_T = numpy.array([nhat[e, :], that[e, :]]) - (alpha, beta) = Ghat_T @ JTJ @ that[e,:] / detJ + (alpha, beta) = Ghat_T @ JTJ @ that[e, :] / detJ # Stuff into the right rows and columns. (idx1, idx2) = (9 + 4*e + 1, 9 + 4*e + 3) @@ -160,24 +140,8 @@ def basis_transformation(self, coordinate_mapping): # internal dofs (AWnc has good conditioning, so leave this alone) for i in range(21, 24): V[i, i] = Literal(1) - #V[21:24, 21:24] = W_check - - # h = coordinate_mapping.cell_size() - # for v in range(3): - # for c in range(3): - # for i in range(30): - # V[i, 3*v+c] = V[i, 3*v+c] / h[v] / h[v] - - # for e in range(3): - # v0id, v1id = [i for i in range(3) if i != e] - # he = (h[v0id] + h[v1id]) / 2 - # for j in range(4): - # for i in range(30): - # V[i, 9+4*e+j] = V[i, 9+4*e+j] * h[e] * h[e] - - - return ListTensor(V.T) + return ListTensor(V.T) def entity_dofs(self): return {0: {0: [0, 1, 2], @@ -186,11 +150,9 @@ def entity_dofs(self): 1: {0: [9, 10, 11, 12], 1: [13, 14, 15, 16], 2: [17, 18, 19, 20]}, 2: {0: [21, 22, 23]}} - @property def index_shape(self): return (24,) - def space_dimension(self): return 24 diff --git a/finat/mtw.py b/finat/mtw.py index 27f474594..b0f7ed04f 100644 --- a/finat/mtw.py +++ b/finat/mtw.py @@ -10,9 +10,10 @@ class MardalTaiWinther(PhysicallyMappedElement, FiatElement): def __init__(self, cell, degree): + if Citations is not None: + Citations().register("Mardal2002") super(MardalTaiWinther, self).__init__(FIAT.MardalTaiWinther(cell, degree)) - def basis_transformation(self, coordinate_mapping): V = numpy.zeros((20, 9), dtype=object) @@ -36,11 +37,10 @@ def basis_transformation(self, coordinate_mapping): JTJ = J_np.T @ J_np for e in range(3): - # Compute alpha and beta for the edge. Ghat_T = numpy.array([nhat[e, :], that[e, :]]) - (alpha, beta) = Ghat_T @ JTJ @ that[e,:] / detJ + (alpha, beta) = Ghat_T @ JTJ @ that[e, :] / detJ # Stuff into the right rows and columns. idx = 3*e + 1 @@ -61,13 +61,11 @@ def entity_closure_dofs(self): 1: [], 2: []}, 1: {0: [0, 1, 2], 1: [3, 4, 5], 2: [6, 7, 8]}, - 2: {0: [0, 1, 2, 3, 4, 5, 6, 7, 8]}} - + 2: {0: [0, 1, 2, 3, 4, 5, 6, 7, 8]}} @property def index_shape(self): return (9,) - def space_dimension(self): return 9 diff --git a/finat/physically_mapped.py b/finat/physically_mapped.py index 6c24e6528..4fc302344 100644 --- a/finat/physically_mapped.py +++ b/finat/physically_mapped.py @@ -84,6 +84,43 @@ doi = {10.1243/03093247V061020} } """) + Citations().add("Mardal2002", """ +@article{Mardal2002, + doi = {10.1137/s0036142901383910}, + year = 2002, + volume = {40}, + number = {5}, + pages = {1605--1631}, + author = {Mardal, K.-A.~ and Tai, X.-C.~ and Winther, R.~}, + title = {A robust finite element method for {Darcy--Stokes} flow}, + journal = {{SIAM} Journal on Numerical Analysis} +} +""") + Citations.add("Arnold2002", """ +@article{arnold2002, + doi = {10.1007/s002110100348}, + year = 2002, + volume = {92}, + number = {3}, + pages = {401--419}, + author = {Arnold, R.~N.~ and Winther, R.~}, + title = {Mixed finite elements for elasticity}, + journal = {Numerische Mathematik} +} +""") + Citations.add("Arnold2003", """ +@article{arnold2003, + doi = {10.1142/s0218202503002507}, + year = 2003, + volume = {13}, + number = {03}, + pages = {295--307}, + author = {Arnold, D.~N.~ and Winther, R.~}, + title = {Nonconforming mixed elements for elasticity}, + journal = {Mathematical Models and Methods in Applied Sciences} +} +""") + except ImportError: Citations = None From 7717aab78cce1a36607c6b4bc505a0b5ce7daea5 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Tue, 18 Aug 2020 14:20:01 -0500 Subject: [PATCH 617/749] remove pytest import --- test/test_aw.py | 1 - test/test_morley.py | 1 - 2 files changed, 2 deletions(-) diff --git a/test/test_aw.py b/test/test_aw.py index e49756264..eee1299be 100644 --- a/test/test_aw.py +++ b/test/test_aw.py @@ -1,4 +1,3 @@ -import pytest import FIAT import finat import numpy as np diff --git a/test/test_morley.py b/test/test_morley.py index d80336f36..d9c32cd01 100644 --- a/test/test_morley.py +++ b/test/test_morley.py @@ -1,4 +1,3 @@ -import pytest import FIAT import finat import numpy as np From 809c178c90fcb063be2cf4849eda9d5d51f25c4e Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Tue, 18 Aug 2020 14:57:21 -0500 Subject: [PATCH 618/749] Fix citations --- finat/physically_mapped.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/finat/physically_mapped.py b/finat/physically_mapped.py index 4fc302344..12746b097 100644 --- a/finat/physically_mapped.py +++ b/finat/physically_mapped.py @@ -96,8 +96,8 @@ journal = {{SIAM} Journal on Numerical Analysis} } """) - Citations.add("Arnold2002", """ -@article{arnold2002, + Citations().add("Arnold2002", """ +@article{Arnold2002, doi = {10.1007/s002110100348}, year = 2002, volume = {92}, @@ -108,7 +108,7 @@ journal = {Numerische Mathematik} } """) - Citations.add("Arnold2003", """ + Citations().add("Arnold2003", """ @article{arnold2003, doi = {10.1142/s0218202503002507}, year = 2003, From 455eaf8c389414e437d7d3b70d9e04410f74bc46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Homolya?= Date: Wed, 19 Aug 2020 16:33:51 +0200 Subject: [PATCH 619/749] Delete trailing whitespace --- finat/direct_serendipity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/direct_serendipity.py b/finat/direct_serendipity.py index 70df1b8c3..2aa45cb5f 100644 --- a/finat/direct_serendipity.py +++ b/finat/direct_serendipity.py @@ -417,7 +417,7 @@ def nodalize(f): def ds_sympy(ct, r, vs=None): - """Symbolically Constructs Arbogast's directly defined C^0 serendipity elements, + """Symbolically Constructs Arbogast's directly defined C^0 serendipity elements, which include all polynomials of degree r plus a couple of rational functions. :param ct: The cell topology of the reference quadrilateral. :param vs: (Optional) coordinates of cell on which to construct the basis. From 9f7f6417c46421a0a516120899d219aa5fd0af78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Homolya?= Date: Wed, 19 Aug 2020 16:35:58 +0200 Subject: [PATCH 620/749] Improve names per Lawrence's request --- test/test_direct_serendipity.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_direct_serendipity.py b/test/test_direct_serendipity.py index 09ab8b36f..83aecb044 100644 --- a/test/test_direct_serendipity.py +++ b/test/test_direct_serendipity.py @@ -72,10 +72,10 @@ def test_kronecker(degree): cell = FIAT.ufc_cell("quadrilateral") element = finat.DirectSerendipity(cell, degree) pts = finat.point_set.PointSet(get_pts(cell, degree)) - vrts = np.asarray(((0.0, 0.0), (1.0, 0.0), (0.1, 1.1), (0.95, 1.01))) - mppng = MyMapping(cell, vrts) + vertices = np.asarray(((0.0, 0.0), (1.0, 0.0), (0.1, 1.1), (0.95, 1.01))) + mapping = MyMapping(cell, vertices) z = tuple([0] * cell.get_spatial_dimension()) - vals = element.basis_evaluation(0, pts, coordinate_mapping=mppng)[z] + vals = element.basis_evaluation(0, pts, coordinate_mapping=mapping)[z] from gem.interpreter import evaluate numvals = evaluate([vals])[0].arr assert np.allclose(numvals, np.eye(*numvals.shape)) From 97189439f657d4ab12c56396d8a5e2ff22545c0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Homolya?= Date: Wed, 19 Aug 2020 16:37:37 +0200 Subject: [PATCH 621/749] Avoid using local imports --- test/test_direct_serendipity.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/test_direct_serendipity.py b/test/test_direct_serendipity.py index 83aecb044..88b925b5d 100644 --- a/test/test_direct_serendipity.py +++ b/test/test_direct_serendipity.py @@ -76,6 +76,5 @@ def test_kronecker(degree): mapping = MyMapping(cell, vertices) z = tuple([0] * cell.get_spatial_dimension()) vals = element.basis_evaluation(0, pts, coordinate_mapping=mapping)[z] - from gem.interpreter import evaluate - numvals = evaluate([vals])[0].arr + numvals = gem.interpreter.evaluate([vals])[0].arr assert np.allclose(numvals, np.eye(*numvals.shape)) From 7b00e4f1b4f564ea71827660d966b86117f7398d Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Wed, 19 Aug 2020 17:53:44 +0100 Subject: [PATCH 622/749] sympy2gem: Handle symengine objects too --- finat/sympy2gem.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/finat/sympy2gem.py b/finat/sympy2gem.py index c33a173c6..80e776a8a 100644 --- a/finat/sympy2gem.py +++ b/finat/sympy2gem.py @@ -1,6 +1,7 @@ from functools import singledispatch, reduce import sympy +import symengine import gem @@ -11,42 +12,50 @@ def sympy2gem(node, self): @sympy2gem.register(sympy.Expr) +@sympy2gem.register(symengine.Expr) def sympy2gem_expr(node, self): raise NotImplementedError("no handler for sympy node type %s" % type(node)) @sympy2gem.register(sympy.Add) +@sympy2gem.register(symengine.Add) def sympy2gem_add(node, self): return reduce(gem.Sum, map(self, node.args)) @sympy2gem.register(sympy.Mul) +@sympy2gem.register(symengine.Mul) def sympy2gem_mul(node, self): return reduce(gem.Product, map(self, node.args)) @sympy2gem.register(sympy.Pow) +@sympy2gem.register(symengine.Pow) def sympy2gem_pow(node, self): return gem.Power(*map(self, node.args)) @sympy2gem.register(sympy.Integer) +@sympy2gem.register(symengine.Integer) @sympy2gem.register(int) def sympy2gem_integer(node, self): return gem.Literal(node) @sympy2gem.register(sympy.Float) +@sympy2gem.register(symengine.Float) @sympy2gem.register(float) def sympy2gem_float(node, self): return gem.Literal(node) @sympy2gem.register(sympy.Symbol) +@sympy2gem.register(symengine.Symbol) def sympy2gem_symbol(node, self): return self.bindings[node] @sympy2gem.register(sympy.Rational) +@sympy2gem.register(symengine.Rational) def sympy2gem_rational(node, self): - return gem.Division(self(node.numerator()), self(node.denominator())) + return gem.Division(*(map(self, node.as_numer_denom()))) From 491d8ac6f2a894091f2160a2b8d0a116ed7a444b Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Wed, 19 Aug 2020 18:04:06 +0100 Subject: [PATCH 623/749] Use symengine for direct Serendipity element Apart from differentiation, it offers an identical interface to sympy, but is many times faster. --- finat/direct_serendipity.py | 47 +++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/finat/direct_serendipity.py b/finat/direct_serendipity.py index 2aa45cb5f..baa824f83 100644 --- a/finat/direct_serendipity.py +++ b/finat/direct_serendipity.py @@ -1,12 +1,13 @@ -import numpy +from itertools import chain, repeat -from finat.finiteelementbase import FiniteElementBase -from finat.physically_mapped import DirectlyDefinedElement, Citations -from FIAT.reference_element import UFCQuadrilateral +import gem +import numpy +import symengine from FIAT.polynomial_set import mis +from FIAT.reference_element import UFCQuadrilateral -import gem -import sympy +from finat.finiteelementbase import FiniteElementBase +from finat.physically_mapped import Citations, DirectlyDefinedElement from finat.sympy2gem import sympy2gem @@ -93,10 +94,11 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): mapper.bindings = repl result = {} + for i in range(order+1): alphas = mis(2, i) for alpha in alphas: - dphis = [phi.diff(*tuple(zip(xx, alpha))) for phi in phis] + dphis = [diff(phi, xx, alpha) for phi in phis] result[alpha] = gem.ListTensor(list(map(mapper, dphis))) return result @@ -123,12 +125,12 @@ def ds1_sympy(ct, vs=None): of the four basis functions. """ if vs is None: - vs = numpy.asarray(list(zip(sympy.symbols('x:4'), - sympy.symbols('y:4')))) + vs = numpy.asarray(list(zip(symengine.symbols('x:4'), + symengine.symbols('y:4')))) else: vs = numpy.asarray(vs) - xx = numpy.asarray(sympy.symbols("x,y")) + xx = numpy.asarray(symengine.symbols("x,y")) ts = numpy.zeros((4, 2), dtype=object) for e in range(4): @@ -216,6 +218,17 @@ def newton_poly(nds, fs, xsym): return result +def diff(expr, xx, alpha): + """Differentiate expr with respect to xx. + + :arg expr: symengine Expression to differentiate. + :arg xx: iterable of coordinates to differentiate with respect to. + :arg alpha: derivative multiindex, one entry for each entry of xx + indicating how many derivatives in that direction. + :returns: New symengine expression.""" + return symengine.diff(expr, *(chain(*(repeat(x, a) for x, a in zip(xx, alpha))))) + + def dsr_sympy(ct, r, vs=None): """Constructs higher-order (>= 2) case of Arbogast's directly defined C^0 serendipity elements, which include all polynomials of degree r plus a couple of rational @@ -228,11 +241,11 @@ def dsr_sympy(ct, r, vs=None): of the four basis functions. """ if vs is None: # do vertices symbolically - vs = numpy.asarray(list(zip(sympy.symbols('x:4'), - sympy.symbols('y:4')))) + vs = numpy.asarray(list(zip(symengine.symbols('x:4'), + symengine.symbols('y:4')))) else: vs = numpy.asarray(vs) - xx = numpy.asarray(sympy.symbols("x,y")) + xx = numpy.asarray(symengine.symbols("x,y")) ts = numpy.zeros((4, 2), dtype=object) for e in range(4): @@ -278,8 +291,8 @@ def dsr_sympy(ct, r, vs=None): mons = [xx[0] ** i * xx[1] ** j for i in range(r-3) for j in range(r-3-i)] - V = sympy.Matrix([[mon.subs(xysub(xx, nd)) for mon in mons] - for nd in internal_nodes]) + V = symengine.Matrix([[mon.subs(xysub(xx, nd)) for mon in mons] + for nd in internal_nodes]) Vinv = V.inv() nmon = len(mons) @@ -295,9 +308,9 @@ def dsr_sympy(ct, r, vs=None): # R for each edge (1 on edge, zero on opposite Rs = [(1 - RV) / 2, (1 + RV) / 2, (1 - RH) / 2, (1 + RH) / 2] - nodes1d = [sympy.Rational(i, r) for i in range(1, r)] + nodes1d = [symengine.Rational(i, r) for i in range(1, r)] - s = sympy.Symbol('s') + s = symengine.Symbol('s') # for each edge: # I need its adjacent two edges From 1b026250b2f73471f0d04fd5a2c90e8fe4c443f6 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Wed, 19 Aug 2020 19:59:20 +0100 Subject: [PATCH 624/749] sympy2gem: Explicitly cast to int/float --- finat/sympy2gem.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/finat/sympy2gem.py b/finat/sympy2gem.py index 80e776a8a..856a48d78 100644 --- a/finat/sympy2gem.py +++ b/finat/sympy2gem.py @@ -8,13 +8,13 @@ @singledispatch def sympy2gem(node, self): - raise AssertionError("sympy node expected, got %s" % type(node)) + raise AssertionError("sympy/symengine node expected, got %s" % type(node)) @sympy2gem.register(sympy.Expr) @sympy2gem.register(symengine.Expr) def sympy2gem_expr(node, self): - raise NotImplementedError("no handler for sympy node type %s" % type(node)) + raise NotImplementedError("no handler for sympy/symengine node type %s" % type(node)) @sympy2gem.register(sympy.Add) @@ -39,14 +39,14 @@ def sympy2gem_pow(node, self): @sympy2gem.register(symengine.Integer) @sympy2gem.register(int) def sympy2gem_integer(node, self): - return gem.Literal(node) + return gem.Literal(int(node)) @sympy2gem.register(sympy.Float) @sympy2gem.register(symengine.Float) @sympy2gem.register(float) def sympy2gem_float(node, self): - return gem.Literal(node) + return gem.Literal(float(node)) @sympy2gem.register(sympy.Symbol) From d343845a0d8144a98411c8e5932b7c814df85976 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Wed, 19 Aug 2020 19:59:41 +0100 Subject: [PATCH 625/749] Make use of symengine optional (defaults off) Seemingly broken at high degree. --- finat/direct_serendipity.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/finat/direct_serendipity.py b/finat/direct_serendipity.py index baa824f83..79755d42e 100644 --- a/finat/direct_serendipity.py +++ b/finat/direct_serendipity.py @@ -3,6 +3,7 @@ import gem import numpy import symengine +import sympy from FIAT.polynomial_set import mis from FIAT.reference_element import UFCQuadrilateral @@ -114,7 +115,7 @@ def xysub(x, y): return {x[0]: y[0], x[1]: y[1]} -def ds1_sympy(ct, vs=None): +def ds1_sympy(ct, vs=None, sp=sympy): """Constructs lowest-order case of Arbogast's directly defined C^0 serendipity elements, which are a special case. :param ct: The cell topology of the reference quadrilateral. @@ -221,15 +222,18 @@ def newton_poly(nds, fs, xsym): def diff(expr, xx, alpha): """Differentiate expr with respect to xx. - :arg expr: symengine Expression to differentiate. + :arg expr: symengine/symengine Expression to differentiate. :arg xx: iterable of coordinates to differentiate with respect to. :arg alpha: derivative multiindex, one entry for each entry of xx indicating how many derivatives in that direction. - :returns: New symengine expression.""" - return symengine.diff(expr, *(chain(*(repeat(x, a) for x, a in zip(xx, alpha))))) + :returns: New symengine/symengine expression.""" + if isinstance(expr, sympy.Expr): + return expr.diff(*(zip(xx, alpha))) + else: + return symengine.diff(expr, *(chain(*(repeat(x, a) for x, a in zip(xx, alpha))))) -def dsr_sympy(ct, r, vs=None): +def dsr_sympy(ct, r, vs=None, sp=sympy): """Constructs higher-order (>= 2) case of Arbogast's directly defined C^0 serendipity elements, which include all polynomials of degree r plus a couple of rational functions. @@ -241,11 +245,11 @@ def dsr_sympy(ct, r, vs=None): of the four basis functions. """ if vs is None: # do vertices symbolically - vs = numpy.asarray(list(zip(symengine.symbols('x:4'), - symengine.symbols('y:4')))) + vs = numpy.asarray(list(zip(sp.symbols('x:4'), + sp.symbols('y:4')))) else: vs = numpy.asarray(vs) - xx = numpy.asarray(symengine.symbols("x,y")) + xx = numpy.asarray(sp.symbols("x,y")) ts = numpy.zeros((4, 2), dtype=object) for e in range(4): @@ -291,8 +295,8 @@ def dsr_sympy(ct, r, vs=None): mons = [xx[0] ** i * xx[1] ** j for i in range(r-3) for j in range(r-3-i)] - V = symengine.Matrix([[mon.subs(xysub(xx, nd)) for mon in mons] - for nd in internal_nodes]) + V = sp.Matrix([[mon.subs(xysub(xx, nd)) for mon in mons] + for nd in internal_nodes]) Vinv = V.inv() nmon = len(mons) @@ -308,9 +312,9 @@ def dsr_sympy(ct, r, vs=None): # R for each edge (1 on edge, zero on opposite Rs = [(1 - RV) / 2, (1 + RV) / 2, (1 - RH) / 2, (1 + RH) / 2] - nodes1d = [symengine.Rational(i, r) for i in range(1, r)] + nodes1d = [sp.Rational(i, r) for i in range(1, r)] - s = symengine.Symbol('s') + s = sp.Symbol('s') # for each edge: # I need its adjacent two edges @@ -429,7 +433,7 @@ def nodalize(f): return vs, xx, numpy.asarray(bfs) -def ds_sympy(ct, r, vs=None): +def ds_sympy(ct, r, vs=None, sp=sympy): """Symbolically Constructs Arbogast's directly defined C^0 serendipity elements, which include all polynomials of degree r plus a couple of rational functions. :param ct: The cell topology of the reference quadrilateral. @@ -440,6 +444,6 @@ def ds_sympy(ct, r, vs=None): of the four basis functions. """ if r == 1: - return ds1_sympy(ct, vs) + return ds1_sympy(ct, vs, sp=sp) else: - return dsr_sympy(ct, r, vs) + return dsr_sympy(ct, r, vs, sp=sp) From b84cfe72bca6dfba639b202ceafcad32ada6fcd1 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Thu, 20 Aug 2020 15:13:56 -0500 Subject: [PATCH 626/749] Now using a direct construction of internal Lagrange polynomials. Also, clean-up and efault to (much-faster) symengine --- finat/direct_serendipity.py | 77 +++++++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 29 deletions(-) diff --git a/finat/direct_serendipity.py b/finat/direct_serendipity.py index 79755d42e..d8b6907ca 100644 --- a/finat/direct_serendipity.py +++ b/finat/direct_serendipity.py @@ -73,12 +73,8 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): ''' ct = self.cell.topology - # Build everything in sympy - vs, xx, phis = ds_sympy(ct, self.degree) - if self.degree == 1: - vs, xx, phis = ds1_sympy(ct) - else: - vs, xx, phis = dsr_sympy(ct, self.degree) + # Build everything in sympy/symengine + vs, xx, phis = ds_sym(ct, self.degree, None, symengine) # and convert -- all this can be used for each derivative! phys_verts = coordinate_mapping.physical_vertices() @@ -115,7 +111,7 @@ def xysub(x, y): return {x[0]: y[0], x[1]: y[1]} -def ds1_sympy(ct, vs=None, sp=sympy): +def ds1_sym(ct, vs=None, sp=symengine): """Constructs lowest-order case of Arbogast's directly defined C^0 serendipity elements, which are a special case. :param ct: The cell topology of the reference quadrilateral. @@ -233,7 +229,7 @@ def diff(expr, xx, alpha): return symengine.diff(expr, *(chain(*(repeat(x, a) for x, a in zip(xx, alpha))))) -def dsr_sympy(ct, r, vs=None, sp=sympy): +def dsr_sym(ct, r, vs=None, sp=symengine): """Constructs higher-order (>= 2) case of Arbogast's directly defined C^0 serendipity elements, which include all polynomials of degree r plus a couple of rational functions. @@ -284,27 +280,51 @@ def dsr_sympy(ct, r, vs=None, sp=sympy): ybar = sum(vs[i, 1] for i in range(4)) / 4 internal_bfs = [bubble / bubble.subs(xysub(xx, (xbar, ybar)))] internal_nodes = [(xbar, ybar)] - else: # build a lattice inside the quad + else: # build a triangular lattice inside the quad dx0 = (vs[1, :] - vs[0, :]) / (r-2) dx1 = (vs[2, :] - vs[0, :]) / (r-2) - internal_nodes = [vs[0, :] + dx0 * i + dx1 * j - for i in range(1, r-2) - for j in range(1, r-1-i)] - - mons = [xx[0] ** i * xx[1] ** j - for i in range(r-3) for j in range(r-3-i)] - - V = sp.Matrix([[mon.subs(xysub(xx, nd)) for mon in mons] - for nd in internal_nodes]) - Vinv = V.inv() - nmon = len(mons) + # Vertices of the triangle + v0 = vs[0, :] + dx0 + dx1 + v1 = vs[0, :] + (r-3) * dx0 + dx1 + v2 = vs[0, :] + dx0 + (r-3) * dx1 + + # Pardon the fortran, but these are barycentric coordinates... + bary = numpy.zeros((3,), dtype="object") + y12 = v1[1] - v2[1] + x21 = v2[0] - v1[0] + x02 = v0[0] - v2[0] + y02 = v0[1] - v2[1] + det = y12 * x02 + x21 * y02 + delx = xx[0] - v2[0] + dely = xx[1] - v2[1] + bary[0] = (y12 * delx + x21 * dely) / det + bary[1] = (-y02 * delx + x02 * dely) / det + bary[2] = 1 - bary[0] - bary[1] + + # And this bit directly constructs the Lagrange polynomials + # of degree r-4 on the triangular lattice inside the triangle. + # This trick is restricted to equispaced points, but we're on a much + # lower degree than r. This bypasses symbolic inversion/etc otherwise + # required to build the Lagrange polynomials. + rm4 = r - 4 + lags = [] + internal_nodes = [] + for i in range(rm4, -1, -1): + for j in range(rm4-i, -1, -1): + k = rm4 - i - j + internal_nodes.append((v0 * i + v1 * j + v2 * k)/rm4) + ii = (i, j, k) + lag_cur = sp.Integer(1) + for q in range(3): + for p in range(ii[q]): + lag_cur *= (rm4 * bary[q] - p) / (ii[q] - p) + lags.append(lag_cur.simplify()) internal_bfs = [] - for j in range(nmon): - preibf = bubble * sum(Vinv[i, j] * mons[i] for i in range(nmon)) - internal_bfs.append(preibf - / preibf.subs(xysub(xx, internal_nodes[j]))) + for lag, nd in zip(lags, internal_nodes): + foo = lag * bubble + internal_bfs.append(foo / foo.subs(xysub(xx, nd))) RV = (lams[0] - lams[1]) / (lams[0] + lams[1]) RH = (lams[2] - lams[3]) / (lams[2] + lams[3]) @@ -317,8 +337,7 @@ def dsr_sympy(ct, r, vs=None, sp=sympy): s = sp.Symbol('s') # for each edge: - # I need its adjacent two edges - # and its opposite edge + # I need its adjacent two edges and its opposite edge # and its "tunnel R" RH or RV # This is very 2d specific. opposite_edges = {e: [eother for eother in ct[1] @@ -433,7 +452,7 @@ def nodalize(f): return vs, xx, numpy.asarray(bfs) -def ds_sympy(ct, r, vs=None, sp=sympy): +def ds_sym(ct, r, vs=None, sp=symengine): """Symbolically Constructs Arbogast's directly defined C^0 serendipity elements, which include all polynomials of degree r plus a couple of rational functions. :param ct: The cell topology of the reference quadrilateral. @@ -444,6 +463,6 @@ def ds_sympy(ct, r, vs=None, sp=sympy): of the four basis functions. """ if r == 1: - return ds1_sympy(ct, vs, sp=sp) + return ds1_sym(ct, vs, sp=sp) else: - return dsr_sympy(ct, r, vs, sp=sp) + return dsr_sym(ct, r, vs, sp=sp) From 6d50fee0518c8c14246a0b38410a887fbbe1155e Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Thu, 20 Aug 2020 15:13:56 -0500 Subject: [PATCH 627/749] Now using a direct construction of internal Lagrange polynomials. Also, clean-up and default to (much-faster) symengine --- finat/direct_serendipity.py | 83 +++++++++++++++++++++++-------------- 1 file changed, 51 insertions(+), 32 deletions(-) diff --git a/finat/direct_serendipity.py b/finat/direct_serendipity.py index 79755d42e..cee4157c7 100644 --- a/finat/direct_serendipity.py +++ b/finat/direct_serendipity.py @@ -73,12 +73,8 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): ''' ct = self.cell.topology - # Build everything in sympy - vs, xx, phis = ds_sympy(ct, self.degree) - if self.degree == 1: - vs, xx, phis = ds1_sympy(ct) - else: - vs, xx, phis = dsr_sympy(ct, self.degree) + # Build everything in sympy/symengine + vs, xx, phis = ds_sym(ct, self.degree, None, symengine) # and convert -- all this can be used for each derivative! phys_verts = coordinate_mapping.physical_vertices() @@ -115,7 +111,7 @@ def xysub(x, y): return {x[0]: y[0], x[1]: y[1]} -def ds1_sympy(ct, vs=None, sp=sympy): +def ds1_sym(ct, vs=None, sp=symengine): """Constructs lowest-order case of Arbogast's directly defined C^0 serendipity elements, which are a special case. :param ct: The cell topology of the reference quadrilateral. @@ -126,12 +122,12 @@ def ds1_sympy(ct, vs=None, sp=sympy): of the four basis functions. """ if vs is None: - vs = numpy.asarray(list(zip(symengine.symbols('x:4'), - symengine.symbols('y:4')))) + vs = numpy.asarray(list(zip(sp.symbols('x:4'), + sp.symbols('y:4')))) else: vs = numpy.asarray(vs) - xx = numpy.asarray(symengine.symbols("x,y")) + xx = numpy.asarray(sp.symbols("x,y")) ts = numpy.zeros((4, 2), dtype=object) for e in range(4): @@ -233,7 +229,7 @@ def diff(expr, xx, alpha): return symengine.diff(expr, *(chain(*(repeat(x, a) for x, a in zip(xx, alpha))))) -def dsr_sympy(ct, r, vs=None, sp=sympy): +def dsr_sym(ct, r, vs=None, sp=symengine): """Constructs higher-order (>= 2) case of Arbogast's directly defined C^0 serendipity elements, which include all polynomials of degree r plus a couple of rational functions. @@ -284,27 +280,51 @@ def dsr_sympy(ct, r, vs=None, sp=sympy): ybar = sum(vs[i, 1] for i in range(4)) / 4 internal_bfs = [bubble / bubble.subs(xysub(xx, (xbar, ybar)))] internal_nodes = [(xbar, ybar)] - else: # build a lattice inside the quad + else: # build a triangular lattice inside the quad dx0 = (vs[1, :] - vs[0, :]) / (r-2) dx1 = (vs[2, :] - vs[0, :]) / (r-2) - internal_nodes = [vs[0, :] + dx0 * i + dx1 * j - for i in range(1, r-2) - for j in range(1, r-1-i)] - - mons = [xx[0] ** i * xx[1] ** j - for i in range(r-3) for j in range(r-3-i)] - - V = sp.Matrix([[mon.subs(xysub(xx, nd)) for mon in mons] - for nd in internal_nodes]) - Vinv = V.inv() - nmon = len(mons) + # Vertices of the triangle + v0 = vs[0, :] + dx0 + dx1 + v1 = vs[0, :] + (r-3) * dx0 + dx1 + v2 = vs[0, :] + dx0 + (r-3) * dx1 + + # Pardon the fortran, but these are barycentric coordinates... + bary = numpy.zeros((3,), dtype="object") + y12 = v1[1] - v2[1] + x21 = v2[0] - v1[0] + x02 = v0[0] - v2[0] + y02 = v0[1] - v2[1] + det = y12 * x02 + x21 * y02 + delx = xx[0] - v2[0] + dely = xx[1] - v2[1] + bary[0] = (y12 * delx + x21 * dely) / det + bary[1] = (-y02 * delx + x02 * dely) / det + bary[2] = 1 - bary[0] - bary[1] + + # And this bit directly constructs the Lagrange polynomials + # of degree r-4 on the triangular lattice inside the triangle. + # This trick is restricted to equispaced points, but we're on a much + # lower degree than r. This bypasses symbolic inversion/etc otherwise + # required to build the Lagrange polynomials. + rm4 = r - 4 + lags = [] + internal_nodes = [] + for i in range(rm4, -1, -1): + for j in range(rm4-i, -1, -1): + k = rm4 - i - j + internal_nodes.append((v0 * i + v1 * j + v2 * k)/rm4) + ii = (i, j, k) + lag_cur = sp.Integer(1) + for q in range(3): + for p in range(ii[q]): + lag_cur *= (rm4 * bary[q] - p) / (ii[q] - p) + lags.append(lag_cur.simplify()) internal_bfs = [] - for j in range(nmon): - preibf = bubble * sum(Vinv[i, j] * mons[i] for i in range(nmon)) - internal_bfs.append(preibf - / preibf.subs(xysub(xx, internal_nodes[j]))) + for lag, nd in zip(lags, internal_nodes): + foo = lag * bubble + internal_bfs.append(foo / foo.subs(xysub(xx, nd))) RV = (lams[0] - lams[1]) / (lams[0] + lams[1]) RH = (lams[2] - lams[3]) / (lams[2] + lams[3]) @@ -317,8 +337,7 @@ def dsr_sympy(ct, r, vs=None, sp=sympy): s = sp.Symbol('s') # for each edge: - # I need its adjacent two edges - # and its opposite edge + # I need its adjacent two edges and its opposite edge # and its "tunnel R" RH or RV # This is very 2d specific. opposite_edges = {e: [eother for eother in ct[1] @@ -433,7 +452,7 @@ def nodalize(f): return vs, xx, numpy.asarray(bfs) -def ds_sympy(ct, r, vs=None, sp=sympy): +def ds_sym(ct, r, vs=None, sp=symengine): """Symbolically Constructs Arbogast's directly defined C^0 serendipity elements, which include all polynomials of degree r plus a couple of rational functions. :param ct: The cell topology of the reference quadrilateral. @@ -444,6 +463,6 @@ def ds_sympy(ct, r, vs=None, sp=sympy): of the four basis functions. """ if r == 1: - return ds1_sympy(ct, vs, sp=sp) + return ds1_sym(ct, vs, sp=sp) else: - return dsr_sympy(ct, r, vs, sp=sp) + return dsr_sym(ct, r, vs, sp=sp) From 90d9fa1ec92f2a35ae1bc4c4778d902c928fb515 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Fri, 21 Aug 2020 14:48:03 +0100 Subject: [PATCH 628/749] Cache symbolic basis functions on element --- finat/direct_serendipity.py | 38 +++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/finat/direct_serendipity.py b/finat/direct_serendipity.py index cee4157c7..b70d5ed50 100644 --- a/finat/direct_serendipity.py +++ b/finat/direct_serendipity.py @@ -6,6 +6,7 @@ import sympy from FIAT.polynomial_set import mis from FIAT.reference_element import UFCQuadrilateral +from gem.utils import cached_property from finat.finiteelementbase import FiniteElementBase from finat.physically_mapped import Citations, DirectlyDefinedElement @@ -22,6 +23,7 @@ def __init__(self, cell, degree): self._cell = cell self._degree = degree + self._deriv_cache = {} @property def cell(self): @@ -63,6 +65,19 @@ def index_shape(self): def value_shape(self): return () + @cached_property + def _basis(self): + return ds_sym(self.cell.topology, self.degree, sp=symengine) + + def _basis_deriv(self, xx, alpha): + key = (tuple(xx), alpha) + _, _, phis = self._basis + try: + return self._deriv_cache[key] + except KeyError: + dphi = tuple(diff(phi, xx, alpha) for phi in phis) + return self._deriv_cache.setdefault(key, dphi) + def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): '''Return code for evaluating the element at known points on the reference element. @@ -71,10 +86,8 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): :param ps: the point set. :param entity: the cell entity on which to tabulate. ''' - ct = self.cell.topology - - # Build everything in sympy/symengine - vs, xx, phis = ds_sym(ct, self.degree, None, symengine) + # Build everything in sympy + vs, xx, _ = self._basis # and convert -- all this can be used for each derivative! phys_verts = coordinate_mapping.physical_vertices() @@ -83,9 +96,10 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): coordinate_mapping.physical_points(ps, entity=entity), ps.indices) - repl = {vs[i, j]: phys_verts[i, j] for i in range(4) for j in range(2)} + repl = dict((vs[idx], phys_verts[idx]) + for idx in numpy.ndindex(vs.shape)) - repl.update({s: phys_points[i] for i, s in enumerate(xx)}) + repl.update(zip(xx, phys_points)) mapper = gem.node.Memoizer(sympy2gem) mapper.bindings = repl @@ -95,7 +109,7 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): for i in range(order+1): alphas = mis(2, i) for alpha in alphas: - dphis = [diff(phi, xx, alpha) for phi in phis] + dphis = self._basis_deriv(xx, alpha) result[alpha] = gem.ListTensor(list(map(mapper, dphis))) return result @@ -111,7 +125,7 @@ def xysub(x, y): return {x[0]: y[0], x[1]: y[1]} -def ds1_sym(ct, vs=None, sp=symengine): +def ds1_sym(ct, *, vs=None, sp=symengine): """Constructs lowest-order case of Arbogast's directly defined C^0 serendipity elements, which are a special case. :param ct: The cell topology of the reference quadrilateral. @@ -229,7 +243,7 @@ def diff(expr, xx, alpha): return symengine.diff(expr, *(chain(*(repeat(x, a) for x, a in zip(xx, alpha))))) -def dsr_sym(ct, r, vs=None, sp=symengine): +def dsr_sym(ct, r, *, vs=None, sp=symengine): """Constructs higher-order (>= 2) case of Arbogast's directly defined C^0 serendipity elements, which include all polynomials of degree r plus a couple of rational functions. @@ -452,7 +466,7 @@ def nodalize(f): return vs, xx, numpy.asarray(bfs) -def ds_sym(ct, r, vs=None, sp=symengine): +def ds_sym(ct, r, *, vs=None, sp=symengine): """Symbolically Constructs Arbogast's directly defined C^0 serendipity elements, which include all polynomials of degree r plus a couple of rational functions. :param ct: The cell topology of the reference quadrilateral. @@ -463,6 +477,6 @@ def ds_sym(ct, r, vs=None, sp=symengine): of the four basis functions. """ if r == 1: - return ds1_sym(ct, vs, sp=sp) + return ds1_sym(ct, vs=vs, sp=sp) else: - return dsr_sym(ct, r, vs, sp=sp) + return dsr_sym(ct, r, vs=vs, sp=sp) From 14c75fe2de34c6558b20a013da340cd885be344a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Homolya?= Date: Sat, 22 Aug 2020 18:49:51 +0200 Subject: [PATCH 629/749] Introduce property 'fiat_equivalent' of FInAT elements --- finat/fiat_elements.py | 5 +++++ finat/finiteelementbase.py | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index bdac53293..95cbb9bd6 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -48,6 +48,11 @@ def index_shape(self): def value_shape(self): return self._element.value_shape() + @property + def fiat_equivalent(self): + # Just return the underlying FIAT element + return self._element + def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): '''Return code for evaluating the element at known points on the reference element. diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 98d1d89f8..e53bc40aa 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -99,6 +99,14 @@ def index_shape(self): def value_shape(self): '''A tuple indicating the shape of the element.''' + @property + def fiat_equivalent(self): + '''The FIAT element equivalent to this FInAT element.''' + raise NotImplementedError(str.format( + "Cannot make equivalent FIAT element for {classname}", + classname=type(self).__name__ + )) + def get_indices(self): '''A tuple of GEM :class:`Index` of the correct extents to loop over the basis functions of this element.''' From d982e1b06cfbd4bfc93415ba70650aebd2cf4b31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Homolya?= Date: Sat, 22 Aug 2020 23:05:53 +0200 Subject: [PATCH 630/749] Implement .fiat_equivalent for most elements --- finat/cube.py | 7 +++++++ finat/discontinuous.py | 14 ++++++++++++++ finat/enriched.py | 14 ++++++++++++++ finat/hdivcurl.py | 11 +++++++++++ finat/quadrature_element.py | 9 +++++++++ finat/tensor_product.py | 9 +++++++++ 6 files changed, 64 insertions(+) diff --git a/finat/cube.py b/finat/cube.py index cab199c75..5e9d9af79 100644 --- a/finat/cube.py +++ b/finat/cube.py @@ -51,6 +51,13 @@ def entity_dofs(self): def space_dimension(self): return self.product.space_dimension() + @cached_property + def fiat_equivalent(self): + # On-demand loading of FIAT module + from FIAT.tensor_product import FlattenedDimensions + + return FlattenedDimensions(self.product.fiat_equivalent) + def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): """Return code for evaluating the element at known points on the reference element. diff --git a/finat/discontinuous.py b/finat/discontinuous.py index 69b6d5fc8..f9d27ee21 100644 --- a/finat/discontinuous.py +++ b/finat/discontinuous.py @@ -45,6 +45,20 @@ def index_shape(self): def value_shape(self): return self.element.value_shape + @cached_property + def fiat_equivalent(self): + from FIAT.discontinuous import DiscontinuousElement + from FIAT.discontinuous_raviart_thomas import DiscontinuousRaviartThomas + from FIAT.raviart_thomas import RaviartThomas + + fiat_element = self.element.fiat_equivalent + if isinstance(fiat_element, RaviartThomas): + ref_el = fiat_element.get_reference_element() + deg = fiat_element.degree() + return DiscontinuousRaviartThomas(ref_el, deg) + else: + return DiscontinuousElement(fiat_element) + def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): return self.element.basis_evaluation(order, ps, entity, coordinate_mapping=coordinate_mapping) diff --git a/finat/enriched.py b/finat/enriched.py index 370642115..06e184384 100644 --- a/finat/enriched.py +++ b/finat/enriched.py @@ -64,6 +64,20 @@ def value_shape(self): shape, = set(elem.value_shape for elem in self.elements) return shape + @cached_property + def fiat_equivalent(self): + # Avoid circular import dependency + from finat.mixed import MixedSubElement + + if all(isinstance(e, MixedSubElement) for e in self.elements): + # EnrichedElement is actually a MixedElement + from FIAT.mixed import MixedElement # on-demand loading + return MixedElement([e.element.fiat_equivalent + for e in self.elements], ref_el=self.cell) + else: + from FIAT.enriched import EnrichedElement # on-demand loading + return EnrichedElement(*[e.fiat_equivalent for e in self.elements]) + def _compose_evaluations(self, results): keys, = set(map(frozenset, results)) diff --git a/finat/hdivcurl.py b/finat/hdivcurl.py index 61bb80429..036161cad 100644 --- a/finat/hdivcurl.py +++ b/finat/hdivcurl.py @@ -1,6 +1,7 @@ from FIAT.reference_element import LINE import gem +from gem.utils import cached_property from finat.finiteelementbase import FiniteElementBase from finat.tensor_product import TensorProductElement @@ -88,6 +89,11 @@ def __init__(self, wrappee): def formdegree(self): return self.cell.get_spatial_dimension() - 1 + @cached_property + def fiat_equivalent(self): + from FIAT.hdivcurl import Hdiv # on-demand loading + return Hdiv(self.wrappee.fiat_equivalent) + @property def mapping(self): return "contravariant piola" @@ -112,6 +118,11 @@ def __init__(self, wrappee): def formdegree(self): return 1 + @cached_property + def fiat_equivalent(self): + from FIAT.hdivcurl import Hcurl # on-demand loading + return Hcurl(self.wrappee.fiat_equivalent) + @property def mapping(self): return "covariant piola" diff --git a/finat/quadrature_element.py b/finat/quadrature_element.py index 1e9c4d0ba..698eb2a3a 100644 --- a/finat/quadrature_element.py +++ b/finat/quadrature_element.py @@ -51,6 +51,15 @@ def index_shape(self): def value_shape(self): return () + @cached_property + def fiat_equivalent(self): + # On-demand loading of FIAT module + from FIAT.quadrature_element import QuadratureElement + + ps = self._rule.point_set + weights = getattr(self._rule, 'weights', None) + return QuadratureElement(self.cell, ps.points, weights) + def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): '''Return code for evaluating the element at known points on the reference element. diff --git a/finat/tensor_product.py b/finat/tensor_product.py index 59a29e980..bd2c2be7c 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -65,6 +65,15 @@ def index_shape(self): def value_shape(self): return self._value_shape + @cached_property + def fiat_equivalent(self): + # On-demand loading of FIAT module + from FIAT.tensor_product import TensorProductElement + + # FIAT TensorProductElement support only 2 factors + A, B = self.factors + return TensorProductElement(A.fiat_equivalent, B.fiat_equivalent) + def _factor_entity(self, entity): # Default entity if entity is None: From f18361d92375e99da2e494d9a5639a80bb522bdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Homolya?= Date: Sat, 22 Aug 2020 23:30:41 +0200 Subject: [PATCH 631/749] Add FIAT wrapper for Bernstein elements --- finat/__init__.py | 1 + finat/fiat_elements.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/finat/__init__.py b/finat/__init__.py index 01685e159..ef7156a72 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -1,3 +1,4 @@ +from .fiat_elements import Bernstein # noqa: F401 from .fiat_elements import Bubble, CrouzeixRaviart, DiscontinuousTaylor # noqa: F401 from .fiat_elements import Lagrange, DiscontinuousLagrange # noqa: F401 from .fiat_elements import DPC, Serendipity # noqa: F401 diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 95cbb9bd6..f918107ba 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -272,6 +272,12 @@ def value_shape(self): return () +class Bernstein(ScalarFiatElement): + # TODO: Replace this with a smarter implementation + def __init__(self, cell, degree): + super(Bernstein, self).__init__(FIAT.Bernstein(cell, degree)) + + class Bubble(ScalarFiatElement): def __init__(self, cell, degree): super(Bubble, self).__init__(FIAT.Bubble(cell, degree)) From 7604e2344bd2daf2e7b389fa249596cc5a4438fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Homolya?= Date: Sun, 23 Aug 2020 00:14:46 +0200 Subject: [PATCH 632/749] Add NodalEnrichedElement wrapper --- finat/__init__.py | 1 + finat/nodal_enriched.py | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 finat/nodal_enriched.py diff --git a/finat/__init__.py b/finat/__init__.py index ef7156a72..81df9f968 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -19,6 +19,7 @@ from .enriched import EnrichedElement # noqa: F401 from .hdivcurl import HCurlElement, HDivElement # noqa: F401 from .mixed import MixedElement # noqa: F401 +from .nodal_enriched import NodalEnrichedElement # noqa: 401 from .quadrature_element import QuadratureElement # noqa: F401 from .restricted import RestrictedElement # noqa: F401 from .runtime_tabulated import RuntimeTabulated # noqa: F401 diff --git a/finat/nodal_enriched.py b/finat/nodal_enriched.py new file mode 100644 index 000000000..adaa5cc59 --- /dev/null +++ b/finat/nodal_enriched.py @@ -0,0 +1,12 @@ +import FIAT + +from finat.fiat_elements import FiatElement + + +class NodalEnrichedElement(FiatElement): + """An enriched element with a nodal basis.""" + + def __init__(self, elements): + fiat_elements = [elem.fiat_equivalent for elem in elements] + nodal_enriched = FIAT.NodalEnrichedElement(*fiat_elements) + super(NodalEnrichedElement, self).__init__(nodal_enriched) From beae633e22981976e8ba2c7636024f6cb8dbbcfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Homolya?= Date: Sun, 23 Aug 2020 00:26:56 +0200 Subject: [PATCH 633/749] Remove most local imports --- finat/cube.py | 6 ++---- finat/enriched.py | 11 ++++++----- finat/hdivcurl.py | 3 +-- finat/quadrature_element.py | 7 +++---- finat/tensor_product.py | 6 ++---- 5 files changed, 14 insertions(+), 19 deletions(-) diff --git a/finat/cube.py b/finat/cube.py index 5e9d9af79..8813fd5c7 100644 --- a/finat/cube.py +++ b/finat/cube.py @@ -2,6 +2,7 @@ from FIAT.reference_element import UFCHexahedron, UFCQuadrilateral from FIAT.reference_element import compute_unflattening_map, flatten_entities +from FIAT.tensor_product import FlattenedDimensions as FIAT_FlattenedDimensions from gem.utils import cached_property @@ -53,10 +54,7 @@ def space_dimension(self): @cached_property def fiat_equivalent(self): - # On-demand loading of FIAT module - from FIAT.tensor_product import FlattenedDimensions - - return FlattenedDimensions(self.product.fiat_equivalent) + return FIAT_FlattenedDimensions(self.product.fiat_equivalent) def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): """Return code for evaluating the element at known points on the diff --git a/finat/enriched.py b/finat/enriched.py index 06e184384..15ace53f3 100644 --- a/finat/enriched.py +++ b/finat/enriched.py @@ -3,6 +3,8 @@ import numpy +import FIAT + import gem from gem.utils import cached_property @@ -71,12 +73,11 @@ def fiat_equivalent(self): if all(isinstance(e, MixedSubElement) for e in self.elements): # EnrichedElement is actually a MixedElement - from FIAT.mixed import MixedElement # on-demand loading - return MixedElement([e.element.fiat_equivalent - for e in self.elements], ref_el=self.cell) + return FIAT.MixedElement([e.element.fiat_equivalent + for e in self.elements], ref_el=self.cell) else: - from FIAT.enriched import EnrichedElement # on-demand loading - return EnrichedElement(*[e.fiat_equivalent for e in self.elements]) + return FIAT.EnrichedElement(*[e.fiat_equivalent + for e in self.elements]) def _compose_evaluations(self, results): keys, = set(map(frozenset, results)) diff --git a/finat/hdivcurl.py b/finat/hdivcurl.py index 036161cad..c184c8162 100644 --- a/finat/hdivcurl.py +++ b/finat/hdivcurl.py @@ -1,3 +1,4 @@ +from FIAT.hdivcurl import Hdiv, Hcurl from FIAT.reference_element import LINE import gem @@ -91,7 +92,6 @@ def formdegree(self): @cached_property def fiat_equivalent(self): - from FIAT.hdivcurl import Hdiv # on-demand loading return Hdiv(self.wrappee.fiat_equivalent) @property @@ -120,7 +120,6 @@ def formdegree(self): @cached_property def fiat_equivalent(self): - from FIAT.hdivcurl import Hcurl # on-demand loading return Hcurl(self.wrappee.fiat_equivalent) @property diff --git a/finat/quadrature_element.py b/finat/quadrature_element.py index 698eb2a3a..33622d60a 100644 --- a/finat/quadrature_element.py +++ b/finat/quadrature_element.py @@ -2,6 +2,8 @@ import numpy +import FIAT + import gem from gem.utils import cached_property @@ -53,12 +55,9 @@ def value_shape(self): @cached_property def fiat_equivalent(self): - # On-demand loading of FIAT module - from FIAT.quadrature_element import QuadratureElement - ps = self._rule.point_set weights = getattr(self._rule, 'weights', None) - return QuadratureElement(self.cell, ps.points, weights) + return FIAT.QuadratureElement(self.cell, ps.points, weights) def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): '''Return code for evaluating the element at known points on the diff --git a/finat/tensor_product.py b/finat/tensor_product.py index bd2c2be7c..7c9b21ef1 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -4,6 +4,7 @@ import numpy +import FIAT from FIAT.polynomial_set import mis from FIAT.reference_element import TensorProductCell @@ -67,12 +68,9 @@ def value_shape(self): @cached_property def fiat_equivalent(self): - # On-demand loading of FIAT module - from FIAT.tensor_product import TensorProductElement - # FIAT TensorProductElement support only 2 factors A, B = self.factors - return TensorProductElement(A.fiat_equivalent, B.fiat_equivalent) + return FIAT.TensorProductElement(A.fiat_equivalent, B.fiat_equivalent) def _factor_entity(self, entity): # Default entity From af826138238d1477340bc30e9e3888ad0448e434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Homolya?= Date: Mon, 24 Aug 2020 12:03:16 +0200 Subject: [PATCH 634/749] Address PR comments --- finat/enriched.py | 4 ++-- finat/fiat_elements.py | 2 +- finat/finiteelementbase.py | 7 +++---- finat/nodal_enriched.py | 6 +++--- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/finat/enriched.py b/finat/enriched.py index 15ace53f3..4ff89fb12 100644 --- a/finat/enriched.py +++ b/finat/enriched.py @@ -76,8 +76,8 @@ def fiat_equivalent(self): return FIAT.MixedElement([e.element.fiat_equivalent for e in self.elements], ref_el=self.cell) else: - return FIAT.EnrichedElement(*[e.fiat_equivalent - for e in self.elements]) + return FIAT.EnrichedElement(*(e.fiat_equivalent + for e in self.elements)) def _compose_evaluations(self, results): keys, = set(map(frozenset, results)) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index f918107ba..557ed2f4b 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -275,7 +275,7 @@ def value_shape(self): class Bernstein(ScalarFiatElement): # TODO: Replace this with a smarter implementation def __init__(self, cell, degree): - super(Bernstein, self).__init__(FIAT.Bernstein(cell, degree)) + super().__init__(FIAT.Bernstein(cell, degree)) class Bubble(ScalarFiatElement): diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index e53bc40aa..f817672b6 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -102,10 +102,9 @@ def value_shape(self): @property def fiat_equivalent(self): '''The FIAT element equivalent to this FInAT element.''' - raise NotImplementedError(str.format( - "Cannot make equivalent FIAT element for {classname}", - classname=type(self).__name__ - )) + raise NotImplementedError( + f"Cannot make equivalent FIAT element for {type(self).__name__}" + ) def get_indices(self): '''A tuple of GEM :class:`Index` of the correct extents to loop over diff --git a/finat/nodal_enriched.py b/finat/nodal_enriched.py index adaa5cc59..065394a01 100644 --- a/finat/nodal_enriched.py +++ b/finat/nodal_enriched.py @@ -7,6 +7,6 @@ class NodalEnrichedElement(FiatElement): """An enriched element with a nodal basis.""" def __init__(self, elements): - fiat_elements = [elem.fiat_equivalent for elem in elements] - nodal_enriched = FIAT.NodalEnrichedElement(*fiat_elements) - super(NodalEnrichedElement, self).__init__(nodal_enriched) + nodal_enriched = FIAT.NodalEnrichedElement(*(elem.fiat_equivalent + for elem in elements)) + super().__init__(nodal_enriched) From 0429738f78dc5e994cd1a49634bff8c85c6b0eec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Homolya?= Date: Tue, 25 Aug 2020 11:26:27 +0200 Subject: [PATCH 635/749] Ignore FIAT.DiscontinuousRaviartThomas --- finat/discontinuous.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/finat/discontinuous.py b/finat/discontinuous.py index f9d27ee21..a608c425f 100644 --- a/finat/discontinuous.py +++ b/finat/discontinuous.py @@ -1,3 +1,5 @@ +import FIAT + from gem.utils import cached_property from finat.finiteelementbase import FiniteElementBase @@ -47,17 +49,7 @@ def value_shape(self): @cached_property def fiat_equivalent(self): - from FIAT.discontinuous import DiscontinuousElement - from FIAT.discontinuous_raviart_thomas import DiscontinuousRaviartThomas - from FIAT.raviart_thomas import RaviartThomas - - fiat_element = self.element.fiat_equivalent - if isinstance(fiat_element, RaviartThomas): - ref_el = fiat_element.get_reference_element() - deg = fiat_element.degree() - return DiscontinuousRaviartThomas(ref_el, deg) - else: - return DiscontinuousElement(fiat_element) + return FIAT.DiscontinuousElement(self.element.fiat_equivalent) def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): return self.element.basis_evaluation(order, ps, entity, coordinate_mapping=coordinate_mapping) From 2175c09cb7c75897235e67ca04b065e16c4a3b6e Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Tue, 25 Aug 2020 17:05:27 -0500 Subject: [PATCH 636/749] remove comments in test code --- test/test_aw.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/test/test_aw.py b/test/test_aw.py index eee1299be..0dc8c4910 100644 --- a/test/test_aw.py +++ b/test/test_aw.py @@ -34,8 +34,6 @@ def test_awnc(): mppng = MyMapping(ref_cell, phys_cell) Mgem = ref_el_finat.basis_transformation(mppng) M = evaluate([Mgem])[0].arr - # print(M) - # print(ref_vals_piola.shape) ref_vals_zany = np.zeros((15, 2, 2, len(phys_pts))) for k in range(ref_vals_zany.shape[3]): for ell1 in range(2): @@ -75,8 +73,6 @@ def test_awc(): mppng = MyMapping(ref_cell, phys_cell) Mgem = ref_el_finat.basis_transformation(mppng) M = evaluate([Mgem])[0].arr - # print(M) - # print(ref_vals_piola.shape) ref_vals_zany = np.zeros((24, 2, 2, len(phys_pts))) for k in range(ref_vals_zany.shape[3]): for ell1 in range(2): @@ -84,13 +80,4 @@ def test_awc(): ref_vals_zany[:, ell1, ell2, k] = \ M @ ref_vals_piola[:, ell1, ell2, k] - # Q = FIAT.make_quadrature(phys_cell, 6) - # vals = phys_element.tabulate(0, Q.pts)[(0, 0)] - # print() - # for i in (0, 9, 21): - # result = 0.0 - # for k in range(len(Q.wts)): - # result += Q.wts[k] * (vals[i, :, :, k] ** 2).sum() - # print(np.sqrt(result)) - assert np.allclose(ref_vals_zany[:21], phys_vals[:21]) From 9b53f3fe04a13a03e0e33ddd2a5ac83004f4ceba Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Tue, 25 Aug 2020 17:07:32 -0500 Subject: [PATCH 637/749] rename mpping --- test/test_aw.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_aw.py b/test/test_aw.py index 0dc8c4910..42ec41863 100644 --- a/test/test_aw.py +++ b/test/test_aw.py @@ -31,8 +31,8 @@ def test_awnc(): J @ ref_vals[i, :, :, k] @ J.T / detJ**2 # Zany map the results - mppng = MyMapping(ref_cell, phys_cell) - Mgem = ref_el_finat.basis_transformation(mppng) + mappng = MyMapping(ref_cell, phys_cell) + Mgem = ref_el_finat.basis_transformation(mappng) M = evaluate([Mgem])[0].arr ref_vals_zany = np.zeros((15, 2, 2, len(phys_pts))) for k in range(ref_vals_zany.shape[3]): From 65922db3da83b780f8b9ae34c33eb21ea7ce9f70 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Tue, 25 Aug 2020 22:29:51 +0100 Subject: [PATCH 638/749] More magic methods for gem Nodes Adds a __matmul__ method, and a componentwise helper so the existing sugar also works transparently for shaped objects. --- gem/gem.py | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 358123485..2859ded3d 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -80,25 +80,52 @@ def __getitem__(self, indices): return Indexed(self, indices) def __add__(self, other): - return Sum(self, as_gem(other)) + return componentwise(Sum, self, as_gem(other)) def __radd__(self, other): return as_gem(other).__add__(self) def __sub__(self, other): - return Sum(self, Product(Literal(-1), as_gem(other))) + return componentwise( + Sum, self, + componentwise(Product, Literal(-1), as_gem(other))) def __rsub__(self, other): return as_gem(other).__sub__(self) def __mul__(self, other): - return Product(self, as_gem(other)) + return componentwise(Product, self, as_gem(other)) def __rmul__(self, other): return as_gem(other).__mul__(self) + def __matmul__(self, other): + other = as_gem(other) + if not self.shape and not other.shape: + return Product(self, other) + elif not (self.shape and other.shape): + raise ValueError("Both objects must have shape for matmul") + elif self.shape[-1] != other.shape[0]: + raise ValueError(f"Mismatching shapes {self.shape} and {other.shape} in matmul") + *i, k = indices(len(self.shape)) + _, *j = indices(len(other.shape)) + expr = Product(Indexed(self, tuple(i) + (k, )), + Indexed(other, (k, ) + tuple(j))) + return ComponentTensor(IndexSum(expr, (k, )), tuple(i) + tuple(j)) + + def __rmatmul__(self, other): + return as_gem(other).__matmul__(self) + + @property + def T(self): + i = indices(len(self.shape)) + return ComponentTensor(Indexed(self, i), tuple(reversed(i))) + def __truediv__(self, other): - return Division(self, as_gem(other)) + other = as_gem(other) + if other.shape: + raise ValueError("Denominator must be scalar") + return componentwise(Division, self, other) def __rtruediv__(self, other): return as_gem(other).__truediv__(self) @@ -979,6 +1006,28 @@ def indices(n): return tuple(Index() for _ in range(n)) +def componentwise(op, *exprs): + """Apply gem op to exprs component-wise and wrap up in a ComponentTensor. + + :arg op: function that returns a gem Node. + :arg exprs: expressions to apply op to. + :raises ValueError: if the expressions have mismatching shapes. + :returns: New gem Node constructed from op. + + Each expression must either have the same shape, or else be + scalar. Shaped expressions are indexed, the op is applied to the + scalar expressions and the result is wrapped up in a ComponentTensor. + + """ + shapes = set(e.shape for e in exprs) + if len(shapes - {()}) > 1: + raise ValueError("expressions must have matching shape (or else be scalar)") + shape = max(shapes) + i = indices(len(shape)) + exprs = tuple(Indexed(e, i) if e.shape else e for e in exprs) + return ComponentTensor(op(*exprs), i) + + def as_gem(expr): """Attempt to convert an expression into GEM. From 8493067c35956d5e6039a2a4622d35aa7f385638 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Tue, 25 Aug 2020 17:09:30 -0500 Subject: [PATCH 639/749] mppng --> mapping in morley test --- test/test_morley.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_morley.py b/test/test_morley.py index d9c32cd01..3d9ceff94 100644 --- a/test/test_morley.py +++ b/test/test_morley.py @@ -13,9 +13,9 @@ def test_morley(): phys_cell = FIAT.ufc_simplex(2) phys_cell.vertices = ((0.0, 0.1), (1.17, -0.09), (0.15, 1.84)) - mppng = MyMapping(ref_cell, phys_cell) + mapping = MyMapping(ref_cell, phys_cell) z = (0, 0) - finat_vals_gem = ref_element.basis_evaluation(0, ref_pts, coordinate_mapping=mppng)[z] + finat_vals_gem = ref_element.basis_evaluation(0, ref_pts, coordinate_mapping=mapping)[z] finat_vals = evaluate([finat_vals_gem])[0].arr phys_cell_FIAT = FIAT.Morley(phys_cell) From ad0b45ab608969e0f822adb76d95d90e7bb4435a Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Wed, 26 Aug 2020 11:34:41 -0500 Subject: [PATCH 640/749] Refactor edge transformations; add entity_closure_dofs --- finat/aw.py | 135 ++++++++++++++++++++--------------------- finat/fiat_elements.py | 5 +- 2 files changed, 69 insertions(+), 71 deletions(-) diff --git a/finat/aw.py b/finat/aw.py index cea70ce52..57d53f0b0 100644 --- a/finat/aw.py +++ b/finat/aw.py @@ -9,57 +9,63 @@ from finat.physically_mapped import PhysicallyMappedElement, Citations +def _edge_transform(T, coordinate_mapping): + Vsub = numpy.zeros((12, 12), dtype=object) + + for multiindex in numpy.ndindex(Vsub.shape): + Vsub[multiindex] = Literal(Vsub[multiindex]) + + for i in range(0, 12, 2): + Vsub[i, i] = Literal(1) + + # This bypasses the GEM wrapper. + that = numpy.array([T.compute_normalized_edge_tangent(i) for i in range(3)]) + nhat = numpy.array([T.compute_normal(i) for i in range(3)]) + + detJ = coordinate_mapping.detJ_at([1/3, 1/3]) + J = coordinate_mapping.jacobian_at([1/3, 1/3]) + J_np = numpy.array([[J[0, 0], J[0, 1]], + [J[1, 0], J[1, 1]]]) + JTJ = J_np.T @ J_np + + for e in range(3): + # Compute alpha and beta for the edge. + Ghat_T = numpy.array([nhat[e, :], that[e, :]]) + + (alpha, beta) = Ghat_T @ JTJ @ that[e, :] / detJ + + # Stuff into the right rows and columns. + (idx1, idx2) = (4*e + 1, 4*e + 3) + Vsub[idx1, idx1-1] = Literal(-1) * alpha / beta + Vsub[idx1, idx1] = Literal(1) / beta + Vsub[idx2, idx2-1] = Literal(-1) * alpha / beta + Vsub[idx2, idx2] = Literal(1) / beta + + return Vsub + + class ArnoldWintherNC(PhysicallyMappedElement, FiatElement): def __init__(self, cell, degree): if Citations is not None: Citations().register("Arnold2003") super(ArnoldWintherNC, self).__init__(FIAT.ArnoldWintherNC(cell, degree)) - def basis_transformation(self, coordinate_mapping, as_numpy=False): + def basis_transformation(self, coordinate_mapping): """Note, the extra 3 dofs which are removed here correspond to the constraints.""" - V = numpy.zeros((18, 15), dtype=object) + T = self.cell + V = numpy.zeros((18, 15), dtype=object) for multiindex in numpy.ndindex(V.shape): V[multiindex] = Literal(V[multiindex]) - for i in range(0, 12, 2): - V[i, i] = Literal(1) - - T = self.cell - - # This bypasses the GEM wrapper. - that = numpy.array([T.compute_normalized_edge_tangent(i) for i in range(3)]) - nhat = numpy.array([T.compute_normal(i) for i in range(3)]) - - detJ = coordinate_mapping.detJ_at([1/3, 1/3]) - J = coordinate_mapping.jacobian_at([1/3, 1/3]) - J_np = numpy.array([[J[0, 0], J[0, 1]], - [J[1, 0], J[1, 1]]]) - JTJ = J_np.T @ J_np - - for e in range(3): - - # Compute alpha and beta for the edge. - Ghat_T = numpy.array([nhat[e, :], that[e, :]]) - - (alpha, beta) = Ghat_T @ JTJ @ that[e, :] / detJ - - # Stuff into the right rows and columns. - (idx1, idx2) = (4*e + 1, 4*e + 3) - V[idx1, idx1-1] = Literal(-1) * alpha / beta - V[idx1, idx1] = Literal(1) / beta - V[idx2, idx2-1] = Literal(-1) * alpha / beta - V[idx2, idx2] = Literal(1) / beta + V[:12, :12] = _edge_transform(T, coordinate_mapping) # internal dofs for i in range(12, 15): V[i, i] = Literal(1) - if as_numpy: - return V.T - else: - return ListTensor(V.T) + return ListTensor(V.T) def entity_dofs(self): return {0: {0: [], @@ -68,6 +74,13 @@ def entity_dofs(self): 1: {0: [0, 1, 2, 3], 1: [4, 5, 6, 7], 2: [8, 9, 10, 11]}, 2: {0: [12, 13, 14]}} + def entity_closure_dofs(self): + return {0: {0: [], + 1: [], + 2: []}, + 1: {0: [0, 1, 2, 3], 1: [4, 5, 6, 7], 2: [8, 9, 10, 11]}, + 2: {0: list(range(15))}} + @property def index_shape(self): return (15,) @@ -83,16 +96,13 @@ def __init__(self, cell, degree): super(ArnoldWinther, self).__init__(FIAT.ArnoldWinther(cell, degree)) def basis_transformation(self, coordinate_mapping): - """The extra 6 dofs removed here correspond to - the constraints.""" + """The extra 6 dofs removed here correspond to the constraints.""" V = numpy.zeros((30, 24), dtype=object) for multiindex in numpy.ndindex(V.shape): V[multiindex] = Literal(V[multiindex]) - # TODO: find a succinct expression for W in terms of J. J = coordinate_mapping.jacobian_at([1/3, 1/3]) - detJ = coordinate_mapping.detJ_at([1/3, 1/3]) W = numpy.zeros((3, 3), dtype=object) W[0, 0] = J[1, 1]*J[1, 1] @@ -104,40 +114,12 @@ def basis_transformation(self, coordinate_mapping): W[2, 0] = J[1, 0]*J[1, 0] W[2, 1] = -2*J[1, 0]*J[0, 0] W[2, 2] = J[0, 0]*J[0, 0] - W_check = W - # Put into the right rows and columns. - V[0:3, 0:3] = V[3:6, 3:6] = V[6:9, 6:9] = W_check - - for i in range(9, 21, 2): - V[i, i] = Literal(1) - - T = self.cell - - # This bypasses the GEM wrapper. - that = numpy.array([T.compute_normalized_edge_tangent(i) for i in range(3)]) - nhat = numpy.array([T.compute_normal(i) for i in range(3)]) - - detJ = coordinate_mapping.detJ_at([1/3, 1/3]) - J = coordinate_mapping.jacobian_at([1/3, 1/3]) - J_np = numpy.array([[J[0, 0], J[0, 1]], - [J[1, 0], J[1, 1]]]) - JTJ = J_np.T @ J_np - - for e in range(3): - - # Compute alpha and beta for the edge. - Ghat_T = numpy.array([nhat[e, :], that[e, :]]) - (alpha, beta) = Ghat_T @ JTJ @ that[e, :] / detJ + # Put into the right rows and columns. + V[0:3, 0:3] = V[3:6, 3:6] = V[6:9, 6:9] = W - # Stuff into the right rows and columns. - (idx1, idx2) = (9 + 4*e + 1, 9 + 4*e + 3) - V[idx1, idx1-1] = Literal(-1) * alpha / beta - V[idx1, idx1] = Literal(1) / beta - V[idx2, idx2-1] = Literal(-1) * alpha / beta - V[idx2, idx2] = Literal(1) / beta + V[9:21, 9:21] = _edge_transform(self.cell, coordinate_mapping) - # internal dofs (AWnc has good conditioning, so leave this alone) for i in range(21, 24): V[i, i] = Literal(1) @@ -150,6 +132,21 @@ def entity_dofs(self): 1: {0: [9, 10, 11, 12], 1: [13, 14, 15, 16], 2: [17, 18, 19, 20]}, 2: {0: [21, 22, 23]}} + # need to overload since we're cutting out some dofs from the FIAT element. + def entity_closure_dofs(self): + ct = self.cell.topology + ecdofs = {i: {} for i in range(3)} + for i in range(3): + ecdofs[0][i] = list(range(3*i, 3*(i+1))) + + for i in range(3): + ecdofs[1][i] = [dof for v in ct[1][i] for dof in ecdofs[0][v]] + \ + list(range(9+4*i, 9+4*(i+1))) + + ecdofs[2][0] = list(range(24)) + + return ecdofs + @property def index_shape(self): return (24,) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index ab8137e1c..fd0852144 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -97,8 +97,9 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): exprs.append(gem.Zero(self.index_shape)) if self.value_shape: # As above, this extent may be different from that advertised by the finat element. - assert len(self.get_indices()) == 1, "Was not expecting more than one index" - beta = (gem.Index(extent=self._element.space_dimension()), ) + beta = gem.indices(*index_shape) + assert len(beta) == len(self.get_indices()) + zeta = self.get_value_indices() result[alpha] = gem.ComponentTensor( gem.Indexed( From 001b730b6517309dc1b41dd9e943ef12b783ae6b Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Wed, 26 Aug 2020 14:31:55 -0500 Subject: [PATCH 641/749] Revert an indexing change that broke things; fix direct serendipity test for new physical geometry interface --- finat/fiat_elements.py | 2 +- test/test_direct_serendipity.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 6c1d1e3dd..6e02d61ef 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -102,7 +102,7 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): exprs.append(gem.Zero(self.index_shape)) if self.value_shape: # As above, this extent may be different from that advertised by the finat element. - beta = gem.indices(*index_shape) + beta = self.get_indices() assert len(beta) == len(self.get_indices()) zeta = self.get_value_indices() diff --git a/test/test_direct_serendipity.py b/test/test_direct_serendipity.py index 88b925b5d..807edd1bf 100644 --- a/test/test_direct_serendipity.py +++ b/test/test_direct_serendipity.py @@ -18,6 +18,9 @@ def cell_size(self): def jacobian_at(self, point): raise NotImplementedError + def detJ_at(self, point): + raise NotImplementedError + def reference_normals(self): raise NotImplementedError From dfcad543ec93c1dbb02de3cb93d5f058b2a8154b Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Fri, 28 Aug 2020 17:19:14 +0100 Subject: [PATCH 642/749] Fix tabulation for zany elements We do need to obey index_shape, but we need to generate indices whose extents match index_shape. --- finat/fiat_elements.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 6e02d61ef..ff10c11f9 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -101,8 +101,9 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): assert np.allclose(table, 0.0) exprs.append(gem.Zero(self.index_shape)) if self.value_shape: - # As above, this extent may be different from that advertised by the finat element. - beta = self.get_indices() + # As above, this extent may be different from that + # advertised by the finat element. + beta = tuple(gem.Index(extent=i) for i in index_shape) assert len(beta) == len(self.get_indices()) zeta = self.get_value_indices() From 3656993a708299c33c777d459003b01d371070cb Mon Sep 17 00:00:00 2001 From: Justincrum Date: Tue, 8 Sep 2020 13:26:06 -0700 Subject: [PATCH 643/749] More stuff for SminusCurl. --- finat/__init__.py | 1 + finat/fiat_elements.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/finat/__init__.py b/finat/__init__.py index 25dacc799..01fe2f29b 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -3,6 +3,7 @@ from .fiat_elements import DPC, Serendipity # noqa: F401 from .fiat_elements import TrimmedSerendipityFace, TrimmedSerendipityEdge # noqa: F401 from .fiat_elements import TrimmedSerendipityDiv #noqa: F401 +from .fiat_elements import TrimmedSerendipityCurl #noqa: F401 from .fiat_elements import BrezziDouglasMarini, BrezziDouglasFortinMarini # noqa: F401 from .fiat_elements import Nedelec, NedelecSecondKind, RaviartThomas # noqa: F401 from .fiat_elements import HellanHerrmannJohnson, Regge # noqa: F401 diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index af3fb402e..359b1ed11 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -332,6 +332,10 @@ class TrimmedSerendipityEdge(VectorFiatElement): def __init__(self, cell, degree): super(TrimmedSerendipityEdge, self).__init__(FIAT.TrimmedSerendipityEdge(cell, degree)) +class TrimmedSerendipityCurl(VectorFiatElement): + def __init__(self, cell, degree): + super(TrimmedSerendipityCurl, self).__init__(FIAT.TrimmedSerendipityCurl(cell, degree)) + class BrezziDouglasMarini(VectorFiatElement): def __init__(self, cell, degree): From be5d0904834e8afe0a633480165bf16fccf68c87 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Thu, 10 Sep 2020 14:19:03 -0500 Subject: [PATCH 644/749] Add citations for KMV elements --- finat/fiat_elements.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 0fe1027ea..1ad2ffec0 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -10,6 +10,37 @@ from finat.finiteelementbase import FiniteElementBase from finat.sympy2gem import sympy2gem +try: + from firedrake_citations import Citations + Citations().add("Geevers2018new", """ +@article{Geevers2018new, + title={New higher-order mass-lumped tetrahedral elements for wave propagation modelling}, + author={Geevers, Sjoerd and Mulder, Wim A and van der Vegt, Jaap JW}, + journal={SIAM journal on scientific computing}, + volume={40}, + number={5}, + pages={A2830--A2857}, + year={2018}, + publisher={SIAM}, + doi={https://doi.org/10.1137/18M1175549}, +} +""") + Citations().add("Chin1999higher", """ +@article{chin1999higher, + title={Higher-order triangular and tetrahedral finite elements with mass lumping for solving the wave equation}, + author={Chin-Joe-Kong, MJS and Mulder, Wim A and Van Veldhuizen, M}, + journal={Journal of Engineering Mathematics}, + volume={35}, + number={4}, + pages={405--426}, + year={1999}, + publisher={Springer}, + doi={https://doi.org/10.1023/A:1004420829610}, +} +""") +except ImportError: + Citations = None + class FiatElement(FiniteElementBase): """Base class for finite elements for which the tabulation is provided @@ -290,6 +321,9 @@ def __init__(self, cell, degree): class KongMulderVeldhuizen(ScalarFiatElement): def __init__(self, cell, degree): super(KongMulderVeldhuizen, self).__init__(FIAT.KongMulderVeldhuizen(cell, degree)) + if Citations is not None: + Citations().register("Chin1999higher") + Citations().register("Geevers2018new") class DiscontinuousLagrange(ScalarFiatElement): From 215f319e2245b136b06c59809f95c67d98099882 Mon Sep 17 00:00:00 2001 From: Justincrum Date: Fri, 25 Sep 2020 11:23:02 -0700 Subject: [PATCH 645/749] Fixed something having variant in it. --- finat/fiat_elements.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 116de3cb5..7a71109d1 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -354,7 +354,8 @@ def __init__(self, cell, degree): class BrezziDouglasMarini(VectorFiatElement): def __init__(self, cell, degree, variant=None): - super(BrezziDouglasMarini, self).__init__(FIAT.BrezziDouglasMarini(cell, degree, variant=variant)) + #super(BrezziDouglasMarini, self).__init__(FIAT.BrezziDouglasMarini(cell, degree, variant=variant)) + super(BrezziDouglasMarini, self).__init__(FIAT.BrezziDouglasMarini(cell, degree)) class BrezziDouglasFortinMarini(VectorFiatElement): From e13c017fbe586378806527c58486f081b6eb7acf Mon Sep 17 00:00:00 2001 From: Patrick Farrell Date: Fri, 27 Nov 2020 13:52:42 +0000 Subject: [PATCH 646/749] Always give weights to a QuadratureElement. --- finat/quadrature_element.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/finat/quadrature_element.py b/finat/quadrature_element.py index 33622d60a..d5f2b3468 100644 --- a/finat/quadrature_element.py +++ b/finat/quadrature_element.py @@ -5,6 +5,7 @@ import FIAT import gem +from gem.interpreter import evaluate from gem.utils import cached_property from finat.finiteelementbase import FiniteElementBase @@ -57,6 +58,12 @@ def value_shape(self): def fiat_equivalent(self): ps = self._rule.point_set weights = getattr(self._rule, 'weights', None) + if weights is None: + # we need the weights. + weights, = evaluate([self._rule.weight_expression]) + weights = weights.arr.flatten() + self._rule.weights = weights + return FIAT.QuadratureElement(self.cell, ps.points, weights) def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): From edceab496608216c900485d915674e49b1ffd728 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Wed, 10 Mar 2021 17:56:47 +0000 Subject: [PATCH 647/749] Make symengine an optional dependency --- finat/direct_serendipity.py | 14 +++++++++----- finat/sympy2gem.py | 8 +++++++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/finat/direct_serendipity.py b/finat/direct_serendipity.py index b70d5ed50..eb279baae 100644 --- a/finat/direct_serendipity.py +++ b/finat/direct_serendipity.py @@ -2,8 +2,12 @@ import gem import numpy -import symengine import sympy +try: + import symengine + symbolics = symengine +except ImportError: + symbolics = sympy from FIAT.polynomial_set import mis from FIAT.reference_element import UFCQuadrilateral from gem.utils import cached_property @@ -67,7 +71,7 @@ def value_shape(self): @cached_property def _basis(self): - return ds_sym(self.cell.topology, self.degree, sp=symengine) + return ds_sym(self.cell.topology, self.degree, sp=symbolics) def _basis_deriv(self, xx, alpha): key = (tuple(xx), alpha) @@ -125,7 +129,7 @@ def xysub(x, y): return {x[0]: y[0], x[1]: y[1]} -def ds1_sym(ct, *, vs=None, sp=symengine): +def ds1_sym(ct, *, vs=None, sp=symbolics): """Constructs lowest-order case of Arbogast's directly defined C^0 serendipity elements, which are a special case. :param ct: The cell topology of the reference quadrilateral. @@ -243,7 +247,7 @@ def diff(expr, xx, alpha): return symengine.diff(expr, *(chain(*(repeat(x, a) for x, a in zip(xx, alpha))))) -def dsr_sym(ct, r, *, vs=None, sp=symengine): +def dsr_sym(ct, r, *, vs=None, sp=symbolics): """Constructs higher-order (>= 2) case of Arbogast's directly defined C^0 serendipity elements, which include all polynomials of degree r plus a couple of rational functions. @@ -466,7 +470,7 @@ def nodalize(f): return vs, xx, numpy.asarray(bfs) -def ds_sym(ct, r, *, vs=None, sp=symengine): +def ds_sym(ct, r, *, vs=None, sp=symbolics): """Symbolically Constructs Arbogast's directly defined C^0 serendipity elements, which include all polynomials of degree r plus a couple of rational functions. :param ct: The cell topology of the reference quadrilateral. diff --git a/finat/sympy2gem.py b/finat/sympy2gem.py index 856a48d78..9c148bb91 100644 --- a/finat/sympy2gem.py +++ b/finat/sympy2gem.py @@ -1,7 +1,13 @@ from functools import singledispatch, reduce import sympy -import symengine +try: + import symengine +except ImportError: + class Mock: + def __getattribute__(self, name): + return Mock + symengine = Mock() import gem From 05a9899d119020a76ed573448c1443d1948f8339 Mon Sep 17 00:00:00 2001 From: Justin Crum Date: Tue, 30 Mar 2021 14:20:51 -0700 Subject: [PATCH 648/749] Small boilerplate change. --- finat/fiat_elements.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 7a71109d1..0817d873b 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -364,8 +364,8 @@ def __init__(self, cell, degree): class Nedelec(VectorFiatElement): - def __init__(self, cell, degree, variant=None): - super(Nedelec, self).__init__(FIAT.Nedelec(cell, degree, variant=variant)) + def __init__(self, cell, degree): + super(Nedelec, self).__init__(FIAT.Nedelec(cell, degree)) class NedelecSecondKind(VectorFiatElement): From f9f215af835e796bd074eabac24f6a4893903bc9 Mon Sep 17 00:00:00 2001 From: Reuben Nixon-Hill Date: Thu, 29 Apr 2021 12:56:40 +0100 Subject: [PATCH 649/749] ComponentTensor: allow zero extent shape indices The assertion which is updated did not account for indices having zero extent which can occur (e.g. when dealing with 0D quantities like vertices) See also https://github.com/FInAT/FInAT/issues/78 which this resolves. --- gem/gem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gem/gem.py b/gem/gem.py index 2859ded3d..dab2609d2 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -651,7 +651,7 @@ def __new__(cls, expression, multiindex): # Collect shape shape = tuple(index.extent for index in multiindex) - assert all(shape) + assert all(s >= 0 for s in shape) # Zero folding if isinstance(expression, Zero): From 9391a88ed5e230a3ad5bf81eb8ed381afe7f46cf Mon Sep 17 00:00:00 2001 From: Reuben Nixon-Hill Date: Thu, 29 Apr 2021 12:42:18 +0100 Subject: [PATCH 650/749] Add docstrings to PointSingleton and PointSet Aims to avoid any ambiguity or confusion about the expected shape of the __init__ arguments. Also clarifies the scalar/vector/tensor nature of point sets. --- finat/point_set.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/finat/point_set.py b/finat/point_set.py index ffc754a8e..2a2b7ab1a 100644 --- a/finat/point_set.py +++ b/finat/point_set.py @@ -9,7 +9,12 @@ class AbstractPointSet(metaclass=ABCMeta): """A way of specifying a known set of points, perhaps with some - (tensor) structure.""" + (tensor) structure. + + Points, when stored, have shape point_set_shape + (point_dimension,) + where point_set_shape is () for scalar, (N,) for N element vector, + (N, M) for N x M matrix etc. + """ @abstractproperty def points(self): @@ -34,15 +39,24 @@ def expression(self): class PointSingleton(AbstractPointSet): - """Just a single point.""" + """A point set representing a single point. + + These have a `gem.Literal` expression and no indices.""" def __init__(self, point): + """Build a PointSingleton from a single point. + + :arg point: A single point of shape (D,) where D is the dimension of + the point.""" point = numpy.asarray(point) + # 1 point ought to be a 1D array - see docstring above and points method assert len(point.shape) == 1 self.point = point @property def points(self): + # Make sure we conform to the expected (# of points, point dimension) + # shape return self.point.reshape(1, -1) @property @@ -55,9 +69,14 @@ def expression(self): class PointSet(AbstractPointSet): - """A basic point set with no internal structure.""" + """A basic point set with no internal structure representing a vector of + points.""" def __init__(self, points): + """Build a PointSet from a vector of points + + :arg points: A vector of N points of shape (N, D) where D is the + dimension of each point.""" points = numpy.asarray(points) assert len(points.shape) == 2 self.points = points From c535c78a52dce680fec8921e092a5f08fffdbde1 Mon Sep 17 00:00:00 2001 From: Reuben Hill Date: Thu, 13 Aug 2020 13:41:06 +0100 Subject: [PATCH 651/749] Add optional rule arg to quadrature_element Allows for cases where one does not want make_quadrature to create the QuadratureRule --- finat/quadrature_element.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/finat/quadrature_element.py b/finat/quadrature_element.py index d5f2b3468..c0cc70e83 100644 --- a/finat/quadrature_element.py +++ b/finat/quadrature_element.py @@ -9,15 +9,20 @@ from gem.utils import cached_property from finat.finiteelementbase import FiniteElementBase -from finat.quadrature import make_quadrature +from finat.quadrature import make_quadrature, AbstractQuadratureRule class QuadratureElement(FiniteElementBase): """A set of quadrature points pretending to be a finite element.""" - def __init__(self, cell, degree, scheme="default"): + def __init__(self, cell, degree, scheme="default", rule=None): self.cell = cell - self._rule = make_quadrature(cell, degree, scheme) + if rule is not None: + if not isinstance(rule, AbstractQuadratureRule): + raise TypeError("rule is not an AbstractQuadratureRule") + self._rule = rule + else: + self._rule = make_quadrature(cell, degree, scheme) @cached_property def cell(self): From 7a16b8f2c9278bf3cf374d47871008d2a19f1498 Mon Sep 17 00:00:00 2001 From: Reuben Hill Date: Thu, 20 Aug 2020 13:31:05 +0100 Subject: [PATCH 652/749] Use @cached_property in PointSingleton No reason for them not to be cached. Also switch indices to a class-level property --- finat/point_set.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/finat/point_set.py b/finat/point_set.py index 2a2b7ab1a..2b718b810 100644 --- a/finat/point_set.py +++ b/finat/point_set.py @@ -53,15 +53,13 @@ def __init__(self, point): assert len(point.shape) == 1 self.point = point - @property + @cached_property def points(self): # Make sure we conform to the expected (# of points, point dimension) # shape return self.point.reshape(1, -1) - @property - def indices(self): - return () + indices = () @cached_property def expression(self): From cffee5e21c90fea553fe27f65f009836e39691c2 Mon Sep 17 00:00:00 2001 From: Francis Aznaran Date: Wed, 19 May 2021 12:15:37 +0100 Subject: [PATCH 653/749] Preliminary attempts at patching up conditioning of the AW elements. --- finat/aw.py | 82 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 76 insertions(+), 6 deletions(-) diff --git a/finat/aw.py b/finat/aw.py index 57d53f0b0..088bf78ea 100644 --- a/finat/aw.py +++ b/finat/aw.py @@ -3,7 +3,9 @@ import FIAT -from gem import Literal, ListTensor +#from gem import Literal, ListTensor +import gem +from gem import Literal, ListTensor, Index, Indexed, Product, IndexSum, MathFunction from finat.fiat_elements import FiatElement from finat.physically_mapped import PhysicallyMappedElement, Citations @@ -27,19 +29,39 @@ def _edge_transform(T, coordinate_mapping): J_np = numpy.array([[J[0, 0], J[0, 1]], [J[1, 0], J[1, 1]]]) JTJ = J_np.T @ J_np + + #phys_len = coordinate_mapping.physical_edge_lengths() for e in range(3): # Compute alpha and beta for the edge. Ghat_T = numpy.array([nhat[e, :], that[e, :]]) (alpha, beta) = Ghat_T @ JTJ @ that[e, :] / detJ - # Stuff into the right rows and columns. (idx1, idx2) = (4*e + 1, 4*e + 3) Vsub[idx1, idx1-1] = Literal(-1) * alpha / beta Vsub[idx1, idx1] = Literal(1) / beta Vsub[idx2, idx2-1] = Literal(-1) * alpha / beta Vsub[idx2, idx2] = Literal(1) / beta + + #vec = J_np @ that[e, :] + #Vec = gem.ListTensor(vec) + #k = gem.Index() + #veck = gem.Indexed(Vec, (k, )) + #veck2 = gem.Product(veck, veck) + #vec2 = gem.IndexSum(veck2, (k,)) + #l2 = gem.MathFunction("sqrt", vec2) + #l2 = phys_len[e] + #for t in range(4): + # for r in range(4): + # Vsub[4*e + t] = Vsub[4*e + t]*l2 + #l2 = Literal(l2) + #Vsub[idx1, idx1-1] = l2*Vsub[idx1, idx1-1] + #Vsub[idx1, idx1] = l2*Vsub[idx1,idx1] + #Vsub[idx2, idx2-1] = l2*Vsub[idx2, idx2-1] + #Vsub[idx2, idx2] = l2*Vsub[idx2, idx2] + #Vsub[4*e,4*e] = l2*Vsub[4*e,4*e] + #Vsub[4*e+2,4*e+2] = l2*Vsub[4*e+2,4*e+2] return Vsub @@ -62,8 +84,39 @@ def basis_transformation(self, coordinate_mapping): V[:12, :12] = _edge_transform(T, coordinate_mapping) # internal dofs - for i in range(12, 15): - V[i, i] = Literal(1) + #for i in range(12, 15): + # V[i, i] = Literal(1) + # The `correct' matrix for the internal DOFs (in the sense of consistent with the paper) + J = coordinate_mapping.jacobian_at([1/3, 1/3]) + + W = numpy.zeros((3, 3), dtype=object) + W[0, 0] = J[1, 1]*J[1, 1] + W[0, 1] = -2*J[1, 1]*J[0, 1] + W[0, 2] = J[0, 1]*J[0, 1] + W[1, 0] = -1*J[1, 1]*J[1, 0] + W[1, 1] = J[1, 1]*J[0, 0] + J[0, 1]*J[1, 0] + W[1, 2] = -1*J[0, 1]*J[0, 0] + W[2, 0] = J[1, 0]*J[1, 0] + W[2, 1] = -2*J[1, 0]*J[0, 0] + W[2, 2] = J[0, 0]*J[0, 0] + + detJ = coordinate_mapping.detJ_at([1/3, 1/3]) + V[12:15, 12:15] = W / detJ + + ## RESCALING FOR CONDITIONING + h = coordinate_mapping.cell_size() + for e in range(3): + id1, id2 = [i for i in range(3) if i != e] + eff_h = (h[id1] + h[id2]) / 2 + for i in range(15): + V[i,4*e] = V[i,4*e]*eff_h + V[i,1+4*e] = V[i,1+4*e]*eff_h + V[i,2+4*e] = V[i,2+4*e]*eff_h*eff_h + V[i,3+4*e] = V[i,3+4*e]*eff_h*eff_h + + + avg_h = (h[0] + h[1] + h[2]) / 3 + V[12:15, 12:15] = V[12:15, 12:15] * (avg_h*avg_h) return ListTensor(V.T) @@ -120,8 +173,25 @@ def basis_transformation(self, coordinate_mapping): V[9:21, 9:21] = _edge_transform(self.cell, coordinate_mapping) - for i in range(21, 24): - V[i, i] = Literal(1) + #for i in range(21, 24): + # V[i, i] = Literal(1) + # The `correct' matrix for the internal DOFs (in the sense of consistent with the paper) + detJ = coordinate_mapping.detJ_at([1/3, 1/3]) + V[21:24, 21:24] = W / detJ + + ## RESCALING FOR CONDITIONING + h = coordinate_mapping.cell_size() + for e in range(3): + id1, id2 = [i for i in range(3) if i != e] + eff_h = (h[id1] + h[id2]) / 2 + for i in range(24): + V[i,9+4*e] = V[i,9+4*e]*eff_h + V[i,10+4*e] = V[i,10+4*e]*eff_h + V[i,11+4*e] = V[i,11+4*e]*eff_h*eff_h + V[i,12+4*e] = V[i,12+4*e]*eff_h*eff_h + + avg_h = (h[0] + h[1] + h[2]) / 3 + V[21:24, 21:24] = V[21:24, 21:24] * (avg_h*avg_h) return ListTensor(V.T) From 2da30d6bee18297c558d9686a9e1eee6af038510 Mon Sep 17 00:00:00 2001 From: Reuben Hill Date: Thu, 20 Aug 2020 16:46:47 +0100 Subject: [PATCH 654/749] New UnknownPointSet for runtime known points Allows a vector of points which are unknown at compile time to be expressed at runtime. The expression given to the UnknownPointSet will be runtime tabulated if it has a name that begins with 'rt_'. Points are a non-indexable/non-iterable UnknownPointsArray object that mocks the usual points attribute of a point set. Also tidy some other comments. --- finat/point_set.py | 60 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/finat/point_set.py b/finat/point_set.py index 2b718b810..8682c1534 100644 --- a/finat/point_set.py +++ b/finat/point_set.py @@ -18,8 +18,8 @@ class AbstractPointSet(metaclass=ABCMeta): @abstractproperty def points(self): - """A flattened numpy array of points with shape - (# of points, point dimension).""" + """A flattened numpy array of points or ``UnknownPointsArray`` + object with shape (# of points, point dimension).""" @property def dimension(self): @@ -41,7 +41,7 @@ def expression(self): class PointSingleton(AbstractPointSet): """A point set representing a single point. - These have a `gem.Literal` expression and no indices.""" + These have a ``gem.Literal`` expression and no indices.""" def __init__(self, point): """Build a PointSingleton from a single point. @@ -66,6 +66,60 @@ def expression(self): return gem.Literal(self.point) +class UnknownPointsArray(): + """A placeholder for a set of unknown points with appropriate length + and size but without indexable values. For use with + :class:`AbstractPointSet`s whose points are not known at compile + time.""" + def __init__(self, shape): + """ + :arg shape: The shape of the unknown set of N points of shape + (N, D) where D is the dimension of each point. + """ + assert len(shape) == 2 + self.shape = shape + + def __len__(self): + return self.shape[0] + + +class UnknownPointSet(AbstractPointSet): + """A point set representing a vector of points with unknown + locations but known ``gem.Variable`` expression. + + The ``.points`` property is an `UnknownPointsArray` object with + shape (N, D) where N is the number of points and D is their + dimension. + + The ``.expression`` property is a derived `gem.partial_indexed` with + shape (D,) and free indices for the points N.""" + + def __init__(self, points_expr): + """Build a PointSingleton from a gem expression for a single point. + + :arg points_expr: A ``gem.Variable`` expression representing a + vector of N points in D dimensions. Should have shape (N, D) + and no free indices. For runtime tabulation the variable + name should begin with \'rt_:\'.""" + assert isinstance(points_expr, gem.Variable) + assert points_expr.free_indices == () + assert len(points_expr.shape) == 2 + self._points_expr = points_expr + + @cached_property + def points(self): + return UnknownPointsArray(self._points_expr.shape) + + @cached_property + def indices(self): + N, _ = self._points_expr.shape + return (gem.Index(extent=N),) + + @cached_property + def expression(self): + return gem.partial_indexed(self._points_expr, self.indices) + + class PointSet(AbstractPointSet): """A basic point set with no internal structure representing a vector of points.""" From a8b0e4dceecb879bb0d93ff1f5d249b2b41b4b40 Mon Sep 17 00:00:00 2001 From: Reuben Nixon-Hill Date: Tue, 11 May 2021 16:09:51 +0100 Subject: [PATCH 655/749] Create QuadratureElement from ref cell and rule This changes the FInAT QuadratureElement API to have it be created from a FIAT reference cell and FInAT quadrature rule (AbstractQuadratureRule). This allows some extra checks to be added when making a QuadratureElement and generally makes the API a bit more robust. To create an element the old way introduce a new make_quadrature_rule function which takes a FIAT reference cell, quadrature degree and optional scheme. Also corrects a small documentation error. --- finat/__init__.py | 2 +- finat/quadrature.py | 2 +- finat/quadrature_element.py | 37 +++++++++++++++++++++++++++++-------- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/finat/__init__.py b/finat/__init__.py index f948953ce..9aa8b0fa7 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -25,7 +25,7 @@ from .hdivcurl import HCurlElement, HDivElement # noqa: F401 from .mixed import MixedElement # noqa: F401 from .nodal_enriched import NodalEnrichedElement # noqa: 401 -from .quadrature_element import QuadratureElement # noqa: F401 +from .quadrature_element import QuadratureElement, make_quadrature_element # noqa: F401 from .restricted import RestrictedElement # noqa: F401 from .runtime_tabulated import RuntimeTabulated # noqa: F401 from . import quadrature # noqa: F401 diff --git a/finat/quadrature.py b/finat/quadrature.py index 85b95758d..a6bbe313f 100644 --- a/finat/quadrature.py +++ b/finat/quadrature.py @@ -23,7 +23,7 @@ def make_quadrature(ref_el, degree, scheme="default"): Gauss scheme on simplices. On tensor-product cells, it is a tensor-product quadrature rule of the subcells. - :arg cell: The FIAT cell to create the quadrature for. + :arg ref_el: The FIAT cell to create the quadrature for. :arg degree: The degree of polynomial that the rule should integrate exactly. """ diff --git a/finat/quadrature_element.py b/finat/quadrature_element.py index c0cc70e83..cc1bc0c3c 100644 --- a/finat/quadrature_element.py +++ b/finat/quadrature_element.py @@ -12,17 +12,38 @@ from finat.quadrature import make_quadrature, AbstractQuadratureRule +def make_quadrature_element(fiat_ref_cell, degree, scheme="default"): + """Construct a :class:`QuadratureElement` from a given a reference + element, degree and scheme. + + :param fiat_ref_cell: The FIAT reference cell to build the + :class:`QuadratureElement` on. + :param degree: The degree of polynomial that the rule should + integrate exactly. + :param scheme: The quadrature scheme to use - e.g. "default", + "canonical" or "KMV". + :returns: The appropriate :class:`QuadratureElement` + """ + rule = make_quadrature(fiat_ref_cell, degree, scheme) + return QuadratureElement(fiat_ref_cell, rule) + + class QuadratureElement(FiniteElementBase): """A set of quadrature points pretending to be a finite element.""" - def __init__(self, cell, degree, scheme="default", rule=None): - self.cell = cell - if rule is not None: - if not isinstance(rule, AbstractQuadratureRule): - raise TypeError("rule is not an AbstractQuadratureRule") - self._rule = rule - else: - self._rule = make_quadrature(cell, degree, scheme) + def __init__(self, fiat_ref_cell, rule): + """Construct a :class:`QuadratureElement`. + + :param fiat_ref_cell: The FIAT reference cell to build the + :class:`QuadratureElement` on + :param rule: A :class:`AbstractQuadratureRule` to use + """ + self.cell = fiat_ref_cell + if not isinstance(rule, AbstractQuadratureRule): + raise TypeError("rule is not an AbstractQuadratureRule") + if fiat_ref_cell.get_dimension() != rule.point_set.dimension: + raise ValueError("Cell dimension does not match rule's point set dimension") + self._rule = rule @cached_property def cell(self): From 81afb0224d0117a4239e539a85d79dfce238c8f4 Mon Sep 17 00:00:00 2001 From: Reuben Nixon-Hill Date: Mon, 17 May 2021 14:30:35 +0100 Subject: [PATCH 656/749] QuadratureElement FIAT equivalent with unknown points Pass array of NaNs as points when creating FIAT equivalent to QuadratureElement. This really should raise a ValueError since there ought not to be a FIAT equivalent where the points are unknown. For compatibility until FInAT dual evaluation is done we pass an array of NaNs of correct shape as the points. --- finat/quadrature_element.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/finat/quadrature_element.py b/finat/quadrature_element.py index cc1bc0c3c..42c97f92c 100644 --- a/finat/quadrature_element.py +++ b/finat/quadrature_element.py @@ -1,3 +1,4 @@ +from finat.point_set import PointSet, UnknownPointSet from functools import reduce import numpy @@ -83,6 +84,12 @@ def value_shape(self): @cached_property def fiat_equivalent(self): ps = self._rule.point_set + if isinstance(ps, UnknownPointSet): + # This really should raise a ValueError since there ought not to be + # a FIAT equivalent where the points are unknown. + # For compatibility until FInAT dual evaluation is done we pass an + # array of NaNs of correct shape as the points. + ps = PointSet(numpy.full(ps.points.shape, numpy.nan)) weights = getattr(self._rule, 'weights', None) if weights is None: # we need the weights. From 5143d65c1ca84ac705b229dd64398a0ec37b9561 Mon Sep 17 00:00:00 2001 From: Francis Aznaran Date: Wed, 19 May 2021 16:32:19 +0100 Subject: [PATCH 657/749] Condition number of AW mass matrices appears to be O(1) under refinement with these scalings. --- finat/aw.py | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/finat/aw.py b/finat/aw.py index 088bf78ea..a18e0127d 100644 --- a/finat/aw.py +++ b/finat/aw.py @@ -109,14 +109,17 @@ def basis_transformation(self, coordinate_mapping): id1, id2 = [i for i in range(3) if i != e] eff_h = (h[id1] + h[id2]) / 2 for i in range(15): - V[i,4*e] = V[i,4*e]*eff_h - V[i,1+4*e] = V[i,1+4*e]*eff_h - V[i,2+4*e] = V[i,2+4*e]*eff_h*eff_h - V[i,3+4*e] = V[i,3+4*e]*eff_h*eff_h - + V[i,4*e] = V[i,4*e]#*eff_h*eff_h + V[i,1+4*e] = V[i,1+4*e]#*eff_h*eff_h + V[i,2+4*e] = V[i,2+4*e]#*eff_h*eff_h*eff_h + V[i,3+4*e] = V[i,3+4*e]#*eff_h*eff_h*eff_h avg_h = (h[0] + h[1] + h[2]) / 3 - V[12:15, 12:15] = V[12:15, 12:15] * (avg_h*avg_h) + #V[12:15, 12:15] = V[12:15, 12:15] * (avg_h*avg_h) + for e in range(3): + for i in range(15): + V[i,12+e] = V[i,12+e]#*(avg_h*avg_h) + # there's some redundancy here return ListTensor(V.T) @@ -181,17 +184,28 @@ def basis_transformation(self, coordinate_mapping): ## RESCALING FOR CONDITIONING h = coordinate_mapping.cell_size() + + for e in range(3): + eff_h = h[e] + for i in range(24): + V[i,3*e] = V[i,3*e]/(eff_h*eff_h) + V[i,1+3*e] = V[i,1+3*e]/(eff_h*eff_h) + V[i,2+3*e] = V[i,2+3*e]/(eff_h*eff_h) + for e in range(3): id1, id2 = [i for i in range(3) if i != e] eff_h = (h[id1] + h[id2]) / 2 for i in range(24): - V[i,9+4*e] = V[i,9+4*e]*eff_h - V[i,10+4*e] = V[i,10+4*e]*eff_h - V[i,11+4*e] = V[i,11+4*e]*eff_h*eff_h - V[i,12+4*e] = V[i,12+4*e]*eff_h*eff_h + V[i,9+4*e] = V[i,9+4*e]#*eff_h + V[i,10+4*e] = V[i,10+4*e]#*eff_h + V[i,11+4*e] = V[i,11+4*e]#*eff_h*eff_h + V[i,12+4*e] = V[i,12+4*e]#*eff_h*eff_h avg_h = (h[0] + h[1] + h[2]) / 3 - V[21:24, 21:24] = V[21:24, 21:24] * (avg_h*avg_h) + #V[21:24, 21:24] = V[21:24, 21:24]# * (avg_h*avg_h) + for e in range(3): + for i in range(24): + V[i,21+e] = V[i,21+e]#*(avg_h*avg_h) return ListTensor(V.T) From 842b8705b12ef39a2f33d4e8aa59417e3e931a6b Mon Sep 17 00:00:00 2001 From: Francis Aznaran Date: Thu, 20 May 2021 16:58:03 +0100 Subject: [PATCH 658/749] Get rid of some repeat code. --- finat/aw.py | 109 +++++++++++++--------------------------------------- 1 file changed, 27 insertions(+), 82 deletions(-) diff --git a/finat/aw.py b/finat/aw.py index a18e0127d..a604d7ba5 100644 --- a/finat/aw.py +++ b/finat/aw.py @@ -30,8 +30,6 @@ def _edge_transform(T, coordinate_mapping): [J[1, 0], J[1, 1]]]) JTJ = J_np.T @ J_np - #phys_len = coordinate_mapping.physical_edge_lengths() - for e in range(3): # Compute alpha and beta for the edge. Ghat_T = numpy.array([nhat[e, :], that[e, :]]) @@ -44,28 +42,26 @@ def _edge_transform(T, coordinate_mapping): Vsub[idx2, idx2-1] = Literal(-1) * alpha / beta Vsub[idx2, idx2] = Literal(1) / beta - #vec = J_np @ that[e, :] - #Vec = gem.ListTensor(vec) - #k = gem.Index() - #veck = gem.Indexed(Vec, (k, )) - #veck2 = gem.Product(veck, veck) - #vec2 = gem.IndexSum(veck2, (k,)) - #l2 = gem.MathFunction("sqrt", vec2) - #l2 = phys_len[e] - #for t in range(4): - # for r in range(4): - # Vsub[4*e + t] = Vsub[4*e + t]*l2 - #l2 = Literal(l2) - #Vsub[idx1, idx1-1] = l2*Vsub[idx1, idx1-1] - #Vsub[idx1, idx1] = l2*Vsub[idx1,idx1] - #Vsub[idx2, idx2-1] = l2*Vsub[idx2, idx2-1] - #Vsub[idx2, idx2] = l2*Vsub[idx2, idx2] - #Vsub[4*e,4*e] = l2*Vsub[4*e,4*e] - #Vsub[4*e+2,4*e+2] = l2*Vsub[4*e+2,4*e+2] - return Vsub +def _evaluation_transform(coordinate_mapping): + J = coordinate_mapping.jacobian_at([1/3, 1/3]) + + W = numpy.zeros((3, 3), dtype=object) + W[0, 0] = J[1, 1]*J[1, 1] + W[0, 1] = -2*J[1, 1]*J[0, 1] + W[0, 2] = J[0, 1]*J[0, 1] + W[1, 0] = -1*J[1, 1]*J[1, 0] + W[1, 1] = J[1, 1]*J[0, 0] + J[0, 1]*J[1, 0] + W[1, 2] = -1*J[0, 1]*J[0, 0] + W[2, 0] = J[1, 0]*J[1, 0] + W[2, 1] = -2*J[1, 0]*J[0, 0] + W[2, 2] = J[0, 0]*J[0, 0] + + return W + + class ArnoldWintherNC(PhysicallyMappedElement, FiatElement): def __init__(self, cell, degree): if Citations is not None: @@ -84,42 +80,14 @@ def basis_transformation(self, coordinate_mapping): V[:12, :12] = _edge_transform(T, coordinate_mapping) # internal dofs - #for i in range(12, 15): - # V[i, i] = Literal(1) - # The `correct' matrix for the internal DOFs (in the sense of consistent with the paper) - J = coordinate_mapping.jacobian_at([1/3, 1/3]) - - W = numpy.zeros((3, 3), dtype=object) - W[0, 0] = J[1, 1]*J[1, 1] - W[0, 1] = -2*J[1, 1]*J[0, 1] - W[0, 2] = J[0, 1]*J[0, 1] - W[1, 0] = -1*J[1, 1]*J[1, 0] - W[1, 1] = J[1, 1]*J[0, 0] + J[0, 1]*J[1, 0] - W[1, 2] = -1*J[0, 1]*J[0, 0] - W[2, 0] = J[1, 0]*J[1, 0] - W[2, 1] = -2*J[1, 0]*J[0, 0] - W[2, 2] = J[0, 0]*J[0, 0] - + W = _evaluation_transform(coordinate_mapping) detJ = coordinate_mapping.detJ_at([1/3, 1/3]) + V[12:15, 12:15] = W / detJ - ## RESCALING FOR CONDITIONING - h = coordinate_mapping.cell_size() - for e in range(3): - id1, id2 = [i for i in range(3) if i != e] - eff_h = (h[id1] + h[id2]) / 2 - for i in range(15): - V[i,4*e] = V[i,4*e]#*eff_h*eff_h - V[i,1+4*e] = V[i,1+4*e]#*eff_h*eff_h - V[i,2+4*e] = V[i,2+4*e]#*eff_h*eff_h*eff_h - V[i,3+4*e] = V[i,3+4*e]#*eff_h*eff_h*eff_h - - avg_h = (h[0] + h[1] + h[2]) / 3 - #V[12:15, 12:15] = V[12:15, 12:15] * (avg_h*avg_h) - for e in range(3): - for i in range(15): - V[i,12+e] = V[i,12+e]#*(avg_h*avg_h) - # there's some redundancy here + # Note: that the edge DOFs are scaled by edge lengths in FIAT implies + # that they are already have the necessary rescaling to improve + # conditioning. return ListTensor(V.T) @@ -159,26 +127,14 @@ def basis_transformation(self, coordinate_mapping): V[multiindex] = Literal(V[multiindex]) J = coordinate_mapping.jacobian_at([1/3, 1/3]) - - W = numpy.zeros((3, 3), dtype=object) - W[0, 0] = J[1, 1]*J[1, 1] - W[0, 1] = -2*J[1, 1]*J[0, 1] - W[0, 2] = J[0, 1]*J[0, 1] - W[1, 0] = -1*J[1, 1]*J[1, 0] - W[1, 1] = J[1, 1]*J[0, 0] + J[0, 1]*J[1, 0] - W[1, 2] = -1*J[0, 1]*J[0, 0] - W[2, 0] = J[1, 0]*J[1, 0] - W[2, 1] = -2*J[1, 0]*J[0, 0] - W[2, 2] = J[0, 0]*J[0, 0] + W = _evaluation_transform(coordinate_mapping) # Put into the right rows and columns. V[0:3, 0:3] = V[3:6, 3:6] = V[6:9, 6:9] = W V[9:21, 9:21] = _edge_transform(self.cell, coordinate_mapping) - #for i in range(21, 24): - # V[i, i] = Literal(1) - # The `correct' matrix for the internal DOFs (in the sense of consistent with the paper) + # internal DOFs detJ = coordinate_mapping.detJ_at([1/3, 1/3]) V[21:24, 21:24] = W / detJ @@ -192,20 +148,9 @@ def basis_transformation(self, coordinate_mapping): V[i,1+3*e] = V[i,1+3*e]/(eff_h*eff_h) V[i,2+3*e] = V[i,2+3*e]/(eff_h*eff_h) - for e in range(3): - id1, id2 = [i for i in range(3) if i != e] - eff_h = (h[id1] + h[id2]) / 2 - for i in range(24): - V[i,9+4*e] = V[i,9+4*e]#*eff_h - V[i,10+4*e] = V[i,10+4*e]#*eff_h - V[i,11+4*e] = V[i,11+4*e]#*eff_h*eff_h - V[i,12+4*e] = V[i,12+4*e]#*eff_h*eff_h - - avg_h = (h[0] + h[1] + h[2]) / 3 - #V[21:24, 21:24] = V[21:24, 21:24]# * (avg_h*avg_h) - for e in range(3): - for i in range(24): - V[i,21+e] = V[i,21+e]#*(avg_h*avg_h) + # Note: that the edge DOFs are scaled by edge lengths in FIAT implies + # that they are already have the necessary rescaling to improve + # conditioning. return ListTensor(V.T) From 5bafc7c8b397bdff02b3284bcba4a884bd51e146 Mon Sep 17 00:00:00 2001 From: Francis Aznaran Date: Tue, 8 Jun 2021 11:47:20 +0100 Subject: [PATCH 659/749] Deal with flake8 issues. --- finat/aw.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/finat/aw.py b/finat/aw.py index a604d7ba5..c2049250b 100644 --- a/finat/aw.py +++ b/finat/aw.py @@ -1,12 +1,7 @@ """Implementation of the Arnold-Winther finite elements.""" import numpy - import FIAT - -#from gem import Literal, ListTensor -import gem -from gem import Literal, ListTensor, Index, Indexed, Product, IndexSum, MathFunction - +from gem import Literal, ListTensor from finat.fiat_elements import FiatElement from finat.physically_mapped import PhysicallyMappedElement, Citations @@ -29,7 +24,7 @@ def _edge_transform(T, coordinate_mapping): J_np = numpy.array([[J[0, 0], J[0, 1]], [J[1, 0], J[1, 1]]]) JTJ = J_np.T @ J_np - + for e in range(3): # Compute alpha and beta for the edge. Ghat_T = numpy.array([nhat[e, :], that[e, :]]) @@ -41,7 +36,7 @@ def _edge_transform(T, coordinate_mapping): Vsub[idx1, idx1] = Literal(1) / beta Vsub[idx2, idx2-1] = Literal(-1) * alpha / beta Vsub[idx2, idx2] = Literal(1) / beta - + return Vsub @@ -126,7 +121,6 @@ def basis_transformation(self, coordinate_mapping): for multiindex in numpy.ndindex(V.shape): V[multiindex] = Literal(V[multiindex]) - J = coordinate_mapping.jacobian_at([1/3, 1/3]) W = _evaluation_transform(coordinate_mapping) # Put into the right rows and columns. @@ -138,15 +132,15 @@ def basis_transformation(self, coordinate_mapping): detJ = coordinate_mapping.detJ_at([1/3, 1/3]) V[21:24, 21:24] = W / detJ - ## RESCALING FOR CONDITIONING + # RESCALING FOR CONDITIONING h = coordinate_mapping.cell_size() for e in range(3): eff_h = h[e] for i in range(24): - V[i,3*e] = V[i,3*e]/(eff_h*eff_h) - V[i,1+3*e] = V[i,1+3*e]/(eff_h*eff_h) - V[i,2+3*e] = V[i,2+3*e]/(eff_h*eff_h) + V[i, 3*e] = V[i, 3*e]/(eff_h*eff_h) + V[i, 1+3*e] = V[i, 1+3*e]/(eff_h*eff_h) + V[i, 2+3*e] = V[i, 2+3*e]/(eff_h*eff_h) # Note: that the edge DOFs are scaled by edge lengths in FIAT implies # that they are already have the necessary rescaling to improve From 71e6b65105691e32a433ee7a4d40b4c53c2fcf79 Mon Sep 17 00:00:00 2001 From: Andrew Whitmell Date: Wed, 19 May 2021 11:01:07 +0100 Subject: [PATCH 660/749] Add flop counting visitor Count the approximate number of flops required for a GEM expression by visiting the scheduled Impero loop tree. Record these flops in the kernels returned by TSFC so that we can inspect them later. --- gem/flop_count.py | 194 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 gem/flop_count.py diff --git a/gem/flop_count.py b/gem/flop_count.py new file mode 100644 index 000000000..3029a899b --- /dev/null +++ b/gem/flop_count.py @@ -0,0 +1,194 @@ +""" +This file contains all the necessary functions to accurately count the +total number of floating point operations for a given script. +""" + +import gem.gem as gem +import gem.impero as imp +from functools import singledispatch +import numpy +import math + + +@singledispatch +def statement(tree, parameters): + raise NotImplementedError + + +@statement.register(imp.Block) +def statement_block(tree, parameters): + flops = sum(statement(child, parameters) for child in tree.children) + return flops + + +@statement.register(imp.For) +def statement_for(tree, parameters): + extent = tree.index.extent + assert extent is not None + child, = tree.children + flops = statement(child, parameters) + return flops * extent + + +@statement.register(imp.Initialise) +def statement_initialise(tree, parameters): + return 0 + + +@statement.register(imp.Accumulate) +def statement_accumulate(tree, parameters): + flops = expression_flops(tree.indexsum.children[0], parameters) + return flops + 1 + + +@statement.register(imp.Return) +def statement_return(tree, parameters): + flops = expression_flops(tree.expression, parameters) + return flops + 1 + + +@statement.register(imp.ReturnAccumulate) +def statement_returnaccumulate(tree, parameters): + flops = expression_flops(tree.indexsum.children[0], parameters) + return flops + 1 + + +@statement.register(imp.Evaluate) +def statement_evaluate(tree, parameters): + flops = expression_flops(tree.expression, parameters, top=True) + return flops + + +@singledispatch +def flops(expr, parameters): + raise NotImplementedError(f"Don't know how to count flops of {type(expr)}") + + +@flops.register(gem.Failure) +def flops_failure(expr, parameters): + raise ValueError("Not expecting a Failure node") + + +@flops.register(gem.Variable) +@flops.register(gem.Identity) +@flops.register(gem.Delta) +@flops.register(gem.Zero) +@flops.register(gem.Literal) +@flops.register(gem.Index) +@flops.register(gem.VariableIndex) +def flops_zero(expr, parameters): + # Initial set up of these Gem nodes are of 0 floating point operations. + return 0 + + +@flops.register(gem.LogicalNot) +@flops.register(gem.LogicalAnd) +@flops.register(gem.LogicalOr) +@flops.register(gem.ListTensor) +def flops_zeroplus(expr, parameters): + # These nodes contribute 0 floating point operations, but their children may not. + return 0 + sum(expression_flops(child, parameters) + for child in expr.children) + + +@flops.register(gem.Product) +def flops_product(expr, parameters): + # Multiplication by -1 is not a flop. + a, b = expr.children + if isinstance(a, gem.Literal) and a.value == -1: + return expression_flops(b, parameters) + elif isinstance(b, gem.Literal) and b.value == -1: + return expression_flops(a, parameters) + else: + return 1 + sum(expression_flops(child, parameters) + for child in expr.children) + + +@flops.register(gem.Sum) +@flops.register(gem.Division) +@flops.register(gem.Comparison) +@flops.register(gem.MathFunction) +@flops.register(gem.MinValue) +@flops.register(gem.MaxValue) +def flops_oneplus(expr, parameters): + return 1 + sum(expression_flops(child, parameters) + for child in expr.children) + + +@flops.register(gem.Power) +def flops_power(expr, parameters): + base, exponent = expr.children + base_flops = expression_flops(base, parameters) + if isinstance(exponent, gem.Literal): + exponent = exponent.value + if exponent > 0 and exponent == math.floor(exponent): + return base_flops + int(math.ceil(math.log2(exponent))) + else: + return base_flops + 5 # heuristic + else: + return base_flops + 5 # heuristic + + +@flops.register(gem.Conditional) +def flops_conditional(expr, parameters): + condition, then, else_ = (expression_flops(child, parameters) + for child in expr.children) + return condition + max(then, else_) + + +@flops.register(gem.Indexed) +@flops.register(gem.FlexiblyIndexed) +def flops_indexed(expr, parameters): + aggregate = sum(expression_flops(child, parameters) + for child in expr.children) + # Average flops per entry + return aggregate / numpy.product(expr.children[0].shape, dtype=int) + + +@flops.register(gem.IndexSum) +def flops_indexsum(expr, parameters): + raise ValueError("Not expecting IndexSum") + + +@flops.register(gem.Inverse) +def flops_inverse(expr, parameters): + n, _ = expr.shape + # 2n^3 + child flop count + return 2*n**3 + sum(expression_flops(child, parameters) + for child in expr.children) + + +@flops.register(gem.Solve) +def flops_solve(expr, parameters): + n, m = expr.shape + # 2mn + inversion cost of A + children flop count + return 2*n*m + 2*n**3 + sum(expression_flops(child, parameters) + for child in expr.children) + + +@flops.register(gem.ComponentTensor) +def flops_componenttensor(expr, parameters): + raise ValueError("Not expecting ComponentTensor") + + +def expression_flops(expression, parameters, top=False): + """An approximation to flops required for each expression. + + :arg expression: GEM expression. + :arg parameters: Useful miscellaneous information. + :arg top: are we at the root? + :returns: flop count for the expression + """ + if not top and expression in parameters.temporaries: + return 0 + else: + return flops(expression, parameters) + + +def count_flops(impero_c): + """An approximation to flops required for a scheduled impero_c tree. + + :arg impero_c: a :class:`~.Impero_C` object. + :returns: approximate flop count for the tree. + """ + return statement(impero_c.tree, impero_c) From 2243be5547530a19545a0495c6b4a085b5ae8ad9 Mon Sep 17 00:00:00 2001 From: Andrew Whitmell Date: Thu, 8 Jul 2021 11:33:17 +0100 Subject: [PATCH 661/749] Created try/except block for impero tree --- gem/flop_count.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gem/flop_count.py b/gem/flop_count.py index 3029a899b..57577e34b 100644 --- a/gem/flop_count.py +++ b/gem/flop_count.py @@ -191,4 +191,7 @@ def count_flops(impero_c): :arg impero_c: a :class:`~.Impero_C` object. :returns: approximate flop count for the tree. """ - return statement(impero_c.tree, impero_c) + try: + return statement(impero_c.tree, impero_c) + except (ValueError, NotImplementedError): + return 0 From bba4768fa4dbc6f628a1253cd24a79867fe1df5b Mon Sep 17 00:00:00 2001 From: Reuben Nixon-Hill Date: Tue, 13 Jul 2021 14:29:54 +0100 Subject: [PATCH 662/749] Compare to spatial dim when making QuadratureElement This fixes #90: the wrong FIAT reference element dimension was being checked against in the case of a tensor product cells --- finat/quadrature_element.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/quadrature_element.py b/finat/quadrature_element.py index 42c97f92c..43d806dfa 100644 --- a/finat/quadrature_element.py +++ b/finat/quadrature_element.py @@ -42,7 +42,7 @@ def __init__(self, fiat_ref_cell, rule): self.cell = fiat_ref_cell if not isinstance(rule, AbstractQuadratureRule): raise TypeError("rule is not an AbstractQuadratureRule") - if fiat_ref_cell.get_dimension() != rule.point_set.dimension: + if fiat_ref_cell.get_spatial_dimension() != rule.point_set.dimension: raise ValueError("Cell dimension does not match rule's point set dimension") self._rule = rule From 5976ca785fd74113b4a074e32e38952e6569c245 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Wed, 18 Aug 2021 13:53:20 +0100 Subject: [PATCH 663/749] Ensure literals in point evaluation are floats --- finat/fiat_elements.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 6939cf998..65ee40d66 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -275,7 +275,8 @@ def is_const(expr): assert table.shape[-1] == m zerocols = np.isclose(abs(table).max(axis=tuple(range(table.ndim - 1))), 0.0) if all(np.logical_or(const_mask, zerocols)): - vals = base_values_sympy[const_mask] + # Casting is safe by assertion of is_const + vals = base_values_sympy[const_mask].astype(np.float64) result[alpha] = gem.Literal(table[..., const_mask].dot(vals)) else: beta = tuple(gem.Index() for s in table.shape[:-1]) From b1ecca301f167b0ebb332cda44c305f75df936b3 Mon Sep 17 00:00:00 2001 From: Matthew Kan Date: Fri, 5 Jun 2020 12:40:51 +0100 Subject: [PATCH 664/749] c_e_d_e: Move dual evaluation to FInAT A new "UFLtoGEMCallback" representing the function to dual evaluate is added. This takes the processed UFL expression and, when called, performs the necessary translation to gem. Also add some helpful documentation to partial_indexed. --- gem/gem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index dab2609d2..570145459 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -866,9 +866,9 @@ def index_sum(expression, indices): def partial_indexed(tensor, indices): - """Generalised indexing into a tensor. The number of indices may - be less than or equal to the rank of the tensor, so the result may - have a non-empty shape. + """Generalised indexing into a tensor by eating shape off the front. + The number of indices may be less than or equal to the rank of the tensor, + so the result may have a non-empty shape. :arg tensor: tensor-valued GEM expression :arg indices: indices, at most as many as the rank of the tensor From 98084c33e2d88c71bbff12b88e1a897552762dbe Mon Sep 17 00:00:00 2001 From: Reuben Nixon-Hill Date: Wed, 27 May 2020 11:55:05 +0100 Subject: [PATCH 665/749] Add dual_basis and dual_evaluation to elements Co-authored-by: Matthew Kan A new dual_basis property is added to all elements which represents the dual basis as a (generally sparse) gem weight tensor Q and corresponding point set x. The general dual evaluation is then Q * fn(x) (the contraction of Q with fn(x) along the the indices of x and any shape introduced by fn). The the dual_evaluation method returns a gem expression for the dual evaluation contraction given some function fn. For more, see the relevant docstrings --- finat/cube.py | 7 +++ finat/fiat_elements.py | 116 +++++++++++++++++++++++++++++++++++ finat/finiteelementbase.py | 87 ++++++++++++++++++++++++++ finat/quadrature_element.py | 21 +++++-- finat/tensor_product.py | 78 +++++++++++++++++++++++ finat/tensorfiniteelement.py | 64 ++++++++++++++++++- 6 files changed, 364 insertions(+), 9 deletions(-) diff --git a/finat/cube.py b/finat/cube.py index 8813fd5c7..377084be6 100644 --- a/finat/cube.py +++ b/finat/cube.py @@ -75,6 +75,13 @@ def point_evaluation(self, order, point, entity=None): return self.product.point_evaluation(order, point, self._unflatten[entity]) + @property + def dual_basis(self): + return self.product.dual_basis + + def dual_evaluation(self, fn): + return self.product.dual_evaluation(fn) + @property def index_shape(self): return self.product.index_shape diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 65ee40d66..37dccd02c 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -5,9 +5,12 @@ import FIAT from FIAT.polynomial_set import mis, form_matrix_product +import sparse + import gem from finat.finiteelementbase import FiniteElementBase +from finat.point_set import PointSet from finat.sympy2gem import sympy2gem try: @@ -173,6 +176,119 @@ def point_evaluation(self, order, refcoords, entity=None): # Dispatch on FIAT element class return point_evaluation(self._element, order, refcoords, (entity_dim, entity_i)) + @property + def dual_basis(self): + # A tensor of weights (of total rank R) to contract with a unique + # vector of points to evaluate at, giving a tensor (of total rank R-1) + # where the first indices (rows) correspond to a basis functional + # (node). + # DOK Sparse matrix in (row, col, higher,..)=>value pairs - to become a + # gem.SparseLiteral. + # Rows are number of nodes/dual functionals. + # Columns are unique points to evaluate. + # Higher indices are tensor indices of the weights when weights are + # tensor valued. + # The columns (unique points to evaluate) are indexed out ready for + # contraction with the point set free index. + Q = {} + + # Dict of unique points to evaluate stored as + # {tuple(pt_hash.flatten()): (x_k, k)} pairs. Points in index k + # order form a vector required for correct contraction with Q. Will + # become a FInAT.PointSet later. + # x_key = numpy.round(x_k, x_key_decimals) such that + # numpy.allclose(x_k, pt_hash, atol=1*10**-dec) == true + x = {} + x_key_decimals = 12 + x_key_atol = 1e-12 # = 1*10**-dec + + # + # BUILD Q TENSOR + # + + # FIXME: The below loop is REALLY SLOW for BDM - Q and x should just be output as the dual basis + + last_shape = None + self.Q_is_identity = True # TODO do this better + + fiat_dual_basis = self._element.dual_basis() + # i are rows of Q + for i, dual in enumerate(fiat_dual_basis): + point_dict = dual.get_point_dict() + for j, (x_j, tuples) in enumerate(point_dict.items()): + x_j = PointSet((x_j,)) # TODO doesn't need to be PointSet + # get weights q_j for point x_j from complicated FIAT dual data + # structure + weight_dict = {cmp: weight for weight, cmp in tuples} + if len(self._element.value_shape()) == 0: + q_j = np.array(weight_dict[tuple()]) + else: + q_j = np.zeros(self._element.value_shape()) + for key, item in weight_dict.items(): + q_j[key] = item + + # Ensure all weights have the same shape + if last_shape is not None: + assert q_j.shape == last_shape + last_shape = q_j.shape + + # Create key into x dict + x_key = np.round(x_j.points, x_key_decimals) + assert np.allclose(x_j.points, x_key, atol=x_key_atol) + x_key = tuple(x_key.flatten()) + + # Get value and index k or add to dict. k are the columns of Q. + try: + x_j, k = x[x_key] + except KeyError: + k = len(x) + x[x_key] = x_j, k + + # q_j may be tensor valued + it = np.nditer(q_j, flags=['multi_index']) + for q_j_entry in it: + Q[(i, k) + it.multi_index] = q_j_entry + if len(set((i, k) + it.multi_index)) > 1: + # Identity has i == k == it.multi_index[0] == ... + # Since i increases from 0 in increments of 1 we know + # that if this check is not triggered we definitely + # have an identity tensor. + self.Q_is_identity = False + + # + # CONVERT Q TO gem.Literal (TODO: should be a sparse tensor) + # + + # temporary until sparse literals are implemented in GEM which will + # automatically convert a dictionary of keys internally. + Q = gem.Literal(sparse.as_coo(Q).todense()) + # + # CONVERT x TO gem.PointSet + # + + # Convert PointSets to a single PointSet with the correct ordering + # for contraction with Q + random_pt, _ = next(iter(x.values())) + dim = random_pt.dimension + allpts = np.empty((len(x), dim), dtype=random_pt.points.dtype) + for _, (x_k, k) in x.items(): + assert x_k.dimension == dim + allpts[k, :] = x_k.points + assert allpts.shape[1] == dim + x = PointSet(allpts) + + # + # INDEX OUT x.indices from Q + # + assert len(x.indices) == 1 + assert Q.shape[1] == x.indices[0].extent + shape_indices = tuple(gem.Index(extent=s) for s in Q.shape) + Q = gem.ComponentTensor( + gem.Indexed(Q, (shape_indices[0],) + x.indices + shape_indices[2:]), + (shape_indices[0],) + shape_indices[2:]) + + return Q, x + @property def mapping(self): mappings = set(self._element.mapping()) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index f817672b6..2321bcede 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -144,11 +144,98 @@ def point_evaluation(self, order, refcoords, entity=None): :param entity: the cell entity on which to tabulate. ''' + @property + def dual_basis(self): + '''Return a dual evaluation gem weight tensor Q and point set x to dual + evaluate a function fn at. + + The general dual evaluation is then Q * fn(x). + + Note that the contraction index of the point set x is indexed out of Q + to avoid confusion when trying index it out of Q later in + dual_evaluation. + + If the dual weights are scalar then Q, for a general scalar FIAT + element, is a matrix with dimensions + (num_nodes, num_points) + where num_points is made a free index that match the free index of x. + + If the dual weights are tensor valued then Q, for a general tensor + valued FIAT element, is a tensor with dimensions + (num_nodes, num_points, dual_weight_shape[0], ..., dual_weight_shape[n]) + where num_points made a free index that matches the free index of x. + + If the dual basis is of a tensor product or FlattenedDimensions element + with N factors then Q in general is a tensor with dimensions + (num_nodes_factor1, ..., num_nodes_factorN, + num_points_factor1, ..., num_points_factorN, + dual_weight_shape[0], ..., dual_weight_shape[n]) + where num_points_factorX are made free indices that match the free + indices of x (which is now a TensorPointSet). + + If the dual basis is of a tensor finite element with some shape + (S1, S2, ..., Sn) then the tensor element tQ is constructed from the + base element's Q by taking the outer product with appropriately sized + identity matrices: + tQ = Q ⊗ 𝟙ₛ₁ ⊗ 𝟙ₛ₂ ⊗ ... ⊗ 𝟙ₛₙ + ''' + raise NotImplementedError( + f"Dual basis not defined for element {type(self).__name__}" + ) + + def dual_evaluation(self, fn): + '''Return code for performing the dual basis evaluation at the nodes of + the reference element. Currently only works for non-derivatives (not + implemented) and flat elements (implemented in TensorFiniteElement and + TensorProductElement). + + :param fn: Callable representing the function to dual evaluate. + Callable should take in an :class:`AbstractPointSet` and + return a GEM expression for evaluation of the function at + those points. If the callable provides a ``.factors`` + property then it may be used for sum factorisation in + :class:`TensorProductElement`s + :returns: A tuple (dual_evaluation_indexed_sum, basis_indices) + ''' + # NOTE: This is a 'flat' implementation that does not deal with + # tensor valued expressions or points. These are dealt with in + # TensorFiniteElement and TensorProductElement + + Q, x = self.dual_basis + + # + # EVALUATE fn AT x + # + expr = fn(x) + + # + # TENSOR CONTRACT Q WITH expr + # + + # NOTE: any shape indices in the expression are because the expression + # is tensor valued. + assert expr.shape == Q.shape[1:] + expr_shape_indices = tuple(gem.Index(extent=ex) for ex in expr.shape) + basis_indices = tuple(gem.Index(extent=ex) for ex in Q.shape[:1]) + try: + assert self.Q_is_identity + assert len(set(Q.shape)) == 1 + assert len(basis_indices) == 1 + # Skip the multiplication by an identity tensor + basis_indices = x.indices + dual_evaluation_indexed_sum = expr + except AssertionError: + dual_evaluation_indexed_sum = gem.optimise.make_product((Q[basis_indices + expr_shape_indices], expr[expr_shape_indices]), x.indices+expr_shape_indices) + + return dual_evaluation_indexed_sum, basis_indices + @abstractproperty def mapping(self): '''Appropriate mapping from the reference cell to a physical cell for all basis functions of the finite element.''' + Q_is_identity = None + def entity_support_dofs(elem, entity_dim): '''Return the map of entity id to the degrees of freedom for which diff --git a/finat/quadrature_element.py b/finat/quadrature_element.py index 43d806dfa..0ddd101c1 100644 --- a/finat/quadrature_element.py +++ b/finat/quadrature_element.py @@ -1,4 +1,4 @@ -from finat.point_set import PointSet, UnknownPointSet +from finat.point_set import UnknownPointSet from functools import reduce import numpy @@ -85,11 +85,7 @@ def value_shape(self): def fiat_equivalent(self): ps = self._rule.point_set if isinstance(ps, UnknownPointSet): - # This really should raise a ValueError since there ought not to be - # a FIAT equivalent where the points are unknown. - # For compatibility until FInAT dual evaluation is done we pass an - # array of NaNs of correct shape as the points. - ps = PointSet(numpy.full(ps.points.shape, numpy.nan)) + raise ValueError("A quadrature element with rule with runtime points has no fiat equivalent!") weights = getattr(self._rule, 'weights', None) if weights is None: # we need the weights. @@ -127,6 +123,19 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): def point_evaluation(self, order, refcoords, entity=None): raise NotImplementedError("QuadratureElement cannot do point evaluation!") + @property + def dual_basis(self): + ps = self._rule.point_set + Q = gem.Literal([self._rule.weights]) + # Index out ps.indices from Q + assert len(ps.indices) == 1 + assert Q.shape[1] == ps.indices[0].extent + shape_indices = tuple(gem.Index(extent=s) for s in Q.shape) + Q = gem.ComponentTensor( + gem.Indexed(Q, (shape_indices[0],) + ps.indices + shape_indices[2:]), + (shape_indices[0],) + shape_indices[2:]) + return Q, ps + @property def mapping(self): return "affine" diff --git a/finat/tensor_product.py b/finat/tensor_product.py index 7c9b21ef1..cca95b8ce 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -3,6 +3,7 @@ from operator import methodcaller import numpy +import operator import FIAT from FIAT.polynomial_set import mis @@ -13,6 +14,7 @@ from finat.finiteelementbase import FiniteElementBase from finat.point_set import PointSingleton, PointSet, TensorPointSet +from finat.cube import FlattenedDimensions class TensorProductElement(FiniteElementBase): @@ -162,6 +164,64 @@ def point_evaluation(self, order, point, entity=None): return self._merge_evaluations(factor_results) + @property + def dual_basis(self): + # Outer product the dual bases of the factors + qs, pss = zip(*(f.dual_basis for f in self.factors)) + # TODO: check if this Q_is_identity calculation is correct + self.Q_is_identity = all(f.Q_is_identity for f in self.factors) + ps = TensorPointSet(pss) + tuples_of_q_shape_indices = tuple(gem.indices(len(q.shape)) for q in qs) + q_muls = reduce(operator.mul, (q[i] for q, i in zip(qs, tuples_of_q_shape_indices))) + # Flatten to get final shape indices + q_shape_indices = tuple(index for tuple_of_indices in tuples_of_q_shape_indices for index in tuple_of_indices) + Q = gem.ComponentTensor(q_muls, q_shape_indices) + return Q, ps + + def dual_evaluation(self, fn): + if not hasattr(fn, 'factors'): + + Q, x = self.dual_basis + + # + # EVALUATE fn AT x + # + expr = fn(x) + + # + # TENSOR CONTRACT Q WITH expr + # + + # NOTE: any shape indices in the expression are because the + # expression is tensor valued. + # NOTE: here the first num_factors rows of Q are node sets (1 for + # each factor) + # TODO: work out if there are any other cases where the basis + # indices in the shape of the dual basis tensor Q are more than + # just the first shape index (e.g. with EnrichedElement) + num_factors = total_num_factors(self.factors) + assert expr.shape == Q.shape[num_factors:] + expr_shape_indices = tuple(gem.Index(extent=ex) for ex in expr.shape) + basis_indices = tuple(gem.Index(extent=ex) for ex in Q.shape[:num_factors]) + # TODO: Work out how to deal with identity Q in this case. + dual_evaluation_indexed_sum = gem.optimise.make_product((Q[basis_indices + expr_shape_indices], expr[expr_shape_indices]), x.indices+expr_shape_indices) + + return dual_evaluation_indexed_sum, basis_indices + + else: + raise NotImplementedError('Sum factorised dual evaluation is not yet implemented') + # TODO do sum factorisation applying function factors to + # dual bases of factors and then putting back together again. + # Will look something like this: + # assert len(fn.factors) == len(self.factors) + # for i, factor in enumerate(self.factors): + # factor_Q, factor_ps = factor.dual_basis + # factor_gem_tensor = factor.dual_evaluation(fn.factors[i]) + # ... + # somehow build up whole dual evaluation using above info + # ... + # return gem_tensor + @cached_property def mapping(self): mappings = [fe.mapping for fe in self.factors if fe.mapping != "affine"] @@ -234,3 +294,21 @@ def factor_point_set(product_cell, product_dim, point_set): return result raise NotImplementedError("How to tabulate TensorProductElement on %s?" % (type(point_set).__name__,)) + + +def total_num_factors(factors): + '''Return the total number of (potentially nested) factors in a list + element factors. + + :arg factors: An iterable of FInAT finite elements + :returns: The total number of factors in the flattened iterable + ''' + num_factors = len(factors) + for factor in factors: + if hasattr(factor, 'factors'): + num_factors += total_num_factors(factor.factors) + if isinstance(factor, FlattenedDimensions): + # FlattenedDimensions introduces another factor without + # having a factors property + num_factors += 1 + return num_factors diff --git a/finat/tensorfiniteelement.py b/finat/tensorfiniteelement.py index 037698d9e..839a009ca 100644 --- a/finat/tensorfiniteelement.py +++ b/finat/tensorfiniteelement.py @@ -5,6 +5,8 @@ import gem from finat.finiteelementbase import FiniteElementBase +from finat.tensor_product import total_num_factors +from finat.cube import FlattenedDimensions class TensorFiniteElement(FiniteElementBase): @@ -36,7 +38,7 @@ def __init__(self, element, shape, transpose=False): we subscript the vector-value with :math:`\gamma\epsilon` then we can write: .. math:: - \boldsymbol\phi_{\gamma\epsilon(i\alpha\beta)} = \delta_{\gamma\alpha}\delta{\epsilon\beta}\phi_i + \boldsymbol\phi_{\gamma\epsilon(i\alpha\beta)} = \delta_{\gamma\alpha}\delta_{\epsilon\beta}\phi_i This form enables the simplification of the loop nests which will eventually be created, so it is the form we employ here.""" @@ -83,9 +85,9 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): r"""Produce the recipe for basis function evaluation at a set of points :math:`q`: .. math:: - \boldsymbol\phi_{(\gamma \epsilon) (i \alpha \beta) q} = \delta_{\alpha \gamma}\delta{\beta \epsilon}\phi_{i q} + \boldsymbol\phi_{(\gamma \epsilon) (i \alpha \beta) q} = \delta_{\alpha \gamma} \delta_{\beta \epsilon}\phi_{i q} - \nabla\boldsymbol\phi_{(\epsilon \gamma \zeta) (i \alpha \beta) q} = \delta_{\alpha \epsilon} \deta{\beta \gamma}\nabla\phi_{\zeta i q} + \nabla\boldsymbol\phi_{(\epsilon \gamma \zeta) (i \alpha \beta) q} = \delta_{\alpha \epsilon} \delta_{\beta \gamma}\nabla\phi_{\zeta i q} """ scalar_evaluation = self._base_element.basis_evaluation return self._tensorise(scalar_evaluation(order, ps, entity, coordinate_mapping=coordinate_mapping)) @@ -120,6 +122,62 @@ def _tensorise(self, scalar_evaluation): ) return result + @property + def dual_basis(self): + + base = self.base_element + Q, points = base.dual_basis + + # Suppose the tensor element has shape (2, 4) + # These identity matrices may have difference sizes depending the shapes + # tQ = Q ⊗ 𝟙₂ ⊗ 𝟙₄ + Q_shape_indices = gem.indices(len(Q.shape)) + i_s = tuple(gem.Index(extent=d) for d in self._shape) + j_s = tuple(gem.Index(extent=d) for d in self._shape) + # we need one delta for each shape axis + deltas = reduce(gem.Product, (gem.Delta(i, j) for i, j in zip(i_s, j_s))) + # TODO Need to check how this plays with the transpose argument to TensorFiniteElement. + tQ = gem.ComponentTensor(Q[Q_shape_indices]*deltas, Q_shape_indices + i_s + j_s) + + return tQ, points + + def dual_evaluation(self, fn): + + Q, x = self.dual_basis + expr = fn(x) + + # TODO: Add shortcut (if relevant) for Q being identity tensor + + # NOTE: any shape indices in the expression are because the expression + # is tensor valued. + assert set(expr.shape) == set(self.value_shape) + + base_value_indices = self.base_element.get_value_indices() + + # Reconstruct the indices from the deltas in dual_basis above and + # contract Q with expr + Q_shape_indices = tuple(gem.Index(extent=ex) for ex in Q.shape) + expr_contraction_indices = tuple(gem.Index(extent=d) for d in self._shape) + basis_tensor_indices = tuple(gem.Index(extent=d) for d in self._shape) + # NOTE: here the first num_factors rows of Q are node sets (1 for + # each factor) + # TODO: work out if there are any other cases where the basis + # indices in the shape of the dual basis tensor Q are more than + # just the first shape index (e.g. with EnrichedElement) + if hasattr(self.base_element, 'factors'): + num_factors = total_num_factors(self.base_element.factors) + elif isinstance(self.base_element, FlattenedDimensions): + # Factor might be a FlattenedDimensions which introduces + # another factor without having a factors property + num_factors = 2 + else: + num_factors = 1 + Q_indexed = Q[Q_shape_indices[:num_factors] + base_value_indices + expr_contraction_indices + basis_tensor_indices] + expr_indexed = expr[expr_contraction_indices + base_value_indices] + dual_evaluation_indexed_sum = gem.optimise.make_product((Q_indexed, expr_indexed), x.indices + base_value_indices + expr_contraction_indices) + basis_indices = Q_shape_indices[:num_factors] + basis_tensor_indices + return dual_evaluation_indexed_sum, basis_indices + @property def mapping(self): return self._base_element.mapping From 313f20cf96b459be5558c2751eb28c7e9f043294 Mon Sep 17 00:00:00 2001 From: Reuben Nixon-Hill Date: Thu, 19 Aug 2021 16:17:13 +0100 Subject: [PATCH 666/749] dual_evaluation: generalise methods Only TensorFiniteElement needs its own routine separate from finiteelementbase because of the shape complications. Sum factorisation is now done automatically thanks to gem.optimise.contraction so there's no need to worry about giving dual evaluation functions fn a .factors property in that case. --- finat/cube.py | 3 --- finat/finiteelementbase.py | 30 +++++++++++++------------- finat/tensor_product.py | 44 -------------------------------------- 3 files changed, 15 insertions(+), 62 deletions(-) diff --git a/finat/cube.py b/finat/cube.py index 377084be6..2a61aeabd 100644 --- a/finat/cube.py +++ b/finat/cube.py @@ -79,9 +79,6 @@ def point_evaluation(self, order, point, entity=None): def dual_basis(self): return self.product.dual_basis - def dual_evaluation(self, fn): - return self.product.dual_evaluation(fn) - @property def index_shape(self): return self.product.index_shape diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 2321bcede..a1dec4366 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -186,15 +186,12 @@ def dual_basis(self): def dual_evaluation(self, fn): '''Return code for performing the dual basis evaluation at the nodes of the reference element. Currently only works for non-derivatives (not - implemented) and flat elements (implemented in TensorFiniteElement and - TensorProductElement). + implemented) and flat elements (implemented in TensorFiniteElement). :param fn: Callable representing the function to dual evaluate. Callable should take in an :class:`AbstractPointSet` and return a GEM expression for evaluation of the function at - those points. If the callable provides a ``.factors`` - property then it may be used for sum factorisation in - :class:`TensorProductElement`s + those points. :returns: A tuple (dual_evaluation_indexed_sum, basis_indices) ''' # NOTE: This is a 'flat' implementation that does not deal with @@ -214,20 +211,23 @@ def dual_evaluation(self, fn): # NOTE: any shape indices in the expression are because the expression # is tensor valued. - assert expr.shape == Q.shape[1:] - expr_shape_indices = tuple(gem.Index(extent=ex) for ex in expr.shape) - basis_indices = tuple(gem.Index(extent=ex) for ex in Q.shape[:1]) + expr_shape_indices = gem.indices(len(expr.shape)) + basis_indices = gem.indices(len(Q.shape) - len(expr.shape)) try: - assert self.Q_is_identity - assert len(set(Q.shape)) == 1 - assert len(basis_indices) == 1 + if not self.Q_is_identity: + raise Exception + if not len(set(Q.shape)) == 1: + raise Exception + if not len(basis_indices) == 1: + raise Exception # Skip the multiplication by an identity tensor basis_indices = x.indices - dual_evaluation_indexed_sum = expr - except AssertionError: - dual_evaluation_indexed_sum = gem.optimise.make_product((Q[basis_indices + expr_shape_indices], expr[expr_shape_indices]), x.indices+expr_shape_indices) + Qfn = expr + except Exception: + Qfn = gem.IndexSum(Q[basis_indices + expr_shape_indices] * expr[expr_shape_indices], x.indices + expr_shape_indices) + Qfn = gem.optimise.contraction(Qfn) - return dual_evaluation_indexed_sum, basis_indices + return Qfn, basis_indices @abstractproperty def mapping(self): diff --git a/finat/tensor_product.py b/finat/tensor_product.py index cca95b8ce..cfefb8f2a 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -178,50 +178,6 @@ def dual_basis(self): Q = gem.ComponentTensor(q_muls, q_shape_indices) return Q, ps - def dual_evaluation(self, fn): - if not hasattr(fn, 'factors'): - - Q, x = self.dual_basis - - # - # EVALUATE fn AT x - # - expr = fn(x) - - # - # TENSOR CONTRACT Q WITH expr - # - - # NOTE: any shape indices in the expression are because the - # expression is tensor valued. - # NOTE: here the first num_factors rows of Q are node sets (1 for - # each factor) - # TODO: work out if there are any other cases where the basis - # indices in the shape of the dual basis tensor Q are more than - # just the first shape index (e.g. with EnrichedElement) - num_factors = total_num_factors(self.factors) - assert expr.shape == Q.shape[num_factors:] - expr_shape_indices = tuple(gem.Index(extent=ex) for ex in expr.shape) - basis_indices = tuple(gem.Index(extent=ex) for ex in Q.shape[:num_factors]) - # TODO: Work out how to deal with identity Q in this case. - dual_evaluation_indexed_sum = gem.optimise.make_product((Q[basis_indices + expr_shape_indices], expr[expr_shape_indices]), x.indices+expr_shape_indices) - - return dual_evaluation_indexed_sum, basis_indices - - else: - raise NotImplementedError('Sum factorised dual evaluation is not yet implemented') - # TODO do sum factorisation applying function factors to - # dual bases of factors and then putting back together again. - # Will look something like this: - # assert len(fn.factors) == len(self.factors) - # for i, factor in enumerate(self.factors): - # factor_Q, factor_ps = factor.dual_basis - # factor_gem_tensor = factor.dual_evaluation(fn.factors[i]) - # ... - # somehow build up whole dual evaluation using above info - # ... - # return gem_tensor - @cached_property def mapping(self): mappings = [fe.mapping for fe in self.factors if fe.mapping != "affine"] From 14554cc561d4d190739f6aabff8d1c2d8329648c Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Thu, 19 Aug 2021 16:52:18 +0100 Subject: [PATCH 667/749] optimise: option to ignore indices in sum factorisation For optimisation of dual evaluation, we want to sum factorise a contraction, but know some sets of indices we just need to treat with delta elimination. Allows caller to exclude these indices from the factorisation routines (which have a hard index limit) to avoid N! behaviour for large N. --- gem/optimise.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/gem/optimise.py b/gem/optimise.py index 70ce294b0..216df068a 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -481,13 +481,18 @@ def traverse_sum(expression, stop_at=None): return result -def contraction(expression): +def contraction(expression, ignore=None): """Optimise the contractions of the tensor product at the root of the expression, including: - IndexSum-Delta cancellation - Sum factorisation + :arg ignore: Optional set of indices to ignore when applying sum + factorisation (otherwise all summation indices will be + considered). Use this if your expression has many contraction + indices. + This routine was designed with finite element coefficient evaluation in mind. """ @@ -498,7 +503,15 @@ def contraction(expression): def rebuild(expression): sum_indices, factors = delta_elimination(*traverse_product(expression)) factors = remove_componenttensors(factors) - return sum_factorise(sum_indices, factors) + if ignore is not None: + # TODO: This is a really blunt instrument and one might + # plausibly want the ignored indices to be contracted on + # the inside rather than the outside. + extra = tuple(i for i in sum_indices if i in ignore) + to_factor = tuple(i for i in sum_indices if i not in ignore) + return IndexSum(sum_factorise(to_factor, factors), extra) + else: + return sum_factorise(sum_indices, factors) # Sometimes the value shape is composed as a ListTensor, which # could get in the way of decomposing factors. In particular, From 931d12322489755add4356dbb1d70fee82b6f047 Mon Sep 17 00:00:00 2001 From: Reuben Nixon-Hill Date: Thu, 19 Aug 2021 16:40:46 +0100 Subject: [PATCH 668/749] tensorfiniteelement: Tidy dual_basis and dual_evaluation Co-authored-by: Lawrence Mitchell --- finat/tensor_product.py | 19 ---------- finat/tensorfiniteelement.py | 70 ++++++++++++++++-------------------- 2 files changed, 31 insertions(+), 58 deletions(-) diff --git a/finat/tensor_product.py b/finat/tensor_product.py index cfefb8f2a..200842b95 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -14,7 +14,6 @@ from finat.finiteelementbase import FiniteElementBase from finat.point_set import PointSingleton, PointSet, TensorPointSet -from finat.cube import FlattenedDimensions class TensorProductElement(FiniteElementBase): @@ -250,21 +249,3 @@ def factor_point_set(product_cell, product_dim, point_set): return result raise NotImplementedError("How to tabulate TensorProductElement on %s?" % (type(point_set).__name__,)) - - -def total_num_factors(factors): - '''Return the total number of (potentially nested) factors in a list - element factors. - - :arg factors: An iterable of FInAT finite elements - :returns: The total number of factors in the flattened iterable - ''' - num_factors = len(factors) - for factor in factors: - if hasattr(factor, 'factors'): - num_factors += total_num_factors(factor.factors) - if isinstance(factor, FlattenedDimensions): - # FlattenedDimensions introduces another factor without - # having a factors property - num_factors += 1 - return num_factors diff --git a/finat/tensorfiniteelement.py b/finat/tensorfiniteelement.py index 839a009ca..ed922c13b 100644 --- a/finat/tensorfiniteelement.py +++ b/finat/tensorfiniteelement.py @@ -5,8 +5,6 @@ import gem from finat.finiteelementbase import FiniteElementBase -from finat.tensor_product import total_num_factors -from finat.cube import FlattenedDimensions class TensorFiniteElement(FiniteElementBase): @@ -124,59 +122,53 @@ def _tensorise(self, scalar_evaluation): @property def dual_basis(self): - base = self.base_element Q, points = base.dual_basis # Suppose the tensor element has shape (2, 4) # These identity matrices may have difference sizes depending the shapes # tQ = Q ⊗ 𝟙₂ ⊗ 𝟙₄ - Q_shape_indices = gem.indices(len(Q.shape)) - i_s = tuple(gem.Index(extent=d) for d in self._shape) - j_s = tuple(gem.Index(extent=d) for d in self._shape) - # we need one delta for each shape axis - deltas = reduce(gem.Product, (gem.Delta(i, j) for i, j in zip(i_s, j_s))) - # TODO Need to check how this plays with the transpose argument to TensorFiniteElement. - tQ = gem.ComponentTensor(Q[Q_shape_indices]*deltas, Q_shape_indices + i_s + j_s) + scalar_i = self.base_element.get_indices() + scalar_vi = self.base_element.get_value_indices() + tensor_i = tuple(gem.Index(extent=d) for d in self._shape) + tensor_vi = tuple(gem.Index(extent=d) for d in self._shape) + # Couple new basis function and value indices + deltas = reduce(gem.Product, (gem.Delta(j, k) + for j, k in zip(tensor_i, tensor_vi))) + if self._transpose: + index_ordering = tensor_i + scalar_i + tensor_vi + scalar_vi + else: + index_ordering = scalar_i + tensor_i + tensor_vi + scalar_vi + Qi = Q[scalar_i + scalar_vi] + tQ = gem.ComponentTensor(Qi*deltas, index_ordering) return tQ, points def dual_evaluation(self, fn): - - Q, x = self.dual_basis + tQ, x = self.dual_basis expr = fn(x) - # TODO: Add shortcut (if relevant) for Q being identity tensor + gem.optimise.contraction(expr) # NOTE: any shape indices in the expression are because the expression # is tensor valued. - assert set(expr.shape) == set(self.value_shape) - - base_value_indices = self.base_element.get_value_indices() - - # Reconstruct the indices from the deltas in dual_basis above and - # contract Q with expr - Q_shape_indices = tuple(gem.Index(extent=ex) for ex in Q.shape) - expr_contraction_indices = tuple(gem.Index(extent=d) for d in self._shape) - basis_tensor_indices = tuple(gem.Index(extent=d) for d in self._shape) - # NOTE: here the first num_factors rows of Q are node sets (1 for - # each factor) - # TODO: work out if there are any other cases where the basis - # indices in the shape of the dual basis tensor Q are more than - # just the first shape index (e.g. with EnrichedElement) - if hasattr(self.base_element, 'factors'): - num_factors = total_num_factors(self.base_element.factors) - elif isinstance(self.base_element, FlattenedDimensions): - # Factor might be a FlattenedDimensions which introduces - # another factor without having a factors property - num_factors = 2 + assert expr.shape == self.value_shape + + scalar_i = self.base_element.get_indices() + scalar_vi = self.base_element.get_value_indices() + tensor_i = tuple(gem.Index(extent=d) for d in self._shape) + tensor_vi = tuple(gem.Index(extent=d) for d in self._shape) + + if self._transpose: + index_ordering = tensor_i + scalar_i + tensor_vi + scalar_vi else: - num_factors = 1 - Q_indexed = Q[Q_shape_indices[:num_factors] + base_value_indices + expr_contraction_indices + basis_tensor_indices] - expr_indexed = expr[expr_contraction_indices + base_value_indices] - dual_evaluation_indexed_sum = gem.optimise.make_product((Q_indexed, expr_indexed), x.indices + base_value_indices + expr_contraction_indices) - basis_indices = Q_shape_indices[:num_factors] + basis_tensor_indices - return dual_evaluation_indexed_sum, basis_indices + index_ordering = scalar_i + tensor_i + tensor_vi + scalar_vi + + tQi = tQ[index_ordering] + expri = expr[tensor_i + scalar_vi] + evaluation = gem.IndexSum(tQi * expri, x.indices + scalar_vi + tensor_i) + evaluation = gem.optimise.contraction(evaluation) + return evaluation, scalar_i + tensor_vi @property def mapping(self): From 17b2d031aabe9fbba42e9dedb3606ea99a73f8b3 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Thu, 19 Aug 2021 16:57:29 +0100 Subject: [PATCH 669/749] base: generalise dual evaluation routine --- finat/finiteelementbase.py | 42 +++++++++++--------------------------- 1 file changed, 12 insertions(+), 30 deletions(-) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index a1dec4366..cf4c7c39f 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -194,48 +194,30 @@ def dual_evaluation(self, fn): those points. :returns: A tuple (dual_evaluation_indexed_sum, basis_indices) ''' - # NOTE: This is a 'flat' implementation that does not deal with - # tensor valued expressions or points. These are dealt with in - # TensorFiniteElement and TensorProductElement - Q, x = self.dual_basis - # - # EVALUATE fn AT x - # expr = fn(x) - - # - # TENSOR CONTRACT Q WITH expr - # - + # Apply sum factorisation and delta elimination to the + # expression + expr = gem.optimise.contraction(expr) # NOTE: any shape indices in the expression are because the expression # is tensor valued. - expr_shape_indices = gem.indices(len(expr.shape)) + assert expr.shape == Q.shape[len(Q.shape)-len(expr.shape):] + shape_indices = gem.indices(len(expr.shape)) basis_indices = gem.indices(len(Q.shape) - len(expr.shape)) - try: - if not self.Q_is_identity: - raise Exception - if not len(set(Q.shape)) == 1: - raise Exception - if not len(basis_indices) == 1: - raise Exception - # Skip the multiplication by an identity tensor - basis_indices = x.indices - Qfn = expr - except Exception: - Qfn = gem.IndexSum(Q[basis_indices + expr_shape_indices] * expr[expr_shape_indices], x.indices + expr_shape_indices) - Qfn = gem.optimise.contraction(Qfn) - - return Qfn, basis_indices + Qi = Q[basis_indices + shape_indices] + expri = expr[shape_indices] + evaluation = gem.IndexSum(Qi * expri, x.indices + shape_indices) + # Don't want to factorise over the shape indices in the + # contraction, so exclude those from the optimisation routines. + evaluation = gem.optimise.contraction(evaluation, shape_indices) + return evaluation, basis_indices @abstractproperty def mapping(self): '''Appropriate mapping from the reference cell to a physical cell for all basis functions of the finite element.''' - Q_is_identity = None - def entity_support_dofs(elem, entity_dim): '''Return the map of entity id to the degrees of freedom for which From 5470e04cbc2e55798e24b130e61110dde915b326 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Thu, 19 Aug 2021 17:00:23 +0100 Subject: [PATCH 670/749] tensor product: Simplify dual basis construction --- finat/tensor_product.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/finat/tensor_product.py b/finat/tensor_product.py index 200842b95..60be75fe6 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -3,7 +3,6 @@ from operator import methodcaller import numpy -import operator import FIAT from FIAT.polynomial_set import mis @@ -166,15 +165,11 @@ def point_evaluation(self, order, point, entity=None): @property def dual_basis(self): # Outer product the dual bases of the factors - qs, pss = zip(*(f.dual_basis for f in self.factors)) - # TODO: check if this Q_is_identity calculation is correct - self.Q_is_identity = all(f.Q_is_identity for f in self.factors) + qs, pss = zip(*(factor.dual_basis for factor in self.factors)) ps = TensorPointSet(pss) - tuples_of_q_shape_indices = tuple(gem.indices(len(q.shape)) for q in qs) - q_muls = reduce(operator.mul, (q[i] for q, i in zip(qs, tuples_of_q_shape_indices))) - # Flatten to get final shape indices - q_shape_indices = tuple(index for tuple_of_indices in tuples_of_q_shape_indices for index in tuple_of_indices) - Q = gem.ComponentTensor(q_muls, q_shape_indices) + qis = tuple(q[gem.indices(len(q.shape))] for q in qs) + indices = tuple(chain(*(q.index_ordering() for q in qis))) + Q = gem.ComponentTensor(reduce(gem.Product, qis), indices) return Q, ps @cached_property From 97bb787da645f5636df764800fa2bc2570eb6240 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Thu, 19 Aug 2021 17:10:20 +0100 Subject: [PATCH 671/749] fiat: simplify translation of FIAT dual to FInAT dual --- finat/fiat_elements.py | 128 ++++++++++++----------------------------- 1 file changed, 37 insertions(+), 91 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 37dccd02c..c74aec104 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -190,103 +190,49 @@ def dual_basis(self): # tensor valued. # The columns (unique points to evaluate) are indexed out ready for # contraction with the point set free index. + dual_basis = self._element.dual_basis() + seen = dict() + allpts = [] + # Find the unique points to evaluate at. + # We might be able to make this a smaller set by treating each + # point one by one, but most of the redundancy comes from + # multiple functionals using the same quadrature rule. + for dual in dual_basis: + pts = dual.get_point_dict().keys() + pts = tuple(sorted(pts)) # need this for determinism + if pts not in seen: + # k are indices into Q (see below) for the seen points + kstart = len(allpts) + kend = kstart + len(pts) + seen[pts] = kstart, kend + allpts.extend(pts) + allpts = np.asarray(allpts, dtype=np.float64) Q = {} - - # Dict of unique points to evaluate stored as - # {tuple(pt_hash.flatten()): (x_k, k)} pairs. Points in index k - # order form a vector required for correct contraction with Q. Will - # become a FInAT.PointSet later. - # x_key = numpy.round(x_k, x_key_decimals) such that - # numpy.allclose(x_k, pt_hash, atol=1*10**-dec) == true - x = {} - x_key_decimals = 12 - x_key_atol = 1e-12 # = 1*10**-dec - - # - # BUILD Q TENSOR - # - - # FIXME: The below loop is REALLY SLOW for BDM - Q and x should just be output as the dual basis - - last_shape = None - self.Q_is_identity = True # TODO do this better - - fiat_dual_basis = self._element.dual_basis() - # i are rows of Q - for i, dual in enumerate(fiat_dual_basis): + for i, dual in enumerate(dual_basis): point_dict = dual.get_point_dict() - for j, (x_j, tuples) in enumerate(point_dict.items()): - x_j = PointSet((x_j,)) # TODO doesn't need to be PointSet - # get weights q_j for point x_j from complicated FIAT dual data - # structure - weight_dict = {cmp: weight for weight, cmp in tuples} - if len(self._element.value_shape()) == 0: - q_j = np.array(weight_dict[tuple()]) - else: - q_j = np.zeros(self._element.value_shape()) - for key, item in weight_dict.items(): - q_j[key] = item - - # Ensure all weights have the same shape - if last_shape is not None: - assert q_j.shape == last_shape - last_shape = q_j.shape - - # Create key into x dict - x_key = np.round(x_j.points, x_key_decimals) - assert np.allclose(x_j.points, x_key, atol=x_key_atol) - x_key = tuple(x_key.flatten()) - - # Get value and index k or add to dict. k are the columns of Q. - try: - x_j, k = x[x_key] - except KeyError: - k = len(x) - x[x_key] = x_j, k - - # q_j may be tensor valued - it = np.nditer(q_j, flags=['multi_index']) - for q_j_entry in it: - Q[(i, k) + it.multi_index] = q_j_entry - if len(set((i, k) + it.multi_index)) > 1: - # Identity has i == k == it.multi_index[0] == ... - # Since i increases from 0 in increments of 1 we know - # that if this check is not triggered we definitely - # have an identity tensor. - self.Q_is_identity = False - - # - # CONVERT Q TO gem.Literal (TODO: should be a sparse tensor) - # - - # temporary until sparse literals are implemented in GEM which will - # automatically convert a dictionary of keys internally. - Q = gem.Literal(sparse.as_coo(Q).todense()) - # - # CONVERT x TO gem.PointSet - # - - # Convert PointSets to a single PointSet with the correct ordering - # for contraction with Q - random_pt, _ = next(iter(x.values())) - dim = random_pt.dimension - allpts = np.empty((len(x), dim), dtype=random_pt.points.dtype) - for _, (x_k, k) in x.items(): - assert x_k.dimension == dim - allpts[k, :] = x_k.points - assert allpts.shape[1] == dim + pts = tuple(sorted(point_dict.keys())) + kstart, kend = seen[pts] + for p, k in zip(pts, range(kstart, kend)): + for weight, cmp in point_dict[p]: + Q[(i, k, *cmp)] = weight + x = PointSet(allpts) + if all(len(set(key)) == 1 and np.isclose(weight, 1) and len(key) == 2 + for key, weight in Q.items()): + # Identity, is this the most general case? + extents = tuple(map(max, zip(*Q.keys()))) + js = tuple(gem.Index(extent=e+1) for e in extents) + assert len(js) == 2 + Q = gem.ComponentTensor(gem.Delta(*js), js) + else: + # temporary until sparse literals are implemented in GEM which will + # automatically convert a dictionary of keys internally. + Q = gem.Literal(sparse.as_coo(Q).todense()) - # - # INDEX OUT x.indices from Q - # assert len(x.indices) == 1 assert Q.shape[1] == x.indices[0].extent - shape_indices = tuple(gem.Index(extent=s) for s in Q.shape) - Q = gem.ComponentTensor( - gem.Indexed(Q, (shape_indices[0],) + x.indices + shape_indices[2:]), - (shape_indices[0],) + shape_indices[2:]) - + i, *js = gem.indices(len(Q.shape) - 1) + Q = gem.ComponentTensor(gem.Indexed(Q, (i, *x.indices, *js)), (i, *js)) return Q, x @property From c2b73d1be80c249c7953b607153935613cf01813 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Thu, 19 Aug 2021 18:06:13 +0100 Subject: [PATCH 672/749] dual_evaluation and dual_basis comment/doc/name improvements Co-authored-by: Reuben Nixon-Hill --- finat/fiat_elements.py | 36 +++++++++--------- finat/finiteelementbase.py | 73 ++++++++++++++++++++++++------------ finat/tensorfiniteelement.py | 15 +++++--- 3 files changed, 76 insertions(+), 48 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index c74aec104..b759e2a1e 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -178,26 +178,14 @@ def point_evaluation(self, order, refcoords, entity=None): @property def dual_basis(self): - # A tensor of weights (of total rank R) to contract with a unique - # vector of points to evaluate at, giving a tensor (of total rank R-1) - # where the first indices (rows) correspond to a basis functional - # (node). - # DOK Sparse matrix in (row, col, higher,..)=>value pairs - to become a - # gem.SparseLiteral. - # Rows are number of nodes/dual functionals. - # Columns are unique points to evaluate. - # Higher indices are tensor indices of the weights when weights are - # tensor valued. - # The columns (unique points to evaluate) are indexed out ready for - # contraction with the point set free index. - dual_basis = self._element.dual_basis() + fiat_dual_basis = self._element.dual_basis() seen = dict() allpts = [] # Find the unique points to evaluate at. # We might be able to make this a smaller set by treating each # point one by one, but most of the redundancy comes from # multiple functionals using the same quadrature rule. - for dual in dual_basis: + for dual in fiat_dual_basis: pts = dual.get_point_dict().keys() pts = tuple(sorted(pts)) # need this for determinism if pts not in seen: @@ -206,20 +194,29 @@ def dual_basis(self): kend = kstart + len(pts) seen[pts] = kstart, kend allpts.extend(pts) - allpts = np.asarray(allpts, dtype=np.float64) + x = PointSet(allpts) + # Build Q. + # Q is a tensor of weights (of total rank R) to contract with a unique + # vector of points to evaluate at, giving a tensor (of total rank R-1) + # where the first indices (rows) correspond to a basis functional + # (node). + # Q is a DOK Sparse matrix in (row, col, higher,..)=>value pairs (to + # become a gem.SparseLiteral when implemented). + # Rows (i) are number of nodes/dual functionals. + # Columns (k) are unique points to evaluate. + # Higher indices (*cmp) are tensor indices of the weights when weights + # are tensor valued. Q = {} - for i, dual in enumerate(dual_basis): + for i, dual in enumerate(fiat_dual_basis): point_dict = dual.get_point_dict() pts = tuple(sorted(point_dict.keys())) kstart, kend = seen[pts] for p, k in zip(pts, range(kstart, kend)): for weight, cmp in point_dict[p]: Q[(i, k, *cmp)] = weight - - x = PointSet(allpts) if all(len(set(key)) == 1 and np.isclose(weight, 1) and len(key) == 2 for key, weight in Q.items()): - # Identity, is this the most general case? + # Identity matrix Q can be expressed symbolically extents = tuple(map(max, zip(*Q.keys()))) js = tuple(gem.Index(extent=e+1) for e in extents) assert len(js) == 2 @@ -229,6 +226,7 @@ def dual_basis(self): # automatically convert a dictionary of keys internally. Q = gem.Literal(sparse.as_coo(Q).todense()) + # Return Q with x.indices already a free index for the consumer to use assert len(x.indices) == 1 assert Q.shape[1] == x.indices[0].extent i, *js = gem.indices(len(Q.shape) - 1) diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index cf4c7c39f..76291f0f4 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -5,6 +5,7 @@ import gem from gem.interpreter import evaluate +from gem.optimise import delta_elimination, sum_factorise, traverse_product from gem.utils import cached_property from finat.quadrature import make_quadrature @@ -149,27 +150,32 @@ def dual_basis(self): '''Return a dual evaluation gem weight tensor Q and point set x to dual evaluate a function fn at. - The general dual evaluation is then Q * fn(x). - - Note that the contraction index of the point set x is indexed out of Q - to avoid confusion when trying index it out of Q later in - dual_evaluation. + The general dual evaluation is then Q * fn(x) (the contraction of Q + with fn(x) along the the indices of x and any shape introduced by fn). If the dual weights are scalar then Q, for a general scalar FIAT element, is a matrix with dimensions - (num_nodes, num_points) - where num_points is made a free index that match the free index of x. + + .. code-block:: text + + (num_nodes, num_points) If the dual weights are tensor valued then Q, for a general tensor valued FIAT element, is a tensor with dimensions - (num_nodes, num_points, dual_weight_shape[0], ..., dual_weight_shape[n]) - where num_points made a free index that matches the free index of x. + + .. code-block:: text + + (num_nodes, num_points, dual_weight_shape[0], ..., dual_weight_shape[n]) If the dual basis is of a tensor product or FlattenedDimensions element with N factors then Q in general is a tensor with dimensions - (num_nodes_factor1, ..., num_nodes_factorN, - num_points_factor1, ..., num_points_factorN, - dual_weight_shape[0], ..., dual_weight_shape[n]) + + .. code-block:: text + + (num_nodes_factor1, ..., num_nodes_factorN, + num_points_factor1, ..., num_points_factorN, + dual_weight_shape[0], ..., dual_weight_shape[n]) + where num_points_factorX are made free indices that match the free indices of x (which is now a TensorPointSet). @@ -177,39 +183,58 @@ def dual_basis(self): (S1, S2, ..., Sn) then the tensor element tQ is constructed from the base element's Q by taking the outer product with appropriately sized identity matrices: - tQ = Q ⊗ 𝟙ₛ₁ ⊗ 𝟙ₛ₂ ⊗ ... ⊗ 𝟙ₛₙ + + .. code-block:: text + + tQ = Q ⊗ 𝟙ₛ₁ ⊗ 𝟙ₛ₂ ⊗ ... ⊗ 𝟙ₛₙ + + .. note:: + + When Q is returned, the contraction indices of the point set are + already free indices rather than being left in its shape (as either + ``num_points`` or ``num_points_factorX``). This is to avoid index + labelling confusion when performing the dual evaluation + contraction. ''' raise NotImplementedError( f"Dual basis not defined for element {type(self).__name__}" ) def dual_evaluation(self, fn): - '''Return code for performing the dual basis evaluation at the nodes of - the reference element. Currently only works for non-derivatives (not - implemented) and flat elements (implemented in TensorFiniteElement). + '''Get a GEM expression for performing the dual basis evaluation at + the nodes of the reference element. Currently only works for + non-derivatives (not implemented) and flat elements (tensor elements + are implemented in :class:`TensorFiniteElement`) :param fn: Callable representing the function to dual evaluate. Callable should take in an :class:`AbstractPointSet` and return a GEM expression for evaluation of the function at those points. - :returns: A tuple (dual_evaluation_indexed_sum, basis_indices) + :returns: A tuple ``(dual_evaluation_gem_expression, basis_indices)`` + where the given ``basis_indices`` are those needed to form a + return expression for the code which is compiled from + ``dual_evaluation_gem_expression`` (alongside any argument + multiindices already encoded within ``fn``) ''' Q, x = self.dual_basis expr = fn(x) - # Apply sum factorisation and delta elimination to the - # expression - expr = gem.optimise.contraction(expr) - # NOTE: any shape indices in the expression are because the expression - # is tensor valued. + # Apply targeted sum factorisation and delta elimination to + # the expression + sum_indices, factors = delta_elimination(*traverse_product(expr)) + expr = sum_factorise(sum_indices, factors) + # NOTE: any shape indices in the expression are because the + # expression is tensor valued. assert expr.shape == Q.shape[len(Q.shape)-len(expr.shape):] shape_indices = gem.indices(len(expr.shape)) basis_indices = gem.indices(len(Q.shape) - len(expr.shape)) Qi = Q[basis_indices + shape_indices] expri = expr[shape_indices] evaluation = gem.IndexSum(Qi * expri, x.indices + shape_indices) - # Don't want to factorise over the shape indices in the - # contraction, so exclude those from the optimisation routines. + # Now we want to factorise over the new contraction with x, + # ignoring any shape indices to avoid hitting the sum- + # factorisation index limit (this is a bit of a hack). + # Really need to do a more targeted job here. evaluation = gem.optimise.contraction(evaluation, shape_indices) return evaluation, basis_indices diff --git a/finat/tensorfiniteelement.py b/finat/tensorfiniteelement.py index ed922c13b..1bd3d72da 100644 --- a/finat/tensorfiniteelement.py +++ b/finat/tensorfiniteelement.py @@ -3,6 +3,7 @@ import numpy import gem +from gem.optimise import delta_elimination, sum_factorise, traverse_product from finat.finiteelementbase import FiniteElementBase @@ -147,11 +148,12 @@ def dual_basis(self): def dual_evaluation(self, fn): tQ, x = self.dual_basis expr = fn(x) - - gem.optimise.contraction(expr) - - # NOTE: any shape indices in the expression are because the expression - # is tensor valued. + # Apply targeted sum factorisation and delta elimination to + # the expression + sum_indices, factors = delta_elimination(*traverse_product(expr)) + expr = sum_factorise(sum_indices, factors) + # NOTE: any shape indices in the expression are because the + # expression is tensor valued. assert expr.shape == self.value_shape scalar_i = self.base_element.get_indices() @@ -167,6 +169,9 @@ def dual_evaluation(self, fn): tQi = tQ[index_ordering] expri = expr[tensor_i + scalar_vi] evaluation = gem.IndexSum(tQi * expri, x.indices + scalar_vi + tensor_i) + # This doesn't work perfectly, the resulting code doesn't have + # a minimal memory footprint, although the operation count + # does appear to be minimal. evaluation = gem.optimise.contraction(evaluation) return evaluation, scalar_i + tensor_vi From bd9a9867a2526d0a849c14450ad9d897e25855d2 Mon Sep 17 00:00:00 2001 From: Reuben Nixon-Hill Date: Fri, 20 Aug 2021 17:28:31 +0100 Subject: [PATCH 673/749] QuadratureElement: implement dual_basis for all schemes Q is just an outer product of identity matrices and the evaluation points are the quadrature points. --- finat/quadrature_element.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/finat/quadrature_element.py b/finat/quadrature_element.py index 0ddd101c1..1e8b46d83 100644 --- a/finat/quadrature_element.py +++ b/finat/quadrature_element.py @@ -126,14 +126,12 @@ def point_evaluation(self, order, refcoords, entity=None): @property def dual_basis(self): ps = self._rule.point_set - Q = gem.Literal([self._rule.weights]) - # Index out ps.indices from Q - assert len(ps.indices) == 1 - assert Q.shape[1] == ps.indices[0].extent - shape_indices = tuple(gem.Index(extent=s) for s in Q.shape) - Q = gem.ComponentTensor( - gem.Indexed(Q, (shape_indices[0],) + ps.indices + shape_indices[2:]), - (shape_indices[0],) + shape_indices[2:]) + multiindex = self.get_indices() + # Evaluation matrix is just an outer product of identity + # matrices, evaluation points are just the quadrature points. + Q = reduce(gem.Product, (gem.Delta(q, r) + for q, r in zip(ps.indices, multiindex))) + Q = gem.ComponentTensor(Q, multiindex) return Q, ps @property From f71ac9d508e713aa538d46ac52e04fd85a33939e Mon Sep 17 00:00:00 2001 From: Reuben Nixon-Hill Date: Fri, 20 Aug 2021 18:07:13 +0100 Subject: [PATCH 674/749] remove sparse lib dependency directly build dense matrix from sparse representation --- finat/fiat_elements.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index b759e2a1e..574c64966 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -5,8 +5,6 @@ import FIAT from FIAT.polynomial_set import mis, form_matrix_product -import sparse - import gem from finat.finiteelementbase import FiniteElementBase @@ -224,7 +222,15 @@ def dual_basis(self): else: # temporary until sparse literals are implemented in GEM which will # automatically convert a dictionary of keys internally. - Q = gem.Literal(sparse.as_coo(Q).todense()) + # TODO the below is unnecessarily slow and would be sped up + # significantly by building Q in a COO format rather than DOK (i.e. + # storing coords and associated data in (nonzeros, entries) shaped + # numpy arrays) to take advantage of numpy multiindexing + Qshape = tuple(s + 1 for s in map(max, *Q)) + Qdense = np.zeros(Qshape) + for idx, value in Q.items(): + Qdense[idx] = value + Q = gem.Literal(Qdense) # Return Q with x.indices already a free index for the consumer to use assert len(x.indices) == 1 From 405c3e495b07c341a4eaebbaa7fa6709773993d3 Mon Sep 17 00:00:00 2001 From: Reuben Nixon-Hill Date: Mon, 23 Aug 2021 12:23:13 +0100 Subject: [PATCH 675/749] dual_basis: make a cached property There is no need to remake it each time --- finat/fiat_elements.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 574c64966..94408c872 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -6,6 +6,7 @@ from FIAT.polynomial_set import mis, form_matrix_product import gem +from gem.utils import cached_property from finat.finiteelementbase import FiniteElementBase from finat.point_set import PointSet @@ -174,8 +175,12 @@ def point_evaluation(self, order, refcoords, entity=None): # Dispatch on FIAT element class return point_evaluation(self._element, order, refcoords, (entity_dim, entity_i)) - @property - def dual_basis(self): + @cached_property + def _dual_basis(self): + # Return the numerical part of the dual basis, this split is + # needed because the dual_basis itself can't produce the same + # point set over and over in case it is used multiple times + # (in for example a tensorproductelement). fiat_dual_basis = self._element.dual_basis() seen = dict() allpts = [] @@ -192,7 +197,6 @@ def dual_basis(self): kend = kstart + len(pts) seen[pts] = kstart, kend allpts.extend(pts) - x = PointSet(allpts) # Build Q. # Q is a tensor of weights (of total rank R) to contract with a unique # vector of points to evaluate at, giving a tensor (of total rank R-1) @@ -227,12 +231,21 @@ def dual_basis(self): # storing coords and associated data in (nonzeros, entries) shaped # numpy arrays) to take advantage of numpy multiindexing Qshape = tuple(s + 1 for s in map(max, *Q)) - Qdense = np.zeros(Qshape) + Qdense = np.zeros(Qshape, dtype=np.float64) for idx, value in Q.items(): Qdense[idx] = value Q = gem.Literal(Qdense) + return Q, np.asarray(allpts) - # Return Q with x.indices already a free index for the consumer to use + @property + def dual_basis(self): + # Return Q with x.indices already a free index for the + # consumer to use + # expensive numerical extraction is done once per element + # instance, but the point set must be created every time we + # build the dual. + Q, pts = self._dual_basis + x = PointSet(pts) assert len(x.indices) == 1 assert Q.shape[1] == x.indices[0].extent i, *js = gem.indices(len(Q.shape) - 1) From 420ef401fa71e668407e96d5c59d4e46fc2eae8f Mon Sep 17 00:00:00 2001 From: Reuben Nixon-Hill Date: Tue, 24 Aug 2021 12:27:26 +0100 Subject: [PATCH 676/749] fiat: Error if dual_basis uses Functional.deriv_dict Also document appropriately --- finat/fiat_elements.py | 2 ++ finat/finiteelementbase.py | 13 ++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 94408c872..9662a386f 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -189,6 +189,8 @@ def _dual_basis(self): # point one by one, but most of the redundancy comes from # multiple functionals using the same quadrature rule. for dual in fiat_dual_basis: + if len(dual.deriv_dict) != 0: + raise NotImplementedError("FIAT dual bases with derivative nodes represented via a ``Functional.deriv_dict`` property do not currently have a FInAT dual basis") pts = dual.get_point_dict().keys() pts = tuple(sorted(pts)) # need this for determinism if pts not in seen: diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 76291f0f4..5d4964e69 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -195,6 +195,13 @@ def dual_basis(self): ``num_points`` or ``num_points_factorX``). This is to avoid index labelling confusion when performing the dual evaluation contraction. + + .. note:: + + FIAT element dual bases are built from their ``Functional.pt_dict`` + properties. Therefore any FIAT dual bases with derivative nodes + represented via a ``Functional.deriv_dict`` property does not + currently have a FInAT dual basis. ''' raise NotImplementedError( f"Dual basis not defined for element {type(self).__name__}" @@ -202,9 +209,9 @@ def dual_basis(self): def dual_evaluation(self, fn): '''Get a GEM expression for performing the dual basis evaluation at - the nodes of the reference element. Currently only works for - non-derivatives (not implemented) and flat elements (tensor elements - are implemented in :class:`TensorFiniteElement`) + the nodes of the reference element. Currently only works for flat + elements: tensor elements are implemented in + :class:`TensorFiniteElement`. :param fn: Callable representing the function to dual evaluate. Callable should take in an :class:`AbstractPointSet` and From 84e248dbd4045f0714e343faec2de3670ae1f766 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Tue, 31 Aug 2021 11:50:54 +0100 Subject: [PATCH 677/749] tfe: Implement entity_dofs --- finat/tensorfiniteelement.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/finat/tensorfiniteelement.py b/finat/tensorfiniteelement.py index 1bd3d72da..102a88398 100644 --- a/finat/tensorfiniteelement.py +++ b/finat/tensorfiniteelement.py @@ -1,9 +1,11 @@ from functools import reduce +from itertools import chain import numpy import gem from gem.optimise import delta_elimination, sum_factorise, traverse_product +from gem.utils import cached_property from finat.finiteelementbase import FiniteElementBase @@ -63,8 +65,31 @@ def degree(self): def formdegree(self): return self._base_element.formdegree + @cached_property + def _entity_dofs(self): + dofs = {} + base_dofs = self._base_element.entity_dofs() + ndof = int(numpy.prod(self._shape, dtype=int)) + + def expand(dofs): + dofs = tuple(dofs) + if self._transpose: + space_dim = self._base_element.space_dimension() + # Components stride by space dimension of base element + iterable = ((v + i*space_dim for v in dofs) + for i in range(ndof)) + else: + # Components packed together + iterable = (range(v*ndof, (v+1)*ndof) for v in dofs) + yield from chain.from_iterable(iterable) + + for dim in self.cell.get_topology().keys(): + dofs[dim] = dict((k, list(expand(d))) + for k, d in base_dofs[dim].items()) + return dofs + def entity_dofs(self): - raise NotImplementedError("No one uses this!") + return self._entity_dofs def space_dimension(self): return int(numpy.prod(self.index_shape)) From b9d014c4eddb35f3c2c37ea477b6249d8997b725 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Fri, 3 Sep 2021 08:41:02 +0100 Subject: [PATCH 678/749] Give discontinuous elements a dual --- finat/discontinuous.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/finat/discontinuous.py b/finat/discontinuous.py index a608c425f..932b80b9c 100644 --- a/finat/discontinuous.py +++ b/finat/discontinuous.py @@ -57,6 +57,10 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): def point_evaluation(self, order, refcoords, entity=None): return self.element.point_evaluation(order, refcoords, entity) + @property + def dual_basis(self): + return self.element.dual_basis + @property def mapping(self): return self.element.mapping From d58de2bcbb47f272c26922077ca2027e11d3adc6 Mon Sep 17 00:00:00 2001 From: ksagiyam Date: Wed, 21 Apr 2021 01:12:18 +0100 Subject: [PATCH 679/749] orientations: define entity_orientations() method for FlattenedDimensions, EnrichedElement, FiatElement, HDivElement, HCurlElement, TensorProductElement --- finat/cube.py | 6 +++++- finat/discontinuous.py | 10 ++++++++++ finat/enriched.py | 28 ++++++++++++++++++++++++++++ finat/fiat_elements.py | 4 ++++ finat/finiteelementbase.py | 25 +++++++++++++++++++++++++ finat/hdivcurl.py | 4 ++++ finat/tensor_product.py | 38 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 114 insertions(+), 1 deletion(-) diff --git a/finat/cube.py b/finat/cube.py index 2a61aeabd..c6b478b28 100644 --- a/finat/cube.py +++ b/finat/cube.py @@ -1,7 +1,7 @@ from __future__ import absolute_import, print_function, division from FIAT.reference_element import UFCHexahedron, UFCQuadrilateral -from FIAT.reference_element import compute_unflattening_map, flatten_entities +from FIAT.reference_element import compute_unflattening_map, flatten_entities, flatten_permutations from FIAT.tensor_product import FlattenedDimensions as FIAT_FlattenedDimensions from gem.utils import cached_property @@ -49,6 +49,10 @@ def _entity_support_dofs(self): def entity_dofs(self): return self._entity_dofs + @cached_property + def entity_permutations(self): + return flatten_permutations(self.product.entity_permutations) + def space_dimension(self): return self.product.space_dimension() diff --git a/finat/discontinuous.py b/finat/discontinuous.py index 932b80b9c..283f2e198 100644 --- a/finat/discontinuous.py +++ b/finat/discontinuous.py @@ -36,6 +36,16 @@ def _entity_dofs(self): def entity_dofs(self): return self._entity_dofs + @cached_property + def entity_permutations(self): + # Return entity_permutations of the base finite element if it only + # has cell degrees of freedom; otherwise entity_permutations is not + # yet implemented for DiscontinuousElement. + if self.element.entity_dofs() == self.element.entity_closure_dofs(): + return self.element.entity_permutations + else: + raise NotImplementedError(f"entity_permutations not yet implemented for a general {type(self)}") + def space_dimension(self): return self.element.space_dimension() diff --git a/finat/enriched.py b/finat/enriched.py index 4ff89fb12..ab9c84720 100644 --- a/finat/enriched.py +++ b/finat/enriched.py @@ -47,6 +47,12 @@ def entity_dofs(self): return concatenate_entity_dofs(self.cell, self.elements, methodcaller("entity_dofs")) + @cached_property + def entity_permutations(self): + '''Return the map of topological entities to the map of + orientations to permutation lists for the finite element''' + return concatenate_entity_permutations(self.elements) + @cached_property def _entity_support_dofs(self): return concatenate_entity_dofs(self.cell, self.elements, @@ -166,3 +172,25 @@ def concatenate_entity_dofs(ref_el, elements, method): for ent, off in dofs.items(): entity_dofs[dim][ent] += list(map(partial(add, offsets[i]), off)) return entity_dofs + + +def concatenate_entity_permutations(elements): + """For each dimension, for each entity, and for each possible + entity orientation, collect the DoF permutation lists from + entity_permutations dicts of elements and concatenate them. + + :arg elements: subelements whose DoF permutation lists are concatenated + :returns: entity_permutation dict of the :class:`EnrichedElement` object + composed of elements. + """ + permutations = {} + for element in elements: + for dim, e_o_p_map in element.entity_permutations.items(): + dim_permutations = permutations.setdefault(dim, {}) + for e, o_p_map in e_o_p_map.items(): + e_dim_permutations = dim_permutations.setdefault(e, {}) + for o, p in o_p_map.items(): + o_e_dim_permutations = e_dim_permutations.setdefault(o, []) + offset = len(o_e_dim_permutations) + o_e_dim_permutations += list(offset + q for q in p) + return permutations diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 9662a386f..e6588cff4 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -70,6 +70,10 @@ def entity_dofs(self): def entity_closure_dofs(self): return self._element.entity_closure_dofs() + @property + def entity_permutations(self): + return self._element.entity_permutations() + def space_dimension(self): return self._element.space_dimension() diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 5d4964e69..9a430ab37 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -33,6 +33,31 @@ def entity_dofs(self): '''Return the map of topological entities to degrees of freedom for the finite element.''' + @property + def entity_permutations(self): + '''Returns a nested dictionary that gives, for each dimension, + for each entity, and for each possible entity orientation, the + DoF permutation array that maps the entity local DoF ordering + to the canonical global DoF ordering. + + The entity permutations `dict` for the degree 4 Lagrange finite + element on the interval, for instance, is given by: + + .. code-block:: python3 + + {0: {0: {0: [0]}, + 1: {0: [0]}}, + 1: {0: {0: [0, 1, 2], + 1: [2, 1, 0]}}} + + Note that there are two entities on dimension ``0`` (vertices), + each of which has only one possible orientation, while there is + a single entity on dimension ``1`` (interval), which has two + possible orientations representing non-reflected and reflected + intervals. + ''' + raise NotImplementedError(f"entity_permutations not yet implemented for {type(self)}") + @cached_property def _entity_closure_dofs(self): # Compute the nodes on the closure of each sub_entity. diff --git a/finat/hdivcurl.py b/finat/hdivcurl.py index c184c8162..bf7ebdb3b 100644 --- a/finat/hdivcurl.py +++ b/finat/hdivcurl.py @@ -33,6 +33,10 @@ def degree(self): def entity_dofs(self): return self.wrappee.entity_dofs() + @property + def entity_permutations(self): + return self.wrappee.entity_permutations + def entity_closure_dofs(self): return self.wrappee.entity_closure_dofs() diff --git a/finat/tensor_product.py b/finat/tensor_product.py index 60be75fe6..9482cad5d 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -55,6 +55,10 @@ def _entity_support_dofs(self): def entity_dofs(self): return self._entity_dofs + @cached_property + def entity_permutations(self): + return compose_permutations(self.factors) + def space_dimension(self): return numpy.prod([fe.space_dimension() for fe in self.factors]) @@ -208,6 +212,40 @@ def productise(factors, method): return dofs +def compose_permutations(factors): + """For the :class:`TensorProductElement` object composed of factors, + construct, for each dimension tuple, for each entity, and for each possible + entity orientation combination, the DoF permutation list. + + :arg factors: element factors. + :returns: entity_permutation dict of the :class:`TensorProductElement` object + composed of factors. + """ + permutations = {} + for dim in product(*[fe.cell.get_topology().keys() + for fe in factors]): + dim_permutations = [] + e_o_p_maps = [fe.entity_permutations[d] + for fe, d in zip(factors, dim)] + for e_tuple in product(*[sorted(e_o_p_map) for e_o_p_map in e_o_p_maps]): + o_p_maps = [e_o_p_map[e] for e_o_p_map, e in zip(e_o_p_maps, e_tuple)] + o_tuple_perm_map = {} + for o_tuple in product(*[o_p_map.keys() for o_p_map in o_p_maps]): + ps = [o_p_map[o] for o_p_map, o in zip(o_p_maps, o_tuple)] + shape = tuple(len(p) for p in ps) + size = numpy.prod(shape) + if size == 0: + o_tuple_perm_map[o_tuple] = [] + else: + a = numpy.arange(size).reshape(shape) + for i, p in enumerate(ps): + a = a.swapaxes(0, i)[p, :].swapaxes(0, i) + o_tuple_perm_map[o_tuple] = a.reshape(-1).tolist() + dim_permutations.append((e_tuple, o_tuple_perm_map)) + permutations[dim] = dict(enumerate(v for k, v in sorted(dim_permutations))) + return permutations + + def factor_point_set(product_cell, product_dim, point_set): """Factors a point set for the product element into a point sets for each subelement. From 6fc07ad9c59c6dc12ce584ed81955c9daa2d4164 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Thu, 7 Oct 2021 19:13:32 +0100 Subject: [PATCH 680/749] Fix O(N^2) behaviour in flop counting We need to turn the list of temporaries into a set since we look up in it all the time. --- gem/flop_count.py | 84 +++++++++++++++++++++++------------------------ 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/gem/flop_count.py b/gem/flop_count.py index 57577e34b..934c3386e 100644 --- a/gem/flop_count.py +++ b/gem/flop_count.py @@ -11,61 +11,61 @@ @singledispatch -def statement(tree, parameters): +def statement(tree, temporaries): raise NotImplementedError @statement.register(imp.Block) -def statement_block(tree, parameters): - flops = sum(statement(child, parameters) for child in tree.children) +def statement_block(tree, temporaries): + flops = sum(statement(child, temporaries) for child in tree.children) return flops @statement.register(imp.For) -def statement_for(tree, parameters): +def statement_for(tree, temporaries): extent = tree.index.extent assert extent is not None child, = tree.children - flops = statement(child, parameters) + flops = statement(child, temporaries) return flops * extent @statement.register(imp.Initialise) -def statement_initialise(tree, parameters): +def statement_initialise(tree, temporaries): return 0 @statement.register(imp.Accumulate) -def statement_accumulate(tree, parameters): - flops = expression_flops(tree.indexsum.children[0], parameters) +def statement_accumulate(tree, temporaries): + flops = expression_flops(tree.indexsum.children[0], temporaries) return flops + 1 @statement.register(imp.Return) -def statement_return(tree, parameters): - flops = expression_flops(tree.expression, parameters) +def statement_return(tree, temporaries): + flops = expression_flops(tree.expression, temporaries) return flops + 1 @statement.register(imp.ReturnAccumulate) -def statement_returnaccumulate(tree, parameters): - flops = expression_flops(tree.indexsum.children[0], parameters) +def statement_returnaccumulate(tree, temporaries): + flops = expression_flops(tree.indexsum.children[0], temporaries) return flops + 1 @statement.register(imp.Evaluate) -def statement_evaluate(tree, parameters): - flops = expression_flops(tree.expression, parameters, top=True) +def statement_evaluate(tree, temporaries): + flops = expression_flops(tree.expression, temporaries, top=True) return flops @singledispatch -def flops(expr, parameters): +def flops(expr, temporaries): raise NotImplementedError(f"Don't know how to count flops of {type(expr)}") @flops.register(gem.Failure) -def flops_failure(expr, parameters): +def flops_failure(expr, temporaries): raise ValueError("Not expecting a Failure node") @@ -76,7 +76,7 @@ def flops_failure(expr, parameters): @flops.register(gem.Literal) @flops.register(gem.Index) @flops.register(gem.VariableIndex) -def flops_zero(expr, parameters): +def flops_zero(expr, temporaries): # Initial set up of these Gem nodes are of 0 floating point operations. return 0 @@ -85,22 +85,22 @@ def flops_zero(expr, parameters): @flops.register(gem.LogicalAnd) @flops.register(gem.LogicalOr) @flops.register(gem.ListTensor) -def flops_zeroplus(expr, parameters): +def flops_zeroplus(expr, temporaries): # These nodes contribute 0 floating point operations, but their children may not. - return 0 + sum(expression_flops(child, parameters) + return 0 + sum(expression_flops(child, temporaries) for child in expr.children) @flops.register(gem.Product) -def flops_product(expr, parameters): +def flops_product(expr, temporaries): # Multiplication by -1 is not a flop. a, b = expr.children if isinstance(a, gem.Literal) and a.value == -1: - return expression_flops(b, parameters) + return expression_flops(b, temporaries) elif isinstance(b, gem.Literal) and b.value == -1: - return expression_flops(a, parameters) + return expression_flops(a, temporaries) else: - return 1 + sum(expression_flops(child, parameters) + return 1 + sum(expression_flops(child, temporaries) for child in expr.children) @@ -110,15 +110,15 @@ def flops_product(expr, parameters): @flops.register(gem.MathFunction) @flops.register(gem.MinValue) @flops.register(gem.MaxValue) -def flops_oneplus(expr, parameters): - return 1 + sum(expression_flops(child, parameters) +def flops_oneplus(expr, temporaries): + return 1 + sum(expression_flops(child, temporaries) for child in expr.children) @flops.register(gem.Power) -def flops_power(expr, parameters): +def flops_power(expr, temporaries): base, exponent = expr.children - base_flops = expression_flops(base, parameters) + base_flops = expression_flops(base, temporaries) if isinstance(exponent, gem.Literal): exponent = exponent.value if exponent > 0 and exponent == math.floor(exponent): @@ -130,59 +130,59 @@ def flops_power(expr, parameters): @flops.register(gem.Conditional) -def flops_conditional(expr, parameters): - condition, then, else_ = (expression_flops(child, parameters) +def flops_conditional(expr, temporaries): + condition, then, else_ = (expression_flops(child, temporaries) for child in expr.children) return condition + max(then, else_) @flops.register(gem.Indexed) @flops.register(gem.FlexiblyIndexed) -def flops_indexed(expr, parameters): - aggregate = sum(expression_flops(child, parameters) +def flops_indexed(expr, temporaries): + aggregate = sum(expression_flops(child, temporaries) for child in expr.children) # Average flops per entry return aggregate / numpy.product(expr.children[0].shape, dtype=int) @flops.register(gem.IndexSum) -def flops_indexsum(expr, parameters): +def flops_indexsum(expr, temporaries): raise ValueError("Not expecting IndexSum") @flops.register(gem.Inverse) -def flops_inverse(expr, parameters): +def flops_inverse(expr, temporaries): n, _ = expr.shape # 2n^3 + child flop count - return 2*n**3 + sum(expression_flops(child, parameters) + return 2*n**3 + sum(expression_flops(child, temporaries) for child in expr.children) @flops.register(gem.Solve) -def flops_solve(expr, parameters): +def flops_solve(expr, temporaries): n, m = expr.shape # 2mn + inversion cost of A + children flop count - return 2*n*m + 2*n**3 + sum(expression_flops(child, parameters) + return 2*n*m + 2*n**3 + sum(expression_flops(child, temporaries) for child in expr.children) @flops.register(gem.ComponentTensor) -def flops_componenttensor(expr, parameters): +def flops_componenttensor(expr, temporaries): raise ValueError("Not expecting ComponentTensor") -def expression_flops(expression, parameters, top=False): +def expression_flops(expression, temporaries, top=False): """An approximation to flops required for each expression. :arg expression: GEM expression. - :arg parameters: Useful miscellaneous information. + :arg temporaries: Expressions that are assigned to temporaries :arg top: are we at the root? :returns: flop count for the expression """ - if not top and expression in parameters.temporaries: + if not top and expression in temporaries: return 0 else: - return flops(expression, parameters) + return flops(expression, temporaries) def count_flops(impero_c): @@ -192,6 +192,6 @@ def count_flops(impero_c): :returns: approximate flop count for the tree. """ try: - return statement(impero_c.tree, impero_c) + return statement(impero_c.tree, set(impero_c.temporaries)) except (ValueError, NotImplementedError): return 0 From d90ed7167f2d2ba10fed31011cb41915f40628cd Mon Sep 17 00:00:00 2001 From: Connor Ward Date: Thu, 4 Nov 2021 17:54:21 +0000 Subject: [PATCH 681/749] Add is_mixed property to EnrichedElement --- finat/enriched.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/finat/enriched.py b/finat/enriched.py index ab9c84720..26e6fe4ee 100644 --- a/finat/enriched.py +++ b/finat/enriched.py @@ -74,10 +74,7 @@ def value_shape(self): @cached_property def fiat_equivalent(self): - # Avoid circular import dependency - from finat.mixed import MixedSubElement - - if all(isinstance(e, MixedSubElement) for e in self.elements): + if self.is_mixed: # EnrichedElement is actually a MixedElement return FIAT.MixedElement([e.element.fiat_equivalent for e in self.elements], ref_el=self.cell) @@ -85,6 +82,13 @@ def fiat_equivalent(self): return FIAT.EnrichedElement(*(e.fiat_equivalent for e in self.elements)) + @cached_property + def is_mixed(self): + # Avoid circular import dependency + from finat.mixed import MixedSubElement + + return all(isinstance(e, MixedSubElement) for e in self.elements) + def _compose_evaluations(self, results): keys, = set(map(frozenset, results)) From b0e2c5e7a2483935d61f7809d3dd1e2d83e8b05e Mon Sep 17 00:00:00 2001 From: Connor Ward Date: Wed, 17 Nov 2021 14:27:11 +0000 Subject: [PATCH 682/749] Add Real element --- finat/__init__.py | 2 +- finat/fiat_elements.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/finat/__init__.py b/finat/__init__.py index 9aa8b0fa7..7fa5bef5e 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -1,6 +1,6 @@ from .fiat_elements import Bernstein # noqa: F401 from .fiat_elements import Bubble, CrouzeixRaviart, DiscontinuousTaylor # noqa: F401 -from .fiat_elements import Lagrange, DiscontinuousLagrange # noqa: F401 +from .fiat_elements import Lagrange, DiscontinuousLagrange, Real # noqa: F401 from .fiat_elements import DPC, Serendipity # noqa: F401 from .fiat_elements import BrezziDouglasMarini, BrezziDouglasFortinMarini # noqa: F401 from .fiat_elements import Nedelec, NedelecSecondKind, RaviartThomas # noqa: F401 diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index e6588cff4..c286676aa 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -432,6 +432,10 @@ def __init__(self, cell, degree): super(DiscontinuousLagrange, self).__init__(FIAT.DiscontinuousLagrange(cell, degree)) +class Real(DiscontinuousLagrange): + ... + + class Serendipity(ScalarFiatElement): def __init__(self, cell, degree): super(Serendipity, self).__init__(FIAT.Serendipity(cell, degree)) From bf3520b6c191837c8603d49522573d2918e13ce0 Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Tue, 7 Dec 2021 15:14:38 +0000 Subject: [PATCH 683/749] Added wrapper for FIAT.FDMElement --- finat/__init__.py | 2 +- finat/spectral.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/finat/__init__.py b/finat/__init__.py index 9aa8b0fa7..04258e4cb 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -16,7 +16,7 @@ from .aw import ArnoldWintherNC # noqa: F401 from .trace import HDivTrace # noqa: F401 from .direct_serendipity import DirectSerendipity # noqa: F401 -from .spectral import GaussLobattoLegendre, GaussLegendre # noqa: F401 +from .spectral import GaussLobattoLegendre, GaussLegendre, FDMElement # noqa: F401 from .tensorfiniteelement import TensorFiniteElement # noqa: F401 from .tensor_product import TensorProductElement # noqa: F401 from .cube import FlattenedDimensions # noqa: F401 diff --git a/finat/spectral.py b/finat/spectral.py index f8fec09b8..8c6753e00 100644 --- a/finat/spectral.py +++ b/finat/spectral.py @@ -62,3 +62,11 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): r, = self.get_indices() result[(0,) * spatial_dim] = gem.ComponentTensor(gem.Delta(q, r), (r,)) return result + + +class FDMElement(ScalarFiatElement): + """1D (dis)continuous element with FDM shape functions.""" + + def __init__(self, cell, degree, formdegree=0): + fiat_element = FIAT.FDMElement(cell, degree, formdegree) + super(FDMElement, self).__init__(fiat_element) From c7078bb003cbc05efb7e6e87e3a83f4ee85c2828 Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Mon, 17 Jan 2022 13:56:09 +0000 Subject: [PATCH 684/749] add FDMHermite element diagonalizing the biharmonic operator in 1D and remove the formdegree argument --- finat/__init__.py | 2 +- finat/spectral.py | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/finat/__init__.py b/finat/__init__.py index 04258e4cb..006d71e32 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -16,7 +16,7 @@ from .aw import ArnoldWintherNC # noqa: F401 from .trace import HDivTrace # noqa: F401 from .direct_serendipity import DirectSerendipity # noqa: F401 -from .spectral import GaussLobattoLegendre, GaussLegendre, FDMElement # noqa: F401 +from .spectral import GaussLobattoLegendre, GaussLegendre, FDMLagrange, FDMHermite # noqa: F401 from .tensorfiniteelement import TensorFiniteElement # noqa: F401 from .tensor_product import TensorProductElement # noqa: F401 from .cube import FlattenedDimensions # noqa: F401 diff --git a/finat/spectral.py b/finat/spectral.py index 8c6753e00..d6e433251 100644 --- a/finat/spectral.py +++ b/finat/spectral.py @@ -64,9 +64,17 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): return result -class FDMElement(ScalarFiatElement): - """1D (dis)continuous element with FDM shape functions.""" +class FDMLagrange(ScalarFiatElement): + """1D CG element with FDM shape functions.""" - def __init__(self, cell, degree, formdegree=0): - fiat_element = FIAT.FDMElement(cell, degree, formdegree) - super(FDMElement, self).__init__(fiat_element) + def __init__(self, cell, degree): + fiat_element = FIAT.FDMLagrange(cell, degree) + super(FDMLagrange, self).__init__(fiat_element) + + +class FDMHermite(ScalarFiatElement): + """1D CG element with FDM shape functions.""" + + def __init__(self, cell, degree): + fiat_element = FIAT.FDMHermite(cell, degree) + super(FDMHermite, self).__init__(fiat_element) From a406a213578ecf398de85421b8e423672775c550 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Thu, 7 Apr 2022 19:03:46 +0100 Subject: [PATCH 685/749] gem: IndexError for invalid literal indices --- gem/gem.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gem/gem.py b/gem/gem.py index 570145459..675cf3dda 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -548,6 +548,8 @@ def __new__(cls, aggregate, multiindex): assert isinstance(index, IndexBase) if isinstance(index, Index): index.set_extent(extent) + elif isinstance(index, int) and not (0 <= index < extent): + raise IndexError("Invalid literal index") # Empty multiindex if not multiindex: From 8054098acce860ec5e42fdd53d906e9aa7e7064c Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Thu, 7 Apr 2022 19:07:12 +0100 Subject: [PATCH 686/749] optimise: New pass to constant fold literal Zeros Having removed eager production of symbolic Zeros from Literals we now need a pass to introduce them. --- gem/optimise.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/gem/optimise.py b/gem/optimise.py index 216df068a..19e7d0620 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -155,6 +155,35 @@ def remove_componenttensors(expressions): return [mapper(expression, ()) for expression in expressions] +@singledispatch +def _constant_fold_zero(node, self): + raise AssertionError("cannot handle type %s" % type(node)) + + +_constant_fold_zero.register(Node)(reuse_if_untouched) + + +@_constant_fold_zero.register(Literal) +def _constant_fold_zero_literal(node, self): + if (node.array == 0).all(): + # All zeros, make symbolic zero + return Zero(node.shape) + else: + return node + + +def constant_fold_zero(exprs): + """Produce symbolic zeros from Literals + + :arg exprs: An iterable of gem expressions. + :returns: A list of gem expressions where any Literal containing + only zeros is replaced by symbolic Zero of the appropriate + shape. + """ + mapper = Memoizer(_constant_fold_zero) + return [mapper(e) for e in exprs] + + def _select_expression(expressions, index): """Helper function to select an expression from a list of expressions with an index. This function expect sanitised input, From c8cbefa621a21e919de9c6ae8d8a6ad9e2680af0 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Thu, 7 Apr 2022 19:04:01 +0100 Subject: [PATCH 687/749] gem: Don't eagerly constant-fold literal Zeros gem.optimise.select_expression relies on every expression provided having the same structural shape. In certain circumstances (restricted elements on tensor product cells) eagerly producing symbolic zeros destroys this assumption. To work around this, we will constant fold later as a separate pass. Fixes #274. --- gem/gem.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 675cf3dda..270776890 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -211,11 +211,7 @@ class Literal(Constant): def __new__(cls, array): array = asarray(array) - if (array == 0).all(): - # All zeros, make symbolic zero - return Zero(array.shape) - else: - return super(Literal, cls).__new__(cls) + return super(Literal, cls).__new__(cls) def __init__(self, array): array = asarray(array) From 1e36990367fa91da3a276570d55b738daf21b50f Mon Sep 17 00:00:00 2001 From: ksagiyam Date: Tue, 1 Mar 2022 13:16:51 +0000 Subject: [PATCH 688/749] gem: add extract_type --- gem/gem.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 570145459..aa1951787 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -22,7 +22,7 @@ import numpy from numpy import asarray -from gem.node import Node as NodeBase +from gem.node import Node as NodeBase, traversal __all__ = ['Node', 'Identity', 'Literal', 'Zero', 'Failure', @@ -33,7 +33,7 @@ 'IndexSum', 'ListTensor', 'Concatenate', 'Delta', 'index_sum', 'partial_indexed', 'reshape', 'view', 'indices', 'as_gem', 'FlexiblyIndexed', - 'Inverse', 'Solve'] + 'Inverse', 'Solve', 'extract_type'] class NodeMeta(type): @@ -1041,3 +1041,8 @@ def as_gem(expr): return Literal(expr) else: raise ValueError("Do not know how to convert %r to GEM" % expr) + + +def extract_type(expressions, klass): + """Collects objects of type klass in expressions.""" + return tuple(node for node in traversal(expressions) if isinstance(node, klass)) From 63a75c51aa4b738029ba42d77115fbeb3b7ffde3 Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Wed, 13 Apr 2022 12:15:03 +0100 Subject: [PATCH 689/749] wrap discontinuous FDM elements --- finat/__init__.py | 2 +- finat/spectral.py | 20 ++++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/finat/__init__.py b/finat/__init__.py index 53e30ebc7..5200489d6 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -16,7 +16,7 @@ from .aw import ArnoldWintherNC # noqa: F401 from .trace import HDivTrace # noqa: F401 from .direct_serendipity import DirectSerendipity # noqa: F401 -from .spectral import GaussLobattoLegendre, GaussLegendre, FDMLagrange, FDMHermite # noqa: F401 +from .spectral import GaussLobattoLegendre, GaussLegendre, FDMLagrange, FDMHermite, FDMDiscontinuousH1, FDMDiscontinuousL2 # noqa: F401 from .tensorfiniteelement import TensorFiniteElement # noqa: F401 from .tensor_product import TensorProductElement # noqa: F401 from .cube import FlattenedDimensions # noqa: F401 diff --git a/finat/spectral.py b/finat/spectral.py index d6e433251..857308874 100644 --- a/finat/spectral.py +++ b/finat/spectral.py @@ -65,7 +65,7 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): class FDMLagrange(ScalarFiatElement): - """1D CG element with FDM shape functions.""" + """1D CG element with FDM shape functions and point evaluation BCs.""" def __init__(self, cell, degree): fiat_element = FIAT.FDMLagrange(cell, degree) @@ -73,8 +73,24 @@ def __init__(self, cell, degree): class FDMHermite(ScalarFiatElement): - """1D CG element with FDM shape functions.""" + """1D CG element with FDM shape functions, point evaluation BCs and derivative BCs.""" def __init__(self, cell, degree): fiat_element = FIAT.FDMHermite(cell, degree) super(FDMHermite, self).__init__(fiat_element) + + +class FDMDiscontinuousH1(ScalarFiatElement): + """1D DG element with FDM shape functions.""" + + def __init__(self, cell, degree): + fiat_element = FIAT.FDMDiscontinuousH1(cell, degree) + super(FDMDiscontinuousH1, self).__init__(fiat_element) + + +class FDMDiscontinuousL2(ScalarFiatElement): + """1D DG element with derivatives of FDM shape functions.""" + + def __init__(self, cell, degree): + fiat_element = FIAT.FDMDiscontinuousL2(cell, degree) + super(FDMDiscontinuousL2, self).__init__(fiat_element) From c3118599fe2c378f66297fc52bd654f683989d99 Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Thu, 12 May 2022 21:55:33 +0100 Subject: [PATCH 690/749] Rename FDM elements --- finat/__init__.py | 2 +- finat/spectral.py | 30 +++++++++++++++++++----------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/finat/__init__.py b/finat/__init__.py index 5200489d6..85092d9d7 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -16,7 +16,7 @@ from .aw import ArnoldWintherNC # noqa: F401 from .trace import HDivTrace # noqa: F401 from .direct_serendipity import DirectSerendipity # noqa: F401 -from .spectral import GaussLobattoLegendre, GaussLegendre, FDMLagrange, FDMHermite, FDMDiscontinuousH1, FDMDiscontinuousL2 # noqa: F401 +from .spectral import GaussLobattoLegendre, GaussLegendre, FDMLagrange, FDMDiscontinuousLagrange, FDMBrokenH1, FDMBrokenL2, FDMHermite # noqa: F401 from .tensorfiniteelement import TensorFiniteElement # noqa: F401 from .tensor_product import TensorProductElement # noqa: F401 from .cube import FlattenedDimensions # noqa: F401 diff --git a/finat/spectral.py b/finat/spectral.py index 857308874..0783aa4ee 100644 --- a/finat/spectral.py +++ b/finat/spectral.py @@ -72,25 +72,33 @@ def __init__(self, cell, degree): super(FDMLagrange, self).__init__(fiat_element) -class FDMHermite(ScalarFiatElement): - """1D CG element with FDM shape functions, point evaluation BCs and derivative BCs.""" +class FDMDiscontinuousLagrange(ScalarFiatElement): + """1D DG element with derivatives of FDM shape functions with point evaluation Bcs.""" def __init__(self, cell, degree): - fiat_element = FIAT.FDMHermite(cell, degree) - super(FDMHermite, self).__init__(fiat_element) + fiat_element = FIAT.FDMDiscontinuousLagrange(cell, degree) + super(FDMDiscontinuousLagrange, self).__init__(fiat_element) -class FDMDiscontinuousH1(ScalarFiatElement): - """1D DG element with FDM shape functions.""" +class FDMBrokenH1(ScalarFiatElement): + """1D Broken CG element with FDM shape functions.""" def __init__(self, cell, degree): - fiat_element = FIAT.FDMDiscontinuousH1(cell, degree) - super(FDMDiscontinuousH1, self).__init__(fiat_element) + fiat_element = FIAT.FDMBrokenH1(cell, degree) + super(FDMBrokenH1, self).__init__(fiat_element) -class FDMDiscontinuousL2(ScalarFiatElement): +class FDMBrokenL2(ScalarFiatElement): """1D DG element with derivatives of FDM shape functions.""" def __init__(self, cell, degree): - fiat_element = FIAT.FDMDiscontinuousL2(cell, degree) - super(FDMDiscontinuousL2, self).__init__(fiat_element) + fiat_element = FIAT.FDMBrokenL2(cell, degree) + super(FDMBrokenL2, self).__init__(fiat_element) + + +class FDMHermite(ScalarFiatElement): + """1D CG element with FDM shape functions, point evaluation BCs and derivative BCs.""" + + def __init__(self, cell, degree): + fiat_element = FIAT.FDMHermite(cell, degree) + super(FDMHermite, self).__init__(fiat_element) From d7c5f6d2da559e1206d00c19b956fc1d84ddbee3 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Fri, 3 Sep 2021 09:39:16 +0100 Subject: [PATCH 691/749] tensor_product: Fix shape of dual basis We can't just naively index the dual of the factors and outer product them because we need to split between index_shape and value_shape and then produce a final dual with (*index_shapes, *value_shapes). So grab some indices for each factor, product those, and then glue the new indices together in the correct order. --- finat/tensor_product.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/finat/tensor_product.py b/finat/tensor_product.py index 9482cad5d..332ddca5b 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -171,9 +171,16 @@ def dual_basis(self): # Outer product the dual bases of the factors qs, pss = zip(*(factor.dual_basis for factor in self.factors)) ps = TensorPointSet(pss) - qis = tuple(q[gem.indices(len(q.shape))] for q in qs) - indices = tuple(chain(*(q.index_ordering() for q in qis))) - Q = gem.ComponentTensor(reduce(gem.Product, qis), indices) + # Naming as _merge_evaluations above + alphas = [factor.get_indices() for factor in self.factors] + zetas = [factor.get_value_indices() for factor in self.factors] + # Index the factors by so that we can reshape into index-shape + # followed by value-shape + qis = [q[alpha + zeta] for q, alpha, zeta in zip(qs, alphas, zetas)] + Q = gem.ComponentTensor( + reduce(gem.Product, qis), + tuple(chain(*(alphas + zetas))) + ) return Q, ps @cached_property From cb9c7b1c906fdf972a3098ae3d8e01518ff6f121 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Fri, 3 Sep 2021 09:40:35 +0100 Subject: [PATCH 692/749] hdivcurl: Implement dual_basis We just need to take the wrappee's dual and promote the value_shape according to the relevant transform. --- finat/hdivcurl.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/finat/hdivcurl.py b/finat/hdivcurl.py index bf7ebdb3b..dce7b0247 100644 --- a/finat/hdivcurl.py +++ b/finat/hdivcurl.py @@ -74,6 +74,19 @@ def point_evaluation(self, order, refcoords, entity=None): core_eval = self.wrappee.point_evaluation(order, refcoords, entity) return self._transform_evaluation(core_eval) + @property + def dual_basis(self): + Q, x = self.wrappee.dual_basis + beta = self.get_indices() + zeta = self.get_value_indices() + # Index out the basis indices from wrapee's Q, to get + # something of wrappee.value_shape, then promote to new shape + # with the same transform as done for basis evaluation + Q = gem.ListTensor(self.transform(gem.partial_indexed(Q, beta))) + # Finally wrap up Q in shape again (now with some extra + # value_shape indices) + return gem.ComponentTensor(Q[zeta], beta + zeta), x + class HDivElement(WrapperElementBase): """H(div) wrapper element for tensor product elements.""" From bd48d60a41417546b51e961e2f41884786e34211 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Thu, 23 Jun 2022 22:17:42 -0500 Subject: [PATCH 693/749] Disable entity_permutations for trimmed serendipity elements --- finat/fiat_elements.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 033d37846..97de5d5a4 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -466,20 +466,37 @@ class TrimmedSerendipityFace(VectorFiatElement): def __init__(self, cell, degree): super(TrimmedSerendipityFace, self).__init__(FIAT.TrimmedSerendipityFace(cell, degree)) + @property + def entity_permutations(self): + raise NotImplementedError(f"entity_permutations not yet implemented for {type(self)}") + class TrimmedSerendipityDiv(VectorFiatElement): def __init__(self, cell, degree): super(TrimmedSerendipityDiv, self).__init__(FIAT.TrimmedSerendipityDiv(cell, degree)) + @property + def entity_permutations(self): + raise NotImplementedError(f"entity_permutations not yet implemented for {type(self)}") + class TrimmedSerendipityEdge(VectorFiatElement): def __init__(self, cell, degree): super(TrimmedSerendipityEdge, self).__init__(FIAT.TrimmedSerendipityEdge(cell, degree)) + @property + def entity_permutations(self): + raise NotImplementedError(f"entity_permutations not yet implemented for {type(self)}") + + class TrimmedSerendipityCurl(VectorFiatElement): def __init__(self, cell, degree): super(TrimmedSerendipityCurl, self).__init__(FIAT.TrimmedSerendipityCurl(cell, degree)) + @property + def entity_permutations(self): + raise NotImplementedError(f"entity_permutations not yet implemented for {type(self)}") + class BrezziDouglasMarini(VectorFiatElement): def __init__(self, cell, degree, variant=None): From 650f7e6e8add1d081ff3f100a490155ecd786cd7 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Thu, 23 Jun 2022 22:56:06 -0500 Subject: [PATCH 694/749] flake8 --- finat/__init__.py | 4 ++-- finat/fiat_elements.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/finat/__init__.py b/finat/__init__.py index 2c6f9034f..c64c186ff 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -3,8 +3,8 @@ from .fiat_elements import Lagrange, DiscontinuousLagrange, Real # noqa: F401 from .fiat_elements import DPC, Serendipity # noqa: F401 from .fiat_elements import TrimmedSerendipityFace, TrimmedSerendipityEdge # noqa: F401 -from .fiat_elements import TrimmedSerendipityDiv #noqa: F401 -from .fiat_elements import TrimmedSerendipityCurl #noqa: F401 +from .fiat_elements import TrimmedSerendipityDiv # noqa: F401 +from .fiat_elements import TrimmedSerendipityCurl # noqa: F401 from .fiat_elements import BrezziDouglasMarini, BrezziDouglasFortinMarini # noqa: F401 from .fiat_elements import Nedelec, NedelecSecondKind, RaviartThomas # noqa: F401 from .fiat_elements import HellanHerrmannJohnson, Regge # noqa: F401 diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 97de5d5a4..480a24640 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -469,7 +469,7 @@ def __init__(self, cell, degree): @property def entity_permutations(self): raise NotImplementedError(f"entity_permutations not yet implemented for {type(self)}") - + class TrimmedSerendipityDiv(VectorFiatElement): def __init__(self, cell, degree): @@ -478,7 +478,7 @@ def __init__(self, cell, degree): @property def entity_permutations(self): raise NotImplementedError(f"entity_permutations not yet implemented for {type(self)}") - + class TrimmedSerendipityEdge(VectorFiatElement): def __init__(self, cell, degree): @@ -487,7 +487,7 @@ def __init__(self, cell, degree): @property def entity_permutations(self): raise NotImplementedError(f"entity_permutations not yet implemented for {type(self)}") - + class TrimmedSerendipityCurl(VectorFiatElement): def __init__(self, cell, degree): @@ -496,11 +496,11 @@ def __init__(self, cell, degree): @property def entity_permutations(self): raise NotImplementedError(f"entity_permutations not yet implemented for {type(self)}") - + class BrezziDouglasMarini(VectorFiatElement): def __init__(self, cell, degree, variant=None): - #super(BrezziDouglasMarini, self).__init__(FIAT.BrezziDouglasMarini(cell, degree, variant=variant)) + # super(BrezziDouglasMarini, self).__init__(FIAT.BrezziDouglasMarini(cell, degree, variant=variant)) super(BrezziDouglasMarini, self).__init__(FIAT.BrezziDouglasMarini(cell, degree)) From 182d8a499d6154c363a22cadf9ba05adca83d681 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Mon, 27 Jun 2022 10:19:51 -0500 Subject: [PATCH 695/749] Fix variant option for Nedelec (inadvertently deleted on merge) --- finat/fiat_elements.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 480a24640..34bf9383b 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -510,8 +510,8 @@ def __init__(self, cell, degree): class Nedelec(VectorFiatElement): - def __init__(self, cell, degree): - super(Nedelec, self).__init__(FIAT.Nedelec(cell, degree)) + def __init__(self, cell, degree, variant): + super(Nedelec, self).__init__(FIAT.Nedelec(cell, degree, variant=variant)) class NedelecSecondKind(VectorFiatElement): From 5cb93322bb8e8ccecfb2fe464e6b9b3053a6d6e1 Mon Sep 17 00:00:00 2001 From: Rob Kirby Date: Mon, 27 Jun 2022 13:34:52 -0500 Subject: [PATCH 696/749] Fix more in variant args --- finat/fiat_elements.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 34bf9383b..da4a62ad3 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -500,8 +500,7 @@ def entity_permutations(self): class BrezziDouglasMarini(VectorFiatElement): def __init__(self, cell, degree, variant=None): - # super(BrezziDouglasMarini, self).__init__(FIAT.BrezziDouglasMarini(cell, degree, variant=variant)) - super(BrezziDouglasMarini, self).__init__(FIAT.BrezziDouglasMarini(cell, degree)) + super(BrezziDouglasMarini, self).__init__(FIAT.BrezziDouglasMarini(cell, degree, variant=variant)) class BrezziDouglasFortinMarini(VectorFiatElement): @@ -510,7 +509,7 @@ def __init__(self, cell, degree): class Nedelec(VectorFiatElement): - def __init__(self, cell, degree, variant): + def __init__(self, cell, degree, variant=None): super(Nedelec, self).__init__(FIAT.Nedelec(cell, degree, variant=variant)) From 143442ee6fdc70e98ced9a8ba641ed020bb78d22 Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Mon, 11 Jul 2022 14:32:47 +0100 Subject: [PATCH 697/749] add FDMQuadrature element --- finat/__init__.py | 2 +- finat/spectral.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/finat/__init__.py b/finat/__init__.py index 34b5cd2f6..8526a8e68 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -19,7 +19,7 @@ from .aw import ArnoldWintherNC # noqa: F401 from .trace import HDivTrace # noqa: F401 from .direct_serendipity import DirectSerendipity # noqa: F401 -from .spectral import GaussLobattoLegendre, GaussLegendre, FDMLagrange, FDMDiscontinuousLagrange, FDMBrokenH1, FDMBrokenL2, FDMHermite # noqa: F401 +from .spectral import GaussLobattoLegendre, GaussLegendre, FDMLagrange, FDMQuadrature, FDMDiscontinuousLagrange, FDMBrokenH1, FDMBrokenL2, FDMHermite # noqa: F401 from .tensorfiniteelement import TensorFiniteElement # noqa: F401 from .tensor_product import TensorProductElement # noqa: F401 from .cube import FlattenedDimensions # noqa: F401 diff --git a/finat/spectral.py b/finat/spectral.py index 0783aa4ee..ded77679f 100644 --- a/finat/spectral.py +++ b/finat/spectral.py @@ -80,6 +80,14 @@ def __init__(self, cell, degree): super(FDMDiscontinuousLagrange, self).__init__(fiat_element) +class FDMQuadrature(ScalarFiatElement): + """1D CG element with FDM shape functions and orthogonalized vertex modes.""" + + def __init__(self, cell, degree): + fiat_element = FIAT.FDMQuadrature(cell, degree) + super(FDMQuadrature, self).__init__(fiat_element) + + class FDMBrokenH1(ScalarFiatElement): """1D Broken CG element with FDM shape functions.""" From 18101c1b74880046097897f2e1692e6d993de9de Mon Sep 17 00:00:00 2001 From: ksagiyam <46749170+ksagiyam@users.noreply.github.com> Date: Wed, 3 Aug 2022 14:30:52 +0100 Subject: [PATCH 698/749] Revert "Maybe fix issue 274" --- gem/gem.py | 8 +++++--- gem/optimise.py | 29 ----------------------------- 2 files changed, 5 insertions(+), 32 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 9a089af70..aa1951787 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -211,7 +211,11 @@ class Literal(Constant): def __new__(cls, array): array = asarray(array) - return super(Literal, cls).__new__(cls) + if (array == 0).all(): + # All zeros, make symbolic zero + return Zero(array.shape) + else: + return super(Literal, cls).__new__(cls) def __init__(self, array): array = asarray(array) @@ -544,8 +548,6 @@ def __new__(cls, aggregate, multiindex): assert isinstance(index, IndexBase) if isinstance(index, Index): index.set_extent(extent) - elif isinstance(index, int) and not (0 <= index < extent): - raise IndexError("Invalid literal index") # Empty multiindex if not multiindex: diff --git a/gem/optimise.py b/gem/optimise.py index 19e7d0620..216df068a 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -155,35 +155,6 @@ def remove_componenttensors(expressions): return [mapper(expression, ()) for expression in expressions] -@singledispatch -def _constant_fold_zero(node, self): - raise AssertionError("cannot handle type %s" % type(node)) - - -_constant_fold_zero.register(Node)(reuse_if_untouched) - - -@_constant_fold_zero.register(Literal) -def _constant_fold_zero_literal(node, self): - if (node.array == 0).all(): - # All zeros, make symbolic zero - return Zero(node.shape) - else: - return node - - -def constant_fold_zero(exprs): - """Produce symbolic zeros from Literals - - :arg exprs: An iterable of gem expressions. - :returns: A list of gem expressions where any Literal containing - only zeros is replaced by symbolic Zero of the appropriate - shape. - """ - mapper = Memoizer(_constant_fold_zero) - return [mapper(e) for e in exprs] - - def _select_expression(expressions, index): """Helper function to select an expression from a list of expressions with an index. This function expect sanitised input, From ef2bbe93ce2fe4ef0f848304fbac6491c50e5c0e Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Wed, 3 Aug 2022 17:39:06 +0100 Subject: [PATCH 699/749] gem: IndexError for invalid literal indices --- gem/gem.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gem/gem.py b/gem/gem.py index aa1951787..f577a3624 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -548,6 +548,8 @@ def __new__(cls, aggregate, multiindex): assert isinstance(index, IndexBase) if isinstance(index, Index): index.set_extent(extent) + elif isinstance(index, int) and not (0 <= index < extent): + raise IndexError("Invalid literal index") # Empty multiindex if not multiindex: From c4c5a9789008ca6bcd49e2cda2252a8947f19ff0 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Wed, 3 Aug 2022 17:43:58 +0100 Subject: [PATCH 700/749] optimise: New pass to constant fold literal Zeros Having removed eager production of symbolic Zeros from Literals we now need a pass to introduce them. --- gem/optimise.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/gem/optimise.py b/gem/optimise.py index 216df068a..19e7d0620 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -155,6 +155,35 @@ def remove_componenttensors(expressions): return [mapper(expression, ()) for expression in expressions] +@singledispatch +def _constant_fold_zero(node, self): + raise AssertionError("cannot handle type %s" % type(node)) + + +_constant_fold_zero.register(Node)(reuse_if_untouched) + + +@_constant_fold_zero.register(Literal) +def _constant_fold_zero_literal(node, self): + if (node.array == 0).all(): + # All zeros, make symbolic zero + return Zero(node.shape) + else: + return node + + +def constant_fold_zero(exprs): + """Produce symbolic zeros from Literals + + :arg exprs: An iterable of gem expressions. + :returns: A list of gem expressions where any Literal containing + only zeros is replaced by symbolic Zero of the appropriate + shape. + """ + mapper = Memoizer(_constant_fold_zero) + return [mapper(e) for e in exprs] + + def _select_expression(expressions, index): """Helper function to select an expression from a list of expressions with an index. This function expect sanitised input, From 03ef874ecf2b48ffca15fa07ef5b4bc566d0c91f Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Wed, 3 Aug 2022 17:45:54 +0100 Subject: [PATCH 701/749] gem: Don't eagerly constant-fold literal Zeros gem.optimise.select_expression relies on every expression provided having the same structural shape. In certain circumstances (restricted elements on tensor product cells) eagerly producing symbolic zeros destroys this assumption. To work around this, we will constant fold later as a separate pass. Fixes #274. --- gem/gem.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index f577a3624..9a089af70 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -211,11 +211,7 @@ class Literal(Constant): def __new__(cls, array): array = asarray(array) - if (array == 0).all(): - # All zeros, make symbolic zero - return Zero(array.shape) - else: - return super(Literal, cls).__new__(cls) + return super(Literal, cls).__new__(cls) def __init__(self, array): array = asarray(array) From 56e52ebc0f89dad3f32ce4496e1e3cfa86ad62b2 Mon Sep 17 00:00:00 2001 From: ksagiyam Date: Wed, 3 Aug 2022 20:26:10 +0100 Subject: [PATCH 702/749] fiat_element: do not Zero() --- finat/fiat_elements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index da4a62ad3..6a753de78 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -136,7 +136,7 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): else: # Make sure numerics satisfies theory assert np.allclose(table, 0.0) - exprs.append(gem.Zero(self.index_shape)) + exprs.append(gem.Literal(np.zeros(self.index_shape))) if self.value_shape: # As above, this extent may be different from that # advertised by the finat element. From 3c111fe9be0978537f2f2fe468ba22b7c9e929f3 Mon Sep 17 00:00:00 2001 From: ksagiyam Date: Wed, 3 Aug 2022 20:24:40 +0100 Subject: [PATCH 703/749] optimise: New pass for ListTensor in constant_fold_zero() --- gem/optimise.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/gem/optimise.py b/gem/optimise.py index 19e7d0620..3194e6efd 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -172,6 +172,17 @@ def _constant_fold_zero_literal(node, self): return node +@_constant_fold_zero.register(ListTensor) +def _constant_fold_zero_listtensor(node, self): + new_children = list(map(self, node.children)) + if all(isinstance(nc, Zero) for nc in new_children): + return Zero(node.shape) + elif all(nc == c for nc, c in zip(new_children, node.children)): + return node + else: + return node.reconstruct(*new_children) + + def constant_fold_zero(exprs): """Produce symbolic zeros from Literals @@ -179,6 +190,10 @@ def constant_fold_zero(exprs): :returns: A list of gem expressions where any Literal containing only zeros is replaced by symbolic Zero of the appropriate shape. + + We need a separate path for ListTensor so that its `reconstruct` + method will not be called when the new children are `Zero()`s; + otherwise Literal `0`s would be reintroduced. """ mapper = Memoizer(_constant_fold_zero) return [mapper(e) for e in exprs] From 79031e2b8e7a2c9d8782bd35101b20d50fade90e Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Mon, 24 Oct 2022 12:00:52 +0100 Subject: [PATCH 704/749] wrap FIAT.Legendre and FIAT.IntegratedLegendre --- finat/__init__.py | 2 +- finat/spectral.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/finat/__init__.py b/finat/__init__.py index 8526a8e68..afed5e3e5 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -19,7 +19,7 @@ from .aw import ArnoldWintherNC # noqa: F401 from .trace import HDivTrace # noqa: F401 from .direct_serendipity import DirectSerendipity # noqa: F401 -from .spectral import GaussLobattoLegendre, GaussLegendre, FDMLagrange, FDMQuadrature, FDMDiscontinuousLagrange, FDMBrokenH1, FDMBrokenL2, FDMHermite # noqa: F401 +from .spectral import GaussLobattoLegendre, GaussLegendre, Legendre, IntegratedLegendre, FDMLagrange, FDMQuadrature, FDMDiscontinuousLagrange, FDMBrokenH1, FDMBrokenL2, FDMHermite # noqa: F401 from .tensorfiniteelement import TensorFiniteElement # noqa: F401 from .tensor_product import TensorProductElement # noqa: F401 from .cube import FlattenedDimensions # noqa: F401 diff --git a/finat/spectral.py b/finat/spectral.py index ded77679f..ed23dbc9f 100644 --- a/finat/spectral.py +++ b/finat/spectral.py @@ -64,6 +64,22 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): return result +class Legendre(ScalarFiatElement): + """1D DG element with Legendre polynomials.""" + + def __init__(self, cell, degree): + fiat_element = FIAT.Legendre(cell, degree) + super(Legendre, self).__init__(fiat_element) + + +class IntegratedLegendre(ScalarFiatElement): + """1D CG element with integrated Legendre polynomials.""" + + def __init__(self, cell, degree): + fiat_element = FIAT.IntegratedLegendre(cell, degree) + super(IntegratedLegendre, self).__init__(fiat_element) + + class FDMLagrange(ScalarFiatElement): """1D CG element with FDM shape functions and point evaluation BCs.""" From 61343342a49f2e4af4d84c6705fc83e5026a2785 Mon Sep 17 00:00:00 2001 From: ksagiyam Date: Thu, 10 Nov 2022 16:21:48 +0000 Subject: [PATCH 705/749] orientation: handle extrinsic orientations in tensor product elements --- finat/tensor_product.py | 136 +++++++++++++++++++++++++++++++++++----- 1 file changed, 120 insertions(+), 16 deletions(-) diff --git a/finat/tensor_product.py b/finat/tensor_product.py index 332ddca5b..b04d91674 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -1,4 +1,5 @@ from functools import reduce +import itertools from itertools import chain, product from operator import methodcaller @@ -227,27 +228,130 @@ def compose_permutations(factors): :arg factors: element factors. :returns: entity_permutation dict of the :class:`TensorProductElement` object composed of factors. + + For tensor-product elements, one needs to consider two kinds of orientations: + extrinsic orientations and intrinsic ("material") orientations. + + Example: + + UFCQuadrilateral := UFCInterval x UFCInterval + + eo (extrinsic orientation): swap axes (X -> y, Y-> x) + io (intrinsic orientation): reflect component intervals + o (total orientation) : (2 ** dim) * eo + io + + eo\\io 0 1 2 3 + + 1---3 0---2 3---1 2---0 + 0 | | | | | | | | + 0---2 1---3 2---0 3---1 + + 2---3 3---2 0---1 1---0 + 1 | | | | | | | | + 0---1 1---0 2---3 3---2 + + .. code-block:: python3 + + import ufl + import FIAT + import finat + + cell = FIAT.ufc_cell(ufl.interval) + elem = finat.DiscontinuousLagrange(cell, 1) + elem = finat.TensorProductElement([elem, elem]) + print(elem.entity_permutations) + + prints: + + {(0, 0): {0: {(0, 0, 0): []}, + 1: {(0, 0, 0): []}, + 2: {(0, 0, 0): []}, + 3: {(0, 0, 0): []}}, + (0, 1): {0: {(0, 0, 0): [], + (0, 0, 1): []}, + 1: {(0, 0, 0): [], + (0, 0, 1): []}}, + (1, 0): {0: {(0, 0, 0): [], + (0, 1, 0): []}, + 1: {(0, 0, 0): [], + (0, 1, 0): []}}, + (1, 1): {0: {(0, 0, 0): [0, 1, 2, 3], + (0, 0, 1): [1, 0, 3, 2], + (0, 1, 0): [2, 3, 0, 1], + (0, 1, 1): [3, 2, 1, 0], + (1, 0, 0): [0, 2, 1, 3], + (1, 0, 1): [2, 0, 3, 1], + (1, 1, 0): [1, 3, 0, 2], + (1, 1, 1): [3, 1, 2, 0]}}} """ + nfactors = len(factors) permutations = {} - for dim in product(*[fe.cell.get_topology().keys() - for fe in factors]): + for dim in product(*[fe.cell.get_topology().keys() for fe in factors]): dim_permutations = [] - e_o_p_maps = [fe.entity_permutations[d] - for fe, d in zip(factors, dim)] + e_o_p_maps = [fe.entity_permutations[d] for fe, d in zip(factors, dim)] for e_tuple in product(*[sorted(e_o_p_map) for e_o_p_map in e_o_p_maps]): - o_p_maps = [e_o_p_map[e] for e_o_p_map, e in zip(e_o_p_maps, e_tuple)] + # Handle extrinsic orientations. + # This is complex and we need to think to make this function more general. + # One interesting case is pyramid x pyramid. There are two types of facets + # in a pyramid cell, quad and triangle, and two types of intervals, ones + # attached to quad (Iq) and ones attached to triangles (It). When we take + # a tensor product of two pyramid cells, there are different kinds of tensor + # product of intervals, i.e., Iq x Iq, Iq x It, It x Iq, It x It, and we + # need a careful thought on how many possible extrinsic orientations we need + # to consider for each. + # For now we restrict ourselves to specific cases. + cells = [fe.cell for fe in factors] + if len(set(cells)) == len(cells): + # All components have different cells. + # Example: triangle x interval. + # dim == (2, 1) -> + # triangle x interval (1 possible extrinsic orientation). + axis_perms = (tuple(range(len(factors))), ) # Identity: no permutations + elif len(set(cells)) == 1 and isinstance(cells[0], FIAT.reference_element.UFCInterval): + # Tensor product of intervals. + # Example: interval x interval x interval x interval + # dim == (0, 1, 1, 1) -> + # point x interval x interval x interval (1! * 3! possible extrinsic orientations). + axis_perms = sorted(itertools.permutations(range(len(factors)))) + for idim, d in enumerate(dim): + if d == 0: + # idim-th component does not contribute to the extrinsic orientation. + axis_perms = [ap for ap in axis_perms if ap[idim] == idim] + else: + # More general tensor product cells. + # Example: triangle x quad x triangle x triangle x interval x interval + # dim == (2, 2, 2, 2, 1, 1) -> + # triangle x quad x triangle x triangle x interval x interval (3! * 1! * 2! possible extrinsic orientations). + raise NotImplementedError(f"Unable to compose permutations for {' x '.join([str(fe) for fe in factors])}") o_tuple_perm_map = {} - for o_tuple in product(*[o_p_map.keys() for o_p_map in o_p_maps]): - ps = [o_p_map[o] for o_p_map, o in zip(o_p_maps, o_tuple)] - shape = tuple(len(p) for p in ps) - size = numpy.prod(shape) - if size == 0: - o_tuple_perm_map[o_tuple] = [] - else: - a = numpy.arange(size).reshape(shape) - for i, p in enumerate(ps): - a = a.swapaxes(0, i)[p, :].swapaxes(0, i) - o_tuple_perm_map[o_tuple] = a.reshape(-1).tolist() + for eo, ap in enumerate(axis_perms): + o_p_maps = [e_o_p_map[e] for e_o_p_map, e in zip(e_o_p_maps, e_tuple)] + for o_tuple in product(*[o_p_map.keys() for o_p_map in o_p_maps]): + ps = [o_p_map[o] for o_p_map, o in zip(o_p_maps, o_tuple)] + shape = [len(p) for p in ps] + for idim in range(len(ap)): + shape[ap[idim]] = len(ps[idim]) + size = numpy.prod(shape) + if size == 0: + o_tuple_perm_map[(eo, ) + o_tuple] = [] + else: + a = numpy.arange(size).reshape(shape) + # Tensorproduct elements on a tensorproduct cell of intervals: + # When we map the reference element to the physical element, we fisrt apply + # the extrinsic orientation and then the intrinsic orientation. + # Thus, to make the a.reshape(-1) trick work in the below, + # we apply the inverse operation on a; we first apply the inverse of the + # intrinsic orientation and then the inverse of the extrinsic orienataion. + for idim, p in enumerate(ps): + # Note that p inverse = p for interval elements. + # Do not use p inverse (just use p) for elements on simplices + # as p already does what we want by construction. + a = a.swapaxes(0, ap[idim])[p, :].swapaxes(0, ap[idim]) + apinv = list(range(nfactors)) + for idim in range(len(ap)): + apinv[ap[idim]] = idim + a = numpy.moveaxis(a, range(nfactors), apinv) + o_tuple_perm_map[(eo, ) + o_tuple] = a.reshape(-1).tolist() dim_permutations.append((e_tuple, o_tuple_perm_map)) permutations[dim] = dict(enumerate(v for k, v in sorted(dim_permutations))) return permutations From 5a2062769e04845a485264e85146dabeeeebd1b4 Mon Sep 17 00:00:00 2001 From: ksagiyam Date: Wed, 16 Nov 2022 10:10:25 +0000 Subject: [PATCH 706/749] tensor_product: update factor_point_set for hex --- finat/tensor_product.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/finat/tensor_product.py b/finat/tensor_product.py index b04d91674..40189c7d9 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -369,7 +369,8 @@ def factor_point_set(product_cell, product_dim, point_set): point_dims = [cell.construct_subelement(dim).get_spatial_dimension() for cell, dim in zip(product_cell.cells, product_dim)] - if isinstance(point_set, TensorPointSet): + if isinstance(point_set, TensorPointSet) and \ + len(product_cell.cells) == len(point_set.factors): # Just give the factors asserting matching dimensions. assert len(point_set.factors) == len(point_dims) assert all(ps.dimension == dim @@ -380,10 +381,9 @@ def factor_point_set(product_cell, product_dim, point_set): # required by the subelements. assert point_set.dimension == sum(point_dims) slices = TensorProductCell._split_slices(point_dims) - if isinstance(point_set, PointSingleton): return [PointSingleton(point_set.point[s]) for s in slices] - elif isinstance(point_set, PointSet): + elif isinstance(point_set, (PointSet, TensorPointSet)): # Use the same point index for the new point sets. result = [] for s in slices: From 74bff493c937fad3969692bdc39a1e077271b9c5 Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Wed, 11 Jan 2023 16:18:37 +0000 Subject: [PATCH 707/749] Expand EnrichedElement (#105) Flatten enriched elements on construction. --- finat/enriched.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/finat/enriched.py b/finat/enriched.py index 26e6fe4ee..fee758a51 100644 --- a/finat/enriched.py +++ b/finat/enriched.py @@ -1,5 +1,6 @@ from functools import partial from operator import add, methodcaller +from itertools import chain import numpy @@ -16,7 +17,7 @@ class EnrichedElement(FiniteElementBase): basis functions of several other finite elements.""" def __new__(cls, elements): - elements = tuple(elements) + elements = tuple(chain.from_iterable(e.elements if isinstance(e, EnrichedElement) else (e,) for e in elements)) if len(elements) == 1: return elements[0] else: From d08cf650e788fe7fb66694e1517197476c1af7de Mon Sep 17 00:00:00 2001 From: ksagiyam Date: Tue, 4 Apr 2023 16:16:03 +0100 Subject: [PATCH 708/749] refactor: make tensorproduct permutations in FIAT --- finat/tensor_product.py | 70 +++-------------------------------------- 1 file changed, 5 insertions(+), 65 deletions(-) diff --git a/finat/tensor_product.py b/finat/tensor_product.py index 40189c7d9..302ccabb7 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -1,5 +1,4 @@ from functools import reduce -import itertools from itertools import chain, product from operator import methodcaller @@ -8,6 +7,7 @@ import FIAT from FIAT.polynomial_set import mis from FIAT.reference_element import TensorProductCell +from FIAT.orientation_utils import make_entity_permutations_tensorproduct import gem from gem.utils import cached_property @@ -284,74 +284,14 @@ def compose_permutations(factors): (1, 1, 0): [1, 3, 0, 2], (1, 1, 1): [3, 1, 2, 0]}}} """ - nfactors = len(factors) permutations = {} - for dim in product(*[fe.cell.get_topology().keys() for fe in factors]): + cells = [fe.cell for fe in factors] + for dim in product(*[cell.get_topology().keys() for cell in cells]): dim_permutations = [] e_o_p_maps = [fe.entity_permutations[d] for fe, d in zip(factors, dim)] for e_tuple in product(*[sorted(e_o_p_map) for e_o_p_map in e_o_p_maps]): - # Handle extrinsic orientations. - # This is complex and we need to think to make this function more general. - # One interesting case is pyramid x pyramid. There are two types of facets - # in a pyramid cell, quad and triangle, and two types of intervals, ones - # attached to quad (Iq) and ones attached to triangles (It). When we take - # a tensor product of two pyramid cells, there are different kinds of tensor - # product of intervals, i.e., Iq x Iq, Iq x It, It x Iq, It x It, and we - # need a careful thought on how many possible extrinsic orientations we need - # to consider for each. - # For now we restrict ourselves to specific cases. - cells = [fe.cell for fe in factors] - if len(set(cells)) == len(cells): - # All components have different cells. - # Example: triangle x interval. - # dim == (2, 1) -> - # triangle x interval (1 possible extrinsic orientation). - axis_perms = (tuple(range(len(factors))), ) # Identity: no permutations - elif len(set(cells)) == 1 and isinstance(cells[0], FIAT.reference_element.UFCInterval): - # Tensor product of intervals. - # Example: interval x interval x interval x interval - # dim == (0, 1, 1, 1) -> - # point x interval x interval x interval (1! * 3! possible extrinsic orientations). - axis_perms = sorted(itertools.permutations(range(len(factors)))) - for idim, d in enumerate(dim): - if d == 0: - # idim-th component does not contribute to the extrinsic orientation. - axis_perms = [ap for ap in axis_perms if ap[idim] == idim] - else: - # More general tensor product cells. - # Example: triangle x quad x triangle x triangle x interval x interval - # dim == (2, 2, 2, 2, 1, 1) -> - # triangle x quad x triangle x triangle x interval x interval (3! * 1! * 2! possible extrinsic orientations). - raise NotImplementedError(f"Unable to compose permutations for {' x '.join([str(fe) for fe in factors])}") - o_tuple_perm_map = {} - for eo, ap in enumerate(axis_perms): - o_p_maps = [e_o_p_map[e] for e_o_p_map, e in zip(e_o_p_maps, e_tuple)] - for o_tuple in product(*[o_p_map.keys() for o_p_map in o_p_maps]): - ps = [o_p_map[o] for o_p_map, o in zip(o_p_maps, o_tuple)] - shape = [len(p) for p in ps] - for idim in range(len(ap)): - shape[ap[idim]] = len(ps[idim]) - size = numpy.prod(shape) - if size == 0: - o_tuple_perm_map[(eo, ) + o_tuple] = [] - else: - a = numpy.arange(size).reshape(shape) - # Tensorproduct elements on a tensorproduct cell of intervals: - # When we map the reference element to the physical element, we fisrt apply - # the extrinsic orientation and then the intrinsic orientation. - # Thus, to make the a.reshape(-1) trick work in the below, - # we apply the inverse operation on a; we first apply the inverse of the - # intrinsic orientation and then the inverse of the extrinsic orienataion. - for idim, p in enumerate(ps): - # Note that p inverse = p for interval elements. - # Do not use p inverse (just use p) for elements on simplices - # as p already does what we want by construction. - a = a.swapaxes(0, ap[idim])[p, :].swapaxes(0, ap[idim]) - apinv = list(range(nfactors)) - for idim in range(len(ap)): - apinv[ap[idim]] = idim - a = numpy.moveaxis(a, range(nfactors), apinv) - o_tuple_perm_map[(eo, ) + o_tuple] = a.reshape(-1).tolist() + o_p_maps = [e_o_p_map[e] for e_o_p_map, e in zip(e_o_p_maps, e_tuple)] + o_tuple_perm_map = make_entity_permutations_tensorproduct(cells, dim, o_p_maps) dim_permutations.append((e_tuple, o_tuple_perm_map)) permutations[dim] = dict(enumerate(v for k, v in sorted(dim_permutations))) return permutations From b04551dba755f788ccf4ccee66a931e140e16906 Mon Sep 17 00:00:00 2001 From: Connor Ward Date: Wed, 10 May 2023 15:31:11 +0100 Subject: [PATCH 709/749] Expunge COFFEE (#291) --- gem/impero_utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gem/impero_utils.py b/gem/impero_utils.py index 3e87bdf58..31f9565bb 100644 --- a/gem/impero_utils.py +++ b/gem/impero_utils.py @@ -2,8 +2,7 @@ terminal Impero operations, and for building any additional data required for straightforward C code generation. -What this module does is independent of whether we eventually generate -C code or a COFFEE AST. +What this module does is independent of the generated code target. """ import collections From 531691ff0fe0439a9739f5bab09d946a82317a44 Mon Sep 17 00:00:00 2001 From: Tom Bendall Date: Tue, 25 Jul 2023 16:56:59 +0100 Subject: [PATCH 710/749] update elements to handle lack of "entity_permutations" correctly --- finat/fiat_elements.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 6b76dd6c6..067d2a914 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -507,11 +507,18 @@ class BrezziDouglasMariniCubeEdge(VectorFiatElement): def __init__(self, cell, degree): super(BrezziDouglasMariniCubeEdge, self).__init__(FIAT.BrezziDouglasMariniCubeEdge(cell, degree)) + @property + def entity_permutations(self): + raise NotImplementedError(f"entity_permutations not yet implemented for {type(self)}") class BrezziDouglasMariniCubeFace(VectorFiatElement): def __init__(self, cell, degree): super(BrezziDouglasMariniCubeFace, self).__init__(FIAT.BrezziDouglasMariniCubeFace(cell, degree)) + @property + def entity_permutations(self): + raise NotImplementedError(f"entity_permutations not yet implemented for {type(self)}") + class BrezziDouglasFortinMarini(VectorFiatElement): def __init__(self, cell, degree): From 02007f09fcdba81851c98b3d016b41526836d185 Mon Sep 17 00:00:00 2001 From: Tom Bendall Date: Wed, 26 Jul 2023 11:40:19 +0100 Subject: [PATCH 711/749] fix lint --- finat/fiat_elements.py | 1 + 1 file changed, 1 insertion(+) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 067d2a914..25721bcc7 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -511,6 +511,7 @@ def __init__(self, cell, degree): def entity_permutations(self): raise NotImplementedError(f"entity_permutations not yet implemented for {type(self)}") + class BrezziDouglasMariniCubeFace(VectorFiatElement): def __init__(self, cell, degree): super(BrezziDouglasMariniCubeFace, self).__init__(FIAT.BrezziDouglasMariniCubeFace(cell, degree)) From 0227fef286da6c6f8125be2481302ab5430bd35d Mon Sep 17 00:00:00 2001 From: Connor Ward Date: Tue, 12 Sep 2023 13:46:27 +0100 Subject: [PATCH 712/749] Adapt to UFL changes in subdomain_id (#297) * Adapt to UFL changes in subdomain_id * Give kernels a simpler name --- gem/coffee.py | 2 +- gem/gem.py | 4 ++-- gem/node.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gem/coffee.py b/gem/coffee.py index be8a041ef..f766a4890 100644 --- a/gem/coffee.py +++ b/gem/coffee.py @@ -182,7 +182,7 @@ def optimise_monomials(monomials, linear_indices): :returns: an iterable of factorised :class:`Monomials`s """ - assert len(set(frozenset(m.sum_indices) for m in monomials)) <= 1,\ + assert len(set(frozenset(m.sum_indices) for m in monomials)) <= 1, \ "All monomials required to have same sum indices for factorisation" result = [m for m in monomials if not m.atomics] # skipped monomials diff --git a/gem/gem.py b/gem/gem.py index 9a089af70..95e8f2f5e 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -221,7 +221,7 @@ def __init__(self, array): self.array = array.astype(complex) def is_equal(self, other): - if type(self) != type(other): + if type(self) is not type(other): return False if self.shape != other.shape: return False @@ -748,7 +748,7 @@ def __repr__(self): def is_equal(self, other): """Common subexpression eliminating equality predicate.""" - if type(self) != type(other): + if type(self) is not type(other): return False if (self.array == other.array).all(): self.array = other.array diff --git a/gem/node.py b/gem/node.py index 1af62a635..31d99b9ec 100644 --- a/gem/node.py +++ b/gem/node.py @@ -84,7 +84,7 @@ def is_equal(self, other): This is the method to potentially override in derived classes, not :meth:`__eq__` or :meth:`__ne__`. """ - if type(self) != type(other): + if type(self) is not type(other): return False self_consargs = self._cons_args(self.children) other_consargs = other._cons_args(other.children) From 5ec6521f10ebc1a7a8bf0868df505f1ea6768780 Mon Sep 17 00:00:00 2001 From: Matthew Scroggs Date: Mon, 16 Oct 2023 17:03:02 +0100 Subject: [PATCH 713/749] Move ufl legacy elements to finat --- finat/tensor_product.py | 3 +- finat/ufl/__init__.py | 23 ++ finat/ufl/brokenelement.py | 54 +++ finat/ufl/elementlist.py | 482 +++++++++++++++++++++++++ finat/ufl/enrichedelement.py | 165 +++++++++ finat/ufl/finiteelement.py | 236 +++++++++++++ finat/ufl/finiteelementbase.py | 277 +++++++++++++++ finat/ufl/hdivcurl.py | 214 ++++++++++++ finat/ufl/mixedelement.py | 563 ++++++++++++++++++++++++++++++ finat/ufl/restrictedelement.py | 114 ++++++ finat/ufl/tensorproductelement.py | 141 ++++++++ 11 files changed, 2270 insertions(+), 2 deletions(-) create mode 100644 finat/ufl/__init__.py create mode 100644 finat/ufl/brokenelement.py create mode 100644 finat/ufl/elementlist.py create mode 100644 finat/ufl/enrichedelement.py create mode 100644 finat/ufl/finiteelement.py create mode 100644 finat/ufl/finiteelementbase.py create mode 100644 finat/ufl/hdivcurl.py create mode 100644 finat/ufl/mixedelement.py create mode 100644 finat/ufl/restrictedelement.py create mode 100644 finat/ufl/tensorproductelement.py diff --git a/finat/tensor_product.py b/finat/tensor_product.py index 302ccabb7..083c509da 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -252,11 +252,10 @@ def compose_permutations(factors): .. code-block:: python3 - import ufl import FIAT import finat - cell = FIAT.ufc_cell(ufl.interval) + cell = FIAT.ufc_cell("interval") elem = finat.DiscontinuousLagrange(cell, 1) elem = finat.TensorProductElement([elem, elem]) print(elem.entity_permutations) diff --git a/finat/ufl/__init__.py b/finat/ufl/__init__.py new file mode 100644 index 000000000..4ddc3953c --- /dev/null +++ b/finat/ufl/__init__.py @@ -0,0 +1,23 @@ +"""Legacy UFL features.""" +# Copyright (C) 2008-2016 Martin Sandve Alnæs +# +# This file was originally part of UFL (https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later +# +# Modified by Kristian B. Oelgaard +# Modified by Marie E. Rognes 2010, 2012 +# Modified by Andrew T. T. McRae 2014 +# Modified by Lawrence Mitchell 2014 +# Modified by Matthew Scroggs, 2023 + +import warnings as _warnings + +from finat.ufl.brokenelement import BrokenElement +from finat.ufl.enrichedelement import EnrichedElement, NodalEnrichedElement +from finat.ufl.finiteelement import FiniteElement +from finat.ufl.finiteelementbase import FiniteElementBase +from finat.ufl.hdivcurl import HCurlElement, HDivElement, WithMapping +from finat.ufl.mixedelement import MixedElement, TensorElement, VectorElement +from finat.ufl.restrictedelement import RestrictedElement +from finat.ufl.tensorproductelement import TensorProductElement diff --git a/finat/ufl/brokenelement.py b/finat/ufl/brokenelement.py new file mode 100644 index 000000000..9127a3976 --- /dev/null +++ b/finat/ufl/brokenelement.py @@ -0,0 +1,54 @@ +"""Element.""" +# -*- coding: utf-8 -*- +# Copyright (C) 2014 Andrew T. T. McRae +# +# This file was originally part of UFL (https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later +# +# Modified by Massimiliano Leoni, 2016 +# Modified by Matthew Scroggs, 2023 + +from finat.ufl.finiteelementbase import FiniteElementBase +from ufl.sobolevspace import L2 + + +class BrokenElement(FiniteElementBase): + """The discontinuous version of an existing Finite Element space.""" + def __init__(self, element): + """Init.""" + self._element = element + + family = "BrokenElement" + cell = element.cell + degree = element.degree() + quad_scheme = element.quadrature_scheme() + value_shape = element.value_shape + reference_value_shape = element.reference_value_shape + FiniteElementBase.__init__(self, family, cell, degree, + quad_scheme, value_shape, reference_value_shape) + + def __repr__(self): + """Doc.""" + return f"BrokenElement({repr(self._element)})" + + def mapping(self): + """Doc.""" + return self._element.mapping() + + @property + def sobolev_space(self): + """Return the underlying Sobolev space.""" + return L2 + + def reconstruct(self, **kwargs): + """Doc.""" + return BrokenElement(self._element.reconstruct(**kwargs)) + + def __str__(self): + """Doc.""" + return f"BrokenElement({repr(self._element)})" + + def shortstr(self): + """Format as string for pretty printing.""" + return f"BrokenElement({repr(self._element)})" diff --git a/finat/ufl/elementlist.py b/finat/ufl/elementlist.py new file mode 100644 index 000000000..15a33476b --- /dev/null +++ b/finat/ufl/elementlist.py @@ -0,0 +1,482 @@ +"""Element. + +This module provides an extensive list of predefined finite element +families. Users or, more likely, form compilers, may register new +elements by calling the function register_element. +""" +# Copyright (C) 2008-2016 Martin Sandve Alnæs and Anders Logg +# +# This file was originally part of UFL (https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later +# +# Modified by Marie E. Rognes , 2010 +# Modified by Lizao Li , 2015, 2016 +# Modified by Massimiliano Leoni, 2016 +# Modified by Robert Kloefkorn, 2022 +# Modified by Matthew Scroggs, 2023 + +import warnings + +from numpy import asarray + +from ufl.cell import Cell, TensorProductCell +from ufl.sobolevspace import H1, H2, L2, HCurl, HDiv, HDivDiv, HEin, HInf +from ufl.utils.formatting import istr + +# List of valid elements +ufl_elements = {} + +# Aliases: aliases[name] (...) -> (standard_name, ...) +aliases = {} + + +# Function for registering new elements +def register_element(family, short_name, value_rank, sobolev_space, mapping, + degree_range, cellnames): + """Register new finite element family.""" + if family in ufl_elements: + raise ValueError(f"Finite element '{family}%s' has already been registered.") + ufl_elements[family] = (family, short_name, value_rank, sobolev_space, + mapping, degree_range, cellnames) + if short_name is not None: + ufl_elements[short_name] = (family, short_name, value_rank, sobolev_space, + mapping, degree_range, cellnames) + + +def register_alias(alias, to): + """Doc.""" + aliases[alias] = to + + +def show_elements(): + """Shows all registered elements.""" + print("Showing all registered elements:") + print("================================") + shown = set() + for k in sorted(ufl_elements.keys()): + data = ufl_elements[k] + if data in shown: + continue + shown.add(data) + (family, short_name, value_rank, sobolev_space, mapping, degree_range, cellnames) = data + print(f"Finite element family: '{family}', '{short_name}'") + print(f"Sobolev space: {sobolev_space}%s") + print(f"Mapping: {mapping}") + print(f"Degree range: {degree_range}") + print(f"Value rank: {value_rank}") + print(f"Defined on cellnames: {cellnames}") + print() + + +# FIXME: Consider cleanup of element names. Use notation from periodic +# table as the main, keep old names as compatibility aliases. + +# NOTE: Any element with polynomial degree 0 will be considered L2, +# independent of the space passed to register_element. + +# NOTE: The mapping of the element basis functions +# from reference to physical representation is +# chosen based on the sobolev space: +# HDiv = contravariant Piola, +# HCurl = covariant Piola, +# H1/L2 = no mapping. + +# TODO: If determining mapping from sobolev_space isn't sufficient in +# the future, add mapping name as another element property. + +# Cell groups +simplices = ("interval", "triangle", "tetrahedron", "pentatope") +cubes = ("interval", "quadrilateral", "hexahedron", "tesseract") +any_cell = (None, + "vertex", "interval", + "triangle", "tetrahedron", "prism", + "pyramid", "quadrilateral", "hexahedron", "pentatope", "tesseract") + +# Elements in the periodic table # TODO: Register these as aliases of +# periodic table element description instead of the other way around +register_element("Lagrange", "CG", 0, H1, "identity", (1, None), + any_cell) # "P" +register_element("Brezzi-Douglas-Marini", "BDM", 1, HDiv, + "contravariant Piola", (1, None), simplices[1:]) # "BDMF" (2d), "N2F" (3d) +register_element("Discontinuous Lagrange", "DG", 0, L2, "identity", (0, None), + any_cell) # "DP" +register_element("Discontinuous Taylor", "TDG", 0, L2, "identity", (0, None), simplices) +register_element("Nedelec 1st kind H(curl)", "N1curl", 1, HCurl, + "covariant Piola", (1, None), simplices[1:]) # "RTE" (2d), "N1E" (3d) +register_element("Nedelec 2nd kind H(curl)", "N2curl", 1, HCurl, + "covariant Piola", (1, None), simplices[1:]) # "BDME" (2d), "N2E" (3d) +register_element("Raviart-Thomas", "RT", 1, HDiv, "contravariant Piola", + (1, None), simplices[1:]) # "RTF" (2d), "N1F" (3d) + +# Elements not in the periodic table +register_element("Argyris", "ARG", 0, H2, "custom", (5, 5), ("triangle",)) +register_element("Bell", "BELL", 0, H2, "custom", (5, 5), ("triangle",)) +register_element("Brezzi-Douglas-Fortin-Marini", "BDFM", 1, HDiv, + "contravariant Piola", (1, None), simplices[1:]) +register_element("Crouzeix-Raviart", "CR", 0, L2, "identity", (1, 1), + simplices[1:]) +# TODO: Implement generic Tear operator for elements instead of this: +register_element("Discontinuous Raviart-Thomas", "DRT", 1, L2, + "contravariant Piola", (1, None), simplices[1:]) +register_element("Hermite", "HER", 0, H1, "custom", (3, 3), simplices) +register_element("Kong-Mulder-Veldhuizen", "KMV", 0, H1, "identity", (1, None), + simplices[1:]) +register_element("Mardal-Tai-Winther", "MTW", 1, H1, "contravariant Piola", (3, 3), + ("triangle",)) +register_element("Morley", "MOR", 0, H2, "custom", (2, 2), ("triangle",)) + +# Special elements +register_element("Boundary Quadrature", "BQ", 0, L2, "identity", (0, None), + any_cell) +register_element("Bubble", "B", 0, H1, "identity", (2, None), simplices) +register_element("FacetBubble", "FB", 0, H1, "identity", (2, None), simplices) +register_element("Quadrature", "Quadrature", 0, L2, "identity", (0, None), + any_cell) +register_element("Real", "R", 0, HInf, "identity", (0, 0), + any_cell + ("TensorProductCell",)) +register_element("Undefined", "U", 0, L2, "identity", (0, None), any_cell) +register_element("Radau", "Rad", 0, L2, "identity", (0, None), ("interval",)) +register_element("Regge", "Regge", 2, HEin, "double covariant Piola", + (0, None), simplices[1:]) +register_element("HDiv Trace", "HDivT", 0, L2, "identity", (0, None), any_cell) +register_element("Hellan-Herrmann-Johnson", "HHJ", 2, HDivDiv, + "double contravariant Piola", (0, None), ("triangle",)) +register_element("Nonconforming Arnold-Winther", "AWnc", 2, HDivDiv, + "double contravariant Piola", (2, 2), ("triangle", "tetrahedron")) +register_element("Conforming Arnold-Winther", "AWc", 2, HDivDiv, + "double contravariant Piola", (3, None), ("triangle", "tetrahedron")) +# Spectral elements. +register_element("Gauss-Legendre", "GL", 0, L2, "identity", (0, None), + ("interval",)) +register_element("Gauss-Lobatto-Legendre", "GLL", 0, H1, "identity", (1, None), + ("interval",)) +register_alias("Lobatto", + lambda family, dim, order, degree: ("Gauss-Lobatto-Legendre", order)) +register_alias("Lob", + lambda family, dim, order, degree: ("Gauss-Lobatto-Legendre", order)) + +register_element("Bernstein", None, 0, H1, "identity", (1, None), simplices) + + +# Let Nedelec H(div) elements be aliases to BDMs/RTs +register_alias("Nedelec 1st kind H(div)", + lambda family, dim, order, degree: ("Raviart-Thomas", order)) +register_alias("N1div", + lambda family, dim, order, degree: ("Raviart-Thomas", order)) + +register_alias("Nedelec 2nd kind H(div)", + lambda family, dim, order, degree: ("Brezzi-Douglas-Marini", + order)) +register_alias("N2div", + lambda family, dim, order, degree: ("Brezzi-Douglas-Marini", + order)) + +# Let Discontinuous Lagrange Trace element be alias to HDiv Trace +register_alias("Discontinuous Lagrange Trace", + lambda family, dim, order, degree: ("HDiv Trace", order)) +register_alias("DGT", + lambda family, dim, order, degree: ("HDiv Trace", order)) + +# New elements introduced for the periodic table 2014 +register_element("Q", None, 0, H1, "identity", (1, None), cubes) +register_element("DQ", None, 0, L2, "identity", (0, None), cubes) +register_element("RTCE", None, 1, HCurl, "covariant Piola", (1, None), + ("quadrilateral",)) +register_element("RTCF", None, 1, HDiv, "contravariant Piola", (1, None), + ("quadrilateral",)) +register_element("NCE", None, 1, HCurl, "covariant Piola", (1, None), + ("hexahedron",)) +register_element("NCF", None, 1, HDiv, "contravariant Piola", (1, None), + ("hexahedron",)) + +register_element("S", None, 0, H1, "identity", (1, None), cubes) +register_element("DPC", None, 0, L2, "identity", (0, None), cubes) +register_element("BDMCE", None, 1, HCurl, "covariant Piola", (1, None), + ("quadrilateral",)) +register_element("BDMCF", None, 1, HDiv, "contravariant Piola", (1, None), + ("quadrilateral",)) +register_element("SminusE", "SminusE", 1, HCurl, "covariant Piola", (1, None), cubes[1:3]) +register_element("SminusF", "SminusF", 1, HDiv, "contravariant Piola", (1, None), cubes[1:2]) +register_element("SminusDiv", "SminusDiv", 1, HDiv, "contravariant Piola", (1, None), cubes[1:3]) +register_element("SminusCurl", "SminusCurl", 1, HCurl, "covariant Piola", (1, None), cubes[1:3]) +register_element("AAE", None, 1, HCurl, "covariant Piola", (1, None), + ("hexahedron",)) +register_element("AAF", None, 1, HDiv, "contravariant Piola", (1, None), + ("hexahedron",)) + +# New aliases introduced for the periodic table 2014 +register_alias("P", lambda family, dim, order, degree: ("Lagrange", order)) +register_alias("DP", lambda family, dim, order, + degree: ("Discontinuous Lagrange", order)) +register_alias("RTE", lambda family, dim, order, + degree: ("Nedelec 1st kind H(curl)", order)) +register_alias("RTF", lambda family, dim, order, + degree: ("Raviart-Thomas", order)) +register_alias("N1E", lambda family, dim, order, + degree: ("Nedelec 1st kind H(curl)", order)) +register_alias("N1F", lambda family, dim, order, degree: ("Raviart-Thomas", + order)) + +register_alias("BDME", lambda family, dim, order, + degree: ("Nedelec 2nd kind H(curl)", order)) +register_alias("BDMF", lambda family, dim, order, + degree: ("Brezzi-Douglas-Marini", order)) +register_alias("N2E", lambda family, dim, order, + degree: ("Nedelec 2nd kind H(curl)", order)) +register_alias("N2F", lambda family, dim, order, + degree: ("Brezzi-Douglas-Marini", order)) + +# discontinuous elements using l2 pullbacks +register_element("DPC L2", None, 0, L2, "L2 Piola", (1, None), cubes) +register_element("DQ L2", None, 0, L2, "L2 Piola", (0, None), cubes) +register_element("Gauss-Legendre L2", "GL L2", 0, L2, "L2 Piola", (0, None), + ("interval",)) +register_element("Discontinuous Lagrange L2", "DG L2", 0, L2, "L2 Piola", (0, None), + any_cell) # "DP" + +register_alias("DP L2", lambda family, dim, order, + degree: ("Discontinuous Lagrange L2", order)) + +register_alias("P- Lambda L2", lambda family, dim, order, + degree: feec_element_l2(family, dim, order, degree)) +register_alias("P Lambda L2", lambda family, dim, order, + degree: feec_element_l2(family, dim, order, degree)) +register_alias("Q- Lambda L2", lambda family, dim, order, + degree: feec_element_l2(family, dim, order, degree)) +register_alias("S Lambda L2", lambda family, dim, order, + degree: feec_element_l2(family, dim, order, degree)) + +register_alias("P- L2", lambda family, dim, order, + degree: feec_element_l2(family, dim, order, degree)) +register_alias("Q- L2", lambda family, dim, order, + degree: feec_element_l2(family, dim, order, degree)) + +# mimetic spectral elements - primal and dual complexs +register_element("Extended-Gauss-Legendre", "EGL", 0, H1, "identity", (2, None), + ("interval",)) +register_element("Extended-Gauss-Legendre Edge", "EGL-Edge", 0, L2, "identity", (1, None), + ("interval",)) +register_element("Extended-Gauss-Legendre Edge L2", "EGL-Edge L2", 0, L2, "L2 Piola", (1, None), + ("interval",)) +register_element("Gauss-Lobatto-Legendre Edge", "GLL-Edge", 0, L2, "identity", (0, None), + ("interval",)) +register_element("Gauss-Lobatto-Legendre Edge L2", "GLL-Edge L2", 0, L2, "L2 Piola", (0, None), + ("interval",)) + +# directly-defined serendipity elements ala Arbogast +# currently the theory is only really worked out for quads. +register_element("Direct Serendipity", "Sdirect", 0, H1, "physical", (1, None), + ("quadrilateral",)) +register_element("Direct Serendipity Full H(div)", "Sdirect H(div)", 1, HDiv, "physical", (1, None), + ("quadrilateral",)) +register_element("Direct Serendipity Reduced H(div)", "Sdirect H(div) red", 1, HDiv, "physical", (1, None), + ("quadrilateral",)) + + +# NOTE- the edge elements for primal mimetic spectral elements are accessed by using +# variant='mse' in the appropriate places + +def feec_element(family, n, r, k): + """Finite element exterior calculus notation. + + n = topological dimension of domain + r = polynomial order + k = form_degree + """ + # Note: We always map to edge elements in 2D, don't know how to + # differentiate otherwise? + + # Mapping from (feec name, domain dimension, form degree) to + # (family name, polynomial order) + _feec_elements = { + "P- Lambda": ( + (("P", r), ("DP", r - 1)), + (("P", r), ("RTE", r), ("DP", r - 1)), + (("P", r), ("N1E", r), ("N1F", r), ("DP", r - 1)), + ), + "P Lambda": ( + (("P", r), ("DP", r)), + (("P", r), ("BDME", r), ("DP", r)), + (("P", r), ("N2E", r), ("N2F", r), ("DP", r)), + ), + "Q- Lambda": ( + (("Q", r), ("DQ", r - 1)), + (("Q", r), ("RTCE", r), ("DQ", r - 1)), + (("Q", r), ("NCE", r), ("NCF", r), ("DQ", r - 1)), + ), + "S Lambda": ( + (("S", r), ("DPC", r)), + (("S", r), ("BDMCE", r), ("DPC", r)), + (("S", r), ("AAE", r), ("AAF", r), ("DPC", r)), + ), + } + + # New notation, old verbose notation (including "Lambda") might be + # removed + _feec_elements["P-"] = _feec_elements["P- Lambda"] + _feec_elements["P"] = _feec_elements["P Lambda"] + _feec_elements["Q-"] = _feec_elements["Q- Lambda"] + _feec_elements["S"] = _feec_elements["S Lambda"] + + family, r = _feec_elements[family][n - 1][k] + + return family, r + + +def feec_element_l2(family, n, r, k): + """Finite element exterior calculus notation. + + n = topological dimension of domain + r = polynomial order + k = form_degree + """ + # Note: We always map to edge elements in 2D, don't know how to + # differentiate otherwise? + + # Mapping from (feec name, domain dimension, form degree) to + # (family name, polynomial order) + _feec_elements = { + "P- Lambda L2": ( + (("P", r), ("DP L2", r - 1)), + (("P", r), ("RTE", r), ("DP L2", r - 1)), + (("P", r), ("N1E", r), ("N1F", r), ("DP L2", r - 1)), + ), + "P Lambda L2": ( + (("P", r), ("DP L2", r)), + (("P", r), ("BDME", r), ("DP L2", r)), + (("P", r), ("N2E", r), ("N2F", r), ("DP L2", r)), + ), + "Q- Lambda L2": ( + (("Q", r), ("DQ L2", r - 1)), + (("Q", r), ("RTCE", r), ("DQ L2", r - 1)), + (("Q", r), ("NCE", r), ("NCF", r), ("DQ L2", r - 1)), + ), + "S Lambda L2": ( + (("S", r), ("DPC L2", r)), + (("S", r), ("BDMCE", r), ("DPC L2", r)), + (("S", r), ("AAE", r), ("AAF", r), ("DPC L2", r)), + ), + } + + # New notation, old verbose notation (including "Lambda") might be + # removed + _feec_elements["P- L2"] = _feec_elements["P- Lambda L2"] + _feec_elements["P L2"] = _feec_elements["P Lambda L2"] + _feec_elements["Q- L2"] = _feec_elements["Q- Lambda L2"] + _feec_elements["S L2"] = _feec_elements["S Lambda L2"] + + family, r = _feec_elements[family][n - 1][k] + + return family, r + + +# General FEEC notation, old verbose (can be removed) +register_alias("P- Lambda", lambda family, dim, order, + degree: feec_element(family, dim, order, degree)) +register_alias("P Lambda", lambda family, dim, order, + degree: feec_element(family, dim, order, degree)) +register_alias("Q- Lambda", lambda family, dim, order, + degree: feec_element(family, dim, order, degree)) +register_alias("S Lambda", lambda family, dim, order, + degree: feec_element(family, dim, order, degree)) + +# General FEEC notation, new compact notation +register_alias("P-", lambda family, dim, order, + degree: feec_element(family, dim, order, degree)) +register_alias("Q-", lambda family, dim, order, + degree: feec_element(family, dim, order, degree)) + + +def canonical_element_description(family, cell, order, form_degree): + """Given basic element information, return corresponding element information on canonical form. + + Input: family, cell, (polynomial) order, form_degree + Output: family (canonical), short_name (for printing), order, value shape, + reference value shape, sobolev_space. + + This is used by the FiniteElement constructor to ved input + data against the element list and aliases defined in ufl. + """ + # Get domain dimensions + if cell is not None: + tdim = cell.topological_dimension() + gdim = cell.geometric_dimension() + if isinstance(cell, Cell): + cellname = cell.cellname() + else: + cellname = None + else: + tdim = None + gdim = None + cellname = None + + # Catch general FEEC notation "P" and "S" + if form_degree is not None and family in ("P", "S"): + family, order = feec_element(family, tdim, order, form_degree) + + if form_degree is not None and family in ("P L2", "S L2"): + family, order = feec_element_l2(family, tdim, order, form_degree) + + # Check whether this family is an alias for something else + while family in aliases: + if tdim is None: + raise ValueError("Need dimension to handle element aliases.") + (family, order) = aliases[family](family, tdim, order, form_degree) + + # Check that the element family exists + if family not in ufl_elements: + raise ValueError(f"Unknown finite element '{family}'.") + + # Check that element data is valid (and also get common family + # name) + (family, short_name, value_rank, sobolev_space, mapping, krange, cellnames) = ufl_elements[family] + + # Accept CG/DG on all kind of cells, but use Q/DQ on "product" cells + if cellname in set(cubes) - set(simplices) or isinstance(cell, TensorProductCell): + if family == "Lagrange": + family = "Q" + elif family == "Discontinuous Lagrange": + if order >= 1: + warnings.warn("Discontinuous Lagrange element requested on %s, creating DQ element." % cell.cellname()) + family = "DQ" + elif family == "Discontinuous Lagrange L2": + if order >= 1: + warnings.warn(f"Discontinuous Lagrange L2 element requested on {cell.cellname()}, " + "creating DQ L2 element.") + family = "DQ L2" + + # Validate cellname if a valid cell is specified + if not (cellname is None or cellname in cellnames): + raise ValueError(f"Cellname '{cellname}' invalid for '{family}' finite element.") + + # Validate order if specified + if order is not None: + if krange is None: + raise ValueError(f"Order {order} invalid for '{family}' finite element, should be None.") + kmin, kmax = krange + if not (kmin is None or (asarray(order) >= kmin).all()): + raise ValueError(f"Order {order} invalid for '{family}' finite element.") + if not (kmax is None or (asarray(order) <= kmax).all()): + raise ValueError(f"Order {istr(order)} invalid for '{family}' finite element.") + + if value_rank == 2: + # Tensor valued fundamental elements in HEin have this shape + if gdim is None or tdim is None: + raise ValueError("Cannot infer shape of element without topological and geometric dimensions.") + reference_value_shape = (tdim, tdim) + value_shape = (gdim, gdim) + elif value_rank == 1: + # Vector valued fundamental elements in HDiv and HCurl have a shape + if gdim is None or tdim is None: + raise ValueError("Cannot infer shape of element without topological and geometric dimensions.") + reference_value_shape = (tdim,) + value_shape = (gdim,) + elif value_rank == 0: + # All other elements are scalar values + reference_value_shape = () + value_shape = () + else: + raise ValueError(f"Invalid value rank {value_rank}.") + + return family, short_name, order, value_shape, reference_value_shape, sobolev_space, mapping diff --git a/finat/ufl/enrichedelement.py b/finat/ufl/enrichedelement.py new file mode 100644 index 000000000..1b7a159fe --- /dev/null +++ b/finat/ufl/enrichedelement.py @@ -0,0 +1,165 @@ +"""This module defines the UFL finite element classes.""" + +# Copyright (C) 2008-2016 Martin Sandve Alnæs +# +# This file was originally part of UFL (https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later +# +# Modified by Kristian B. Oelgaard +# Modified by Marie E. Rognes 2010, 2012 +# Modified by Massimiliano Leoni, 2016 +# Modified by Matthew Scroggs, 2023 + +from finat.ufl.finiteelementbase import FiniteElementBase + + +class EnrichedElementBase(FiniteElementBase): + """The vector sum of several finite element spaces.""" + + def __init__(self, *elements): + """Doc.""" + self._elements = elements + + cell = elements[0].cell + if not all(e.cell == cell for e in elements[1:]): + raise ValueError("Cell mismatch for sub elements of enriched element.") + + if isinstance(elements[0].degree(), int): + degrees = {e.degree() for e in elements} - {None} + degree = max(degrees) if degrees else None + else: + degree = tuple(map(max, zip(*[e.degree() for e in elements]))) + + # We can allow the scheme not to be defined, but all defined + # should be equal + quad_schemes = [e.quadrature_scheme() for e in elements] + quad_schemes = [qs for qs in quad_schemes if qs is not None] + quad_scheme = quad_schemes[0] if quad_schemes else None + if not all(qs == quad_scheme for qs in quad_schemes): + raise ValueError("Quadrature scheme mismatch.") + + value_shape = elements[0].value_shape + if not all(e.value_shape == value_shape for e in elements[1:]): + raise ValueError("Element value shape mismatch.") + + reference_value_shape = elements[0].reference_value_shape + if not all(e.reference_value_shape == reference_value_shape for e in elements[1:]): + raise ValueError("Element reference value shape mismatch.") + + # mapping = elements[0].mapping() # FIXME: This fails for a mixed subelement here. + # if not all(e.mapping() == mapping for e in elements[1:]): + # raise ValueError("Element mapping mismatch.") + + # Get name of subclass: EnrichedElement or NodalEnrichedElement + class_name = self.__class__.__name__ + + # Initialize element data + FiniteElementBase.__init__(self, class_name, cell, degree, + quad_scheme, value_shape, + reference_value_shape) + + def mapping(self): + """Doc.""" + return self._elements[0].mapping() + + @property + def sobolev_space(self): + """Return the underlying Sobolev space.""" + elements = [e for e in self._elements] + if all(e.sobolev_space == elements[0].sobolev_space + for e in elements): + return elements[0].sobolev_space + else: + # Find smallest shared Sobolev space over all sub elements + spaces = [e.sobolev_space for e in elements] + superspaces = [{s} | set(s.parents) for s in spaces] + intersect = set.intersection(*superspaces) + for s in intersect.copy(): + for parent in s.parents: + intersect.discard(parent) + + sobolev_space, = intersect + return sobolev_space + + def variant(self): + """Doc.""" + try: + variant, = {e.variant() for e in self._elements} + return variant + except ValueError: + return None + + def reconstruct(self, **kwargs): + """Doc.""" + return type(self)(*[e.reconstruct(**kwargs) for e in self._elements]) + + @property + def embedded_subdegree(self): + """Return embedded subdegree.""" + if isinstance(self._degree, int): + return self._degree + else: + return min(e.embedded_subdegree for e in self._elements) + + @property + def embedded_superdegree(self): + """Return embedded superdegree.""" + if isinstance(self._degree, int): + return self._degree + else: + return max(e.embedded_superdegree for e in self._elements) + + +class EnrichedElement(EnrichedElementBase): + r"""The vector sum of several finite element spaces. + + .. math:: \\textrm{EnrichedElement}(V, Q) = \\{v + q | v \\in V, q \\in Q\\}. + + Dual basis is a concatenation of subelements dual bases; + primal basis is a concatenation of subelements primal bases; + resulting element is not nodal even when subelements are. + Structured basis may be exploited in form compilers. + """ + + def is_cellwise_constant(self): + """Return whether the basis functions of this element is spatially constant over each cell.""" + return all(e.is_cellwise_constant() for e in self._elements) + + def __repr__(self): + """Doc.""" + return "EnrichedElement(" + ", ".join(repr(e) for e in self._elements) + ")" + + def __str__(self): + """Format as string for pretty printing.""" + return "<%s>" % " + ".join(str(e) for e in self._elements) + + def shortstr(self): + """Format as string for pretty printing.""" + return "<%s>" % " + ".join(e.shortstr() for e in self._elements) + + +class NodalEnrichedElement(EnrichedElementBase): + r"""The vector sum of several finite element spaces. + + .. math:: \\textrm{EnrichedElement}(V, Q) = \\{v + q | v \\in V, q \\in Q\\}. + + Primal basis is reorthogonalized to dual basis which is + a concatenation of subelements dual bases; resulting + element is nodal. + """ + def is_cellwise_constant(self): + """Return whether the basis functions of this element is spatially constant over each cell.""" + return False + + def __repr__(self): + """Doc.""" + return "NodalEnrichedElement(" + ", ".join(repr(e) for e in self._elements) + ")" + + def __str__(self): + """Format as string for pretty printing.""" + return "" % ", ".join(str(e) for e in self._elements) + + def shortstr(self): + """Format as string for pretty printing.""" + return "NodalEnriched(%s)" % ", ".join(e.shortstr() for e in self._elements) diff --git a/finat/ufl/finiteelement.py b/finat/ufl/finiteelement.py new file mode 100644 index 000000000..2d240e048 --- /dev/null +++ b/finat/ufl/finiteelement.py @@ -0,0 +1,236 @@ +"""This module defines the UFL finite element classes.""" +# Copyright (C) 2008-2016 Martin Sandve Alnæs +# +# This file was originally part of UFL (https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later +# +# Modified by Kristian B. Oelgaard +# Modified by Marie E. Rognes 2010, 2012 +# Modified by Anders Logg 2014 +# Modified by Massimiliano Leoni, 2016 +# Modified by Matthew Scroggs, 2023 + +from ufl.cell import TensorProductCell, as_cell +from finat.ufl.elementlist import canonical_element_description, simplices +from finat.ufl.finiteelementbase import FiniteElementBase +from ufl.utils.formatting import istr + + +class FiniteElement(FiniteElementBase): + """The basic finite element class for all simple finite elements.""" + # TODO: Move these to base? + __slots__ = ("_short_name", "_sobolev_space", + "_mapping", "_variant", "_repr") + + def __new__(cls, + family, + cell=None, + degree=None, + form_degree=None, + quad_scheme=None, + variant=None): + """Intercepts construction to expand CG, DG, RTCE and RTCF spaces on TensorProductCells.""" + if cell is not None: + cell = as_cell(cell) + + if isinstance(cell, TensorProductCell): + # Delay import to avoid circular dependency at module load time + from finat.ufl.enrichedelement import EnrichedElement + from finat.ufl.hdivcurl import HCurlElement as HCurl + from finat.ufl.hdivcurl import HDivElement as HDiv + from finat.ufl.tensorproductelement import TensorProductElement + + family, short_name, degree, value_shape, reference_value_shape, sobolev_space, mapping = \ + canonical_element_description(family, cell, degree, form_degree) + + if family in ["RTCF", "RTCE"]: + cell_h, cell_v = cell.sub_cells() + if cell_h.cellname() != "interval": + raise ValueError(f"{family} is available on TensorProductCell(interval, interval) only.") + if cell_v.cellname() != "interval": + raise ValueError(f"{family} is available on TensorProductCell(interval, interval) only.") + + C_elt = FiniteElement("CG", "interval", degree, variant=variant) + D_elt = FiniteElement("DG", "interval", degree - 1, variant=variant) + + CxD_elt = TensorProductElement(C_elt, D_elt, cell=cell) + DxC_elt = TensorProductElement(D_elt, C_elt, cell=cell) + + if family == "RTCF": + return EnrichedElement(HDiv(CxD_elt), HDiv(DxC_elt)) + if family == "RTCE": + return EnrichedElement(HCurl(CxD_elt), HCurl(DxC_elt)) + + elif family == "NCF": + cell_h, cell_v = cell.sub_cells() + if cell_h.cellname() != "quadrilateral": + raise ValueError(f"{family} is available on TensorProductCell(quadrilateral, interval) only.") + if cell_v.cellname() != "interval": + raise ValueError(f"{family} is available on TensorProductCell(quadrilateral, interval) only.") + + Qc_elt = FiniteElement("RTCF", "quadrilateral", degree, variant=variant) + Qd_elt = FiniteElement("DQ", "quadrilateral", degree - 1, variant=variant) + + Id_elt = FiniteElement("DG", "interval", degree - 1, variant=variant) + Ic_elt = FiniteElement("CG", "interval", degree, variant=variant) + + return EnrichedElement(HDiv(TensorProductElement(Qc_elt, Id_elt, cell=cell)), + HDiv(TensorProductElement(Qd_elt, Ic_elt, cell=cell))) + + elif family == "NCE": + cell_h, cell_v = cell.sub_cells() + if cell_h.cellname() != "quadrilateral": + raise ValueError(f"{family} is available on TensorProductCell(quadrilateral, interval) only.") + if cell_v.cellname() != "interval": + raise ValueError(f"{family} is available on TensorProductCell(quadrilateral, interval) only.") + + Qc_elt = FiniteElement("Q", "quadrilateral", degree, variant=variant) + Qd_elt = FiniteElement("RTCE", "quadrilateral", degree, variant=variant) + + Id_elt = FiniteElement("DG", "interval", degree - 1, variant=variant) + Ic_elt = FiniteElement("CG", "interval", degree, variant=variant) + + return EnrichedElement(HCurl(TensorProductElement(Qc_elt, Id_elt, cell=cell)), + HCurl(TensorProductElement(Qd_elt, Ic_elt, cell=cell))) + + elif family == "Q": + return TensorProductElement(*[FiniteElement("CG", c, degree, variant=variant) + for c in cell.sub_cells()], + cell=cell) + + elif family == "DQ": + def dq_family(cell): + """Doc.""" + return "DG" if cell.cellname() in simplices else "DQ" + return TensorProductElement(*[FiniteElement(dq_family(c), c, degree, variant=variant) + for c in cell.sub_cells()], + cell=cell) + + elif family == "DQ L2": + def dq_family_l2(cell): + """Doc.""" + return "DG L2" if cell.cellname() in simplices else "DQ L2" + return TensorProductElement(*[FiniteElement(dq_family_l2(c), c, degree, variant=variant) + for c in cell.sub_cells()], + cell=cell) + + return super(FiniteElement, cls).__new__(cls) + + def __init__(self, + family, + cell=None, + degree=None, + form_degree=None, + quad_scheme=None, + variant=None): + """Create finite element. + + Args: + family: The finite element family + cell: The geometric cell + degree: The polynomial degree (optional) + form_degree: The form degree (FEEC notation, used when field is + viewed as k-form) + quad_scheme: The quadrature scheme (optional) + variant: Hint for the local basis function variant (optional) + """ + # Note: Unfortunately, dolfin sometimes passes None for + # cell. Until this is fixed, allow it: + if cell is not None: + cell = as_cell(cell) + + ( + family, short_name, degree, value_shape, reference_value_shape, sobolev_space, mapping + ) = canonical_element_description(family, cell, degree, form_degree) + + # TODO: Move these to base? Might be better to instead + # simplify base though. + self._sobolev_space = sobolev_space + self._mapping = mapping + self._short_name = short_name or family + self._variant = variant + + # Type check variant + if variant is not None and not isinstance(variant, str): + raise ValueError("Illegal variant: must be string or None") + + # Initialize element data + FiniteElementBase.__init__(self, family, cell, degree, quad_scheme, + value_shape, reference_value_shape) + + # Cache repr string + qs = self.quadrature_scheme() + if qs is None: + quad_str = "" + else: + quad_str = ", quad_scheme=%s" % repr(qs) + v = self.variant() + if v is None: + var_str = "" + else: + var_str = ", variant=%s" % repr(v) + self._repr = "FiniteElement(%s, %s, %s%s%s)" % ( + repr(self.family()), repr(self.cell), repr(self.degree()), quad_str, var_str) + assert '"' not in self._repr + + def __repr__(self): + """Format as string for evaluation as Python object.""" + return self._repr + + def _is_globally_constant(self): + """Doc.""" + return self.family() == "Real" + + def _is_linear(self): + """Doc.""" + return self.family() == "Lagrange" and self.degree() == 1 + + def mapping(self): + """Return the mapping type for this element .""" + return self._mapping + + @property + def sobolev_space(self): + """Return the underlying Sobolev space.""" + return self._sobolev_space + + def variant(self): + """Return the variant used to initialise the element.""" + return self._variant + + def reconstruct(self, family=None, cell=None, degree=None, quad_scheme=None, variant=None): + """Construct a new FiniteElement object with some properties replaced with new values.""" + if family is None: + family = self.family() + if cell is None: + cell = self.cell + if degree is None: + degree = self.degree() + if quad_scheme is None: + quad_scheme = self.quadrature_scheme() + if variant is None: + variant = self.variant() + return FiniteElement(family, cell, degree, quad_scheme=quad_scheme, variant=variant) + + def __str__(self): + """Format as string for pretty printing.""" + qs = self.quadrature_scheme() + qs = "" if qs is None else "(%s)" % qs + v = self.variant() + v = "" if v is None else "(%s)" % v + return "<%s%s%s%s on a %s>" % (self._short_name, istr(self.degree()), + qs, v, self.cell) + + def shortstr(self): + """Format as string for pretty printing.""" + return f"{self._short_name}{istr(self.degree())}({self.quadrature_scheme()},{istr(self.variant())})" + + def __getnewargs__(self): + """Return the arguments which pickle needs to recreate the object.""" + return (self.family(), + self.cell, + self.degree(), + None, + self.quadrature_scheme(), + self.variant()) diff --git a/finat/ufl/finiteelementbase.py b/finat/ufl/finiteelementbase.py new file mode 100644 index 000000000..08c500316 --- /dev/null +++ b/finat/ufl/finiteelementbase.py @@ -0,0 +1,277 @@ +"""This module defines the UFL finite element classes.""" + +# Copyright (C) 2008-2016 Martin Sandve Alnæs +# +# This file was originally part of UFL (https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later +# +# Modified by Kristian B. Oelgaard +# Modified by Marie E. Rognes 2010, 2012 +# Modified by Massimiliano Leoni, 2016 +# Modified by Matthew Scroggs, 2023 + +from abc import abstractmethod, abstractproperty + +from ufl import pullback +from ufl.cell import AbstractCell, as_cell +from ufl.finiteelement import AbstractFiniteElement +from ufl.utils.sequences import product + + +class FiniteElementBase(AbstractFiniteElement): + """Base class for all finite elements.""" + __slots__ = ("_family", "_cell", "_degree", "_quad_scheme", + "_value_shape", "_reference_value_shape") + + # TODO: Not all these should be in the base class! In particular + # family, degree, and quad_scheme do not belong here. + def __init__(self, family, cell, degree, quad_scheme, value_shape, + reference_value_shape): + """Initialize basic finite element data.""" + if not (degree is None or isinstance(degree, (int, tuple))): + raise ValueError("Invalid degree type.") + if not isinstance(value_shape, tuple): + raise ValueError("Invalid value_shape type.") + if not isinstance(reference_value_shape, tuple): + raise ValueError("Invalid reference_value_shape type.") + + if cell is not None: + cell = as_cell(cell) + if not isinstance(cell, AbstractCell): + raise ValueError("Invalid cell type.") + + self._family = family + self._cell = cell + self._degree = degree + self._value_shape = value_shape + self._reference_value_shape = reference_value_shape + self._quad_scheme = quad_scheme + + @abstractmethod + def __repr__(self): + """Format as string for evaluation as Python object.""" + pass + + @abstractproperty + def sobolev_space(self): + """Return the underlying Sobolev space.""" + pass + + @abstractmethod + def mapping(self): + """Return the mapping type for this element.""" + pass + + def _is_globally_constant(self): + """Check if the element is a global constant. + + For Real elements, this should return True. + """ + return False + + def _is_linear(self): + """Check if the element is Lagrange degree 1.""" + return False + + def _ufl_hash_data_(self): + """Doc.""" + return repr(self) + + def _ufl_signature_data_(self): + """Doc.""" + return repr(self) + + def __hash__(self): + """Compute hash code for insertion in hashmaps.""" + return hash(self._ufl_hash_data_()) + + def __eq__(self, other): + """Compute element equality for insertion in hashmaps.""" + return type(self) is type(other) and self._ufl_hash_data_() == other._ufl_hash_data_() + + def __ne__(self, other): + """Compute element inequality for insertion in hashmaps.""" + return not self.__eq__(other) + + def __lt__(self, other): + """Compare elements by repr, to give a natural stable sorting.""" + return repr(self) < repr(other) + + def family(self): # FIXME: Undefined for base? + """Return finite element family.""" + return self._family + + def variant(self): + """Return the variant used to initialise the element.""" + return None + + def degree(self, component=None): + """Return polynomial degree of finite element.""" + return self._degree + + def quadrature_scheme(self): + """Return quadrature scheme of finite element.""" + return self._quad_scheme + + @property + def cell(self): + """Return cell of finite element.""" + return self._cell + + def is_cellwise_constant(self, component=None): + """Return whether the basis functions of this element is spatially constant over each cell.""" + return self._is_globally_constant() or self.degree() == 0 + + @property + def value_shape(self): + """Return the shape of the value space on the global domain.""" + return self._value_shape + + @property + def reference_value_shape(self): + """Return the shape of the value space on the reference cell.""" + return self._reference_value_shape + + @property + def value_size(self): + """Return the integer product of the value shape.""" + return product(self.value_shape) + + @property + def reference_value_size(self): + """Return the integer product of the reference value shape.""" + return product(self.reference_value_shape) + + def symmetry(self): # FIXME: different approach + r"""Return the symmetry dict. + + This is a mapping :math:`c_0 \\to c_1` + meaning that component :math:`c_0` is represented by component + :math:`c_1`. + A component is a tuple of one or more ints. + """ + return {} + + def _check_component(self, i): + """Check that component index i is valid.""" + sh = self.value_shape + r = len(sh) + if not (len(i) == r and all(j < k for (j, k) in zip(i, sh))): + raise ValueError( + f"Illegal component index {i} (value rank {len(i)}) " + f"for element (value rank {r}).") + + def extract_subelement_component(self, i): + """Extract direct subelement index and subelement relative component index for a given component index.""" + if isinstance(i, int): + i = (i,) + self._check_component(i) + return (None, i) + + def extract_component(self, i): + """Recursively extract component index relative to a (simple) element. + + and that element for given value component index. + """ + if isinstance(i, int): + i = (i,) + self._check_component(i) + return (i, self) + + def _check_reference_component(self, i): + """Check that reference component index i is valid.""" + sh = self.value_shape + r = len(sh) + if not (len(i) == r and all(j < k for (j, k) in zip(i, sh))): + raise ValueError( + f"Illegal component index {i} (value rank {len(i)}) " + f"for element (value rank {r}).") + + def extract_subelement_reference_component(self, i): + """Extract direct subelement index and subelement relative. + + reference component index for a given reference component index. + """ + if isinstance(i, int): + i = (i,) + self._check_reference_component(i) + return (None, i) + + def extract_reference_component(self, i): + """Recursively extract reference component index relative to a (simple) element. + + and that element for given reference value component index. + """ + if isinstance(i, int): + i = (i,) + self._check_reference_component(i) + return (i, self) + + @property + def num_sub_elements(self): + """Return number of sub-elements.""" + return 0 + + @property + def sub_elements(self): + """Return list of sub-elements.""" + return [] + + def __add__(self, other): + """Add two elements, creating an enriched element.""" + if not isinstance(other, FiniteElementBase): + raise ValueError(f"Can't add element and {other.__class__}.") + from finat.ufl import EnrichedElement + return EnrichedElement(self, other) + + def __mul__(self, other): + """Multiply two elements, creating a mixed element.""" + if not isinstance(other, FiniteElementBase): + raise ValueError("Can't multiply element and {other.__class__}.") + from finat.ufl import MixedElement + return MixedElement(self, other) + + def __getitem__(self, index): + """Restrict finite element to a subdomain, subcomponent or topology (cell).""" + if index in ("facet", "interior"): + from finat.ufl import RestrictedElement + return RestrictedElement(self, index) + else: + raise KeyError(f"Invalid index for restriction: {repr(index)}") + + def __iter__(self): + """Iter.""" + raise TypeError(f"'{type(self).__name__}' object is not iterable") + + @property + def embedded_superdegree(self): + """Doc.""" + return self.degree() + + @property + def embedded_subdegree(self): + """Doc.""" + return self.degree() + + @property + def pullback(self): + """Get the pull back.""" + if self.mapping() == "identity": + return pullback.identity_pullback + elif self.mapping() == "L2 Piola": + return pullback.l2_piola + elif self.mapping() == "covariant Piola": + return pullback.covariant_piola + elif self.mapping() == "contravariant Piola": + return pullback.contravariant_piola + elif self.mapping() == "double covariant Piola": + return pullback.double_covariant_piola + elif self.mapping() == "double contravariant Piola": + return pullback.double_contravariant_piola + elif self.mapping() == "custom": + return pullback.custom_pullback + elif self.mapping() == "physical": + return pullback.physical_pullback + + raise ValueError(f"Unsupported mapping: {self.mapping()}") diff --git a/finat/ufl/hdivcurl.py b/finat/ufl/hdivcurl.py new file mode 100644 index 000000000..682105e7e --- /dev/null +++ b/finat/ufl/hdivcurl.py @@ -0,0 +1,214 @@ +"""Doc.""" +# Copyright (C) 2008-2016 Andrew T. T. McRae +# +# This file was originally part of UFL (https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later +# +# Modified by Massimiliano Leoni, 2016 +# Modified by Matthew Scroggs, 2023 + +from finat.ufl.finiteelementbase import FiniteElementBase +from ufl.sobolevspace import L2, HCurl, HDiv + + +class HDivElement(FiniteElementBase): + """A div-conforming version of an outer product element, assuming this makes mathematical sense.""" + __slots__ = ("_element", ) + + def __init__(self, element): + """Doc.""" + self._element = element + + family = "TensorProductElement" + cell = element.cell + degree = element.degree() + quad_scheme = element.quadrature_scheme() + value_shape = (element.cell.geometric_dimension(),) + reference_value_shape = (element.cell.topological_dimension(),) + + # Skipping TensorProductElement constructor! Bad code smell, refactor to avoid this non-inheritance somehow. + FiniteElementBase.__init__(self, family, cell, degree, + quad_scheme, value_shape, reference_value_shape) + + def __repr__(self): + """Doc.""" + return f"HDivElement({repr(self._element)})" + + def mapping(self): + """Doc.""" + return "contravariant Piola" + + @property + def sobolev_space(self): + """Return the underlying Sobolev space.""" + return HDiv + + def reconstruct(self, **kwargs): + """Doc.""" + return HDivElement(self._element.reconstruct(**kwargs)) + + def variant(self): + """Doc.""" + return self._element.variant() + + def __str__(self): + """Doc.""" + return f"HDivElement({repr(self._element)})" + + def shortstr(self): + """Format as string for pretty printing.""" + return f"HDivElement({self._element.shortstr()})" + + @property + def embedded_subdegree(self): + """Return embedded subdegree.""" + return self._element.embedded_subdegree + + @property + def embedded_superdegree(self): + """Return embedded superdegree.""" + return self._element.embedded_superdegree + + +class HCurlElement(FiniteElementBase): + """A curl-conforming version of an outer product element, assuming this makes mathematical sense.""" + __slots__ = ("_element",) + + def __init__(self, element): + """Doc.""" + self._element = element + + family = "TensorProductElement" + cell = element.cell + degree = element.degree() + quad_scheme = element.quadrature_scheme() + cell = element.cell + value_shape = (cell.geometric_dimension(),) + reference_value_shape = (cell.topological_dimension(),) # TODO: Is this right? + # Skipping TensorProductElement constructor! Bad code smell, + # refactor to avoid this non-inheritance somehow. + FiniteElementBase.__init__(self, family, cell, degree, quad_scheme, + value_shape, reference_value_shape) + + def __repr__(self): + """Doc.""" + return f"HCurlElement({repr(self._element)})" + + def mapping(self): + """Doc.""" + return "covariant Piola" + + @property + def sobolev_space(self): + """Return the underlying Sobolev space.""" + return HCurl + + def reconstruct(self, **kwargs): + """Doc.""" + return HCurlElement(self._element.reconstruct(**kwargs)) + + def variant(self): + """Doc.""" + return self._element.variant() + + def __str__(self): + """Doc.""" + return f"HCurlElement({repr(self._element)})" + + def shortstr(self): + """Format as string for pretty printing.""" + return f"HCurlElement({self._element.shortstr()})" + + +class WithMapping(FiniteElementBase): + """Specify an alternative mapping for the wrappee. + + For example, + to use identity mapping instead of Piola map with an element E, + write + remapped = WithMapping(E, "identity") + """ + + def __init__(self, wrapee, mapping): + """Doc.""" + if mapping == "symmetries": + raise ValueError("Can't change mapping to 'symmetries'") + self._mapping = mapping + self.wrapee = wrapee + + def __getattr__(self, attr): + """Doc.""" + try: + return getattr(self.wrapee, attr) + except AttributeError: + raise AttributeError("'%s' object has no attribute '%s'" % + (type(self).__name__, attr)) + + def __repr__(self): + """Doc.""" + return f"WithMapping({repr(self.wrapee)}, '{self._mapping}')" + + @property + def value_shape(self): + """Doc.""" + gdim = self.cell.geometric_dimension() + mapping = self.mapping() + if mapping in {"covariant Piola", "contravariant Piola"}: + return (gdim,) + elif mapping in {"double covariant Piola", "double contravariant Piola"}: + return (gdim, gdim) + else: + return self.wrapee.value_shape + + @property + def reference_value_shape(self): + """Doc.""" + tdim = self.cell.topological_dimension() + mapping = self.mapping() + if mapping in {"covariant Piola", "contravariant Piola"}: + return (tdim,) + elif mapping in {"double covariant Piola", "double contravariant Piola"}: + return (tdim, tdim) + else: + return self.wrapee.reference_value_shape + + def mapping(self): + """Doc.""" + return self._mapping + + @property + def sobolev_space(self): + """Return the underlying Sobolev space.""" + if self.wrapee.mapping() == self.mapping(): + return self.wrapee.sobolev_space + else: + return L2 + + def reconstruct(self, **kwargs): + """Doc.""" + mapping = kwargs.pop("mapping", self._mapping) + wrapee = self.wrapee.reconstruct(**kwargs) + return type(self)(wrapee, mapping) + + def variant(self): + """Doc.""" + return self.wrapee.variant() + + def __str__(self): + """Doc.""" + return f"WithMapping({repr(self.wrapee)}, {self._mapping})" + + def shortstr(self): + """Doc.""" + return f"WithMapping({self.wrapee.shortstr()}, {self._mapping})" + + @property + def embedded_subdegree(self): + """Return embedded subdegree.""" + return self._element.embedded_subdegree + + @property + def embedded_superdegree(self): + """Return embedded superdegree.""" + return self._element.embedded_superdegree diff --git a/finat/ufl/mixedelement.py b/finat/ufl/mixedelement.py new file mode 100644 index 000000000..a6f8fbadb --- /dev/null +++ b/finat/ufl/mixedelement.py @@ -0,0 +1,563 @@ +"""This module defines the UFL finite element classes.""" +# Copyright (C) 2008-2016 Martin Sandve Alnæs +# +# This file was originally part of UFL (https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later +# +# Modified by Kristian B. Oelgaard +# Modified by Marie E. Rognes 2010, 2012 +# Modified by Anders Logg 2014 +# Modified by Massimiliano Leoni, 2016 +# Modified by Matthew Scroggs, 2023 + +import numpy as np + +from ufl.cell import as_cell +from finat.ufl.finiteelement import FiniteElement +from finat.ufl.finiteelementbase import FiniteElementBase +from ufl.permutation import compute_indices +from ufl.pullback import IdentityPullback, MixedPullback, SymmetricPullback +from ufl.utils.indexflattening import flatten_multiindex, shape_to_strides, unflatten_index +from ufl.utils.sequences import max_degree, product + + +class MixedElement(FiniteElementBase): + """A finite element composed of a nested hierarchy of mixed or simple elements.""" + __slots__ = ("_sub_elements", "_cells") + + def __init__(self, *elements, **kwargs): + """Create mixed finite element from given list of elements.""" + if type(self) is MixedElement: + if kwargs: + raise ValueError("Not expecting keyword arguments to MixedElement constructor.") + + # Un-nest arguments if we get a single argument with a list of elements + if len(elements) == 1 and isinstance(elements[0], (tuple, list)): + elements = elements[0] + # Interpret nested tuples as sub-mixedelements recursively + elements = [MixedElement(e) if isinstance(e, (tuple, list)) else e + for e in elements] + self._sub_elements = elements + + # Pick the first cell, for now all should be equal + cells = tuple(sorted(set(element.cell for element in elements) - set([None]))) + self._cells = cells + if cells: + cell = cells[0] + # Require that all elements are defined on the same cell + if not all(c == cell for c in cells[1:]): + raise ValueError("Sub elements must live on the same cell.") + else: + cell = None + + # Check that all elements use the same quadrature scheme TODO: + # We can allow the scheme not to be defined. + if len(elements) == 0: + quad_scheme = None + else: + quad_scheme = elements[0].quadrature_scheme() + if not all(e.quadrature_scheme() == quad_scheme for e in elements): + raise ValueError("Quadrature scheme mismatch for sub elements of mixed element.") + + # Compute value sizes in global and reference configurations + value_size_sum = sum(product(s.value_shape) for s in self._sub_elements) + reference_value_size_sum = sum(product(s.reference_value_shape) for s in self._sub_elements) + + # Default value shape: Treated simply as all subelement values + # unpacked in a vector. + value_shape = kwargs.get('value_shape', (value_size_sum,)) + + # Default reference value shape: Treated simply as all + # subelement reference values unpacked in a vector. + reference_value_shape = kwargs.get('reference_value_shape', (reference_value_size_sum,)) + + # Validate value_shape (deliberately not for subclasses + # VectorElement and TensorElement) + if type(self) is MixedElement: + # This is not valid for tensor elements with symmetries, + # assume subclasses deal with their own validation + if product(value_shape) != value_size_sum: + raise ValueError("Provided value_shape doesn't match the " + "total value size of all subelements.") + + # Initialize element data + degrees = {e.degree() for e in self._sub_elements} - {None} + degree = max_degree(degrees) if degrees else None + FiniteElementBase.__init__(self, "Mixed", cell, degree, quad_scheme, + value_shape, reference_value_shape) + + def __repr__(self): + """Doc.""" + return "MixedElement(" + ", ".join(repr(e) for e in self._sub_elements) + ")" + + def _is_linear(self): + """Doc.""" + return all(i._is_linear() for i in self._sub_elements) + + def reconstruct_from_elements(self, *elements): + """Reconstruct a mixed element from new subelements.""" + if all(a == b for (a, b) in zip(elements, self._sub_elements)): + return self + return MixedElement(*elements) + + def symmetry(self): + r"""Return the symmetry dict, which is a mapping :math:`c_0 \\to c_1`. + + meaning that component :math:`c_0` is represented by component + :math:`c_1`. + A component is a tuple of one or more ints. + """ + # Build symmetry map from symmetries of subelements + sm = {} + # Base index of the current subelement into mixed value + j = 0 + for e in self._sub_elements: + sh = e.value_shape + st = shape_to_strides(sh) + # Map symmetries of subelement into index space of this + # element + for c0, c1 in e.symmetry().items(): + j0 = flatten_multiindex(c0, st) + j + j1 = flatten_multiindex(c1, st) + j + sm[(j0,)] = (j1,) + # Update base index for next element + j += product(sh) + if j != product(self.value_shape): + raise ValueError("Size mismatch in symmetry algorithm.") + return sm or {} + + @property + def sobolev_space(self): + """Doc.""" + return max(e.sobolev_space for e in self._sub_elements) + + def mapping(self): + """Doc.""" + if all(e.mapping() == "identity" for e in self._sub_elements): + return "identity" + else: + return "undefined" + + @property + def num_sub_elements(self): + """Return number of sub elements.""" + return len(self._sub_elements) + + @property + def sub_elements(self): + """Return list of sub elements.""" + return self._sub_elements + + def extract_subelement_component(self, i): + """Extract direct subelement index and subelement relative. + + component index for a given component index. + """ + if isinstance(i, int): + i = (i,) + self._check_component(i) + + # Select between indexing modes + if len(self.value_shape) == 1: + # Indexing into a long vector of flattened subelement + # shapes + j, = i + + # Find subelement for this index + for sub_element_index, e in enumerate(self._sub_elements): + sh = e.value_shape + si = product(sh) + if j < si: + break + j -= si + if j < 0: + raise ValueError("Moved past last value component!") + + # Convert index into a shape tuple + st = shape_to_strides(sh) + component = unflatten_index(j, st) + else: + # Indexing into a multidimensional tensor where subelement + # index is first axis + sub_element_index = i[0] + if sub_element_index >= len(self._sub_elements): + raise ValueError(f"Illegal component index (dimension {sub_element_index}).") + component = i[1:] + return (sub_element_index, component) + + def extract_component(self, i): + """Recursively extract component index relative to a (simple) element. + + and that element for given value component index. + """ + sub_element_index, component = self.extract_subelement_component(i) + return self._sub_elements[sub_element_index].extract_component(component) + + def extract_subelement_reference_component(self, i): + """Extract direct subelement index and subelement relative. + + reference_component index for a given reference_component index. + """ + if isinstance(i, int): + i = (i,) + self._check_reference_component(i) + + # Select between indexing modes + assert len(self.reference_value_shape) == 1 + # Indexing into a long vector of flattened subelement shapes + j, = i + + # Find subelement for this index + for sub_element_index, e in enumerate(self._sub_elements): + sh = e.reference_value_shape + si = product(sh) + if j < si: + break + j -= si + if j < 0: + raise ValueError("Moved past last value reference_component!") + + # Convert index into a shape tuple + st = shape_to_strides(sh) + reference_component = unflatten_index(j, st) + return (sub_element_index, reference_component) + + def extract_reference_component(self, i): + """Recursively extract reference_component index relative to a (simple) element. + + and that element for given value reference_component index. + """ + sub_element_index, reference_component = self.extract_subelement_reference_component(i) + return self._sub_elements[sub_element_index].extract_reference_component(reference_component) + + def is_cellwise_constant(self, component=None): + """Return whether the basis functions of this element is spatially constant over each cell.""" + if component is None: + return all(e.is_cellwise_constant() for e in self.sub_elements) + else: + i, e = self.extract_component(component) + return e.is_cellwise_constant() + + def degree(self, component=None): + """Return polynomial degree of finite element.""" + if component is None: + return self._degree # from FiniteElementBase, computed as max of subelements in __init__ + else: + i, e = self.extract_component(component) + return e.degree() + + @property + def embedded_subdegree(self): + """Return embedded subdegree.""" + if isinstance(self._degree, int): + return self._degree + else: + return min(e.embedded_subdegree for e in self.sub_elements) + + @property + def embedded_superdegree(self): + """Return embedded superdegree.""" + if isinstance(self._degree, int): + return self._degree + else: + return max(e.embedded_superdegree for e in self.sub_elements) + + def reconstruct(self, **kwargs): + """Doc.""" + return MixedElement(*[e.reconstruct(**kwargs) for e in self.sub_elements]) + + def variant(self): + """Doc.""" + try: + variant, = {e.variant() for e in self.sub_elements} + return variant + except ValueError: + return None + + def __str__(self): + """Format as string for pretty printing.""" + tmp = ", ".join(str(element) for element in self._sub_elements) + return "" + + def shortstr(self): + """Format as string for pretty printing.""" + tmp = ", ".join(element.shortstr() for element in self._sub_elements) + return "Mixed<" + tmp + ">" + + @property + def pullback(self): + """Get the pull back.""" + for e in self.sub_elements: + if not isinstance(e.pullback, IdentityPullback): + return MixedPullback(self) + return IdentityPullback() + + +class VectorElement(MixedElement): + """A special case of a mixed finite element where all elements are equal.""" + + __slots__ = ("_repr", "_mapping", "_sub_element") + + def __init__(self, family, cell=None, degree=None, dim=None, + form_degree=None, quad_scheme=None, variant=None): + """Create vector element (repeated mixed element).""" + if isinstance(family, FiniteElementBase): + sub_element = family + cell = sub_element.cell + variant = sub_element.variant() + else: + if cell is not None: + cell = as_cell(cell) + # Create sub element + sub_element = FiniteElement(family, cell, degree, + form_degree=form_degree, + quad_scheme=quad_scheme, + variant=variant) + + # Set default size if not specified + if dim is None: + if cell is None: + raise ValueError("Cannot infer vector dimension without a cell.") + dim = cell.geometric_dimension() + + self._mapping = sub_element.mapping() + # Create list of sub elements for mixed element constructor + sub_elements = [sub_element] * dim + + # Compute value shapes + value_shape = (dim,) + sub_element.value_shape + reference_value_shape = (dim,) + sub_element.reference_value_shape + + # Initialize element data + MixedElement.__init__(self, sub_elements, value_shape=value_shape, + reference_value_shape=reference_value_shape) + + FiniteElementBase.__init__(self, sub_element.family(), sub_element.cell, sub_element.degree(), + sub_element.quadrature_scheme(), value_shape, reference_value_shape) + + self._sub_element = sub_element + + if variant is None: + var_str = "" + else: + var_str = ", variant='" + variant + "'" + + # Cache repr string + self._repr = f"VectorElement({repr(sub_element)}, dim={dim}{var_str})" + + def __repr__(self): + """Doc.""" + return self._repr + + def reconstruct(self, **kwargs): + """Doc.""" + sub_element = self._sub_element.reconstruct(**kwargs) + return VectorElement(sub_element, dim=len(self.sub_elements)) + + def variant(self): + """Return the variant used to initialise the element.""" + return self._sub_element.variant() + + def mapping(self): + """Doc.""" + return self._mapping + + def __str__(self): + """Format as string for pretty printing.""" + return ("" % + (len(self._sub_elements), self._sub_element)) + + def shortstr(self): + """Format as string for pretty printing.""" + return "Vector<%d x %s>" % (len(self._sub_elements), + self._sub_element.shortstr()) + + +class TensorElement(MixedElement): + """A special case of a mixed finite element where all elements are equal.""" + __slots__ = ("_sub_element", "_shape", "_symmetry", + "_sub_element_mapping", + "_flattened_sub_element_mapping", + "_mapping", "_repr") + + def __init__(self, family, cell=None, degree=None, shape=None, + symmetry=None, quad_scheme=None, variant=None): + """Create tensor element (repeated mixed element with optional symmetries).""" + if isinstance(family, FiniteElementBase): + sub_element = family + cell = sub_element.cell + variant = sub_element.variant() + else: + if cell is not None: + cell = as_cell(cell) + # Create scalar sub element + sub_element = FiniteElement(family, cell, degree, quad_scheme=quad_scheme, + variant=variant) + + # Set default shape if not specified + if shape is None: + if cell is None: + raise ValueError("Cannot infer tensor shape without a cell.") + dim = cell.geometric_dimension() + shape = (dim, dim) + + if symmetry is None: + symmetry = {} + elif symmetry is True: + # Construct default symmetry dict for matrix elements + if not (len(shape) == 2 and shape[0] == shape[1]): + raise ValueError("Cannot set automatic symmetry for non-square tensor.") + symmetry = dict(((i, j), (j, i)) for i in range(shape[0]) + for j in range(shape[1]) if i > j) + else: + if not isinstance(symmetry, dict): + raise ValueError("Expecting symmetry to be None (unset), True, or dict.") + + # Validate indices in symmetry dict + for i, j in symmetry.items(): + if len(i) != len(j): + raise ValueError("Non-matching length of symmetry index tuples.") + for k in range(len(i)): + if not (i[k] >= 0 and j[k] >= 0 and i[k] < shape[k] and j[k] < shape[k]): + raise ValueError("Symmetry dimensions out of bounds.") + + # Compute all index combinations for given shape + indices = compute_indices(shape) + + # Compute mapping from indices to sub element number, + # accounting for symmetry + sub_elements = [] + sub_element_mapping = {} + for index in indices: + if index in symmetry: + continue + sub_element_mapping[index] = len(sub_elements) + sub_elements += [sub_element] + + # Update mapping for symmetry + for index in indices: + if index in symmetry: + sub_element_mapping[index] = sub_element_mapping[symmetry[index]] + flattened_sub_element_mapping = [sub_element_mapping[index] for i, + index in enumerate(indices)] + + # Compute value shape + value_shape = shape + + # Compute reference value shape based on symmetries + if symmetry: + reference_value_shape = (product(shape) - len(symmetry),) + self._mapping = "symmetries" + else: + reference_value_shape = shape + self._mapping = sub_element.mapping() + + value_shape = value_shape + sub_element.value_shape + reference_value_shape = reference_value_shape + sub_element.reference_value_shape + # Initialize element data + MixedElement.__init__(self, sub_elements, value_shape=value_shape, + reference_value_shape=reference_value_shape) + self._family = sub_element.family() + self._degree = sub_element.degree() + self._sub_element = sub_element + self._shape = shape + self._symmetry = symmetry + self._sub_element_mapping = sub_element_mapping + self._flattened_sub_element_mapping = flattened_sub_element_mapping + + if variant is None: + var_str = "" + else: + var_str = ", variant='" + variant + "'" + + # Cache repr string + self._repr = (f"TensorElement({repr(sub_element)}, shape={shape}, " + f"symmetry={symmetry}{var_str})") + + @property + def pullback(self): + """Get pull back.""" + if len(self._symmetry) > 0: + sub_element_value_shape = self.sub_elements[0].value_shape + for e in self.sub_elements: + if e.value_shape != sub_element_value_shape: + raise ValueError("Sub-elements must all have the same value size") + symmetry = {} + n = 0 + for i in np.ndindex(self.value_shape[:len(self.value_shape)-len(sub_element_value_shape)]): + if i in self._symmetry and self._symmetry[i] in symmetry: + symmetry[i] = symmetry[self._symmetry[i]] + else: + symmetry[i] = n + n += 1 + return SymmetricPullback(self, symmetry) + return super().pullback + + def __repr__(self): + """Doc.""" + return self._repr + + def variant(self): + """Return the variant used to initialise the element.""" + return self._sub_element.variant() + + def mapping(self): + """Doc.""" + return self._mapping + + def flattened_sub_element_mapping(self): + """Doc.""" + return self._flattened_sub_element_mapping + + def extract_subelement_component(self, i): + """Extract direct subelement index and subelement relative. + + component index for a given component index. + """ + if isinstance(i, int): + i = (i,) + self._check_component(i) + + i = self.symmetry().get(i, i) + l = len(self._shape) # noqa: E741 + ii = i[:l] + jj = i[l:] + if ii not in self._sub_element_mapping: + raise ValueError(f"Illegal component index {i}.") + k = self._sub_element_mapping[ii] + return (k, jj) + + def symmetry(self): + r"""Return the symmetry dict, which is a mapping :math:`c_0 \\to c_1`. + + meaning that component :math:`c_0` is represented by component + :math:`c_1`. + A component is a tuple of one or more ints. + """ + return self._symmetry + + def reconstruct(self, **kwargs): + """Doc.""" + sub_element = self._sub_element.reconstruct(**kwargs) + return TensorElement(sub_element, shape=self._shape, symmetry=self._symmetry) + + def __str__(self): + """Format as string for pretty printing.""" + if self._symmetry: + tmp = ", ".join("%s -> %s" % (a, b) for (a, b) in self._symmetry.items()) + sym = " with symmetries (%s)" % tmp + else: + sym = "" + return ("" % + (self.value_shape, self._sub_element, sym)) + + def shortstr(self): + """Format as string for pretty printing.""" + if self._symmetry: + tmp = ", ".join("%s -> %s" % (a, b) for (a, b) in self._symmetry.items()) + sym = " with symmetries (%s)" % tmp + else: + sym = "" + return "Tensor<%s x %s%s>" % (self.value_shape, + self._sub_element.shortstr(), sym) diff --git a/finat/ufl/restrictedelement.py b/finat/ufl/restrictedelement.py new file mode 100644 index 000000000..a5426fbb1 --- /dev/null +++ b/finat/ufl/restrictedelement.py @@ -0,0 +1,114 @@ +"""This module defines the UFL finite element classes.""" + +# Copyright (C) 2008-2016 Martin Sandve Alnæs +# +# This file was originally part of UFL (https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later +# +# Modified by Kristian B. Oelgaard +# Modified by Marie E. Rognes 2010, 2012 +# Modified by Massimiliano Leoni, 2016 +# Modified by Matthew Scroggs, 2023 + +from finat.ufl.finiteelementbase import FiniteElementBase +from ufl.sobolevspace import L2 + +valid_restriction_domains = ("interior", "facet", "face", "edge", "vertex") + + +class RestrictedElement(FiniteElementBase): + """Represents the restriction of a finite element to a type of cell entity.""" + + def __init__(self, element, restriction_domain): + """Doc.""" + if not isinstance(element, FiniteElementBase): + raise ValueError("Expecting a finite element instance.") + if restriction_domain not in valid_restriction_domains: + raise ValueError(f"Expecting one of the strings: {valid_restriction_domains}") + + FiniteElementBase.__init__(self, "RestrictedElement", element.cell, + element.degree(), + element.quadrature_scheme(), + element.value_shape, + element.reference_value_shape) + + self._element = element + + self._restriction_domain = restriction_domain + + def __repr__(self): + """Doc.""" + return f"RestrictedElement({repr(self._element)}, {repr(self._restriction_domain)})" + + @property + def sobolev_space(self): + """Doc.""" + if self._restriction_domain == "interior": + return L2 + else: + return self._element.sobolev_space + + def is_cellwise_constant(self): + """Return whether the basis functions of this element is spatially constant over each cell.""" + return self._element.is_cellwise_constant() + + def _is_linear(self): + """Doc.""" + return self._element._is_linear() + + def sub_element(self): + """Return the element which is restricted.""" + return self._element + + def mapping(self): + """Doc.""" + return self._element.mapping() + + def restriction_domain(self): + """Return the domain onto which the element is restricted.""" + return self._restriction_domain + + def reconstruct(self, **kwargs): + """Doc.""" + element = self._element.reconstruct(**kwargs) + return RestrictedElement(element, self._restriction_domain) + + def __str__(self): + """Format as string for pretty printing.""" + return "<%s>|_{%s}" % (self._element, self._restriction_domain) + + def shortstr(self): + """Format as string for pretty printing.""" + return "<%s>|_{%s}" % (self._element.shortstr(), + self._restriction_domain) + + def symmetry(self): + r"""Return the symmetry dict, which is a mapping :math:`c_0 \\to c_1`. + + meaning that component :math:`c_0` is represented by component + :math:`c_1`. A component is a tuple of one or more ints. + """ + return self._element.symmetry() + + @property + def num_sub_elements(self): + """Return number of sub elements.""" + return self._element.num_sub_elements + + @property + def sub_elements(self): + """Return list of sub elements.""" + return self._element.sub_elements + + def num_restricted_sub_elements(self): + """Return number of restricted sub elements.""" + return 1 + + def restricted_sub_elements(self): + """Return list of restricted sub elements.""" + return (self._element,) + + def variant(self): + """Doc.""" + return self._element.variant() diff --git a/finat/ufl/tensorproductelement.py b/finat/ufl/tensorproductelement.py new file mode 100644 index 000000000..de946dadf --- /dev/null +++ b/finat/ufl/tensorproductelement.py @@ -0,0 +1,141 @@ +"""This module defines the UFL finite element classes.""" + +# Copyright (C) 2008-2016 Martin Sandve Alnæs +# +# This file was originally part of UFL (https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later +# +# Modified by Kristian B. Oelgaard +# Modified by Marie E. Rognes 2010, 2012 +# Modified by Massimiliano Leoni, 2016 +# Modified by Matthew Scroggs, 2023 + +from itertools import chain + +from ufl.cell import TensorProductCell, as_cell +from finat.ufl.finiteelementbase import FiniteElementBase +from ufl.sobolevspace import DirectionalSobolevSpace + + +class TensorProductElement(FiniteElementBase): + r"""The tensor product of :math:`d` element spaces. + + .. math:: V = V_1 \otimes V_2 \otimes ... \otimes V_d + + Given bases :math:`\{\phi_{j_i}\}` of the spaces :math:`V_i` for :math:`i = 1, ...., d`, + :math:`\{ \phi_{j_1} \otimes \phi_{j_2} \otimes \cdots \otimes \phi_{j_d} + \}` forms a basis for :math:`V`. + """ + __slots__ = ("_sub_elements", "_cell") + + def __init__(self, *elements, **kwargs): + """Create TensorProductElement from a given list of elements.""" + if not elements: + raise ValueError("Cannot create TensorProductElement from empty list.") + + keywords = list(kwargs.keys()) + if keywords and keywords != ["cell"]: + raise ValueError("TensorProductElement got an unexpected keyword argument '%s'" % keywords[0]) + cell = kwargs.get("cell") + + family = "TensorProductElement" + + if cell is None: + # Define cell as the product of each elements cell + cell = TensorProductCell(*[e.cell for e in elements]) + else: + cell = as_cell(cell) + + # Define polynomial degree as a tuple of sub-degrees + degree = tuple(e.degree() for e in elements) + + # No quadrature scheme defined + quad_scheme = None + + # match FIAT implementation + value_shape = tuple(chain(*[e.value_shape for e in elements])) + reference_value_shape = tuple(chain(*[e.reference_value_shape for e in elements])) + if len(value_shape) > 1: + raise ValueError("Product of vector-valued elements not supported") + if len(reference_value_shape) > 1: + raise ValueError("Product of vector-valued elements not supported") + + FiniteElementBase.__init__(self, family, cell, degree, + quad_scheme, value_shape, + reference_value_shape) + self._sub_elements = elements + self._cell = cell + + def __repr__(self): + """Doc.""" + return "TensorProductElement(" + ", ".join(repr(e) for e in self._sub_elements) + f", cell={repr(self._cell)})" + + def mapping(self): + """Doc.""" + if all(e.mapping() == "identity" for e in self._sub_elements): + return "identity" + elif all(e.mapping() == "L2 Piola" for e in self._sub_elements): + return "L2 Piola" + else: + return "undefined" + + @property + def sobolev_space(self): + """Return the underlying Sobolev space of the TensorProductElement.""" + elements = self._sub_elements + if all(e.sobolev_space == elements[0].sobolev_space + for e in elements): + return elements[0].sobolev_space + else: + # Generate a DirectionalSobolevSpace which contains + # continuity information parametrized by spatial index + orders = [] + for e in elements: + e_dim = e.cell.geometric_dimension() + e_order = (e.sobolev_space._order,) * e_dim + orders.extend(e_order) + return DirectionalSobolevSpace(orders) + + @property + def num_sub_elements(self): + """Return number of subelements.""" + return len(self._sub_elements) + + @property + def sub_elements(self): + """Return subelements (factors).""" + return self._sub_elements + + def reconstruct(self, **kwargs): + """Doc.""" + cell = kwargs.pop("cell", self.cell) + return TensorProductElement(*[e.reconstruct(**kwargs) for e in self.sub_elements], cell=cell) + + def variant(self): + """Doc.""" + try: + variant, = {e.variant() for e in self.sub_elements} + return variant + except ValueError: + return None + + def __str__(self): + """Pretty-print.""" + return "TensorProductElement(%s, cell=%s)" \ + % (', '.join([str(e) for e in self._sub_elements]), str(self._cell)) + + def shortstr(self): + """Short pretty-print.""" + return "TensorProductElement(%s, cell=%s)" \ + % (', '.join([e.shortstr() for e in self._sub_elements]), str(self._cell)) + + @property + def embedded_superdegree(self): + """Doc.""" + return sum(self.degree()) + + @property + def embedded_subdegree(self): + """Doc.""" + return min(self.degree()) From fa7657b412e1bb53c4e5121301f6708bf1f59e0a Mon Sep 17 00:00:00 2001 From: Matthew Scroggs Date: Mon, 16 Oct 2023 17:29:54 +0100 Subject: [PATCH 714/749] flake --- finat/point_set.py | 4 ++-- finat/ufl/__init__.py | 18 ++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/finat/point_set.py b/finat/point_set.py index 8682c1534..43db02207 100644 --- a/finat/point_set.py +++ b/finat/point_set.py @@ -147,7 +147,7 @@ def expression(self): def almost_equal(self, other, tolerance=1e-12): """Approximate numerical equality of point sets""" - return type(self) == type(other) and \ + return type(self) is type(other) and \ self.points.shape == other.points.shape and \ numpy.allclose(self.points, other.points, rtol=0, atol=tolerance) @@ -197,7 +197,7 @@ def expression(self): def almost_equal(self, other, tolerance=1e-12): """Approximate numerical equality of point sets""" - return type(self) == type(other) and \ + return type(self) is type(other) and \ len(self.factors) == len(other.factors) and \ all(s.almost_equal(o, tolerance=tolerance) for s, o in zip(self.factors, other.factors)) diff --git a/finat/ufl/__init__.py b/finat/ufl/__init__.py index 4ddc3953c..5a1f4dff3 100644 --- a/finat/ufl/__init__.py +++ b/finat/ufl/__init__.py @@ -11,13 +11,11 @@ # Modified by Lawrence Mitchell 2014 # Modified by Matthew Scroggs, 2023 -import warnings as _warnings - -from finat.ufl.brokenelement import BrokenElement -from finat.ufl.enrichedelement import EnrichedElement, NodalEnrichedElement -from finat.ufl.finiteelement import FiniteElement -from finat.ufl.finiteelementbase import FiniteElementBase -from finat.ufl.hdivcurl import HCurlElement, HDivElement, WithMapping -from finat.ufl.mixedelement import MixedElement, TensorElement, VectorElement -from finat.ufl.restrictedelement import RestrictedElement -from finat.ufl.tensorproductelement import TensorProductElement +from finat.ufl.brokenelement import BrokenElement # noqa: F401 +from finat.ufl.enrichedelement import EnrichedElement, NodalEnrichedElement # noqa: F401 +from finat.ufl.finiteelement import FiniteElement # noqa: F401 +from finat.ufl.finiteelementbase import FiniteElementBase # noqa: F401 +from finat.ufl.hdivcurl import HCurlElement, HDivElement, WithMapping # noqa: F401 +from finat.ufl.mixedelement import MixedElement, TensorElement, VectorElement # noqa: F401 +from finat.ufl.restrictedelement import RestrictedElement # noqa: F401 +from finat.ufl.tensorproductelement import TensorProductElement # noqa: F401 From 87ee4d14301dbcee39d2cf918829c6ae6fc76573 Mon Sep 17 00:00:00 2001 From: Matthew Scroggs Date: Wed, 8 Nov 2023 17:06:35 +0000 Subject: [PATCH 715/749] add HDiv and HCurl functions --- finat/ufl/__init__.py | 2 +- finat/ufl/hdivcurl.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/finat/ufl/__init__.py b/finat/ufl/__init__.py index 5a1f4dff3..21a7d13d1 100644 --- a/finat/ufl/__init__.py +++ b/finat/ufl/__init__.py @@ -15,7 +15,7 @@ from finat.ufl.enrichedelement import EnrichedElement, NodalEnrichedElement # noqa: F401 from finat.ufl.finiteelement import FiniteElement # noqa: F401 from finat.ufl.finiteelementbase import FiniteElementBase # noqa: F401 -from finat.ufl.hdivcurl import HCurlElement, HDivElement, WithMapping # noqa: F401 +from finat.ufl.hdivcurl import HCurlElement, HDivElement, WithMapping, HDiv, HCurl # noqa: F401 from finat.ufl.mixedelement import MixedElement, TensorElement, VectorElement # noqa: F401 from finat.ufl.restrictedelement import RestrictedElement # noqa: F401 from finat.ufl.tensorproductelement import TensorProductElement # noqa: F401 diff --git a/finat/ufl/hdivcurl.py b/finat/ufl/hdivcurl.py index 682105e7e..7fed79d09 100644 --- a/finat/ufl/hdivcurl.py +++ b/finat/ufl/hdivcurl.py @@ -212,3 +212,11 @@ def embedded_subdegree(self): def embedded_superdegree(self): """Return embedded superdegree.""" return self._element.embedded_superdegree + + +def HDiv(element): + return HDivElement(element) + + +def HCurl(element): + return HDivElement(element) From 1d91b5b20af27e7c2122f1eef38baf5e27ec65ad Mon Sep 17 00:00:00 2001 From: Matthew Scroggs Date: Wed, 8 Nov 2023 17:17:01 +0000 Subject: [PATCH 716/749] import as --- finat/ufl/hdivcurl.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/finat/ufl/hdivcurl.py b/finat/ufl/hdivcurl.py index 7fed79d09..5f5aaa6cc 100644 --- a/finat/ufl/hdivcurl.py +++ b/finat/ufl/hdivcurl.py @@ -9,7 +9,9 @@ # Modified by Matthew Scroggs, 2023 from finat.ufl.finiteelementbase import FiniteElementBase -from ufl.sobolevspace import L2, HCurl, HDiv +from ufl.sobolevspace import L2 +from ufl.sobolevspace import HCurl as HCurlSobolevSpace +from ufl.sobolevspace import HDiv as HDivSobolevSpace class HDivElement(FiniteElementBase): @@ -42,7 +44,7 @@ def mapping(self): @property def sobolev_space(self): """Return the underlying Sobolev space.""" - return HDiv + return HDivSobolevSpace def reconstruct(self, **kwargs): """Doc.""" @@ -102,7 +104,7 @@ def mapping(self): @property def sobolev_space(self): """Return the underlying Sobolev space.""" - return HCurl + return HCurlSobolevSpace def reconstruct(self, **kwargs): """Doc.""" From e70de4b006f581ee962b72ba8052ced9e3147d0e Mon Sep 17 00:00:00 2001 From: Matthew Scroggs Date: Wed, 8 Nov 2023 17:30:01 +0000 Subject: [PATCH 717/749] implement CallableSobolevSpace --- finat/ufl/hdivcurl.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/finat/ufl/hdivcurl.py b/finat/ufl/hdivcurl.py index 5f5aaa6cc..791b2b4f1 100644 --- a/finat/ufl/hdivcurl.py +++ b/finat/ufl/hdivcurl.py @@ -9,11 +9,32 @@ # Modified by Matthew Scroggs, 2023 from finat.ufl.finiteelementbase import FiniteElementBase -from ufl.sobolevspace import L2 +from ufl.sobolevspace import L2, SobolevSpace from ufl.sobolevspace import HCurl as HCurlSobolevSpace from ufl.sobolevspace import HDiv as HDivSobolevSpace +class CallableSobolevSpace(SobolevSpace): + """A Sobolev space that can be called to create HDiv and HCurl elements.""" + + def __init__(self, name, parents=None): + super().__init__(name, parents) + + def __call__(self, element): + """Syntax shortcut to create a HDivElement or HCurlElement.""" + if self.name == "HDiv": + return HDivElement(element) + elif self.name == "HCurl": + return HCurlElement(element) + raise NotImplementedError( + "SobolevSpace has no call operator (only the specific HDiv and HCurl instances)." + ) + + +HCurl = CallableSobolevSpace(HCurlSobolevSpace.name, HCurlSobolevSpace.parents) +HDiv = CallableSobolevSpace(HDivSobolevSpace.name, HDivSobolevSpace.parents) + + class HDivElement(FiniteElementBase): """A div-conforming version of an outer product element, assuming this makes mathematical sense.""" __slots__ = ("_element", ) @@ -214,11 +235,3 @@ def embedded_subdegree(self): def embedded_superdegree(self): """Return embedded superdegree.""" return self._element.embedded_superdegree - - -def HDiv(element): - return HDivElement(element) - - -def HCurl(element): - return HDivElement(element) From d9651adbedc378b1a98c6dc0acdd917222c45ca6 Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Fri, 10 Nov 2023 22:42:25 +0000 Subject: [PATCH 718/749] Subclass spectral Lagrange elements --- finat/spectral.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/finat/spectral.py b/finat/spectral.py index ed23dbc9f..b54d6626d 100644 --- a/finat/spectral.py +++ b/finat/spectral.py @@ -2,11 +2,11 @@ import gem -from finat.fiat_elements import ScalarFiatElement +from finat.fiat_elements import ScalarFiatElement, Lagrange, DiscontinuousLagrange from finat.point_set import GaussLobattoLegendrePointSet, GaussLegendrePointSet -class GaussLobattoLegendre(ScalarFiatElement): +class GaussLobattoLegendre(Lagrange): """1D continuous element with nodes at the Gauss-Lobatto points.""" def __init__(self, cell, degree): @@ -35,7 +35,7 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): return result -class GaussLegendre(ScalarFiatElement): +class GaussLegendre(DiscontinuousLagrange): """1D discontinuous element with nodes at the Gauss-Legendre points.""" def __init__(self, cell, degree): From 34029ea501a64b1c61ffad0cf0b7db3fd138654d Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Fri, 10 Nov 2023 22:45:44 +0000 Subject: [PATCH 719/749] lint E721 do not compare types, for exact checks use is --- finat/point_set.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/point_set.py b/finat/point_set.py index 8682c1534..c4833cb12 100644 --- a/finat/point_set.py +++ b/finat/point_set.py @@ -197,7 +197,7 @@ def expression(self): def almost_equal(self, other, tolerance=1e-12): """Approximate numerical equality of point sets""" - return type(self) == type(other) and \ + return type(self) is type(other) and \ len(self.factors) == len(other.factors) and \ all(s.almost_equal(o, tolerance=tolerance) for s, o in zip(self.factors, other.factors)) From 2c45da7f1fc0fef0b33496108e74b337bf2cab5a Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Fri, 10 Nov 2023 22:48:03 +0000 Subject: [PATCH 720/749] same lint again --- finat/point_set.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/point_set.py b/finat/point_set.py index c4833cb12..43db02207 100644 --- a/finat/point_set.py +++ b/finat/point_set.py @@ -147,7 +147,7 @@ def expression(self): def almost_equal(self, other, tolerance=1e-12): """Approximate numerical equality of point sets""" - return type(self) == type(other) and \ + return type(self) is type(other) and \ self.points.shape == other.points.shape and \ numpy.allclose(self.points, other.points, rtol=0, atol=tolerance) From 9a711e399ca5f2b94fab99b9ec566ca74d7af8a3 Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Fri, 10 Nov 2023 22:52:13 +0000 Subject: [PATCH 721/749] use correct super constructor --- finat/spectral.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/finat/spectral.py b/finat/spectral.py index b54d6626d..bd8acd64d 100644 --- a/finat/spectral.py +++ b/finat/spectral.py @@ -11,7 +11,7 @@ class GaussLobattoLegendre(Lagrange): def __init__(self, cell, degree): fiat_element = FIAT.GaussLobattoLegendre(cell, degree) - super(GaussLobattoLegendre, self).__init__(fiat_element) + super(Lagrange, self).__init__(fiat_element) def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): '''Return code for evaluating the element at known points on the @@ -40,7 +40,7 @@ class GaussLegendre(DiscontinuousLagrange): def __init__(self, cell, degree): fiat_element = FIAT.GaussLegendre(cell, degree) - super(GaussLegendre, self).__init__(fiat_element) + super(DiscontinuousLagrange, self).__init__(fiat_element) def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): '''Return code for evaluating the element at known points on the From 7e910693ec108cce09a1ed988c019e9e1557a09d Mon Sep 17 00:00:00 2001 From: Connor Ward Date: Tue, 21 Nov 2023 09:59:16 +0000 Subject: [PATCH 722/749] Add missing embedded_superdegree and embedded_subdegree properties (#113) --- finat/ufl/brokenelement.py | 10 ++++++++++ finat/ufl/hdivcurl.py | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/finat/ufl/brokenelement.py b/finat/ufl/brokenelement.py index 9127a3976..2b8a75468 100644 --- a/finat/ufl/brokenelement.py +++ b/finat/ufl/brokenelement.py @@ -52,3 +52,13 @@ def __str__(self): def shortstr(self): """Format as string for pretty printing.""" return f"BrokenElement({repr(self._element)})" + + @property + def embedded_subdegree(self): + """Return embedded subdegree.""" + return self._element.embedded_subdegree + + @property + def embedded_superdegree(self): + """Return embedded superdegree.""" + return self._element.embedded_superdegree diff --git a/finat/ufl/hdivcurl.py b/finat/ufl/hdivcurl.py index 791b2b4f1..45ef44e79 100644 --- a/finat/ufl/hdivcurl.py +++ b/finat/ufl/hdivcurl.py @@ -143,6 +143,16 @@ def shortstr(self): """Format as string for pretty printing.""" return f"HCurlElement({self._element.shortstr()})" + @property + def embedded_subdegree(self): + """Return embedded subdegree.""" + return self._element.embedded_subdegree + + @property + def embedded_superdegree(self): + """Return embedded superdegree.""" + return self._element.embedded_superdegree + class WithMapping(FiniteElementBase): """Specify an alternative mapping for the wrappee. From 0b537f305225b6a5426251e03c5a45cd63b2f927 Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Tue, 21 Nov 2023 18:48:23 +0000 Subject: [PATCH 723/749] Tabulate Ciarlet generically --- finat/fiat_elements.py | 61 ------------------------------------------ 1 file changed, 61 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 25721bcc7..cff77b223 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -3,7 +3,6 @@ from functools import singledispatch import FIAT -from FIAT.polynomial_set import mis, form_matrix_product import gem from gem.utils import cached_property @@ -317,66 +316,6 @@ def point_evaluation_generic(fiat_element, order, refcoords, entity): return result -@point_evaluation.register(FIAT.CiarletElement) -def point_evaluation_ciarlet(fiat_element, order, refcoords, entity): - # Coordinates on the reference entity (SymPy) - esd, = refcoords.shape - Xi = sp.symbols('X Y Z')[:esd] - - # Coordinates on the reference cell - cell = fiat_element.get_reference_element() - X = cell.get_entity_transform(*entity)(Xi) - - # Evaluate expansion set at SymPy point - poly_set = fiat_element.get_nodal_basis() - degree = poly_set.get_embedded_degree() - base_values = poly_set.get_expansion_set().tabulate(degree, [X]) - m = len(base_values) - assert base_values.shape == (m, 1) - base_values_sympy = np.array(list(base_values.flat)) - - # Find constant polynomials - def is_const(expr): - try: - float(expr) - return True - except TypeError: - return False - const_mask = np.array(list(map(is_const, base_values_sympy))) - - # Convert SymPy expression to GEM - mapper = gem.node.Memoizer(sympy2gem) - mapper.bindings = {s: gem.Indexed(refcoords, (i,)) - for i, s in enumerate(Xi)} - base_values = gem.ListTensor(list(map(mapper, base_values.flat))) - - # Populate result dict, creating precomputed coefficient - # matrices for each derivative tuple. - result = {} - for i in range(order + 1): - for alpha in mis(cell.get_spatial_dimension(), i): - D = form_matrix_product(poly_set.get_dmats(), alpha) - table = np.dot(poly_set.get_coeffs(), np.transpose(D)) - assert table.shape[-1] == m - zerocols = np.isclose(abs(table).max(axis=tuple(range(table.ndim - 1))), 0.0) - if all(np.logical_or(const_mask, zerocols)): - # Casting is safe by assertion of is_const - vals = base_values_sympy[const_mask].astype(np.float64) - result[alpha] = gem.Literal(table[..., const_mask].dot(vals)) - else: - beta = tuple(gem.Index() for s in table.shape[:-1]) - k = gem.Index() - result[alpha] = gem.ComponentTensor( - gem.IndexSum( - gem.Product(gem.Indexed(gem.Literal(table), beta + (k,)), - gem.Indexed(base_values, (k,))), - (k,) - ), - beta - ) - return result - - class Regge(FiatElement): # naturally tensor valued def __init__(self, cell, degree): super(Regge, self).__init__(FIAT.Regge(cell, degree)) From c4e3b58c2cd82becda3ae36ccfcd1fe353bdebb1 Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Wed, 22 Nov 2023 10:19:01 +0000 Subject: [PATCH 724/749] remove singledispatch --- finat/fiat_elements.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index cff77b223..ea63a1e9d 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -1,6 +1,5 @@ import numpy as np import sympy as sp -from functools import singledispatch import FIAT @@ -175,7 +174,6 @@ def point_evaluation(self, order, refcoords, entity=None): esd = self.cell.construct_subelement(entity_dim).get_spatial_dimension() assert isinstance(refcoords, gem.Node) and refcoords.shape == (esd,) - # Dispatch on FIAT element class return point_evaluation(self._element, order, refcoords, (entity_dim, entity_i)) @cached_property @@ -267,13 +265,7 @@ def mapping(self): return result -@singledispatch def point_evaluation(fiat_element, order, refcoords, entity): - raise AssertionError("FIAT element expected!") - - -@point_evaluation.register(FIAT.FiniteElement) -def point_evaluation_generic(fiat_element, order, refcoords, entity): # Coordinates on the reference entity (SymPy) esd, = refcoords.shape Xi = sp.symbols('X Y Z')[:esd] From cd26c09b918050c47832e8e5ed89dff360aeec16 Mon Sep 17 00:00:00 2001 From: Connor Ward Date: Wed, 22 Nov 2023 14:03:39 +0000 Subject: [PATCH 725/749] Allow embedded_subdegree and embedded_superdegree to handle tuples (#116) --- finat/ufl/finiteelement.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/finat/ufl/finiteelement.py b/finat/ufl/finiteelement.py index 2d240e048..d547b50d4 100644 --- a/finat/ufl/finiteelement.py +++ b/finat/ufl/finiteelement.py @@ -234,3 +234,19 @@ def __getnewargs__(self): None, self.quadrature_scheme(), self.variant()) + + @property + def embedded_subdegree(self): + """Return embedded subdegree.""" + if isinstance(self.degree(), int): + return self.degree() + else: + return min(self.degree()) + + @property + def embedded_superdegree(self): + """Return embedded superdegree.""" + if isinstance(self.degree(), int): + return self.degree() + else: + return max(self.degree()) From 3de93233523bc786add65830e0ea1799835cd191 Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Sun, 31 Dec 2023 13:35:11 -0600 Subject: [PATCH 726/749] Support Lagrange/IntegratedLegendre variants --- finat/fiat_elements.py | 4 ++-- finat/spectral.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index ea63a1e9d..9880318c7 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -346,8 +346,8 @@ def __init__(self, cell, degree): class Lagrange(ScalarFiatElement): - def __init__(self, cell, degree): - super(Lagrange, self).__init__(FIAT.Lagrange(cell, degree)) + def __init__(self, cell, degree, variant=None): + super(Lagrange, self).__init__(FIAT.Lagrange(cell, degree, variant=variant)) class KongMulderVeldhuizen(ScalarFiatElement): diff --git a/finat/spectral.py b/finat/spectral.py index bd8acd64d..8489be041 100644 --- a/finat/spectral.py +++ b/finat/spectral.py @@ -65,7 +65,7 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): class Legendre(ScalarFiatElement): - """1D DG element with Legendre polynomials.""" + """DG element with Legendre polynomials.""" def __init__(self, cell, degree): fiat_element = FIAT.Legendre(cell, degree) @@ -73,10 +73,10 @@ def __init__(self, cell, degree): class IntegratedLegendre(ScalarFiatElement): - """1D CG element with integrated Legendre polynomials.""" + """CG element with integrated Legendre polynomials.""" - def __init__(self, cell, degree): - fiat_element = FIAT.IntegratedLegendre(cell, degree) + def __init__(self, cell, degree, variant=None): + fiat_element = FIAT.IntegratedLegendre(cell, degree, variant=variant) super(IntegratedLegendre, self).__init__(fiat_element) From d3d205a1f78d053c06ae6436706d18c5378a7330 Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Thu, 8 Feb 2024 21:43:44 +0000 Subject: [PATCH 727/749] Fix dual basis for singleton weight --- finat/fiat_elements.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 9880318c7..5b7baaac8 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -233,7 +233,10 @@ def _dual_basis(self): # significantly by building Q in a COO format rather than DOK (i.e. # storing coords and associated data in (nonzeros, entries) shaped # numpy arrays) to take advantage of numpy multiindexing - Qshape = tuple(s + 1 for s in map(max, *Q)) + if len(Q) == 1: + Qshape = tuple(s + 1 for s in tuple(Q)[0]) + else: + Qshape = tuple(s + 1 for s in map(max, *Q)) Qdense = np.zeros(Qshape, dtype=np.float64) for idx, value in Q.items(): Qdense[idx] = value From 58823dbfba9588a6ce4c5dd9676558788ceed2ea Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Wed, 1 May 2024 16:41:09 +0100 Subject: [PATCH 728/749] C0/C1 Macroelements (#123) * DG variant in constructor * Support FIAT macro elements * Legendre variant * TensorFiniteElement overload complex * cleanup Bell, register and attempt to transform HCT --------- Co-authored-by: Rob Kirby --- finat/__init__.py | 2 + finat/cell_tools.py | 5 ++ finat/cube.py | 12 ++- finat/direct_serendipity.py | 4 + finat/discontinuous.py | 4 + finat/enriched.py | 10 ++- finat/fiat_elements.py | 30 +++---- finat/finiteelementbase.py | 10 ++- finat/hct.py | 147 +++++++++++++++++++++++++++++++++++ finat/hdivcurl.py | 4 + finat/mixed.py | 4 + finat/physically_mapped.py | 9 +++ finat/quadrature.py | 12 ++- finat/quadrature_element.py | 6 +- finat/spectral.py | 4 +- finat/tensor_product.py | 4 + finat/tensorfiniteelement.py | 4 + finat/ufl/elementlist.py | 2 + test/fiat_mapping.py | 5 +- test/test_hct.py | 49 ++++++++++++ 20 files changed, 291 insertions(+), 36 deletions(-) create mode 100644 finat/cell_tools.py create mode 100644 finat/hct.py create mode 100644 test/test_hct.py diff --git a/finat/__init__.py b/finat/__init__.py index e52787df4..a94b090c4 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -11,6 +11,7 @@ from .fiat_elements import FacetBubble # noqa: F401 from .fiat_elements import KongMulderVeldhuizen # noqa: F401 from .argyris import Argyris # noqa: F401 +from .hct import HsiehCloughTocher, ReducedHsiehCloughTocher # noqa: F401 from .bell import Bell # noqa: F401 from .hermite import Hermite # noqa: F401 from .mtw import MardalTaiWinther # noqa: F401 @@ -32,3 +33,4 @@ from .restricted import RestrictedElement # noqa: F401 from .runtime_tabulated import RuntimeTabulated # noqa: F401 from . import quadrature # noqa: F401 +from . import cell_tools # noqa: F401 diff --git a/finat/cell_tools.py b/finat/cell_tools.py new file mode 100644 index 000000000..cedc8bc66 --- /dev/null +++ b/finat/cell_tools.py @@ -0,0 +1,5 @@ +"""Find the maximal complex in a list of cell complexes. +This is a pass-through from FIAT so that FInAT clients +(e.g. tsfc) don't have to directly import FIAT.""" + +from FIAT.reference_element import max_complex # noqa: F401 diff --git a/finat/cube.py b/finat/cube.py index c6b478b28..4d7e3687e 100644 --- a/finat/cube.py +++ b/finat/cube.py @@ -1,9 +1,9 @@ -from __future__ import absolute_import, print_function, division +from __future__ import absolute_import, division, print_function -from FIAT.reference_element import UFCHexahedron, UFCQuadrilateral -from FIAT.reference_element import compute_unflattening_map, flatten_entities, flatten_permutations +from FIAT.reference_element import (UFCHexahedron, UFCQuadrilateral, + compute_unflattening_map, flatten_entities, + flatten_permutations) from FIAT.tensor_product import FlattenedDimensions as FIAT_FlattenedDimensions - from gem.utils import cached_property from finat.finiteelementbase import FiniteElementBase @@ -29,6 +29,10 @@ def cell(self): else: raise NotImplementedError("Cannot guess cell for spatial dimension %s" % dim) + @property + def complex(self): + return self.product.complex + @property def degree(self): unique_degree, = set(self.product.degree) diff --git a/finat/direct_serendipity.py b/finat/direct_serendipity.py index eb279baae..fd084a4d0 100644 --- a/finat/direct_serendipity.py +++ b/finat/direct_serendipity.py @@ -33,6 +33,10 @@ def __init__(self, cell, degree): def cell(self): return self._cell + @property + def complex(self): + return self._cell + @property def degree(self): return self._degree diff --git a/finat/discontinuous.py b/finat/discontinuous.py index 283f2e198..faff0a471 100644 --- a/finat/discontinuous.py +++ b/finat/discontinuous.py @@ -16,6 +16,10 @@ def __init__(self, element): def cell(self): return self.element.cell + @property + def complex(self): + return self.element.complex + @property def degree(self): return self.element.degree diff --git a/finat/enriched.py b/finat/enriched.py index fee758a51..29e42feab 100644 --- a/finat/enriched.py +++ b/finat/enriched.py @@ -1,12 +1,10 @@ from functools import partial -from operator import add, methodcaller from itertools import chain - -import numpy +from operator import add, methodcaller import FIAT - import gem +import numpy from gem.utils import cached_property from finat.finiteelementbase import FiniteElementBase @@ -30,6 +28,10 @@ def cell(self): result, = set(elem.cell for elem in self.elements) return result + @cached_property + def complex(self): + return FIAT.reference_element.max_complex(set(elem.complex for elem in self.elements)) + @cached_property def degree(self): return tree_map(max, *[elem.degree for elem in self.elements]) diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index 5b7baaac8..ddd117f51 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -1,9 +1,7 @@ -import numpy as np -import sympy as sp - import FIAT - import gem +import numpy as np +import sympy as sp from gem.utils import cached_property from finat.finiteelementbase import FiniteElementBase @@ -53,6 +51,10 @@ def __init__(self, fiat_element): def cell(self): return self._element.get_reference_element() + @property + def complex(self): + return self._element.get_reference_complex() + @property def degree(self): # Requires FIAT.CiarletElement @@ -120,7 +122,14 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): exprs = [] for table in table_roll: - if derivative < self.degree: + if derivative == self.degree and not self.complex.is_macrocell(): + # Make sure numerics satisfies theory + exprs.append(gem.Literal(table[0])) + elif derivative > self.degree: + # Make sure numerics satisfies theory + assert np.allclose(table, 0.0) + exprs.append(gem.Literal(np.zeros(self.index_shape))) + else: point_indices = ps.indices point_shape = tuple(index.extent for index in point_indices) @@ -128,13 +137,6 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): gem.Literal(table.reshape(point_shape + index_shape)), point_indices )) - elif derivative == self.degree: - # Make sure numerics satisfies theory - exprs.append(gem.Literal(table[0])) - else: - # Make sure numerics satisfies theory - assert np.allclose(table, 0.0) - exprs.append(gem.Literal(np.zeros(self.index_shape))) if self.value_shape: # As above, this extent may be different from that # advertised by the finat element. @@ -362,8 +364,8 @@ def __init__(self, cell, degree): class DiscontinuousLagrange(ScalarFiatElement): - def __init__(self, cell, degree): - super(DiscontinuousLagrange, self).__init__(FIAT.DiscontinuousLagrange(cell, degree)) + def __init__(self, cell, degree, variant=None): + super(DiscontinuousLagrange, self).__init__(FIAT.DiscontinuousLagrange(cell, degree, variant=variant)) class Real(DiscontinuousLagrange): diff --git a/finat/finiteelementbase.py b/finat/finiteelementbase.py index 9a430ab37..64a6399d2 100644 --- a/finat/finiteelementbase.py +++ b/finat/finiteelementbase.py @@ -1,9 +1,8 @@ -from abc import ABCMeta, abstractproperty, abstractmethod +from abc import ABCMeta, abstractmethod, abstractproperty from itertools import chain -import numpy - import gem +import numpy from gem.interpreter import evaluate from gem.optimise import delta_elimination, sum_factorise, traverse_product from gem.utils import cached_property @@ -17,6 +16,11 @@ class FiniteElementBase(metaclass=ABCMeta): def cell(self): '''The reference cell on which the element is defined.''' + @property + def complex(self): + '''The reference cell complex over which bases are defined. + Can be different than self.cell in the case of macro elements.''' + @abstractproperty def degree(self): '''The degree of the embedding polynomial space. diff --git a/finat/hct.py b/finat/hct.py new file mode 100644 index 000000000..d24bca21c --- /dev/null +++ b/finat/hct.py @@ -0,0 +1,147 @@ +import FIAT +import numpy +from gem import ListTensor, Literal, partial_indexed + +from finat.fiat_elements import ScalarFiatElement +from finat.physically_mapped import Citations, PhysicallyMappedElement +from copy import deepcopy + + +class HsiehCloughTocher(PhysicallyMappedElement, ScalarFiatElement): + def __init__(self, cell, degree, avg=False): + if degree != 3: + raise ValueError("Degree must be 3 for HCT element") + if Citations is not None: + Citations().register("Clough1965") + self.avg = avg + super().__init__(FIAT.HsiehCloughTocher(cell)) + + def basis_transformation(self, coordinate_mapping): + # Jacobians at cell center + J = coordinate_mapping.jacobian_at([1/3, 1/3]) + + rns = coordinate_mapping.reference_normals() + pts = coordinate_mapping.physical_tangents() + pns = coordinate_mapping.physical_normals() + + pel = coordinate_mapping.physical_edge_lengths() + + d = self.cell.get_dimension() + ndof = self.space_dimension() + V = numpy.eye(ndof, dtype=object) + for multiindex in numpy.ndindex(V.shape): + V[multiindex] = Literal(V[multiindex]) + + voffset = d+1 + for v in range(d+1): + s = voffset * v + for i in range(d): + for j in range(d): + V[s+1+i, s+1+j] = J[j, i] + + for e in range(3): + s = (d+1) * voffset + e + v0id, v1id = [i * voffset for i in range(3) if i != e] + + nhat = partial_indexed(rns, (e, )) + t = partial_indexed(pts, (e, )) + n = partial_indexed(pns, (e, )) + + Bn = J @ nhat / pel[e] + Bnn = Bn @ n + Bnt = Bn @ t + + if self.avg: + Bnn = Bnn * pel[e] + V[s, s] = Bnn + V[s, v0id] = Literal(-1) * Bnt + V[s, v1id] = Bnt + + # Patch up conditioning + h = coordinate_mapping.cell_size() + for v in range(d+1): + s = voffset * v + for k in range(d): + V[:, s+1+k] /= h[v] + + for e in range(3): + v0id, v1id = [i for i in range(3) if i != e] + V[:, 9+e] *= 2 / (h[v0id] + h[v1id]) + return ListTensor(V.T) + + +class ReducedHsiehCloughTocher(PhysicallyMappedElement, ScalarFiatElement): + def __init__(self, cell, degree): + if degree != 3: + raise ValueError("Degree must be 3 for reduced HCT element") + if Citations is not None: + Citations().register("Clough1965") + super().__init__(FIAT.HsiehCloughTocher(cell, reduced=True)) + + def basis_transformation(self, coordinate_mapping): + # Jacobians at cell center + J = coordinate_mapping.jacobian_at([1/3, 1/3]) + + rns = coordinate_mapping.reference_normals() + pts = coordinate_mapping.physical_tangents() + # pns = coordinate_mapping.physical_normals() + + pel = coordinate_mapping.physical_edge_lengths() + + d = self.cell.get_dimension() + numbf = self._element.space_dimension() + ndof = self.space_dimension() + # rectangular to toss out the constraint dofs + V = numpy.eye(numbf, ndof, dtype=object) + for multiindex in numpy.ndindex(V.shape): + V[multiindex] = Literal(V[multiindex]) + + voffset = d+1 + for v in range(d+1): + s = voffset * v + for i in range(d): + for j in range(d): + V[s+1+i, s+1+j] = J[j, i] + + for e in range(3): + s = (d+1) * voffset + e + v0id, v1id = [i * voffset for i in range(3) if i != e] + + nhat = partial_indexed(rns, (e, )) + t = partial_indexed(pts, (e, )) + + # n = partial_indexed(pns, (e, )) + # Bnn = (J @ nhat) @ n + # V[s, s] = Bnn + + Bnt = (J @ nhat) @ t + V[s, v0id] = Literal(1/5) * Bnt / pel[e] + V[s, v1id] = Literal(-1) * V[s, v0id] + + R = Literal(1/10) * Bnt * t + V[s, v0id + 1] = R[0] + V[s, v0id + 2] = R[1] + V[s, v1id + 1] = R[0] + V[s, v1id + 2] = R[1] + + # Patch up conditioning + h = coordinate_mapping.cell_size() + for v in range(d+1): + s = voffset * v + for k in range(d): + V[:, s+1+k] /= h[v] + return ListTensor(V.T) + + def entity_dofs(self): + edofs = deepcopy(super(ReducedHsiehCloughTocher, self).entity_dofs()) + dim = 1 + for entity in edofs[dim]: + edofs[dim][entity] = [] + return edofs + + @property + def index_shape(self): + return (9,) + + def space_dimension(self): + return 9 diff --git a/finat/hdivcurl.py b/finat/hdivcurl.py index dce7b0247..b9ed8fc70 100644 --- a/finat/hdivcurl.py +++ b/finat/hdivcurl.py @@ -26,6 +26,10 @@ def __init__(self, wrappee, transform): def cell(self): return self.wrappee.cell + @property + def complex(self): + return self.wrappee.complex + @property def degree(self): return self.wrappee.degree diff --git a/finat/mixed.py b/finat/mixed.py index 099613956..2da4b6270 100644 --- a/finat/mixed.py +++ b/finat/mixed.py @@ -37,6 +37,10 @@ def __init__(self, element, size, offset): def cell(self): return self.element.cell + @property + def complex(self): + return self.element.complex + @property def degree(self): return self.element.degree diff --git a/finat/physically_mapped.py b/finat/physically_mapped.py index 3b6782e7a..9234e6ffe 100644 --- a/finat/physically_mapped.py +++ b/finat/physically_mapped.py @@ -32,6 +32,15 @@ eprint = {1808.05513}, primaryclass = {cs.MS} }""") + Citations().add("Clough1965", """ +@inproceedings{Clough1965, + author = {R. W. Clough, J. L. Tocher}, + title = {Finite element stiffness matricess for analysis of plate bending}, + booktitle = {Proc. of the First Conf. on Matrix Methods in Struct. Mech}, + year = 1965, + pages = {515-546}, +} +""") Citations().add("Argyris1968", """ @Article{Argyris1968, author = {J. H. Argyris and I. Fried and D. W. Scharpf}, diff --git a/finat/quadrature.py b/finat/quadrature.py index a6bbe313f..9bf1056ef 100644 --- a/finat/quadrature.py +++ b/finat/quadrature.py @@ -1,16 +1,14 @@ from abc import ABCMeta, abstractproperty from functools import reduce -import numpy - import gem -from gem.utils import cached_property - -from FIAT.reference_element import LINE, QUADRILATERAL, TENSORPRODUCT +import numpy from FIAT.quadrature import GaussLegendreQuadratureLineRule from FIAT.quadrature_schemes import create_quadrature as fiat_scheme +from FIAT.reference_element import LINE, QUADRILATERAL, TENSORPRODUCT +from gem.utils import cached_property -from finat.point_set import PointSet, GaussLegendrePointSet, TensorPointSet +from finat.point_set import GaussLegendrePointSet, PointSet, TensorPointSet def make_quadrature(ref_el, degree, scheme="default"): @@ -44,7 +42,7 @@ def make_quadrature(ref_el, degree, scheme="default"): if degree < 0: raise ValueError("Need positive degree, not %d" % degree) - if ref_el.get_shape() == LINE: + if ref_el.get_shape() == LINE and not ref_el.is_macrocell(): # FIAT uses Gauss-Legendre line quadature, however, since we # symbolically label it as such, we wish not to risk attaching # the wrong label in case FIAT changes. So we explicitly ask diff --git a/finat/quadrature_element.py b/finat/quadrature_element.py index 1e8b46d83..3f17ec399 100644 --- a/finat/quadrature_element.py +++ b/finat/quadrature_element.py @@ -25,7 +25,7 @@ def make_quadrature_element(fiat_ref_cell, degree, scheme="default"): "canonical" or "KMV". :returns: The appropriate :class:`QuadratureElement` """ - rule = make_quadrature(fiat_ref_cell, degree, scheme) + rule = make_quadrature(fiat_ref_cell, degree, scheme=scheme) return QuadratureElement(fiat_ref_cell, rule) @@ -50,6 +50,10 @@ def __init__(self, fiat_ref_cell, rule): def cell(self): pass # set at initialisation + @property + def complex(self): + return self.cell + @property def degree(self): raise NotImplementedError("QuadratureElement does not represent a polynomial space.") diff --git a/finat/spectral.py b/finat/spectral.py index 8489be041..c5641ecd0 100644 --- a/finat/spectral.py +++ b/finat/spectral.py @@ -67,8 +67,8 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): class Legendre(ScalarFiatElement): """DG element with Legendre polynomials.""" - def __init__(self, cell, degree): - fiat_element = FIAT.Legendre(cell, degree) + def __init__(self, cell, degree, variant=None): + fiat_element = FIAT.Legendre(cell, degree, variant=variant) super(Legendre, self).__init__(fiat_element) diff --git a/finat/tensor_product.py b/finat/tensor_product.py index 083c509da..de3708fbd 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -34,6 +34,10 @@ def __init__(self, factors): def cell(self): return TensorProductCell(*[fe.cell for fe in self.factors]) + @cached_property + def complex(self): + return TensorProductCell(*[fe.complex for fe in self.factors]) + @property def degree(self): return tuple(fe.degree for fe in self.factors) diff --git a/finat/tensorfiniteelement.py b/finat/tensorfiniteelement.py index 102a88398..c0a8aa91e 100644 --- a/finat/tensorfiniteelement.py +++ b/finat/tensorfiniteelement.py @@ -57,6 +57,10 @@ def base_element(self): def cell(self): return self._base_element.cell + @property + def complex(self): + return self._base_element.complex + @property def degree(self): return self._base_element.degree diff --git a/finat/ufl/elementlist.py b/finat/ufl/elementlist.py index 15a33476b..f8f8ac33b 100644 --- a/finat/ufl/elementlist.py +++ b/finat/ufl/elementlist.py @@ -111,6 +111,8 @@ def show_elements(): # Elements not in the periodic table register_element("Argyris", "ARG", 0, H2, "custom", (5, 5), ("triangle",)) +register_element("Hsieh-Clough-Tocher", "HCT", 0, H2, "custom", (3, 3), ("triangle",)) +register_element("Reduced-Hsieh-Clough-Tocher", "HCT-red", 0, H2, "custom", (3, 3), ("triangle",)) register_element("Bell", "BELL", 0, H2, "custom", (5, 5), ("triangle",)) register_element("Brezzi-Douglas-Fortin-Marini", "BDFM", 1, HDiv, "contravariant Piola", (1, None), simplices[1:]) diff --git a/test/fiat_mapping.py b/test/fiat_mapping.py index 2a744019e..296e71191 100644 --- a/test/fiat_mapping.py +++ b/test/fiat_mapping.py @@ -1,6 +1,6 @@ import FIAT -import numpy as np import gem +import numpy as np from finat.physically_mapped import PhysicalGeometry @@ -30,6 +30,9 @@ def detJ_at(self, point): def jacobian_at(self, point): return gem.Literal(self.A) + def normalized_reference_edge_tangents(self): + return gem.Literal(np.asarray([self.ref_cell.compute_normalized_edge_tangent(i) for i in range(3)])) + def reference_normals(self): return gem.Literal( np.asarray([self.ref_cell.compute_normal(i) diff --git a/test/test_hct.py b/test/test_hct.py new file mode 100644 index 000000000..926064d71 --- /dev/null +++ b/test/test_hct.py @@ -0,0 +1,49 @@ +import FIAT +import finat +import numpy as np +from gem.interpreter import evaluate + +from fiat_mapping import MyMapping + + +def test_hct(): + degree = 3 + ref_cell = FIAT.ufc_simplex(2) + ref_pts = finat.point_set.PointSet(FIAT.reference_element.make_lattice(ref_cell.vertices, degree)) + ref_element = finat.HsiehCloughTocher(ref_cell, degree, avg=True) + + phys_cell = FIAT.ufc_simplex(2) + phys_cell.vertices = ((0.0, 0.1), (1.17, -0.09), (0.15, 1.84)) + + mapping = MyMapping(ref_cell, phys_cell) + z = (0, 0) + finat_vals_gem = ref_element.basis_evaluation(0, ref_pts, coordinate_mapping=mapping)[z] + finat_vals = evaluate([finat_vals_gem])[0].arr + + phys_cell_FIAT = FIAT.HsiehCloughTocher(phys_cell, degree) + phys_points = FIAT.reference_element.make_lattice(phys_cell.vertices, degree) + phys_vals = phys_cell_FIAT.tabulate(0, phys_points)[z] + + assert np.allclose(finat_vals.T, phys_vals) + + +def test_reduced_hct(): + degree = 3 + ref_cell = FIAT.ufc_simplex(2) + ref_pts = finat.point_set.PointSet(FIAT.reference_element.make_lattice(ref_cell.vertices, degree)) + ref_element = finat.ReducedHsiehCloughTocher(ref_cell, degree) + + phys_cell = FIAT.ufc_simplex(2) + phys_cell.vertices = ((0.0, 0.1), (1.17, -0.09), (0.15, 1.84)) + + mapping = MyMapping(ref_cell, phys_cell) + z = (0, 0) + finat_vals_gem = ref_element.basis_evaluation(0, ref_pts, coordinate_mapping=mapping)[z] + finat_vals = evaluate([finat_vals_gem])[0].arr + + phys_cell_FIAT = FIAT.HsiehCloughTocher(phys_cell, degree, reduced=True) + phys_points = FIAT.reference_element.make_lattice(phys_cell.vertices, degree) + phys_vals = phys_cell_FIAT.tabulate(0, phys_points)[z] + + numbf = ref_element.space_dimension() + assert np.allclose(finat_vals.T, phys_vals[:numbf]) From ecc0001628a071ce38b3c1b7c2b61272802fa3f9 Mon Sep 17 00:00:00 2001 From: Jack Betteridge <43041811+JDBetteridge@users.noreply.github.com> Date: Fri, 7 Jun 2024 15:30:55 +0100 Subject: [PATCH 729/749] JDBetteridge/numpy2 rebase (#313) * Numpy 2.0 compat * fixup --------- Co-authored-by: Connor Ward --- gem/flop_count.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gem/flop_count.py b/gem/flop_count.py index 934c3386e..b9595e817 100644 --- a/gem/flop_count.py +++ b/gem/flop_count.py @@ -142,7 +142,7 @@ def flops_indexed(expr, temporaries): aggregate = sum(expression_flops(child, temporaries) for child in expr.children) # Average flops per entry - return aggregate / numpy.product(expr.children[0].shape, dtype=int) + return aggregate / numpy.prod(expr.children[0].shape, dtype=int) @flops.register(gem.IndexSum) From b074885ba58253ba0d1f5f5b6fec3fe79dbba391 Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Wed, 19 Jun 2024 16:53:16 +0100 Subject: [PATCH 730/749] Add Johnson Mercier as PhysicallyMappedElement (#126) Co-authored-by: Rob Kirby --- finat/__init__.py | 9 ++- finat/aw.py | 104 ++++++++++++++++++++++++----------- finat/hct.py | 13 +++-- finat/johnson_mercier.py | 34 ++++++++++++ finat/physically_mapped.py | 8 +++ finat/ufl/elementlist.py | 2 + test/test_hct.py | 56 ++++++++++--------- test/test_johnson_mercier.py | 78 ++++++++++++++++++++++++++ 8 files changed, 237 insertions(+), 67 deletions(-) create mode 100644 finat/johnson_mercier.py create mode 100644 test/test_johnson_mercier.py diff --git a/finat/__init__.py b/finat/__init__.py index a94b090c4..0f14b89be 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -10,16 +10,19 @@ from .fiat_elements import HellanHerrmannJohnson, Regge # noqa: F401 from .fiat_elements import FacetBubble # noqa: F401 from .fiat_elements import KongMulderVeldhuizen # noqa: F401 + from .argyris import Argyris # noqa: F401 -from .hct import HsiehCloughTocher, ReducedHsiehCloughTocher # noqa: F401 +from .aw import ArnoldWinther # noqa: F401 +from .aw import ArnoldWintherNC # noqa: F401 from .bell import Bell # noqa: F401 +from .hct import HsiehCloughTocher, ReducedHsiehCloughTocher # noqa: F401 from .hermite import Hermite # noqa: F401 +from .johnson_mercier import JohnsonMercier # noqa: F401 from .mtw import MardalTaiWinther # noqa: F401 from .morley import Morley # noqa: F401 -from .aw import ArnoldWinther # noqa: F401 -from .aw import ArnoldWintherNC # noqa: F401 from .trace import HDivTrace # noqa: F401 from .direct_serendipity import DirectSerendipity # noqa: F401 + from .spectral import GaussLobattoLegendre, GaussLegendre, Legendre, IntegratedLegendre, FDMLagrange, FDMQuadrature, FDMDiscontinuousLagrange, FDMBrokenH1, FDMBrokenL2, FDMHermite # noqa: F401 from .tensorfiniteelement import TensorFiniteElement # noqa: F401 from .tensor_product import TensorProductElement # noqa: F401 diff --git a/finat/aw.py b/finat/aw.py index c2049250b..63cf159ee 100644 --- a/finat/aw.py +++ b/finat/aw.py @@ -1,41 +1,81 @@ """Implementation of the Arnold-Winther finite elements.""" -import numpy import FIAT -from gem import Literal, ListTensor +import numpy +from gem import ListTensor, Literal, partial_indexed + from finat.fiat_elements import FiatElement -from finat.physically_mapped import PhysicallyMappedElement, Citations +from finat.physically_mapped import Citations, PhysicallyMappedElement -def _edge_transform(T, coordinate_mapping): - Vsub = numpy.zeros((12, 12), dtype=object) +def _facet_transform(fiat_cell, facet_moment_degree, coordinate_mapping): + sd = fiat_cell.get_spatial_dimension() + top = fiat_cell.get_topology() + num_facets = len(top[sd-1]) + dimPk_facet = FIAT.expansions.polynomial_dimension( + fiat_cell.construct_subelement(sd-1), facet_moment_degree) + dofs_per_facet = sd * dimPk_facet + ndofs = num_facets * dofs_per_facet + Vsub = numpy.eye(ndofs, dtype=object) for multiindex in numpy.ndindex(Vsub.shape): Vsub[multiindex] = Literal(Vsub[multiindex]) - for i in range(0, 12, 2): - Vsub[i, i] = Literal(1) - - # This bypasses the GEM wrapper. - that = numpy.array([T.compute_normalized_edge_tangent(i) for i in range(3)]) - nhat = numpy.array([T.compute_normal(i) for i in range(3)]) - - detJ = coordinate_mapping.detJ_at([1/3, 1/3]) - J = coordinate_mapping.jacobian_at([1/3, 1/3]) - J_np = numpy.array([[J[0, 0], J[0, 1]], - [J[1, 0], J[1, 1]]]) - JTJ = J_np.T @ J_np - - for e in range(3): - # Compute alpha and beta for the edge. - Ghat_T = numpy.array([nhat[e, :], that[e, :]]) - - (alpha, beta) = Ghat_T @ JTJ @ that[e, :] / detJ - # Stuff into the right rows and columns. - (idx1, idx2) = (4*e + 1, 4*e + 3) - Vsub[idx1, idx1-1] = Literal(-1) * alpha / beta - Vsub[idx1, idx1] = Literal(1) / beta - Vsub[idx2, idx2-1] = Literal(-1) * alpha / beta - Vsub[idx2, idx2] = Literal(1) / beta + bary = [1/(sd+1)] * sd + detJ = coordinate_mapping.detJ_at(bary) + J = coordinate_mapping.jacobian_at(bary) + rns = coordinate_mapping.reference_normals() + offset = dofs_per_facet + if sd == 2: + R = Literal(numpy.array([[0, -1], [1, 0]])) + + for e in range(num_facets): + nhat = partial_indexed(rns, (e, )) + that = R @ nhat + Jn = J @ nhat + Jt = J @ that + + # Compute alpha and beta for the edge. + alpha = (Jn @ Jt) / detJ + beta = (Jt @ Jt) / detJ + # Stuff into the right rows and columns. + for i in range(dimPk_facet): + idx = offset*e + i * dimPk_facet + 1 + Vsub[idx, idx-1] = Literal(-1) * alpha / beta + Vsub[idx, idx] = Literal(1) / beta + elif sd == 3: + for f in range(num_facets): + nhat = fiat_cell.compute_normal(f) + nhat /= numpy.linalg.norm(nhat) + ehats = fiat_cell.compute_tangents(sd-1, f) + rels = [numpy.linalg.norm(ehat) for ehat in ehats] + thats = [a / b for a, b in zip(ehats, rels)] + vf = fiat_cell.volume_of_subcomplex(sd-1, f) + + scale = 1.0 / numpy.dot(thats[1], numpy.cross(thats[0], nhat)) + orth_vecs = [scale * numpy.cross(nhat, thats[1]), + scale * numpy.cross(thats[0], nhat)] + + Jn = J @ Literal(nhat) + Jts = [J @ Literal(that) for that in thats] + Jorth = [J @ Literal(ov) for ov in orth_vecs] + + alphas = [(Jn @ Jts[i] / detJ) * (Literal(rels[i]) / Literal(2*vf)) for i in range(sd-1)] + betas = [Jorth[0] @ Jts[i] / detJ for i in range(sd-1)] + gammas = [Jorth[1] @ Jts[i] / detJ for i in range(sd-1)] + + det = betas[0] * gammas[1] - betas[1] * gammas[0] + + for i in range(dimPk_facet): + idx = offset*f + i * sd + + Vsub[idx+1, idx] = (alphas[1] * gammas[0] + - alphas[0] * gammas[1]) / det + Vsub[idx+1, idx+1] = gammas[1] / det + Vsub[idx+1, idx+2] = Literal(-1) * gammas[0] / det + Vsub[idx+2, idx] = (alphas[0] * betas[1] + - alphas[1] * betas[0]) / det + Vsub[idx+2, idx+1] = Literal(-1) * betas[1] / det + Vsub[idx+2, idx+2] = betas[0] / det return Vsub @@ -66,13 +106,11 @@ def __init__(self, cell, degree): def basis_transformation(self, coordinate_mapping): """Note, the extra 3 dofs which are removed here correspond to the constraints.""" - - T = self.cell V = numpy.zeros((18, 15), dtype=object) for multiindex in numpy.ndindex(V.shape): V[multiindex] = Literal(V[multiindex]) - V[:12, :12] = _edge_transform(T, coordinate_mapping) + V[:12, :12] = _facet_transform(self.cell, 1, coordinate_mapping) # internal dofs W = _evaluation_transform(coordinate_mapping) @@ -126,7 +164,7 @@ def basis_transformation(self, coordinate_mapping): # Put into the right rows and columns. V[0:3, 0:3] = V[3:6, 3:6] = V[6:9, 6:9] = W - V[9:21, 9:21] = _edge_transform(self.cell, coordinate_mapping) + V[9:21, 9:21] = _facet_transform(self.cell, 1, coordinate_mapping) # internal DOFs detJ = coordinate_mapping.detJ_at([1/3, 1/3]) diff --git a/finat/hct.py b/finat/hct.py index d24bca21c..6c5fe5a9b 100644 --- a/finat/hct.py +++ b/finat/hct.py @@ -78,6 +78,13 @@ def __init__(self, cell, degree): Citations().register("Clough1965") super().__init__(FIAT.HsiehCloughTocher(cell, reduced=True)) + reduced_dofs = deepcopy(self._element.entity_dofs()) + sd = cell.get_spatial_dimension() + fdim = sd - 1 + for entity in reduced_dofs[fdim]: + reduced_dofs[fdim][entity] = [] + self._entity_dofs = reduced_dofs + def basis_transformation(self, coordinate_mapping): # Jacobians at cell center J = coordinate_mapping.jacobian_at([1/3, 1/3]) @@ -133,11 +140,7 @@ def basis_transformation(self, coordinate_mapping): return ListTensor(V.T) def entity_dofs(self): - edofs = deepcopy(super(ReducedHsiehCloughTocher, self).entity_dofs()) - dim = 1 - for entity in edofs[dim]: - edofs[dim][entity] = [] - return edofs + return self._entity_dofs @property def index_shape(self): diff --git a/finat/johnson_mercier.py b/finat/johnson_mercier.py new file mode 100644 index 000000000..b5da38785 --- /dev/null +++ b/finat/johnson_mercier.py @@ -0,0 +1,34 @@ +import FIAT +import numpy +from gem import ListTensor, Literal + +from finat.aw import _facet_transform +from finat.fiat_elements import FiatElement +from finat.physically_mapped import Citations, PhysicallyMappedElement + + +class JohnsonMercier(PhysicallyMappedElement, FiatElement): # symmetric matrix valued + def __init__(self, cell, degree, variant=None): + if degree != 1: + raise ValueError("Degree must be 1 for Johnson-Mercier element") + if Citations is not None: + Citations().register("Gopalakrishnan2024") + self._indices = slice(None, None) + super(JohnsonMercier, self).__init__(FIAT.JohnsonMercier(cell, degree, variant=variant)) + + def basis_transformation(self, coordinate_mapping): + numbf = self._element.space_dimension() + ndof = self.space_dimension() + V = numpy.eye(numbf, ndof, dtype=object) + for multiindex in numpy.ndindex(V.shape): + V[multiindex] = Literal(V[multiindex]) + + Vsub = _facet_transform(self.cell, 1, coordinate_mapping) + Vsub = Vsub[:, self._indices] + m, n = Vsub.shape + V[:m, :n] = Vsub + + # Note: that the edge DOFs are scaled by edge lengths in FIAT implies + # that they are already have the necessary rescaling to improve + # conditioning. + return ListTensor(V.T) diff --git a/finat/physically_mapped.py b/finat/physically_mapped.py index 9234e6ffe..50ef62f2d 100644 --- a/finat/physically_mapped.py +++ b/finat/physically_mapped.py @@ -136,6 +136,14 @@ year={2017}, institution={Tech. Rep. ICES REPORT 17-28, Institute for Computational Engineering and Sciences} } +""") + Citations().add("Gopalakrishnan2024", """ +@article{gopalakrishnan2024johnson, + title={{The Johnson-Mercier elasticity element in any dimensions}}, + author={Gopalakrishnan, J and Guzman, J and Lee, J J}, + journal={arXiv preprint arXiv:2403.13189}, + year={2024} +} """) except ImportError: Citations = None diff --git a/finat/ufl/elementlist.py b/finat/ufl/elementlist.py index f8f8ac33b..565d9461d 100644 --- a/finat/ufl/elementlist.py +++ b/finat/ufl/elementlist.py @@ -142,6 +142,8 @@ def show_elements(): register_element("Regge", "Regge", 2, HEin, "double covariant Piola", (0, None), simplices[1:]) register_element("HDiv Trace", "HDivT", 0, L2, "identity", (0, None), any_cell) +register_element("Johnson-Mercier", "JM", 2, HDivDiv, + "double contravariant Piola", (1, 1), simplices[1:]) register_element("Hellan-Herrmann-Johnson", "HHJ", 2, HDivDiv, "double contravariant Piola", (0, None), ("triangle",)) register_element("Nonconforming Arnold-Winther", "AWnc", 2, HDivDiv, diff --git a/test/test_hct.py b/test/test_hct.py index 926064d71..f5a09a7f8 100644 --- a/test/test_hct.py +++ b/test/test_hct.py @@ -1,49 +1,53 @@ import FIAT import finat import numpy as np +import pytest from gem.interpreter import evaluate from fiat_mapping import MyMapping -def test_hct(): - degree = 3 - ref_cell = FIAT.ufc_simplex(2) - ref_pts = finat.point_set.PointSet(FIAT.reference_element.make_lattice(ref_cell.vertices, degree)) - ref_element = finat.HsiehCloughTocher(ref_cell, degree, avg=True) +@pytest.fixture +def ref_cell(request): + K = FIAT.ufc_simplex(2) + return K - phys_cell = FIAT.ufc_simplex(2) - phys_cell.vertices = ((0.0, 0.1), (1.17, -0.09), (0.15, 1.84)) - mapping = MyMapping(ref_cell, phys_cell) - z = (0, 0) - finat_vals_gem = ref_element.basis_evaluation(0, ref_pts, coordinate_mapping=mapping)[z] - finat_vals = evaluate([finat_vals_gem])[0].arr +@pytest.fixture +def phys_cell(request): + K = FIAT.ufc_simplex(2) + K.vertices = ((0.0, 0.1), (1.17, -0.09), (0.15, 1.84)) + return K - phys_cell_FIAT = FIAT.HsiehCloughTocher(phys_cell, degree) - phys_points = FIAT.reference_element.make_lattice(phys_cell.vertices, degree) - phys_vals = phys_cell_FIAT.tabulate(0, phys_points)[z] - assert np.allclose(finat_vals.T, phys_vals) +def make_unisolvent_points(element): + degree = element.degree() + ref_complex = element.get_reference_complex() + sd = ref_complex.get_spatial_dimension() + top = ref_complex.get_topology() + pts = [] + for cell in top[sd]: + pts.extend(ref_complex.make_points(sd, cell, degree+sd+1)) + return pts -def test_reduced_hct(): +@pytest.mark.parametrize("reduced", (False, True), ids=("standard", "reduced")) +def test_hct(ref_cell, phys_cell, reduced): degree = 3 - ref_cell = FIAT.ufc_simplex(2) - ref_pts = finat.point_set.PointSet(FIAT.reference_element.make_lattice(ref_cell.vertices, degree)) - ref_element = finat.ReducedHsiehCloughTocher(ref_cell, degree) - - phys_cell = FIAT.ufc_simplex(2) - phys_cell.vertices = ((0.0, 0.1), (1.17, -0.09), (0.15, 1.84)) + if reduced: + ref_element = finat.ReducedHsiehCloughTocher(ref_cell, degree) + else: + ref_element = finat.HsiehCloughTocher(ref_cell, degree, avg=True) + ref_pts = finat.point_set.PointSet(make_unisolvent_points(ref_element._element)) mapping = MyMapping(ref_cell, phys_cell) - z = (0, 0) + z = (0,) * ref_element.cell.get_spatial_dimension() finat_vals_gem = ref_element.basis_evaluation(0, ref_pts, coordinate_mapping=mapping)[z] finat_vals = evaluate([finat_vals_gem])[0].arr - phys_cell_FIAT = FIAT.HsiehCloughTocher(phys_cell, degree, reduced=True) - phys_points = FIAT.reference_element.make_lattice(phys_cell.vertices, degree) - phys_vals = phys_cell_FIAT.tabulate(0, phys_points)[z] + phys_element = FIAT.HsiehCloughTocher(phys_cell, degree, reduced=reduced) + phys_points = make_unisolvent_points(phys_element) + phys_vals = phys_element.tabulate(0, phys_points)[z] numbf = ref_element.space_dimension() assert np.allclose(finat_vals.T, phys_vals[:numbf]) diff --git a/test/test_johnson_mercier.py b/test/test_johnson_mercier.py new file mode 100644 index 000000000..aa90e7ec6 --- /dev/null +++ b/test/test_johnson_mercier.py @@ -0,0 +1,78 @@ +import FIAT +import finat +import numpy as np +import pytest +from gem.interpreter import evaluate + +from fiat_mapping import MyMapping + + +def make_unisolvent_points(element): + degree = element.degree() + ref_complex = element.get_reference_complex() + sd = ref_complex.get_spatial_dimension() + top = ref_complex.get_topology() + pts = [] + for cell in top[sd]: + pts.extend(ref_complex.make_points(sd, cell, degree+sd+1)) + return pts + + +@pytest.mark.parametrize('phys_verts', + [((0.0, 0.0), (2.0, 0.1), (0.0, 1.0)), + ((0, 0, 0), (1., 0.1, -0.37), + (0.01, 0.987, -.23), + (-0.1, -0.2, 1.38))]) +def test_johnson_mercier(phys_verts): + degree = 1 + variant = None + sd = len(phys_verts) - 1 + z = tuple(0 for _ in range(sd)) + ref_cell = FIAT.ufc_simplex(sd) + ref_el_finat = finat.JohnsonMercier(ref_cell, degree, variant=variant) + indices = ref_el_finat._indices + + ref_element = ref_el_finat._element + ref_pts = make_unisolvent_points(ref_element) + ref_vals = ref_element.tabulate(0, ref_pts)[z] + + phys_cell = FIAT.ufc_simplex(sd) + phys_cell.vertices = phys_verts + phys_element = type(ref_element)(phys_cell, degree, variant=variant) + + phys_pts = make_unisolvent_points(phys_element) + phys_vals = phys_element.tabulate(0, phys_pts)[z] + + # Piola map the reference elements + J, b = FIAT.reference_element.make_affine_mapping(ref_cell.vertices, + phys_cell.vertices) + detJ = np.linalg.det(J) + + ref_vals_piola = np.zeros(ref_vals.shape) + for i in range(ref_vals.shape[0]): + for k in range(ref_vals.shape[3]): + ref_vals_piola[i, :, :, k] = \ + J @ ref_vals[i, :, :, k] @ J.T / detJ**2 + + num_dofs = ref_el_finat.space_dimension() + num_bfs = phys_element.space_dimension() + num_facet_bfs = (sd + 1) * len(phys_element.dual.entity_ids[sd-1][0]) + + # Zany map the results + mappng = MyMapping(ref_cell, phys_cell) + Mgem = ref_el_finat.basis_transformation(mappng) + M = evaluate([Mgem])[0].arr + ref_vals_zany = np.zeros((num_dofs, sd, sd, len(phys_pts))) + for k in range(ref_vals_zany.shape[3]): + for ell1 in range(sd): + for ell2 in range(sd): + ref_vals_zany[:, ell1, ell2, k] = \ + M @ ref_vals_piola[:, ell1, ell2, k] + + # Solve for the basis transformation and compare results + Phi = ref_vals_piola.reshape(num_bfs, -1) + phi = phys_vals.reshape(num_bfs, -1) + Mh = np.linalg.solve(Phi @ Phi.T, Phi @ phi.T).T + assert np.allclose(M[:num_facet_bfs], Mh[indices][:num_facet_bfs]) + + assert np.allclose(ref_vals_zany[:num_facet_bfs], phys_vals[indices][:num_facet_bfs]) From 0f661e4797e48dd020459661b2a7537ed1dcf3be Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Wed, 26 Jun 2024 17:12:19 +0100 Subject: [PATCH 731/749] GEM: sympy2gem conditional support (#128) Co-authored-by: David A. Ham --- finat/sympy2gem.py | 70 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/finat/sympy2gem.py b/finat/sympy2gem.py index 9c148bb91..ccbfbf7aa 100644 --- a/finat/sympy2gem.py +++ b/finat/sympy2gem.py @@ -1,5 +1,6 @@ from functools import singledispatch, reduce +import numpy import sympy try: import symengine @@ -65,3 +66,72 @@ def sympy2gem_symbol(node, self): @sympy2gem.register(symengine.Rational) def sympy2gem_rational(node, self): return gem.Division(*(map(self, node.as_numer_denom()))) + + +@sympy2gem.register(sympy.Abs) +@sympy2gem.register(symengine.Abs) +def sympy2gem_abs(node, self): + return gem.MathFunction("abs", *map(self, node.args)) + + +@sympy2gem.register(sympy.Not) +@sympy2gem.register(symengine.Not) +def sympy2gem_not(node, self): + return gem.LogicalNot(*map(self, node.args)) + + +@sympy2gem.register(sympy.Or) +@sympy2gem.register(symengine.Or) +def sympy2gem_or(node, self): + return reduce(gem.LogicalOr, map(self, node.args)) + + +@sympy2gem.register(sympy.And) +@sympy2gem.register(symengine.And) +def sympy2gem_and(node, self): + return reduce(gem.LogicalAnd, map(self, node.args)) + + +@sympy2gem.register(sympy.Eq) +@sympy2gem.register(symengine.Eq) +def sympy2gem_eq(node, self): + return gem.Comparison("==", *map(self, node.args)) + + +@sympy2gem.register(sympy.Gt) +def sympy2gem_gt(node, self): + return gem.Comparison(">", *map(self, node.args)) + + +@sympy2gem.register(sympy.Ge) +def sympy2gem_ge(node, self): + return gem.Comparison(">=", *map(self, node.args)) + + +@sympy2gem.register(sympy.Lt) +@sympy2gem.register(symengine.Lt) +def sympy2gem_lt(node, self): + return gem.Comparison("<", *map(self, node.args)) + + +@sympy2gem.register(sympy.Le) +@sympy2gem.register(symengine.Le) +def sympy2gem_le(node, self): + return gem.Comparison("<=", *map(self, node.args)) + + +@sympy2gem.register(sympy.Piecewise) +@sympy2gem.register(symengine.Piecewise) +def sympy2gem_conditional(node, self): + expr = None + pieces = [] + for v, c in node.args: + if isinstance(c, (bool, numpy.bool)) and c: + expr = self(v) + break + pieces.append((v, c)) + if expr is None: + expr = gem.Literal(float("nan")) + for v, c in reversed(pieces): + expr = gem.Conditional(self(c), self(v), expr) + return expr From a6fe525448878764f6f9d460b2ac158743c61882 Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Fri, 28 Jun 2024 23:01:22 +0100 Subject: [PATCH 732/749] numpy 2.0 fix --- gem/flop_count.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gem/flop_count.py b/gem/flop_count.py index 934c3386e..b9595e817 100644 --- a/gem/flop_count.py +++ b/gem/flop_count.py @@ -142,7 +142,7 @@ def flops_indexed(expr, temporaries): aggregate = sum(expression_flops(child, temporaries) for child in expr.children) # Average flops per entry - return aggregate / numpy.product(expr.children[0].shape, dtype=int) + return aggregate / numpy.prod(expr.children[0].shape, dtype=int) @flops.register(gem.IndexSum) From 6eb834ac5d58719e84ffc3154430f5fe2c41eae7 Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Thu, 11 Jul 2024 18:17:28 +0100 Subject: [PATCH 733/749] High order HCT (#129) --- finat/argyris.py | 161 +++++++++++++++++++++------------ finat/bell.py | 79 ++++++++-------- finat/hct.py | 104 ++++++++------------- finat/hermite.py | 2 +- finat/morley.py | 2 +- finat/physically_mapped.py | 11 +++ finat/ufl/elementlist.py | 4 +- test/fiat_mapping.py | 10 ++ test/test_hct.py | 53 ----------- test/test_mass_conditioning.py | 51 +++++++++++ test/test_zany_mapping.py | 101 +++++++++++++++++++++ 11 files changed, 355 insertions(+), 223 deletions(-) delete mode 100644 test/test_hct.py create mode 100644 test/test_mass_conditioning.py create mode 100644 test/test_zany_mapping.py diff --git a/finat/argyris.py b/finat/argyris.py index 38713d947..adba5eac6 100644 --- a/finat/argyris.py +++ b/finat/argyris.py @@ -1,42 +1,78 @@ import numpy +from math import comb import FIAT -from gem import Literal, ListTensor +from gem import Literal, ListTensor, partial_indexed from finat.fiat_elements import ScalarFiatElement from finat.physically_mapped import PhysicallyMappedElement, Citations +def _edge_transform(V, voffset, fiat_cell, moment_deg, coordinate_mapping, avg=False): + """Basis transformation for integral edge moments.""" + + J = coordinate_mapping.jacobian_at([1/3, 1/3]) + rns = coordinate_mapping.reference_normals() + pts = coordinate_mapping.physical_tangents() + pns = coordinate_mapping.physical_normals() + pel = coordinate_mapping.physical_edge_lengths() + + top = fiat_cell.get_topology() + eoffset = 2 * moment_deg - 1 + toffset = moment_deg - 1 + for e in sorted(top[1]): + nhat = partial_indexed(rns, (e, )) + n = partial_indexed(pns, (e, )) + t = partial_indexed(pts, (e, )) + Bn = J @ nhat / pel[e] + Bnt = Bn @ t + Bnn = Bn @ n + if avg: + Bnn = Bnn * pel[e] + + v0id, v1id = (v * voffset for v in top[1][e]) + s0 = len(top[0]) * voffset + e * eoffset + for k in range(moment_deg): + s = s0 + k + P1 = Literal(comb(k + 2, k)) + P0 = -(-1)**k * P1 + V[s, s] = Bnn + V[s, v1id] = P1 * Bnt + V[s, v0id] = P0 * Bnt + if k > 0: + V[s, s + toffset] = -1 * Bnt + + class Argyris(PhysicallyMappedElement, ScalarFiatElement): - def __init__(self, cell, degree): - if degree != 5: - raise ValueError("Degree must be 5 for Argyris element") + def __init__(self, cell, degree=5, variant=None, avg=False): if Citations is not None: Citations().register("Argyris1968") - super().__init__(FIAT.QuinticArgyris(cell)) + if variant is None: + variant = "integral" + if variant == "point" and degree != 5: + raise NotImplementedError("Degree must be 5 for 'point' variant of Argyris") + fiat_element = FIAT.Argyris(cell, degree, variant=variant) + self.variant = variant + self.avg = avg + super().__init__(fiat_element) def basis_transformation(self, coordinate_mapping): - # Jacobians at edge midpoints + # Jacobian at barycenter J = coordinate_mapping.jacobian_at([1/3, 1/3]) - rns = coordinate_mapping.reference_normals() - pns = coordinate_mapping.physical_normals() - - pts = coordinate_mapping.physical_tangents() - - pel = coordinate_mapping.physical_edge_lengths() - - V = numpy.zeros((21, 21), dtype=object) - + ndof = self.space_dimension() + V = numpy.eye(ndof, dtype=object) for multiindex in numpy.ndindex(V.shape): V[multiindex] = Literal(V[multiindex]) - for v in range(3): - s = 6*v - V[s, s] = Literal(1) - for i in range(2): - for j in range(2): + sd = self.cell.get_spatial_dimension() + top = self.cell.get_topology() + voffset = (sd+1)*sd//2 + sd + 1 + for v in sorted(top[0]): + s = voffset * v + for i in range(sd): + for j in range(sd): V[s+1+i, s+1+j] = J[j, i] V[s+3, s+3] = J[0, 0]*J[0, 0] V[s+3, s+4] = 2*J[0, 0]*J[1, 0] @@ -48,47 +84,54 @@ def basis_transformation(self, coordinate_mapping): V[s+5, s+4] = 2*J[0, 1]*J[1, 1] V[s+5, s+5] = J[1, 1]*J[1, 1] - for e in range(3): - v0id, v1id = [i for i in range(3) if i != e] - - # nhat . J^{-T} . t - foo = (rns[e, 0]*(J[0, 0]*pts[e, 0] + J[1, 0]*pts[e, 1]) - + rns[e, 1]*(J[0, 1]*pts[e, 0] + J[1, 1]*pts[e, 1])) - - # vertex points - V[18+e, 6*v0id] = -15/8 * (foo / pel[e]) - V[18+e, 6*v1id] = 15/8 * (foo / pel[e]) - - # vertex derivatives - for i in (0, 1): - V[18+e, 6*v0id+1+i] = -7/16*foo*pts[e, i] - V[18+e, 6*v1id+1+i] = V[18+e, 6*v0id+1+i] - - # second derivatives - tau = [pts[e, 0]*pts[e, 0], - 2*pts[e, 0]*pts[e, 1], - pts[e, 1]*pts[e, 1]] - - for i in (0, 1, 2): - V[18+e, 6*v0id+3+i] = -1/32 * (pel[e]*foo*tau[i]) - V[18+e, 6*v1id+3+i] = 1/32 * (pel[e]*foo*tau[i]) - - V[18+e, 18+e] = (rns[e, 0]*(J[0, 0]*pns[e, 0] + J[1, 0]*pns[e, 1]) - + rns[e, 1]*(J[0, 1]*pns[e, 0] + J[1, 1]*pns[e, 1])) + q = self.degree - 4 + if self.variant == "integral": + _edge_transform(V, voffset, self.cell, q, coordinate_mapping, avg=self.avg) + else: + rns = coordinate_mapping.reference_normals() + pns = coordinate_mapping.physical_normals() + pts = coordinate_mapping.physical_tangents() + pel = coordinate_mapping.physical_edge_lengths() + for e in sorted(top[1]): + nhat = partial_indexed(rns, (e, )) + n = partial_indexed(pns, (e, )) + t = partial_indexed(pts, (e, )) + Bn = J @ nhat + Bnt = Bn @ t + Bnn = Bn @ n + + s = len(top[0]) * voffset + e + v0id, v1id = (v * voffset for v in top[1][e]) + V[s, s] = Bnn + + # vertex points + V[s, v1id] = 15/8 * Bnt / pel[e] + V[s, v0id] = -1 * V[s, v1id] + + # vertex derivatives + for i in range(sd): + V[s, v1id+1+i] = -7/16 * Bnt * t[i] + V[s, v0id+1+i] = V[s, v1id+1+i] + + # second derivatives + tau = [t[0]*t[0], 2*t[0]*t[1], t[1]*t[1]] + for i in range(len(tau)): + V[s, v1id+3+i] = 1/32 * (pel[e] * Bnt * tau[i]) + V[s, v0id+3+i] = -1 * V[s, v1id+3+i] # Patch up conditioning h = coordinate_mapping.cell_size() - - for v in range(3): - for k in range(2): - for i in range(21): - V[i, 6*v+1+k] = V[i, 6*v+1+k] / h[v] - for k in range(3): - for i in range(21): - V[i, 6*v+3+k] = V[i, 6*v+3+k] / (h[v]*h[v]) - for e in range(3): - v0id, v1id = [i for i in range(3) if i != e] - for i in range(21): - V[i, 18+e] = 2*V[i, 18+e] / (h[v0id] + h[v1id]) + for v in sorted(top[0]): + for k in range(sd): + V[:, voffset*v+1+k] *= 1 / h[v] + for k in range((sd+1)*sd//2): + V[:, voffset*v+3+k] *= 1 / (h[v]*h[v]) + + if self.variant == "point": + eoffset = 2 * q - 1 + for e in sorted(top[1]): + v0, v1 = top[1][e] + s = len(top[0]) * voffset + e * eoffset + V[:, s:s+q] *= 2 / (h[v0] + h[v1]) return ListTensor(V.T) diff --git a/finat/bell.py b/finat/bell.py index 561d6be35..caab5c001 100644 --- a/finat/bell.py +++ b/finat/bell.py @@ -2,16 +2,16 @@ import FIAT -from gem import Literal, ListTensor +from gem import Literal, ListTensor, partial_indexed from finat.fiat_elements import ScalarFiatElement from finat.physically_mapped import PhysicallyMappedElement, Citations class Bell(PhysicallyMappedElement, ScalarFiatElement): - def __init__(self, cell, degree): + def __init__(self, cell, degree=5): if degree != 5: - raise ValueError("Degree must be 3 for Bell element") + raise ValueError("Degree must be 5 for Bell element") if Citations is not None: Citations().register("Bell1969") super().__init__(FIAT.Bell(cell)) @@ -20,22 +20,20 @@ def basis_transformation(self, coordinate_mapping): # Jacobians at edge midpoints J = coordinate_mapping.jacobian_at([1/3, 1/3]) - rns = coordinate_mapping.reference_normals() - - pts = coordinate_mapping.physical_tangents() - - pel = coordinate_mapping.physical_edge_lengths() - - V = numpy.zeros((21, 18), dtype=object) - + numbf = self._element.space_dimension() + ndof = self.space_dimension() + # rectangular to toss out the constraint dofs + V = numpy.eye(numbf, ndof, dtype=object) for multiindex in numpy.ndindex(V.shape): V[multiindex] = Literal(V[multiindex]) - for v in range(3): - s = 6*v - V[s, s] = Literal(1) - for i in range(2): - for j in range(2): + sd = self.cell.get_spatial_dimension() + top = self.cell.get_topology() + voffset = sd + 1 + (sd*(sd+1))//2 + for v in sorted(top[1]): + s = voffset * v + for i in range(sd): + for j in range(sd): V[s+1+i, s+1+j] = J[j, i] V[s+3, s+3] = J[0, 0]*J[0, 0] V[s+3, s+4] = 2*J[0, 0]*J[1, 0] @@ -47,40 +45,39 @@ def basis_transformation(self, coordinate_mapping): V[s+5, s+4] = 2*J[0, 1]*J[1, 1] V[s+5, s+5] = J[1, 1]*J[1, 1] - for e in range(3): - v0id, v1id = [i for i in range(3) if i != e] + rns = coordinate_mapping.reference_normals() + pts = coordinate_mapping.physical_tangents() + pel = coordinate_mapping.physical_edge_lengths() + for e in sorted(top[1]): + s = len(top[0]) * voffset + e + v0id, v1id = (v * voffset for v in top[1][e]) - # nhat . J^{-T} . t - foo = (rns[e, 0]*(J[0, 0]*pts[e, 0] + J[1, 0]*pts[e, 1]) - + rns[e, 1]*(J[0, 1]*pts[e, 0] + J[1, 1]*pts[e, 1])) + nhat = partial_indexed(rns, (e, )) + t = partial_indexed(pts, (e, )) + Bnt = (J @ nhat) @ t # vertex points - V[18+e, 6*v0id] = -1/21 * (foo / pel[e]) - V[18+e, 6*v1id] = 1/21 * (foo / pel[e]) + V[s, v1id] = 1/21 * Bnt / pel[e] + V[s, v0id] = -1 * V[s, v1id] # vertex derivatives - for i in (0, 1): - V[18+e, 6*v0id+1+i] = -1/42*foo*pts[e, i] - V[18+e, 6*v1id+1+i] = V[18+e, 6*v0id+1+i] + for i in range(sd): + V[s, v1id+1+i] = -1/42 * Bnt * t[i] + V[s, v0id+1+i] = V[s, v1id+1+i] # second derivatives - tau = [pts[e, 0]*pts[e, 0], - 2*pts[e, 0]*pts[e, 1], - pts[e, 1]*pts[e, 1]] - - for i in (0, 1, 2): - V[18+e, 6*v0id+3+i] = -1/252 * (pel[e]*foo*tau[i]) - V[18+e, 6*v1id+3+i] = 1/252 * (pel[e]*foo*tau[i]) + tau = [t[0]*t[0], 2*t[0]*t[1], t[1]*t[1]] + for i in range(len(tau)): + V[s, v1id+3+i] = 1/252 * (pel[e] * Bnt * tau[i]) + V[s, v0id+3+i] = -1 * V[s, v1id+3+i] + # Patch up conditioning h = coordinate_mapping.cell_size() - - for v in range(3): - for k in range(2): - for i in range(21): - V[i, 6*v+1+k] = V[i, 6*v+1+k] / h[v] - for k in range(3): - for i in range(21): - V[i, 6*v+3+k] = V[i, 6*v+3+k] / (h[v]*h[v]) + for v in sorted(top[0]): + for k in range(sd): + V[:, voffset*v+1+k] *= 1/h[v] + for k in range((sd+1)*sd//2): + V[:, voffset*v+3+k] *= 1/(h[v]*h[v]) return ListTensor(V.T) diff --git a/finat/hct.py b/finat/hct.py index 6c5fe5a9b..5ba95ff00 100644 --- a/finat/hct.py +++ b/finat/hct.py @@ -2,76 +2,54 @@ import numpy from gem import ListTensor, Literal, partial_indexed +from finat.argyris import _edge_transform from finat.fiat_elements import ScalarFiatElement from finat.physically_mapped import Citations, PhysicallyMappedElement from copy import deepcopy class HsiehCloughTocher(PhysicallyMappedElement, ScalarFiatElement): - def __init__(self, cell, degree, avg=False): - if degree != 3: - raise ValueError("Degree must be 3 for HCT element") + def __init__(self, cell, degree=3, avg=False): + if degree < 3: + raise ValueError("HCT only defined for degree >= 3") if Citations is not None: Citations().register("Clough1965") + if degree > 3: + Citations().register("Groselj2022") self.avg = avg - super().__init__(FIAT.HsiehCloughTocher(cell)) + super().__init__(FIAT.HsiehCloughTocher(cell, degree)) def basis_transformation(self, coordinate_mapping): # Jacobians at cell center J = coordinate_mapping.jacobian_at([1/3, 1/3]) - rns = coordinate_mapping.reference_normals() - pts = coordinate_mapping.physical_tangents() - pns = coordinate_mapping.physical_normals() - - pel = coordinate_mapping.physical_edge_lengths() - - d = self.cell.get_dimension() ndof = self.space_dimension() V = numpy.eye(ndof, dtype=object) for multiindex in numpy.ndindex(V.shape): V[multiindex] = Literal(V[multiindex]) - voffset = d+1 - for v in range(d+1): + sd = self.cell.get_dimension() + top = self.cell.get_topology() + voffset = sd + 1 + for v in sorted(top[0]): s = voffset * v - for i in range(d): - for j in range(d): + for i in range(sd): + for j in range(sd): V[s+1+i, s+1+j] = J[j, i] - for e in range(3): - s = (d+1) * voffset + e - v0id, v1id = [i * voffset for i in range(3) if i != e] - - nhat = partial_indexed(rns, (e, )) - t = partial_indexed(pts, (e, )) - n = partial_indexed(pns, (e, )) - - Bn = J @ nhat / pel[e] - Bnn = Bn @ n - Bnt = Bn @ t - - if self.avg: - Bnn = Bnn * pel[e] - V[s, s] = Bnn - V[s, v0id] = Literal(-1) * Bnt - V[s, v1id] = Bnt + q = self.degree - 2 + _edge_transform(V, voffset, self.cell, q, coordinate_mapping, avg=self.avg) # Patch up conditioning h = coordinate_mapping.cell_size() - for v in range(d+1): - s = voffset * v - for k in range(d): - V[:, s+1+k] /= h[v] - - for e in range(3): - v0id, v1id = [i for i in range(3) if i != e] - V[:, 9+e] *= 2 / (h[v0id] + h[v1id]) + for v in sorted(top[0]): + for k in range(sd): + V[:, voffset*v+1+k] /= h[v] return ListTensor(V.T) class ReducedHsiehCloughTocher(PhysicallyMappedElement, ScalarFiatElement): - def __init__(self, cell, degree): + def __init__(self, cell, degree=3): if degree != 3: raise ValueError("Degree must be 3 for reduced HCT element") if Citations is not None: @@ -80,22 +58,14 @@ def __init__(self, cell, degree): reduced_dofs = deepcopy(self._element.entity_dofs()) sd = cell.get_spatial_dimension() - fdim = sd - 1 - for entity in reduced_dofs[fdim]: - reduced_dofs[fdim][entity] = [] + for entity in reduced_dofs[sd-1]: + reduced_dofs[sd-1][entity] = [] self._entity_dofs = reduced_dofs def basis_transformation(self, coordinate_mapping): - # Jacobians at cell center + # Jacobian at barycenter J = coordinate_mapping.jacobian_at([1/3, 1/3]) - rns = coordinate_mapping.reference_normals() - pts = coordinate_mapping.physical_tangents() - # pns = coordinate_mapping.physical_normals() - - pel = coordinate_mapping.physical_edge_lengths() - - d = self.cell.get_dimension() numbf = self._element.space_dimension() ndof = self.space_dimension() # rectangular to toss out the constraint dofs @@ -103,25 +73,27 @@ def basis_transformation(self, coordinate_mapping): for multiindex in numpy.ndindex(V.shape): V[multiindex] = Literal(V[multiindex]) - voffset = d+1 - for v in range(d+1): + sd = self.cell.get_spatial_dimension() + top = self.cell.get_topology() + voffset = sd + 1 + for v in sorted(top[0]): s = voffset * v - for i in range(d): - for j in range(d): + for i in range(sd): + for j in range(sd): V[s+1+i, s+1+j] = J[j, i] - for e in range(3): - s = (d+1) * voffset + e - v0id, v1id = [i * voffset for i in range(3) if i != e] + rns = coordinate_mapping.reference_normals() + pts = coordinate_mapping.physical_tangents() + pel = coordinate_mapping.physical_edge_lengths() + + for e in sorted(top[1]): + s = len(top[0]) * voffset + e + v0id, v1id = (v * voffset for v in top[1][e]) nhat = partial_indexed(rns, (e, )) t = partial_indexed(pts, (e, )) - - # n = partial_indexed(pns, (e, )) - # Bnn = (J @ nhat) @ n - # V[s, s] = Bnn - Bnt = (J @ nhat) @ t + V[s, v0id] = Literal(1/5) * Bnt / pel[e] V[s, v1id] = Literal(-1) * V[s, v0id] @@ -133,9 +105,9 @@ def basis_transformation(self, coordinate_mapping): # Patch up conditioning h = coordinate_mapping.cell_size() - for v in range(d+1): + for v in sorted(top[0]): s = voffset * v - for k in range(d): + for k in range(sd): V[:, s+1+k] /= h[v] return ListTensor(V.T) diff --git a/finat/hermite.py b/finat/hermite.py index 6f453cda5..6049d92e8 100644 --- a/finat/hermite.py +++ b/finat/hermite.py @@ -8,7 +8,7 @@ class Hermite(PhysicallyMappedElement, ScalarFiatElement): - def __init__(self, cell, degree): + def __init__(self, cell, degree=3): if degree != 3: raise ValueError("Degree must be 3 for Hermite element") if Citations is not None: diff --git a/finat/morley.py b/finat/morley.py index 73e060192..3d3c7f8bf 100644 --- a/finat/morley.py +++ b/finat/morley.py @@ -9,7 +9,7 @@ class Morley(PhysicallyMappedElement, ScalarFiatElement): - def __init__(self, cell, degree): + def __init__(self, cell, degree=2): if degree != 2: raise ValueError("Degree must be 2 for Morley element") if Citations is not None: diff --git a/finat/physically_mapped.py b/finat/physically_mapped.py index 50ef62f2d..716f7044e 100644 --- a/finat/physically_mapped.py +++ b/finat/physically_mapped.py @@ -144,6 +144,17 @@ journal={arXiv preprint arXiv:2403.13189}, year={2024} } +""") + Citations().add("Groselj2022", """ +@article{groselj2022generalized, + title={{Generalized C1 Clough--Tocher splines for CAGD and FEM}}, + author={Gro{\v{s}}elj, Jan and Knez, Marjeta}, + journal={Computer Methods in Applied Mechanics and Engineering}, + volume={395}, + pages={114983}, + year={2022}, + publisher={Elsevier} +} """) except ImportError: Citations = None diff --git a/finat/ufl/elementlist.py b/finat/ufl/elementlist.py index 565d9461d..fcb02be90 100644 --- a/finat/ufl/elementlist.py +++ b/finat/ufl/elementlist.py @@ -110,8 +110,8 @@ def show_elements(): (1, None), simplices[1:]) # "RTF" (2d), "N1F" (3d) # Elements not in the periodic table -register_element("Argyris", "ARG", 0, H2, "custom", (5, 5), ("triangle",)) -register_element("Hsieh-Clough-Tocher", "HCT", 0, H2, "custom", (3, 3), ("triangle",)) +register_element("Argyris", "ARG", 0, H2, "custom", (5, None), ("triangle",)) +register_element("Hsieh-Clough-Tocher", "HCT", 0, H2, "custom", (3, None), ("triangle",)) register_element("Reduced-Hsieh-Clough-Tocher", "HCT-red", 0, H2, "custom", (3, 3), ("triangle",)) register_element("Bell", "BELL", 0, H2, "custom", (5, 5), ("triangle",)) register_element("Brezzi-Douglas-Fortin-Marini", "BDFM", 1, HDiv, diff --git a/test/fiat_mapping.py b/test/fiat_mapping.py index 296e71191..d857b2577 100644 --- a/test/fiat_mapping.py +++ b/test/fiat_mapping.py @@ -60,3 +60,13 @@ def physical_points(self, ps, entity=None): def physical_vertices(self): return gem.Literal(self.phys_cell.verts) + + +class FiredrakeMapping(MyMapping): + + def cell_size(self): + # Firedrake interprets this as 2x the circumradius + cs = (np.prod([self.phys_cell.volume_of_subcomplex(1, i) + for i in range(3)]) + / 2.0 / self.phys_cell.volume()) + return np.asarray([cs for _ in range(3)]) diff --git a/test/test_hct.py b/test/test_hct.py deleted file mode 100644 index f5a09a7f8..000000000 --- a/test/test_hct.py +++ /dev/null @@ -1,53 +0,0 @@ -import FIAT -import finat -import numpy as np -import pytest -from gem.interpreter import evaluate - -from fiat_mapping import MyMapping - - -@pytest.fixture -def ref_cell(request): - K = FIAT.ufc_simplex(2) - return K - - -@pytest.fixture -def phys_cell(request): - K = FIAT.ufc_simplex(2) - K.vertices = ((0.0, 0.1), (1.17, -0.09), (0.15, 1.84)) - return K - - -def make_unisolvent_points(element): - degree = element.degree() - ref_complex = element.get_reference_complex() - sd = ref_complex.get_spatial_dimension() - top = ref_complex.get_topology() - pts = [] - for cell in top[sd]: - pts.extend(ref_complex.make_points(sd, cell, degree+sd+1)) - return pts - - -@pytest.mark.parametrize("reduced", (False, True), ids=("standard", "reduced")) -def test_hct(ref_cell, phys_cell, reduced): - degree = 3 - if reduced: - ref_element = finat.ReducedHsiehCloughTocher(ref_cell, degree) - else: - ref_element = finat.HsiehCloughTocher(ref_cell, degree, avg=True) - ref_pts = finat.point_set.PointSet(make_unisolvent_points(ref_element._element)) - - mapping = MyMapping(ref_cell, phys_cell) - z = (0,) * ref_element.cell.get_spatial_dimension() - finat_vals_gem = ref_element.basis_evaluation(0, ref_pts, coordinate_mapping=mapping)[z] - finat_vals = evaluate([finat_vals_gem])[0].arr - - phys_element = FIAT.HsiehCloughTocher(phys_cell, degree, reduced=reduced) - phys_points = make_unisolvent_points(phys_element) - phys_vals = phys_element.tabulate(0, phys_points)[z] - - numbf = ref_element.space_dimension() - assert np.allclose(finat_vals.T, phys_vals[:numbf]) diff --git a/test/test_mass_conditioning.py b/test/test_mass_conditioning.py new file mode 100644 index 000000000..996f093d7 --- /dev/null +++ b/test/test_mass_conditioning.py @@ -0,0 +1,51 @@ +import FIAT +import finat +import numpy as np +import pytest +from gem.interpreter import evaluate + +from fiat_mapping import FiredrakeMapping + + +@pytest.mark.parametrize("element, degree, variant", [ + (finat.Hermite, 3, None), + (finat.ReducedHsiehCloughTocher, 3, None), + (finat.HsiehCloughTocher, 3, None), + (finat.HsiehCloughTocher, 4, None), + (finat.Bell, 5, None), + (finat.Argyris, 5, "point"), + (finat.Argyris, 5, None), + (finat.Argyris, 6, None), + ]) +def test_mass_scaling(element, degree, variant): + sd = 2 + ref_cell = FIAT.ufc_simplex(sd) + if variant is not None: + ref_element = element(ref_cell, degree, variant=variant) + else: + ref_element = element(ref_cell, degree) + + Q = finat.quadrature.make_quadrature(ref_cell, 2*degree) + qpts = Q.point_set + qwts = Q.weights + + kappa = [] + for k in range(3): + h = 2 ** -k + phys_cell = FIAT.ufc_simplex(2) + new_verts = h * np.array(phys_cell.get_vertices()) + phys_cell.vertices = tuple(map(tuple, new_verts)) + mapping = FiredrakeMapping(ref_cell, phys_cell) + J_gem = mapping.jacobian_at(ref_cell.make_points(sd, 0, sd+1)[0]) + J = evaluate([J_gem])[0].arr + + z = (0,) * ref_element.cell.get_spatial_dimension() + finat_vals_gem = ref_element.basis_evaluation(0, qpts, coordinate_mapping=mapping)[z] + phis = evaluate([finat_vals_gem])[0].arr.T + + M = np.dot(np.multiply(phis, qwts * abs(np.linalg.det(J))), phis.T) + kappa.append(np.linalg.cond(M)) + + kappa = np.array(kappa) + ratio = kappa[1:] / kappa[:-1] + assert np.allclose(ratio, 1, atol=0.1) diff --git a/test/test_zany_mapping.py b/test/test_zany_mapping.py new file mode 100644 index 000000000..f7db7f769 --- /dev/null +++ b/test/test_zany_mapping.py @@ -0,0 +1,101 @@ +import FIAT +import finat +import numpy as np +import pytest +from gem.interpreter import evaluate + +from fiat_mapping import MyMapping + + +@pytest.fixture +def ref_cell(request): + K = FIAT.ufc_simplex(2) + return K + + +@pytest.fixture +def phys_cell(request): + K = FIAT.ufc_simplex(2) + K.vertices = ((0.0, 0.1), (1.17, -0.09), (0.15, 1.84)) + return K + + +def make_unisolvent_points(element): + degree = element.degree() + ref_complex = element.get_reference_complex() + top = ref_complex.get_topology() + pts = [] + for dim in top: + for entity in top[dim]: + pts.extend(ref_complex.make_points(dim, entity, degree, variant="gll")) + return pts + + +def check_zany_mapping(finat_element, phys_element): + ref_element = finat_element.fiat_equivalent + + ref_cell = ref_element.get_reference_element() + phys_cell = phys_element.get_reference_element() + mapping = MyMapping(ref_cell, phys_cell) + + ref_points = make_unisolvent_points(ref_element) + ps = finat.point_set.PointSet(ref_points) + + z = (0,) * finat_element.cell.get_spatial_dimension() + finat_vals_gem = finat_element.basis_evaluation(0, ps, coordinate_mapping=mapping)[z] + finat_vals = evaluate([finat_vals_gem])[0].arr.T + + phys_points = make_unisolvent_points(phys_element) + phys_vals = phys_element.tabulate(0, phys_points)[z] + + numdofs = finat_element.space_dimension() + numbfs = phys_element.space_dimension() + if numbfs == numdofs: + # Solve for the basis transformation and compare results + ref_vals = ref_element.tabulate(0, ref_points)[z] + Phi = ref_vals.reshape(numbfs, -1) + phi = phys_vals.reshape(numbfs, -1) + Mh = np.linalg.solve(Phi @ Phi.T, Phi @ phi.T).T + Mh[abs(Mh) < 1E-10] = 0 + # edofs = finat_element.entity_dofs() + # i = len(edofs[2][0]) + # offset = len(edofs[1][0]) + i + # print() + # print(Mh.T[-offset:-i]) + # print(M.T[-offset:-i]) + + Mgem = finat_element.basis_transformation(mapping) + M = evaluate([Mgem])[0].arr + assert np.allclose(M, Mh, atol=1E-9) + + assert np.allclose(finat_vals, phys_vals[:numdofs]) + + +@pytest.mark.parametrize("element", [ + finat.Morley, + finat.Hermite, + finat.ReducedHsiehCloughTocher, + finat.Bell]) +def test_C1_elements(ref_cell, phys_cell, element): + kwargs = {} + if element == finat.ReducedHsiehCloughTocher: + kwargs = dict(reduced=True) + finat_element = element(ref_cell) + phys_element = type(finat_element.fiat_equivalent)(phys_cell, **kwargs) + check_zany_mapping(finat_element, phys_element) + + +@pytest.mark.parametrize("element, degree", [ + *((finat.Argyris, k) for k in range(5, 8)), + *((finat.HsiehCloughTocher, k) for k in range(3, 6)) + ]) +def test_high_order_C1_elements(ref_cell, phys_cell, element, degree): + finat_element = element(ref_cell, degree, avg=True) + phys_element = type(finat_element.fiat_equivalent)(phys_cell, degree) + check_zany_mapping(finat_element, phys_element) + + +def test_argyris_point(ref_cell, phys_cell): + finat_element = finat.Argyris(ref_cell, variant="point") + phys_element = type(finat_element.fiat_equivalent)(phys_cell, variant="point") + check_zany_mapping(finat_element, phys_element) From 2c7aa93f6209d58128c2b1c3632adbdceaea6db6 Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Thu, 11 Jul 2024 18:32:25 +0100 Subject: [PATCH 734/749] sympy2gem: Fix boolean type checking (#130) --- finat/sympy2gem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finat/sympy2gem.py b/finat/sympy2gem.py index ccbfbf7aa..b93e14a6a 100644 --- a/finat/sympy2gem.py +++ b/finat/sympy2gem.py @@ -126,7 +126,7 @@ def sympy2gem_conditional(node, self): expr = None pieces = [] for v, c in node.args: - if isinstance(c, (bool, numpy.bool)) and c: + if isinstance(c, (bool, numpy.bool, sympy.logic.boolalg.BooleanTrue)) and c: expr = self(v) break pieces.append((v, c)) From 0b60daad2ee291c72c2c06b710019c2e9bd0897c Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Wed, 17 Jul 2024 20:10:48 +0100 Subject: [PATCH 735/749] HCT: basis transformation for hierarchical edge functions (#131) --- finat/argyris.py | 40 ++++++++++++++++++++++++++-------------- finat/hct.py | 7 ++++--- test/test_morley.py | 25 ------------------------- 3 files changed, 30 insertions(+), 42 deletions(-) delete mode 100644 test/test_morley.py diff --git a/finat/argyris.py b/finat/argyris.py index adba5eac6..3186ad7ee 100644 --- a/finat/argyris.py +++ b/finat/argyris.py @@ -9,18 +9,28 @@ from finat.physically_mapped import PhysicallyMappedElement, Citations -def _edge_transform(V, voffset, fiat_cell, moment_deg, coordinate_mapping, avg=False): - """Basis transformation for integral edge moments.""" - - J = coordinate_mapping.jacobian_at([1/3, 1/3]) +def _edge_transform(V, vorder, eorder, fiat_cell, coordinate_mapping, avg=False): + """Basis transformation for integral edge moments. + + :arg V: the transpose of the basis transformation. + :arg vorder: the jet order at vertices, matching the Jacobi weights in the + normal derivative moments on edges. + :arg eorder: the order of the normal derivative moments. + :arg fiat_cell: the reference triangle. + :arg coordinate_mapping: the coordinate mapping. + :kwarg avg: are we scaling integrals by dividing by the edge length? + """ + sd = fiat_cell.get_spatial_dimension() + J = coordinate_mapping.jacobian_at(fiat_cell.make_points(sd, 0, sd+1)[0]) rns = coordinate_mapping.reference_normals() pts = coordinate_mapping.physical_tangents() pns = coordinate_mapping.physical_normals() pel = coordinate_mapping.physical_edge_lengths() + # number of DOFs per vertex/edge + voffset = comb(sd + vorder, vorder) + eoffset = 2 * eorder + 1 top = fiat_cell.get_topology() - eoffset = 2 * moment_deg - 1 - toffset = moment_deg - 1 for e in sorted(top[1]): nhat = partial_indexed(rns, (e, )) n = partial_indexed(pns, (e, )) @@ -33,15 +43,16 @@ def _edge_transform(V, voffset, fiat_cell, moment_deg, coordinate_mapping, avg=F v0id, v1id = (v * voffset for v in top[1][e]) s0 = len(top[0]) * voffset + e * eoffset - for k in range(moment_deg): + for k in range(eorder+1): s = s0 + k - P1 = Literal(comb(k + 2, k)) + # Jacobi polynomial at the endpoints + P1 = comb(k + vorder, k) P0 = -(-1)**k * P1 V[s, s] = Bnn V[s, v1id] = P1 * Bnt V[s, v0id] = P0 * Bnt if k > 0: - V[s, s + toffset] = -1 * Bnt + V[s, s + eorder] = -1 * Bnt class Argyris(PhysicallyMappedElement, ScalarFiatElement): @@ -68,7 +79,8 @@ def basis_transformation(self, coordinate_mapping): sd = self.cell.get_spatial_dimension() top = self.cell.get_topology() - voffset = (sd+1)*sd//2 + sd + 1 + vorder = 2 + voffset = comb(sd + vorder, vorder) for v in sorted(top[0]): s = voffset * v for i in range(sd): @@ -84,9 +96,9 @@ def basis_transformation(self, coordinate_mapping): V[s+5, s+4] = 2*J[0, 1]*J[1, 1] V[s+5, s+5] = J[1, 1]*J[1, 1] - q = self.degree - 4 + eorder = self.degree - 5 if self.variant == "integral": - _edge_transform(V, voffset, self.cell, q, coordinate_mapping, avg=self.avg) + _edge_transform(V, vorder, eorder, self.cell, coordinate_mapping, avg=self.avg) else: rns = coordinate_mapping.reference_normals() pns = coordinate_mapping.physical_normals() @@ -128,10 +140,10 @@ def basis_transformation(self, coordinate_mapping): V[:, voffset*v+3+k] *= 1 / (h[v]*h[v]) if self.variant == "point": - eoffset = 2 * q - 1 + eoffset = 2 * eorder + 1 for e in sorted(top[1]): v0, v1 = top[1][e] s = len(top[0]) * voffset + e * eoffset - V[:, s:s+q] *= 2 / (h[v0] + h[v1]) + V[:, s:s+eorder+1] *= 2 / (h[v0] + h[v1]) return ListTensor(V.T) diff --git a/finat/hct.py b/finat/hct.py index 5ba95ff00..3e1cb693f 100644 --- a/finat/hct.py +++ b/finat/hct.py @@ -30,15 +30,16 @@ def basis_transformation(self, coordinate_mapping): sd = self.cell.get_dimension() top = self.cell.get_topology() - voffset = sd + 1 + voffset = 1 + sd for v in sorted(top[0]): s = voffset * v for i in range(sd): for j in range(sd): V[s+1+i, s+1+j] = J[j, i] - q = self.degree - 2 - _edge_transform(V, voffset, self.cell, q, coordinate_mapping, avg=self.avg) + vorder = 1 + eorder = self.degree - 3 + _edge_transform(V, vorder, eorder, self.cell, coordinate_mapping, avg=self.avg) # Patch up conditioning h = coordinate_mapping.cell_size() diff --git a/test/test_morley.py b/test/test_morley.py deleted file mode 100644 index 3d9ceff94..000000000 --- a/test/test_morley.py +++ /dev/null @@ -1,25 +0,0 @@ -import FIAT -import finat -import numpy as np -from gem.interpreter import evaluate -from fiat_mapping import MyMapping - - -def test_morley(): - ref_cell = FIAT.ufc_simplex(2) - ref_element = finat.Morley(ref_cell, 2) - ref_pts = finat.point_set.PointSet(ref_cell.make_points(2, 0, 4)) - - phys_cell = FIAT.ufc_simplex(2) - phys_cell.vertices = ((0.0, 0.1), (1.17, -0.09), (0.15, 1.84)) - - mapping = MyMapping(ref_cell, phys_cell) - z = (0, 0) - finat_vals_gem = ref_element.basis_evaluation(0, ref_pts, coordinate_mapping=mapping)[z] - finat_vals = evaluate([finat_vals_gem])[0].arr - - phys_cell_FIAT = FIAT.Morley(phys_cell) - phys_points = phys_cell.make_points(2, 0, 4) - phys_vals = phys_cell_FIAT.tabulate(0, phys_points)[z] - - assert np.allclose(finat_vals, phys_vals.T) From 660bda0301a7691d4a3020ad305b3c8d8eb25edc Mon Sep 17 00:00:00 2001 From: Robert Kirby Date: Tue, 23 Jul 2024 03:58:05 -0500 Subject: [PATCH 736/749] Add QuadraticPowellSabin{6|12} (#132) Co-authored-by: Pablo Brubeck --- finat/__init__.py | 1 + finat/physically_mapped.py | 16 ++++++- finat/powell_sabin.py | 77 ++++++++++++++++++++++++++++++++++ finat/ufl/elementlist.py | 2 + test/test_mass_conditioning.py | 20 +++++---- test/test_zany_mapping.py | 21 +++++----- 6 files changed, 117 insertions(+), 20 deletions(-) create mode 100644 finat/powell_sabin.py diff --git a/finat/__init__.py b/finat/__init__.py index 0f14b89be..d103061a9 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -16,6 +16,7 @@ from .aw import ArnoldWintherNC # noqa: F401 from .bell import Bell # noqa: F401 from .hct import HsiehCloughTocher, ReducedHsiehCloughTocher # noqa: F401 +from .powell_sabin import QuadraticPowellSabin6, QuadraticPowellSabin12 # noqa: F401 from .hermite import Hermite # noqa: F401 from .johnson_mercier import JohnsonMercier # noqa: F401 from .mtw import MardalTaiWinther # noqa: F401 diff --git a/finat/physically_mapped.py b/finat/physically_mapped.py index 716f7044e..2edb132ad 100644 --- a/finat/physically_mapped.py +++ b/finat/physically_mapped.py @@ -1,6 +1,7 @@ -import gem from abc import ABCMeta, abstractmethod +import gem + try: from firedrake_citations import Citations Citations().add("Kirby2018zany", """ @@ -156,6 +157,19 @@ publisher={Elsevier} } """) + Citations().add("PowellSabin1977", """ +@article{powell1977piecewise, + title={Piecewise quadratic approximations on triangles}, + author={Powell, Michael JD and Sabin, Malcolm A}, + journal={ACM Transactions on Mathematical Software}, + volume={3}, + number={4}, + pages={316--325}, + year={1977}, + publisher={ACM New York, NY, USA} +} +""") + except ImportError: Citations = None diff --git a/finat/powell_sabin.py b/finat/powell_sabin.py new file mode 100644 index 000000000..08e2a9082 --- /dev/null +++ b/finat/powell_sabin.py @@ -0,0 +1,77 @@ +import FIAT +import numpy +from gem import ListTensor, Literal + +from finat.argyris import _edge_transform +from finat.fiat_elements import ScalarFiatElement +from finat.physically_mapped import Citations, PhysicallyMappedElement + + +class QuadraticPowellSabin6(PhysicallyMappedElement, ScalarFiatElement): + def __init__(self, cell, degree=2): + if degree != 2: + raise ValueError("Degree must be 2 for PS6") + if Citations is not None: + Citations().register("PowellSabin1977") + super().__init__(FIAT.QuadraticPowellSabin6(cell)) + + def basis_transformation(self, coordinate_mapping): + Js = [coordinate_mapping.jacobian_at(vertex) + for vertex in self.cell.get_vertices()] + + h = coordinate_mapping.cell_size() + + d = self.cell.get_dimension() + numbf = self.space_dimension() + + M = numpy.eye(numbf, dtype=object) + + for multiindex in numpy.ndindex(M.shape): + M[multiindex] = Literal(M[multiindex]) + + cur = 0 + for i in range(d+1): + cur += 1 # skip the vertex + J = Js[i] + for j in range(d): + for k in range(d): + M[cur+j, cur+k] = J[j, k] / h[i] + cur += d + + return ListTensor(M) + + +class QuadraticPowellSabin12(PhysicallyMappedElement, ScalarFiatElement): + def __init__(self, cell, degree=2, avg=False): + if degree != 2: + raise ValueError("Degree must be 2 for PS12") + self.avg = avg + if Citations is not None: + Citations().register("PowellSabin1977") + super().__init__(FIAT.QuadraticPowellSabin12(cell)) + + def basis_transformation(self, coordinate_mapping): + J = coordinate_mapping.jacobian_at([1/3, 1/3]) + + ndof = self.space_dimension() + V = numpy.eye(ndof, dtype=object) + for multiindex in numpy.ndindex(V.shape): + V[multiindex] = Literal(V[multiindex]) + + sd = self.cell.get_dimension() + top = self.cell.get_topology() + voffset = sd + 1 + for v in sorted(top[0]): + s = voffset * v + for i in range(sd): + for j in range(sd): + V[s+1+i, s+1+j] = J[j, i] + + _edge_transform(V, 1, 0, self.cell, coordinate_mapping, avg=self.avg) + + # Patch up conditioning + h = coordinate_mapping.cell_size() + for v in sorted(top[0]): + for k in range(sd): + V[:, voffset*v+1+k] /= h[v] + return ListTensor(V.T) diff --git a/finat/ufl/elementlist.py b/finat/ufl/elementlist.py index fcb02be90..3c5445aa1 100644 --- a/finat/ufl/elementlist.py +++ b/finat/ufl/elementlist.py @@ -135,6 +135,8 @@ def show_elements(): register_element("FacetBubble", "FB", 0, H1, "identity", (2, None), simplices) register_element("Quadrature", "Quadrature", 0, L2, "identity", (0, None), any_cell) +register_element("QuadraticPowellSabin6", "PS6", 0, H2, "custom", (2, 2), ("triangle",)) +register_element("QuadraticPowellSabin12", "PS12", 0, H2, "custom", (2, 2), ("triangle",)) register_element("Real", "R", 0, HInf, "identity", (0, 0), any_cell + ("TensorProductCell",)) register_element("Undefined", "U", 0, L2, "identity", (0, None), any_cell) diff --git a/test/test_mass_conditioning.py b/test/test_mass_conditioning.py index 996f093d7..cc4eafcaa 100644 --- a/test/test_mass_conditioning.py +++ b/test/test_mass_conditioning.py @@ -8,15 +8,17 @@ @pytest.mark.parametrize("element, degree, variant", [ - (finat.Hermite, 3, None), - (finat.ReducedHsiehCloughTocher, 3, None), - (finat.HsiehCloughTocher, 3, None), - (finat.HsiehCloughTocher, 4, None), - (finat.Bell, 5, None), - (finat.Argyris, 5, "point"), - (finat.Argyris, 5, None), - (finat.Argyris, 6, None), - ]) + (finat.Hermite, 3, None), + (finat.QuadraticPowellSabin6, 2, None), + (finat.QuadraticPowellSabin12, 2, None), + (finat.ReducedHsiehCloughTocher, 3, None), + (finat.HsiehCloughTocher, 3, None), + (finat.HsiehCloughTocher, 4, None), + (finat.Bell, 5, None), + (finat.Argyris, 5, "point"), + (finat.Argyris, 5, None), + (finat.Argyris, 6, None), +]) def test_mass_scaling(element, degree, variant): sd = 2 ref_cell = FIAT.ufc_simplex(sd) diff --git a/test/test_zany_mapping.py b/test/test_zany_mapping.py index f7db7f769..5e3108866 100644 --- a/test/test_zany_mapping.py +++ b/test/test_zany_mapping.py @@ -57,15 +57,11 @@ def check_zany_mapping(finat_element, phys_element): phi = phys_vals.reshape(numbfs, -1) Mh = np.linalg.solve(Phi @ Phi.T, Phi @ phi.T).T Mh[abs(Mh) < 1E-10] = 0 - # edofs = finat_element.entity_dofs() - # i = len(edofs[2][0]) - # offset = len(edofs[1][0]) + i - # print() - # print(Mh.T[-offset:-i]) - # print(M.T[-offset:-i]) Mgem = finat_element.basis_transformation(mapping) M = evaluate([Mgem])[0].arr + if not np.allclose(M, Mh): + print(Mh-M) assert np.allclose(M, Mh, atol=1E-9) assert np.allclose(finat_vals, phys_vals[:numdofs]) @@ -73,22 +69,27 @@ def check_zany_mapping(finat_element, phys_element): @pytest.mark.parametrize("element", [ finat.Morley, + finat.QuadraticPowellSabin6, + finat.QuadraticPowellSabin12, finat.Hermite, finat.ReducedHsiehCloughTocher, finat.Bell]) def test_C1_elements(ref_cell, phys_cell, element): kwargs = {} + finat_kwargs = {} if element == finat.ReducedHsiehCloughTocher: kwargs = dict(reduced=True) - finat_element = element(ref_cell) + if element == finat.QuadraticPowellSabin12: + finat_kwargs = dict(avg=True) + finat_element = element(ref_cell, **finat_kwargs) phys_element = type(finat_element.fiat_equivalent)(phys_cell, **kwargs) check_zany_mapping(finat_element, phys_element) @pytest.mark.parametrize("element, degree", [ - *((finat.Argyris, k) for k in range(5, 8)), - *((finat.HsiehCloughTocher, k) for k in range(3, 6)) - ]) + *((finat.Argyris, k) for k in range(5, 8)), + *((finat.HsiehCloughTocher, k) for k in range(3, 6)) +]) def test_high_order_C1_elements(ref_cell, phys_cell, element, degree): finat_element = element(ref_cell, degree, avg=True) phys_element = type(finat_element.fiat_equivalent)(phys_cell, degree) From 58e4c5cde060b0a9d38931ee255ee7a235aa5824 Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Tue, 30 Jul 2024 19:12:08 +0100 Subject: [PATCH 737/749] RestrictedElement: fixes for prisms (#133) --- finat/restricted.py | 25 ++++------------ test/test_restriction.py | 65 +++++++++++++++++++++++----------------- 2 files changed, 43 insertions(+), 47 deletions(-) diff --git a/finat/restricted.py b/finat/restricted.py index 92f294b51..cbd56b38f 100644 --- a/finat/restricted.py +++ b/finat/restricted.py @@ -32,7 +32,8 @@ def restrict(element, domain, take_closure): @restrict.register(FiatElement) def restrict_fiat(element, domain, take_closure): try: - return FiatElement(FIAT.RestrictedElement(element._element, restriction_domain=domain)) + return FiatElement(FIAT.RestrictedElement(element._element, + restriction_domain=domain, take_closure=take_closure)) except ValueError: return null_element @@ -52,6 +53,8 @@ def restrict_flattened_dimensions(element, domain, take_closure): @restrict.register(finat.DiscontinuousElement) +@restrict.register(finat.DiscontinuousLagrange) +@restrict.register(finat.Legendre) def restrict_discontinuous(element, domain, take_closure): if domain == "interior": return element @@ -113,22 +116,6 @@ def restrict_mixed(element, domain, take_closure): raise AssertionError("Was expecting this to be handled inside EnrichedElement restriction") -@restrict.register(finat.GaussLobattoLegendre) -def restrict_gll(element, domain, take_closure): - try: - return FiatElement(FIAT.RestrictedElement(element._element, restriction_domain=domain)) - except ValueError: - return null_element - - -@restrict.register(finat.GaussLegendre) -def restrict_gl(element, domain, take_closure): - if domain == "interior": - return element - else: - return null_element - - def r_to_codim(restriction, dim): if restriction == "interior": return 0 @@ -178,7 +165,6 @@ def restrict_tpe(element, domain, take_closure): # R(I, 0)⊗R(I, 1) ⊕ R(I, 1)⊗R(I, 0) ⊕ R(I, 0)⊗R(I, 0) factors = element.factors dimension = element.cell.get_spatial_dimension() - # Figure out which codim entity we're selecting codim = r_to_codim(domain, dimension) # And the range of codims. @@ -192,13 +178,14 @@ def restrict_tpe(element, domain, take_closure): for c in range(codim, upper))) if all(d <= factor.cell.get_dimension() for d, factor in zip(candidate, factors))) + take_closure = False elements = [] for decomposition in restrictions: # Recurse, but don't take closure in recursion (since we # handled it already). new_factors = tuple( restrict(factor, codim_to_r(codim, factor.cell.get_dimension()), - take_closure=False) + take_closure) for factor, codim in zip(factors, decomposition)) # If one of the factors was empty then the whole TPE is empty, # so skip. diff --git a/test/test_restriction.py b/test/test_restriction.py index bd224fdc5..443292a04 100644 --- a/test/test_restriction.py +++ b/test/test_restriction.py @@ -34,68 +34,77 @@ def restriction(request): return request.param -@pytest.fixture(params=["tet", "quad"], scope="module") +@pytest.fixture(params=["tet", "quad", "prism"], scope="module") def cell(request): - return request.param + if request.param == "tet": + cell = (FIAT.ufc_simplex(3),) + elif request.param == "quad": + interval = FIAT.ufc_simplex(1) + cell = (interval, interval) + elif request.param == "prism": + triangle = FIAT.ufc_simplex(2) + interval = FIAT.ufc_simplex(1) + cell = (triangle, interval) + return cell @pytest.fixture def ps(cell): - if cell == "tet": - return PointSet([[1/3, 1/4, 1/5]]) - elif cell == "quad": - return PointSet([[1/3, 1/4]]) + dim = sum(e.get_spatial_dimension() for e in cell) + return PointSet([[1/3, 1/4, 1/5][:dim]]) @pytest.fixture(scope="module") def scalar_element(cell): - if cell == "tet": - return finat.Lagrange(FIAT.reference_element.UFCTetrahedron(), 4) - elif cell == "quad": - interval = FIAT.reference_element.UFCInterval() + if len(cell) == 1: + return finat.Lagrange(cell[0], 4) + else: + e1, e2 = cell return finat.FlattenedDimensions( finat.TensorProductElement([ - finat.GaussLobattoLegendre(interval, 3), - finat.GaussLobattoLegendre(interval, 3)] + finat.GaussLobattoLegendre(e1, 3), + finat.GaussLobattoLegendre(e2, 3)] ) ) @pytest.fixture(scope="module") def hdiv_element(cell): - if cell == "tet": - return finat.RaviartThomas(FIAT.reference_element.UFCTetrahedron(), 3, variant="integral(3)") - elif cell == "quad": - interval = FIAT.reference_element.UFCInterval() + if len(cell) == 1: + return finat.RaviartThomas(cell[0], 3, variant="integral(3)") + else: + e1, e2 = cell + element = finat.GaussLobattoLegendre if e1.get_spatial_dimension() == 1 else finat.RaviartThomas return finat.FlattenedDimensions( finat.EnrichedElement([ finat.HDivElement( finat.TensorProductElement([ - finat.GaussLobattoLegendre(interval, 3), - finat.GaussLegendre(interval, 3)])), + element(e1, 3), + finat.GaussLegendre(e2, 3)])), finat.HDivElement( finat.TensorProductElement([ - finat.GaussLegendre(interval, 3), - finat.GaussLobattoLegendre(interval, 3)])) + finat.GaussLegendre(e1, 3), + finat.GaussLobattoLegendre(e2, 3)])) ])) @pytest.fixture(scope="module") def hcurl_element(cell): - if cell == "tet": - return finat.Nedelec(FIAT.reference_element.UFCTetrahedron(), 3, variant="integral(3)") - elif cell == "quad": - interval = FIAT.reference_element.UFCInterval() + if len(cell) == 1: + return finat.Nedelec(cell[0], 3, variant="integral(3)") + else: + e1, e2 = cell + element = finat.GaussLegendre if e1.get_spatial_dimension() == 1 else finat.Nedelec return finat.FlattenedDimensions( finat.EnrichedElement([ finat.HCurlElement( finat.TensorProductElement([ - finat.GaussLobattoLegendre(interval, 3), - finat.GaussLegendre(interval, 3)])), + finat.GaussLobattoLegendre(e1, 3), + finat.GaussLegendre(e2, 3)])), finat.HCurlElement( finat.TensorProductElement([ - finat.GaussLegendre(interval, 3), - finat.GaussLobattoLegendre(interval, 3)])) + element(e1, 3), + finat.GaussLobattoLegendre(e2, 3)])) ])) From 1990cb611ca978e0bbb111d3ec02e03837f7efd5 Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Fri, 16 Aug 2024 15:20:20 +0100 Subject: [PATCH 738/749] finat.ufl.RestrictedElement: support restriction_domain="reduced" (#135) --- finat/ufl/finiteelementbase.py | 3 ++- finat/ufl/restrictedelement.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/finat/ufl/finiteelementbase.py b/finat/ufl/finiteelementbase.py index 08c500316..2505e2899 100644 --- a/finat/ufl/finiteelementbase.py +++ b/finat/ufl/finiteelementbase.py @@ -234,7 +234,8 @@ def __mul__(self, other): def __getitem__(self, index): """Restrict finite element to a subdomain, subcomponent or topology (cell).""" - if index in ("facet", "interior"): + from finat.ufl.restrictedelement import valid_restriction_domains + if index in valid_restriction_domains: from finat.ufl import RestrictedElement return RestrictedElement(self, index) else: diff --git a/finat/ufl/restrictedelement.py b/finat/ufl/restrictedelement.py index a5426fbb1..2c302ab55 100644 --- a/finat/ufl/restrictedelement.py +++ b/finat/ufl/restrictedelement.py @@ -14,7 +14,7 @@ from finat.ufl.finiteelementbase import FiniteElementBase from ufl.sobolevspace import L2 -valid_restriction_domains = ("interior", "facet", "face", "edge", "vertex") +valid_restriction_domains = ("interior", "facet", "face", "edge", "vertex", "reduced") class RestrictedElement(FiniteElementBase): From 673f7437a7194f8b2e209e750708fb260ec9890b Mon Sep 17 00:00:00 2001 From: Jack Betteridge <43041811+JDBetteridge@users.noreply.github.com> Date: Wed, 4 Sep 2024 16:25:17 +0100 Subject: [PATCH 739/749] Use md5 to hash FiniteElementBase (#134) --- finat/ufl/finiteelementbase.py | 3 +- test/test_hash.py | 53 ++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 test/test_hash.py diff --git a/finat/ufl/finiteelementbase.py b/finat/ufl/finiteelementbase.py index 2505e2899..97ea88a6f 100644 --- a/finat/ufl/finiteelementbase.py +++ b/finat/ufl/finiteelementbase.py @@ -12,6 +12,7 @@ # Modified by Matthew Scroggs, 2023 from abc import abstractmethod, abstractproperty +from hashlib import md5 from ufl import pullback from ufl.cell import AbstractCell, as_cell @@ -84,7 +85,7 @@ def _ufl_signature_data_(self): def __hash__(self): """Compute hash code for insertion in hashmaps.""" - return hash(self._ufl_hash_data_()) + return int.from_bytes(md5(self._ufl_hash_data_().encode()).digest(), byteorder='big') def __eq__(self, other): """Compute element equality for insertion in hashmaps.""" diff --git a/test/test_hash.py b/test/test_hash.py new file mode 100644 index 000000000..d74f0ecd4 --- /dev/null +++ b/test/test_hash.py @@ -0,0 +1,53 @@ +import os +import sys +import subprocess +import textwrap + +import ufl +import finat.ufl + + +def test_same_hash(): + """ The same element created twice should have the same hash. + """ + cg = finat.ufl.finiteelement.FiniteElement("Lagrange", ufl.cell.Cell("triangle"), 1) + same_cg = finat.ufl.finiteelement.FiniteElement("Lagrange", ufl.cell.Cell("triangle"), 1) + assert hash(cg) == hash(same_cg) + + +def test_different_hash(): + """ Two different elements should have different hashes. + """ + cg = finat.ufl.finiteelement.FiniteElement("Lagrange", ufl.cell.Cell("triangle"), 1) + dg = finat.ufl.finiteelement.FiniteElement("DG", ufl.cell.Cell("triangle"), 2) + assert hash(cg) != hash(dg) + + +def test_variant_hashes_different(): + """ Different variants of the same element should have different hashes. + """ + dg = finat.ufl.finiteelement.FiniteElement("DG", ufl.cell.Cell("triangle"), 2) + dg_gll = finat.ufl.finiteelement.FiniteElement("DG", ufl.cell.Cell("triangle"), 2, variant="gll") + assert hash(dg) != hash(dg_gll) + + +def test_persistent_hash(tmp_path): + """ Hashes should be the same across Python invocations. + """ + filename = "print_hash.py" + code = textwrap.dedent("""\ + import ufl + import finat.ufl + + dg = finat.ufl.finiteelement.FiniteElement("RT", ufl.cell.Cell("triangle"), 1) + print(hash(dg)) + """) + filepath = tmp_path.joinpath(filename) + with open(filepath, "w") as fh: + fh.write(code) + + output1 = subprocess.run([sys.executable, filepath], capture_output=True) + assert output1.returncode == os.EX_OK + output2 = subprocess.run([sys.executable, filepath], capture_output=True) + assert output2.returncode == os.EX_OK + assert output1.stdout == output2.stdout From 014f2468f45388d06cfadbfe63c5c608c26483ae Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Wed, 4 Sep 2024 16:35:08 +0100 Subject: [PATCH 740/749] sympy2gem: if-then-else (#137) --- finat/sympy2gem.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/finat/sympy2gem.py b/finat/sympy2gem.py index b93e14a6a..29add8760 100644 --- a/finat/sympy2gem.py +++ b/finat/sympy2gem.py @@ -42,6 +42,13 @@ def sympy2gem_pow(node, self): return gem.Power(*map(self, node.args)) +@sympy2gem.register(sympy.logic.boolalg.BooleanTrue) +@sympy2gem.register(sympy.logic.boolalg.BooleanFalse) +@sympy2gem.register(bool) +def sympy2gem_boolean(node, self): + return gem.Literal(bool(node)) + + @sympy2gem.register(sympy.Integer) @sympy2gem.register(symengine.Integer) @sympy2gem.register(int) @@ -135,3 +142,8 @@ def sympy2gem_conditional(node, self): for v, c in reversed(pieces): expr = gem.Conditional(self(c), self(v), expr) return expr + + +@sympy2gem.register(sympy.ITE) +def sympy2gem_ifthenelse(node, self): + return gem.Conditional(*map(self, node.args)) From 2c5b6624a562e2744cf97d81f88d473b613c63bb Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Tue, 8 Oct 2024 14:31:19 +0100 Subject: [PATCH 741/749] Add physically mapped Stokes elements (#139) --- finat/__init__.py | 5 ++ finat/alfeld_sorokina.py | 40 +++++++++ finat/arnold_qin.py | 19 +++++ finat/aw.py | 91 ++++++--------------- finat/bell.py | 2 - finat/bernardi_raugel.py | 19 +++++ finat/christiansen_hu.py | 11 +++ finat/cube.py | 2 +- finat/discontinuous.py | 2 +- finat/fiat_elements.py | 46 +++++------ finat/guzman_neilan.py | 19 +++++ finat/hct.py | 4 - finat/hdivcurl.py | 6 +- finat/hermite.py | 2 - finat/johnson_mercier.py | 6 +- finat/mixed.py | 2 +- finat/morley.py | 2 - finat/mtw.py | 2 +- finat/physically_mapped.py | 58 ++++++++++++- finat/piola_mapped.py | 154 +++++++++++++++++++++++++++++++++++ finat/point_set.py | 4 +- finat/powell_sabin.py | 4 - finat/spectral.py | 20 ++--- finat/trace.py | 2 +- finat/ufl/elementlist.py | 21 +++-- finat/ufl/finiteelement.py | 2 +- test/test_aw.py | 83 ------------------- test/test_johnson_mercier.py | 78 ------------------ test/test_zany_mapping.py | 152 +++++++++++++++++++++++++++++++--- 29 files changed, 551 insertions(+), 307 deletions(-) create mode 100644 finat/alfeld_sorokina.py create mode 100644 finat/arnold_qin.py create mode 100644 finat/bernardi_raugel.py create mode 100644 finat/christiansen_hu.py create mode 100644 finat/guzman_neilan.py create mode 100644 finat/piola_mapped.py delete mode 100644 test/test_aw.py delete mode 100644 test/test_johnson_mercier.py diff --git a/finat/__init__.py b/finat/__init__.py index d103061a9..1919010fe 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -15,7 +15,12 @@ from .aw import ArnoldWinther # noqa: F401 from .aw import ArnoldWintherNC # noqa: F401 from .bell import Bell # noqa: F401 +from .bernardi_raugel import BernardiRaugel, BernardiRaugelBubble # noqa: F401 from .hct import HsiehCloughTocher, ReducedHsiehCloughTocher # noqa: F401 +from .arnold_qin import ArnoldQin, ReducedArnoldQin # noqa: F401 +from .christiansen_hu import ChristiansenHu # noqa: F401 +from .alfeld_sorokina import AlfeldSorokina # noqa: F401 +from .guzman_neilan import GuzmanNeilan, GuzmanNeilanBubble # noqa: F401 from .powell_sabin import QuadraticPowellSabin6, QuadraticPowellSabin12 # noqa: F401 from .hermite import Hermite # noqa: F401 from .johnson_mercier import JohnsonMercier # noqa: F401 diff --git a/finat/alfeld_sorokina.py b/finat/alfeld_sorokina.py new file mode 100644 index 000000000..94a102b6e --- /dev/null +++ b/finat/alfeld_sorokina.py @@ -0,0 +1,40 @@ +import FIAT +import numpy +from gem import ListTensor, Literal + +from finat.fiat_elements import FiatElement +from finat.physically_mapped import Citations, PhysicallyMappedElement +from finat.piola_mapped import piola_inverse + + +class AlfeldSorokina(PhysicallyMappedElement, FiatElement): + def __init__(self, cell, degree=2): + if Citations is not None: + Citations().register("AlfeldSorokina2016") + super().__init__(FIAT.AlfeldSorokina(cell, degree)) + + def basis_transformation(self, coordinate_mapping): + sd = self.cell.get_spatial_dimension() + bary, = self.cell.make_points(sd, 0, sd+1) + J = coordinate_mapping.jacobian_at(bary) + detJ = coordinate_mapping.detJ_at(bary) + + ndof = self.space_dimension() + V = numpy.eye(ndof, dtype=object) + for multiindex in numpy.ndindex(V.shape): + V[multiindex] = Literal(V[multiindex]) + + Finv = piola_inverse(self.cell, J, detJ) + edofs = self.entity_dofs() + for dim in edofs: + for entity in sorted(edofs[dim]): + dofs = edofs[dim][entity] + if dim == 0: + s = dofs[0] + V[s, s] = detJ + dofs = dofs[1:] + for i in range(0, len(dofs), sd): + s = dofs[i:i+sd] + V[numpy.ix_(s, s)] = Finv + + return ListTensor(V.T) diff --git a/finat/arnold_qin.py b/finat/arnold_qin.py new file mode 100644 index 000000000..2d84eea1a --- /dev/null +++ b/finat/arnold_qin.py @@ -0,0 +1,19 @@ +import FIAT + +from finat.physically_mapped import Citations +from finat.fiat_elements import FiatElement +from finat.piola_mapped import PiolaBubbleElement + + +class ArnoldQin(FiatElement): + def __init__(self, cell, degree=2): + if Citations is not None: + Citations().register("ArnoldQin1992") + super().__init__(FIAT.ArnoldQin(cell, degree)) + + +class ReducedArnoldQin(PiolaBubbleElement): + def __init__(self, cell, degree=2): + if Citations is not None: + Citations().register("ArnoldQin1992") + super().__init__(FIAT.ArnoldQin(cell, degree, reduced=True)) diff --git a/finat/aw.py b/finat/aw.py index 63cf159ee..34d482f41 100644 --- a/finat/aw.py +++ b/finat/aw.py @@ -1,10 +1,11 @@ """Implementation of the Arnold-Winther finite elements.""" import FIAT import numpy -from gem import ListTensor, Literal, partial_indexed +from gem import ListTensor, Literal from finat.fiat_elements import FiatElement from finat.physically_mapped import Citations, PhysicallyMappedElement +from finat.piola_mapped import normal_tangential_edge_transform, normal_tangential_face_transform def _facet_transform(fiat_cell, facet_moment_degree, coordinate_mapping): @@ -16,72 +17,30 @@ def _facet_transform(fiat_cell, facet_moment_degree, coordinate_mapping): dofs_per_facet = sd * dimPk_facet ndofs = num_facets * dofs_per_facet - Vsub = numpy.eye(ndofs, dtype=object) - for multiindex in numpy.ndindex(Vsub.shape): - Vsub[multiindex] = Literal(Vsub[multiindex]) + V = numpy.eye(ndofs, dtype=object) + for multiindex in numpy.ndindex(V.shape): + V[multiindex] = Literal(V[multiindex]) - bary = [1/(sd+1)] * sd - detJ = coordinate_mapping.detJ_at(bary) + bary, = fiat_cell.make_points(sd, 0, sd+1) J = coordinate_mapping.jacobian_at(bary) - rns = coordinate_mapping.reference_normals() - offset = dofs_per_facet + detJ = coordinate_mapping.detJ_at(bary) if sd == 2: - R = Literal(numpy.array([[0, -1], [1, 0]])) - - for e in range(num_facets): - nhat = partial_indexed(rns, (e, )) - that = R @ nhat - Jn = J @ nhat - Jt = J @ that - - # Compute alpha and beta for the edge. - alpha = (Jn @ Jt) / detJ - beta = (Jt @ Jt) / detJ - # Stuff into the right rows and columns. - for i in range(dimPk_facet): - idx = offset*e + i * dimPk_facet + 1 - Vsub[idx, idx-1] = Literal(-1) * alpha / beta - Vsub[idx, idx] = Literal(1) / beta + transform = normal_tangential_edge_transform elif sd == 3: - for f in range(num_facets): - nhat = fiat_cell.compute_normal(f) - nhat /= numpy.linalg.norm(nhat) - ehats = fiat_cell.compute_tangents(sd-1, f) - rels = [numpy.linalg.norm(ehat) for ehat in ehats] - thats = [a / b for a, b in zip(ehats, rels)] - vf = fiat_cell.volume_of_subcomplex(sd-1, f) - - scale = 1.0 / numpy.dot(thats[1], numpy.cross(thats[0], nhat)) - orth_vecs = [scale * numpy.cross(nhat, thats[1]), - scale * numpy.cross(thats[0], nhat)] - - Jn = J @ Literal(nhat) - Jts = [J @ Literal(that) for that in thats] - Jorth = [J @ Literal(ov) for ov in orth_vecs] - - alphas = [(Jn @ Jts[i] / detJ) * (Literal(rels[i]) / Literal(2*vf)) for i in range(sd-1)] - betas = [Jorth[0] @ Jts[i] / detJ for i in range(sd-1)] - gammas = [Jorth[1] @ Jts[i] / detJ for i in range(sd-1)] + transform = normal_tangential_face_transform - det = betas[0] * gammas[1] - betas[1] * gammas[0] + for f in range(num_facets): + rows = transform(fiat_cell, J, detJ, f) + for i in range(dimPk_facet): + s = dofs_per_facet*f + i * sd + V[s+1:s+sd, s:s+sd] = rows + return V - for i in range(dimPk_facet): - idx = offset*f + i * sd - Vsub[idx+1, idx] = (alphas[1] * gammas[0] - - alphas[0] * gammas[1]) / det - Vsub[idx+1, idx+1] = gammas[1] / det - Vsub[idx+1, idx+2] = Literal(-1) * gammas[0] / det - Vsub[idx+2, idx] = (alphas[0] * betas[1] - - alphas[1] * betas[0]) / det - Vsub[idx+2, idx+1] = Literal(-1) * betas[1] / det - Vsub[idx+2, idx+2] = betas[0] / det - - return Vsub - - -def _evaluation_transform(coordinate_mapping): - J = coordinate_mapping.jacobian_at([1/3, 1/3]) +def _evaluation_transform(fiat_cell, coordinate_mapping): + sd = fiat_cell.get_spatial_dimension() + bary, = fiat_cell.make_points(sd, 0, sd+1) + J = coordinate_mapping.jacobian_at(bary) W = numpy.zeros((3, 3), dtype=object) W[0, 0] = J[1, 1]*J[1, 1] @@ -98,10 +57,10 @@ def _evaluation_transform(coordinate_mapping): class ArnoldWintherNC(PhysicallyMappedElement, FiatElement): - def __init__(self, cell, degree): + def __init__(self, cell, degree=2): if Citations is not None: Citations().register("Arnold2003") - super(ArnoldWintherNC, self).__init__(FIAT.ArnoldWintherNC(cell, degree)) + super().__init__(FIAT.ArnoldWintherNC(cell, degree)) def basis_transformation(self, coordinate_mapping): """Note, the extra 3 dofs which are removed here @@ -113,7 +72,7 @@ def basis_transformation(self, coordinate_mapping): V[:12, :12] = _facet_transform(self.cell, 1, coordinate_mapping) # internal dofs - W = _evaluation_transform(coordinate_mapping) + W = _evaluation_transform(self.cell, coordinate_mapping) detJ = coordinate_mapping.detJ_at([1/3, 1/3]) V[12:15, 12:15] = W / detJ @@ -147,10 +106,10 @@ def space_dimension(self): class ArnoldWinther(PhysicallyMappedElement, FiatElement): - def __init__(self, cell, degree): + def __init__(self, cell, degree=3): if Citations is not None: Citations().register("Arnold2002") - super(ArnoldWinther, self).__init__(FIAT.ArnoldWinther(cell, degree)) + super().__init__(FIAT.ArnoldWinther(cell, degree)) def basis_transformation(self, coordinate_mapping): """The extra 6 dofs removed here correspond to the constraints.""" @@ -159,7 +118,7 @@ def basis_transformation(self, coordinate_mapping): for multiindex in numpy.ndindex(V.shape): V[multiindex] = Literal(V[multiindex]) - W = _evaluation_transform(coordinate_mapping) + W = _evaluation_transform(self.cell, coordinate_mapping) # Put into the right rows and columns. V[0:3, 0:3] = V[3:6, 3:6] = V[6:9, 6:9] = W diff --git a/finat/bell.py b/finat/bell.py index caab5c001..6dbd564c2 100644 --- a/finat/bell.py +++ b/finat/bell.py @@ -10,8 +10,6 @@ class Bell(PhysicallyMappedElement, ScalarFiatElement): def __init__(self, cell, degree=5): - if degree != 5: - raise ValueError("Degree must be 5 for Bell element") if Citations is not None: Citations().register("Bell1969") super().__init__(FIAT.Bell(cell)) diff --git a/finat/bernardi_raugel.py b/finat/bernardi_raugel.py new file mode 100644 index 000000000..6295e9533 --- /dev/null +++ b/finat/bernardi_raugel.py @@ -0,0 +1,19 @@ +import FIAT + +from finat.physically_mapped import Citations +from finat.piola_mapped import PiolaBubbleElement + + +class BernardiRaugel(PiolaBubbleElement): + def __init__(self, cell, degree=None, subdegree=1): + sd = cell.get_spatial_dimension() + if degree is None: + degree = sd + if Citations is not None: + Citations().register("BernardiRaugel1985") + super().__init__(FIAT.BernardiRaugel(cell, degree, subdegree=subdegree)) + + +class BernardiRaugelBubble(BernardiRaugel): + def __init__(self, cell, degree=None): + super().__init__(cell, degree=degree, subdegree=0) diff --git a/finat/christiansen_hu.py b/finat/christiansen_hu.py new file mode 100644 index 000000000..abdfb99f6 --- /dev/null +++ b/finat/christiansen_hu.py @@ -0,0 +1,11 @@ +import FIAT + +from finat.physically_mapped import Citations +from finat.piola_mapped import PiolaBubbleElement + + +class ChristiansenHu(PiolaBubbleElement): + def __init__(self, cell, degree=1): + if Citations is not None: + Citations().register("ChristiansenHu2019") + super().__init__(FIAT.ChristiansenHu(cell, degree)) diff --git a/finat/cube.py b/finat/cube.py index 4d7e3687e..39aeccd04 100644 --- a/finat/cube.py +++ b/finat/cube.py @@ -15,7 +15,7 @@ class FlattenedDimensions(FiniteElementBase): dimensions.""" def __init__(self, element): - super(FlattenedDimensions, self).__init__() + super().__init__() self.product = element self._unflatten = compute_unflattening_map(element.cell.get_topology()) diff --git a/finat/discontinuous.py b/finat/discontinuous.py index faff0a471..f5d15ae01 100644 --- a/finat/discontinuous.py +++ b/finat/discontinuous.py @@ -9,7 +9,7 @@ class DiscontinuousElement(FiniteElementBase): """Element wrapper that makes a FInAT element discontinuous.""" def __init__(self, element): - super(DiscontinuousElement, self).__init__() + super().__init__() self.element = element @property diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index ddd117f51..f8af03858 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -44,7 +44,7 @@ class FiatElement(FiniteElementBase): """Base class for finite elements for which the tabulation is provided by FIAT.""" def __init__(self, fiat_element): - super(FiatElement, self).__init__() + super().__init__() self._element = fiat_element @property @@ -315,12 +315,12 @@ def point_evaluation(fiat_element, order, refcoords, entity): class Regge(FiatElement): # naturally tensor valued def __init__(self, cell, degree): - super(Regge, self).__init__(FIAT.Regge(cell, degree)) + super().__init__(FIAT.Regge(cell, degree)) class HellanHerrmannJohnson(FiatElement): # symmetric matrix valued def __init__(self, cell, degree): - super(HellanHerrmannJohnson, self).__init__(FIAT.HellanHerrmannJohnson(cell, degree)) + super().__init__(FIAT.HellanHerrmannJohnson(cell, degree)) class ScalarFiatElement(FiatElement): @@ -337,27 +337,27 @@ def __init__(self, cell, degree): class Bubble(ScalarFiatElement): def __init__(self, cell, degree): - super(Bubble, self).__init__(FIAT.Bubble(cell, degree)) + super().__init__(FIAT.Bubble(cell, degree)) class FacetBubble(ScalarFiatElement): def __init__(self, cell, degree): - super(FacetBubble, self).__init__(FIAT.FacetBubble(cell, degree)) + super().__init__(FIAT.FacetBubble(cell, degree)) class CrouzeixRaviart(ScalarFiatElement): def __init__(self, cell, degree): - super(CrouzeixRaviart, self).__init__(FIAT.CrouzeixRaviart(cell, degree)) + super().__init__(FIAT.CrouzeixRaviart(cell, degree)) class Lagrange(ScalarFiatElement): def __init__(self, cell, degree, variant=None): - super(Lagrange, self).__init__(FIAT.Lagrange(cell, degree, variant=variant)) + super().__init__(FIAT.Lagrange(cell, degree, variant=variant)) class KongMulderVeldhuizen(ScalarFiatElement): def __init__(self, cell, degree): - super(KongMulderVeldhuizen, self).__init__(FIAT.KongMulderVeldhuizen(cell, degree)) + super().__init__(FIAT.KongMulderVeldhuizen(cell, degree)) if Citations is not None: Citations().register("Chin1999higher") Citations().register("Geevers2018new") @@ -365,7 +365,7 @@ def __init__(self, cell, degree): class DiscontinuousLagrange(ScalarFiatElement): def __init__(self, cell, degree, variant=None): - super(DiscontinuousLagrange, self).__init__(FIAT.DiscontinuousLagrange(cell, degree, variant=variant)) + super().__init__(FIAT.DiscontinuousLagrange(cell, degree, variant=variant)) class Real(DiscontinuousLagrange): @@ -374,17 +374,17 @@ class Real(DiscontinuousLagrange): class Serendipity(ScalarFiatElement): def __init__(self, cell, degree): - super(Serendipity, self).__init__(FIAT.Serendipity(cell, degree)) + super().__init__(FIAT.Serendipity(cell, degree)) class DPC(ScalarFiatElement): def __init__(self, cell, degree): - super(DPC, self).__init__(FIAT.DPC(cell, degree)) + super().__init__(FIAT.DPC(cell, degree)) class DiscontinuousTaylor(ScalarFiatElement): def __init__(self, cell, degree): - super(DiscontinuousTaylor, self).__init__(FIAT.DiscontinuousTaylor(cell, degree)) + super().__init__(FIAT.DiscontinuousTaylor(cell, degree)) class VectorFiatElement(FiatElement): @@ -395,12 +395,12 @@ def value_shape(self): class RaviartThomas(VectorFiatElement): def __init__(self, cell, degree, variant=None): - super(RaviartThomas, self).__init__(FIAT.RaviartThomas(cell, degree, variant=variant)) + super().__init__(FIAT.RaviartThomas(cell, degree, variant=variant)) class TrimmedSerendipityFace(VectorFiatElement): def __init__(self, cell, degree): - super(TrimmedSerendipityFace, self).__init__(FIAT.TrimmedSerendipityFace(cell, degree)) + super().__init__(FIAT.TrimmedSerendipityFace(cell, degree)) @property def entity_permutations(self): @@ -409,7 +409,7 @@ def entity_permutations(self): class TrimmedSerendipityDiv(VectorFiatElement): def __init__(self, cell, degree): - super(TrimmedSerendipityDiv, self).__init__(FIAT.TrimmedSerendipityDiv(cell, degree)) + super().__init__(FIAT.TrimmedSerendipityDiv(cell, degree)) @property def entity_permutations(self): @@ -418,7 +418,7 @@ def entity_permutations(self): class TrimmedSerendipityEdge(VectorFiatElement): def __init__(self, cell, degree): - super(TrimmedSerendipityEdge, self).__init__(FIAT.TrimmedSerendipityEdge(cell, degree)) + super().__init__(FIAT.TrimmedSerendipityEdge(cell, degree)) @property def entity_permutations(self): @@ -427,7 +427,7 @@ def entity_permutations(self): class TrimmedSerendipityCurl(VectorFiatElement): def __init__(self, cell, degree): - super(TrimmedSerendipityCurl, self).__init__(FIAT.TrimmedSerendipityCurl(cell, degree)) + super().__init__(FIAT.TrimmedSerendipityCurl(cell, degree)) @property def entity_permutations(self): @@ -436,12 +436,12 @@ def entity_permutations(self): class BrezziDouglasMarini(VectorFiatElement): def __init__(self, cell, degree, variant=None): - super(BrezziDouglasMarini, self).__init__(FIAT.BrezziDouglasMarini(cell, degree, variant=variant)) + super().__init__(FIAT.BrezziDouglasMarini(cell, degree, variant=variant)) class BrezziDouglasMariniCubeEdge(VectorFiatElement): def __init__(self, cell, degree): - super(BrezziDouglasMariniCubeEdge, self).__init__(FIAT.BrezziDouglasMariniCubeEdge(cell, degree)) + super().__init__(FIAT.BrezziDouglasMariniCubeEdge(cell, degree)) @property def entity_permutations(self): @@ -450,7 +450,7 @@ def entity_permutations(self): class BrezziDouglasMariniCubeFace(VectorFiatElement): def __init__(self, cell, degree): - super(BrezziDouglasMariniCubeFace, self).__init__(FIAT.BrezziDouglasMariniCubeFace(cell, degree)) + super().__init__(FIAT.BrezziDouglasMariniCubeFace(cell, degree)) @property def entity_permutations(self): @@ -459,14 +459,14 @@ def entity_permutations(self): class BrezziDouglasFortinMarini(VectorFiatElement): def __init__(self, cell, degree): - super(BrezziDouglasFortinMarini, self).__init__(FIAT.BrezziDouglasFortinMarini(cell, degree)) + super().__init__(FIAT.BrezziDouglasFortinMarini(cell, degree)) class Nedelec(VectorFiatElement): def __init__(self, cell, degree, variant=None): - super(Nedelec, self).__init__(FIAT.Nedelec(cell, degree, variant=variant)) + super().__init__(FIAT.Nedelec(cell, degree, variant=variant)) class NedelecSecondKind(VectorFiatElement): def __init__(self, cell, degree, variant=None): - super(NedelecSecondKind, self).__init__(FIAT.NedelecSecondKind(cell, degree, variant=variant)) + super().__init__(FIAT.NedelecSecondKind(cell, degree, variant=variant)) diff --git a/finat/guzman_neilan.py b/finat/guzman_neilan.py new file mode 100644 index 000000000..c48b1bb2f --- /dev/null +++ b/finat/guzman_neilan.py @@ -0,0 +1,19 @@ +import FIAT + +from finat.physically_mapped import Citations +from finat.piola_mapped import PiolaBubbleElement + + +class GuzmanNeilan(PiolaBubbleElement): + def __init__(self, cell, degree=None, subdegree=1): + sd = cell.get_spatial_dimension() + if degree is None: + degree = sd + if Citations is not None: + Citations().register("GuzmanNeilan2019") + super().__init__(FIAT.GuzmanNeilan(cell, degree, subdegree=subdegree)) + + +class GuzmanNeilanBubble(GuzmanNeilan): + def __init__(self, cell, degree=None): + super().__init__(cell, degree=degree, subdegree=0) diff --git a/finat/hct.py b/finat/hct.py index 3e1cb693f..c3f66b270 100644 --- a/finat/hct.py +++ b/finat/hct.py @@ -10,8 +10,6 @@ class HsiehCloughTocher(PhysicallyMappedElement, ScalarFiatElement): def __init__(self, cell, degree=3, avg=False): - if degree < 3: - raise ValueError("HCT only defined for degree >= 3") if Citations is not None: Citations().register("Clough1965") if degree > 3: @@ -51,8 +49,6 @@ def basis_transformation(self, coordinate_mapping): class ReducedHsiehCloughTocher(PhysicallyMappedElement, ScalarFiatElement): def __init__(self, cell, degree=3): - if degree != 3: - raise ValueError("Degree must be 3 for reduced HCT element") if Citations is not None: Citations().register("Clough1965") super().__init__(FIAT.HsiehCloughTocher(cell, reduced=True)) diff --git a/finat/hdivcurl.py b/finat/hdivcurl.py index b9ed8fc70..a75c3f1fa 100644 --- a/finat/hdivcurl.py +++ b/finat/hdivcurl.py @@ -11,7 +11,7 @@ class WrapperElementBase(FiniteElementBase): """Common base class for H(div) and H(curl) element wrappers.""" def __init__(self, wrappee, transform): - super(WrapperElementBase, self).__init__() + super().__init__() self.wrappee = wrappee """An appropriate tensor product FInAT element whose basis functions are mapped to produce an H(div) or H(curl) @@ -105,7 +105,7 @@ def __init__(self, wrappee): raise ValueError("H(div) requires (n-1)-form element!") transform = select_hdiv_transformer(wrappee) - super(HDivElement, self).__init__(wrappee, transform) + super().__init__(wrappee, transform) @property def formdegree(self): @@ -133,7 +133,7 @@ def __init__(self, wrappee): raise ValueError("H(curl) requires 1-form element!") transform = select_hcurl_transformer(wrappee) - super(HCurlElement, self).__init__(wrappee, transform) + super().__init__(wrappee, transform) @property def formdegree(self): diff --git a/finat/hermite.py b/finat/hermite.py index 6049d92e8..31a557125 100644 --- a/finat/hermite.py +++ b/finat/hermite.py @@ -9,8 +9,6 @@ class Hermite(PhysicallyMappedElement, ScalarFiatElement): def __init__(self, cell, degree=3): - if degree != 3: - raise ValueError("Degree must be 3 for Hermite element") if Citations is not None: Citations().register("Ciarlet1972") super().__init__(FIAT.CubicHermite(cell)) diff --git a/finat/johnson_mercier.py b/finat/johnson_mercier.py index b5da38785..285ae4f66 100644 --- a/finat/johnson_mercier.py +++ b/finat/johnson_mercier.py @@ -8,13 +8,11 @@ class JohnsonMercier(PhysicallyMappedElement, FiatElement): # symmetric matrix valued - def __init__(self, cell, degree, variant=None): - if degree != 1: - raise ValueError("Degree must be 1 for Johnson-Mercier element") + def __init__(self, cell, degree=1, variant=None): if Citations is not None: Citations().register("Gopalakrishnan2024") self._indices = slice(None, None) - super(JohnsonMercier, self).__init__(FIAT.JohnsonMercier(cell, degree, variant=variant)) + super().__init__(FIAT.JohnsonMercier(cell, degree, variant=variant)) def basis_transformation(self, coordinate_mapping): numbf = self._element.space_dimension() diff --git a/finat/mixed.py b/finat/mixed.py index 2da4b6270..e8b371c3b 100644 --- a/finat/mixed.py +++ b/finat/mixed.py @@ -28,7 +28,7 @@ def __init__(self, element, size, offset): assert 0 <= offset <= size assert offset + numpy.prod(element.value_shape, dtype=int) <= size - super(MixedSubElement, self).__init__() + super().__init__() self.element = element self.size = size self.offset = offset diff --git a/finat/morley.py b/finat/morley.py index 3d3c7f8bf..6b9038f85 100644 --- a/finat/morley.py +++ b/finat/morley.py @@ -10,8 +10,6 @@ class Morley(PhysicallyMappedElement, ScalarFiatElement): def __init__(self, cell, degree=2): - if degree != 2: - raise ValueError("Degree must be 2 for Morley element") if Citations is not None: Citations().register("Morley1971") super().__init__(FIAT.Morley(cell)) diff --git a/finat/mtw.py b/finat/mtw.py index b0f7ed04f..1de6133eb 100644 --- a/finat/mtw.py +++ b/finat/mtw.py @@ -9,7 +9,7 @@ class MardalTaiWinther(PhysicallyMappedElement, FiatElement): - def __init__(self, cell, degree): + def __init__(self, cell, degree=3): if Citations is not None: Citations().register("Mardal2002") super(MardalTaiWinther, self).__init__(FIAT.MardalTaiWinther(cell, degree)) diff --git a/finat/physically_mapped.py b/finat/physically_mapped.py index 2edb132ad..a7d846d76 100644 --- a/finat/physically_mapped.py +++ b/finat/physically_mapped.py @@ -36,7 +36,7 @@ Citations().add("Clough1965", """ @inproceedings{Clough1965, author = {R. W. Clough, J. L. Tocher}, - title = {Finite element stiffness matricess for analysis of plate bending}, + title = {Finite element stiffness matrices for analysis of plate bending}, booktitle = {Proc. of the First Conf. on Matrix Methods in Struct. Mech}, year = 1965, pages = {515-546}, @@ -169,6 +169,62 @@ publisher={ACM New York, NY, USA} } """) + Citations().add("AlfeldSorokina2016", """ +@article{alfeld2016linear, + title={Linear differential operators on bivariate spline spaces and spline vector fields}, + author={Alfeld, Peter and Sorokina, Tatyana}, + journal={BIT Numerical Mathematics}, + volume={56}, + number={1}, + pages={15--32}, + year={2016}, + publisher={Springer} +} +""") + Citations().add("ArnoldQin1992", """ +@article{arnold1992quadratic, + title={{Quadratic velocity/linear pressure Stokes elements}}, + author={Arnold, Douglas N and Qin, Jinshui}, + journal={Advances in computer methods for partial differential equations}, + volume={7}, + pages={28--34}, + year={1992} +} +""") + Citations().add("ChristiansenHu2019", """ +@article{christiansen2019finite, + title={A finite element for Stokes with a commuting diagram }, + author={Christiansen, Snorre H and Hu, Kaibo}, + journal={Mathematical Analysis in Fluid and Gas Dynamics}, + volume={2107}, + pages={172--183}, + year={2019} +} +""") + Citations().add("GuzmanNeilan2019", """ +@article{guzman2019infsup, + author = {Guzm\'{a}n, Johnny and Neilan, Michael}, + title = {{Inf-Sup Stable Finite Elements on Barycentric Refinements Producing Divergence--Free Approximations in Arbitrary Dimensions}}, + journal = {SIAM Journal on Numerical Analysis}, + volume = {56}, + number = {5}, + pages = {2826-2844}, + year = {2018}, + doi = {10.1137/17M1153467} +} +""") + Citations().add("BernardiRaugel1985", """ +@article{bernardi-raugel-0, + AUTHOR = {Bernardi, Christine and Raugel, Genevi\\`eve}, + TITLE = {Analysis of some finite elements for the {Stokes} problem}, + JOURNAL = {Mathematics of Computation}, + VOLUME = {44}, + YEAR = {1985}, + DOI = {10.1090/S0025-5718-1985-0771031-7}, + PAGES = {{71--79}} +} +""") + except ImportError: Citations = None diff --git a/finat/piola_mapped.py b/finat/piola_mapped.py new file mode 100644 index 000000000..aea4dcab1 --- /dev/null +++ b/finat/piola_mapped.py @@ -0,0 +1,154 @@ +import numpy + +from finat.fiat_elements import FiatElement +from finat.physically_mapped import PhysicallyMappedElement +from gem import Literal, ListTensor +from copy import deepcopy + + +def determinant(A): + """Return the determinant of A""" + n = A.shape[0] + if n == 0: + return 1 + elif n == 1: + return A[0, 0] + elif n == 2: + return A[0, 0] * A[1, 1] - A[0, 1] * A[1, 0] + else: + detA = A[0, 0] * determinant(A[1:, 1:]) + cols = numpy.ones(A.shape[1], dtype=bool) + for j in range(1, n): + cols[j] = False + detA += (-1)**j * A[0, j] * determinant(A[1:][:, cols]) + cols[j] = True + return detA + + +def adjugate(A): + """Return the adjugate matrix of A""" + A = numpy.asarray(A) + C = numpy.zeros_like(A) + rows = numpy.ones(A.shape[0], dtype=bool) + cols = numpy.ones(A.shape[1], dtype=bool) + for i in range(A.shape[0]): + rows[i] = False + for j in range(A.shape[1]): + cols[j] = False + C[j, i] = (-1)**(i+j)*determinant(A[rows, :][:, cols]) + cols[j] = True + rows[i] = True + return C + + +def piola_inverse(fiat_cell, J, detJ): + """Return the basis transformation of evaluation at a point. + This simply inverts the Piola transform inv(J / detJ) = adj(J).""" + sd = fiat_cell.get_spatial_dimension() + Jnp = numpy.array([[J[i, j] for j in range(sd)] for i in range(sd)]) + return adjugate(Jnp) + + +def normal_tangential_edge_transform(fiat_cell, J, detJ, f): + """Return the basis transformation of + normal and tangential edge moments""" + R = numpy.array([[0, 1], [-1, 0]]) + that = fiat_cell.compute_edge_tangent(f) + that /= numpy.linalg.norm(that) + nhat = R @ that + Jn = J @ Literal(nhat) + Jt = J @ Literal(that) + alpha = Jn @ Jt + beta = Jt @ Jt + # Compute the last row of inv([[1, 0], [alpha/detJ, beta/detJ]]) + row = (-1 * alpha / beta, detJ / beta) + return row + + +def normal_tangential_face_transform(fiat_cell, J, detJ, f): + """Return the basis transformation of + normal and tangential face moments""" + # Compute the reciprocal basis + thats = fiat_cell.compute_tangents(2, f) + nhat = numpy.cross(*thats) + nhat /= numpy.dot(nhat, nhat) + orth_vecs = numpy.array([nhat, + numpy.cross(nhat, thats[1]), + numpy.cross(thats[0], nhat)]) + # Compute A = (alpha, beta, gamma) + Jts = J @ Literal(thats.T) + Jorths = J @ Literal(orth_vecs.T) + A = Jorths.T @ Jts + # Compute the last two rows of inv([[1, 0, 0], A.T/detJ]) + det0 = A[1, 0] * A[2, 1] - A[1, 1] * A[2, 0] + det1 = A[2, 0] * A[0, 1] - A[2, 1] * A[0, 0] + det2 = A[0, 0] * A[1, 1] - A[0, 1] * A[1, 0] + scale = detJ / det0 + rows = ((-1 * det1 / det0, -1 * scale * A[2, 1], scale * A[2, 0]), + (-1 * det2 / det0, scale * A[1, 1], -1 * scale * A[1, 0])) + return rows + + +class PiolaBubbleElement(PhysicallyMappedElement, FiatElement): + """A general class to transform Piola-mapped elements with normal facet bubbles.""" + def __init__(self, fiat_element): + mapping, = set(fiat_element.mapping()) + if mapping != "contravariant piola": + raise ValueError(f"{type(fiat_element).__name__} needs to be Piola mapped.") + super().__init__(fiat_element) + + # On each facet we expect the normal dof followed by the tangential ones + # The tangential dofs should be numbered last, and are constrained to be zero + sd = self.cell.get_spatial_dimension() + reduced_dofs = deepcopy(self._element.entity_dofs()) + cur = reduced_dofs[sd-1][0][0] + for entity in reduced_dofs[sd-1]: + reduced_dofs[sd-1][entity] = [cur] + cur += 1 + self._entity_dofs = reduced_dofs + self._space_dimension = cur + + def entity_dofs(self): + return self._entity_dofs + + @property + def index_shape(self): + return (self._space_dimension,) + + def space_dimension(self): + return self._space_dimension + + def basis_transformation(self, coordinate_mapping): + sd = self.cell.get_spatial_dimension() + bary, = self.cell.make_points(sd, 0, sd+1) + J = coordinate_mapping.jacobian_at(bary) + detJ = coordinate_mapping.detJ_at(bary) + + dofs = self.entity_dofs() + edofs = self._element.entity_dofs() + ndof = self.space_dimension() + numbf = self._element.space_dimension() + V = numpy.eye(numbf, ndof, dtype=object) + for multiindex in numpy.ndindex(V.shape): + V[multiindex] = Literal(V[multiindex]) + + # Undo the Piola transform for non-facet bubble basis functions + Finv = piola_inverse(self.cell, J, detJ) + for dim in range(sd-1): + for e in sorted(dofs[dim]): + for k in range(0, len(dofs[dim][e]), sd): + cur = dofs[dim][e][k:k+sd] + V[numpy.ix_(cur, cur)] = Finv + + # Unpick the normal component for the facet bubbles + if sd == 2: + transform = normal_tangential_edge_transform + elif sd == 3: + transform = normal_tangential_face_transform + + for f in sorted(dofs[sd-1]): + rows = numpy.asarray(transform(self.cell, J, detJ, f)) + fdofs = dofs[sd-1][f] + bfs = edofs[sd-1][f][1:] + V[numpy.ix_(bfs, fdofs)] = rows[..., :len(fdofs)] + return ListTensor(V.T) diff --git a/finat/point_set.py b/finat/point_set.py index 43db02207..63ab8dc84 100644 --- a/finat/point_set.py +++ b/finat/point_set.py @@ -158,7 +158,7 @@ class GaussLegendrePointSet(PointSet): This facilitates implementing discontinuous spectral elements. """ def __init__(self, points): - super(GaussLegendrePointSet, self).__init__(points) + super().__init__(points) assert self.points.shape[1] == 1 @@ -168,7 +168,7 @@ class GaussLobattoLegendrePointSet(PointSet): This facilitates implementing continuous spectral elements. """ def __init__(self, points): - super(GaussLobattoLegendrePointSet, self).__init__(points) + super().__init__(points) assert self.points.shape[1] == 1 diff --git a/finat/powell_sabin.py b/finat/powell_sabin.py index 08e2a9082..0c0dfc0b7 100644 --- a/finat/powell_sabin.py +++ b/finat/powell_sabin.py @@ -9,8 +9,6 @@ class QuadraticPowellSabin6(PhysicallyMappedElement, ScalarFiatElement): def __init__(self, cell, degree=2): - if degree != 2: - raise ValueError("Degree must be 2 for PS6") if Citations is not None: Citations().register("PowellSabin1977") super().__init__(FIAT.QuadraticPowellSabin6(cell)) @@ -43,8 +41,6 @@ def basis_transformation(self, coordinate_mapping): class QuadraticPowellSabin12(PhysicallyMappedElement, ScalarFiatElement): def __init__(self, cell, degree=2, avg=False): - if degree != 2: - raise ValueError("Degree must be 2 for PS12") self.avg = avg if Citations is not None: Citations().register("PowellSabin1977") diff --git a/finat/spectral.py b/finat/spectral.py index c5641ecd0..3b4d46323 100644 --- a/finat/spectral.py +++ b/finat/spectral.py @@ -22,7 +22,7 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): :param entity: the cell entity on which to tabulate. ''' - result = super(GaussLobattoLegendre, self).basis_evaluation(order, ps, entity) + result = super().basis_evaluation(order, ps, entity) cell_dimension = self.cell.get_dimension() if entity is None or entity == (cell_dimension, 0): # on cell interior space_dim = self.space_dimension() @@ -51,7 +51,7 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): :param entity: the cell entity on which to tabulate. ''' - result = super(GaussLegendre, self).basis_evaluation(order, ps, entity) + result = super().basis_evaluation(order, ps, entity) cell_dimension = self.cell.get_dimension() if entity is None or entity == (cell_dimension, 0): # on cell interior space_dim = self.space_dimension() @@ -69,7 +69,7 @@ class Legendre(ScalarFiatElement): def __init__(self, cell, degree, variant=None): fiat_element = FIAT.Legendre(cell, degree, variant=variant) - super(Legendre, self).__init__(fiat_element) + super().__init__(fiat_element) class IntegratedLegendre(ScalarFiatElement): @@ -77,7 +77,7 @@ class IntegratedLegendre(ScalarFiatElement): def __init__(self, cell, degree, variant=None): fiat_element = FIAT.IntegratedLegendre(cell, degree, variant=variant) - super(IntegratedLegendre, self).__init__(fiat_element) + super().__init__(fiat_element) class FDMLagrange(ScalarFiatElement): @@ -85,7 +85,7 @@ class FDMLagrange(ScalarFiatElement): def __init__(self, cell, degree): fiat_element = FIAT.FDMLagrange(cell, degree) - super(FDMLagrange, self).__init__(fiat_element) + super().__init__(fiat_element) class FDMDiscontinuousLagrange(ScalarFiatElement): @@ -93,7 +93,7 @@ class FDMDiscontinuousLagrange(ScalarFiatElement): def __init__(self, cell, degree): fiat_element = FIAT.FDMDiscontinuousLagrange(cell, degree) - super(FDMDiscontinuousLagrange, self).__init__(fiat_element) + super().__init__(fiat_element) class FDMQuadrature(ScalarFiatElement): @@ -101,7 +101,7 @@ class FDMQuadrature(ScalarFiatElement): def __init__(self, cell, degree): fiat_element = FIAT.FDMQuadrature(cell, degree) - super(FDMQuadrature, self).__init__(fiat_element) + super().__init__(fiat_element) class FDMBrokenH1(ScalarFiatElement): @@ -109,7 +109,7 @@ class FDMBrokenH1(ScalarFiatElement): def __init__(self, cell, degree): fiat_element = FIAT.FDMBrokenH1(cell, degree) - super(FDMBrokenH1, self).__init__(fiat_element) + super().__init__(fiat_element) class FDMBrokenL2(ScalarFiatElement): @@ -117,7 +117,7 @@ class FDMBrokenL2(ScalarFiatElement): def __init__(self, cell, degree): fiat_element = FIAT.FDMBrokenL2(cell, degree) - super(FDMBrokenL2, self).__init__(fiat_element) + super().__init__(fiat_element) class FDMHermite(ScalarFiatElement): @@ -125,4 +125,4 @@ class FDMHermite(ScalarFiatElement): def __init__(self, cell, degree): fiat_element = FIAT.FDMHermite(cell, degree) - super(FDMHermite, self).__init__(fiat_element) + super().__init__(fiat_element) diff --git a/finat/trace.py b/finat/trace.py index 9b621c81e..d5e784281 100644 --- a/finat/trace.py +++ b/finat/trace.py @@ -5,4 +5,4 @@ class HDivTrace(ScalarFiatElement): def __init__(self, cell, degree): - super(HDivTrace, self).__init__(FIAT.HDivTrace(cell, degree)) + super().__init__(FIAT.HDivTrace(cell, degree)) diff --git a/finat/ufl/elementlist.py b/finat/ufl/elementlist.py index 3c5445aa1..1ba31fc27 100644 --- a/finat/ufl/elementlist.py +++ b/finat/ufl/elementlist.py @@ -111,9 +111,9 @@ def show_elements(): # Elements not in the periodic table register_element("Argyris", "ARG", 0, H2, "custom", (5, None), ("triangle",)) -register_element("Hsieh-Clough-Tocher", "HCT", 0, H2, "custom", (3, None), ("triangle",)) -register_element("Reduced-Hsieh-Clough-Tocher", "HCT-red", 0, H2, "custom", (3, 3), ("triangle",)) register_element("Bell", "BELL", 0, H2, "custom", (5, 5), ("triangle",)) +register_element("Bernardi-Raugel", "BR", 1, H1, "contravariant Piola", (2, 3), simplices[1:]) +register_element("Bernardi-Raugel Bubble", "BRB", 1, H1, "contravariant Piola", (2, 3), simplices[1:]) register_element("Brezzi-Douglas-Fortin-Marini", "BDFM", 1, HDiv, "contravariant Piola", (1, None), simplices[1:]) register_element("Crouzeix-Raviart", "CR", 0, L2, "identity", (1, 1), @@ -128,6 +128,19 @@ def show_elements(): ("triangle",)) register_element("Morley", "MOR", 0, H2, "custom", (2, 2), ("triangle",)) +# Macro elements +register_element("QuadraticPowellSabin6", "PS6", 0, H2, "custom", (2, 2), ("triangle",)) +register_element("QuadraticPowellSabin12", "PS12", 0, H2, "custom", (2, 2), ("triangle",)) +register_element("Hsieh-Clough-Tocher", "HCT", 0, H2, "custom", (3, None), ("triangle",)) +register_element("Reduced-Hsieh-Clough-Tocher", "HCT-red", 0, H2, "custom", (3, 3), ("triangle",)) +register_element("Arnold-Qin", "AQ", 1, H1, "identity", (2, 2), ("triangle",)) +register_element("Reduced-Arnold-Qin", "AQ-red", 1, H1, "contravariant Piola", (2, 2), ("triangle",)) +register_element("Christiansen-Hu", "CH", 1, H1, "contravariant Piola", (1, 1), simplices[1:]) +register_element("Guzman-Neilan", "GN", 1, H1, "contravariant Piola", (2, 3), simplices[1:]) +register_element("Guzman-Neilan Bubble", "GNB", 1, H1, "contravariant Piola", (2, 3), simplices[1:]) +register_element("Alfeld-Sorokina", "AS", 1, H1, "contravariant Piola", (2, 2), simplices[1:]) +register_element("Johnson-Mercier", "JM", 2, HDivDiv, "double contravariant Piola", (1, 1), simplices[1:]) + # Special elements register_element("Boundary Quadrature", "BQ", 0, L2, "identity", (0, None), any_cell) @@ -135,8 +148,6 @@ def show_elements(): register_element("FacetBubble", "FB", 0, H1, "identity", (2, None), simplices) register_element("Quadrature", "Quadrature", 0, L2, "identity", (0, None), any_cell) -register_element("QuadraticPowellSabin6", "PS6", 0, H2, "custom", (2, 2), ("triangle",)) -register_element("QuadraticPowellSabin12", "PS12", 0, H2, "custom", (2, 2), ("triangle",)) register_element("Real", "R", 0, HInf, "identity", (0, 0), any_cell + ("TensorProductCell",)) register_element("Undefined", "U", 0, L2, "identity", (0, None), any_cell) @@ -144,8 +155,6 @@ def show_elements(): register_element("Regge", "Regge", 2, HEin, "double covariant Piola", (0, None), simplices[1:]) register_element("HDiv Trace", "HDivT", 0, L2, "identity", (0, None), any_cell) -register_element("Johnson-Mercier", "JM", 2, HDivDiv, - "double contravariant Piola", (1, 1), simplices[1:]) register_element("Hellan-Herrmann-Johnson", "HHJ", 2, HDivDiv, "double contravariant Piola", (0, None), ("triangle",)) register_element("Nonconforming Arnold-Winther", "AWnc", 2, HDivDiv, diff --git a/finat/ufl/finiteelement.py b/finat/ufl/finiteelement.py index d547b50d4..7027b318f 100644 --- a/finat/ufl/finiteelement.py +++ b/finat/ufl/finiteelement.py @@ -115,7 +115,7 @@ def dq_family_l2(cell): for c in cell.sub_cells()], cell=cell) - return super(FiniteElement, cls).__new__(cls) + return super().__new__(cls) def __init__(self, family, diff --git a/test/test_aw.py b/test/test_aw.py deleted file mode 100644 index 42ec41863..000000000 --- a/test/test_aw.py +++ /dev/null @@ -1,83 +0,0 @@ -import FIAT -import finat -import numpy as np -from gem.interpreter import evaluate -from fiat_mapping import MyMapping - - -def test_awnc(): - ref_cell = FIAT.ufc_simplex(2) - ref_el_finat = finat.ArnoldWintherNC(ref_cell, 2) - ref_element = ref_el_finat._element - ref_pts = ref_cell.make_points(2, 0, 3) - ref_vals = ref_element.tabulate(0, ref_pts)[0, 0] - - phys_cell = FIAT.ufc_simplex(2) - phys_cell.vertices = ((0.0, 0.0), (2.0, 0.1), (0.0, 1.0)) - phys_element = ref_element.__class__(phys_cell, 2) - - phys_pts = phys_cell.make_points(2, 0, 3) - phys_vals = phys_element.tabulate(0, phys_pts)[0, 0] - - # Piola map the reference elements - J, b = FIAT.reference_element.make_affine_mapping(ref_cell.vertices, - phys_cell.vertices) - detJ = np.linalg.det(J) - - ref_vals_piola = np.zeros(ref_vals.shape) - for i in range(ref_vals.shape[0]): - for k in range(ref_vals.shape[3]): - ref_vals_piola[i, :, :, k] = \ - J @ ref_vals[i, :, :, k] @ J.T / detJ**2 - - # Zany map the results - mappng = MyMapping(ref_cell, phys_cell) - Mgem = ref_el_finat.basis_transformation(mappng) - M = evaluate([Mgem])[0].arr - ref_vals_zany = np.zeros((15, 2, 2, len(phys_pts))) - for k in range(ref_vals_zany.shape[3]): - for ell1 in range(2): - for ell2 in range(2): - ref_vals_zany[:, ell1, ell2, k] = \ - M @ ref_vals_piola[:, ell1, ell2, k] - - assert np.allclose(ref_vals_zany[:12], phys_vals[:12]) - - -def test_awc(): - ref_cell = FIAT.ufc_simplex(2) - ref_element = FIAT.ArnoldWinther(ref_cell, 3) - ref_el_finat = finat.ArnoldWinther(ref_cell, 3) - ref_pts = ref_cell.make_points(2, 0, 3) - ref_vals = ref_element.tabulate(0, ref_pts)[0, 0] - - phys_cell = FIAT.ufc_simplex(2) - pvs = np.array(((0.0, 0.0), (1.0, 0.1), (0.0, 2.0))) - phys_cell.vertices = pvs * 0.5 - phys_element = FIAT.ArnoldWinther(phys_cell, 3) - phys_pts = phys_cell.make_points(2, 0, 3) - phys_vals = phys_element.tabulate(0, phys_pts)[0, 0] - - # Piola map the reference elements - J, b = FIAT.reference_element.make_affine_mapping(ref_cell.vertices, - phys_cell.vertices) - detJ = np.linalg.det(J) - - ref_vals_piola = np.zeros(ref_vals.shape) - for i in range(ref_vals.shape[0]): - for k in range(ref_vals.shape[3]): - ref_vals_piola[i, :, :, k] = \ - J @ ref_vals[i, :, :, k] @ J.T / detJ**2 - - # Zany map the results - mppng = MyMapping(ref_cell, phys_cell) - Mgem = ref_el_finat.basis_transformation(mppng) - M = evaluate([Mgem])[0].arr - ref_vals_zany = np.zeros((24, 2, 2, len(phys_pts))) - for k in range(ref_vals_zany.shape[3]): - for ell1 in range(2): - for ell2 in range(2): - ref_vals_zany[:, ell1, ell2, k] = \ - M @ ref_vals_piola[:, ell1, ell2, k] - - assert np.allclose(ref_vals_zany[:21], phys_vals[:21]) diff --git a/test/test_johnson_mercier.py b/test/test_johnson_mercier.py deleted file mode 100644 index aa90e7ec6..000000000 --- a/test/test_johnson_mercier.py +++ /dev/null @@ -1,78 +0,0 @@ -import FIAT -import finat -import numpy as np -import pytest -from gem.interpreter import evaluate - -from fiat_mapping import MyMapping - - -def make_unisolvent_points(element): - degree = element.degree() - ref_complex = element.get_reference_complex() - sd = ref_complex.get_spatial_dimension() - top = ref_complex.get_topology() - pts = [] - for cell in top[sd]: - pts.extend(ref_complex.make_points(sd, cell, degree+sd+1)) - return pts - - -@pytest.mark.parametrize('phys_verts', - [((0.0, 0.0), (2.0, 0.1), (0.0, 1.0)), - ((0, 0, 0), (1., 0.1, -0.37), - (0.01, 0.987, -.23), - (-0.1, -0.2, 1.38))]) -def test_johnson_mercier(phys_verts): - degree = 1 - variant = None - sd = len(phys_verts) - 1 - z = tuple(0 for _ in range(sd)) - ref_cell = FIAT.ufc_simplex(sd) - ref_el_finat = finat.JohnsonMercier(ref_cell, degree, variant=variant) - indices = ref_el_finat._indices - - ref_element = ref_el_finat._element - ref_pts = make_unisolvent_points(ref_element) - ref_vals = ref_element.tabulate(0, ref_pts)[z] - - phys_cell = FIAT.ufc_simplex(sd) - phys_cell.vertices = phys_verts - phys_element = type(ref_element)(phys_cell, degree, variant=variant) - - phys_pts = make_unisolvent_points(phys_element) - phys_vals = phys_element.tabulate(0, phys_pts)[z] - - # Piola map the reference elements - J, b = FIAT.reference_element.make_affine_mapping(ref_cell.vertices, - phys_cell.vertices) - detJ = np.linalg.det(J) - - ref_vals_piola = np.zeros(ref_vals.shape) - for i in range(ref_vals.shape[0]): - for k in range(ref_vals.shape[3]): - ref_vals_piola[i, :, :, k] = \ - J @ ref_vals[i, :, :, k] @ J.T / detJ**2 - - num_dofs = ref_el_finat.space_dimension() - num_bfs = phys_element.space_dimension() - num_facet_bfs = (sd + 1) * len(phys_element.dual.entity_ids[sd-1][0]) - - # Zany map the results - mappng = MyMapping(ref_cell, phys_cell) - Mgem = ref_el_finat.basis_transformation(mappng) - M = evaluate([Mgem])[0].arr - ref_vals_zany = np.zeros((num_dofs, sd, sd, len(phys_pts))) - for k in range(ref_vals_zany.shape[3]): - for ell1 in range(sd): - for ell2 in range(sd): - ref_vals_zany[:, ell1, ell2, k] = \ - M @ ref_vals_piola[:, ell1, ell2, k] - - # Solve for the basis transformation and compare results - Phi = ref_vals_piola.reshape(num_bfs, -1) - phi = phys_vals.reshape(num_bfs, -1) - Mh = np.linalg.solve(Phi @ Phi.T, Phi @ phi.T).T - assert np.allclose(M[:num_facet_bfs], Mh[indices][:num_facet_bfs]) - - assert np.allclose(ref_vals_zany[:num_facet_bfs], phys_vals[indices][:num_facet_bfs]) diff --git a/test/test_zany_mapping.py b/test/test_zany_mapping.py index 5e3108866..f77c58735 100644 --- a/test/test_zany_mapping.py +++ b/test/test_zany_mapping.py @@ -20,19 +20,36 @@ def phys_cell(request): return K -def make_unisolvent_points(element): +def make_unisolvent_points(element, interior=False): degree = element.degree() ref_complex = element.get_reference_complex() top = ref_complex.get_topology() pts = [] - for dim in top: + if interior: + dim = ref_complex.get_spatial_dimension() for entity in top[dim]: - pts.extend(ref_complex.make_points(dim, entity, degree, variant="gll")) + pts.extend(ref_complex.make_points(dim, entity, degree+dim+1, variant="gll")) + else: + for dim in top: + for entity in top[dim]: + pts.extend(ref_complex.make_points(dim, entity, degree, variant="gll")) return pts +def reconstruct_fiat_element(fiat_element, *args, **kwargs): + indices = None + if isinstance(fiat_element, FIAT.RestrictedElement): + indices = fiat_element._indices + fiat_element = fiat_element._element + fe = type(fiat_element)(*args, **kwargs) + if indices is not None: + fe = FIAT.RestrictedElement(fe, indices=indices) + return fe + + def check_zany_mapping(finat_element, phys_element): ref_element = finat_element.fiat_equivalent + shape = ref_element.value_shape() ref_cell = ref_element.get_reference_element() phys_cell = phys_element.get_reference_element() @@ -43,7 +60,7 @@ def check_zany_mapping(finat_element, phys_element): z = (0,) * finat_element.cell.get_spatial_dimension() finat_vals_gem = finat_element.basis_evaluation(0, ps, coordinate_mapping=mapping)[z] - finat_vals = evaluate([finat_vals_gem])[0].arr.T + finat_vals = evaluate([finat_vals_gem])[0].arr.transpose(*range(1, len(shape)+2), 0) phys_points = make_unisolvent_points(phys_element) phys_vals = phys_element.tabulate(0, phys_points)[z] @@ -60,9 +77,7 @@ def check_zany_mapping(finat_element, phys_element): Mgem = finat_element.basis_transformation(mapping) M = evaluate([Mgem])[0].arr - if not np.allclose(M, Mh): - print(Mh-M) - assert np.allclose(M, Mh, atol=1E-9) + assert np.allclose(M, Mh, atol=1E-9), str(Mh.T-M.T) assert np.allclose(finat_vals, phys_vals[:numdofs]) @@ -73,7 +88,8 @@ def check_zany_mapping(finat_element, phys_element): finat.QuadraticPowellSabin12, finat.Hermite, finat.ReducedHsiehCloughTocher, - finat.Bell]) + finat.Bell, + ]) def test_C1_elements(ref_cell, phys_cell, element): kwargs = {} finat_kwargs = {} @@ -82,7 +98,7 @@ def test_C1_elements(ref_cell, phys_cell, element): if element == finat.QuadraticPowellSabin12: finat_kwargs = dict(avg=True) finat_element = element(ref_cell, **finat_kwargs) - phys_element = type(finat_element.fiat_equivalent)(phys_cell, **kwargs) + phys_element = reconstruct_fiat_element(finat_element.fiat_equivalent, phys_cell, **kwargs) check_zany_mapping(finat_element, phys_element) @@ -92,11 +108,125 @@ def test_C1_elements(ref_cell, phys_cell, element): ]) def test_high_order_C1_elements(ref_cell, phys_cell, element, degree): finat_element = element(ref_cell, degree, avg=True) - phys_element = type(finat_element.fiat_equivalent)(phys_cell, degree) + phys_element = reconstruct_fiat_element(finat_element.fiat_equivalent, phys_cell, degree) check_zany_mapping(finat_element, phys_element) def test_argyris_point(ref_cell, phys_cell): finat_element = finat.Argyris(ref_cell, variant="point") - phys_element = type(finat_element.fiat_equivalent)(phys_cell, variant="point") + phys_element = reconstruct_fiat_element(finat_element.fiat_equivalent, phys_cell, variant="point") check_zany_mapping(finat_element, phys_element) + + +def check_zany_piola_mapping(finat_element, phys_element): + ref_element = finat_element._element + ref_cell = ref_element.get_reference_element() + phys_cell = phys_element.get_reference_element() + sd = ref_cell.get_spatial_dimension() + try: + indices = finat_element._indices + except AttributeError: + indices = slice(None) + + shape = ref_element.value_shape() + ref_pts = make_unisolvent_points(ref_element, interior=True) + ref_vals = ref_element.tabulate(0, ref_pts)[(0,)*sd] + + phys_pts = make_unisolvent_points(phys_element, interior=True) + phys_vals = phys_element.tabulate(0, phys_pts)[(0,)*sd] + + # Piola map the reference elements + J, b = FIAT.reference_element.make_affine_mapping(ref_cell.vertices, + phys_cell.vertices) + detJ = np.linalg.det(J) + K = J / detJ + if len(shape) == 2: + piola_map = lambda x: K @ x @ K.T + else: + piola_map = lambda x: K @ x + + ref_vals_piola = np.zeros(ref_vals.shape) + for i in range(ref_vals.shape[0]): + for k in range(ref_vals.shape[-1]): + ref_vals_piola[i, ..., k] = piola_map(ref_vals[i, ..., k]) + + dofs = finat_element.entity_dofs() + num_bfs = phys_element.space_dimension() + num_dofs = finat_element.space_dimension() + num_facet_dofs = num_dofs - sum(len(dofs[sd][entity]) for entity in dofs[sd]) + + # Zany map the results + mappng = MyMapping(ref_cell, phys_cell) + Mgem = finat_element.basis_transformation(mappng) + M = evaluate([Mgem])[0].arr + shp = (num_dofs, *shape, -1) + ref_vals_zany = (M @ ref_vals_piola.reshape(num_bfs, -1)).reshape(shp) + + # Solve for the basis transformation and compare results + Phi = ref_vals_piola.reshape(num_bfs, -1) + phi = phys_vals.reshape(num_bfs, -1) + Mh = np.linalg.solve(Phi @ Phi.T, Phi @ phi.T).T + M = M[:num_facet_dofs] + Mh = Mh[indices][:num_facet_dofs] + Mh[abs(Mh) < 1E-10] = 0.0 + M[abs(M) < 1E-10] = 0.0 + assert np.allclose(M, Mh), str(M.T - Mh.T) + assert np.allclose(ref_vals_zany[:num_facet_dofs], phys_vals[indices][:num_facet_dofs]) + + +@pytest.mark.parametrize("element", [ + finat.MardalTaiWinther, + finat.BernardiRaugel, + finat.BernardiRaugelBubble, + finat.ReducedArnoldQin, + finat.AlfeldSorokina, + finat.ChristiansenHu, + finat.ArnoldWinther, + finat.ArnoldWintherNC, + finat.JohnsonMercier, + finat.GuzmanNeilan, + finat.GuzmanNeilanBubble, + ]) +def test_piola_triangle(ref_cell, phys_cell, element): + finat_element = element(ref_cell) + kwargs = {} + if isinstance(finat_element, (finat.BernardiRaugelBubble, finat.GuzmanNeilanBubble)): + kwargs["subdegree"] = 0 + elif isinstance(finat_element, finat.ReducedArnoldQin): + kwargs["reduced"] = True + phys_element = reconstruct_fiat_element(finat_element.fiat_equivalent, phys_cell, **kwargs) + check_zany_piola_mapping(finat_element, phys_element) + + +@pytest.fixture +def ref_tet(request): + K = FIAT.ufc_simplex(3) + return K + + +@pytest.fixture +def phys_tet(request): + K = FIAT.ufc_simplex(3) + K.vertices = ((0, 0, 0), + (1., 0.1, -0.37), + (0.01, 0.987, -.23), + (-0.1, -0.2, 1.38)) + return K + + +@pytest.mark.parametrize("element", [ + finat.BernardiRaugel, + finat.BernardiRaugelBubble, + finat.ChristiansenHu, + finat.AlfeldSorokina, + finat.JohnsonMercier, + finat.GuzmanNeilan, + finat.GuzmanNeilanBubble, + ]) +def test_piola_tetrahedron(ref_tet, phys_tet, element): + finat_element = element(ref_tet) + kwargs = {} + if isinstance(finat_element, (finat.BernardiRaugelBubble, finat.GuzmanNeilanBubble)): + kwargs["subdegree"] = 0 + phys_element = reconstruct_fiat_element(finat_element.fiat_equivalent, phys_tet, **kwargs) + check_zany_piola_mapping(finat_element, phys_element) From 03819896276b4e7718e52e28042c3764fe8d0efa Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Wed, 16 Oct 2024 16:51:20 +0100 Subject: [PATCH 742/749] Guzman-Neilan H1(div) macroelement (#140) --- finat/__init__.py | 2 +- finat/alfeld_sorokina.py | 26 +++++++++------- finat/bernardi_raugel.py | 9 ++---- finat/guzman_neilan.py | 33 ++++++++++++++------ finat/physically_mapped.py | 4 +-- finat/piola_mapped.py | 32 +++++++++++++------ finat/ufl/elementlist.py | 19 ++++++++---- finat/ufl/enrichedelement.py | 10 ++---- finat/ufl/finiteelement.py | 25 +++++++++------ finat/ufl/mixedelement.py | 10 ++---- test/test_zany_mapping.py | 59 +++++++++++------------------------- 11 files changed, 117 insertions(+), 112 deletions(-) diff --git a/finat/__init__.py b/finat/__init__.py index 1919010fe..e7a993de7 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -20,7 +20,7 @@ from .arnold_qin import ArnoldQin, ReducedArnoldQin # noqa: F401 from .christiansen_hu import ChristiansenHu # noqa: F401 from .alfeld_sorokina import AlfeldSorokina # noqa: F401 -from .guzman_neilan import GuzmanNeilan, GuzmanNeilanBubble # noqa: F401 +from .guzman_neilan import GuzmanNeilanFirstKindH1, GuzmanNeilanSecondKindH1, GuzmanNeilanBubble, GuzmanNeilanH1div # noqa: F401 from .powell_sabin import QuadraticPowellSabin6, QuadraticPowellSabin12 # noqa: F401 from .hermite import Hermite # noqa: F401 from .johnson_mercier import JohnsonMercier # noqa: F401 diff --git a/finat/alfeld_sorokina.py b/finat/alfeld_sorokina.py index 94a102b6e..3cc791136 100644 --- a/finat/alfeld_sorokina.py +++ b/finat/alfeld_sorokina.py @@ -19,22 +19,26 @@ def basis_transformation(self, coordinate_mapping): J = coordinate_mapping.jacobian_at(bary) detJ = coordinate_mapping.detJ_at(bary) + dofs = self.entity_dofs() ndof = self.space_dimension() V = numpy.eye(ndof, dtype=object) for multiindex in numpy.ndindex(V.shape): V[multiindex] = Literal(V[multiindex]) + # Undo the Piola transform + nodes = self._element.get_dual_set().nodes Finv = piola_inverse(self.cell, J, detJ) - edofs = self.entity_dofs() - for dim in edofs: - for entity in sorted(edofs[dim]): - dofs = edofs[dim][entity] - if dim == 0: - s = dofs[0] - V[s, s] = detJ - dofs = dofs[1:] - for i in range(0, len(dofs), sd): - s = dofs[i:i+sd] - V[numpy.ix_(s, s)] = Finv + for dim in sorted(dofs): + for e in sorted(dofs[dim]): + k = 0 + while k < len(dofs[dim][e]): + cur = dofs[dim][e][k] + if len(nodes[cur].deriv_dict) > 0: + V[cur, cur] = detJ + k += 1 + else: + s = dofs[dim][e][k:k+sd] + V[numpy.ix_(s, s)] = Finv + k += sd return ListTensor(V.T) diff --git a/finat/bernardi_raugel.py b/finat/bernardi_raugel.py index 6295e9533..90969d961 100644 --- a/finat/bernardi_raugel.py +++ b/finat/bernardi_raugel.py @@ -5,15 +5,12 @@ class BernardiRaugel(PiolaBubbleElement): - def __init__(self, cell, degree=None, subdegree=1): - sd = cell.get_spatial_dimension() - if degree is None: - degree = sd + def __init__(self, cell, order=1): if Citations is not None: Citations().register("BernardiRaugel1985") - super().__init__(FIAT.BernardiRaugel(cell, degree, subdegree=subdegree)) + super().__init__(FIAT.BernardiRaugel(cell, order=order)) class BernardiRaugelBubble(BernardiRaugel): def __init__(self, cell, degree=None): - super().__init__(cell, degree=degree, subdegree=0) + super().__init__(cell, order=0) diff --git a/finat/guzman_neilan.py b/finat/guzman_neilan.py index c48b1bb2f..09c24c3da 100644 --- a/finat/guzman_neilan.py +++ b/finat/guzman_neilan.py @@ -4,16 +4,31 @@ from finat.piola_mapped import PiolaBubbleElement -class GuzmanNeilan(PiolaBubbleElement): - def __init__(self, cell, degree=None, subdegree=1): - sd = cell.get_spatial_dimension() - if degree is None: - degree = sd +class GuzmanNeilanFirstKindH1(PiolaBubbleElement): + """Pk^d enriched with Guzman-Neilan bubbles.""" + def __init__(self, cell, order=1): if Citations is not None: - Citations().register("GuzmanNeilan2019") - super().__init__(FIAT.GuzmanNeilan(cell, degree, subdegree=subdegree)) + Citations().register("GuzmanNeilan2018") + super().__init__(FIAT.GuzmanNeilanFirstKindH1(cell, order=order)) -class GuzmanNeilanBubble(GuzmanNeilan): +class GuzmanNeilanSecondKindH1(PiolaBubbleElement): + """C0 Pk^d(Alfeld) enriched with Guzman-Neilan bubbles.""" + def __init__(self, cell, order=1): + if Citations is not None: + Citations().register("GuzmanNeilan2018") + super().__init__(FIAT.GuzmanNeilanSecondKindH1(cell, order=order)) + + +class GuzmanNeilanBubble(GuzmanNeilanFirstKindH1): + """Modified Bernardi-Raugel bubbles that are C^0 P_dim(Alfeld) with constant divergence.""" def __init__(self, cell, degree=None): - super().__init__(cell, degree=degree, subdegree=0) + super().__init__(cell, order=0) + + +class GuzmanNeilanH1div(PiolaBubbleElement): + """Alfeld-Sorokina nodally enriched with Guzman-Neilan bubbles.""" + def __init__(self, cell, degree=None): + if Citations is not None: + Citations().register("GuzmanNeilan2018") + super().__init__(FIAT.GuzmanNeilanH1div(cell, degree=degree)) diff --git a/finat/physically_mapped.py b/finat/physically_mapped.py index a7d846d76..d165f8910 100644 --- a/finat/physically_mapped.py +++ b/finat/physically_mapped.py @@ -201,8 +201,8 @@ year={2019} } """) - Citations().add("GuzmanNeilan2019", """ -@article{guzman2019infsup, + Citations().add("GuzmanNeilan2018", """ +@article{guzman2018infsup, author = {Guzm\'{a}n, Johnny and Neilan, Michael}, title = {{Inf-Sup Stable Finite Elements on Barycentric Refinements Producing Divergence--Free Approximations in Arbitrary Dimensions}}, journal = {SIAM Journal on Numerical Analysis}, diff --git a/finat/piola_mapped.py b/finat/piola_mapped.py index aea4dcab1..2a82896ec 100644 --- a/finat/piola_mapped.py +++ b/finat/piola_mapped.py @@ -101,12 +101,14 @@ def __init__(self, fiat_element): # The tangential dofs should be numbered last, and are constrained to be zero sd = self.cell.get_spatial_dimension() reduced_dofs = deepcopy(self._element.entity_dofs()) + reduced_dim = 0 cur = reduced_dofs[sd-1][0][0] - for entity in reduced_dofs[sd-1]: + for entity in sorted(reduced_dofs[sd-1]): + reduced_dim += len(reduced_dofs[sd-1][entity][1:]) reduced_dofs[sd-1][entity] = [cur] cur += 1 self._entity_dofs = reduced_dofs - self._space_dimension = cur + self._space_dimension = fiat_element.space_dimension() - reduced_dim def entity_dofs(self): return self._entity_dofs @@ -125,7 +127,7 @@ def basis_transformation(self, coordinate_mapping): detJ = coordinate_mapping.detJ_at(bary) dofs = self.entity_dofs() - edofs = self._element.entity_dofs() + bfs = self._element.entity_dofs() ndof = self.space_dimension() numbf = self._element.space_dimension() V = numpy.eye(numbf, ndof, dtype=object) @@ -133,12 +135,22 @@ def basis_transformation(self, coordinate_mapping): V[multiindex] = Literal(V[multiindex]) # Undo the Piola transform for non-facet bubble basis functions + nodes = self._element.get_dual_set().nodes Finv = piola_inverse(self.cell, J, detJ) - for dim in range(sd-1): + for dim in dofs: + if dim == sd-1: + continue for e in sorted(dofs[dim]): - for k in range(0, len(dofs[dim][e]), sd): - cur = dofs[dim][e][k:k+sd] - V[numpy.ix_(cur, cur)] = Finv + k = 0 + while k < len(dofs[dim][e]): + cur = dofs[dim][e][k] + if len(nodes[cur].deriv_dict) > 0: + V[cur, cur] = detJ + k += 1 + else: + s = dofs[dim][e][k:k+sd] + V[numpy.ix_(s, s)] = Finv + k += sd # Unpick the normal component for the facet bubbles if sd == 2: @@ -148,7 +160,7 @@ def basis_transformation(self, coordinate_mapping): for f in sorted(dofs[sd-1]): rows = numpy.asarray(transform(self.cell, J, detJ, f)) - fdofs = dofs[sd-1][f] - bfs = edofs[sd-1][f][1:] - V[numpy.ix_(bfs, fdofs)] = rows[..., :len(fdofs)] + cur_dofs = dofs[sd-1][f] + cur_bfs = bfs[sd-1][f][1:] + V[numpy.ix_(cur_bfs, cur_dofs)] = rows[..., :len(cur_dofs)] return ListTensor(V.T) diff --git a/finat/ufl/elementlist.py b/finat/ufl/elementlist.py index 1ba31fc27..fd8e1f957 100644 --- a/finat/ufl/elementlist.py +++ b/finat/ufl/elementlist.py @@ -112,8 +112,8 @@ def show_elements(): # Elements not in the periodic table register_element("Argyris", "ARG", 0, H2, "custom", (5, None), ("triangle",)) register_element("Bell", "BELL", 0, H2, "custom", (5, 5), ("triangle",)) -register_element("Bernardi-Raugel", "BR", 1, H1, "contravariant Piola", (2, 3), simplices[1:]) -register_element("Bernardi-Raugel Bubble", "BRB", 1, H1, "contravariant Piola", (2, 3), simplices[1:]) +register_element("Bernardi-Raugel", "BR", 1, H1, "contravariant Piola", (1, None), simplices[1:]) +register_element("Bernardi-Raugel Bubble", "BRB", 1, H1, "contravariant Piola", (None, None), simplices[1:]) register_element("Brezzi-Douglas-Fortin-Marini", "BDFM", 1, HDiv, "contravariant Piola", (1, None), simplices[1:]) register_element("Crouzeix-Raviart", "CR", 0, L2, "identity", (1, 1), @@ -133,13 +133,17 @@ def show_elements(): register_element("QuadraticPowellSabin12", "PS12", 0, H2, "custom", (2, 2), ("triangle",)) register_element("Hsieh-Clough-Tocher", "HCT", 0, H2, "custom", (3, None), ("triangle",)) register_element("Reduced-Hsieh-Clough-Tocher", "HCT-red", 0, H2, "custom", (3, 3), ("triangle",)) +register_element("Johnson-Mercier", "JM", 2, HDivDiv, "double contravariant Piola", (1, 1), simplices[1:]) + register_element("Arnold-Qin", "AQ", 1, H1, "identity", (2, 2), ("triangle",)) register_element("Reduced-Arnold-Qin", "AQ-red", 1, H1, "contravariant Piola", (2, 2), ("triangle",)) register_element("Christiansen-Hu", "CH", 1, H1, "contravariant Piola", (1, 1), simplices[1:]) -register_element("Guzman-Neilan", "GN", 1, H1, "contravariant Piola", (2, 3), simplices[1:]) -register_element("Guzman-Neilan Bubble", "GNB", 1, H1, "contravariant Piola", (2, 3), simplices[1:]) register_element("Alfeld-Sorokina", "AS", 1, H1, "contravariant Piola", (2, 2), simplices[1:]) -register_element("Johnson-Mercier", "JM", 2, HDivDiv, "double contravariant Piola", (1, 1), simplices[1:]) + +register_element("Guzman-Neilan 1st kind H1", "GN", 1, H1, "contravariant Piola", (1, None), simplices[1:]) +register_element("Guzman-Neilan 2nd kind H1", "GN2", 1, H1, "contravariant Piola", (1, None), simplices[1:]) +register_element("Guzman-Neilan H1(div)", "GNH1div", 1, H1, "contravariant Piola", (2, None), simplices[1:]) +register_element("Guzman-Neilan Bubble", "GNB", 1, H1, "contravariant Piola", (None, None), simplices[1:]) # Special elements register_element("Boundary Quadrature", "BQ", 0, L2, "identity", (0, None), @@ -494,4 +498,7 @@ def canonical_element_description(family, cell, order, form_degree): else: raise ValueError(f"Invalid value rank {value_rank}.") - return family, short_name, order, value_shape, reference_value_shape, sobolev_space, mapping + embedded_degree = order + if any(bubble in family for bubble in ("Guzman-Neilan", "Bernardi-Raugel")): + embedded_degree = tdim + return family, short_name, order, value_shape, reference_value_shape, sobolev_space, mapping, embedded_degree diff --git a/finat/ufl/enrichedelement.py b/finat/ufl/enrichedelement.py index 1b7a159fe..2c836d10b 100644 --- a/finat/ufl/enrichedelement.py +++ b/finat/ufl/enrichedelement.py @@ -97,18 +97,12 @@ def reconstruct(self, **kwargs): @property def embedded_subdegree(self): """Return embedded subdegree.""" - if isinstance(self._degree, int): - return self._degree - else: - return min(e.embedded_subdegree for e in self._elements) + return min(e.embedded_subdegree for e in self._elements) @property def embedded_superdegree(self): """Return embedded superdegree.""" - if isinstance(self._degree, int): - return self._degree - else: - return max(e.embedded_superdegree for e in self._elements) + return max(e.embedded_superdegree for e in self._elements) class EnrichedElement(EnrichedElementBase): diff --git a/finat/ufl/finiteelement.py b/finat/ufl/finiteelement.py index 7027b318f..97e51d6b1 100644 --- a/finat/ufl/finiteelement.py +++ b/finat/ufl/finiteelement.py @@ -41,7 +41,7 @@ def __new__(cls, from finat.ufl.hdivcurl import HDivElement as HDiv from finat.ufl.tensorproductelement import TensorProductElement - family, short_name, degree, value_shape, reference_value_shape, sobolev_space, mapping = \ + family, short_name, degree, value_shape, reference_value_shape, sobolev_space, mapping, embedded_degree = \ canonical_element_description(family, cell, degree, form_degree) if family in ["RTCF", "RTCE"]: @@ -141,7 +141,7 @@ def __init__(self, cell = as_cell(cell) ( - family, short_name, degree, value_shape, reference_value_shape, sobolev_space, mapping + family, short_name, degree, value_shape, reference_value_shape, sobolev_space, mapping, embedded_degree ) = canonical_element_description(family, cell, degree, form_degree) # TODO: Move these to base? Might be better to instead @@ -150,6 +150,7 @@ def __init__(self, self._mapping = mapping self._short_name = short_name or family self._variant = variant + self._embedded_degree = embedded_degree # Type check variant if variant is not None and not isinstance(variant, str): @@ -238,15 +239,19 @@ def __getnewargs__(self): @property def embedded_subdegree(self): """Return embedded subdegree.""" - if isinstance(self.degree(), int): - return self.degree() - else: - return min(self.degree()) + subdegree = self.degree() + if not isinstance(subdegree, int): + subdegree = min(subdegree) + if isinstance(self._embedded_degree, int): + subdegree = min(subdegree, self._embedded_degree) + return subdegree @property def embedded_superdegree(self): """Return embedded superdegree.""" - if isinstance(self.degree(), int): - return self.degree() - else: - return max(self.degree()) + superdegree = self.degree() + if not isinstance(superdegree, int): + superdegree = max(superdegree) + if isinstance(self._embedded_degree, int): + superdegree = max(superdegree, self._embedded_degree) + return superdegree diff --git a/finat/ufl/mixedelement.py b/finat/ufl/mixedelement.py index a6f8fbadb..a5fc54a91 100644 --- a/finat/ufl/mixedelement.py +++ b/finat/ufl/mixedelement.py @@ -250,18 +250,12 @@ def degree(self, component=None): @property def embedded_subdegree(self): """Return embedded subdegree.""" - if isinstance(self._degree, int): - return self._degree - else: - return min(e.embedded_subdegree for e in self.sub_elements) + return min(e.embedded_subdegree for e in self.sub_elements) @property def embedded_superdegree(self): """Return embedded superdegree.""" - if isinstance(self._degree, int): - return self._degree - else: - return max(e.embedded_superdegree for e in self.sub_elements) + return max(e.embedded_superdegree for e in self.sub_elements) def reconstruct(self, **kwargs): """Doc.""" diff --git a/test/test_zany_mapping.py b/test/test_zany_mapping.py index f77c58735..89cffac19 100644 --- a/test/test_zany_mapping.py +++ b/test/test_zany_mapping.py @@ -36,18 +36,9 @@ def make_unisolvent_points(element, interior=False): return pts -def reconstruct_fiat_element(fiat_element, *args, **kwargs): - indices = None - if isinstance(fiat_element, FIAT.RestrictedElement): - indices = fiat_element._indices - fiat_element = fiat_element._element - fe = type(fiat_element)(*args, **kwargs) - if indices is not None: - fe = FIAT.RestrictedElement(fe, indices=indices) - return fe - - -def check_zany_mapping(finat_element, phys_element): +def check_zany_mapping(element, ref_cell, phys_cell, *args, **kwargs): + phys_element = element(phys_cell, *args, **kwargs).fiat_equivalent + finat_element = element(ref_cell, *args, **kwargs) ref_element = finat_element.fiat_equivalent shape = ref_element.value_shape() @@ -91,15 +82,10 @@ def check_zany_mapping(finat_element, phys_element): finat.Bell, ]) def test_C1_elements(ref_cell, phys_cell, element): - kwargs = {} finat_kwargs = {} - if element == finat.ReducedHsiehCloughTocher: - kwargs = dict(reduced=True) if element == finat.QuadraticPowellSabin12: finat_kwargs = dict(avg=True) - finat_element = element(ref_cell, **finat_kwargs) - phys_element = reconstruct_fiat_element(finat_element.fiat_equivalent, phys_cell, **kwargs) - check_zany_mapping(finat_element, phys_element) + check_zany_mapping(element, ref_cell, phys_cell, **finat_kwargs) @pytest.mark.parametrize("element, degree", [ @@ -107,18 +93,18 @@ def test_C1_elements(ref_cell, phys_cell, element): *((finat.HsiehCloughTocher, k) for k in range(3, 6)) ]) def test_high_order_C1_elements(ref_cell, phys_cell, element, degree): - finat_element = element(ref_cell, degree, avg=True) - phys_element = reconstruct_fiat_element(finat_element.fiat_equivalent, phys_cell, degree) - check_zany_mapping(finat_element, phys_element) + check_zany_mapping(element, ref_cell, phys_cell, degree, avg=True) def test_argyris_point(ref_cell, phys_cell): - finat_element = finat.Argyris(ref_cell, variant="point") - phys_element = reconstruct_fiat_element(finat_element.fiat_equivalent, phys_cell, variant="point") - check_zany_mapping(finat_element, phys_element) + element = finat.Argyris + check_zany_mapping(element, ref_cell, phys_cell, variant="point") -def check_zany_piola_mapping(finat_element, phys_element): +def check_zany_piola_mapping(element, ref_cell, phys_cell, *args, **kwargs): + phys_element = element(phys_cell).fiat_equivalent + finat_element = element(ref_cell) + ref_element = finat_element._element ref_cell = ref_element.get_reference_element() phys_cell = phys_element.get_reference_element() @@ -184,18 +170,12 @@ def check_zany_piola_mapping(finat_element, phys_element): finat.ArnoldWinther, finat.ArnoldWintherNC, finat.JohnsonMercier, - finat.GuzmanNeilan, + finat.GuzmanNeilanFirstKindH1, + finat.GuzmanNeilanSecondKindH1, finat.GuzmanNeilanBubble, ]) def test_piola_triangle(ref_cell, phys_cell, element): - finat_element = element(ref_cell) - kwargs = {} - if isinstance(finat_element, (finat.BernardiRaugelBubble, finat.GuzmanNeilanBubble)): - kwargs["subdegree"] = 0 - elif isinstance(finat_element, finat.ReducedArnoldQin): - kwargs["reduced"] = True - phys_element = reconstruct_fiat_element(finat_element.fiat_equivalent, phys_cell, **kwargs) - check_zany_piola_mapping(finat_element, phys_element) + check_zany_piola_mapping(element, ref_cell, phys_cell) @pytest.fixture @@ -220,13 +200,10 @@ def phys_tet(request): finat.ChristiansenHu, finat.AlfeldSorokina, finat.JohnsonMercier, - finat.GuzmanNeilan, + finat.GuzmanNeilanFirstKindH1, + finat.GuzmanNeilanSecondKindH1, finat.GuzmanNeilanBubble, + finat.GuzmanNeilanH1div, ]) def test_piola_tetrahedron(ref_tet, phys_tet, element): - finat_element = element(ref_tet) - kwargs = {} - if isinstance(finat_element, (finat.BernardiRaugelBubble, finat.GuzmanNeilanBubble)): - kwargs["subdegree"] = 0 - phys_element = reconstruct_fiat_element(finat_element.fiat_equivalent, phys_tet, **kwargs) - check_zany_piola_mapping(finat_element, phys_element) + check_zany_piola_mapping(element, ref_tet, phys_tet) From c37a93d02f92a14957bba1e5974d69a5763abec8 Mon Sep 17 00:00:00 2001 From: ksagiyam <46749170+ksagiyam@users.noreply.github.com> Date: Wed, 6 Nov 2024 14:02:16 +0000 Subject: [PATCH 743/749] Ksagiyam/hex interior facet (#138) --- finat/quadrature.py | 46 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/finat/quadrature.py b/finat/quadrature.py index 9bf1056ef..ec6def127 100644 --- a/finat/quadrature.py +++ b/finat/quadrature.py @@ -34,7 +34,7 @@ def make_quadrature(ref_el, degree, scheme="default"): assert len(ref_el.cells) == len(degree) quad_rules = [make_quadrature(c, d, scheme) for c, d in zip(ref_el.cells, degree)] - return TensorProductQuadratureRule(quad_rules) + return TensorProductQuadratureRule(quad_rules, ref_el=ref_el) if ref_el.get_shape() == QUADRILATERAL: return make_quadrature(ref_el.product, degree, scheme) @@ -50,10 +50,10 @@ def make_quadrature(ref_el, degree, scheme="default"): num_points = (degree + 1 + 1) // 2 # exact integration fiat_rule = GaussLegendreQuadratureLineRule(ref_el, num_points) point_set = GaussLegendrePointSet(fiat_rule.get_points()) - return QuadratureRule(point_set, fiat_rule.get_weights()) + return QuadratureRule(point_set, fiat_rule.get_weights(), ref_el=ref_el, io_ornt_map_tuple=fiat_rule._intrinsic_orientation_permutation_map_tuple) fiat_rule = fiat_scheme(ref_el, degree, scheme) - return QuadratureRule(PointSet(fiat_rule.get_points()), fiat_rule.get_weights()) + return QuadratureRule(PointSet(fiat_rule.get_points()), fiat_rule.get_weights(), ref_el=ref_el, io_ornt_map_tuple=fiat_rule._intrinsic_orientation_permutation_map_tuple) class AbstractQuadratureRule(metaclass=ABCMeta): @@ -69,16 +69,46 @@ def weight_expression(self): """GEM expression describing the weights, with the same free indices as the point set.""" + @cached_property + def extrinsic_orientation_permutation_map(self): + """A map from extrinsic orientations to corresponding axis permutation matrices. + + Notes + ----- + result[eo] gives the physical axis-reference axis permutation matrix corresponding to + eo (extrinsic orientation). + + """ + if self.ref_el is None: + raise ValueError("Must set ref_el") + return self.ref_el.extrinsic_orientation_permutation_map + + @cached_property + def intrinsic_orientation_permutation_map_tuple(self): + """A tuple of maps from intrinsic orientations to corresponding point permutations for each reference cell axis. + + Notes + ----- + result[axis][io] gives the physical point-reference point permutation array corresponding to + io (intrinsic orientation) on ``axis``. + + """ + if any(m is None for m in self._intrinsic_orientation_permutation_map_tuple): + raise ValueError("Must set _intrinsic_orientation_permutation_map_tuple") + return self._intrinsic_orientation_permutation_map_tuple + class QuadratureRule(AbstractQuadratureRule): """Generic quadrature rule with no internal structure.""" - def __init__(self, point_set, weights): + def __init__(self, point_set, weights, ref_el=None, io_ornt_map_tuple=(None, )): weights = numpy.asarray(weights) assert len(point_set.points) == len(weights) + self.ref_el = ref_el self.point_set = point_set self.weights = numpy.asarray(weights) + self._intrinsic_orientation_permutation_map_tuple = io_ornt_map_tuple @cached_property def point_set(self): @@ -92,8 +122,14 @@ def weight_expression(self): class TensorProductQuadratureRule(AbstractQuadratureRule): """Quadrature rule which is a tensor product of other rules.""" - def __init__(self, factors): + def __init__(self, factors, ref_el=None): + self.ref_el = ref_el self.factors = tuple(factors) + self._intrinsic_orientation_permutation_map_tuple = tuple( + m + for factor in factors + for m in factor._intrinsic_orientation_permutation_map_tuple + ) @cached_property def point_set(self): From 348569fc6bbba508f7a6a74d3f17c388d8d6e55f Mon Sep 17 00:00:00 2001 From: ksagiyam <46749170+ksagiyam@users.noreply.github.com> Date: Wed, 6 Nov 2024 14:07:48 +0000 Subject: [PATCH 744/749] generalise VariableIndex and FlexiblyIndexed (#317) hex: enable interior facet integration --- gem/gem.py | 325 ++++++++++++++++++++++++++++++++++++++-------- gem/node.py | 47 ++++++- gem/optimise.py | 27 +++- gem/scheduling.py | 9 +- 4 files changed, 336 insertions(+), 72 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index 95e8f2f5e..c37e43529 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -24,16 +24,21 @@ from gem.node import Node as NodeBase, traversal +from FIAT.orientation_utils import Orientation as FIATOrientation + __all__ = ['Node', 'Identity', 'Literal', 'Zero', 'Failure', - 'Variable', 'Sum', 'Product', 'Division', 'Power', + 'Variable', 'Sum', 'Product', 'Division', 'FloorDiv', 'Remainder', 'Power', 'MathFunction', 'MinValue', 'MaxValue', 'Comparison', 'LogicalNot', 'LogicalAnd', 'LogicalOr', 'Conditional', 'Index', 'VariableIndex', 'Indexed', 'ComponentTensor', - 'IndexSum', 'ListTensor', 'Concatenate', 'Delta', + 'IndexSum', 'ListTensor', 'Concatenate', 'Delta', 'OrientationVariableIndex', 'index_sum', 'partial_indexed', 'reshape', 'view', 'indices', 'as_gem', 'FlexiblyIndexed', - 'Inverse', 'Solve', 'extract_type'] + 'Inverse', 'Solve', 'extract_type', 'uint_type'] + + +uint_type = numpy.uintc class NodeMeta(type): @@ -130,6 +135,24 @@ def __truediv__(self, other): def __rtruediv__(self, other): return as_gem(other).__truediv__(self) + def __floordiv__(self, other): + other = as_gem_uint(other) + if other.shape: + raise ValueError("Denominator must be scalar") + return componentwise(FloorDiv, self, other) + + def __rfloordiv__(self, other): + return as_gem_uint(other).__floordiv__(self) + + def __mod__(self, other): + other = as_gem_uint(other) + if other.shape: + raise ValueError("Denominator must be scalar") + return componentwise(Remainder, self, other) + + def __rmod__(self, other): + return as_gem_uint(other).__mod__(self) + class Terminal(Node): """Abstract class for terminal GEM nodes.""" @@ -167,7 +190,8 @@ class Constant(Terminal): - array: numpy array of values - value: float or complex value (scalars only) """ - __slots__ = () + __slots__ = ('dtype',) + __back__ = ('dtype',) class Zero(Constant): @@ -176,13 +200,14 @@ class Zero(Constant): __slots__ = ('shape',) __front__ = ('shape',) - def __init__(self, shape=()): + def __init__(self, shape=(), dtype=float): self.shape = shape + self.dtype = dtype @property def value(self): assert not self.shape - return 0.0 + return numpy.array(0, dtype=self.dtype).item() class Identity(Constant): @@ -191,8 +216,9 @@ class Identity(Constant): __slots__ = ('dim',) __front__ = ('dim',) - def __init__(self, dim): + def __init__(self, dim, dtype=float): self.dim = dim + self.dtype = dtype @property def shape(self): @@ -200,7 +226,7 @@ def shape(self): @property def array(self): - return numpy.eye(self.dim) + return numpy.eye(self.dim, dtype=self.dtype) class Literal(Constant): @@ -209,16 +235,24 @@ class Literal(Constant): __slots__ = ('array',) __front__ = ('array',) - def __new__(cls, array): + def __new__(cls, array, dtype=None): array = asarray(array) return super(Literal, cls).__new__(cls) - def __init__(self, array): + def __init__(self, array, dtype=None): array = asarray(array) - try: - self.array = array.astype(float, casting="safe") - except TypeError: - self.array = array.astype(complex) + if dtype is None: + # Assume float or complex. + try: + self.array = array.astype(float, casting="safe") + self.dtype = float + except TypeError: + self.array = array.astype(complex) + self.dtype = complex + else: + # Can be int, etc. + self.array = array.astype(dtype) + self.dtype = dtype def is_equal(self, other): if type(self) is not type(other): @@ -243,12 +277,13 @@ def shape(self): class Variable(Terminal): """Symbolic variable tensor""" - __slots__ = ('name', 'shape') - __front__ = ('name', 'shape') + __slots__ = ('name', 'shape', 'dtype') + __front__ = ('name', 'shape', 'dtype') - def __init__(self, name, shape): + def __init__(self, name, shape, dtype=None): self.name = name self.shape = shape + self.dtype = dtype class Sum(Scalar): @@ -265,7 +300,8 @@ def __new__(cls, a, b): return a if isinstance(a, Constant) and isinstance(b, Constant): - return Literal(a.value + b.value) + dtype = numpy.result_type(a.dtype, b.dtype) + return Literal(a.value + b.value, dtype=dtype) self = super(Sum, cls).__new__(cls) self.children = a, b @@ -289,7 +325,8 @@ def __new__(cls, a, b): return a if isinstance(a, Constant) and isinstance(b, Constant): - return Literal(a.value * b.value) + dtype = numpy.result_type(a.dtype, b.dtype) + return Literal(a.value * b.value, dtype=dtype) self = super(Product, cls).__new__(cls) self.children = a, b @@ -313,13 +350,62 @@ def __new__(cls, a, b): return a if isinstance(a, Constant) and isinstance(b, Constant): - return Literal(a.value / b.value) + dtype = numpy.result_type(a.dtype, b.dtype) + return Literal(a.value / b.value, dtype=dtype) self = super(Division, cls).__new__(cls) self.children = a, b return self +class FloorDiv(Scalar): + __slots__ = ('children',) + + def __new__(cls, a, b): + assert not a.shape + assert not b.shape + # TODO: Attach dtype property to Node and check that + # numpy.result_dtype(a.dtype, b.dtype) is uint type. + # dtype is currently attached only to {Constant, Variable}. + # Constant folding + if isinstance(b, Zero): + raise ValueError("division by zero") + if isinstance(a, Zero): + return Zero(dtype=a.dtype) + if isinstance(b, Constant) and b.value == 1: + return a + if isinstance(a, Constant) and isinstance(b, Constant): + dtype = numpy.result_type(a.dtype, b.dtype) + return Literal(a.value // b.value, dtype=dtype) + self = super(FloorDiv, cls).__new__(cls) + self.children = a, b + return self + + +class Remainder(Scalar): + __slots__ = ('children',) + + def __new__(cls, a, b): + assert not a.shape + assert not b.shape + # TODO: Attach dtype property to Node and check that + # numpy.result_dtype(a.dtype, b.dtype) is uint type. + # dtype is currently attached only to {Constant, Variable}. + # Constant folding + if isinstance(b, Zero): + raise ValueError("division by zero") + if isinstance(a, Zero): + return Zero(dtype=a.dtype) + if isinstance(b, Constant) and b.value == 1: + return Zero(dtype=b.dtype) + if isinstance(a, Constant) and isinstance(b, Constant): + dtype = numpy.result_type(a.dtype, b.dtype) + return Literal(a.value % b.value, dtype=dtype) + self = super(Remainder, cls).__new__(cls) + self.children = a, b + return self + + class Power(Scalar): __slots__ = ('children',) @@ -329,14 +415,16 @@ def __new__(cls, base, exponent): # Constant folding if isinstance(base, Zero): + dtype = numpy.result_type(base.dtype, exponent.dtype) if isinstance(exponent, Zero): raise ValueError("cannot solve 0^0") - return Zero() + return Zero(dtype=dtype) elif isinstance(exponent, Zero): - return one - - if isinstance(base, Constant) and isinstance(exponent, Constant): - return Literal(base.value ** exponent.value) + dtype = numpy.result_type(base.dtype, exponent.dtype) + return Literal(1, dtype=dtype) + elif isinstance(base, Constant) and isinstance(exponent, Constant): + dtype = numpy.result_type(base.dtype, exponent.dtype) + return Literal(base.value ** exponent.value, dtype=dtype) self = super(Power, cls).__new__(cls) self.children = base, exponent @@ -502,7 +590,6 @@ class VariableIndex(IndexBase): def __init__(self, expression): assert isinstance(expression, Node) - assert not expression.free_indices assert not expression.shape self.expression = expression @@ -517,20 +604,20 @@ def __ne__(self, other): return not self.__eq__(other) def __hash__(self): - return hash((VariableIndex, self.expression)) + return hash((type(self), self.expression)) def __str__(self): return str(self.expression) def __repr__(self): - return "VariableIndex(%r)" % (self.expression,) + return "%r(%r)" % (type(self), self.expression,) def __reduce__(self): - return VariableIndex, (self.expression,) + return type(self), (self.expression,) class Indexed(Scalar): - __slots__ = ('children', 'multiindex') + __slots__ = ('children', 'multiindex', 'indirect_children') __back__ = ('multiindex',) def __new__(cls, aggregate, multiindex): @@ -553,41 +640,59 @@ def __new__(cls, aggregate, multiindex): # Zero folding if isinstance(aggregate, Zero): - return Zero() + return Zero(dtype=aggregate.dtype) # All indices fixed if all(isinstance(i, int) for i in multiindex): if isinstance(aggregate, Constant): - return Literal(aggregate.array[multiindex]) + return Literal(aggregate.array[multiindex], dtype=aggregate.dtype) elif isinstance(aggregate, ListTensor): return aggregate.array[multiindex] self = super(Indexed, cls).__new__(cls) self.children = (aggregate,) self.multiindex = multiindex + self.indirect_children = tuple(i.expression for i in self.multiindex if isinstance(i, VariableIndex)) - new_indices = tuple(i for i in multiindex if isinstance(i, Index)) - self.free_indices = unique(aggregate.free_indices + new_indices) + new_indices = [] + for i in multiindex: + if isinstance(i, Index): + new_indices.append(i) + elif isinstance(i, VariableIndex): + new_indices.extend(i.expression.free_indices) + self.free_indices = unique(aggregate.free_indices + tuple(new_indices)) return self def index_ordering(self): """Running indices in the order of indexing in this node.""" - return tuple(i for i in self.multiindex if isinstance(i, Index)) + free_indices = [] + for i in self.multiindex: + if isinstance(i, Index): + free_indices.append(i) + elif isinstance(i, VariableIndex): + free_indices.extend(i.expression.free_indices) + return tuple(free_indices) class FlexiblyIndexed(Scalar): """Flexible indexing of :py:class:`Variable`s to implement views and reshapes (splitting dimensions only).""" - __slots__ = ('children', 'dim2idxs') + __slots__ = ('children', 'dim2idxs', 'indirect_children') __back__ = ('dim2idxs',) def __init__(self, variable, dim2idxs): """Construct a flexibly indexed node. - :arg variable: a node that has a shape - :arg dim2idxs: describes the mapping of indices + Parameters + ---------- + variable : Node + `Node` that has a shape. + dim2idxs : tuple + Tuple of (offset, ((index, stride), (...), ...)) mapping indices, + where offset is {Node, int}, index is {Index, VariableIndex, int}, and + stride is {Node, int}. For example, if ``variable`` is rank two, and ``dim2idxs`` is @@ -600,40 +705,73 @@ def __init__(self, variable, dim2idxs): """ assert variable.shape assert len(variable.shape) == len(dim2idxs) - dim2idxs_ = [] free_indices = [] for dim, (offset, idxs) in zip(variable.shape, dim2idxs): offset_ = offset idxs_ = [] last = 0 - for idx in idxs: - index, stride = idx + if isinstance(offset, Node): + free_indices.extend(offset.free_indices) + for index, stride in idxs: if isinstance(index, Index): assert index.extent is not None free_indices.append(index) idxs_.append((index, stride)) last += (index.extent - 1) * stride + elif isinstance(index, VariableIndex): + base_indices = index.expression.free_indices + assert all(base_index.extent is not None for base_index in base_indices) + free_indices.extend(base_indices) + idxs_.append((index, stride)) + # last += (unknown_extent - 1) * stride elif isinstance(index, int): - offset_ += index * stride + # TODO: Attach dtype to each Node. + # Here, we should simply be able to do: + # >>> offset_ += index * stride + # but "+" and "*" are not currently correctly overloaded + # for indices (integers); they assume floats. + if not isinstance(offset, Integral): + raise NotImplementedError(f"Found non-Integral offset : {offset}") + if isinstance(stride, Constant): + offset_ += index * stride.value + else: + offset_ += index * stride else: raise ValueError("Unexpected index type for flexible indexing") - - if dim is not None and offset_ + last >= dim: + if isinstance(stride, Node): + free_indices.extend(stride.free_indices) + if dim is not None and isinstance(offset_ + last, Integral) and offset_ + last >= dim: raise ValueError("Offset {0} and indices {1} exceed dimension {2}".format(offset, idxs, dim)) - dim2idxs_.append((offset_, tuple(idxs_))) - self.children = (variable,) self.dim2idxs = tuple(dim2idxs_) self.free_indices = unique(free_indices) + indirect_children = [] + for offset, idxs in self.dim2idxs: + if isinstance(offset, Node): + indirect_children.append(offset) + for idx, stride in idxs: + if isinstance(idx, VariableIndex): + indirect_children.append(idx.expression) + if isinstance(stride, Node): + indirect_children.append(stride) + self.indirect_children = tuple(indirect_children) def index_ordering(self): """Running indices in the order of indexing in this node.""" - return tuple(index - for _, idxs in self.dim2idxs - for index, _ in idxs - if isinstance(index, Index)) + free_indices = [] + for offset, idxs in self.dim2idxs: + if isinstance(offset, Node): + free_indices.extend(offset.free_indices) + for index, stride in idxs: + if isinstance(index, Index): + free_indices.append(index) + elif isinstance(index, VariableIndex): + free_indices.extend(index.expression.free_indices) + if isinstance(stride, Node): + free_indices.extend(stride.free_indices) + return tuple(free_indices) class ComponentTensor(Node): @@ -653,7 +791,7 @@ def __new__(cls, expression, multiindex): # Zero folding if isinstance(expression, Zero): - return Zero(shape) + return Zero(shape, dtype=expression.dtype) self = super(ComponentTensor, cls).__new__(cls) self.children = (expression,) @@ -771,7 +909,8 @@ class Concatenate(Node): def __new__(cls, *children): if all(isinstance(child, Zero) for child in children): size = int(sum(numpy.prod(child.shape, dtype=int) for child in children)) - return Zero((size,)) + dtype = numpy.result_type(*(child.dtype for child in children)) + return Zero((size,), dtype=dtype) self = super(Concatenate, cls).__new__(cls) self.children = children @@ -802,7 +941,12 @@ def __new__(cls, i, j): self.i = i self.j = j # Set up free indices - free_indices = tuple(index for index in (i, j) if isinstance(index, Index)) + free_indices = [] + for index in (i, j): + if isinstance(index, Index): + free_indices.append(index) + elif isinstance(index, VariableIndex): + raise NotImplementedError("Can not make Delta with VariableIndex") self.free_indices = tuple(unique(free_indices)) return self @@ -845,6 +989,34 @@ def __init__(self, A, B): self.shape = A.shape[1:] + B.shape[1:] +class OrientationVariableIndex(VariableIndex, FIATOrientation): + """VariableIndex representing a fiat orientation. + + Notes + ----- + In the current implementation, we need to extract + `VariableIndex.expression` as index arithmetic + is not supported (indices are not `Node`). + + """ + + def __floordiv__(self, other): + other = other.expression if isinstance(other, VariableIndex) else as_gem_uint(other) + return type(self)(FloorDiv(self.expression, other)) + + def __rfloordiv__(self, other): + other = other.expression if isinstance(other, VariableIndex) else as_gem_uint(other) + return type(self)(FloorDiv(other, self.expression)) + + def __mod__(self, other): + other = other.expression if isinstance(other, VariableIndex) else as_gem_uint(other) + return type(self)(Remainder(self.expression, other)) + + def __rmod__(self, other): + other = other.expression if isinstance(other, VariableIndex) else as_gem_uint(other) + return type(self)(Remainder(other, self.expression)) + + def unique(indices): """Sorts free indices and eliminates duplicates. @@ -1027,11 +1199,23 @@ def componentwise(op, *exprs): def as_gem(expr): - """Attempt to convert an expression into GEM. + """Attempt to convert an expression into GEM of scalar type. + + Parameters + ---------- + expr : Node or Number + The expression. + + Returns + ------- + Node + A GEM representation of the expression. + + Raises + ------ + ValueError + If conversion was not possible. - :arg expr: The expression. - :returns: A GEM representation of the expression. - :raises ValueError: if conversion was not possible. """ if isinstance(expr, Node): return expr @@ -1041,6 +1225,33 @@ def as_gem(expr): raise ValueError("Do not know how to convert %r to GEM" % expr) +def as_gem_uint(expr): + """Attempt to convert an expression into GEM of uint type. + + Parameters + ---------- + expr : Node or Integral + The expression. + + Returns + ------- + Node + A GEM representation of the expression. + + Raises + ------ + ValueError + If conversion was not possible. + + """ + if isinstance(expr, Node): + return expr + elif isinstance(expr, Integral): + return Literal(expr, dtype=uint_type) + else: + raise ValueError("Do not know how to convert %r to GEM" % expr) + + def extract_type(expressions, klass): """Collects objects of type klass in expressions.""" return tuple(node for node in traversal(expressions) if isinstance(node, klass)) diff --git a/gem/node.py b/gem/node.py index 31d99b9ec..5d9c5bf04 100644 --- a/gem/node.py +++ b/gem/node.py @@ -2,6 +2,7 @@ expression DAG languages.""" import collections +import gem class Node(object): @@ -99,8 +100,23 @@ def get_hash(self): return hash((type(self),) + self._cons_args(self.children)) +def _make_traversal_children(node): + if isinstance(node, (gem.Indexed, gem.FlexiblyIndexed)): + # Include child nodes hidden in index expressions. + return node.children + node.indirect_children + else: + return node.children + + def pre_traversal(expression_dags): - """Pre-order traversal of the nodes of expression DAGs.""" + """Pre-order traversal of the nodes of expression DAGs. + + Notes + ----- + This function also walks through nodes in index expressions + (e.g., `VariableIndex`s); see ``_make_traversal_children()``. + + """ seen = set() lifo = [] # Some roots might be same, but they must be visited only once. @@ -114,14 +130,23 @@ def pre_traversal(expression_dags): while lifo: node = lifo.pop() yield node - for child in reversed(node.children): + children = _make_traversal_children(node) + for child in reversed(children): if child not in seen: seen.add(child) lifo.append(child) def post_traversal(expression_dags): - """Post-order traversal of the nodes of expression DAGs.""" + """Post-order traversal of the nodes of expression DAGs. + + Notes + ----- + This function also walks through nodes in index expressions + (e.g., `VariableIndex`s); see ``_make_traversal_children()``. + + + """ seen = set() lifo = [] # Some roots might be same, but they must be visited only once. @@ -130,13 +155,13 @@ def post_traversal(expression_dags): for root in expression_dags: if root not in seen: seen.add(root) - lifo.append((root, list(root.children))) + lifo.append((root, list(_make_traversal_children(root)))) while lifo: node, deps = lifo[-1] for i, dep in enumerate(deps): if dep is not None and dep not in seen: - lifo.append((dep, list(dep.children))) + lifo.append((dep, list(_make_traversal_children(dep)))) deps[i] = None break else: @@ -150,10 +175,18 @@ def post_traversal(expression_dags): def collect_refcount(expression_dags): - """Collects reference counts for a multi-root expression DAG.""" + """Collects reference counts for a multi-root expression DAG. + + Notes + ----- + This function also collects reference counts of nodes + in index expressions (e.g., `VariableIndex`s); see + ``_make_traversal_children()``. + + """ result = collections.Counter(expression_dags) for node in traversal(expression_dags): - result.update(node.children) + result.update(_make_traversal_children(node)) return result diff --git a/gem/optimise.py b/gem/optimise.py index 3194e6efd..7d6c8ecd6 100644 --- a/gem/optimise.py +++ b/gem/optimise.py @@ -4,6 +4,7 @@ from collections import OrderedDict, defaultdict from functools import singledispatch, partial, reduce from itertools import combinations, permutations, zip_longest +from numbers import Integral import numpy @@ -95,11 +96,19 @@ def replace_indices(node, self, subst): replace_indices.register(Node)(reuse_if_untouched_arg) +def _replace_indices_atomic(i, self, subst): + if isinstance(i, VariableIndex): + new_expr = self(i.expression, subst) + return i if new_expr == i.expression else VariableIndex(new_expr) + else: + substitute = dict(subst) + return substitute.get(i, i) + + @replace_indices.register(Delta) def replace_indices_delta(node, self, subst): - substitute = dict(subst) - i = substitute.get(node.i, node.i) - j = substitute.get(node.j, node.j) + i = _replace_indices_atomic(node.i, self, subst) + j = _replace_indices_atomic(node.j, self, subst) if i == node.i and j == node.j: return node else: @@ -110,7 +119,9 @@ def replace_indices_delta(node, self, subst): def replace_indices_indexed(node, self, subst): child, = node.children substitute = dict(subst) - multiindex = tuple(substitute.get(i, i) for i in node.multiindex) + multiindex = [] + for i in node.multiindex: + multiindex.append(_replace_indices_atomic(i, self, subst)) if isinstance(child, ComponentTensor): # Indexing into ComponentTensor # Inline ComponentTensor and augment the substitution rules @@ -130,9 +141,11 @@ def replace_indices_flexiblyindexed(node, self, subst): child, = node.children assert not child.free_indices - substitute = dict(subst) dim2idxs = tuple( - (offset, tuple((substitute.get(i, i), s) for i, s in idxs)) + ( + offset if isinstance(offset, Integral) else _replace_indices_atomic(offset, self, subst), + tuple((_replace_indices_atomic(i, self, subst), s if isinstance(s, Integral) else self(s, subst)) for i, s in idxs) + ) for offset, idxs in node.dim2idxs ) @@ -145,6 +158,8 @@ def replace_indices_flexiblyindexed(node, self, subst): def filtered_replace_indices(node, self, subst): """Wrapper for :func:`replace_indices`. At each call removes substitution rules that do not apply.""" + if any(isinstance(k, VariableIndex) for k, _ in subst): + raise NotImplementedError("Can not replace VariableIndex (will need inverse)") filtered_subst = tuple((k, v) for k, v in subst if k in node.free_indices) return replace_indices(node, self, filtered_subst) diff --git a/gem/scheduling.py b/gem/scheduling.py index 831ee048b..41039a0e6 100644 --- a/gem/scheduling.py +++ b/gem/scheduling.py @@ -3,6 +3,7 @@ import collections import functools +import itertools from gem import gem, impero from gem.node import collect_refcount @@ -116,8 +117,12 @@ def handle(ops, push, decref, node): elif isinstance(node, gem.Zero): # should rarely happen assert not node.shape elif isinstance(node, (gem.Indexed, gem.FlexiblyIndexed)): - # Indexing always inlined - decref(node.children[0]) + if node.indirect_children: + # Do not inline; + # Index expression can be involved if it contains VariableIndex. + ops.append(impero.Evaluate(node)) + for child in itertools.chain(node.children, node.indirect_children): + decref(child) elif isinstance(node, gem.IndexSum): ops.append(impero.Noop(node)) push(impero.Accumulate(node)) From e3b9bbe74233c3bed4dee86b551ef31a8498f671 Mon Sep 17 00:00:00 2001 From: Matthew Scroggs Date: Fri, 15 Nov 2024 16:26:46 +0000 Subject: [PATCH 745/749] Update UFL element interface (#118) Co-authored-by: Pablo Brubeck --- finat/ufl/brokenelement.py | 3 +- finat/ufl/elementlist.py | 11 ++--- finat/ufl/enrichedelement.py | 7 +--- finat/ufl/finiteelement.py | 6 +-- finat/ufl/finiteelementbase.py | 31 ++++---------- finat/ufl/hdivcurl.py | 13 +++--- finat/ufl/mixedelement.py | 70 +++++++++++++------------------ finat/ufl/restrictedelement.py | 1 - finat/ufl/tensorproductelement.py | 9 ++-- 9 files changed, 53 insertions(+), 98 deletions(-) diff --git a/finat/ufl/brokenelement.py b/finat/ufl/brokenelement.py index 2b8a75468..3e8202883 100644 --- a/finat/ufl/brokenelement.py +++ b/finat/ufl/brokenelement.py @@ -23,10 +23,9 @@ def __init__(self, element): cell = element.cell degree = element.degree() quad_scheme = element.quadrature_scheme() - value_shape = element.value_shape reference_value_shape = element.reference_value_shape FiniteElementBase.__init__(self, family, cell, degree, - quad_scheme, value_shape, reference_value_shape) + quad_scheme, reference_value_shape) def __repr__(self): """Doc.""" diff --git a/finat/ufl/elementlist.py b/finat/ufl/elementlist.py index fd8e1f957..e07ae8f19 100644 --- a/finat/ufl/elementlist.py +++ b/finat/ufl/elementlist.py @@ -420,14 +420,12 @@ def canonical_element_description(family, cell, order, form_degree): # Get domain dimensions if cell is not None: tdim = cell.topological_dimension() - gdim = cell.geometric_dimension() if isinstance(cell, Cell): cellname = cell.cellname() else: cellname = None else: tdim = None - gdim = None cellname = None # Catch general FEEC notation "P" and "S" @@ -481,24 +479,21 @@ def canonical_element_description(family, cell, order, form_degree): if value_rank == 2: # Tensor valued fundamental elements in HEin have this shape - if gdim is None or tdim is None: + if tdim is None: raise ValueError("Cannot infer shape of element without topological and geometric dimensions.") reference_value_shape = (tdim, tdim) - value_shape = (gdim, gdim) elif value_rank == 1: # Vector valued fundamental elements in HDiv and HCurl have a shape - if gdim is None or tdim is None: + if tdim is None: raise ValueError("Cannot infer shape of element without topological and geometric dimensions.") reference_value_shape = (tdim,) - value_shape = (gdim,) elif value_rank == 0: # All other elements are scalar values reference_value_shape = () - value_shape = () else: raise ValueError(f"Invalid value rank {value_rank}.") embedded_degree = order if any(bubble in family for bubble in ("Guzman-Neilan", "Bernardi-Raugel")): embedded_degree = tdim - return family, short_name, order, value_shape, reference_value_shape, sobolev_space, mapping, embedded_degree + return family, short_name, order, reference_value_shape, sobolev_space, mapping, embedded_degree diff --git a/finat/ufl/enrichedelement.py b/finat/ufl/enrichedelement.py index 2c836d10b..a054885bb 100644 --- a/finat/ufl/enrichedelement.py +++ b/finat/ufl/enrichedelement.py @@ -39,10 +39,6 @@ def __init__(self, *elements): if not all(qs == quad_scheme for qs in quad_schemes): raise ValueError("Quadrature scheme mismatch.") - value_shape = elements[0].value_shape - if not all(e.value_shape == value_shape for e in elements[1:]): - raise ValueError("Element value shape mismatch.") - reference_value_shape = elements[0].reference_value_shape if not all(e.reference_value_shape == reference_value_shape for e in elements[1:]): raise ValueError("Element reference value shape mismatch.") @@ -56,8 +52,7 @@ def __init__(self, *elements): # Initialize element data FiniteElementBase.__init__(self, class_name, cell, degree, - quad_scheme, value_shape, - reference_value_shape) + quad_scheme, reference_value_shape) def mapping(self): """Doc.""" diff --git a/finat/ufl/finiteelement.py b/finat/ufl/finiteelement.py index 97e51d6b1..36c3d9221 100644 --- a/finat/ufl/finiteelement.py +++ b/finat/ufl/finiteelement.py @@ -41,7 +41,7 @@ def __new__(cls, from finat.ufl.hdivcurl import HDivElement as HDiv from finat.ufl.tensorproductelement import TensorProductElement - family, short_name, degree, value_shape, reference_value_shape, sobolev_space, mapping, embedded_degree = \ + family, short_name, degree, reference_value_shape, sobolev_space, mapping, embedded_degree = \ canonical_element_description(family, cell, degree, form_degree) if family in ["RTCF", "RTCE"]: @@ -141,7 +141,7 @@ def __init__(self, cell = as_cell(cell) ( - family, short_name, degree, value_shape, reference_value_shape, sobolev_space, mapping, embedded_degree + family, short_name, degree, reference_value_shape, sobolev_space, mapping, embedded_degree ) = canonical_element_description(family, cell, degree, form_degree) # TODO: Move these to base? Might be better to instead @@ -158,7 +158,7 @@ def __init__(self, # Initialize element data FiniteElementBase.__init__(self, family, cell, degree, quad_scheme, - value_shape, reference_value_shape) + reference_value_shape) # Cache repr string qs = self.quadrature_scheme() diff --git a/finat/ufl/finiteelementbase.py b/finat/ufl/finiteelementbase.py index 97ea88a6f..73307f565 100644 --- a/finat/ufl/finiteelementbase.py +++ b/finat/ufl/finiteelementbase.py @@ -23,17 +23,15 @@ class FiniteElementBase(AbstractFiniteElement): """Base class for all finite elements.""" __slots__ = ("_family", "_cell", "_degree", "_quad_scheme", - "_value_shape", "_reference_value_shape") + "_reference_value_shape") # TODO: Not all these should be in the base class! In particular # family, degree, and quad_scheme do not belong here. - def __init__(self, family, cell, degree, quad_scheme, value_shape, + def __init__(self, family, cell, degree, quad_scheme, reference_value_shape): """Initialize basic finite element data.""" if not (degree is None or isinstance(degree, (int, tuple))): raise ValueError("Invalid degree type.") - if not isinstance(value_shape, tuple): - raise ValueError("Invalid value_shape type.") if not isinstance(reference_value_shape, tuple): raise ValueError("Invalid reference_value_shape type.") @@ -45,7 +43,6 @@ def __init__(self, family, cell, degree, quad_scheme, value_shape, self._family = family self._cell = cell self._degree = degree - self._value_shape = value_shape self._reference_value_shape = reference_value_shape self._quad_scheme = quad_scheme @@ -124,21 +121,11 @@ def is_cellwise_constant(self, component=None): """Return whether the basis functions of this element is spatially constant over each cell.""" return self._is_globally_constant() or self.degree() == 0 - @property - def value_shape(self): - """Return the shape of the value space on the global domain.""" - return self._value_shape - @property def reference_value_shape(self): """Return the shape of the value space on the reference cell.""" return self._reference_value_shape - @property - def value_size(self): - """Return the integer product of the value shape.""" - return product(self.value_shape) - @property def reference_value_size(self): """Return the integer product of the reference value shape.""" @@ -154,35 +141,35 @@ def symmetry(self): # FIXME: different approach """ return {} - def _check_component(self, i): + def _check_component(self, domain, i): """Check that component index i is valid.""" - sh = self.value_shape + sh = self.value_shape(domain.geometric_dimension()) r = len(sh) if not (len(i) == r and all(j < k for (j, k) in zip(i, sh))): raise ValueError( f"Illegal component index {i} (value rank {len(i)}) " f"for element (value rank {r}).") - def extract_subelement_component(self, i): + def extract_subelement_component(self, domain, i): """Extract direct subelement index and subelement relative component index for a given component index.""" if isinstance(i, int): i = (i,) - self._check_component(i) + self._check_component(domain, i) return (None, i) - def extract_component(self, i): + def extract_component(self, domain, i): """Recursively extract component index relative to a (simple) element. and that element for given value component index. """ if isinstance(i, int): i = (i,) - self._check_component(i) + self._check_component(domain, i) return (i, self) def _check_reference_component(self, i): """Check that reference component index i is valid.""" - sh = self.value_shape + sh = self.reference_value_shape r = len(sh) if not (len(i) == r and all(j < k for (j, k) in zip(i, sh))): raise ValueError( diff --git a/finat/ufl/hdivcurl.py b/finat/ufl/hdivcurl.py index 45ef44e79..ed188038e 100644 --- a/finat/ufl/hdivcurl.py +++ b/finat/ufl/hdivcurl.py @@ -47,12 +47,11 @@ def __init__(self, element): cell = element.cell degree = element.degree() quad_scheme = element.quadrature_scheme() - value_shape = (element.cell.geometric_dimension(),) reference_value_shape = (element.cell.topological_dimension(),) # Skipping TensorProductElement constructor! Bad code smell, refactor to avoid this non-inheritance somehow. FiniteElementBase.__init__(self, family, cell, degree, - quad_scheme, value_shape, reference_value_shape) + quad_scheme, reference_value_shape) def __repr__(self): """Doc.""" @@ -107,12 +106,11 @@ def __init__(self, element): degree = element.degree() quad_scheme = element.quadrature_scheme() cell = element.cell - value_shape = (cell.geometric_dimension(),) reference_value_shape = (cell.topological_dimension(),) # TODO: Is this right? # Skipping TensorProductElement constructor! Bad code smell, # refactor to avoid this non-inheritance somehow. FiniteElementBase.__init__(self, family, cell, degree, quad_scheme, - value_shape, reference_value_shape) + reference_value_shape) def __repr__(self): """Doc.""" @@ -182,17 +180,16 @@ def __repr__(self): """Doc.""" return f"WithMapping({repr(self.wrapee)}, '{self._mapping}')" - @property - def value_shape(self): + def value_shape(self, domain): """Doc.""" - gdim = self.cell.geometric_dimension() + gdim = domain.geometric_dimension() mapping = self.mapping() if mapping in {"covariant Piola", "contravariant Piola"}: return (gdim,) elif mapping in {"double covariant Piola", "double contravariant Piola"}: return (gdim, gdim) else: - return self.wrapee.value_shape + return self.wrapee.value_shape(domain) @property def reference_value_shape(self): diff --git a/finat/ufl/mixedelement.py b/finat/ufl/mixedelement.py index a5fc54a91..e1dd1ccd5 100644 --- a/finat/ufl/mixedelement.py +++ b/finat/ufl/mixedelement.py @@ -17,7 +17,7 @@ from finat.ufl.finiteelement import FiniteElement from finat.ufl.finiteelementbase import FiniteElementBase from ufl.permutation import compute_indices -from ufl.pullback import IdentityPullback, MixedPullback, SymmetricPullback +from ufl.pullback import MixedPullback, SymmetricPullback from ufl.utils.indexflattening import flatten_multiindex, shape_to_strides, unflatten_index from ufl.utils.sequences import max_degree, product @@ -61,31 +61,17 @@ def __init__(self, *elements, **kwargs): raise ValueError("Quadrature scheme mismatch for sub elements of mixed element.") # Compute value sizes in global and reference configurations - value_size_sum = sum(product(s.value_shape) for s in self._sub_elements) reference_value_size_sum = sum(product(s.reference_value_shape) for s in self._sub_elements) - # Default value shape: Treated simply as all subelement values - # unpacked in a vector. - value_shape = kwargs.get('value_shape', (value_size_sum,)) - # Default reference value shape: Treated simply as all # subelement reference values unpacked in a vector. reference_value_shape = kwargs.get('reference_value_shape', (reference_value_size_sum,)) - # Validate value_shape (deliberately not for subclasses - # VectorElement and TensorElement) - if type(self) is MixedElement: - # This is not valid for tensor elements with symmetries, - # assume subclasses deal with their own validation - if product(value_shape) != value_size_sum: - raise ValueError("Provided value_shape doesn't match the " - "total value size of all subelements.") - # Initialize element data degrees = {e.degree() for e in self._sub_elements} - {None} degree = max_degree(degrees) if degrees else None FiniteElementBase.__init__(self, "Mixed", cell, degree, quad_scheme, - value_shape, reference_value_shape) + reference_value_shape) def __repr__(self): """Doc.""" @@ -101,7 +87,7 @@ def reconstruct_from_elements(self, *elements): return self return MixedElement(*elements) - def symmetry(self): + def symmetry(self, domain): r"""Return the symmetry dict, which is a mapping :math:`c_0 \\to c_1`. meaning that component :math:`c_0` is represented by component @@ -113,7 +99,7 @@ def symmetry(self): # Base index of the current subelement into mixed value j = 0 for e in self._sub_elements: - sh = e.value_shape + sh = e.value_shape(domain) st = shape_to_strides(sh) # Map symmetries of subelement into index space of this # element @@ -123,7 +109,7 @@ def symmetry(self): sm[(j0,)] = (j1,) # Update base index for next element j += product(sh) - if j != product(self.value_shape): + if j != product(self.value_shape(domain)): raise ValueError("Size mismatch in symmetry algorithm.") return sm or {} @@ -149,7 +135,7 @@ def sub_elements(self): """Return list of sub elements.""" return self._sub_elements - def extract_subelement_component(self, i): + def extract_subelement_component(self, domain, i): """Extract direct subelement index and subelement relative. component index for a given component index. @@ -159,14 +145,14 @@ def extract_subelement_component(self, i): self._check_component(i) # Select between indexing modes - if len(self.value_shape) == 1: + if len(self.value_shape(domain)) == 1: # Indexing into a long vector of flattened subelement # shapes j, = i # Find subelement for this index for sub_element_index, e in enumerate(self._sub_elements): - sh = e.value_shape + sh = e.value_shape(domain) si = product(sh) if j < si: break @@ -282,10 +268,7 @@ def shortstr(self): @property def pullback(self): """Get the pull back.""" - for e in self.sub_elements: - if not isinstance(e.pullback, IdentityPullback): - return MixedPullback(self) - return IdentityPullback() + return MixedPullback(self) class VectorElement(MixedElement): @@ -313,22 +296,22 @@ def __init__(self, family, cell=None, degree=None, dim=None, if dim is None: if cell is None: raise ValueError("Cannot infer vector dimension without a cell.") - dim = cell.geometric_dimension() + # TODO: is this the right default + dim = cell.topological_dimension() self._mapping = sub_element.mapping() # Create list of sub elements for mixed element constructor sub_elements = [sub_element] * dim # Compute value shapes - value_shape = (dim,) + sub_element.value_shape reference_value_shape = (dim,) + sub_element.reference_value_shape # Initialize element data - MixedElement.__init__(self, sub_elements, value_shape=value_shape, + MixedElement.__init__(self, sub_elements, reference_value_shape=reference_value_shape) FiniteElementBase.__init__(self, sub_element.family(), sub_element.cell, sub_element.degree(), - sub_element.quadrature_scheme(), value_shape, reference_value_shape) + sub_element.quadrature_scheme(), reference_value_shape) self._sub_element = sub_element @@ -367,6 +350,11 @@ def shortstr(self): return "Vector<%d x %s>" % (len(self._sub_elements), self._sub_element.shortstr()) + @property + def pullback(self): + """Get the pull back.""" + return self._sub_element.pullback + class TensorElement(MixedElement): """A special case of a mixed finite element where all elements are equal.""" @@ -393,7 +381,8 @@ def __init__(self, family, cell=None, degree=None, shape=None, if shape is None: if cell is None: raise ValueError("Cannot infer tensor shape without a cell.") - dim = cell.geometric_dimension() + # TODO: is this the right default + dim = cell.topological_dimension() shape = (dim, dim) if symmetry is None: @@ -436,9 +425,6 @@ def __init__(self, family, cell=None, degree=None, shape=None, flattened_sub_element_mapping = [sub_element_mapping[index] for i, index in enumerate(indices)] - # Compute value shape - value_shape = shape - # Compute reference value shape based on symmetries if symmetry: reference_value_shape = (product(shape) - len(symmetry),) @@ -447,10 +433,9 @@ def __init__(self, family, cell=None, degree=None, shape=None, reference_value_shape = shape self._mapping = sub_element.mapping() - value_shape = value_shape + sub_element.value_shape reference_value_shape = reference_value_shape + sub_element.reference_value_shape # Initialize element data - MixedElement.__init__(self, sub_elements, value_shape=value_shape, + MixedElement.__init__(self, sub_elements, reference_value_shape=reference_value_shape) self._family = sub_element.family() self._degree = sub_element.degree() @@ -473,20 +458,21 @@ def __init__(self, family, cell=None, degree=None, shape=None, def pullback(self): """Get pull back.""" if len(self._symmetry) > 0: - sub_element_value_shape = self.sub_elements[0].value_shape + sub_element_value_shape = self.sub_elements[0].reference_value_shape for e in self.sub_elements: - if e.value_shape != sub_element_value_shape: + if e.reference_value_shape != sub_element_value_shape: raise ValueError("Sub-elements must all have the same value size") symmetry = {} n = 0 - for i in np.ndindex(self.value_shape[:len(self.value_shape)-len(sub_element_value_shape)]): + for i in np.ndindex(self._shape): if i in self._symmetry and self._symmetry[i] in symmetry: symmetry[i] = symmetry[self._symmetry[i]] else: symmetry[i] = n n += 1 return SymmetricPullback(self, symmetry) - return super().pullback + + return self._sub_element.pullback def __repr__(self): """Doc.""" @@ -544,7 +530,7 @@ def __str__(self): else: sym = "" return ("" % - (self.value_shape, self._sub_element, sym)) + (self.reference_value_shape, self._sub_element, sym)) def shortstr(self): """Format as string for pretty printing.""" @@ -553,5 +539,5 @@ def shortstr(self): sym = " with symmetries (%s)" % tmp else: sym = "" - return "Tensor<%s x %s%s>" % (self.value_shape, + return "Tensor<%s x %s%s>" % (self.reference_value_shape, self._sub_element.shortstr(), sym) diff --git a/finat/ufl/restrictedelement.py b/finat/ufl/restrictedelement.py index 2c302ab55..797cca22c 100644 --- a/finat/ufl/restrictedelement.py +++ b/finat/ufl/restrictedelement.py @@ -30,7 +30,6 @@ def __init__(self, element, restriction_domain): FiniteElementBase.__init__(self, "RestrictedElement", element.cell, element.degree(), element.quadrature_scheme(), - element.value_shape, element.reference_value_shape) self._element = element diff --git a/finat/ufl/tensorproductelement.py b/finat/ufl/tensorproductelement.py index de946dadf..760626f87 100644 --- a/finat/ufl/tensorproductelement.py +++ b/finat/ufl/tensorproductelement.py @@ -54,16 +54,12 @@ def __init__(self, *elements, **kwargs): quad_scheme = None # match FIAT implementation - value_shape = tuple(chain(*[e.value_shape for e in elements])) reference_value_shape = tuple(chain(*[e.reference_value_shape for e in elements])) - if len(value_shape) > 1: - raise ValueError("Product of vector-valued elements not supported") if len(reference_value_shape) > 1: raise ValueError("Product of vector-valued elements not supported") FiniteElementBase.__init__(self, family, cell, degree, - quad_scheme, value_shape, - reference_value_shape) + quad_scheme, reference_value_shape) self._sub_elements = elements self._cell = cell @@ -92,7 +88,8 @@ def sobolev_space(self): # continuity information parametrized by spatial index orders = [] for e in elements: - e_dim = e.cell.geometric_dimension() + # TODO: is this the right value for e_dim + e_dim = e.cell.topological_dimension() e_order = (e.sobolev_space._order,) * e_dim orders.extend(e_order) return DirectionalSobolevSpace(orders) From 50bc36fd4262daba2e3e7cddcfbd4c92c2abdf69 Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Tue, 19 Nov 2024 18:12:47 +0000 Subject: [PATCH 746/749] Allow HHJ in 3D (#142) Co-authored-by: Francis Aznaran --- finat/__init__.py | 3 + finat/argyris.py | 123 ++++++++------- finat/aw.py | 89 ++++------- finat/bell.py | 74 ++++----- finat/fiat_elements.py | 18 ++- finat/hct.py | 72 ++++----- finat/hz.py | 51 +++++++ finat/mtw.py | 69 ++++----- finat/physically_mapped.py | 10 ++ finat/piola_mapped.py | 1 - finat/ufl/elementlist.py | 119 ++++++--------- finat/ufl/finiteelementbase.py | 2 + test/test_zany_mapping.py | 265 +++++++++++++++------------------ 13 files changed, 432 insertions(+), 464 deletions(-) create mode 100644 finat/hz.py diff --git a/finat/__init__.py b/finat/__init__.py index e7a993de7..cad018212 100644 --- a/finat/__init__.py +++ b/finat/__init__.py @@ -8,12 +8,15 @@ from .fiat_elements import BrezziDouglasMarini, BrezziDouglasFortinMarini # noqa: F401 from .fiat_elements import Nedelec, NedelecSecondKind, RaviartThomas # noqa: F401 from .fiat_elements import HellanHerrmannJohnson, Regge # noqa: F401 +from .fiat_elements import GopalakrishnanLedererSchoberlFirstKind # noqa: F401 +from .fiat_elements import GopalakrishnanLedererSchoberlSecondKind # noqa: F401 from .fiat_elements import FacetBubble # noqa: F401 from .fiat_elements import KongMulderVeldhuizen # noqa: F401 from .argyris import Argyris # noqa: F401 from .aw import ArnoldWinther # noqa: F401 from .aw import ArnoldWintherNC # noqa: F401 +from .hz import HuZhang # noqa: F401 from .bell import Bell # noqa: F401 from .bernardi_raugel import BernardiRaugel, BernardiRaugelBubble # noqa: F401 from .hct import HsiehCloughTocher, ReducedHsiehCloughTocher # noqa: F401 diff --git a/finat/argyris.py b/finat/argyris.py index 3186ad7ee..2563d99c1 100644 --- a/finat/argyris.py +++ b/finat/argyris.py @@ -3,12 +3,61 @@ import FIAT -from gem import Literal, ListTensor, partial_indexed +from gem import Literal, ListTensor from finat.fiat_elements import ScalarFiatElement from finat.physically_mapped import PhysicallyMappedElement, Citations +def _vertex_transform(V, vorder, fiat_cell, coordinate_mapping): + """Basis transformation for evaluation, gradient, and hessian at vertices.""" + sd = fiat_cell.get_spatial_dimension() + top = fiat_cell.get_topology() + bary, = fiat_cell.make_points(sd, 0, sd+1) + J = coordinate_mapping.jacobian_at(bary) + + gdofs = sd + G = [[J[j, i] for j in range(sd)] for i in range(sd)] + + if vorder < 2: + hdofs = 0 + H = [[]] + else: + hdofs = (sd*(sd+1))//2 + indices = [(i, j) for i in range(sd) for j in range(i, sd)] + H = numpy.zeros((hdofs, hdofs), dtype=object) + for p, (i, j) in enumerate(indices): + for q, (m, n) in enumerate(indices): + H[p, q] = J[m, i] * J[n, j] + J[m, j] * J[n, i] + H[:, [i == j for i, j in indices]] *= 0.5 + + s = 0 + for v in sorted(top[0]): + s += 1 + V[s:s+gdofs, s:s+gdofs] = G + s += gdofs + V[s:s+hdofs, s:s+hdofs] = H + s += hdofs + return V + + +def _normal_tangential_transform(fiat_cell, J, detJ, f): + R = numpy.array([[0, 1], [-1, 0]]) + that = fiat_cell.compute_edge_tangent(f) + nhat = R @ that + Jn = J @ Literal(nhat) + Jt = J @ Literal(that) + alpha = Jn @ Jt + beta = Jt @ Jt + Bnn = detJ / beta + Bnt = alpha / beta + + Lhat = numpy.linalg.norm(that) + Bnn = Bnn * Lhat + Bnt = Bnt / Lhat + return Bnn, Bnt, Jt + + def _edge_transform(V, vorder, eorder, fiat_cell, coordinate_mapping, avg=False): """Basis transformation for integral edge moments. @@ -21,10 +70,9 @@ def _edge_transform(V, vorder, eorder, fiat_cell, coordinate_mapping, avg=False) :kwarg avg: are we scaling integrals by dividing by the edge length? """ sd = fiat_cell.get_spatial_dimension() - J = coordinate_mapping.jacobian_at(fiat_cell.make_points(sd, 0, sd+1)[0]) - rns = coordinate_mapping.reference_normals() - pts = coordinate_mapping.physical_tangents() - pns = coordinate_mapping.physical_normals() + bary, = fiat_cell.make_points(sd, 0, sd+1) + J = coordinate_mapping.jacobian_at(bary) + detJ = coordinate_mapping.detJ_at(bary) pel = coordinate_mapping.physical_edge_lengths() # number of DOFs per vertex/edge @@ -32,12 +80,7 @@ def _edge_transform(V, vorder, eorder, fiat_cell, coordinate_mapping, avg=False) eoffset = 2 * eorder + 1 top = fiat_cell.get_topology() for e in sorted(top[1]): - nhat = partial_indexed(rns, (e, )) - n = partial_indexed(pns, (e, )) - t = partial_indexed(pts, (e, )) - Bn = J @ nhat / pel[e] - Bnt = Bn @ t - Bnn = Bn @ n + Bnn, Bnt, Jt = _normal_tangential_transform(fiat_cell, J, detJ, e) if avg: Bnn = Bnn * pel[e] @@ -69,75 +112,55 @@ def __init__(self, cell, degree=5, variant=None, avg=False): super().__init__(fiat_element) def basis_transformation(self, coordinate_mapping): - # Jacobian at barycenter - J = coordinate_mapping.jacobian_at([1/3, 1/3]) + sd = self.cell.get_spatial_dimension() + top = self.cell.get_topology() ndof = self.space_dimension() V = numpy.eye(ndof, dtype=object) for multiindex in numpy.ndindex(V.shape): V[multiindex] = Literal(V[multiindex]) - sd = self.cell.get_spatial_dimension() - top = self.cell.get_topology() vorder = 2 voffset = comb(sd + vorder, vorder) - for v in sorted(top[0]): - s = voffset * v - for i in range(sd): - for j in range(sd): - V[s+1+i, s+1+j] = J[j, i] - V[s+3, s+3] = J[0, 0]*J[0, 0] - V[s+3, s+4] = 2*J[0, 0]*J[1, 0] - V[s+3, s+5] = J[1, 0]*J[1, 0] - V[s+4, s+3] = J[0, 0]*J[0, 1] - V[s+4, s+4] = J[0, 0]*J[1, 1] + J[1, 0]*J[0, 1] - V[s+4, s+5] = J[1, 0]*J[1, 1] - V[s+5, s+3] = J[0, 1]*J[0, 1] - V[s+5, s+4] = 2*J[0, 1]*J[1, 1] - V[s+5, s+5] = J[1, 1]*J[1, 1] - eorder = self.degree - 5 + + _vertex_transform(V, vorder, self.cell, coordinate_mapping) if self.variant == "integral": _edge_transform(V, vorder, eorder, self.cell, coordinate_mapping, avg=self.avg) else: - rns = coordinate_mapping.reference_normals() - pns = coordinate_mapping.physical_normals() - pts = coordinate_mapping.physical_tangents() + bary, = self.cell.make_points(sd, 0, sd+1) + J = coordinate_mapping.jacobian_at(bary) + detJ = coordinate_mapping.detJ_at(bary) pel = coordinate_mapping.physical_edge_lengths() for e in sorted(top[1]): - nhat = partial_indexed(rns, (e, )) - n = partial_indexed(pns, (e, )) - t = partial_indexed(pts, (e, )) - Bn = J @ nhat - Bnt = Bn @ t - Bnn = Bn @ n - - s = len(top[0]) * voffset + e + s = len(top[0]) * voffset + e * (eorder+1) v0id, v1id = (v * voffset for v in top[1][e]) - V[s, s] = Bnn + Bnn, Bnt, Jt = _normal_tangential_transform(self.cell, J, detJ, e) + + # edge midpoint normal derivative + V[s, s] = Bnn * pel[e] # vertex points - V[s, v1id] = 15/8 * Bnt / pel[e] + V[s, v1id] = 15/8 * Bnt V[s, v0id] = -1 * V[s, v1id] # vertex derivatives for i in range(sd): - V[s, v1id+1+i] = -7/16 * Bnt * t[i] + V[s, v1id+1+i] = -7/16 * Bnt * Jt[i] V[s, v0id+1+i] = V[s, v1id+1+i] # second derivatives - tau = [t[0]*t[0], 2*t[0]*t[1], t[1]*t[1]] + tau = [Jt[0]*Jt[0], 2*Jt[0]*Jt[1], Jt[1]*Jt[1]] for i in range(len(tau)): - V[s, v1id+3+i] = 1/32 * (pel[e] * Bnt * tau[i]) + V[s, v1id+3+i] = 1/32 * Bnt * tau[i] V[s, v0id+3+i] = -1 * V[s, v1id+3+i] # Patch up conditioning h = coordinate_mapping.cell_size() for v in sorted(top[0]): - for k in range(sd): - V[:, voffset*v+1+k] *= 1 / h[v] - for k in range((sd+1)*sd//2): - V[:, voffset*v+3+k] *= 1 / (h[v]*h[v]) + s = voffset*v + 1 + V[:, s:s+sd] *= 1 / h[v] + V[:, s+sd:voffset*(v+1)] *= 1 / (h[v]*h[v]) if self.variant == "point": eoffset = 2 * eorder + 1 diff --git a/finat/aw.py b/finat/aw.py index 34d482f41..c02195814 100644 --- a/finat/aw.py +++ b/finat/aw.py @@ -5,7 +5,7 @@ from finat.fiat_elements import FiatElement from finat.physically_mapped import Citations, PhysicallyMappedElement -from finat.piola_mapped import normal_tangential_edge_transform, normal_tangential_face_transform +from finat.piola_mapped import adjugate, normal_tangential_edge_transform, normal_tangential_face_transform def _facet_transform(fiat_cell, facet_moment_degree, coordinate_mapping): @@ -41,18 +41,15 @@ def _evaluation_transform(fiat_cell, coordinate_mapping): sd = fiat_cell.get_spatial_dimension() bary, = fiat_cell.make_points(sd, 0, sd+1) J = coordinate_mapping.jacobian_at(bary) - - W = numpy.zeros((3, 3), dtype=object) - W[0, 0] = J[1, 1]*J[1, 1] - W[0, 1] = -2*J[1, 1]*J[0, 1] - W[0, 2] = J[0, 1]*J[0, 1] - W[1, 0] = -1*J[1, 1]*J[1, 0] - W[1, 1] = J[1, 1]*J[0, 0] + J[0, 1]*J[1, 0] - W[1, 2] = -1*J[0, 1]*J[0, 0] - W[2, 0] = J[1, 0]*J[1, 0] - W[2, 1] = -2*J[1, 0]*J[0, 0] - W[2, 2] = J[0, 0]*J[0, 0] - + K = adjugate([[J[i, j] for j in range(sd)] for i in range(sd)]) + + indices = [(i, j) for i in range(sd) for j in range(i, sd)] + ncomp = len(indices) + W = numpy.zeros((ncomp, ncomp), dtype=object) + for p, (i, j) in enumerate(indices): + for q, (m, n) in enumerate(indices): + W[p, q] = 0.5*(K[i, m] * K[j, n] + K[j, m] * K[i, n]) + W[:, [i != j for i, j in indices]] *= 2 return W @@ -65,18 +62,14 @@ def __init__(self, cell, degree=2): def basis_transformation(self, coordinate_mapping): """Note, the extra 3 dofs which are removed here correspond to the constraints.""" - V = numpy.zeros((18, 15), dtype=object) + numbf = self._element.space_dimension() + ndof = self.space_dimension() + V = numpy.eye(numbf, ndof, dtype=object) for multiindex in numpy.ndindex(V.shape): V[multiindex] = Literal(V[multiindex]) V[:12, :12] = _facet_transform(self.cell, 1, coordinate_mapping) - # internal dofs - W = _evaluation_transform(self.cell, coordinate_mapping) - detJ = coordinate_mapping.detJ_at([1/3, 1/3]) - - V[12:15, 12:15] = W / detJ - # Note: that the edge DOFs are scaled by edge lengths in FIAT implies # that they are already have the necessary rescaling to improve # conditioning. @@ -90,16 +83,9 @@ def entity_dofs(self): 1: {0: [0, 1, 2, 3], 1: [4, 5, 6, 7], 2: [8, 9, 10, 11]}, 2: {0: [12, 13, 14]}} - def entity_closure_dofs(self): - return {0: {0: [], - 1: [], - 2: []}, - 1: {0: [0, 1, 2, 3], 1: [4, 5, 6, 7], 2: [8, 9, 10, 11]}, - 2: {0: list(range(15))}} - @property def index_shape(self): - return (15,) + return (self.space_dimension(),) def space_dimension(self): return 15 @@ -112,37 +98,35 @@ def __init__(self, cell, degree=3): super().__init__(FIAT.ArnoldWinther(cell, degree)) def basis_transformation(self, coordinate_mapping): - """The extra 6 dofs removed here correspond to the constraints.""" - V = numpy.zeros((30, 24), dtype=object) - + # The extra 6 dofs removed here correspond to the constraints + numbf = self._element.space_dimension() + ndof = self.space_dimension() + V = numpy.eye(numbf, ndof, dtype=object) for multiindex in numpy.ndindex(V.shape): V[multiindex] = Literal(V[multiindex]) + sd = self.cell.get_spatial_dimension() W = _evaluation_transform(self.cell, coordinate_mapping) + ncomp = W.shape[0] # Put into the right rows and columns. V[0:3, 0:3] = V[3:6, 3:6] = V[6:9, 6:9] = W + num_verts = sd + 1 + cur = num_verts * ncomp - V[9:21, 9:21] = _facet_transform(self.cell, 1, coordinate_mapping) - - # internal DOFs - detJ = coordinate_mapping.detJ_at([1/3, 1/3]) - V[21:24, 21:24] = W / detJ + Vsub = _facet_transform(self.cell, 1, coordinate_mapping) + fdofs = Vsub.shape[0] + V[cur:cur+fdofs, cur:cur+fdofs] = Vsub + cur += fdofs # RESCALING FOR CONDITIONING h = coordinate_mapping.cell_size() - - for e in range(3): - eff_h = h[e] - for i in range(24): - V[i, 3*e] = V[i, 3*e]/(eff_h*eff_h) - V[i, 1+3*e] = V[i, 1+3*e]/(eff_h*eff_h) - V[i, 2+3*e] = V[i, 2+3*e]/(eff_h*eff_h) + for e in range(num_verts): + V[:, ncomp*e:ncomp*(e+1)] *= 1/(h[e] * h[e]) # Note: that the edge DOFs are scaled by edge lengths in FIAT implies # that they are already have the necessary rescaling to improve # conditioning. - return ListTensor(V.T) def entity_dofs(self): @@ -152,24 +136,9 @@ def entity_dofs(self): 1: {0: [9, 10, 11, 12], 1: [13, 14, 15, 16], 2: [17, 18, 19, 20]}, 2: {0: [21, 22, 23]}} - # need to overload since we're cutting out some dofs from the FIAT element. - def entity_closure_dofs(self): - ct = self.cell.topology - ecdofs = {i: {} for i in range(3)} - for i in range(3): - ecdofs[0][i] = list(range(3*i, 3*(i+1))) - - for i in range(3): - ecdofs[1][i] = [dof for v in ct[1][i] for dof in ecdofs[0][v]] + \ - list(range(9+4*i, 9+4*(i+1))) - - ecdofs[2][0] = list(range(24)) - - return ecdofs - @property def index_shape(self): - return (24,) + return (self.space_dimension(),) def space_dimension(self): return 24 diff --git a/finat/bell.py b/finat/bell.py index 6dbd564c2..d13a40c42 100644 --- a/finat/bell.py +++ b/finat/bell.py @@ -1,11 +1,12 @@ -import numpy - import FIAT - -from gem import Literal, ListTensor, partial_indexed +import numpy +from math import comb +from gem import Literal, ListTensor from finat.fiat_elements import ScalarFiatElement from finat.physically_mapped import PhysicallyMappedElement, Citations +from finat.argyris import _vertex_transform, _normal_tangential_transform +from copy import deepcopy class Bell(PhysicallyMappedElement, ScalarFiatElement): @@ -14,9 +15,19 @@ def __init__(self, cell, degree=5): Citations().register("Bell1969") super().__init__(FIAT.Bell(cell)) + reduced_dofs = deepcopy(self._element.entity_dofs()) + sd = cell.get_spatial_dimension() + for entity in reduced_dofs[sd-1]: + reduced_dofs[sd-1][entity] = [] + self._entity_dofs = reduced_dofs + def basis_transformation(self, coordinate_mapping): - # Jacobians at edge midpoints - J = coordinate_mapping.jacobian_at([1/3, 1/3]) + # Jacobian at barycenter + sd = self.cell.get_spatial_dimension() + top = self.cell.get_topology() + bary, = self.cell.make_points(sd, 0, sd+1) + J = coordinate_mapping.jacobian_at(bary) + detJ = coordinate_mapping.detJ_at(bary) numbf = self._element.space_dimension() ndof = self.space_dimension() @@ -25,57 +36,36 @@ def basis_transformation(self, coordinate_mapping): for multiindex in numpy.ndindex(V.shape): V[multiindex] = Literal(V[multiindex]) - sd = self.cell.get_spatial_dimension() - top = self.cell.get_topology() - voffset = sd + 1 + (sd*(sd+1))//2 - for v in sorted(top[1]): - s = voffset * v - for i in range(sd): - for j in range(sd): - V[s+1+i, s+1+j] = J[j, i] - V[s+3, s+3] = J[0, 0]*J[0, 0] - V[s+3, s+4] = 2*J[0, 0]*J[1, 0] - V[s+3, s+5] = J[1, 0]*J[1, 0] - V[s+4, s+3] = J[0, 0]*J[0, 1] - V[s+4, s+4] = J[0, 0]*J[1, 1] + J[1, 0]*J[0, 1] - V[s+4, s+5] = J[1, 0]*J[1, 1] - V[s+5, s+3] = J[0, 1]*J[0, 1] - V[s+5, s+4] = 2*J[0, 1]*J[1, 1] - V[s+5, s+5] = J[1, 1]*J[1, 1] - - rns = coordinate_mapping.reference_normals() - pts = coordinate_mapping.physical_tangents() - pel = coordinate_mapping.physical_edge_lengths() + vorder = 2 + _vertex_transform(V, vorder, self.cell, coordinate_mapping) + + voffset = comb(sd + vorder, vorder) for e in sorted(top[1]): s = len(top[0]) * voffset + e v0id, v1id = (v * voffset for v in top[1][e]) - - nhat = partial_indexed(rns, (e, )) - t = partial_indexed(pts, (e, )) - Bnt = (J @ nhat) @ t + Bnn, Bnt, Jt = _normal_tangential_transform(self.cell, J, detJ, e) # vertex points - V[s, v1id] = 1/21 * Bnt / pel[e] + V[s, v1id] = 1/21 * Bnt V[s, v0id] = -1 * V[s, v1id] # vertex derivatives for i in range(sd): - V[s, v1id+1+i] = -1/42 * Bnt * t[i] + V[s, v1id+1+i] = -1/42 * Bnt * Jt[i] V[s, v0id+1+i] = V[s, v1id+1+i] # second derivatives - tau = [t[0]*t[0], 2*t[0]*t[1], t[1]*t[1]] + tau = [Jt[0]*Jt[0], 2*Jt[0]*Jt[1], Jt[1]*Jt[1]] for i in range(len(tau)): - V[s, v1id+3+i] = 1/252 * (pel[e] * Bnt * tau[i]) + V[s, v1id+3+i] = 1/252 * Bnt * tau[i] V[s, v0id+3+i] = -1 * V[s, v1id+3+i] # Patch up conditioning h = coordinate_mapping.cell_size() for v in sorted(top[0]): - for k in range(sd): - V[:, voffset*v+1+k] *= 1/h[v] - for k in range((sd+1)*sd//2): - V[:, voffset*v+3+k] *= 1/(h[v]*h[v]) + s = voffset * v + 1 + V[:, s:s+sd] *= 1/h[v] + V[:, s+sd:voffset*(v+1)] *= 1/(h[v]*h[v]) return ListTensor(V.T) @@ -84,11 +74,7 @@ def basis_transformation(self, coordinate_mapping): # under the edge constraint. However, we only have an 18 DOF # element. def entity_dofs(self): - return {0: {0: list(range(6)), - 1: list(range(6, 12)), - 2: list(range(12, 18))}, - 1: {0: [], 1: [], 2: []}, - 2: {0: []}} + return self._entity_dofs @property def index_shape(self): diff --git a/finat/fiat_elements.py b/finat/fiat_elements.py index f8af03858..1f2081894 100644 --- a/finat/fiat_elements.py +++ b/finat/fiat_elements.py @@ -313,14 +313,24 @@ def point_evaluation(fiat_element, order, refcoords, entity): return result -class Regge(FiatElement): # naturally tensor valued - def __init__(self, cell, degree): - super().__init__(FIAT.Regge(cell, degree)) +class Regge(FiatElement): # symmetric matrix valued + def __init__(self, cell, degree, variant=None): + super().__init__(FIAT.Regge(cell, degree, variant=variant)) class HellanHerrmannJohnson(FiatElement): # symmetric matrix valued + def __init__(self, cell, degree, variant=None): + super().__init__(FIAT.HellanHerrmannJohnson(cell, degree, variant=variant)) + + +class GopalakrishnanLedererSchoberlFirstKind(FiatElement): # traceless matrix valued + def __init__(self, cell, degree): + super().__init__(FIAT.GopalakrishnanLedererSchoberlFirstKind(cell, degree)) + + +class GopalakrishnanLedererSchoberlSecondKind(FiatElement): # traceless matrix valued def __init__(self, cell, degree): - super().__init__(FIAT.HellanHerrmannJohnson(cell, degree)) + super().__init__(FIAT.GopalakrishnanLedererSchoberlSecondKind(cell, degree)) class ScalarFiatElement(FiatElement): diff --git a/finat/hct.py b/finat/hct.py index c3f66b270..86db3b45f 100644 --- a/finat/hct.py +++ b/finat/hct.py @@ -1,10 +1,11 @@ import FIAT import numpy -from gem import ListTensor, Literal, partial_indexed +from math import comb +from gem import ListTensor, Literal -from finat.argyris import _edge_transform from finat.fiat_elements import ScalarFiatElement from finat.physically_mapped import Citations, PhysicallyMappedElement +from finat.argyris import _vertex_transform, _edge_transform, _normal_tangential_transform from copy import deepcopy @@ -18,9 +19,6 @@ def __init__(self, cell, degree=3, avg=False): super().__init__(FIAT.HsiehCloughTocher(cell, degree)) def basis_transformation(self, coordinate_mapping): - # Jacobians at cell center - J = coordinate_mapping.jacobian_at([1/3, 1/3]) - ndof = self.space_dimension() V = numpy.eye(ndof, dtype=object) for multiindex in numpy.ndindex(V.shape): @@ -28,22 +26,18 @@ def basis_transformation(self, coordinate_mapping): sd = self.cell.get_dimension() top = self.cell.get_topology() - voffset = 1 + sd - for v in sorted(top[0]): - s = voffset * v - for i in range(sd): - for j in range(sd): - V[s+1+i, s+1+j] = J[j, i] vorder = 1 eorder = self.degree - 3 + voffset = comb(sd + vorder, vorder) + _vertex_transform(V, vorder, self.cell, coordinate_mapping) _edge_transform(V, vorder, eorder, self.cell, coordinate_mapping, avg=self.avg) # Patch up conditioning h = coordinate_mapping.cell_size() for v in sorted(top[0]): - for k in range(sd): - V[:, voffset*v+1+k] /= h[v] + s = voffset*v + 1 + V[:, s:s+sd] *= 1/h[v] return ListTensor(V.T) @@ -60,9 +54,8 @@ def __init__(self, cell, degree=3): self._entity_dofs = reduced_dofs def basis_transformation(self, coordinate_mapping): - # Jacobian at barycenter - J = coordinate_mapping.jacobian_at([1/3, 1/3]) - + sd = self.cell.get_spatial_dimension() + top = self.cell.get_topology() numbf = self._element.space_dimension() ndof = self.space_dimension() # rectangular to toss out the constraint dofs @@ -70,44 +63,39 @@ def basis_transformation(self, coordinate_mapping): for multiindex in numpy.ndindex(V.shape): V[multiindex] = Literal(V[multiindex]) - sd = self.cell.get_spatial_dimension() - top = self.cell.get_topology() - voffset = sd + 1 - for v in sorted(top[0]): - s = voffset * v - for i in range(sd): - for j in range(sd): - V[s+1+i, s+1+j] = J[j, i] - - rns = coordinate_mapping.reference_normals() - pts = coordinate_mapping.physical_tangents() - pel = coordinate_mapping.physical_edge_lengths() + vorder = 1 + voffset = comb(sd + vorder, vorder) + _vertex_transform(V, vorder, self.cell, coordinate_mapping) + # Jacobian at barycenter + bary, = self.cell.make_points(sd, 0, sd+1) + J = coordinate_mapping.jacobian_at(bary) + detJ = coordinate_mapping.detJ_at(bary) for e in sorted(top[1]): s = len(top[0]) * voffset + e v0id, v1id = (v * voffset for v in top[1][e]) + Bnn, Bnt, Jt = _normal_tangential_transform(self.cell, J, detJ, e) - nhat = partial_indexed(rns, (e, )) - t = partial_indexed(pts, (e, )) - Bnt = (J @ nhat) @ t + # vertex points + V[s, v0id] = 1/5 * Bnt + V[s, v1id] = -1 * V[s, v0id] - V[s, v0id] = Literal(1/5) * Bnt / pel[e] - V[s, v1id] = Literal(-1) * V[s, v0id] - - R = Literal(1/10) * Bnt * t - V[s, v0id + 1] = R[0] - V[s, v0id + 2] = R[1] - V[s, v1id + 1] = R[0] - V[s, v1id + 2] = R[1] + # vertex derivatives + for i in range(sd): + V[s, v1id+1+i] = 1/10 * Bnt * Jt[i] + V[s, v0id+1+i] = V[s, v1id+1+i] # Patch up conditioning h = coordinate_mapping.cell_size() for v in sorted(top[0]): - s = voffset * v - for k in range(sd): - V[:, s+1+k] /= h[v] + s = voffset * v + 1 + V[:, s:s+sd] *= 1/h[v] return ListTensor(V.T) + # This wipes out the edge dofs. FIAT gives a 12 DOF element + # because we need some extra functions to help with transforming + # under the edge constraint. However, we only have a 9 DOF + # element. def entity_dofs(self): return self._entity_dofs diff --git a/finat/hz.py b/finat/hz.py new file mode 100644 index 000000000..620fef61a --- /dev/null +++ b/finat/hz.py @@ -0,0 +1,51 @@ +"""Implementation of the Hu-Zhang finite elements.""" +import numpy +import FIAT +from gem import Literal, ListTensor +from finat.fiat_elements import FiatElement +from finat.physically_mapped import PhysicallyMappedElement, Citations +from finat.aw import _facet_transform, _evaluation_transform + + +class HuZhang(PhysicallyMappedElement, FiatElement): + def __init__(self, cell, degree=3, variant=None): + if Citations is not None: + Citations().register("Hu2015") + self.variant = variant + super().__init__(FIAT.HuZhang(cell, degree, variant=variant)) + + def basis_transformation(self, coordinate_mapping): + ndofs = self.space_dimension() + V = numpy.eye(ndofs, dtype=object) + for multiindex in numpy.ndindex(V.shape): + V[multiindex] = Literal(V[multiindex]) + + sd = self.cell.get_spatial_dimension() + W = _evaluation_transform(self.cell, coordinate_mapping) + + # Put into the right rows and columns. + V[0:3, 0:3] = V[3:6, 3:6] = V[6:9, 6:9] = W + ncomp = W.shape[0] + num_verts = sd+1 + cur = num_verts * ncomp + + Vsub = _facet_transform(self.cell, self.degree-2, coordinate_mapping) + fdofs = Vsub.shape[0] + V[cur:cur+fdofs, cur:cur+fdofs] = Vsub + cur += fdofs + + # internal DOFs + if self.variant == "point": + while cur < ndofs: + V[cur:cur+ncomp, cur:cur+ncomp] = W + cur += ncomp + + # RESCALING FOR CONDITIONING + h = coordinate_mapping.cell_size() + for e in range(num_verts): + V[:, ncomp*e:ncomp*(e+1)] *= 1/(h[e] * h[e]) + + # Note: that the edge DOFs are scaled by edge lengths in FIAT implies + # that they are already have the necessary rescaling to improve + # conditioning. + return ListTensor(V.T) diff --git a/finat/mtw.py b/finat/mtw.py index 1de6133eb..354f17af6 100644 --- a/finat/mtw.py +++ b/finat/mtw.py @@ -6,66 +6,49 @@ from finat.fiat_elements import FiatElement from finat.physically_mapped import PhysicallyMappedElement, Citations +from finat.piola_mapped import normal_tangential_edge_transform +from copy import deepcopy class MardalTaiWinther(PhysicallyMappedElement, FiatElement): def __init__(self, cell, degree=3): if Citations is not None: Citations().register("Mardal2002") - super(MardalTaiWinther, self).__init__(FIAT.MardalTaiWinther(cell, degree)) + super().__init__(FIAT.MardalTaiWinther(cell, degree)) - def basis_transformation(self, coordinate_mapping): - V = numpy.zeros((20, 9), dtype=object) + reduced_dofs = deepcopy(self._element.entity_dofs()) + sd = cell.get_spatial_dimension() + fdofs = sd + 1 + reduced_dofs[sd][0] = [] + for f in reduced_dofs[sd-1]: + reduced_dofs[sd-1][f] = reduced_dofs[sd-1][f][:fdofs] + self._entity_dofs = reduced_dofs + self._space_dimension = fdofs * len(reduced_dofs[sd-1]) + def basis_transformation(self, coordinate_mapping): + numbf = self._element.space_dimension() + ndof = self.space_dimension() + V = numpy.eye(numbf, ndof, dtype=object) for multiindex in numpy.ndindex(V.shape): V[multiindex] = Literal(V[multiindex]) - for i in range(0, 9, 3): - V[i, i] = Literal(1) - V[i+2, i+2] = Literal(1) - - T = self.cell - - # This bypasses the GEM wrapper. - that = numpy.array([T.compute_normalized_edge_tangent(i) for i in range(3)]) - nhat = numpy.array([T.compute_normal(i) for i in range(3)]) - - detJ = coordinate_mapping.detJ_at([1/3, 1/3]) - J = coordinate_mapping.jacobian_at([1/3, 1/3]) - J_np = numpy.array([[J[0, 0], J[0, 1]], - [J[1, 0], J[1, 1]]]) - JTJ = J_np.T @ J_np - - for e in range(3): - # Compute alpha and beta for the edge. - Ghat_T = numpy.array([nhat[e, :], that[e, :]]) - - (alpha, beta) = Ghat_T @ JTJ @ that[e, :] / detJ - - # Stuff into the right rows and columns. - idx = 3*e + 1 - V[idx, idx-1] = Literal(-1) * alpha / beta - V[idx, idx] = Literal(1) / beta + sd = self.cell.get_spatial_dimension() + bary, = self.cell.make_points(sd, 0, sd+1) + J = coordinate_mapping.jacobian_at(bary) + detJ = coordinate_mapping.detJ_at(bary) + entity_dofs = self.entity_dofs() + for f in sorted(entity_dofs[sd-1]): + cur = entity_dofs[sd-1][f][0] + V[cur+1, cur:cur+sd] = normal_tangential_edge_transform(self.cell, J, detJ, f) return ListTensor(V.T) def entity_dofs(self): - return {0: {0: [], - 1: [], - 2: []}, - 1: {0: [0, 1, 2], 1: [3, 4, 5], 2: [6, 7, 8]}, - 2: {0: []}} - - def entity_closure_dofs(self): - return {0: {0: [], - 1: [], - 2: []}, - 1: {0: [0, 1, 2], 1: [3, 4, 5], 2: [6, 7, 8]}, - 2: {0: [0, 1, 2, 3, 4, 5, 6, 7, 8]}} + return self._entity_dofs @property def index_shape(self): - return (9,) + return (self._space_dimension,) def space_dimension(self): - return 9 + return self._space_dimension diff --git a/finat/physically_mapped.py b/finat/physically_mapped.py index d165f8910..bab8c0739 100644 --- a/finat/physically_mapped.py +++ b/finat/physically_mapped.py @@ -129,6 +129,16 @@ title = {Nonconforming mixed elements for elasticity}, journal = {Mathematical Models and Methods in Applied Sciences} } +""") + Citations().add("Hu2015", """ +@article{Hu2015, + author = {Hu, J.~ and Zhang, S.~}, + title = {A family of conforming mixed finite elements for linear elasticity on triangular grids}, + year = {2015}, + month = jan, + archiveprefix = {arXiv}, + eprint = {1406.7457}, +} """) Citations().add("Arbogast2017", """ @techreport{Arbogast2017, diff --git a/finat/piola_mapped.py b/finat/piola_mapped.py index 2a82896ec..a39d24603 100644 --- a/finat/piola_mapped.py +++ b/finat/piola_mapped.py @@ -151,7 +151,6 @@ def basis_transformation(self, coordinate_mapping): s = dofs[dim][e][k:k+sd] V[numpy.ix_(s, s)] = Finv k += sd - # Unpick the normal component for the facet bubbles if sd == 2: transform = normal_tangential_edge_transform diff --git a/finat/ufl/elementlist.py b/finat/ufl/elementlist.py index e07ae8f19..172eb78f6 100644 --- a/finat/ufl/elementlist.py +++ b/finat/ufl/elementlist.py @@ -15,13 +15,14 @@ # Modified by Massimiliano Leoni, 2016 # Modified by Robert Kloefkorn, 2022 # Modified by Matthew Scroggs, 2023 +# Modified by Pablo Brubeck, 2024 import warnings from numpy import asarray from ufl.cell import Cell, TensorProductCell -from ufl.sobolevspace import H1, H2, L2, HCurl, HDiv, HDivDiv, HEin, HInf +from ufl.sobolevspace import H1, H2, L2, HCurl, HDiv, HDivDiv, HCurlDiv, HEin, HInf from ufl.utils.formatting import istr # List of valid elements @@ -75,57 +76,45 @@ def show_elements(): # NOTE: Any element with polynomial degree 0 will be considered L2, # independent of the space passed to register_element. -# NOTE: The mapping of the element basis functions -# from reference to physical representation is -# chosen based on the sobolev space: -# HDiv = contravariant Piola, -# HCurl = covariant Piola, -# H1/L2 = no mapping. - -# TODO: If determining mapping from sobolev_space isn't sufficient in -# the future, add mapping name as another element property. - # Cell groups simplices = ("interval", "triangle", "tetrahedron", "pentatope") cubes = ("interval", "quadrilateral", "hexahedron", "tesseract") -any_cell = (None, - "vertex", "interval", - "triangle", "tetrahedron", "prism", - "pyramid", "quadrilateral", "hexahedron", "pentatope", "tesseract") +any_cell = (None, "vertex", *simplices, *cubes[1:], "prism", "pyramid") # Elements in the periodic table # TODO: Register these as aliases of # periodic table element description instead of the other way around -register_element("Lagrange", "CG", 0, H1, "identity", (1, None), - any_cell) # "P" -register_element("Brezzi-Douglas-Marini", "BDM", 1, HDiv, - "contravariant Piola", (1, None), simplices[1:]) # "BDMF" (2d), "N2F" (3d) -register_element("Discontinuous Lagrange", "DG", 0, L2, "identity", (0, None), - any_cell) # "DP" +register_element("Lagrange", "CG", 0, H1, "identity", (1, None), any_cell) # "P" +register_element("Brezzi-Douglas-Marini", "BDM", 1, HDiv, "contravariant Piola", (1, None), simplices[1:]) # "BDMF" (2d), "N2F" (3d) +register_element("Discontinuous Lagrange", "DG", 0, L2, "identity", (0, None), any_cell) # "DP" register_element("Discontinuous Taylor", "TDG", 0, L2, "identity", (0, None), simplices) -register_element("Nedelec 1st kind H(curl)", "N1curl", 1, HCurl, - "covariant Piola", (1, None), simplices[1:]) # "RTE" (2d), "N1E" (3d) -register_element("Nedelec 2nd kind H(curl)", "N2curl", 1, HCurl, - "covariant Piola", (1, None), simplices[1:]) # "BDME" (2d), "N2E" (3d) -register_element("Raviart-Thomas", "RT", 1, HDiv, "contravariant Piola", - (1, None), simplices[1:]) # "RTF" (2d), "N1F" (3d) +register_element("Nedelec 1st kind H(curl)", "N1curl", 1, HCurl, "covariant Piola", (1, None), simplices[1:]) # "RTE" (2d), "N1E" (3d) +register_element("Nedelec 2nd kind H(curl)", "N2curl", 1, HCurl, "covariant Piola", (1, None), simplices[1:]) # "BDME" (2d), "N2E" (3d) +register_element("Raviart-Thomas", "RT", 1, HDiv, "contravariant Piola", (1, None), simplices[1:]) # "RTF" (2d), "N1F" (3d) # Elements not in the periodic table -register_element("Argyris", "ARG", 0, H2, "custom", (5, None), ("triangle",)) -register_element("Bell", "BELL", 0, H2, "custom", (5, 5), ("triangle",)) +# TODO: Implement generic Tear operator for elements instead of this: +register_element("Brezzi-Douglas-Fortin-Marini", "BDFM", 1, HDiv, "contravariant Piola", (1, None), simplices[1:]) +register_element("Crouzeix-Raviart", "CR", 0, L2, "identity", (1, 1), simplices[1:]) +register_element("Discontinuous Raviart-Thomas", "DRT", 1, L2, "contravariant Piola", (1, None), simplices[1:]) +register_element("Kong-Mulder-Veldhuizen", "KMV", 0, H1, "identity", (1, None), simplices[1:]) + +# Tensor elements +register_element("Regge", "Regge", 2, HEin, "double covariant Piola", (0, None), simplices) +register_element("Hellan-Herrmann-Johnson", "HHJ", 2, HDivDiv, "double contravariant Piola", (0, None), ("triangle", "tetrahedron")) +register_element("Gopalakrishnan-Lederer-Schoberl 1st kind", "GLS", 2, HCurlDiv, "covariant contravariant Piola", (1, None), simplices[1:]) +register_element("Gopalakrishnan-Lederer-Schoberl 2nd kind", "GLS2", 2, HCurlDiv, "covariant contravariant Piola", (0, None), simplices[1:]) + +register_element("Nonconforming Arnold-Winther", "AWnc", 2, HDiv, "double contravariant Piola", (2, 2), ("triangle",)) +register_element("Conforming Arnold-Winther", "AWc", 2, HDiv, "double contravariant Piola", (3, None), ("triangle",)) +register_element("Hu-Zhang", "HZ", 2, HDiv, "double contravariant Piola", (3, None), ("triangle")) + +# Zany elements register_element("Bernardi-Raugel", "BR", 1, H1, "contravariant Piola", (1, None), simplices[1:]) register_element("Bernardi-Raugel Bubble", "BRB", 1, H1, "contravariant Piola", (None, None), simplices[1:]) -register_element("Brezzi-Douglas-Fortin-Marini", "BDFM", 1, HDiv, - "contravariant Piola", (1, None), simplices[1:]) -register_element("Crouzeix-Raviart", "CR", 0, L2, "identity", (1, 1), - simplices[1:]) -# TODO: Implement generic Tear operator for elements instead of this: -register_element("Discontinuous Raviart-Thomas", "DRT", 1, L2, - "contravariant Piola", (1, None), simplices[1:]) +register_element("Mardal-Tai-Winther", "MTW", 1, H1, "contravariant Piola", (3, 3), ("triangle",)) register_element("Hermite", "HER", 0, H1, "custom", (3, 3), simplices) -register_element("Kong-Mulder-Veldhuizen", "KMV", 0, H1, "identity", (1, None), - simplices[1:]) -register_element("Mardal-Tai-Winther", "MTW", 1, H1, "contravariant Piola", (3, 3), - ("triangle",)) +register_element("Argyris", "ARG", 0, H2, "custom", (5, None), ("triangle",)) +register_element("Bell", "BELL", 0, H2, "custom", (5, 5), ("triangle",)) register_element("Morley", "MOR", 0, H2, "custom", (2, 2), ("triangle",)) # Macro elements @@ -133,7 +122,7 @@ def show_elements(): register_element("QuadraticPowellSabin12", "PS12", 0, H2, "custom", (2, 2), ("triangle",)) register_element("Hsieh-Clough-Tocher", "HCT", 0, H2, "custom", (3, None), ("triangle",)) register_element("Reduced-Hsieh-Clough-Tocher", "HCT-red", 0, H2, "custom", (3, 3), ("triangle",)) -register_element("Johnson-Mercier", "JM", 2, HDivDiv, "double contravariant Piola", (1, 1), simplices[1:]) +register_element("Johnson-Mercier", "JM", 2, HDiv, "double contravariant Piola", (1, 1), simplices[1:]) register_element("Arnold-Qin", "AQ", 1, H1, "identity", (2, 2), ("triangle",)) register_element("Reduced-Arnold-Qin", "AQ-red", 1, H1, "contravariant Piola", (2, 2), ("triangle",)) @@ -146,30 +135,18 @@ def show_elements(): register_element("Guzman-Neilan Bubble", "GNB", 1, H1, "contravariant Piola", (None, None), simplices[1:]) # Special elements -register_element("Boundary Quadrature", "BQ", 0, L2, "identity", (0, None), - any_cell) +register_element("Boundary Quadrature", "BQ", 0, L2, "identity", (0, None), any_cell) register_element("Bubble", "B", 0, H1, "identity", (2, None), simplices) register_element("FacetBubble", "FB", 0, H1, "identity", (2, None), simplices) -register_element("Quadrature", "Quadrature", 0, L2, "identity", (0, None), - any_cell) -register_element("Real", "R", 0, HInf, "identity", (0, 0), - any_cell + ("TensorProductCell",)) +register_element("Quadrature", "Quadrature", 0, L2, "identity", (0, None), any_cell) +register_element("Real", "R", 0, HInf, "identity", (0, 0), any_cell + ("TensorProductCell",)) register_element("Undefined", "U", 0, L2, "identity", (0, None), any_cell) register_element("Radau", "Rad", 0, L2, "identity", (0, None), ("interval",)) -register_element("Regge", "Regge", 2, HEin, "double covariant Piola", - (0, None), simplices[1:]) register_element("HDiv Trace", "HDivT", 0, L2, "identity", (0, None), any_cell) -register_element("Hellan-Herrmann-Johnson", "HHJ", 2, HDivDiv, - "double contravariant Piola", (0, None), ("triangle",)) -register_element("Nonconforming Arnold-Winther", "AWnc", 2, HDivDiv, - "double contravariant Piola", (2, 2), ("triangle", "tetrahedron")) -register_element("Conforming Arnold-Winther", "AWc", 2, HDivDiv, - "double contravariant Piola", (3, None), ("triangle", "tetrahedron")) + # Spectral elements. -register_element("Gauss-Legendre", "GL", 0, L2, "identity", (0, None), - ("interval",)) -register_element("Gauss-Lobatto-Legendre", "GLL", 0, H1, "identity", (1, None), - ("interval",)) +register_element("Gauss-Legendre", "GL", 0, L2, "identity", (0, None), ("interval",)) +register_element("Gauss-Lobatto-Legendre", "GLL", 0, H1, "identity", (1, None), ("interval",)) register_alias("Lobatto", lambda family, dim, order, degree: ("Gauss-Lobatto-Legendre", order)) register_alias("Lob", @@ -200,29 +177,21 @@ def show_elements(): # New elements introduced for the periodic table 2014 register_element("Q", None, 0, H1, "identity", (1, None), cubes) register_element("DQ", None, 0, L2, "identity", (0, None), cubes) -register_element("RTCE", None, 1, HCurl, "covariant Piola", (1, None), - ("quadrilateral",)) -register_element("RTCF", None, 1, HDiv, "contravariant Piola", (1, None), - ("quadrilateral",)) -register_element("NCE", None, 1, HCurl, "covariant Piola", (1, None), - ("hexahedron",)) -register_element("NCF", None, 1, HDiv, "contravariant Piola", (1, None), - ("hexahedron",)) +register_element("RTCE", None, 1, HCurl, "covariant Piola", (1, None), ("quadrilateral",)) +register_element("RTCF", None, 1, HDiv, "contravariant Piola", (1, None), ("quadrilateral",)) +register_element("NCE", None, 1, HCurl, "covariant Piola", (1, None), ("hexahedron",)) +register_element("NCF", None, 1, HDiv, "contravariant Piola", (1, None), ("hexahedron",)) register_element("S", None, 0, H1, "identity", (1, None), cubes) register_element("DPC", None, 0, L2, "identity", (0, None), cubes) -register_element("BDMCE", None, 1, HCurl, "covariant Piola", (1, None), - ("quadrilateral",)) -register_element("BDMCF", None, 1, HDiv, "contravariant Piola", (1, None), - ("quadrilateral",)) +register_element("BDMCE", None, 1, HCurl, "covariant Piola", (1, None), ("quadrilateral",)) +register_element("BDMCF", None, 1, HDiv, "contravariant Piola", (1, None), ("quadrilateral",)) register_element("SminusE", "SminusE", 1, HCurl, "covariant Piola", (1, None), cubes[1:3]) register_element("SminusF", "SminusF", 1, HDiv, "contravariant Piola", (1, None), cubes[1:2]) register_element("SminusDiv", "SminusDiv", 1, HDiv, "contravariant Piola", (1, None), cubes[1:3]) register_element("SminusCurl", "SminusCurl", 1, HCurl, "covariant Piola", (1, None), cubes[1:3]) -register_element("AAE", None, 1, HCurl, "covariant Piola", (1, None), - ("hexahedron",)) -register_element("AAF", None, 1, HDiv, "contravariant Piola", (1, None), - ("hexahedron",)) +register_element("AAE", None, 1, HCurl, "covariant Piola", (1, None), ("hexahedron",)) +register_element("AAF", None, 1, HDiv, "contravariant Piola", (1, None), ("hexahedron",)) # New aliases introduced for the periodic table 2014 register_alias("P", lambda family, dim, order, degree: ("Lagrange", order)) diff --git a/finat/ufl/finiteelementbase.py b/finat/ufl/finiteelementbase.py index 73307f565..3a84f908e 100644 --- a/finat/ufl/finiteelementbase.py +++ b/finat/ufl/finiteelementbase.py @@ -258,6 +258,8 @@ def pullback(self): return pullback.double_covariant_piola elif self.mapping() == "double contravariant Piola": return pullback.double_contravariant_piola + elif self.mapping() == "covariant contravariant Piola": + return pullback.covariant_contravariant_piola elif self.mapping() == "custom": return pullback.custom_pullback elif self.mapping() == "physical": diff --git a/test/test_zany_mapping.py b/test/test_zany_mapping.py index 89cffac19..9444f5e24 100644 --- a/test/test_zany_mapping.py +++ b/test/test_zany_mapping.py @@ -7,19 +7,6 @@ from fiat_mapping import MyMapping -@pytest.fixture -def ref_cell(request): - K = FIAT.ufc_simplex(2) - return K - - -@pytest.fixture -def phys_cell(request): - K = FIAT.ufc_simplex(2) - K.vertices = ((0.0, 0.1), (1.17, -0.09), (0.15, 1.84)) - return K - - def make_unisolvent_points(element, interior=False): degree = element.degree() ref_complex = element.get_reference_complex() @@ -39,80 +26,11 @@ def make_unisolvent_points(element, interior=False): def check_zany_mapping(element, ref_cell, phys_cell, *args, **kwargs): phys_element = element(phys_cell, *args, **kwargs).fiat_equivalent finat_element = element(ref_cell, *args, **kwargs) - ref_element = finat_element.fiat_equivalent - shape = ref_element.value_shape() - - ref_cell = ref_element.get_reference_element() - phys_cell = phys_element.get_reference_element() - mapping = MyMapping(ref_cell, phys_cell) - - ref_points = make_unisolvent_points(ref_element) - ps = finat.point_set.PointSet(ref_points) - - z = (0,) * finat_element.cell.get_spatial_dimension() - finat_vals_gem = finat_element.basis_evaluation(0, ps, coordinate_mapping=mapping)[z] - finat_vals = evaluate([finat_vals_gem])[0].arr.transpose(*range(1, len(shape)+2), 0) - - phys_points = make_unisolvent_points(phys_element) - phys_vals = phys_element.tabulate(0, phys_points)[z] - - numdofs = finat_element.space_dimension() - numbfs = phys_element.space_dimension() - if numbfs == numdofs: - # Solve for the basis transformation and compare results - ref_vals = ref_element.tabulate(0, ref_points)[z] - Phi = ref_vals.reshape(numbfs, -1) - phi = phys_vals.reshape(numbfs, -1) - Mh = np.linalg.solve(Phi @ Phi.T, Phi @ phi.T).T - Mh[abs(Mh) < 1E-10] = 0 - - Mgem = finat_element.basis_transformation(mapping) - M = evaluate([Mgem])[0].arr - assert np.allclose(M, Mh, atol=1E-9), str(Mh.T-M.T) - - assert np.allclose(finat_vals, phys_vals[:numdofs]) - - -@pytest.mark.parametrize("element", [ - finat.Morley, - finat.QuadraticPowellSabin6, - finat.QuadraticPowellSabin12, - finat.Hermite, - finat.ReducedHsiehCloughTocher, - finat.Bell, - ]) -def test_C1_elements(ref_cell, phys_cell, element): - finat_kwargs = {} - if element == finat.QuadraticPowellSabin12: - finat_kwargs = dict(avg=True) - check_zany_mapping(element, ref_cell, phys_cell, **finat_kwargs) - - -@pytest.mark.parametrize("element, degree", [ - *((finat.Argyris, k) for k in range(5, 8)), - *((finat.HsiehCloughTocher, k) for k in range(3, 6)) -]) -def test_high_order_C1_elements(ref_cell, phys_cell, element, degree): - check_zany_mapping(element, ref_cell, phys_cell, degree, avg=True) - - -def test_argyris_point(ref_cell, phys_cell): - element = finat.Argyris - check_zany_mapping(element, ref_cell, phys_cell, variant="point") - - -def check_zany_piola_mapping(element, ref_cell, phys_cell, *args, **kwargs): - phys_element = element(phys_cell).fiat_equivalent - finat_element = element(ref_cell) ref_element = finat_element._element ref_cell = ref_element.get_reference_element() phys_cell = phys_element.get_reference_element() sd = ref_cell.get_spatial_dimension() - try: - indices = finat_element._indices - except AttributeError: - indices = slice(None) shape = ref_element.value_shape() ref_pts = make_unisolvent_points(ref_element, interior=True) @@ -121,89 +39,146 @@ def check_zany_piola_mapping(element, ref_cell, phys_cell, *args, **kwargs): phys_pts = make_unisolvent_points(phys_element, interior=True) phys_vals = phys_element.tabulate(0, phys_pts)[(0,)*sd] - # Piola map the reference elements - J, b = FIAT.reference_element.make_affine_mapping(ref_cell.vertices, - phys_cell.vertices) - detJ = np.linalg.det(J) - K = J / detJ - if len(shape) == 2: - piola_map = lambda x: K @ x @ K.T + mapping = ref_element.mapping()[0] + if mapping == "affine": + ref_vals_piola = ref_vals else: - piola_map = lambda x: K @ x + # Piola map the reference elements + J, b = FIAT.reference_element.make_affine_mapping(ref_cell.vertices, + phys_cell.vertices) + K = [] + if "covariant" in mapping: + K.append(np.linalg.inv(J).T) + if "contravariant" in mapping: + K.append(J / np.linalg.det(J)) + + if len(shape) == 2: + piola_map = lambda x: K[0] @ x @ K[-1].T + else: + piola_map = lambda x: K[0] @ x + + ref_vals_piola = np.zeros(ref_vals.shape) + for i in range(ref_vals.shape[0]): + for k in range(ref_vals.shape[-1]): + ref_vals_piola[i, ..., k] = piola_map(ref_vals[i, ..., k]) - ref_vals_piola = np.zeros(ref_vals.shape) - for i in range(ref_vals.shape[0]): - for k in range(ref_vals.shape[-1]): - ref_vals_piola[i, ..., k] = piola_map(ref_vals[i, ..., k]) - - dofs = finat_element.entity_dofs() + # Zany map the results num_bfs = phys_element.space_dimension() num_dofs = finat_element.space_dimension() - num_facet_dofs = num_dofs - sum(len(dofs[sd][entity]) for entity in dofs[sd]) - - # Zany map the results mappng = MyMapping(ref_cell, phys_cell) - Mgem = finat_element.basis_transformation(mappng) - M = evaluate([Mgem])[0].arr - shp = (num_dofs, *shape, -1) - ref_vals_zany = (M @ ref_vals_piola.reshape(num_bfs, -1)).reshape(shp) + try: + Mgem = finat_element.basis_transformation(mappng) + M = evaluate([Mgem])[0].arr + ref_vals_zany = np.tensordot(M, ref_vals_piola, (-1, 0)) + except AttributeError: + M = np.eye(num_dofs, num_bfs) + ref_vals_zany = ref_vals_piola # Solve for the basis transformation and compare results Phi = ref_vals_piola.reshape(num_bfs, -1) phi = phys_vals.reshape(num_bfs, -1) - Mh = np.linalg.solve(Phi @ Phi.T, Phi @ phi.T).T - M = M[:num_facet_dofs] - Mh = Mh[indices][:num_facet_dofs] + Vh, residual, *_ = np.linalg.lstsq(Phi.T, phi.T) + Mh = Vh.T + Mh = Mh[:num_dofs] Mh[abs(Mh) < 1E-10] = 0.0 M[abs(M) < 1E-10] = 0.0 - assert np.allclose(M, Mh), str(M.T - Mh.T) - assert np.allclose(ref_vals_zany[:num_facet_dofs], phys_vals[indices][:num_facet_dofs]) - - -@pytest.mark.parametrize("element", [ - finat.MardalTaiWinther, - finat.BernardiRaugel, - finat.BernardiRaugelBubble, - finat.ReducedArnoldQin, - finat.AlfeldSorokina, - finat.ChristiansenHu, - finat.ArnoldWinther, - finat.ArnoldWintherNC, - finat.JohnsonMercier, - finat.GuzmanNeilanFirstKindH1, - finat.GuzmanNeilanSecondKindH1, - finat.GuzmanNeilanBubble, - ]) -def test_piola_triangle(ref_cell, phys_cell, element): - check_zany_piola_mapping(element, ref_cell, phys_cell) + assert np.allclose(residual, 0), str(M.T - Mh.T) + assert np.allclose(ref_vals_zany, phys_vals[:num_dofs]) @pytest.fixture -def ref_tet(request): - K = FIAT.ufc_simplex(3) +def ref_el(request): + K = {dim: FIAT.ufc_simplex(dim) for dim in (2, 3)} return K @pytest.fixture -def phys_tet(request): - K = FIAT.ufc_simplex(3) - K.vertices = ((0, 0, 0), - (1., 0.1, -0.37), - (0.01, 0.987, -.23), - (-0.1, -0.2, 1.38)) +def phys_el(request): + K = {dim: FIAT.ufc_simplex(dim) for dim in (2, 3)} + K[2].vertices = ((0.0, 0.1), (1.17, -0.09), (0.15, 1.84)) + K[3].vertices = ((0, 0, 0), + (1., 0.1, -0.37), + (0.01, 0.987, -.23), + (-0.1, -0.2, 1.38)) return K @pytest.mark.parametrize("element", [ - finat.BernardiRaugel, - finat.BernardiRaugelBubble, - finat.ChristiansenHu, - finat.AlfeldSorokina, - finat.JohnsonMercier, - finat.GuzmanNeilanFirstKindH1, - finat.GuzmanNeilanSecondKindH1, - finat.GuzmanNeilanBubble, - finat.GuzmanNeilanH1div, + finat.Morley, + finat.Hermite, + finat.Bell, + ]) +def test_C1_elements(ref_el, phys_el, element): + check_zany_mapping(element, ref_el[2], phys_el[2]) + + +@pytest.mark.parametrize("element", [ + finat.QuadraticPowellSabin6, + finat.QuadraticPowellSabin12, + finat.ReducedHsiehCloughTocher, + ]) +def test_C1_macroelements(ref_el, phys_el, element): + kwargs = {} + if element == finat.QuadraticPowellSabin12: + kwargs = dict(avg=True) + check_zany_mapping(element, ref_el[2], phys_el[2], **kwargs) + + +@pytest.mark.parametrize("element, degree", [ + *((finat.Argyris, k) for k in range(5, 8)), + *((finat.HsiehCloughTocher, k) for k in range(3, 6)) +]) +def test_high_order_C1_elements(ref_el, phys_el, element, degree): + check_zany_mapping(element, ref_el[2], phys_el[2], degree, avg=True) + + +def test_argyris_point(ref_el, phys_el): + check_zany_mapping(finat.Argyris, ref_el[2], phys_el[2], variant="point") + + +zany_piola_elements = { + 2: [ + finat.MardalTaiWinther, + finat.ReducedArnoldQin, + finat.ArnoldWinther, + finat.ArnoldWintherNC, + ], + 3: [ + finat.BernardiRaugel, + finat.BernardiRaugelBubble, + finat.AlfeldSorokina, + finat.ChristiansenHu, + finat.JohnsonMercier, + finat.GuzmanNeilanFirstKindH1, + finat.GuzmanNeilanSecondKindH1, + finat.GuzmanNeilanBubble, + finat.GuzmanNeilanH1div, + ], +} + + +@pytest.mark.parametrize("dimension, element", [ + *((2, e) for e in zany_piola_elements[2]), + *((2, e) for e in zany_piola_elements[3]), + *((3, e) for e in zany_piola_elements[3]), + ]) +def test_piola(ref_el, phys_el, element, dimension): + check_zany_mapping(element, ref_el[dimension], phys_el[dimension]) + + +@pytest.mark.parametrize("element, degree, variant", [ + *((finat.HuZhang, k, v) for v in ("integral", "point") for k in range(3, 6)), +]) +def test_piola_triangle_high_order(ref_el, phys_el, element, degree, variant): + check_zany_mapping(element, ref_el[2], phys_el[2], degree, variant) + + +@pytest.mark.parametrize("element, degree", [ + *((finat.Regge, k) for k in range(3)), + *((finat.HellanHerrmannJohnson, k) for k in range(3)), + *((finat.GopalakrishnanLedererSchoberlFirstKind, k) for k in range(1, 4)), + *((finat.GopalakrishnanLedererSchoberlSecondKind, k) for k in range(0, 3)), ]) -def test_piola_tetrahedron(ref_tet, phys_tet, element): - check_zany_piola_mapping(element, ref_tet, phys_tet) +@pytest.mark.parametrize("dimension", [2, 3]) +def test_affine(ref_el, phys_el, element, degree, dimension): + check_zany_mapping(element, ref_el[dimension], phys_el[dimension], degree) From f4c486673eeb278fc5fdece1581542dd14bf2e93 Mon Sep 17 00:00:00 2001 From: ksagiyam <46749170+ksagiyam@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:08:00 +0000 Subject: [PATCH 747/749] gem: attach dtype to every node (#327) --- gem/gem.py | 113 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 74 insertions(+), 39 deletions(-) diff --git a/gem/gem.py b/gem/gem.py index c37e43529..ab70385f7 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -38,7 +38,7 @@ 'Inverse', 'Solve', 'extract_type', 'uint_type'] -uint_type = numpy.uintc +uint_type = numpy.dtype(numpy.uintc) class NodeMeta(type): @@ -56,6 +56,9 @@ def __call__(self, *args, **kwargs): if not hasattr(obj, 'free_indices'): obj.free_indices = unique(chain(*[c.free_indices for c in obj.children])) + # Set dtype if not set already. + if not hasattr(obj, 'dtype'): + obj.dtype = obj.inherit_dtype_from_children(obj.children) return obj @@ -63,7 +66,7 @@ def __call__(self, *args, **kwargs): class Node(NodeBase, metaclass=NodeMeta): """Abstract GEM node class.""" - __slots__ = ('free_indices',) + __slots__ = ('free_indices', 'dtype') def is_equal(self, other): """Common subexpression eliminating equality predicate. @@ -153,16 +156,46 @@ def __mod__(self, other): def __rmod__(self, other): return as_gem_uint(other).__mod__(self) + @staticmethod + def inherit_dtype_from_children(children): + if any(c.dtype is None for c in children): + # Set dtype = None will let _assign_dtype() + # assign the default dtype for this node later. + return + else: + return numpy.result_type(*(c.dtype for c in children)) + class Terminal(Node): """Abstract class for terminal GEM nodes.""" - __slots__ = () + __slots__ = ('_dtype',) children = () is_equal = NodeBase.is_equal + @property + def dtype(self): + """dtype of the node. + + We only need to set dtype (or _dtype) on terminal nodes, and + other nodes inherit dtype from their children. + + Currently dtype is significant only for nodes under index DAGs + (DAGs underneath `VariableIndex`s representing indices), and + `VariableIndex` checks if the dtype of the node that it wraps is + of uint_type. _assign_dtype() will then assign uint_type to those nodes. + + dtype can be `None` otherwise, and _assign_dtype() will assign + the default dtype to those nodes. + + """ + if hasattr(self, '_dtype'): + return self._dtype + else: + raise AttributeError(f"Must set _dtype on terminal node, {type(self)}") + class Scalar(Node): """Abstract class for scalar-valued GEM nodes.""" @@ -181,6 +214,7 @@ class Failure(Terminal): def __init__(self, shape, exception): self.shape = shape self.exception = exception + self._dtype = None class Constant(Terminal): @@ -190,8 +224,7 @@ class Constant(Terminal): - array: numpy array of values - value: float or complex value (scalars only) """ - __slots__ = ('dtype',) - __back__ = ('dtype',) + pass class Zero(Constant): @@ -199,15 +232,16 @@ class Zero(Constant): __slots__ = ('shape',) __front__ = ('shape',) + __back__ = ('dtype',) - def __init__(self, shape=(), dtype=float): + def __init__(self, shape=(), dtype=None): self.shape = shape - self.dtype = dtype + self._dtype = dtype @property def value(self): assert not self.shape - return numpy.array(0, dtype=self.dtype).item() + return numpy.array(0, dtype=self.dtype or float).item() class Identity(Constant): @@ -215,10 +249,11 @@ class Identity(Constant): __slots__ = ('dim',) __front__ = ('dim',) + __back__ = ('dtype',) - def __init__(self, dim, dtype=float): + def __init__(self, dim, dtype=None): self.dim = dim - self.dtype = dtype + self._dtype = dtype @property def shape(self): @@ -234,6 +269,7 @@ class Literal(Constant): __slots__ = ('array',) __front__ = ('array',) + __back__ = ('dtype',) def __new__(cls, array, dtype=None): array = asarray(array) @@ -245,14 +281,12 @@ def __init__(self, array, dtype=None): # Assume float or complex. try: self.array = array.astype(float, casting="safe") - self.dtype = float except TypeError: self.array = array.astype(complex) - self.dtype = complex else: # Can be int, etc. self.array = array.astype(dtype) - self.dtype = dtype + self._dtype = self.array.dtype def is_equal(self, other): if type(self) is not type(other): @@ -277,13 +311,14 @@ def shape(self): class Variable(Terminal): """Symbolic variable tensor""" - __slots__ = ('name', 'shape', 'dtype') - __front__ = ('name', 'shape', 'dtype') + __slots__ = ('name', 'shape') + __front__ = ('name', 'shape') + __back__ = ('dtype',) def __init__(self, name, shape, dtype=None): self.name = name self.shape = shape - self.dtype = dtype + self._dtype = dtype class Sum(Scalar): @@ -300,8 +335,7 @@ def __new__(cls, a, b): return a if isinstance(a, Constant) and isinstance(b, Constant): - dtype = numpy.result_type(a.dtype, b.dtype) - return Literal(a.value + b.value, dtype=dtype) + return Literal(a.value + b.value, dtype=Node.inherit_dtype_from_children([a, b])) self = super(Sum, cls).__new__(cls) self.children = a, b @@ -325,8 +359,7 @@ def __new__(cls, a, b): return a if isinstance(a, Constant) and isinstance(b, Constant): - dtype = numpy.result_type(a.dtype, b.dtype) - return Literal(a.value * b.value, dtype=dtype) + return Literal(a.value * b.value, dtype=Node.inherit_dtype_from_children([a, b])) self = super(Product, cls).__new__(cls) self.children = a, b @@ -350,8 +383,7 @@ def __new__(cls, a, b): return a if isinstance(a, Constant) and isinstance(b, Constant): - dtype = numpy.result_type(a.dtype, b.dtype) - return Literal(a.value / b.value, dtype=dtype) + return Literal(a.value / b.value, dtype=Node.inherit_dtype_from_children([a, b])) self = super(Division, cls).__new__(cls) self.children = a, b @@ -364,18 +396,17 @@ class FloorDiv(Scalar): def __new__(cls, a, b): assert not a.shape assert not b.shape - # TODO: Attach dtype property to Node and check that - # numpy.result_dtype(a.dtype, b.dtype) is uint type. - # dtype is currently attached only to {Constant, Variable}. + dtype = Node.inherit_dtype_from_children([a, b]) + if dtype != uint_type: + raise ValueError(f"dtype ({dtype}) != unit_type ({uint_type})") # Constant folding if isinstance(b, Zero): raise ValueError("division by zero") if isinstance(a, Zero): - return Zero(dtype=a.dtype) + return Zero(dtype=dtype) if isinstance(b, Constant) and b.value == 1: return a if isinstance(a, Constant) and isinstance(b, Constant): - dtype = numpy.result_type(a.dtype, b.dtype) return Literal(a.value // b.value, dtype=dtype) self = super(FloorDiv, cls).__new__(cls) self.children = a, b @@ -388,18 +419,17 @@ class Remainder(Scalar): def __new__(cls, a, b): assert not a.shape assert not b.shape - # TODO: Attach dtype property to Node and check that - # numpy.result_dtype(a.dtype, b.dtype) is uint type. - # dtype is currently attached only to {Constant, Variable}. + dtype = Node.inherit_dtype_from_children([a, b]) + if dtype != uint_type: + raise ValueError(f"dtype ({dtype}) != uint_type ({uint_type})") # Constant folding if isinstance(b, Zero): raise ValueError("division by zero") if isinstance(a, Zero): - return Zero(dtype=a.dtype) + return Zero(dtype=dtype) if isinstance(b, Constant) and b.value == 1: - return Zero(dtype=b.dtype) + return Zero(dtype=dtype) if isinstance(a, Constant) and isinstance(b, Constant): - dtype = numpy.result_type(a.dtype, b.dtype) return Literal(a.value % b.value, dtype=dtype) self = super(Remainder, cls).__new__(cls) self.children = a, b @@ -412,18 +442,16 @@ class Power(Scalar): def __new__(cls, base, exponent): assert not base.shape assert not exponent.shape + dtype = Node.inherit_dtype_from_children([base, exponent]) # Constant folding if isinstance(base, Zero): - dtype = numpy.result_type(base.dtype, exponent.dtype) if isinstance(exponent, Zero): raise ValueError("cannot solve 0^0") return Zero(dtype=dtype) elif isinstance(exponent, Zero): - dtype = numpy.result_type(base.dtype, exponent.dtype) return Literal(1, dtype=dtype) elif isinstance(base, Constant) and isinstance(exponent, Constant): - dtype = numpy.result_type(base.dtype, exponent.dtype) return Literal(base.value ** exponent.value, dtype=dtype) self = super(Power, cls).__new__(cls) @@ -483,6 +511,7 @@ def __init__(self, op, a, b): self.operator = op self.children = a, b + self.dtype = None # Do not inherit dtype from children. class LogicalNot(Scalar): @@ -529,6 +558,7 @@ def __new__(cls, condition, then, else_): self = super(Conditional, cls).__new__(cls) self.children = condition, then, else_ self.shape = then.shape + self.dtype = Node.inherit_dtype_from_children([then, else_]) return self @@ -591,6 +621,8 @@ class VariableIndex(IndexBase): def __init__(self, expression): assert isinstance(expression, Node) assert not expression.shape + if expression.dtype != uint_type: + raise ValueError(f"expression.dtype ({expression.dtype}) != uint_type ({uint_type})") self.expression = expression def __eq__(self, other): @@ -846,6 +878,7 @@ class ListTensor(Node): def __new__(cls, array): array = asarray(array) assert numpy.prod(array.shape) + dtype = Node.inherit_dtype_from_children(tuple(array.flat)) # Handle children with shape child_shape = array.flat[0].shape @@ -861,7 +894,7 @@ def __new__(cls, array): # Constant folding if all(isinstance(elem, Constant) for elem in array.flat): - return Literal(numpy.vectorize(attrgetter('value'))(array)) + return Literal(numpy.vectorize(attrgetter('value'))(array), dtype=dtype) self = super(ListTensor, cls).__new__(cls) self.array = array @@ -907,9 +940,9 @@ class Concatenate(Node): __slots__ = ('children',) def __new__(cls, *children): + dtype = Node.inherit_dtype_from_children(children) if all(isinstance(child, Zero) for child in children): size = int(sum(numpy.prod(child.shape, dtype=int) for child in children)) - dtype = numpy.result_type(*(child.dtype for child in children)) return Zero((size,), dtype=dtype) self = super(Concatenate, cls).__new__(cls) @@ -924,8 +957,9 @@ def shape(self): class Delta(Scalar, Terminal): __slots__ = ('i', 'j') __front__ = ('i', 'j') + __back__ = ('dtype',) - def __new__(cls, i, j): + def __new__(cls, i, j, dtype=None): assert isinstance(i, IndexBase) assert isinstance(j, IndexBase) @@ -948,6 +982,7 @@ def __new__(cls, i, j): elif isinstance(index, VariableIndex): raise NotImplementedError("Can not make Delta with VariableIndex") self.free_indices = tuple(unique(free_indices)) + self._dtype = dtype return self From 14b757fb53b4497994dbcc63af452a012e95343f Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Thu, 28 Nov 2024 10:02:23 +0000 Subject: [PATCH 748/749] Use static Literal for faster code generation (#146) --- finat/alfeld_sorokina.py | 9 +++------ finat/argyris.py | 7 ++----- finat/aw.py | 17 +++++------------ finat/bell.py | 9 +++------ finat/hct.py | 14 ++++---------- finat/hermite.py | 13 +++---------- finat/hz.py | 9 +++------ finat/johnson_mercier.py | 9 +++------ finat/morley.py | 10 +++------- finat/mtw.py | 10 +++------- finat/physically_mapped.py | 14 ++++++++++++++ finat/piola_mapped.py | 6 ++---- finat/powell_sabin.py | 17 ++++------------- 13 files changed, 52 insertions(+), 92 deletions(-) diff --git a/finat/alfeld_sorokina.py b/finat/alfeld_sorokina.py index 3cc791136..8e1275b38 100644 --- a/finat/alfeld_sorokina.py +++ b/finat/alfeld_sorokina.py @@ -1,9 +1,9 @@ import FIAT import numpy -from gem import ListTensor, Literal +from gem import ListTensor from finat.fiat_elements import FiatElement -from finat.physically_mapped import Citations, PhysicallyMappedElement +from finat.physically_mapped import Citations, identity, PhysicallyMappedElement from finat.piola_mapped import piola_inverse @@ -20,10 +20,7 @@ def basis_transformation(self, coordinate_mapping): detJ = coordinate_mapping.detJ_at(bary) dofs = self.entity_dofs() - ndof = self.space_dimension() - V = numpy.eye(ndof, dtype=object) - for multiindex in numpy.ndindex(V.shape): - V[multiindex] = Literal(V[multiindex]) + V = identity(self.space_dimension()) # Undo the Piola transform nodes = self._element.get_dual_set().nodes diff --git a/finat/argyris.py b/finat/argyris.py index 2563d99c1..562ae41b2 100644 --- a/finat/argyris.py +++ b/finat/argyris.py @@ -6,7 +6,7 @@ from gem import Literal, ListTensor from finat.fiat_elements import ScalarFiatElement -from finat.physically_mapped import PhysicallyMappedElement, Citations +from finat.physically_mapped import Citations, identity, PhysicallyMappedElement def _vertex_transform(V, vorder, fiat_cell, coordinate_mapping): @@ -115,10 +115,7 @@ def basis_transformation(self, coordinate_mapping): sd = self.cell.get_spatial_dimension() top = self.cell.get_topology() - ndof = self.space_dimension() - V = numpy.eye(ndof, dtype=object) - for multiindex in numpy.ndindex(V.shape): - V[multiindex] = Literal(V[multiindex]) + V = identity(self.space_dimension()) vorder = 2 voffset = comb(sd + vorder, vorder) diff --git a/finat/aw.py b/finat/aw.py index c02195814..80d7b7ea1 100644 --- a/finat/aw.py +++ b/finat/aw.py @@ -1,10 +1,10 @@ """Implementation of the Arnold-Winther finite elements.""" import FIAT import numpy -from gem import ListTensor, Literal +from gem import ListTensor from finat.fiat_elements import FiatElement -from finat.physically_mapped import Citations, PhysicallyMappedElement +from finat.physically_mapped import Citations, identity, PhysicallyMappedElement from finat.piola_mapped import adjugate, normal_tangential_edge_transform, normal_tangential_face_transform @@ -16,10 +16,7 @@ def _facet_transform(fiat_cell, facet_moment_degree, coordinate_mapping): fiat_cell.construct_subelement(sd-1), facet_moment_degree) dofs_per_facet = sd * dimPk_facet ndofs = num_facets * dofs_per_facet - - V = numpy.eye(ndofs, dtype=object) - for multiindex in numpy.ndindex(V.shape): - V[multiindex] = Literal(V[multiindex]) + V = identity(ndofs) bary, = fiat_cell.make_points(sd, 0, sd+1) J = coordinate_mapping.jacobian_at(bary) @@ -64,9 +61,7 @@ def basis_transformation(self, coordinate_mapping): correspond to the constraints.""" numbf = self._element.space_dimension() ndof = self.space_dimension() - V = numpy.eye(numbf, ndof, dtype=object) - for multiindex in numpy.ndindex(V.shape): - V[multiindex] = Literal(V[multiindex]) + V = identity(numbf, ndof) V[:12, :12] = _facet_transform(self.cell, 1, coordinate_mapping) @@ -101,9 +96,7 @@ def basis_transformation(self, coordinate_mapping): # The extra 6 dofs removed here correspond to the constraints numbf = self._element.space_dimension() ndof = self.space_dimension() - V = numpy.eye(numbf, ndof, dtype=object) - for multiindex in numpy.ndindex(V.shape): - V[multiindex] = Literal(V[multiindex]) + V = identity(numbf, ndof) sd = self.cell.get_spatial_dimension() W = _evaluation_transform(self.cell, coordinate_mapping) diff --git a/finat/bell.py b/finat/bell.py index d13a40c42..ce7d0002d 100644 --- a/finat/bell.py +++ b/finat/bell.py @@ -1,10 +1,9 @@ import FIAT -import numpy from math import comb -from gem import Literal, ListTensor +from gem import ListTensor from finat.fiat_elements import ScalarFiatElement -from finat.physically_mapped import PhysicallyMappedElement, Citations +from finat.physically_mapped import Citations, identity, PhysicallyMappedElement from finat.argyris import _vertex_transform, _normal_tangential_transform from copy import deepcopy @@ -32,9 +31,7 @@ def basis_transformation(self, coordinate_mapping): numbf = self._element.space_dimension() ndof = self.space_dimension() # rectangular to toss out the constraint dofs - V = numpy.eye(numbf, ndof, dtype=object) - for multiindex in numpy.ndindex(V.shape): - V[multiindex] = Literal(V[multiindex]) + V = identity(numbf, ndof) vorder = 2 _vertex_transform(V, vorder, self.cell, coordinate_mapping) diff --git a/finat/hct.py b/finat/hct.py index 86db3b45f..f072d8efe 100644 --- a/finat/hct.py +++ b/finat/hct.py @@ -1,10 +1,9 @@ import FIAT -import numpy from math import comb -from gem import ListTensor, Literal +from gem import ListTensor from finat.fiat_elements import ScalarFiatElement -from finat.physically_mapped import Citations, PhysicallyMappedElement +from finat.physically_mapped import Citations, identity, PhysicallyMappedElement from finat.argyris import _vertex_transform, _edge_transform, _normal_tangential_transform from copy import deepcopy @@ -19,10 +18,7 @@ def __init__(self, cell, degree=3, avg=False): super().__init__(FIAT.HsiehCloughTocher(cell, degree)) def basis_transformation(self, coordinate_mapping): - ndof = self.space_dimension() - V = numpy.eye(ndof, dtype=object) - for multiindex in numpy.ndindex(V.shape): - V[multiindex] = Literal(V[multiindex]) + V = identity(self.space_dimension()) sd = self.cell.get_dimension() top = self.cell.get_topology() @@ -59,9 +55,7 @@ def basis_transformation(self, coordinate_mapping): numbf = self._element.space_dimension() ndof = self.space_dimension() # rectangular to toss out the constraint dofs - V = numpy.eye(numbf, ndof, dtype=object) - for multiindex in numpy.ndindex(V.shape): - V[multiindex] = Literal(V[multiindex]) + V = identity(numbf, ndof) vorder = 1 voffset = comb(sd + vorder, vorder) diff --git a/finat/hermite.py b/finat/hermite.py index 31a557125..ae54bdc1b 100644 --- a/finat/hermite.py +++ b/finat/hermite.py @@ -1,10 +1,8 @@ -import numpy - import FIAT -from gem import Literal, ListTensor +from gem import ListTensor from finat.fiat_elements import ScalarFiatElement -from finat.physically_mapped import PhysicallyMappedElement, Citations +from finat.physically_mapped import Citations, identity, PhysicallyMappedElement class Hermite(PhysicallyMappedElement, ScalarFiatElement): @@ -20,12 +18,7 @@ def basis_transformation(self, coordinate_mapping): h = coordinate_mapping.cell_size() d = self.cell.get_dimension() - numbf = self.space_dimension() - - M = numpy.eye(numbf, dtype=object) - - for multiindex in numpy.ndindex(M.shape): - M[multiindex] = Literal(M[multiindex]) + M = identity(self.space_dimension()) cur = 0 for i in range(d+1): diff --git a/finat/hz.py b/finat/hz.py index 620fef61a..54e4502ed 100644 --- a/finat/hz.py +++ b/finat/hz.py @@ -1,9 +1,8 @@ """Implementation of the Hu-Zhang finite elements.""" -import numpy import FIAT -from gem import Literal, ListTensor +from gem import ListTensor from finat.fiat_elements import FiatElement -from finat.physically_mapped import PhysicallyMappedElement, Citations +from finat.physically_mapped import Citations, identity, PhysicallyMappedElement from finat.aw import _facet_transform, _evaluation_transform @@ -16,9 +15,7 @@ def __init__(self, cell, degree=3, variant=None): def basis_transformation(self, coordinate_mapping): ndofs = self.space_dimension() - V = numpy.eye(ndofs, dtype=object) - for multiindex in numpy.ndindex(V.shape): - V[multiindex] = Literal(V[multiindex]) + V = identity(ndofs) sd = self.cell.get_spatial_dimension() W = _evaluation_transform(self.cell, coordinate_mapping) diff --git a/finat/johnson_mercier.py b/finat/johnson_mercier.py index 285ae4f66..8dcaab51e 100644 --- a/finat/johnson_mercier.py +++ b/finat/johnson_mercier.py @@ -1,10 +1,9 @@ import FIAT -import numpy -from gem import ListTensor, Literal +from gem import ListTensor from finat.aw import _facet_transform from finat.fiat_elements import FiatElement -from finat.physically_mapped import Citations, PhysicallyMappedElement +from finat.physically_mapped import Citations, identity, PhysicallyMappedElement class JohnsonMercier(PhysicallyMappedElement, FiatElement): # symmetric matrix valued @@ -17,10 +16,8 @@ def __init__(self, cell, degree=1, variant=None): def basis_transformation(self, coordinate_mapping): numbf = self._element.space_dimension() ndof = self.space_dimension() - V = numpy.eye(numbf, ndof, dtype=object) - for multiindex in numpy.ndindex(V.shape): - V[multiindex] = Literal(V[multiindex]) + V = identity(numbf, ndof) Vsub = _facet_transform(self.cell, 1, coordinate_mapping) Vsub = Vsub[:, self._indices] m, n = Vsub.shape diff --git a/finat/morley.py b/finat/morley.py index 6b9038f85..986a4471b 100644 --- a/finat/morley.py +++ b/finat/morley.py @@ -1,11 +1,9 @@ -import numpy - import FIAT -from gem import Literal, ListTensor +from gem import ListTensor from finat.fiat_elements import ScalarFiatElement -from finat.physically_mapped import PhysicallyMappedElement, Citations +from finat.physically_mapped import Citations, identity, PhysicallyMappedElement class Morley(PhysicallyMappedElement, ScalarFiatElement): @@ -25,9 +23,7 @@ def basis_transformation(self, coordinate_mapping): pel = coordinate_mapping.physical_edge_lengths() - V = numpy.eye(6, dtype=object) - for multiindex in numpy.ndindex(V.shape): - V[multiindex] = Literal(V[multiindex]) + V = identity(self.space_dimension()) for i in range(3): V[i+3, i+3] = (rns[i, 0]*(pns[i, 0]*J[0, 0] + pns[i, 1]*J[1, 0]) diff --git a/finat/mtw.py b/finat/mtw.py index 354f17af6..55d1a1f71 100644 --- a/finat/mtw.py +++ b/finat/mtw.py @@ -1,11 +1,9 @@ -import numpy - import FIAT -from gem import Literal, ListTensor +from gem import ListTensor from finat.fiat_elements import FiatElement -from finat.physically_mapped import PhysicallyMappedElement, Citations +from finat.physically_mapped import Citations, identity, PhysicallyMappedElement from finat.piola_mapped import normal_tangential_edge_transform from copy import deepcopy @@ -28,9 +26,7 @@ def __init__(self, cell, degree=3): def basis_transformation(self, coordinate_mapping): numbf = self._element.space_dimension() ndof = self.space_dimension() - V = numpy.eye(numbf, ndof, dtype=object) - for multiindex in numpy.ndindex(V.shape): - V[multiindex] = Literal(V[multiindex]) + V = identity(numbf, ndof) sd = self.cell.get_spatial_dimension() bary, = self.cell.make_points(sd, 0, sd+1) diff --git a/finat/physically_mapped.py b/finat/physically_mapped.py index bab8c0739..2ea497614 100644 --- a/finat/physically_mapped.py +++ b/finat/physically_mapped.py @@ -1,6 +1,7 @@ from abc import ABCMeta, abstractmethod import gem +import numpy try: from firedrake_citations import Citations @@ -267,8 +268,10 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None): assert coordinate_mapping is not None M = self.basis_transformation(coordinate_mapping) + M, = gem.optimise.constant_fold_zero((M,)) def matvec(table): + table, = gem.optimise.constant_fold_zero((table,)) i, j = gem.indices(2) value_indices = self.get_value_indices() table = gem.Indexed(table, (j, ) + value_indices) @@ -372,3 +375,14 @@ def physical_vertices(self): :returns: a GEM expression for the physical vertices, shape (gdim, ).""" + + +zero = gem.Zero() +one = gem.Literal(1.0) + + +def identity(*shape): + V = numpy.eye(*shape, dtype=object) + for multiindex in numpy.ndindex(V.shape): + V[multiindex] = zero if V[multiindex] == 0 else one + return V diff --git a/finat/piola_mapped.py b/finat/piola_mapped.py index a39d24603..18f51e99e 100644 --- a/finat/piola_mapped.py +++ b/finat/piola_mapped.py @@ -1,7 +1,7 @@ import numpy from finat.fiat_elements import FiatElement -from finat.physically_mapped import PhysicallyMappedElement +from finat.physically_mapped import identity, PhysicallyMappedElement from gem import Literal, ListTensor from copy import deepcopy @@ -130,9 +130,7 @@ def basis_transformation(self, coordinate_mapping): bfs = self._element.entity_dofs() ndof = self.space_dimension() numbf = self._element.space_dimension() - V = numpy.eye(numbf, ndof, dtype=object) - for multiindex in numpy.ndindex(V.shape): - V[multiindex] = Literal(V[multiindex]) + V = identity(numbf, ndof) # Undo the Piola transform for non-facet bubble basis functions nodes = self._element.get_dual_set().nodes diff --git a/finat/powell_sabin.py b/finat/powell_sabin.py index 0c0dfc0b7..d82e7f3f5 100644 --- a/finat/powell_sabin.py +++ b/finat/powell_sabin.py @@ -1,10 +1,9 @@ import FIAT -import numpy -from gem import ListTensor, Literal +from gem import ListTensor from finat.argyris import _edge_transform from finat.fiat_elements import ScalarFiatElement -from finat.physically_mapped import Citations, PhysicallyMappedElement +from finat.physically_mapped import Citations, identity, PhysicallyMappedElement class QuadraticPowellSabin6(PhysicallyMappedElement, ScalarFiatElement): @@ -20,12 +19,7 @@ def basis_transformation(self, coordinate_mapping): h = coordinate_mapping.cell_size() d = self.cell.get_dimension() - numbf = self.space_dimension() - - M = numpy.eye(numbf, dtype=object) - - for multiindex in numpy.ndindex(M.shape): - M[multiindex] = Literal(M[multiindex]) + M = identity(self.space_dimension()) cur = 0 for i in range(d+1): @@ -49,10 +43,7 @@ def __init__(self, cell, degree=2, avg=False): def basis_transformation(self, coordinate_mapping): J = coordinate_mapping.jacobian_at([1/3, 1/3]) - ndof = self.space_dimension() - V = numpy.eye(ndof, dtype=object) - for multiindex in numpy.ndindex(V.shape): - V[multiindex] = Literal(V[multiindex]) + V = identity(self.space_dimension()) sd = self.cell.get_dimension() top = self.cell.get_topology() From 39247cb429a4c064b1893749d71cfc5aa54b7a90 Mon Sep 17 00:00:00 2001 From: Connor Ward Date: Wed, 4 Dec 2024 12:21:15 +0000 Subject: [PATCH 749/749] Move FInAT and gem into FIAT repository --- .github/workflows/docs.yml | 32 ++ .github/workflows/pythonapp.yml | 20 +- .gitignore | 6 +- doc/sphinx/Makefile | 177 ----------- doc/sphinx/requirements.txt | 3 - doc/sphinx/source/conf.py | 289 ------------------ doc/sphinx/source/index.rst | 37 --- docs/Makefile | 80 ++--- docs/source/conf.py | 206 ++++++------- docs/source/index.rst | 41 ++- {doc/sphinx => docs}/source/installation.rst | 0 {doc/sphinx => docs}/source/manual.rst | 0 {doc/sphinx => docs}/source/releases.rst | 0 {doc/sphinx => docs}/source/releases/next.rst | 0 .../source/releases/v1.6.0.rst | 0 .../source/releases/v2016.1.0.rst | 0 .../source/releases/v2016.2.0.rst | 0 .../source/releases/v2017.1.0.post1.rst | 0 .../source/releases/v2017.1.0.rst | 0 .../source/releases/v2017.2.0.rst | 0 .../source/releases/v2018.1.0.rst | 0 .../source/releases/v2019.1.0.rst | 0 finat/point_set.py | 2 +- finat/tensor_product.py | 6 +- finat/ufl/finiteelement.py | 16 +- gem/gem.py | 2 +- pyproject.toml | 39 +++ setup.cfg | 4 +- setup.py | 34 --- test/{ => FIAT}/regression/.gitignore | 0 test/{ => FIAT}/regression/README.rst | 0 test/{ => FIAT}/regression/conftest.py | 0 .../regression/fiat-reference-data-id | 0 test/{ => FIAT}/regression/scripts/download | 0 test/{ => FIAT}/regression/scripts/getdata | 0 .../regression/scripts/getreferencerepo | 0 test/{ => FIAT}/regression/scripts/parameters | 0 test/{ => FIAT}/regression/scripts/upload | 0 test/{ => FIAT}/regression/test_regression.py | 0 test/{ => FIAT}/unit/test_argyris.py | 0 test/{ => FIAT}/unit/test_awc.py | 0 test/{ => FIAT}/unit/test_awnc.py | 0 test/{ => FIAT}/unit/test_bernstein.py | 0 test/{ => FIAT}/unit/test_discontinuous_pc.py | 0 .../unit/test_discontinuous_taylor.py | 0 .../unit/test_facet_support_dofs.py | 0 test/{ => FIAT}/unit/test_fdm.py | 0 test/{ => FIAT}/unit/test_fiat.py | 0 test/{ => FIAT}/unit/test_gauss_legendre.py | 0 .../unit/test_gauss_lobatto_legendre.py | 0 test/{ => FIAT}/unit/test_gauss_radau.py | 0 .../test_gopalakrishnan_lederer_schoberl.py | 0 test/{ => FIAT}/unit/test_hct.py | 0 test/{ => FIAT}/unit/test_hdivtrace.py | 0 test/{ => FIAT}/unit/test_hierarchical.py | 0 test/{ => FIAT}/unit/test_johnson_mercier.py | 0 .../unit/test_kong_mulder_veldhuizen.py | 0 test/{ => FIAT}/unit/test_macro.py | 0 test/{ => FIAT}/unit/test_mtw.py | 0 test/{ => FIAT}/unit/test_orientation.py | 0 test/{ => FIAT}/unit/test_pointwise_dual.py | 0 test/{ => FIAT}/unit/test_polynomial.py | 0 test/{ => FIAT}/unit/test_powell_sabin.py | 0 test/{ => FIAT}/unit/test_quadrature.py | 0 .../unit/test_quadrature_element.py | 0 .../{ => FIAT}/unit/test_reference_element.py | 0 test/{ => FIAT}/unit/test_regge_hhj.py | 0 test/{ => FIAT}/unit/test_serendipity.py | 0 test/{ => FIAT}/unit/test_stokes_complex.py | 0 test/{ => FIAT}/unit/test_tensor_product.py | 0 test/README | 6 - test/{ => finat}/fiat_mapping.py | 0 test/{ => finat}/test_direct_serendipity.py | 0 test/{ => finat}/test_hash.py | 0 test/{ => finat}/test_mass_conditioning.py | 0 .../test_point_evaluation_ciarlet.py | 0 test/{ => finat}/test_restriction.py | 0 test/{ => finat}/test_zany_mapping.py | 0 78 files changed, 249 insertions(+), 751 deletions(-) create mode 100644 .github/workflows/docs.yml delete mode 100644 doc/sphinx/Makefile delete mode 100644 doc/sphinx/requirements.txt delete mode 100644 doc/sphinx/source/conf.py delete mode 100644 doc/sphinx/source/index.rst rename {doc/sphinx => docs}/source/installation.rst (100%) rename {doc/sphinx => docs}/source/manual.rst (100%) rename {doc/sphinx => docs}/source/releases.rst (100%) rename {doc/sphinx => docs}/source/releases/next.rst (100%) rename {doc/sphinx => docs}/source/releases/v1.6.0.rst (100%) rename {doc/sphinx => docs}/source/releases/v2016.1.0.rst (100%) rename {doc/sphinx => docs}/source/releases/v2016.2.0.rst (100%) rename {doc/sphinx => docs}/source/releases/v2017.1.0.post1.rst (100%) rename {doc/sphinx => docs}/source/releases/v2017.1.0.rst (100%) rename {doc/sphinx => docs}/source/releases/v2017.2.0.rst (100%) rename {doc/sphinx => docs}/source/releases/v2018.1.0.rst (100%) rename {doc/sphinx => docs}/source/releases/v2019.1.0.rst (100%) create mode 100644 pyproject.toml delete mode 100755 setup.py rename test/{ => FIAT}/regression/.gitignore (100%) rename test/{ => FIAT}/regression/README.rst (100%) rename test/{ => FIAT}/regression/conftest.py (100%) rename test/{ => FIAT}/regression/fiat-reference-data-id (100%) rename test/{ => FIAT}/regression/scripts/download (100%) rename test/{ => FIAT}/regression/scripts/getdata (100%) rename test/{ => FIAT}/regression/scripts/getreferencerepo (100%) rename test/{ => FIAT}/regression/scripts/parameters (100%) rename test/{ => FIAT}/regression/scripts/upload (100%) rename test/{ => FIAT}/regression/test_regression.py (100%) rename test/{ => FIAT}/unit/test_argyris.py (100%) rename test/{ => FIAT}/unit/test_awc.py (100%) rename test/{ => FIAT}/unit/test_awnc.py (100%) rename test/{ => FIAT}/unit/test_bernstein.py (100%) rename test/{ => FIAT}/unit/test_discontinuous_pc.py (100%) rename test/{ => FIAT}/unit/test_discontinuous_taylor.py (100%) rename test/{ => FIAT}/unit/test_facet_support_dofs.py (100%) rename test/{ => FIAT}/unit/test_fdm.py (100%) rename test/{ => FIAT}/unit/test_fiat.py (100%) rename test/{ => FIAT}/unit/test_gauss_legendre.py (100%) rename test/{ => FIAT}/unit/test_gauss_lobatto_legendre.py (100%) rename test/{ => FIAT}/unit/test_gauss_radau.py (100%) rename test/{ => FIAT}/unit/test_gopalakrishnan_lederer_schoberl.py (100%) rename test/{ => FIAT}/unit/test_hct.py (100%) rename test/{ => FIAT}/unit/test_hdivtrace.py (100%) rename test/{ => FIAT}/unit/test_hierarchical.py (100%) rename test/{ => FIAT}/unit/test_johnson_mercier.py (100%) rename test/{ => FIAT}/unit/test_kong_mulder_veldhuizen.py (100%) rename test/{ => FIAT}/unit/test_macro.py (100%) rename test/{ => FIAT}/unit/test_mtw.py (100%) rename test/{ => FIAT}/unit/test_orientation.py (100%) rename test/{ => FIAT}/unit/test_pointwise_dual.py (100%) rename test/{ => FIAT}/unit/test_polynomial.py (100%) rename test/{ => FIAT}/unit/test_powell_sabin.py (100%) rename test/{ => FIAT}/unit/test_quadrature.py (100%) rename test/{ => FIAT}/unit/test_quadrature_element.py (100%) rename test/{ => FIAT}/unit/test_reference_element.py (100%) rename test/{ => FIAT}/unit/test_regge_hhj.py (100%) rename test/{ => FIAT}/unit/test_serendipity.py (100%) rename test/{ => FIAT}/unit/test_stokes_complex.py (100%) rename test/{ => FIAT}/unit/test_tensor_product.py (100%) delete mode 100644 test/README rename test/{ => finat}/fiat_mapping.py (100%) rename test/{ => finat}/test_direct_serendipity.py (100%) rename test/{ => finat}/test_hash.py (100%) rename test/{ => finat}/test_mass_conditioning.py (100%) rename test/{ => finat}/test_point_evaluation_ciarlet.py (100%) rename test/{ => finat}/test_restriction.py (100%) rename test/{ => finat}/test_zany_mapping.py (100%) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..15c4bb309 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,32 @@ +name: github pages + +on: + push: + branches: + - master + +jobs: + build_docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup python + uses: actions/setup-python@v3 + with: + python-version: 3.12 + - name: Install + run: | + python -m pip install .[doc] + - name: Build docs + run: | + make -C docs html + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./docs/build/html + publish_branch: gh-pages + enable_jekyll: false + allow_empty_commit: false + user_name: 'github-actions[bot]' + user_email: 'github-actions[bot]@users.noreply.github.com' diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index ce154290f..45581cf42 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -4,7 +4,11 @@ name: FIAT CI -on: [push, pull_request] +on: + push: + branches: + - master + pull_request: jobs: build: @@ -12,7 +16,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest] - python-version: ['3.9', '3.10', '3.11', '3.12'] + python-version: ['3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 @@ -28,12 +32,14 @@ jobs: run: | python -m pip install pydocstyle python -m pydocstyle . - - name: Install FIAT - run: pip install . - - name: Test with pytest + - name: Install FIAT and CI dependencies run: | - python -m pip install coveralls pytest pytest-cov pytest-xdist - DATA_REPO_GIT="" python -m pytest --cov=FIAT/ test/ + python -m pip install '.[test]' + python -m pip install coveralls pytest-cov + - name: Test FIAT + run: DATA_REPO_GIT="" python -m pytest --cov=FIAT/ test/FIAT + - name: Test FInAT + run: DATA_REPO_GIT="" python -m pytest --cov=finat/ --cov=gem/ test/finat - name: Coveralls if: ${{ github.repository == 'FEniCS/fiat' && github.head_ref == '' && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11' }} env: diff --git a/.gitignore b/.gitignore index 26346c53d..b41093856 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,8 @@ /doc/sphinx/source/api-doc release/ -/doc/sphinx/source/_build/ \ No newline at end of file +/docs/build/ +/docs/source/FIAT.rst +/docs/source/finat.rst +/docs/source/finat.ufl.rst +/docs/source/gem.rst diff --git a/doc/sphinx/Makefile b/doc/sphinx/Makefile deleted file mode 100644 index 995d1834e..000000000 --- a/doc/sphinx/Makefile +++ /dev/null @@ -1,177 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = build - -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) -$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/FIniteelementAutomaticTabulatorFIAT.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/FIniteelementAutomaticTabulatorFIAT.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/FIniteelementAutomaticTabulatorFIAT" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/FIniteelementAutomaticTabulatorFIAT" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/doc/sphinx/requirements.txt b/doc/sphinx/requirements.txt deleted file mode 100644 index 0a3d875fb..000000000 --- a/doc/sphinx/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -numpy -sympy -sphinx==1.7.0 diff --git a/doc/sphinx/source/conf.py b/doc/sphinx/source/conf.py deleted file mode 100644 index f97faaf30..000000000 --- a/doc/sphinx/source/conf.py +++ /dev/null @@ -1,289 +0,0 @@ -# -*- coding: utf-8 -*- -# -# FInite element Automatic Tabulator (FIAT) documentation build configuration file, created by -# sphinx-quickstart on Wed Nov 4 15:38:29 2015. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys -import os -import shlex -import pkg_resources -import datetime - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.coverage', - 'sphinx.ext.mathjax', - 'sphinx.ext.viewcode', -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'FInite element Automatic Tabulator (FIAT)' -this_year = datetime.date.today().year -copyright = u'%s, FEniCS Project' % this_year -version = pkg_resources.get_distribution("fenics-fiat").version -release = version - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = [] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'default' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -#html_extra_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'FIniteelementAutomaticTabulatorFIATdoc' - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - ('index', 'FIniteelementAutomaticTabulatorFIAT.tex', u'FInite element Automatic Tabulator (FIAT) Documentation', - u'FEniCS Project', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'finiteelementautomatictabulatorfiat', u'FInite element Automatic Tabulator (FIAT) Documentation', - [u'FEniCS Project'], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ('index', 'FIniteelementAutomaticTabulatorFIAT', u'FInite element Automatic Tabulator (FIAT) Documentation', - u'FEniCS Project', 'FIniteelementAutomaticTabulatorFIAT', 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False - - -# Configuration for intersphinx -intersphinx_mapping = { - 'recursivenodes': ('https://tisaac.gitlab.io/recursivenodes/', None), - 'numpy': ('https://numpy.org/doc/stable/', None), - 'python': ('https://docs.python.org/3/', None), -} - - -def run_apidoc(_): - modules = ['FIAT'] - - # Get location of Sphinx files - sphinx_source_dir = os.path.abspath(os.path.dirname(__file__)) - repo_dir = os.path.abspath(os.path.join(sphinx_source_dir, os.path.pardir, - os.path.pardir, os.path.pardir)) - apidoc_dir = os.path.join(sphinx_source_dir, "api-doc") - - from sphinx.ext.apidoc import main - for module in modules: - # Generate .rst files ready for autodoc - module_dir = os.path.join(repo_dir, module) - main(["-f", "-d", "1", "-o", apidoc_dir, module_dir]) - -def setup(app): - app.connect('builder-inited', run_apidoc) diff --git a/doc/sphinx/source/index.rst b/doc/sphinx/source/index.rst deleted file mode 100644 index 9dc306dc2..000000000 --- a/doc/sphinx/source/index.rst +++ /dev/null @@ -1,37 +0,0 @@ -.. title:: FIAT - - -======================================== -FIAT: FInite element Automatic Tabulator -======================================== - -FIAT is a Python package for automatic generation of finite element -basis functions. It is capable of generating finite element basis -functions for a wide range of finite element families on simplices -(lines, triangles and tetrahedra), including the Lagrange elements, -and the elements of Raviart-Thomas, Brezzi-Douglas-Marini and Nedelec. -It is also capable of generating tensor-product elements and a number -more exotic elements, such as the Argyris, Hermite and Morley -elements. - -FIAT is part of the FEniCS Project. - -For more information, visit http://www.fenicsproject.org. - - -Documentation -============= - -.. toctree:: - :titlesonly: - :maxdepth: 1 - - installation - manual - API reference - releases - -[FIXME: These links don't belong here, should go under API reference somehow.] - -* :ref:`genindex` -* :ref:`modindex` diff --git a/docs/Makefile b/docs/Makefile index d866dab1c..60333b3ae 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -7,8 +7,6 @@ SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build -PYTHONPATH := $(PWD)/..:$(PYTHONPATH) - # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) @@ -21,14 +19,11 @@ ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) sou # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp \ -devhelp epub latex latexpdf text man changes linkcheck doctest gettext \ -serve +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" - @echo " serve to launch a local web server to serve up documentation" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @@ -51,93 +46,76 @@ help: @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" -TARGETS = html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - - -livehtml: html - python server.py - -serve: html - cd $(BUILDDIR)/html; python -m SimpleHTTPServer $(PORT) - -source/teamgrid.rst: source/team.py - cd source; python team.py - -.PHONY: source/obtaining_pyop2.rst - -source/obtaining_pyop2.rst: - wget https://raw.github.com/OP2/PyOP2/master/README.rst -O $@ - -apidoc: $(GENERATED_FILES) - sphinx-apidoc ../finat -o source/ -f -T - clean: rm -rf $(BUILDDIR)/* -buildhtml: apidoc - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html +apidoc: + sphinx-apidoc -f -T -o source/ ../FIAT + sphinx-apidoc -f -T -o source/ ../finat + sphinx-apidoc -f -T -o source/ ../gem -html: apidoc buildhtml +html: apidoc + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." -dirhtml: apidoc +dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." -singlehtml: apidoc +singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." -pickle: apidoc +pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." -json: apidoc +json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." -htmlhelp: apidoc +htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." -qthelp: apidoc +qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/FInAT.qhcp" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/FIniteelementAutomaticTabulatorFIAT.qhcp" @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/FInAT.qhc" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/FIniteelementAutomaticTabulatorFIAT.qhc" -devhelp: apidoc +devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/FInAT" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/FInAT" + @echo "# mkdir -p $$HOME/.local/share/devhelp/FIniteelementAutomaticTabulatorFIAT" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/FIniteelementAutomaticTabulatorFIAT" @echo "# devhelp" -epub: apidoc +epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." -latex: apidoc +latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." -latexpdf: apidoc +latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @@ -149,46 +127,46 @@ latexpdfja: $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." -text: apidoc +text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." -man: apidoc +man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." -texinfo: apidoc +texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." -info: apidoc +info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." -gettext: apidoc +gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." -changes: apidoc +changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." -linkcheck: apidoc +linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." -doctest: apidoc +doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/source/conf.py b/docs/source/conf.py index 90f399c58..9963d35cd 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # -# FInAT documentation build configuration file, created by -# sphinx-quickstart on Thu Aug 14 11:38:06 2014. +# FInite element Automatic Tabulator (FIAT) documentation build configuration file, created by +# sphinx-quickstart on Wed Nov 4 15:38:29 2015. # # This file is execfile()d with the current directory set to its # containing dir. @@ -12,66 +12,62 @@ # All configuration values have a default; values that are commented out # serve to show the default. +import sys +import os +import shlex +import pkg_resources +import datetime + # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -# sys.path.insert(0, os.path.abspath('.')) +#sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -# needs_sphinx = '1.0' +#needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.intersphinx", - "sphinx.ext.mathjax", - "sphinx.ext.viewcode", + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', + 'sphinx.ext.coverage', + 'sphinx.ext.mathjax', + 'sphinx.ext.viewcode', ] -# Both the class’ and the __init__ method’s docstring are concatenated and -# inserted into the class definition -autoclass_content = "both" # Add any paths that contain templates here, relative to this directory. -templates_path = ["_templates"] +templates_path = ['_templates'] # The suffix of source filenames. -source_suffix = ".rst" +source_suffix = '.rst' # The encoding of source files. -# source_encoding = 'utf-8-sig' +#source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = "index" +master_doc = 'index' # General information about the project. -project = u"FInAT" -copyright = u"2014--2020, David A. Ham, Robert C. Kirby and others" - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = "0.1" -# The full version, including alpha/beta/rc tags. -release = "0.1" +project = u'FInite element Automatic Tabulator (FIAT)' +this_year = datetime.date.today().year +copyright = u'%s, FEniCS Project' % this_year +version = pkg_resources.get_distribution("fenics-fiat").version +release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None +#language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -# today = '' +#today = '' # Else, today_fmt is used as the format for a strftime call. -# today_fmt = '%B %d, %Y' +#today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -79,172 +75,149 @@ # The reST default role (used for this markup: `text`) to use for all # documents. -# default_role = None +#default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -# add_function_parentheses = True +#add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -# add_module_names = True +#add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -# show_authors = False +#show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = "sphinx" +pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -# modindex_common_prefix = [] +#modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -# keep_warnings = False +#keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = "finat" +html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -# html_theme_options = {} +#html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -html_theme_path = ["_themes"] +#html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -# html_title = None +#html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -# html_short_title = None +#html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -# html_logo = None +#html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -# html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -# html_static_path = ['_static'] +#html_favicon = None # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -# html_extra_path = [] +#html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -# html_last_updated_fmt = '%b %d, %Y' +#html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -html_use_smartypants = True +#html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -# html_sidebars = {} +#html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -# html_additional_pages = {} +#html_additional_pages = {} # If false, no module index is generated. -# html_domain_indices = True +#html_domain_indices = True # If false, no index is generated. -# html_use_index = True +#html_use_index = True # If true, the index is split into individual pages for each letter. -# html_split_index = False +#html_split_index = False # If true, links to the reST sources are added to the pages. -# html_show_sourcelink = True +#html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -# html_show_sphinx = True +#html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -# html_show_copyright = True +#html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -# html_use_opensearch = '' +#html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -# html_file_suffix = None - -# Language to be used for generating the HTML full-text search index. -# Sphinx supports the following languages: -# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' -# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' -# html_search_language = 'en' - -# A dictionary with options for the search language support, empty by default. -# Now only 'ja' uses this config value -# html_search_options = {'type': 'default'} - -# The name of a javascript file (relative to the configuration directory) that -# implements a search results scorer. If empty, the default will be used. -# html_search_scorer = 'scorer.js' +#html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = "FInATdoc" +htmlhelp_basename = 'FIniteelementAutomaticTabulatorFIATdoc' + # -- Options for LaTeX output --------------------------------------------- latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - "papersize": "a4paper", - # The font size ('10pt', '11pt' or '12pt'). - # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. - # 'preamble': '', - # Latex figure (float) alignment - # 'figure_align': 'htbp', +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ( - "index", - "FInAT.tex", - u"FInAT Documentation", - u"David A. Ham and Robert C. Kirby", - "manual", - ), + ('index', 'FIniteelementAutomaticTabulatorFIAT.tex', u'FInite element Automatic Tabulator (FIAT) Documentation', + u'FEniCS Project', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -# latex_logo = None +#latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -# latex_use_parts = False +#latex_use_parts = False # If true, show page references after internal links. -# latex_show_pagerefs = False +#latex_show_pagerefs = False # If true, show URL addresses after external links. -# latex_show_urls = False +#latex_show_urls = False # Documents to append as an appendix to all manuals. -# latex_appendices = [] +#latex_appendices = [] # If false, no module index is generated. -# latex_domain_indices = True +#latex_domain_indices = True # -- Options for manual page output --------------------------------------- @@ -252,11 +225,12 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ("index", "finat", u"FInAT Documentation", [u"David A. Ham and Robert C. Kirby"], 1) + ('index', 'finiteelementautomatictabulatorfiat', u'FInite element Automatic Tabulator (FIAT) Documentation', + [u'FEniCS Project'], 1) ] # If true, show URL addresses after external links. -# man_show_urls = False +#man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -265,29 +239,27 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ( - "index", - "FInAT", - u"FInAT Documentation", - u"David A. Ham and Robert C. Kirby", - "FInAT", - "One line description of project.", - "Miscellaneous", - ), + ('index', 'FIniteelementAutomaticTabulatorFIAT', u'FInite element Automatic Tabulator (FIAT) Documentation', + u'FEniCS Project', 'FIniteelementAutomaticTabulatorFIAT', 'One line description of project.', + 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. -# texinfo_appendices = [] +#texinfo_appendices = [] # If false, no module index is generated. -# texinfo_domain_indices = True +#texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -# texinfo_show_urls = 'footnote' +#texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -# texinfo_no_detailmenu = False +#texinfo_no_detailmenu = False -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {"https://docs.python.org/3/": None} +# Configuration for intersphinx +intersphinx_mapping = { + 'recursivenodes': ('https://tisaac.gitlab.io/recursivenodes/', None), + 'numpy': ('https://numpy.org/doc/stable/', None), + 'python': ('https://docs.python.org/3/', None), +} diff --git a/docs/source/index.rst b/docs/source/index.rst index cef474f5d..1deda454c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,23 +1,34 @@ -.. FInAT documentation master file, created by - sphinx-quickstart on Thu Aug 14 11:38:06 2014. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. +.. title:: FIAT -Welcome to FInAT's documentation! -================================= -Contents: +======================================== +FIAT: FInite element Automatic Tabulator +======================================== -.. toctree:: - :maxdepth: 2 +FIAT is a Python package for automatic generation of finite element +basis functions. It is capable of generating finite element basis +functions for a wide range of finite element families on simplices +(lines, triangles and tetrahedra), including the Lagrange elements, +and the elements of Raviart-Thomas, Brezzi-Douglas-Marini and Nedelec. +It is also capable of generating tensor-product elements and a number +more exotic elements, such as the Argyris, Hermite and Morley +elements. - finat +FIAT is part of the FEniCS Project. +For more information, visit http://www.fenicsproject.org. -Indices and tables -================== -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` +Documentation +============= +.. toctree:: + :titlesonly: + :maxdepth: 1 + + installation + manual + releases + FIAT + finat + gem diff --git a/doc/sphinx/source/installation.rst b/docs/source/installation.rst similarity index 100% rename from doc/sphinx/source/installation.rst rename to docs/source/installation.rst diff --git a/doc/sphinx/source/manual.rst b/docs/source/manual.rst similarity index 100% rename from doc/sphinx/source/manual.rst rename to docs/source/manual.rst diff --git a/doc/sphinx/source/releases.rst b/docs/source/releases.rst similarity index 100% rename from doc/sphinx/source/releases.rst rename to docs/source/releases.rst diff --git a/doc/sphinx/source/releases/next.rst b/docs/source/releases/next.rst similarity index 100% rename from doc/sphinx/source/releases/next.rst rename to docs/source/releases/next.rst diff --git a/doc/sphinx/source/releases/v1.6.0.rst b/docs/source/releases/v1.6.0.rst similarity index 100% rename from doc/sphinx/source/releases/v1.6.0.rst rename to docs/source/releases/v1.6.0.rst diff --git a/doc/sphinx/source/releases/v2016.1.0.rst b/docs/source/releases/v2016.1.0.rst similarity index 100% rename from doc/sphinx/source/releases/v2016.1.0.rst rename to docs/source/releases/v2016.1.0.rst diff --git a/doc/sphinx/source/releases/v2016.2.0.rst b/docs/source/releases/v2016.2.0.rst similarity index 100% rename from doc/sphinx/source/releases/v2016.2.0.rst rename to docs/source/releases/v2016.2.0.rst diff --git a/doc/sphinx/source/releases/v2017.1.0.post1.rst b/docs/source/releases/v2017.1.0.post1.rst similarity index 100% rename from doc/sphinx/source/releases/v2017.1.0.post1.rst rename to docs/source/releases/v2017.1.0.post1.rst diff --git a/doc/sphinx/source/releases/v2017.1.0.rst b/docs/source/releases/v2017.1.0.rst similarity index 100% rename from doc/sphinx/source/releases/v2017.1.0.rst rename to docs/source/releases/v2017.1.0.rst diff --git a/doc/sphinx/source/releases/v2017.2.0.rst b/docs/source/releases/v2017.2.0.rst similarity index 100% rename from doc/sphinx/source/releases/v2017.2.0.rst rename to docs/source/releases/v2017.2.0.rst diff --git a/doc/sphinx/source/releases/v2018.1.0.rst b/docs/source/releases/v2018.1.0.rst similarity index 100% rename from doc/sphinx/source/releases/v2018.1.0.rst rename to docs/source/releases/v2018.1.0.rst diff --git a/doc/sphinx/source/releases/v2019.1.0.rst b/docs/source/releases/v2019.1.0.rst similarity index 100% rename from doc/sphinx/source/releases/v2019.1.0.rst rename to docs/source/releases/v2019.1.0.rst diff --git a/finat/point_set.py b/finat/point_set.py index 63ab8dc84..1497308c7 100644 --- a/finat/point_set.py +++ b/finat/point_set.py @@ -95,7 +95,7 @@ class UnknownPointSet(AbstractPointSet): shape (D,) and free indices for the points N.""" def __init__(self, points_expr): - """Build a PointSingleton from a gem expression for a single point. + r"""Build a PointSingleton from a gem expression for a single point. :arg points_expr: A ``gem.Variable`` expression representing a vector of N points in D dimensions. Should have shape (N, D) diff --git a/finat/tensor_product.py b/finat/tensor_product.py index de3708fbd..f0fd58477 100644 --- a/finat/tensor_product.py +++ b/finat/tensor_product.py @@ -225,7 +225,7 @@ def productise(factors, method): def compose_permutations(factors): - """For the :class:`TensorProductElement` object composed of factors, + r"""For the :class:`TensorProductElement` object composed of factors, construct, for each dimension tuple, for each entity, and for each possible entity orientation combination, the DoF permutation list. @@ -236,7 +236,8 @@ def compose_permutations(factors): For tensor-product elements, one needs to consider two kinds of orientations: extrinsic orientations and intrinsic ("material") orientations. - Example: + Example + ------- UFCQuadrilateral := UFCInterval x UFCInterval @@ -286,6 +287,7 @@ def compose_permutations(factors): (1, 0, 1): [2, 0, 3, 1], (1, 1, 0): [1, 3, 0, 2], (1, 1, 1): [3, 1, 2, 0]}}} + """ permutations = {} cells = [fe.cell for fe in factors] diff --git a/finat/ufl/finiteelement.py b/finat/ufl/finiteelement.py index 36c3d9221..4cf34c90d 100644 --- a/finat/ufl/finiteelement.py +++ b/finat/ufl/finiteelement.py @@ -126,14 +126,14 @@ def __init__(self, variant=None): """Create finite element. - Args: - family: The finite element family - cell: The geometric cell - degree: The polynomial degree (optional) - form_degree: The form degree (FEEC notation, used when field is - viewed as k-form) - quad_scheme: The quadrature scheme (optional) - variant: Hint for the local basis function variant (optional) + :arg family: The finite element family + :arg cell: The geometric cell + :arg degree: The polynomial degree (optional) + :arg form_degree: The form degree (FEEC notation, used when field is + viewed as k-form) + :arg quad_scheme: The quadrature scheme (optional) + :arg variant: Hint for the local basis function variant (optional) + """ # Note: Unfortunately, dolfin sometimes passes None for # cell. Until this is fixed, allow it: diff --git a/gem/gem.py b/gem/gem.py index ab70385f7..9f00d5535 100644 --- a/gem/gem.py +++ b/gem/gem.py @@ -177,7 +177,7 @@ class Terminal(Node): @property def dtype(self): - """dtype of the node. + """Data type of the node. We only need to set dtype (or _dtype) on terminal nodes, and other nodes inherit dtype from their children. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..6e10c7b8a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,39 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "fenics-fiat" +version = "2024.0.0" +dependencies = [ + "numpy>=1.16", + "recursivenodes", + "scipy", + "symengine", + "sympy", + "fenics-ufl @ git+https://github.com/firedrakeproject/ufl.git", +] +requires-python = ">=3.10" +authors = [ + {name = "Robert C. Kirby et al.", email = "fenics-dev@googlegroups.com"}, + {name = "Imperial College London and others", email = "david.ham@imperial.ac.uk"}, +] +description = "FInite element Automatic Tabulator" +readme = "README.rst" +classifiers = [ + "Programming Language :: Python", + "License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", +] + +[project.urls] +Repository = "https://github.com/firedrakeproject/fiat.git" + +[project.optional-dependencies] +doc = [ + "setuptools", # for pkg_resources + "sphinx", +] +test = ["pytest"] + +[tool.setuptools] +packages = ["FIAT", "finat", "finat.ufl", "gem"] diff --git a/setup.cfg b/setup.cfg index fdc13e4fb..d5f41d15c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,8 +1,8 @@ [flake8] -ignore = E501,E226,E731,W504, +ignore = E501,E226,E731,W503,W504, # ambiguous variable name E741 -exclude = .git,__pycache__,doc/sphinx/source/conf.py,build,dist +exclude = .git,__pycache__,docs/source/conf.py,build,dist min-version = 3.0 [pydocstyle] diff --git a/setup.py b/setup.py deleted file mode 100755 index d55401a90..000000000 --- a/setup.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python - -import sys - -try: - from setuptools import setup -except ImportError: - from distutils.core import setup - -if sys.version_info < (3, 0): - print("Python 3.0 or higher required, please upgrade.") - sys.exit(1) - -version = "2019.2.0.dev0" - -url = "https://bitbucket.org/fenics-project/fiat/" -tarball = None -if 'dev' not in version: - tarball = url + "downloads/fenics-fiat-%s.tar.gz" % version - -setup( - name="fenics-fiat", - description="FInite element Automatic Tabulator", - version=version, - author="Robert C. Kirby et al.", - author_email="fenics-dev@googlegroups.com", - url=url, - download_url=tarball, - license="LGPL v3 or later", - packages=["FIAT"], - install_requires=[ - "setuptools", "numpy", "recursivenodes", "scipy", "sympy" - ] -) diff --git a/test/regression/.gitignore b/test/FIAT/regression/.gitignore similarity index 100% rename from test/regression/.gitignore rename to test/FIAT/regression/.gitignore diff --git a/test/regression/README.rst b/test/FIAT/regression/README.rst similarity index 100% rename from test/regression/README.rst rename to test/FIAT/regression/README.rst diff --git a/test/regression/conftest.py b/test/FIAT/regression/conftest.py similarity index 100% rename from test/regression/conftest.py rename to test/FIAT/regression/conftest.py diff --git a/test/regression/fiat-reference-data-id b/test/FIAT/regression/fiat-reference-data-id similarity index 100% rename from test/regression/fiat-reference-data-id rename to test/FIAT/regression/fiat-reference-data-id diff --git a/test/regression/scripts/download b/test/FIAT/regression/scripts/download similarity index 100% rename from test/regression/scripts/download rename to test/FIAT/regression/scripts/download diff --git a/test/regression/scripts/getdata b/test/FIAT/regression/scripts/getdata similarity index 100% rename from test/regression/scripts/getdata rename to test/FIAT/regression/scripts/getdata diff --git a/test/regression/scripts/getreferencerepo b/test/FIAT/regression/scripts/getreferencerepo similarity index 100% rename from test/regression/scripts/getreferencerepo rename to test/FIAT/regression/scripts/getreferencerepo diff --git a/test/regression/scripts/parameters b/test/FIAT/regression/scripts/parameters similarity index 100% rename from test/regression/scripts/parameters rename to test/FIAT/regression/scripts/parameters diff --git a/test/regression/scripts/upload b/test/FIAT/regression/scripts/upload similarity index 100% rename from test/regression/scripts/upload rename to test/FIAT/regression/scripts/upload diff --git a/test/regression/test_regression.py b/test/FIAT/regression/test_regression.py similarity index 100% rename from test/regression/test_regression.py rename to test/FIAT/regression/test_regression.py diff --git a/test/unit/test_argyris.py b/test/FIAT/unit/test_argyris.py similarity index 100% rename from test/unit/test_argyris.py rename to test/FIAT/unit/test_argyris.py diff --git a/test/unit/test_awc.py b/test/FIAT/unit/test_awc.py similarity index 100% rename from test/unit/test_awc.py rename to test/FIAT/unit/test_awc.py diff --git a/test/unit/test_awnc.py b/test/FIAT/unit/test_awnc.py similarity index 100% rename from test/unit/test_awnc.py rename to test/FIAT/unit/test_awnc.py diff --git a/test/unit/test_bernstein.py b/test/FIAT/unit/test_bernstein.py similarity index 100% rename from test/unit/test_bernstein.py rename to test/FIAT/unit/test_bernstein.py diff --git a/test/unit/test_discontinuous_pc.py b/test/FIAT/unit/test_discontinuous_pc.py similarity index 100% rename from test/unit/test_discontinuous_pc.py rename to test/FIAT/unit/test_discontinuous_pc.py diff --git a/test/unit/test_discontinuous_taylor.py b/test/FIAT/unit/test_discontinuous_taylor.py similarity index 100% rename from test/unit/test_discontinuous_taylor.py rename to test/FIAT/unit/test_discontinuous_taylor.py diff --git a/test/unit/test_facet_support_dofs.py b/test/FIAT/unit/test_facet_support_dofs.py similarity index 100% rename from test/unit/test_facet_support_dofs.py rename to test/FIAT/unit/test_facet_support_dofs.py diff --git a/test/unit/test_fdm.py b/test/FIAT/unit/test_fdm.py similarity index 100% rename from test/unit/test_fdm.py rename to test/FIAT/unit/test_fdm.py diff --git a/test/unit/test_fiat.py b/test/FIAT/unit/test_fiat.py similarity index 100% rename from test/unit/test_fiat.py rename to test/FIAT/unit/test_fiat.py diff --git a/test/unit/test_gauss_legendre.py b/test/FIAT/unit/test_gauss_legendre.py similarity index 100% rename from test/unit/test_gauss_legendre.py rename to test/FIAT/unit/test_gauss_legendre.py diff --git a/test/unit/test_gauss_lobatto_legendre.py b/test/FIAT/unit/test_gauss_lobatto_legendre.py similarity index 100% rename from test/unit/test_gauss_lobatto_legendre.py rename to test/FIAT/unit/test_gauss_lobatto_legendre.py diff --git a/test/unit/test_gauss_radau.py b/test/FIAT/unit/test_gauss_radau.py similarity index 100% rename from test/unit/test_gauss_radau.py rename to test/FIAT/unit/test_gauss_radau.py diff --git a/test/unit/test_gopalakrishnan_lederer_schoberl.py b/test/FIAT/unit/test_gopalakrishnan_lederer_schoberl.py similarity index 100% rename from test/unit/test_gopalakrishnan_lederer_schoberl.py rename to test/FIAT/unit/test_gopalakrishnan_lederer_schoberl.py diff --git a/test/unit/test_hct.py b/test/FIAT/unit/test_hct.py similarity index 100% rename from test/unit/test_hct.py rename to test/FIAT/unit/test_hct.py diff --git a/test/unit/test_hdivtrace.py b/test/FIAT/unit/test_hdivtrace.py similarity index 100% rename from test/unit/test_hdivtrace.py rename to test/FIAT/unit/test_hdivtrace.py diff --git a/test/unit/test_hierarchical.py b/test/FIAT/unit/test_hierarchical.py similarity index 100% rename from test/unit/test_hierarchical.py rename to test/FIAT/unit/test_hierarchical.py diff --git a/test/unit/test_johnson_mercier.py b/test/FIAT/unit/test_johnson_mercier.py similarity index 100% rename from test/unit/test_johnson_mercier.py rename to test/FIAT/unit/test_johnson_mercier.py diff --git a/test/unit/test_kong_mulder_veldhuizen.py b/test/FIAT/unit/test_kong_mulder_veldhuizen.py similarity index 100% rename from test/unit/test_kong_mulder_veldhuizen.py rename to test/FIAT/unit/test_kong_mulder_veldhuizen.py diff --git a/test/unit/test_macro.py b/test/FIAT/unit/test_macro.py similarity index 100% rename from test/unit/test_macro.py rename to test/FIAT/unit/test_macro.py diff --git a/test/unit/test_mtw.py b/test/FIAT/unit/test_mtw.py similarity index 100% rename from test/unit/test_mtw.py rename to test/FIAT/unit/test_mtw.py diff --git a/test/unit/test_orientation.py b/test/FIAT/unit/test_orientation.py similarity index 100% rename from test/unit/test_orientation.py rename to test/FIAT/unit/test_orientation.py diff --git a/test/unit/test_pointwise_dual.py b/test/FIAT/unit/test_pointwise_dual.py similarity index 100% rename from test/unit/test_pointwise_dual.py rename to test/FIAT/unit/test_pointwise_dual.py diff --git a/test/unit/test_polynomial.py b/test/FIAT/unit/test_polynomial.py similarity index 100% rename from test/unit/test_polynomial.py rename to test/FIAT/unit/test_polynomial.py diff --git a/test/unit/test_powell_sabin.py b/test/FIAT/unit/test_powell_sabin.py similarity index 100% rename from test/unit/test_powell_sabin.py rename to test/FIAT/unit/test_powell_sabin.py diff --git a/test/unit/test_quadrature.py b/test/FIAT/unit/test_quadrature.py similarity index 100% rename from test/unit/test_quadrature.py rename to test/FIAT/unit/test_quadrature.py diff --git a/test/unit/test_quadrature_element.py b/test/FIAT/unit/test_quadrature_element.py similarity index 100% rename from test/unit/test_quadrature_element.py rename to test/FIAT/unit/test_quadrature_element.py diff --git a/test/unit/test_reference_element.py b/test/FIAT/unit/test_reference_element.py similarity index 100% rename from test/unit/test_reference_element.py rename to test/FIAT/unit/test_reference_element.py diff --git a/test/unit/test_regge_hhj.py b/test/FIAT/unit/test_regge_hhj.py similarity index 100% rename from test/unit/test_regge_hhj.py rename to test/FIAT/unit/test_regge_hhj.py diff --git a/test/unit/test_serendipity.py b/test/FIAT/unit/test_serendipity.py similarity index 100% rename from test/unit/test_serendipity.py rename to test/FIAT/unit/test_serendipity.py diff --git a/test/unit/test_stokes_complex.py b/test/FIAT/unit/test_stokes_complex.py similarity index 100% rename from test/unit/test_stokes_complex.py rename to test/FIAT/unit/test_stokes_complex.py diff --git a/test/unit/test_tensor_product.py b/test/FIAT/unit/test_tensor_product.py similarity index 100% rename from test/unit/test_tensor_product.py rename to test/FIAT/unit/test_tensor_product.py diff --git a/test/README b/test/README deleted file mode 100644 index 6a6f779f4..000000000 --- a/test/README +++ /dev/null @@ -1,6 +0,0 @@ -Run tests by:: - - py.test [--skip-download] - py.test [--skip-download] regression/ - py.test unit/ - py.test unit/foo.py diff --git a/test/fiat_mapping.py b/test/finat/fiat_mapping.py similarity index 100% rename from test/fiat_mapping.py rename to test/finat/fiat_mapping.py diff --git a/test/test_direct_serendipity.py b/test/finat/test_direct_serendipity.py similarity index 100% rename from test/test_direct_serendipity.py rename to test/finat/test_direct_serendipity.py diff --git a/test/test_hash.py b/test/finat/test_hash.py similarity index 100% rename from test/test_hash.py rename to test/finat/test_hash.py diff --git a/test/test_mass_conditioning.py b/test/finat/test_mass_conditioning.py similarity index 100% rename from test/test_mass_conditioning.py rename to test/finat/test_mass_conditioning.py diff --git a/test/test_point_evaluation_ciarlet.py b/test/finat/test_point_evaluation_ciarlet.py similarity index 100% rename from test/test_point_evaluation_ciarlet.py rename to test/finat/test_point_evaluation_ciarlet.py diff --git a/test/test_restriction.py b/test/finat/test_restriction.py similarity index 100% rename from test/test_restriction.py rename to test/finat/test_restriction.py diff --git a/test/test_zany_mapping.py b/test/finat/test_zany_mapping.py similarity index 100% rename from test/test_zany_mapping.py rename to test/finat/test_zany_mapping.py