Using a simple 3d mesh from gmsh

Hello there, it’s been a while I have been using FEniCS(x) from time to time, and I have some old codes and therefore ways of doing things, which I have no idea whether they are well adapted to today’s code and way of doing things.

My goal for now is to solve a simple equation (Ohm’s law) in a simple 3D shape (a bar). I have created a file.msh using the Gmsh GUI, this produced the following file.geo:

//+
SetFactory("OpenCASCADE");
Box(1) = {0, 0, 0, 1e-2, 1e-2, 1};

//+
Physical Volume("the_wire", 13) = {1};
//+
Physical Surface("left_end", 14) = {5};
//+
Physical Surface("right_end", 15) = {6};

and a corresponding file.msh. As far as I understand, nowadays, it is preferred to use pygmsh, but I’m not sure how to reproduce the very same file.msh.

Now, the next step is to use FEniCSx on the file.msh. My attempt is the following:

import meshio
# 1: sample volume
# surfaces
# 5 left end
# 6 right end

msh = meshio.read("file.msh")

So far so good, “msh” yields:

<meshio mesh object>
  Number of points: 47
  Number of cells:
    triangle: 4
    triangle: 4
    tetra: 71
  Cell sets: left_end, right_end, the_wire, gmsh:bounding_entities
  Point data: gmsh:dim_tags
  Cell data: gmsh:physical, gmsh:geometrical
  Field data: left_end, right_end, the_wire

I then proceed with:

meshio.write("mesh.xdmf", meshio.Mesh(points=msh.points, cells={"tetra": msh.cells["tetra"]}))
meshio.write("mf.xdmf", meshio.Mesh(points=msh.points, cells={"triangle": msh.cells["triangle"]},
                                    cell_data={"triangle": {"name_to_read": msh.cell_data["triangle"]["gmsh:physical"]}}))
meshio.write("cf.xdmf", meshio.Mesh(
    points=msh.points, cells={"tetra": msh.cells["tetra"]},
    cell_data={"tetra": {"name_to_read":
                            msh.cell_data["tetra"]["gmsh:physical"]}}))
mesh = Mesh()

but this yielded:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [5], in <cell line: 1>()
----> 1 meshio.write("mesh.xdmf", meshio.Mesh(points=msh.points, cells={"tetra": msh.cells["tetra"]}))
      2 meshio.write("mf.xdmf", meshio.Mesh(points=msh.points, cells={"triangle": msh.cells["triangle"]},
      3                                     cell_data={"triangle": {"name_to_read": msh.cell_data["triangle"]["gmsh:physical"]}}))
      4 meshio.write("cf.xdmf", meshio.Mesh(
      5     points=msh.points, cells={"tetra": msh.cells["tetra"]},
      6     cell_data={"tetra": {"name_to_read":
      7                             msh.cell_data["tetra"]["gmsh:physical"]}}))

TypeError: list indices must be integers or slices, not str

Googling the error yielded me to try:

msh = meshio.read("file.msh")
for key in msh.cell_data_dict["gmsh:physical"].keys():
    if key == "triangle":
        triangle_data = msh.cell_data_dict["gmsh:physical"][key]
    elif key == "tetra":
        tetra_data = msh.cell_data_dict["gmsh:physical"][key]
for cell in msh.cells:
    if cell.type == "tetra":
        tetra_cells = cell.data
    elif cell.type == "triangle":
        triangle_cells = cell.data
tetra_mesh = meshio.Mesh(points=msh.points, cells={"tetra": tetra_cells},
                         cell_data={"name_to_read":[tetra_data]})
triangle_mesh =meshio.Mesh(points=msh.points,
                           cells=[("triangle", triangle_cells)],
                           cell_data={"name_to_read":[triangle_data]})
meshio.write("plate.xdmf", tetra_mesh)
meshio.write("mf.xdmf", triangle_mesh)

but this yields:

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Input In [9], in <cell line: 14>()
     11         triangle_cells = cell.data
     12 tetra_mesh = meshio.Mesh(points=msh.points, cells={"tetra": tetra_cells},
     13                          cell_data={"name_to_read":[tetra_data]})
---> 14 triangle_mesh =meshio.Mesh(points=msh.points,
     15                            cells=[("triangle", triangle_cells)],
     16                            cell_data={"name_to_read":[triangle_data]})
     17 meshio.write("plate.xdmf", tetra_mesh)
     18 meshio.write("mf.xdmf", triangle_mesh)

File /usr/local/lib/python3.9/dist-packages/meshio/_mesh.py:182, in Mesh.__init__(self, points, cells, point_data, cell_data, field_data, point_sets, cell_sets, gmsh_periodic, info)
    180 data[k] = np.asarray(data[k])
    181 if len(data[k]) != len(self.cells[k]):
--> 182     raise ValueError(
    183         "Incompatible cell data. "
    184         + f"Cell block {k} ('{self.cells[k].type}') "
    185         + f"has length {len(self.cells[k])}, but "
    186         + f"corresponding cell data item has length {len(data[k])}."
    187     )

ValueError: Incompatible cell data. Cell block 0 ('triangle') has length 4, but corresponding cell data item has length 8.

Googling the error yielded me to try:

import meshio


def create_mesh(mesh, cell_type, prune_z=False):
    cells = mesh.get_cells_type(cell_type)
    cell_data = mesh.get_cell_data("gmsh:physical", cell_type)
    out_mesh = meshio.Mesh(points=mesh.points, cells={
                           cell_type: cells}, cell_data={"name_to_read": [cell_data]})
    if prune_z:
        out_mesh.prune_z_0()
    return out_mesh


msh = meshio.read("file.msh")
facet_mesh = create_mesh(msh, "triangle", prune_z=True)
meshio.write("facet_mesh.xdmf", facet_mesh)

triangle_mesh = create_mesh(msh, "tetra", prune_z=True)
meshio.write("mesh.xdmf", triangle_mesh)

but this yielded the error:

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Input In [15], in <cell line: 15>()
     11     return out_mesh
     14 msh = meshio.read("file.msh")
---> 15 facet_mesh = create_mesh(msh, "triangle", prune_z=True)
     16 meshio.write("facet_mesh.xdmf", facet_mesh)
     18 triangle_mesh = create_mesh(msh, "tetra", prune_z=True)

Input In [15], in create_mesh(mesh, cell_type, prune_z)
      7 out_mesh = meshio.Mesh(points=mesh.points, cells={
      8                        cell_type: cells}, cell_data={"name_to_read": [cell_data]})
      9 if prune_z:
---> 10     out_mesh.prune_z_0()
     11 return out_mesh

AttributeError: 'Mesh' object has no attribute 'prune_z_0'

I ran out of ideas on how to achieve my goal. Any pointer is welcome, and if there’s a quicker/neater way of doing it with pygmsh, then let me know, I’d rather learn good habits than using an archaic way of doing things.

Consider:
https://jorgensd.github.io/dolfinx-tutorial/chapter3/subdomains.html?highlight=meshio#subdomains-defined-from-external-mesh-data

Thanks @dokken.
Unfortunately, I haven’t solved my problem yet. I followed your suggestion (I created a new Jupyter notebook session with your tutorial version of FEniCSx). I started with my file.msh already created using Gmsh GUI, and then followed your page as follows:

from mpi4py import MPI
import gmsh
from dolfinx.io import gmshio
mesh, cell_markers, facet_markers = gmshio.read_from_msh("meshes/long_wire.msh", MPI.COMM_WORLD, gdim=3)

which produced the output:

Info    : Reading 'meshes/long_wire.msh'...
Info    : 27 entities
Info    : 47 nodes
Info    : 79 elements
Info    : Done reading 'meshes/long_wire.msh'

So far so good. Then:

proc = MPI.COMM_WORLD.rank 
import meshio
def create_mesh(mesh, cell_type, prune_z=False):
    cells = mesh.get_cells_type(cell_type)
    cell_data = mesh.get_cell_data("gmsh:physical", cell_type)
    points = mesh.points[:,:2] if prune_z else mesh.points
    out_mesh = meshio.Mesh(points=points, cells={cell_type: cells}, cell_data={"name_to_read":[cell_data]})
    return out_mesh

if proc == 0:
    # Read in mesh
    msh = meshio.read("meshes/long_wire.msh")
   
    # Create and save one file for the mesh, and one file for the facets 
    triangle_mesh = create_mesh(msh, "triangle", prune_z=False)
    line_mesh = create_mesh(msh, "line", prune_z=False)
    meshio.write("mesh.xdmf", triangle_mesh)
    meshio.write("mt.xdmf", line_mesh)

Where I set prune_z = False because my mesh is three-dimensional. But this returned the error:

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In [20], line 17
     15 # Create and save one file for the mesh, and one file for the facets 
     16 triangle_mesh = create_mesh(msh, "triangle", prune_z=False)
---> 17 line_mesh = create_mesh(msh, "line", prune_z=False)
     18 meshio.write("mesh.xdmf", triangle_mesh)
     19 meshio.write("mt.xdmf", line_mesh)

Cell In [20], line 6, in create_mesh(mesh, cell_type, prune_z)
      4 def create_mesh(mesh, cell_type, prune_z=False):
      5     cells = mesh.get_cells_type(cell_type)
----> 6     cell_data = mesh.get_cell_data("gmsh:physical", cell_type)
      7     points = mesh.points[:,:2] if prune_z else mesh.points
      8     out_mesh = meshio.Mesh(points=points, cells={cell_type: cells}, cell_data={"name_to_read":[cell_data]})

File /usr/local/lib/python3.10/dist-packages/meshio/_mesh.py:249, in Mesh.get_cell_data(self, name, cell_type)
    248 def get_cell_data(self, name: str, cell_type: str):
--> 249     return np.concatenate(
    250         [d for c, d in zip(self.cells, self.cell_data[name]) if c.type == cell_type]
    251     )

File <__array_function__ internals>:180, in concatenate(*args, **kwargs)

ValueError: need at least one array to concatenate

I don’t really know why I get this error. “triangle mesh” yields

<meshio mesh object>
  Number of points: 47
  Number of cells:
    triangle: 8
  Cell data: name_to_read

for information.

It is quite obvious, as your mesh consists of triangles and tetrahedrons, not triangles and lines.
Change line to tetra

1 Like

Great, this works!
Following the tutorial, I get a line that crashes the kernel (no traceback by default).
The part:

from dolfinx.io import XDMFFile
with XDMFFile(MPI.COMM_WORLD, "mesh.xdmf", "r") as xdmf:
    mesh = xdmf.read_mesh(name="Grid")
    ct = xdmf.read_meshtags(mesh, name="Grid")

It’s the last line that crashes the kernel, no matter how many times I rerun the full code, that line crashes it.

Well, its hard to give you any pointers without a reproducible example. How does the mesh look like when visualized in paraview?

I see, I gave my gmsh code in the 1st post. I generated the mesh in the automated way with gmsh GUI.
I am used to Gmsh to visualize meshes. When I use Paraview to open the file mesh.xdmf, it asks me which data reader to use (it gives me 3 choices, but visually I see no difference): XDMF reader, Xdmf3ReaderS and Xdmf3ReaderT. I see 2 squares, corresponding to the wire’s ends.

Hmm, but maybe it’s fine? After all, I only defined those 2 surfaces as physical surfaces. When I use the “Point Gaussian” representation of the mesh in Paraview, I get the following:

which looks “OK” in that this resembles what I see with Gmsh.

Note that the mesh you want to write to mesh.xdmf is the mesh with tetrahedrons, not triangles.

1 Like

I also use the Gmsh GUI. Here’s the script I use to convert my .msh files to .xdmf, which works for 1D, 2D, or 3D meshes. You can call msh_to_xmdf from a Python script, or you can run it from the command line with

python3 [-prune] <filename>

From the command line, the -prune option causes the script to keep only x-coordinate (for 1D meshes) or only the x- and y- coordinates (for 2D meshes). This file produces a file named <filename>.xdmf which contains the mesh geometry and (if present) cell markers. If facet markers are present, the file also writes a file named <filename>_facets.xdmf which contains the facet markers. If you happen to find any errors with the script, please let me know!

#! python3
# -*- coding: utf-8 -*-
#
# mesh_convert.py
#
# Convert Gmsh .msh files to FEniCS-friendly formats.
#
# Created:        2022-11-07 12:58:10 by Connor D. Pierce
# Last modified:  2022-11-13 11:58:39 by Connor D. Pierce
#
# Copyright (c) 2022 Connor D. Pierce
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.

# You should have received a copy of the GNU Lesser General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: LGPL-3.0-or-later


"""
Convert Gmsh .msh files to FEniCS-friendly formats.

Functions
=========
msh_to_xdmf
"""


import meshio


def msh_to_xdmf(filename: str, prune: bool = True):
    """Convert a .msh file to .xdmf.
    
    Parameters
    ==========
    filename:
        The file name to read/write (not including extension). The extension ".msh" is
        appended to create the 'read from' file name, and the extension ".xdmf" is
        appended to create the 'write to' file name. If any mesh facets are marked as
        "Physical" regions, the suffix "_facets.xdmf" is appended to create the file
        name for exporting this information.
    prune:
        Force the geometric dimension to match the topological dimension of the mesh by
        removing geometry coordinates of higher dimension than the mesh topology (i.e.
        for 1D meshes, only the x-coordinate is retained, for 2D meshes only the
        x- and y-coordinates are retained, etc.)
    """

    # Importing mesh from gmsh and defining surface and boundary markers
    msh = meshio.read(filename + ".msh")

    # Determine mesh topological dimension
    if "tetra" in msh.cells_dict:
        tdim = 3
        cell_lbl = "tetra"
        facet_lbl = "triangle"
    elif "triangle" in msh.cells_dict:
        tdim = 2
        cell_lbl = "triangle"
        facet_lbl = "line"
    else:
        tdim = 1
        cell_lbl = "line"
        facet_lbl = "point"

    # Extract node coordinates
    if prune:
        points = msh.points[:, :tdim]
    else:
        points = msh.points

    # Extract cells and facets
    cells = msh.cells_dict[cell_lbl]
    if facet_lbl in msh.cells_dict:
        facets = msh.cells_dict[facet_lbl]
    else:
        facets = None

    # Extract cell and facet markers
    if cell_lbl in msh.cell_data_dict["gmsh:physical"]:
        cell_data = msh.cell_data_dict["gmsh:physical"][cell_lbl]
    else:
        cell_data = None

    if facet_lbl in msh.cell_data_dict["gmsh:physical"]:
        facet_data = msh.cell_data_dict["gmsh:physical"][facet_lbl]
    else:
        facet_data = None

    cell_mesh = meshio.Mesh(
        points=points,
        cells={cell_lbl: cells},
        point_data=None,
        cell_data={"cell_marker": [cell_data]} if cell_data is not None else None,
        field_data=None,
        point_sets=None,
        cell_sets=None,
        gmsh_periodic=None, #TODO: see if it is necessary to propagate this data
        info=None,
    )
    meshio.write(filename + ".xdmf", cell_mesh)

    if facet_data is not None:
        facet_mesh = meshio.Mesh(
            points=points,
            cells={facet_lbl: facets},
            cell_data={"facet_marker": [facet_data]},
        )
        meshio.write(filename + "_facets.xdmf", facet_mesh)


if __name__ == "__main__":
    import sys
    
    prune = "-prune" in sys.argv
    msh_to_xdmf(sys.argv[-1], prune)
2 Likes

Thank you very much guys!
Very nice of you to share your code @conpierce8, this will help many people, I am sure.
Meanwhile I might start a new thread related to this one, I will try to convert an old code I have for FEniCS to be used with FEniCSx. The mesh was the first part.