forked from FEniCS/fiat
-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathreference_element.py
More file actions
2011 lines (1606 loc) · 70.2 KB
/
Copy pathreference_element.py
File metadata and controls
2011 lines (1606 loc) · 70.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Copyright (C) 2008 Robert C. Kirby (Texas Tech University)
#
# This file is part of FIAT (https://www.fenicsproject.org)
#
# SPDX-License-Identifier: LGPL-3.0-or-later
#
# Modified by David A. Ham (david.ham@imperial.ac.uk), 2014
# Modified by Lizao Li (lzlarryli@gmail.com), 2016
"""
Abstract class and particular implementations of finite element
reference simplex geometry/topology.
Provides an abstract base class and particular implementations for the
reference simplex geometry and topology.
The rest of FIAT is abstracted over this module so that different
reference element geometry (e.g. a vertex at (0,0) versus at (-1,-1))
and orderings of entities have a single point of entry.
Currently implemented are UFC and Default Line, Triangle and Tetrahedron.
"""
import operator
from collections import defaultdict
from functools import reduce
from itertools import chain, count, product
from math import factorial
import numpy
from gem.utils import safe_repr
from recursivenodes.nodes import _decode_family, _recursive
from FIAT.orientation_utils import (
Orientation,
make_cell_orientation_reflection_map_simplex,
make_cell_orientation_reflection_map_tensorproduct,
make_entity_permutations_simplex,
)
POINT = 0
LINE = 1
TRIANGLE = 2
TETRAHEDRON = 3
QUADRILATERAL = 11
HEXAHEDRON = 111
TENSORPRODUCT = 99
hypercube_shapes = {2: QUADRILATERAL, 3: HEXAHEDRON}
def multiindex_equal(d, isum, imin=0):
"""A generator for d-tuple multi-indices whose sum is isum and minimum is imin.
"""
if d <= 0:
return
imax = isum - (d - 1) * imin
if imax < imin:
return
for i in range(imin, imax):
for a in multiindex_equal(d - 1, isum - i, imin=imin):
yield a + (i,)
yield (imin,) * (d - 1) + (imax,)
def lattice_iter(start, finish, depth):
"""Generator iterating over the depth-dimensional lattice of
integers between start and (finish-1). This works on simplices in
0d, 1d, 2d, 3d, and beyond"""
if depth == 0:
yield tuple()
elif depth == 1:
for ii in range(start, finish):
yield (ii,)
else:
for ii in range(start, finish):
for jj in lattice_iter(start, finish - ii, depth - 1):
yield jj + (ii,)
def make_lattice(verts, n, interior=0, variant=None):
"""Constructs a lattice of points on the simplex defined by verts.
For example, the 1:st order lattice will be just the vertices.
The optional argument interior specifies how many points from
the boundary to omit. For example, on a line with n = 2,
and interior = 0, this function will return the vertices and
midpoint, but with interior = 1, it will only return the
midpoint."""
if variant is None:
variant = "equispaced"
recursivenodes_families = {
"equispaced": "equi",
"equispaced_interior": "equi_interior",
"gll": "lgl"}
family = recursivenodes_families.get(variant, variant)
family = _decode_family(family)
D = len(verts)
X = numpy.array(verts)
get_point = lambda alpha: tuple(numpy.dot(_recursive(D - 1, n, alpha, family), X))
return list(map(get_point, multiindex_equal(D, n, interior)))
def linalg_subspace_intersection(A, B):
"""Computes the intersection of the subspaces spanned by the
columns of 2-dimensional arrays A,B using the algorithm found in
Golub and van Loan (3rd ed) p. 604. A should be in
R^{m,p} and B should be in R^{m,q}. Returns an orthonormal basis
for the intersection of the spaces, stored in the columns of
the result."""
# check that vectors are in same space
if A.shape[0] != B.shape[0]:
raise Exception("Dimension error")
# A,B are matrices of column vectors
# compute the intersection of span(A) and span(B)
# Compute the principal vectors/angles between the subspaces, G&vL
# p.604
(qa, _ra) = numpy.linalg.qr(A)
(qb, _rb) = numpy.linalg.qr(B)
C = numpy.dot(numpy.transpose(qa), qb)
(y, c, _zt) = numpy.linalg.svd(C)
U = numpy.dot(qa, y)
rank_c = len([s for s in c if numpy.abs(1.0 - s) < 1.e-10])
return U[:, :rank_c]
class Cell:
"""Abstract class for a reference cell. Provides accessors for
geometry (vertex coordinates) as well as topology (orderings of
vertices that make up edges, faces, etc."""
def __init__(self, shape, vertices, topology):
"""The constructor takes a shape code, the physical vertices expressed
as a list of tuples of numbers, and the topology of a cell.
The topology is stored as a dictionary of dictionaries t[i][j]
where i is the dimension and j is the index of the facet of
that dimension. The result is a list of the vertices
comprising the facet."""
self.shape = shape
self.vertices = vertices
self.topology = topology
# Given the topology, work out for each entity in the cell,
# which other entities it contains.
self.sub_entities = {}
for dim, entities in topology.items():
self.sub_entities[dim] = {}
for e, v in entities.items():
vertices = frozenset(v)
sub_entities = []
for dim_, entities_ in topology.items():
for e_, vertices_ in entities_.items():
if vertices.issuperset(vertices_):
sub_entities.append((dim_, e_))
# Sort for the sake of determinism and by UFC conventions
self.sub_entities[dim][e] = sorted(sub_entities)
# Build super-entity dictionary by inverting the sub-entity dictionary
self.super_entities = {dim: {entity: [] for entity in topology[dim]} for dim in topology}
for dim0 in topology:
for e0 in topology[dim0]:
for dim1, e1 in self.sub_entities[dim0][e0]:
self.super_entities[dim1][e1].append((dim0, e0))
# Build connectivity dictionary for easier queries
self.connectivity = {}
for dim0 in sorted(topology):
for dim1 in sorted(topology):
self.connectivity[(dim0, dim1)] = []
for entity in sorted(topology[dim0]):
children = self.sub_entities[dim0][entity]
parents = self.super_entities[dim0][entity]
for dim1 in sorted(topology):
neighbors = children if dim1 < dim0 else parents
d01_entities = tuple(e for d, e in neighbors if d == dim1)
self.connectivity[(dim0, dim1)].append(d01_entities)
# Dictionary with derived cells
self._split_cache = {}
def __repr__(self):
return f"{type(self).__name__}({self.shape!r}, {safe_repr(self.vertices)}, {self.topology!r})"
def _key(self):
"""Hashable object key data (excluding type)."""
# Default: only type matters
return None
def __hash__(self):
return hash((type(self), self._key()))
def get_shape(self):
"""Returns the code for the element's shape."""
return self.shape
def get_vertices(self):
"""Returns an iterable of the element's vertices, each stored as a
tuple."""
return self.vertices
def get_spatial_dimension(self):
"""Returns the spatial dimension in which the element lives."""
return len(self.vertices[0])
def get_topology(self):
"""Returns a dictionary encoding the topology of the element.
The dictionary's keys are the spatial dimensions (0, 1, ...)
and each value is a dictionary mapping."""
return self.topology
def get_connectivity(self):
"""Returns a dictionary encoding the connectivity of the element.
The dictionary's keys are the spatial dimensions pairs ((1, 0),
(2, 0), (2, 1), ...) and each value is a list with entities
of second dimension ordered by local dim0-dim1 numbering."""
return self.connectivity
def get_vertices_of_subcomplex(self, t):
"""Returns the tuple of vertex coordinates associated with the labels
contained in the iterable t."""
return tuple(self.vertices[ti] for ti in t)
def get_dimension(self):
"""Returns the subelement dimension of the cell. For tensor
product cells, this a tuple of dimensions for each cell in the
product. For all other cells, this is the same as the spatial
dimension."""
raise NotImplementedError("Should be implemented in a subclass.")
def construct_subelement(self, dimension):
"""Constructs the reference element of a cell subentity
specified by subelement dimension.
:arg dimension: `tuple` for tensor product cells, `int` otherwise
"""
raise NotImplementedError("Should be implemented in a subclass.")
def construct_subcomplex(self, dimension):
"""Constructs the reference subcomplex of the parent cell subentity
specified by subcomplex dimension.
:arg dimension: `tuple` for tensor product cells, `int` otherwise
"""
if self.get_parent() is None:
return self.construct_subelement(dimension)
raise NotImplementedError("Should be implemented in a subclass.")
def get_entity_transform(self, dim, entity_i):
"""Returns a mapping of point coordinates from the
`entity_i`-th subentity of dimension `dim` to the cell.
:arg dim: `tuple` for tensor product cells, `int` otherwise
:arg entity_i: entity number (integer)
"""
raise NotImplementedError("Should be implemented in a subclass.")
def point_entity_ids(self, points, tol=1E-10):
"""Returns the topological entity association for points on this cell."""
raise NotImplementedError("Should be implemented in a subclass.")
def symmetry_group_size(self, dim):
"""Returns the size of the symmetry group of an entity of
dimension `dim`."""
raise NotImplementedError("Should be implemented in a subclass.")
def cell_orientation_reflection_map(self):
"""Return the map indicating whether each possible cell orientation causes reflection (``1``) or not (``0``)."""
raise NotImplementedError("Should be implemented in a subclass.")
def extract_extrinsic_orientation(self, o):
"""Extract extrinsic orientation.
Parameters
----------
o : Orientation
Total orientation.
Returns
-------
Orientation
Extrinsic orientation.
"""
raise NotImplementedError("Should be implemented in a subclass.")
def extract_intrinsic_orientation(self, o, axis):
"""Extract intrinsic orientation.
Parameters
----------
o : Orientation
Total orientation.
axis : int
Reference cell axis for which intrinsic orientation is computed.
Returns
-------
Orientation
Intrinsic orientation.
"""
raise NotImplementedError("Should be implemented in a subclass.")
@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).
"""
raise NotImplementedError("Should be implemented in a subclass.")
def is_simplex(self):
return False
def is_macrocell(self):
return False
def get_interior_facets(self, dim):
"""Return the interior facets this cell is a split and () otherwise."""
return ()
def get_parent(self):
"""Return the parent cell if this cell is a split and None otherwise."""
return None
def get_parent_complex(self):
"""Return the parent complex if this cell is a split and None otherwise."""
return None
def is_parent(self, other, strict=False):
"""Return whether this cell is the parent of the other cell."""
parent = other
if strict:
parent = parent.get_parent_complex()
while parent is not None:
if self == parent:
return True
parent = parent.get_parent_complex()
return False
def __eq__(self, other):
if self is other:
return True
A, B = self.get_vertices(), other.get_vertices()
if not (len(A) == len(B) and numpy.allclose(A, B)):
return False
atop = self.get_topology()
btop = other.get_topology()
for dim in atop:
if set(atop[dim].values()) != set(btop[dim].values()):
return False
return True
def __ne__(self, other):
return not self.__eq__(other)
def __gt__(self, other):
return other.is_parent(self, strict=True)
def __lt__(self, other):
return self.is_parent(other, strict=True)
def __ge__(self, other):
return other.is_parent(self, strict=False)
def __le__(self, other):
return self.is_parent(other, strict=False)
class SimplicialComplex(Cell):
r"""Abstract class for a simplicial complex.
This consists of list of vertex locations and a topology map defining facets.
"""
def __init__(self, shape, vertices, topology):
# Make sure that every facet has the right number of vertices to be
# a simplex.
for dim in topology:
for entity in topology[dim]:
assert len(topology[dim][entity]) == dim + 1
super().__init__(shape, vertices, topology)
def compute_normal(self, facet_i, cell=None):
"""Returns the unit normal vector to facet i of codimension 1."""
t = self.get_topology()
sd = self.get_spatial_dimension()
# To handle simplicial complex case:
# Find a subcell of which facet_i is on the boundary
# Note: this is trivial and vastly overengineered for the single-cell
# case.
if cell is None:
cell = next(k for k, facets in enumerate(self.connectivity[(sd, sd-1)])
if facet_i in facets)
verts = numpy.asarray(self.get_vertices_of_subcomplex(t[sd][cell]))
# Interval case
if self.get_shape() == LINE:
v_i = t[1][cell].index(t[0][facet_i][0])
n = verts[v_i] - verts[[1, 0][v_i]]
return n / numpy.linalg.norm(n)
# vectors from vertex 0 to each other vertex.
vert_vecs_from_v0 = verts[1:, :] - verts[:1, :]
(u, s, _) = numpy.linalg.svd(vert_vecs_from_v0)
rank = len([si for si in s if si > 1.e-10])
# this is the set of vectors that span the simplex
spanu = u[:, :rank]
vert_coords_of_facet = \
self.get_vertices_of_subcomplex(t[sd-1][facet_i])
# now I find everything normal to the facet.
vcf = numpy.asarray(vert_coords_of_facet)
facet_span = vcf[1:, :] - vcf[:1, :]
(_, sf, vft) = numpy.linalg.svd(facet_span)
# now get the null space from vft
rankfacet = len([si for si in sf if si > 1.e-10])
facet_normal_space = numpy.transpose(vft[rankfacet:, :])
# now, I have to compute the intersection of
# facet_span with facet_normal_space
foo = linalg_subspace_intersection(facet_normal_space, spanu)
num_cols = foo.shape[1]
if num_cols != 1:
raise Exception("barf in normal computation")
# now need to get the correct sign
# get a vector in the direction
nfoo = foo[:, 0]
# what is the vertex not in the facet?
verts_set = set(t[sd][cell])
verts_facet = set(t[sd - 1][facet_i])
verts_diff = verts_set.difference(verts_facet)
if len(verts_diff) != 1:
raise Exception("barf in normal computation: getting sign")
vert_off = verts_diff.pop()
vert_on = verts_facet.pop()
# get a vector from the off vertex to the facet
v_to_facet = numpy.array(self.vertices[vert_on]) \
- numpy.array(self.vertices[vert_off])
if numpy.dot(v_to_facet, nfoo) > 0.0:
return nfoo
else:
return -nfoo
def compute_tangents(self, dim, i):
"""Computes tangents in any dimension based on differences
between vertices and the first vertex of the i:th facet
of dimension dim. Returns a (possibly empty) list.
These tangents are *NOT* normalized to have unit length."""
t = self.get_topology()
vs = numpy.array(self.get_vertices_of_subcomplex(t[dim][i]))
return vs[1:] - vs[:1]
def compute_normalized_tangents(self, dim, i):
"""Computes tangents in any dimension based on differences
between vertices and the first vertex of the i:th facet
of dimension dim. Returns a (possibly empty) list.
These tangents are normalized to have unit length."""
ts = self.compute_tangents(dim, i)
ts /= numpy.linalg.norm(ts, axis=1)[:, None]
return ts
def compute_edge_tangent(self, edge_i):
"""Computes the nonnormalized tangent to a 1-dimensional facet.
returns a single vector."""
t = self.get_topology()
vs = numpy.asarray(self.get_vertices_of_subcomplex(t[1][edge_i]))
return vs[1] - vs[0]
def compute_normalized_edge_tangent(self, edge_i):
"""Computes the unit tangent vector to a 1-dimensional facet"""
v = self.compute_edge_tangent(edge_i)
v /= numpy.linalg.norm(v)
return v
def compute_face_tangents(self, face_i):
"""Computes the two tangents to a face. Only implemented
for a tetrahedron."""
if self.get_spatial_dimension() != 3:
raise Exception("can't get face tangents yet")
t = self.get_topology()
vs = numpy.asarray(self.get_vertices_of_subcomplex(t[2][face_i]))
return vs[1:] - vs[:1]
def compute_face_edge_tangents(self, dim, entity_id):
"""Computes all the edge tangents of any k-face with k>=1.
The result is a array of binom(dim+1,2) vectors.
This agrees with `compute_edge_tangent` when dim=1.
"""
vert_ids = self.get_topology()[dim][entity_id]
vert_coords = numpy.asarray(self.get_vertices_of_subcomplex(vert_ids))
v0 = []
v1 = []
for source in range(dim):
for dest in range(source + 1, dim + 1):
v0.append(source)
v1.append(dest)
return vert_coords[v1] - vert_coords[v0]
def make_points(self, dim, entity_id, order, variant=None, interior=1):
"""Constructs a lattice of points on the entity_id:th
facet of dimension dim. Order indicates how many points to
include in each direction."""
if dim == 0:
return (self.get_vertices()[entity_id], )
elif 0 < dim <= self.get_spatial_dimension():
entity_verts = \
self.get_vertices_of_subcomplex(
self.get_topology()[dim][entity_id])
return make_lattice(entity_verts, order, interior=interior, variant=variant)
else:
raise ValueError("illegal dimension")
def volume(self):
"""Computes the volume of the simplicial complex in the appropriate
dimensional measure."""
sd = self.get_spatial_dimension()
return sum(self.volume_of_subcomplex(sd, k)
for k in self.topology[sd])
def volume_of_subcomplex(self, dim, facet_no):
vids = self.topology[dim][facet_no]
return volume(self.get_vertices_of_subcomplex(vids))
def compute_scaled_normal(self, facet_i):
"""Returns the unit normal to facet_i of scaled by the
volume of that facet."""
dim = self.get_spatial_dimension()
if dim == 2:
n, = self.compute_tangents(dim-1, facet_i)
n[0], n[1] = n[1], -n[0]
return n
elif dim == 3:
return -numpy.cross(*self.compute_tangents(dim-1, facet_i))
v = self.volume_of_subcomplex(dim - 1, facet_i)
return self.compute_normal(facet_i) * v
def compute_reference_normal(self, facet_dim, facet_i):
"""Returns the unit normal in infinity norm to facet_i."""
assert facet_dim == self.get_spatial_dimension() - 1
n = SimplicialComplex.compute_normal(self, facet_i) # skip UFC overrides
return n / numpy.linalg.norm(n, numpy.inf)
def get_entity_transform(self, dim, entity):
"""Returns a mapping of point coordinates from the
`entity`-th subentity of dimension `dim` to the cell.
:arg dim: subentity dimension (integer)
:arg entity: entity number (integer)
"""
topology = self.get_topology()
celldim = self.get_spatial_dimension()
codim = celldim - dim
if dim == 0:
# Special case vertices.
i, = topology[dim][entity]
offset = numpy.asarray(self.get_vertices()[i])
C = numpy.zeros((dim, ) + offset.shape)
elif dim == celldim and len(self.topology[celldim]) == 1:
assert entity == 0
return lambda x: x
else:
subcell = self.construct_subelement(dim)
subdim = subcell.get_spatial_dimension()
assert subdim == celldim - codim
# Entity vertices in entity space.
v_e = numpy.asarray(subcell.get_vertices())
A = v_e[1:] - v_e[:1]
# Entity vertices in cell space.
v_c = numpy.asarray(self.get_vertices_of_subcomplex(topology[dim][entity]))
B = v_c[1:] - v_c[:1]
C = numpy.linalg.solve(A, B)
offset = v_c[0] - numpy.dot(v_e[0], C)
def transform(point):
out = numpy.dot(point, C)
return numpy.add(out, offset, out=out)
return transform
def get_dimension(self):
"""Returns the subelement dimension of the cell. Same as the
spatial dimension."""
return self.get_spatial_dimension()
def compute_barycentric_coordinates(self, points, entity=None, rescale=False, facet_ordering=False):
"""Returns the barycentric coordinates of a list of points on the complex."""
# if isinstance(points, numpy.ndarray) and len(points) == 0:
# return points
if entity is None:
entity = (self.get_spatial_dimension(), 0)
entity_dim, entity_id = entity
top = self.get_topology()
sd = self.get_spatial_dimension()
# get a subcell containing the entity and the restriction indices of the entity
# indices = slice(None)
indices = list(range(sd+1))
subcomplex = top[entity_dim][entity_id]
if entity_dim != sd:
cell_id = self.connectivity[(entity_dim, sd)][entity_id][0]
indices = [i for i, v in enumerate(top[sd][cell_id]) if v in subcomplex]
subcomplex = top[sd][cell_id]
if facet_ordering:
indices = indices[::-1]
cell_verts = self.get_vertices_of_subcomplex(subcomplex)
ref_verts = numpy.eye(sd + 1)
A, b = make_affine_mapping(cell_verts, ref_verts)
A, b = A[indices], b[indices]
if rescale:
# rescale barycentric coordinates by the height wrt. to the facet
h = 1 / numpy.linalg.norm(A, axis=1)
b *= h
A *= h[:, None]
# out = numpy.dot(points, A.T)
out = points @ A.T
# return numpy.add(out, b)
return out + b
def compute_bubble(self, points, entity=None):
"""Returns the lowest-order bubble on an entity evaluated at the given
points on the cell."""
return numpy.prod(self.compute_barycentric_coordinates(points, entity), axis=1)
def distance_to_point_l1(self, points, entity=None, rescale=False):
# noqa: D301
"""Get the L1 distance (aka 'manhatten', 'taxicab' or rectilinear
distance) from an entity to a point with 0.0 if the point is inside the entity.
Parameters
----------
points : numpy.ndarray or list
The coordinates of the points.
entity : tuple or None
A tuple of entity dimension and entity id.
rescale : bool
If true, the L1 distance is measured with respect to rescaled
barycentric coordinates, such that the L1 and L2 distances agree
for points opposite to a single facet.
Returns
-------
numpy.float64 or numpy.ndarray
The L1 distance, also known as taxicab, manhatten or rectilinear
distance, of the cell to the point. If 0.0 the point is inside the
cell.
Notes
-----
This is done with the help of barycentric coordinates where the general
algorithm is to compute the most negative (i.e. minimum) barycentric
coordinate then return its negative. For implementation reasons we
return the sum of all the negative barycentric coordinates. In each of
the below examples the point coordinate is `X` with appropriate
dimensions.
Consider, for example, a UFCInterval. We have two vertices which make
the interval,
`P0 = [0]` and
`P1 = [1]`.
Our point is
`X = [x]`.
Barycentric coordinates are defined as
`X = alpha * P0 + beta * P1` where
`alpha + beta = 1.0`.
The solution is
`alpha = 1 - X[0] = 1 - x` and
`beta = X[0] = x`.
If both `alpha` and `beta` are positive, the point is inside the
reference interval.
`---regionA---P0=0------P1=1---regionB---`
If we are in `regionA`, `alpha` is negative and
`-alpha = X[0] - 1.0` is the (positive) distance from `P0`.
If we are in `regionB`, `beta` is negative and `-beta = -X[0]` is
the exact (positive) distance from `P1`. Since we are in 1D the L1
distance is the same as the L2 distance. If we are in the interval we
can just return 0.0.
Things get more complicated when we consider higher dimensions.
Consider a UFCTriangle. We have three vertices which make the
reference triangle,
`P0 = (0, 0)`,
`P1 = (1, 0)` and
`P2 = (0, 1)`.
Our point is
`X = [x, y]`.
Below is a diagram of the cell (which may not render correctly in
sphinx):
.. code-block:: text
```
y-axis
|
|
(0,1) P2
| \\
| \\
| \\
| \\
| T \\
| \\
| \\
| \\
---P0--------P1--- x-axis
(0,0) | (1,0)
```
Barycentric coordinates are defined as
`X = alpha * P0 + beta * P1 + gamma * P2` where
`alpha + beta + gamma = 1.0`.
The solution is
`alpha = 1 - X[0] - X[1] = 1 - x - y`,
`beta = X[0] = x` and
`gamma = X[1] = y`.
If all three are positive, the point is inside the reference cell.
If any are negative, we are outside it. The absolute sum of any
negative barycentric coordinates usefully gives the L1 distance from
the cell to the point. For example the point (1,1) has L1 distance
1 from the cell: on this case alpha = -1, beta = 1 and gamma = 1.
-alpha = 1 is the L1 distance. For comparison the L2 distance (the
length of the vector from the nearest point on the cell to the point)
is sqrt(0.5^2 + 0.5^2) = 0.707. Similarly the point (-1.0, -1.0) has
alpha = 3, beta = -1 and gamma = -1. The absolute sum of beta and gamma
2 which is again the L1 distance. The L2 distance in this case is
sqrt(1^2 + 1^2) = 1.414.
For a UFCTetrahedron we have four vertices
`P0 = (0,0,0)`,
`P1 = (1,0,0)`,
`P2 = (0,1,0)` and
`P3 = (0,0,1)`.
Our point is
`X = [x, y, z]`.
The barycentric coordinates are defined as
`X = alpha * P0 + beta * P1 + gamma * P2 + delta * P3`
where
`alpha + beta + gamma + delta = 1.0`.
The solution is
`alpha = 1 - X[0] - X[1] - X[2] = 1 - x - y - z`,
`beta = X[0] = x`,
`gamma = X[1] = y` and
`delta = X[2] = z`.
The rules are the same as for the tetrahedron but with one extra
barycentric coordinate. Our approximate distance, the absolute sum of
the negative barycentric coordinates, is at worse around 4 times the
actual distance to the tetrahedron.
"""
# sum the negative part of each barycentric coordinate
bary = self.compute_barycentric_coordinates(points, entity=entity, rescale=rescale)
return 0.5 * abs(numpy.sum(abs(bary) - bary, axis=-1))
def contains_point(self, point, epsilon=0.0, entity=None):
"""Checks if reference cell contains given point
(with numerical tolerance as given by the L1 distance (aka 'manhatten',
'taxicab' or rectilinear distance) to the cell.
Parameters
----------
point : numpy.ndarray, list or symbolic expression
The coordinates of the point.
epsilon : float
The tolerance for the check.
entity : tuple or None
A tuple of entity dimension and entity id.
Returns
-------
bool : True if the point is inside the cell, False otherwise.
"""
return self.distance_to_point_l1(point, entity=entity) <= epsilon
def point_entity_ids(self, points, tol=1e-10):
"""Return the topological entity association for points on this cell."""
top = self.get_topology()
entity_ids = {dim: {entity: [] for entity in top[dim]} for dim in top}
invtop = {top[d][e]: (d, e) for d in top for e in top[d]}
sd = self.get_spatial_dimension()
seen = []
for cell in top[sd]:
cell_verts = top[sd][cell]
bary = self.compute_barycentric_coordinates(points, entity=(sd, cell))
distance_to_cell = 0.5 * abs(numpy.sum(abs(bary) - bary, axis=-1))
points_in_cell = numpy.flatnonzero(distance_to_cell <= tol)
candidates = numpy.setdiff1d(points_in_cell, seen)
candidates = candidates[numpy.lexsort(bary[candidates].T)]
for i in candidates.tolist():
entity_verts = numpy.flatnonzero(bary[i] > tol)
verts = tuple(cell_verts[v] for v in entity_verts)
dim, entity = invtop[verts]
entity_ids[dim][entity].append(i)
seen.append(i)
if len(seen) == len(points):
break
return entity_ids
def extract_extrinsic_orientation(self, o):
"""Extract extrinsic orientation.
Parameters
----------
o : Orientation
Total orientation.
Returns
-------
Orientation
Extrinsic orientation.
"""
if not isinstance(o, Orientation):
raise TypeError(f"Expecting an instance of Orientation : got {o}")
return 0
def extract_intrinsic_orientation(self, o, axis):
"""Extract intrinsic orientation.
Parameters
----------
o : Orientation
Total orientation.
axis : int
Reference cell axis for which intrinsic orientation is computed.
Returns
-------
Orientation
Intrinsic orientation.
"""
if not isinstance(o, Orientation):
raise TypeError(f"Expecting an instance of Orientation : got {o}")
if axis != 0:
raise ValueError(f"axis ({axis}) != 0")
return o
@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).
"""
return numpy.diag((1, )).astype(int).reshape((1, 1, 1))
class Simplex(SimplicialComplex):
r"""Abstract class for a reference simplex.
Orientation of a physical cell is computed systematically
by comparing the canonical orderings of its facets and
the facets in the FIAT reference cell.
As an example, we compute the orientation of a
triangular cell:
+ +
| \ | \
1 0 47 42
| \ | \
+--2---+ +--43--+
FIAT canonical Mapped example physical cell
Suppose that the facets of the physical cell
are canonically ordered as:
C = [43, 42, 47]
FIAT facet to Physical facet map is given by:
M = [42, 47, 43]
Then the orientation of the cell is computed as:
C.index(M[0]) = 1; C.remove(M[0])
C.index(M[1]) = 1; C.remove(M[1])
C.index(M[2]) = 0; C.remove(M[2])
o = (1 * 2!) + (1 * 1!) + (0 * 0!) = 3
"""
def is_simplex(self):
return True
def symmetry_group_size(self, dim):
return factorial(dim + 1)
def cell_orientation_reflection_map(self):
"""Return the map indicating whether each possible cell orientation causes reflection (``1``) or not (``0``)."""
return make_cell_orientation_reflection_map_simplex(self.get_dimension())
def get_facet_element(self):
dimension = self.get_spatial_dimension()
return self.construct_subelement(dimension - 1)
# Backwards compatible name
ReferenceElement = Simplex
class UFCSimplex(Simplex):
def construct_subelement(self, dimension):
"""Constructs the reference element of a cell subentity
specified by subelement dimension.
:arg dimension: subentity dimension (integer)
"""
return ufc_simplex(dimension)
class DefaultSimplex(Simplex):
def construct_subelement(self, dimension):
"""Constructs the reference element of a cell subentity
specified by subelement dimension.
:arg dimension: subentity dimension (integer)
"""
return default_simplex(dimension)
class SymmetricSimplex(Simplex):
def construct_subelement(self, dimension):
"""Constructs the reference element of a cell subentity
specified by subelement dimension.
:arg dimension: subentity dimension (integer)
"""
return symmetric_simplex(dimension)
class Point(Simplex):
"""This is the reference point."""
def __init__(self):
verts = ((),)
topology = {0: {0: (0,)}}
super().__init__(POINT, verts, topology)
def construct_subelement(self, dimension):
"""Constructs the reference element of a cell subentity
specified by subelement dimension.
:arg dimension: subentity dimension (integer). Must be zero.
"""
assert dimension == 0
return self
class DefaultLine(DefaultSimplex):
"""This is the reference line with vertices (-1.0,) and (1.0,)."""
def __init__(self):
verts = ((-1.0,), (1.0,))
edges = {0: (0, 1)}