Skip to content

Commit 0dee630

Browse files
authored
Maintenance (#1082)
* Fix MeshDG.draw * Deprecate skfem.visuals.glvis * skip ex32 test for python 3.12 due to pyamg assuming setuptools * Fix FacetBasis for quadratic meshes * Add Mesh.remove_unused_nodes * add Mesh.remove_duplicate_nodes * reorient boundary normals to outward normals in ElementGlobal * colorbar=False will now disable colorbar
1 parent 44916dc commit 0dee630

15 files changed

+185
-51
lines changed

README.md

+13
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,8 @@ with respect to documented and/or tested features.
214214
- Removed: Python 3.7 support
215215
- Removed: `MappingMortar` and `MortarFacetBasis` in favor of
216216
`skfem.supermeshing`
217+
- Deprecated: `skfem.visuals.glvis`; current version is broken and no
218+
replacement is being planned
217219
- Added: Python 3.12 support
218220
- Added: `Mesh.load` supports new keyword arguments
219221
`ignore_orientation=True` and `ignore_interior_facets=True` which
@@ -222,7 +224,18 @@ with respect to documented and/or tested features.
222224
respectively.
223225
- Added: `skfem.supermeshing` (requires `shapely>=2`) for creating quadrature
224226
rules for interpolating between two 1D or 2D meshes.
227+
- Added: `Mesh.remove_unused_nodes`
228+
- Added: `Mesh.remove_duplicate_nodes`
229+
- Added: `Mesh.remove_elements` now supports passing any subdomain
230+
reference through `Mesh.normalize_elements`; subdomains and
231+
boundaries are also properly preserved
225232
- Fixed: `MeshTet` uniform refine was reindexing subdomains incorrectly
233+
- Fixed: `MeshDG.draw` did not work; now calls `Basis.draw` which
234+
works for any mesh topology
235+
- Fixed: `FacetBasis` now works with `MeshTri2`, `MeshQuad2`,
236+
`MeshTet2` and `MeshHex2`
237+
- Fixed: `ElementGlobal` now uses outward normals to initialize DOFs
238+
on boundary factes
226239

227240
## [8.1.0] - 2023-06-16
228241

docs/examples/ex35.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -221,10 +221,9 @@
221221
from packaging import version
222222
from pathlib import Path
223223

224-
from skfem.mesh import MeshTri
225-
from skfem.assembly import Basis, FacetBasis
226-
from skfem.utils import solve, asm, condense, projection
227-
from skfem.element import ElementTriP1
224+
from skfem import (MeshTri, Basis, FacetBasis,
225+
solve, asm, condense, projection,
226+
ElementTriP1)
228227
from skfem.models.poisson import laplace, unit_load, mass
229228
from skfem.io.json import from_file
230229

skfem/element/element_global.py

+14
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,20 @@ def _eval_dofs(self, mesh, tind=None):
166166
-w['n'][itr, 0, :]])
167167
w['n'][itr] /= np.linalg.norm(w['n'][itr], axis=0)
168168

169+
# swap
170+
from skfem.assembly import FacetBasis
171+
fb = FacetBasis(mesh, mesh.elem(), intorder=0)
172+
ix = np.isin(mesh.t2f, mesh.boundary_facets())
173+
sortix = np.argsort(mesh.t2f[ix])
174+
norms1 = np.vstack((w['n'][:, 0, :][ix][sortix],
175+
w['n'][:, 1, :][ix][sortix]))
176+
norms2 = fb.normals[:, :, 0]
177+
signs = np.diag(norms1.T @ norms2)
178+
signs1 = signs.copy()
179+
signs1[sortix] = signs
180+
w['n'][:, 0, :][ix] *= signs1
181+
w['n'][:, 1, :][ix] *= signs1
182+
169183
# evaluate dofs, gdof implemented in subclasses
170184
for itr in range(N):
171185
for jtr in range(N):

skfem/mapping/mapping_isoparametric.py

+14
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ def Fmap(self, i, X, tind=None):
6666
def bndmap(self, i, X, find=None):
6767
p = self.mesh.doflocs
6868
facets = self.mesh.facets
69+
if len(self.mesh.dofs.edge_dofs) > 0:
70+
facets = np.vstack((facets,
71+
self.mesh.dofs.edge_dofs[0, self.mesh.f2e]))
72+
# TODO currently supports only one DOF per edge (slice 0 idx)
73+
if len(self.mesh.dofs.facet_dofs) > 0:
74+
facets = np.vstack((facets,
75+
self.mesh.dofs.facet_dofs))
6976
if find is None:
7077
out = np.zeros((facets.shape[1], X.shape[1]))
7178
for itr in range(facets.shape[0]):
@@ -82,6 +89,13 @@ def bndmap(self, i, X, find=None):
8289
def bndJ(self, i, j, X, find=None):
8390
p = self.mesh.doflocs
8491
facets = self.mesh.facets
92+
if len(self.mesh.dofs.edge_dofs) > 0:
93+
facets = np.vstack((facets,
94+
self.mesh.dofs.edge_dofs[0, self.mesh.f2e]))
95+
# TODO currently supports only one DOF per edge (slice 0 idx)
96+
if len(self.mesh.dofs.facet_dofs) > 0:
97+
facets = np.vstack((facets,
98+
self.mesh.dofs.facet_dofs))
8599
if find is None:
86100
out = np.zeros((facets.shape[1], X.shape[1]))
87101
for itr in range(facets.shape[0]):

skfem/mesh/mesh.py

+39-13
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,15 @@ def f2t(self):
112112
self._f2t = self.build_inverse(self.t, self.t2f)
113113
return self._f2t
114114

115+
@property
116+
def f2e(self):
117+
if not hasattr(self, '_f2e'):
118+
_, self._f2e = self.build_entities(
119+
self.facets,
120+
self.bndelem.refdom.facets,
121+
)
122+
return self._f2e
123+
115124
@property
116125
def edges(self):
117126
if not hasattr(self, '_edges'):
@@ -573,6 +582,13 @@ def is_valid(self, raise_=False) -> bool:
573582
logger.debug("Mesh validation completed with no warnings.")
574583
return True
575584

585+
@staticmethod
586+
def _remove_duplicate_nodes(p, t):
587+
tmp = np.ascontiguousarray(p.T)
588+
tmp, ixa, ixb = np.unique(tmp.view([('', tmp.dtype)] * tmp.shape[1]),
589+
return_index=True, return_inverse=True)
590+
return p[:, ixa], ixb[t]
591+
576592
def __iter__(self):
577593
return iter((self.doflocs, self.t))
578594

@@ -606,12 +622,7 @@ def __add__(self, other):
606622
p = np.hstack((self.p.round(decimals=8),
607623
other.p.round(decimals=8)))
608624
t = np.hstack((self.t, other.t + self.p.shape[1]))
609-
tmp = np.ascontiguousarray(p.T)
610-
tmp, ixa, ixb = np.unique(tmp.view([('', tmp.dtype)] * tmp.shape[1]),
611-
return_index=True, return_inverse=True)
612-
p = p[:, ixa]
613-
t = ixb[t]
614-
return cls(p, t)
625+
return cls(*self._remove_duplicate_nodes(p, t))
615626

616627
def __repr__(self):
617628
rep = ""
@@ -1038,27 +1049,42 @@ def restrict(self, elements):
10381049
newf[self.boundaries[k]])
10391050
for k in self.boundaries}
10401051
if self.boundaries is not None else None),
1041-
_subdomains=({k: newt[np.intersect1d(self.subdomains[k], elements)]
1052+
_subdomains=({k: newt[np.intersect1d(self.subdomains[k],
1053+
elements).astype(np.int64)]
10421054
for k in self.subdomains}
10431055
if self.subdomains is not None else None),
10441056
)
10451057

1046-
def remove_elements(self, element_indices: ndarray):
1058+
def remove_elements(self, elements):
10471059
"""Construct a new mesh by removing elements.
10481060
10491061
Parameters
10501062
----------
1051-
element_indices
1052-
List of element indices to remove.
1063+
elements
1064+
Criteria of which elements to include. This input is normalized
1065+
using ``self.normalize_elements``.
10531066
10541067
"""
1055-
p, t, _ = self._reix(np.delete(self.t, element_indices, axis=1))
1068+
elements = self.normalize_elements(elements)
1069+
return self.restrict(np.setdiff1d(np.arange(self.t.shape[1],
1070+
dtype=np.int64),
1071+
elements))
1072+
1073+
def remove_unused_nodes(self):
1074+
p, t, _ = self._reix(self.t)
1075+
return replace(
1076+
self,
1077+
doflocs=p,
1078+
t=t,
1079+
)
1080+
1081+
def remove_duplicate_nodes(self):
1082+
p, t = self._remove_duplicate_nodes(self.doflocs,
1083+
self.t)
10561084
return replace(
10571085
self,
10581086
doflocs=p,
10591087
t=t,
1060-
_boundaries=None,
1061-
_subdomains=None,
10621088
)
10631089

10641090
def element_finder(self, mapping=None):

skfem/mesh/mesh_2d_2.py

+4
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ def element_finder(self, *args, **kwargs):
1111
@classmethod
1212
def init_refdom(cls):
1313
return cls.__bases__[-1].init_refdom()
14+
15+
def draw(self, *args, **kwargs):
16+
from ..assembly import CellBasis
17+
return CellBasis(self, self.elem()).draw(*args, **kwargs)

skfem/mesh/mesh_dg.py

+4
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,7 @@ def load(cls, *args, **kwargs):
109109

110110
def element_finder(self, *args, **kwargs):
111111
raise NotImplementedError
112+
113+
def draw(self, *args, **kwargs):
114+
from ..assembly import CellBasis
115+
return CellBasis(self, self.elem()).draw(*args, **kwargs)

skfem/mesh/mesh_tri_1_dg.py

+15-6
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,25 @@ class MeshTri1DG(MeshDG, MeshTri1):
1616
doflocs: ndarray = field(
1717
default_factory=lambda: np.array(
1818
[
19-
[0.0, 0.0],
20-
[1.0, 0.0],
21-
[0.0, 1.0],
22-
[1.0, 0.0],
23-
[0.0, 1.0],
24-
[1.0, 1.0],
19+
[0., 0.],
20+
[1., 0.],
21+
[1., 1.],
22+
[0., 0.],
23+
[0., 1.],
24+
[1., 1.],
2525
],
2626
dtype=np.float64,
2727
).T
2828
)
29+
t: ndarray = field(
30+
default_factory=lambda: np.array(
31+
[
32+
[0, 1, 3],
33+
[0, 2, 3],
34+
],
35+
dtype=np.int64,
36+
).T
37+
)
2938
elem: Type[Element] = ElementTriP1DG
3039
affine: bool = False
3140
sort_t: bool = False

skfem/refdom.py

+4
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ class RefTet(Refdom):
104104
[0, 3],
105105
[1, 3],
106106
[2, 3]]
107+
normals = np.array([[0., 0., -1.],
108+
[0., -1., 0.],
109+
[-1., 0., 0.],
110+
[1., 1., 1.]])
107111
brefdom = RefTri
108112
nnodes = 4
109113
nfacets = 4

skfem/utils.py

+1-24
Original file line numberDiff line numberDiff line change
@@ -717,29 +717,6 @@ def projection(fun,
717717
diff: Optional[int] = None,
718718
I: Optional[ndarray] = None,
719719
expand: bool = False) -> ndarray:
720-
"""Perform projections onto a finite element basis.
721-
722-
Parameters
723-
----------
724-
fun
725-
A solution vector or a function handle.
726-
basis_to
727-
The finite element basis to project to.
728-
basis_from
729-
The finite element basis to project from.
730-
diff
731-
Differentiate with respect to the given dimension.
732-
I
733-
Index set for limiting the projection to a subset.
734-
expand
735-
Passed to :func:`skfem.utils.condense`.
736-
737-
Returns
738-
-------
739-
ndarray
740-
The projected solution vector.
741-
742-
"""
743720

744721
@BilinearForm
745722
def mass(u, v, w):
@@ -783,7 +760,7 @@ def deriv(u, v, w):
783760
return solve_linear(M, f)
784761

785762

786-
@deprecated("Basis.project")
763+
@deprecated("Basis.project (will be removed in the next release)")
787764
def project(fun,
788765
basis_from: Optional[AbstractBasis] = None,
789766
basis_to: Optional[AbstractBasis] = None,

skfem/visuals/glvis.py

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from typing import Optional
66
from glvis import glvis
77

8+
from skfem.generic_utils import deprecated
9+
810

911
MESH_TYPE_MAPPING = {
1012
skfem.MeshTet1: 4,
@@ -64,6 +66,7 @@ def _to_float_string(arr):
6466
return s.getvalue().decode()
6567

6668

69+
@deprecated("skfem.visuals.matplotlib.plot (no replacement)")
6770
def plot(basis, x, keys: Optional[str] = None):
6871
m = basis.mesh
6972
if isinstance(basis.elem, skfem.ElementVector):
@@ -99,5 +102,6 @@ def plot(basis, x, keys: Optional[str] = None):
99102
))
100103

101104

105+
@deprecated("skfem.visuals.matplotlib.draw (no replacement)")
102106
def draw(basis, keys: Optional[str] = None):
103107
return plot(basis, basis.zeros(), keys=keys)

skfem/visuals/matplotlib.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ def plot_meshtri(m: MeshTri1, z: ndarray, **kwargs) -> Axes:
291291
levels=kwargs["levels"],
292292
**{**{'colors': 'k'}, **plot_kwargs})
293293

294-
if "colorbar" in kwargs:
294+
if "colorbar" in kwargs and kwargs["colorbar"] is not False:
295295
if isinstance(kwargs["colorbar"], str):
296296
plt.colorbar(im, ax=ax, label=kwargs["colorbar"])
297297
elif isinstance(kwargs["colorbar"], dict):

tests/test_assembly.py

+21
Original file line numberDiff line numberDiff line change
@@ -747,3 +747,24 @@ def test_hcurl_projections_3d():
747747

748748
assert_almost_equal(curly, curlx)
749749
assert_almost_equal(curly, curla)
750+
751+
752+
def test_element_global_boundary_normal():
753+
754+
mesh = MeshTri.init_sqsymmetric().refined(3)
755+
basis = Basis(mesh, ElementTriMorley())
756+
757+
D = basis.get_dofs().all('u_n')
758+
x = basis.zeros()
759+
x[D] = 1
760+
761+
@BilinearForm
762+
def bilinf(u, v, w):
763+
return ddot(u.hess, v.hess)
764+
765+
A = bilinf.assemble(basis)
766+
f = 0 * unit_load.assemble(basis)
767+
768+
y = solve(*condense(A, f, D=basis.get_dofs(), x=x))
769+
770+
assert (y[basis.get_dofs(elements=True).all('u')] <= 0).all()

tests/test_examples.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from unittest import TestCase, main
2+
import pytest
3+
import sys
24

35
import numpy as np
46

@@ -14,7 +16,8 @@ class TestEx02(TestCase):
1416

1517
def runTest(self):
1618
import docs.examples.ex02 as ex02
17-
self.assertAlmostEqual(np.max(ex02.x), 0.001217973811129439)
19+
self.assertAlmostEqual(np.max(ex02.x[ex02.ib.nodal_dofs[0]]),
20+
0.00033840961095522285)
1821

1922

2023
class TestEx03(TestCase):
@@ -254,6 +257,8 @@ def runTest(self):
254257
self.assertAlmostEqual(L[0], 22.597202568397734, delta=1e-6)
255258

256259

260+
@pytest.mark.skipif(sys.version_info > (3,12),
261+
reason="Python 3.12 has no setuptools; assumed by pyamg")
257262
class TestEx32(TestCase):
258263

259264
def runTest(self):

0 commit comments

Comments
 (0)