Something wrong with my mesh

I am trying to make a mesh consisting of 3 quarters of an annulus completed with a distorted quarter annulus. I have a working FEniCSx code with a working mesh consisting of a full annulus with the 2 Physical Surface regions. The working mesh can be built with the following file.geo:

// Gmsh project created on Sat Mar  8 22:37:56 2025
SetFactory("OpenCASCADE");
//+
Mesh.CharacteristicLengthMax = 0.0009;
Mesh.CharacteristicLengthMin = 0.0001;

Disk(1) = {0, 0, 0, 0.05, 0.05};
//+
Disk(2) = {0, 0, 0, 0.035, 0.035};


BooleanDifference{ Surface{1}; Delete;}{ Surface{2}; Delete;}
// Surface 3 is the full annulus. I need to split it into 2.

Point(4) = {0, 0.05, 0};
//+
Point(5) = {0, 0.035, 0};
//+
// Origin.
Point(6) = {0, 0, 0};


Line(4) = {2,3};
Line(5) = {4,5};

Circle(6) = {5, 6, 2};

Circle(7) = {4, 6, 3};
Curve Loop(8) = {5,6,4,7};




Plane Surface(9) = {8};

// 3-quarter surface
BooleanDifference{ Surface{1}; Delete;}{ Surface{9};}

BooleanFragments{ Surface{1}; Surface{9}; Delete; }{ }

Physical Surface("three_quarter_material", 20) = {1};
Physical Surface("quarter_material", 21) = {9};
Physical Curve("theta_0", 22) = {4};
Physical Curve("theta_pi_over_2", 23) = {5};
Physical Curve("r_inner_quarter", 24) = {6};
Physical Curve("r_outer_quarter", 25) = {7};
Physical Curve("r_inner_three_quarter", 26) = {8};
Physical Curve("r_outer_three_quarter", 27) = {9};

I can examine the annulus with gmsh and see that all Physical Curves and surfaces are fine.

Here is my Python code that produces the modified quarter-annulus msh file:

import gmsh
import numpy as np
from math import pi, sqrt

# -----------------------------------------------------------------------------
# User parameters
# -----------------------------------------------------------------------------
r_o, r_i   = 0.05, 0.035
A_i, B, n_pt = 0.008, 0.003, 30

gmsh.initialize()
gmsh.model.add("annulus_full")
gmsh.option.setNumber("Mesh.CharacteristicLengthMax", 0.0009)
gmsh.option.setNumber("Mesh.CharacteristicLengthMin", 0.0001)

# -----------------------------------------------------------------------------
# 1) Build 3/4‐annulus in OCC
# -----------------------------------------------------------------------------
c1 = gmsh.model.occ.addDisk(0, 0, 0, r_o, r_o)
c2 = gmsh.model.occ.addDisk(0, 0, 0, r_i, r_i)
ann, _ = gmsh.model.occ.cut([(2, c1)], [(2, c2)])
ann_tag = ann[0][1]

L  = 2*r_o
p1 = gmsh.model.occ.addPoint(0,   0,   0)
p2 = gmsh.model.occ.addPoint(0,   L,   0)
p3 = gmsh.model.occ.addPoint(L,   L,   0)
p4 = gmsh.model.occ.addPoint(L,   0,   0)
l1 = gmsh.model.occ.addLine(p1, p2)  # θ=π/2 radial
l2 = gmsh.model.occ.addLine(p2, p3)
l3 = gmsh.model.occ.addLine(p3, p4)
l4 = gmsh.model.occ.addLine(p4, p1)  # θ=0 radial
wloop = gmsh.model.occ.addCurveLoop([l1, l2, l3, l4])
wedge  = gmsh.model.occ.addPlaneSurface([wloop])

three, _ = gmsh.model.occ.cut([(2, ann_tag)], [(2, wedge)])
three_tag = three[0][1]
gmsh.model.occ.synchronize()

# -----------------------------------------------------------------------------
# 2) Build quarter patch in OCC
# -----------------------------------------------------------------------------
k    = r_i*A_i + (pi/8)*A_i**2
a,b,c= pi/8, r_o, -k
disc = b*b - 4*a*c
if disc < 0:
    raise RuntimeError("No real solution for A_i")
A_o = (-b + sqrt(disc))/(2*a)

θ     = np.linspace(0, 0.5*pi, n_pt)
r_out = r_o + A_o*np.sin(2*θ) + B*np.sin(4*θ)
r_in  = r_i + A_i*np.sin(2*θ) + B*np.sin(4*θ)
x_o,y_o = r_out*np.cos(θ), r_out*np.sin(θ)
x_i,y_i = r_in *np.cos(θ), r_in  *np.sin(θ)

ms    = (r_out.max() - r_in.min())/5.0
pts_o = [gmsh.model.occ.addPoint(x,y,0,ms) for x,y in zip(x_o,y_o)]
pts_i = [gmsh.model.occ.addPoint(x,y,0,ms) for x,y in zip(x_i,y_i)]

curves_q = []
for i in range(n_pt-1):
    curves_q.append(gmsh.model.occ.addLine(pts_o[i],   pts_o[i+1]))  # outer arc
curves_q.append(gmsh.model.occ.addLine(pts_o[-1], pts_i[-1]))      # θ=π/2
for i in range(n_pt-1,0,-1):
    curves_q.append(gmsh.model.occ.addLine(pts_i[i],   pts_i[i-1]))  # inner arc
curves_q.append(gmsh.model.occ.addLine(pts_i[0],   pts_o[0]))      # θ=0

loop_q  = gmsh.model.occ.addCurveLoop(curves_q)
quarter = gmsh.model.occ.addPlaneSurface([loop_q])
gmsh.model.occ.synchronize()

outer_q     = curves_q[:n_pt-1]
thetaPi2_q  = curves_q[n_pt-1]
inner_q     = curves_q[n_pt:-1]     # <-- this slice was fixed earlier
theta0_q    = curves_q[-1]

# -----------------------------------------------------------------------------
# 3) Fragment copy of 3/4 with quarter patch
# -----------------------------------------------------------------------------
cp = gmsh.model.occ.copy([(2, three_tag)])
copy_tag = next(t for d,t in cp if d==2)
gmsh.model.occ.synchronize()

out, mapEnt = gmsh.model.occ.fragment(
  [(2, copy_tag)], [(2, quarter)],
  removeObject=False, removeTool=False
)
gmsh.model.occ.synchronize()

# Tag surfaces
surfs = [t for d,t in out if d==2]
surf3, surf1 = surfs
pg = gmsh.model.addPhysicalGroup(2, [surf3], tag=20)
gmsh.model.setPhysicalName(2, pg, "three_quarter_material")
pg = gmsh.model.addPhysicalGroup(2, [surf1], tag=21)
gmsh.model.setPhysicalName(2, pg, "quarter_material")

# -----------------------------------------------------------------------------
# 4) Radial curve tags via mapEnt
# -----------------------------------------------------------------------------
theta0_new, thetaPi2_new = [], []
for (dNew, tNew), elders in zip(out, mapEnt):
    if dNew!=1: continue
    old1D = [t for (d,t) in elders if d==1]
    if l4 in old1D:      theta0_new.append(tNew)
    elif l1 in old1D:    thetaPi2_new.append(tNew)

theta0_all   = [theta0_q]   + theta0_new
thetaPi2_all = [thetaPi2_q] + thetaPi2_new

# -----------------------------------------------------------------------------
# 5) Classify the 3/4 arcs, *first* dropping any radials by angle
# -----------------------------------------------------------------------------
# ensure 1D mesh so every curve has nodes
gmsh.model.mesh.generate(1)

# grab all boundary‐curve tags of the 3/4‐piece
b3all   = gmsh.model.getBoundary([(2, surf3)],
                                 oriented=False, recursive=False)
curves3 = [t for (d,t) in b3all if d==1]

# now drop the θ≈0 and θ≈π/2 radials by checking the mean angle of their nodes
tol = 1e-3
filtered = []
for c in curves3:
    _, coords, _ = gmsh.model.mesh.getNodes(1, c)
    xs, ys = coords[0::3], coords[1::3]
    theta_mean = np.mean(np.arctan2(ys, xs))
    # normalize to [0,2π)
    if theta_mean < 0:
        theta_mean += 2*np.pi
    # if it's within tol of 0 or π/2, skip it
    if abs(theta_mean) < tol or abs(theta_mean - 0.5*np.pi) < tol:
        continue
    filtered.append(c)

# now only the two arc‐curves remain
curves3 = filtered

rinner3, router3 = [], []
for c in curves3:
    _, coords, _ = gmsh.model.mesh.getNodes(1, c)
    xs, ys = coords[0::3], coords[1::3]
    ravg = np.mean(np.hypot(xs, ys))
    if abs(ravg - r_i) < abs(ravg - r_o):
        rinner3.append(c)
    else:
        router3.append(c)

# -----------------------------------------------------------------------------
# 6) Create the six Physical Curve groups
# -----------------------------------------------------------------------------
groups = {
  22: ("theta_0",               theta0_all),
  23: ("theta_pi_over_2",       thetaPi2_all),
  24: ("r_inner_quarter",       inner_q),
  25: ("r_outer_quarter",       outer_q),
  26: ("r_inner_three_quarter", rinner3),
  27: ("r_outer_three_quarter", router3),
}

for tag,(name,clist) in groups.items():
    if not clist:
        raise RuntimeError(f"No curves found for {name}")
    pg = gmsh.model.addPhysicalGroup(1, clist, tag=tag)
    gmsh.model.setPhysicalName(1, pg, name)

# -----------------------------------------------------------------------------
# 7) Mesh 2D & write
# -----------------------------------------------------------------------------
gmsh.model.mesh.generate(1)
gmsh.model.mesh.generate(2)

gmsh.model.mesh.partition(0)         # collapse everything into partition 0

gmsh.write("annulus_full.msh")
gmsh.finalize()

The produced mesh looks totally fine with Gmsh GUI. All the physical curves and surfaces seem to be OK.

However, my working FEnicsx code now yields:

Info    : Reading 'meshes/annulus_full.msh'...
Info    : 153 entities
Info    : 6782 nodes
Info    : 13508 elements
Info    : Done reading 'meshes/annulus_full.msh'
Invalid rank, error stack:
internal_Issend(117): MPI_Issend(buf=0x55c7aed13b21, count=1, MPI_BYTE, 1, 1, comm=0x84000005, request=0x55c7ae7e87c4) failed
internal_Issend(78).: Invalid rank has value 1 but must be nonnegative and less than 1
Abort(656902) on node 0 (rank 0 in comm 416): application called MPI_Abort(comm=0x84000005, 656902) - process 0

Which makes me think something is wrong with my mesh, but it’s very hard to debug, I don’t know what line/feature is faulty.

A MWE (untested):


import gmsh
from dolfinx.io import gmshio, VTXWriter
from mpi4py import MPI
from ufl import Measure, SpatialCoordinate, dx, FacetNormal

class TheMesh:
    def __init__(self, mesh_file="meshes/quarter_annulus.msh") -> None:
        self.mesh, self.cell_markers, self.facet_markers = gmshio.read_from_msh(mesh_file, MPI.COMM_WORLD, gdim=2)
        self.x = SpatialCoordinate(self.mesh)
        self.dx = Measure("dx", domain=self.mesh, subdomain_data=self.cell_markers)
        self.ds = Measure("ds", domain=self.mesh, subdomain_data=self.facet_markers)
        self.dS = Measure("dS", domain=self.mesh, subdomain_data=self.facet_markers)
        self.n = FacetNormal(self.mesh)

the_mesh=TheMesh(mesh_file="meshes/annulus_full.msh")

Any pointer would be very much welcome.

Please discuss this first on gmsh support channel Issues · gmsh / gmsh · GitLab . Once you are sure that the mesh generated by gmsh is ok, feel free to open again a thread here.

It is quite likely that you have tagged the wrong surfaces.

Please make sure that you only include the code of generating the mesh + reading it in, and ensure that the failure lies in the line:

and not any other line of your code.
See for instance: Adding 1d physical groups to gmsh model causes errors when convert it to dolfinx mesh - #2 by dokken