Merge lp:~fluidity-core/fluidity/firedrake-mixed-space-expressions into lp:fluidity/firedrake

Proposed by Lawrence Mitchell
Status: Rejected
Rejected by: Lawrence Mitchell
Proposed branch: lp:~fluidity-core/fluidity/firedrake-mixed-space-expressions
Merge into: lp:fluidity/firedrake
Diff against target: 501 lines (+280/-52)
4 files modified
python/firedrake/assemble_expressions.py (+99/-37)
python/firedrake/core_types.pyx (+130/-13)
python/firedrake/ufl_expr.py (+7/-0)
tests/firedrake_expressions/test_firedrake_expressions.py (+44/-2)
To merge this branch: bzr merge lp:~fluidity-core/fluidity/firedrake-mixed-space-expressions
Reviewer Review Type Date Requested Status
David Ham Needs Fixing
Review via email: mp+183478@code.launchpad.net

Description of the change

Adds minimal infrastructure for mixed function spaces, along with support for expression evaluation.

LHS form assembly does not work correctly, because of a mismatch in what pyop2 wants and ffc generates. I believe that is on someone's roadmap.

So this is more an early review request to see if this the correct approach.

As a bonus, expression evaluation is fixed for vector function spaces. And errors are correctly raised for augmented assignment on mismatched function spaces.

To post a comment you must log in.
Revision history for this message
David Ham (david-ham) wrote :

Please add docstrings, especially in core_types.pyx

Please add tests for such functionality as exists. For example firedrake_expressions could be expanded or duplicated to have some mixed functionspace cases in it.

Does any of the PyOP2 interaction work? For example if RHS assembly works then a simple integral test could be included.

review: Needs Fixing
Revision history for this message
Lawrence Mitchell (wence) wrote :

I believe RHS assembly doesn't work at all yet, but Florian suggests that may be here soon.

I'll fix up the rest.

4297. By Lawrence Mitchell

Add docstrings to MixedFunctionSpace

4298. By Lawrence Mitchell

Add some tests of mixed space expressions

Revision history for this message
Lawrence Mitchell (wence) wrote :

docstrings and some tests added.

Note that for reasons I don't profess to understand the following does not work:

U = FunctionSpace(mesh, 'DG', 0)
W = U*U

w = Function(W)
v = Function(W)
w.assign(2*v)
 => AttributeError: 'ComponentTensor' object has no attribute '_operands'

Equally:

w.assign(v**2)
 => generated code is bad
w.assign(v**v)
 => UFLException: Cannot take the power of a non-scalar expression.

So this still needs some work.

Revision history for this message
Lawrence Mitchell (wence) wrote :

So, foo**bar doesn't work at all for mixed spaces. This is the same as dolfin. I think it would be possible to make it work, but I'm not sure if it actually makes sense. The error is deep in UFL.

The component tensor error I think needs one to unpick more UFL classes.

The other option is to lie about the ufl_element in the mixed function spaces. If I say that

fs._ufl_element = self._function_spaces[0]._ufl_element then everything works. But that's tremendously icky.

4299. By Lawrence Mitchell

Implement more expressions on mixed spaces

Instead of carrying around a slot on every class we need to index, just
keep a global index object that we look at if trying to generate an
expression over a mixed space.

This simplifies the code, and makes implementing correct expression
assembly for Indexed and ComponentTensor expressions much easier. So
do that, and add appropriate tests.

4300. By Lawrence Mitchell

Add more docstrings

Revision history for this message
Lawrence Mitchell (wence) wrote :

So I fixed more of the things, so that now:

a.assign(3*b + c)

on mixed spaces should work.

Revision history for this message
Florian Rathgeber (florian-rathgeber) wrote :

I've merged this into the firedrake-mixed branch for now since it depends on the mixed data types.

Revision history for this message
Lawrence Mitchell (wence) wrote :

This will come in with the mixed space stuff since it now lives there, so this merge proposal is obsoleted.

Unmerged revisions

4300. By Lawrence Mitchell

Add more docstrings

4299. By Lawrence Mitchell

Implement more expressions on mixed spaces

Instead of carrying around a slot on every class we need to index, just
keep a global index object that we look at if trying to generate an
expression over a mixed space.

This simplifies the code, and makes implementing correct expression
assembly for Indexed and ComponentTensor expressions much easier. So
do that, and add appropriate tests.

4298. By Lawrence Mitchell

Add some tests of mixed space expressions

4297. By Lawrence Mitchell

Add docstrings to MixedFunctionSpace

4296. By Lawrence Mitchell

Support expressions on mixed spaces

As a bonus, this fixes expressions on VectorFunctionSpaces and
correctly raises an error for augmented assignment on mismatching
FunctionSpaces.

4295. By Lawrence Mitchell

Add rudimentary support for mixed function spaces

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'python/firedrake/assemble_expressions.py'
2--- python/firedrake/assemble_expressions.py 2013-08-27 15:14:32 +0000
3+++ python/firedrake/assemble_expressions.py 2013-09-03 15:11:37 +0000
4@@ -5,6 +5,11 @@
5 import core_types
6 import cgen
7
8+# Global state telling us which function space in a mixed space we're indexing into
9+# while generating the kernel.
10+global _fs_idx
11+_fs_idx = None
12+
13 class DummyFunction(ufl.Coefficient):
14 """A dummy object to take the place of a Function in the
15 expression. This has the sole role of producing the right strings when
16@@ -21,16 +26,29 @@
17 self.intent = intent
18
19 def __str__(self):
20- if (self.function.rank()==0):
21+ # To generate the correct kernel body for a mixed function
22+ # space we need to know which of the sub spaces we're
23+ # currently generating for. The only way to do this is to
24+ # peek at some hidden state.
25+ if _fs_idx is not None:
26+ fs = self.function.function_space()[_fs_idx]
27+ else:
28+ fs = self.function.function_space()
29+ if isinstance(fs, core_types.VectorFunctionSpace):
30+ return "fn_%d[dim]" % self.argnum
31+ else:
32 return "fn_%d[0]" % self.argnum
33- else:
34- return "fn_%d[0][dim]" % self.argnum
35
36 @property
37 def arg(self):
38- argtype = self.function.dat.ctype+"*"
39- if (self.function.rank()>0):
40- arg += "*"
41+ if _fs_idx is not None:
42+ dat = self.function.dat[_fs_idx]
43+ fs = self.function.function_space()[_fs_idx]
44+ else:
45+ dat = self.function.dat
46+ fs = self.function.function_space()
47+ argtype = dat.ctype+"*"
48+
49 name = " fn_" + `self.argnum`
50
51 return cgen.Value(argtype, name)
52@@ -69,8 +87,13 @@
53 new_lhs.intent = op2.RW
54
55 except KeyError:
56+ if transformer._function_space is None:
57+ transformer._function_space = lhs._function_space
58+ elif transformer._function_space != lhs._function_space:
59+ raise ValueError("Expression has incompatible function spaces")
60 transformer._args[lhs] = DummyFunction(lhs, len(transformer._args),
61 intent=op2.WRITE)
62+ transformer._function_space = lhs._function_space
63 new_lhs = transformer._args[lhs]
64
65 return [new_lhs, self._operands[1]]
66@@ -89,6 +112,10 @@
67 try:
68 new_lhs = transformer._args[lhs]
69 except KeyError:
70+ if transformer._function_space is None:
71+ transformer._function_space = lhs._function_space
72+ elif transformer._function_space != lhs._function_space:
73+ raise ValueError("Expression has incompatible function spaces")
74 transformer._args[lhs] = DummyFunction(lhs, len(transformer._args))
75 new_lhs = transformer._args[lhs]
76
77@@ -139,6 +166,20 @@
78 def __str__(self):
79 return "log(%s)" % str(self._argument)
80
81+
82+class Indexed(ufl.indexed.Indexed):
83+ """Subclass of :class:`ufl.indexed.Indexed` which prints the unindexed expression."""
84+
85+ def __str__(self):
86+ return "%s" % self._expression
87+
88+class ComponentTensor(ufl.tensors.ComponentTensor):
89+ """Subclass of :class:`ufl.tensors.ComponentTensor` which prints the expression
90+without any indexed set notation."""
91+
92+ def __str__(self):
93+ return "%s" % self._expression
94+
95 class ExpressionWalker(ReuseTransformer):
96 def __init__(self):
97 ReuseTransformer.__init__(self)
98@@ -148,14 +189,11 @@
99 self._result = None
100
101 def coefficient(self, o):
102-
103 if isinstance(o, core_types.Function):
104 if self._function_space is None:
105 self._function_space = o._function_space
106- else:
107- if self._function_space != o._function_space:
108- raise ValueError("Expression has incompatible function spaces")
109-
110+ elif self._function_space != o._function_space:
111+ raise ValueError("Expression has incompatible function spaces")
112 try:
113 arg = self._args[o]
114 if arg.intent == op2.WRITE:
115@@ -179,6 +217,12 @@
116 condition = ReuseTransformer.reuse_if_possible
117 math_function = ReuseTransformer.reuse_if_possible
118
119+ def component_tensor(self, o, *operands):
120+ return ComponentTensor(*operands)
121+
122+ def indexed(self, o, *operands):
123+ return Indexed(*operands)
124+
125 def power(self, o, *operands):
126 # Need to convert notation to c for exponents.
127 return Power(*operands)
128@@ -198,7 +242,6 @@
129 operands = [operands[0], self.visit(operands[1])]
130
131 else:
132-
133 # For all other operators, just visit the children.
134 operands = map(self.visit, o._operands)
135
136@@ -208,41 +251,60 @@
137 """Produce a :class:`PyOP2.Kernel` from the processed UFL expression
138 expr and the corresponding args.
139 """
140-
141- fs = args[0].function.function_space()
142-
143- body=cgen.Block()
144-
145- if isinstance(fs, core_types.VectorFunctionSpace):
146- body.append(
147- [cgen.Value("int", "dim"),
148- cgen.For("dim = 0",
149- "dim < %s"%fs.dof_dset.cdim,
150- "++i",
151- cgen.Line(str(expr)+";")
152+ def gen(fs, idx=None):
153+ body=cgen.Block()
154+ global _fs_idx
155+ _fs_idx = idx
156+ if isinstance(fs, core_types.VectorFunctionSpace):
157+ body.extend([cgen.Value("int", "dim"),
158+ cgen.For("dim = 0",
159+ "dim < %s"%fs.dof_dset.cdim,
160+ "++dim",
161+ cgen.Line(str(expr)+";")
162 )
163 ]
164 )
165+ else:
166+ body.append(cgen.Line(str(expr)+";"))
167+
168+ fdecl = cgen.FunctionDeclaration(
169+ cgen.Value("void", "expression"),
170+ [arg.arg for arg in args])
171+
172+ kernel = str(cgen.FunctionBody(fdecl, body))
173+ _fs_idx = None
174+ return op2.Kernel(kernel, "expression")
175+
176+ space = args[0].function.function_space()
177+
178+ if isinstance(space, core_types.MixedFunctionSpace):
179+ for i, subspace in enumerate(space.split()):
180+ yield gen(subspace, i)
181 else:
182- body.append(cgen.Line(str(expr)+";"))
183-
184- fdecl = cgen.FunctionDeclaration(
185- cgen.Value("void", "expression"),
186- [arg.arg for arg in args])
187-
188- return op2.Kernel(str(cgen.FunctionBody(fdecl, body)), "expression")
189+ yield gen(space)
190+
191
192 def evaluate_preprocessed_expression(expr, args):
193
194- kernel = expression_kernel(expr, args)
195+ kernels = expression_kernel(expr, args)
196
197 fs = args[0].function._function_space
198
199- parloop_args = [kernel, fs.node_set] +\
200- [arg.function.dat(arg.intent)
201- for arg in args ]
202-
203- op2.par_loop(*parloop_args)
204+ if isinstance(fs, core_types.MixedFunctionSpace):
205+ spaces = fs.split()
206+ else:
207+ spaces = [fs]
208+
209+ for i, (kernel, space) in enumerate(zip(kernels, spaces)):
210+ parloop_args = [kernel, space.node_set]
211+ for arg in args:
212+ if isinstance(fs, core_types.MixedFunctionSpace):
213+ dat = arg.function.dat[i]
214+ else:
215+ dat = arg.function.dat
216+ parloop_args.append(dat(arg.intent))
217+
218+ op2.par_loop(*parloop_args)
219
220 def evaluate_expression(expr):
221 """Evaluates UFL expressions on Functions.
222
223=== modified file 'python/firedrake/core_types.pyx'
224--- python/firedrake/core_types.pyx 2013-08-30 10:55:57 +0000
225+++ python/firedrake/core_types.pyx 2013-09-03 15:11:37 +0000
226@@ -443,12 +443,11 @@
227 this :class:`FunctionSpace`."""
228 return op2.DataSet(self.node_set, self.dim)
229
230- def make_dat(self):
231+ def make_dat(self, val=None, uid=None):
232 """Return a newly allocated :class:`pyop2.Dat` defined on the
233 :attr:`dof.dset` of this :class:`Function`."""
234-
235- return op2.Dat(self.dof_dset, np.zeros(self.dof_count),
236- valuetype)
237+ data = val if val is not None else np.zeros(self.dof_count)
238+ return op2.Dat(self.dof_dset, data, valuetype, uid=uid)
239
240
241 @utils.cached_property
242@@ -475,6 +474,12 @@
243 """The :class:`Mesh` used to construct this :class:`FunctionSpace`."""
244 return self._mesh
245
246+ def __mul__(self, other):
247+ """Create a :class:`MixedFunctionSpace` composed of this
248+ :class:`FunctionSpace` and other"""
249+ return MixedFunctionSpace(self, other)
250+
251+
252 class VectorFunctionSpace(FunctionSpace):
253 def __init__(self, mesh, family, degree, dim = None, name = None):
254 super(VectorFunctionSpace, self).__init__(mesh, family, degree, name)
255@@ -486,6 +491,125 @@
256 else:
257 self._dim = dim
258
259+
260+class MixedFunctionSpace(FunctionSpace):
261+ """A representation of a mixed function space."""
262+ def __init__(self, *args, **kwargs):
263+ """Build a :class:`MixedFunctionSpace` from some number of
264+ :class:`FunctionSpaces`.
265+
266+ :arg args: base :class:`FunctionSpace`\s to be composed.
267+ This can also be a list or tuple of :class:`FunctionSpace`\s (for
268+ Dolfin compatibilty).
269+ :arg name: set the name of the :class:`MixedFunctionSpace`."""
270+ tmp = []
271+ def flatten(a):
272+ for arg in a:
273+ if isinstance(arg, FunctionSpace):
274+ tmp.append(arg)
275+ elif isinstance(arg, MixedFunctionSpace):
276+ flatten(arg)
277+ else:
278+ raise RuntimeError("Don't know how to build a MixedFunctionSpace using a %s" % type(arg))
279+ if len(args) == 1:
280+ args = args[0]
281+ flatten(args)
282+ self._function_spaces = tuple(tmp)
283+ mesh = self._function_spaces[0].mesh()
284+ for fs in self._function_spaces:
285+ assert fs.mesh() == mesh, \
286+ "Mismatching meshes in mixed function space definition"
287+ self._mesh = mesh
288+ self._ufl_element = ufl.MixedElement(*[fs.ufl_element() \
289+ for fs in self._function_spaces])
290+ self.name = kwargs.get('name')
291+
292+ def split(self):
293+ """The :class:`FunctionSpace`\s this
294+ :class:`MixedFunctionSpace` is composed of"""
295+ return self._function_spaces
296+
297+ def sub(self, i):
298+ """Return the `i`th :class:`FunctionSpace` in this :class:`MixedFunctionSpace`."""
299+ return self[i]
300+
301+ def num_sub_spaces(self):
302+ """Return the number of :class:`FunctionSpace`\s this
303+ :class:`MixedFunctionSpace` is composed of."""
304+ return len(self)
305+
306+ def __len__(self):
307+ """Return the number of :class:`FunctionSpace`\s this
308+ :class:`MixedFunctionSpace` is composed of."""
309+ return len(self._function_spaces)
310+
311+ def __getitem__(self, i):
312+ """Return the `i`th :class:`FunctionSpace` in this
313+ :class:`MixedFunctionSpace`."""
314+ return self._function_spaces[i]
315+
316+ @property
317+ def dim(self):
318+ """Return a tuple of the :attr:`FunctionSpace.dim`\s of the
319+ :class:`FunctionSpace`\s this :class:`MixedFunctionSpace` is
320+ composed of."""
321+ return tuple(fs.dim for fs in self._function_spaces)
322+
323+ @property
324+ def node_count(self):
325+ """Return a tuple of the :attr:`FunctionSpace.node_count`\s of the
326+ :class:`FunctionSpace`\s this :class:`MixedFunctionSpace` is
327+ composed of."""
328+ return tuple(fs.node_count for fs in self._function_spaces)
329+
330+ @property
331+ def dof_count(self):
332+ """Return a tuple of the :attr:`FunctionSpace.dof_count`\s of the
333+ :class:`FunctionSpace`\s this :class:`MixedFunctionSpace` is
334+ composed of."""
335+ return tuple(fs.dof_count for fs in self._function_spaces)
336+
337+ @utils.cached_property
338+ def node_set(self):
339+ """A :class:`pyop2.MixedSet` containing the nodes of this
340+ :class:`MixedFunctionSpace`. This is composed of the
341+ :attr:`FunctionSpace.node_set`\s of the underlying
342+ :class:`FunctionSpace`\s this :class:`MixedFunctionSpace` is
343+ composed of."""
344+ return op2.MixedSet([fs.node_set for fs in self._function_spaces])
345+
346+ @utils.cached_property
347+ def dof_dset(self):
348+ """A :class:`pyop2.MixedDataSet` containing the degrees of freedom of
349+ this :class:`MixedFunctionSpace`. This is composed of the
350+ :attr:`FunctionSpace.dof_dset`\s of the underlying
351+ :class:`FunctionSpace`\s this :class:`MixedFunctionSpace` is
352+ composed of."""
353+ return op2.MixedDataSet([fs.dof_dset for fs in self._function_spaces])
354+
355+ @utils.cached_property
356+ def cell_node_map(self):
357+ """A :class:`pyop2.MixedMap` from the :attr:`Mesh.cell_set` of the
358+ underlying mesh to the :attr:`node_set` of this
359+ :class:`MixedFunctionSpace`. This is composed of the
360+ :attr:`FunctionSpace.cell_node_map`\s of the underlying
361+ :class:`FunctionSpace`\s this :class:`MixedFunctionSpace` is
362+ composed of."""
363+ return op2.MixedMap([fs.cell_node_map for fs in self._function_spaces])
364+
365+ def make_dat(self, vals=None, uid=None):
366+ """Build a :class:`pyop2.MixedDat` on this
367+ :class:`MixedFunctionSpace`. If `vals` is provided, it should
368+ be an iterable of numpy data arrays of the same length as the
369+ :class:`MixedFunctionSpace`."""
370+ if vals is not None:
371+ assert len(vals) == len(self)
372+ else:
373+ vals = [None] * len(self)
374+ return op2.MixedDat([fs.make_dat(val, uid) \
375+ for fs, val in zip(self._function_spaces, vals)])
376+
377+
378 class Function(ufl.Coefficient):
379 """A :class:`Function` represents a discretised field over the
380 domain defined by the underlying :class:`Mesh`. Functions are
381@@ -507,7 +631,7 @@
382
383 if isinstance(function_space, Function):
384 self._function_space = function_space._function_space
385- elif isinstance(function_space, FunctionSpace):
386+ elif isinstance(function_space, (MixedFunctionSpace, FunctionSpace)):
387 self._function_space = function_space
388 else:
389 raise NotImplementedError("Can't make a Function defined on a "
390@@ -518,13 +642,7 @@
391 self.name = name
392 self.uid = _new_uid()
393
394- if val is not None:
395- self.dat = op2.Dat(self.dof_dset, val, valuetype, self.name,
396- uid=self.uid)
397- else:
398- self.dat = op2.Dat(self.dof_dset,
399- np.zeros(self._function_space.dof_count),
400- valuetype, self.name, uid=self.uid)
401+ self.dat = self._function_space.make_dat(val, self.uid)
402
403 self._repr = None
404
405@@ -656,4 +774,3 @@
406 assemble_expressions.IDiv(self, expr))
407
408 return self
409-
410
411=== modified file 'python/firedrake/ufl_expr.py'
412--- python/firedrake/ufl_expr.py 2013-08-27 15:14:32 +0000
413+++ python/firedrake/ufl_expr.py 2013-09-03 15:11:37 +0000
414@@ -2,6 +2,7 @@
415 import ufl.argument
416 from ufl.assertions import ufl_assert
417 from ufl.finiteelement import FiniteElementBase
418+from ufl.split_functions import split
419
420 class Argument(ufl.argument.Argument):
421
422@@ -41,3 +42,9 @@
423
424 def TrialFunction(function_space):
425 return Argument(function_space.ufl_element(), function_space, -1)
426+
427+def TestFunctions(function_space):
428+ return split(TestFunction(function_space))
429+
430+def TrialFunctions(function_space):
431+ return split(TrialFunction(function_space))
432
433=== modified file 'tests/firedrake_expressions/test_firedrake_expressions.py'
434--- tests/firedrake_expressions/test_firedrake_expressions.py 2013-08-28 09:58:18 +0000
435+++ tests/firedrake_expressions/test_firedrake_expressions.py 2013-09-03 15:11:37 +0000
436@@ -5,8 +5,12 @@
437 mesh = fd.Mesh("square-2d.node")
438
439 fs = fd.FunctionSpace(mesh, "Lagrange", 1)
440-
441+
442+mixed = fs * fd.FunctionSpace(mesh, "DG", 1)
443+
444 f = fd.Function(fs, name="f")
445+mf = fd.Function(mixed, name="mixed")
446+
447 one = fd.Function(fs, name="one")
448 two = fd.Function(fs, name="two")
449 minusthree = fd.Function(fs, name="minusthree")
450@@ -48,6 +52,35 @@
451 return (str(f)+" /= "+str(expr)+", "+str(f), x,
452 np.all(f.dat.data == x))
453
454+def mixed_assign(f, expr, x):
455+ f.assign(expr)
456+ return (str(f)+" = "+str(expr)+", "+str(f), x,
457+ np.all([np.all(data == x) for data in f.dat.data]))
458+
459+def mixed_iadd(f, expr, x):
460+ f += expr
461+ return (str(f)+" += "+str(expr)+", "+str(f), x,
462+ np.all([np.all(data == x) for data in f.dat.data]))
463+
464+def mixed_isub(f, expr, x):
465+ f -= expr
466+ return (str(f)+" -= "+str(expr)+", "+str(f), x,
467+ np.all([np.all(data == x) for data in f.dat.data]))
468+
469+def mixed_imul(f, expr, x):
470+ f *= expr
471+ return (str(f)+" *= "+str(expr)+", "+str(f), x,
472+ np.all([np.all(data == x) for data in f.dat.data]))
473+
474+def mixed_idiv(f, expr, x):
475+ f /= expr
476+ return (str(f)+" /= "+str(expr)+", "+str(f), x,
477+ np.all([np.all(data == x) for data in f.dat.data]))
478+
479+def mixed_assign(f, expr, x):
480+ f.assign(expr)
481+ return (str(f)+" = "+str(expr)+", "+str(f), x,
482+ np.all([np.all(data == x) for data in f.dat.data]))
483 tests = [
484 test(one + two, 3),
485 test(ufl.ln(one), 0),
486@@ -59,7 +92,16 @@
487 iaddtest(f, 2, 7),
488 isubtest(f, 2, 5),
489 imultest(f, 2, 10),
490- idivtest(f, 2, 5)
491+ idivtest(f, 2, 5),
492+ mixed_assign(mf, 1, 1),
493+ mixed_iadd(mf, 2, 3),
494+ mixed_iadd(mf, mf, 6),
495+ mixed_isub(mf, 10, -4),
496+ mixed_idiv(mf, -4, 1),
497+ mixed_imul(mf, 10, 10),
498+ mixed_idiv(mf, mf, 1),
499+ mixed_assign(mf, mf + mf, 2),
500+ mixed_assign(mf, 2*mf, 4)
501 ]
502
503

Subscribers

People subscribed via source and target branches

to all changes: