kinnala / scikit-fem

Simple finite element assemblers
https://scikit-fem.readthedocs.io
BSD 3-Clause "New" or "Revised" License
495 stars 79 forks source link

Mesh.facets_around() not working after refinement #1154

Open duarte-jfs opened 1 month ago

duarte-jfs commented 1 month ago

I was working on the mesh refinement, trying to understand how can boundaries be kept, and came across the issue of the mesh.facets_around() not working properly after a refinement.

from collections import OrderedDict

import matplotlib.pyplot as plt
import numpy as np
import shapely
import shapely.affinity
from skfem import Basis, ElementTriP0, adaptive_theta
from skfem.io.meshio import from_meshio

from femwell.maxwell.waveguide import compute_modes, eval_error_estimator
from femwell.mesh import mesh_from_OrderedDict

polygons_paper = OrderedDict(
    left=shapely.LineString(((0, 0), (0, 1))),
    right=shapely.LineString(((1, 0), (1, 1))),
    top=shapely.LineString(((0, 1), (1, 1))),
    square=shapely.box(0.6, 0.6, 0.8, 0.8),
    core=shapely.box(0, 0, 0.5, 0.5),
    clad=shapely.box(0, 0, 1, 1),
)

boundaries = [
    [lambda x: x[0] == np.min(x[0]), lambda x: x[0] == np.max(x[0])],
    [lambda x: x[0] == np.min(x[0]), lambda x: x[0] == np.max(x[0])],
    [lambda x: x[0] == np.min(x[0]), lambda x: x[1] == np.max(x[1])],
    [lambda x: x[0] == np.min(x[0]), lambda x: x[1] == np.max(x[1])],
]

epsilons_paper = [
    {"core": 2.25, "clad": 1, 'square': 1},
    {"core": 8, "clad": 1, 'square': 1},
    {"core": 1, "clad": 2.25, 'square': 1},
    {"core": 1, "clad": 8, 'square': 1},
]

neff_values_paper = [1.27627404, 2.65679692, 1.387926425, 2.761465320]
num = 3

mesh = from_meshio(
    mesh_from_OrderedDict(polygons_paper, {}, default_resolution_max=0.1, filename="mesh.msh")
)

basis0 = Basis(mesh, ElementTriP0())
epsilon = basis0.zeros()
for subdomain, e in epsilons_paper[num].items():
    epsilon[basis0.get_dofs(elements=subdomain)] = e

modes = compute_modes(
    basis0, epsilon, wavelength=1.5, num_modes=1, order=1, metallic_boundaries=boundaries[num]
)

check_boundary(mesh)
# print(mesh.)
mesh = refined(adaptive_theta(eval_error_estimator(modes[0].basis, modes[0].E), theta=0.5))
check_boundary(mesh)

Before refinement: image

After refinement: image

kinnala commented 1 month ago

Thanks for the report. Based on this, all named boundaries should be removed from the mesh during adaptive refine: https://github.com/kinnala/scikit-fem/blob/c6b36757a2ba3ebb048abfc9d49a9a9205155a35/skfem/mesh/mesh_tri_1.py#L389

What is check_mesh doing?

kinnala commented 1 month ago

Anyhow, don’t expect this to be fixed any time soon. It is a somewhat challenging indexing problem to solve in general.

duarte-jfs commented 1 month ago

Sorry, the check_boundary is just taking care of the plots. Getting elements coordinates and normal vectors and plotting.

Based on this, all named boundaries should be removed from the mesh during adaptive refine:

The thing is, the named boundaries are not preserved already. What is preserved is the sub domains, but the ordering may be messed up. I don't know

Anyhow, don’t expect this to be fixed any time soon. It is a somewhat challenging indexing problem to solve in general.

Yes, I can only imagine. It would be a great feature though

kinnala commented 1 month ago

I don't quite follow what happens in this issue. Can you provide the code of check_mesh?

duarte-jfs commented 1 month ago

I don't quite follow what happens in this issue. Can you provide the code of check_mesh?

def check_boundary(mesh):
    plt.figure()
    plt.scatter(*mesh.p, marker = 'o', facecolor = 'None', edgecolor = 'black', s = 15)

    # boundary = mesh.boundaries['square___clad']
    boundary = mesh.facets_around(mesh.subdomains['square'])
    facet_basis = basis0.boundary(boundary)
    facets = mesh.facets[:,boundary]
    colors = plt.cm.jet(np.linspace(0,1,facets.shape[1]))

    for i in range(facets.shape[1]):
        # plt.text(*data[:,i], f'{i}')

        x = [mesh.p[0,facets[0,i]], mesh.p[0,facets[1,i]]]
        y = [mesh.p[1,facets[0,i]], mesh.p[1,facets[1,i]]]

        norm = np.asarray([facet_basis.normals[0,i,0], 
                           facet_basis.normals[1,i,0]])

        norm = norm/np.sqrt(np.sum(np.abs(norm)**2)) * 0.1

        plt.arrow(np.mean(x), np.mean(y), dx = norm[0], dy = norm[1], color = colors[i], 
                  head_width = 0.03, 
                  head_length = 0.01,
                  length_includes_head = True)

        plt.plot(x, y, color = colors[i])
        # plt.text(np.mean(x), np.mean(y), f"{i}")

    plt.scatter(mesh.p[0,facets[0]], mesh.p[1,facets[0]], c = colors, s = 20)
    plt.scatter(mesh.p[0,facets[1]], mesh.p[1,facets[1]], c = colors, s = 20)
kinnala commented 1 week ago

I'm unable to reproduce this with my own minimal example, perhaps it was fixed by another issue, or then I misunderstood the issue:

from skfem import *
import numpy as np

m = MeshTri().refined(4).with_subdomains({'test': lambda x: (x[0] < 0.5) * (x[0] > 0.3) * (x[1] < 0.5) * (x[1] > 0.3)})
m = m.with_boundaries({'test': m.facets_around('test')})
m.draw(boundaries=True).show()
m = m.refined(np.arange(200))
m = m.with_boundaries({'test': m.facets_around('test')})
m.draw(boundaries=True).show()

Before refine:

image

After refine:

image

The normal vector should be pointing opposite to the red triangle.