Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
39b7e6d
added MFEM 3D export feature
mkofler96 Jul 5, 2024
923a777
removed BC parameter and improved doc string
mkofler96 Jul 10, 2024
90c6ad2
added tests for MFEM export
mkofler96 Jul 10, 2024
c0f31fb
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 10, 2024
fb64bc5
Apply Clemen's suggestion for whitespace to \t
mkofler96 Jul 11, 2024
60d45a8
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 11, 2024
4516a55
linebreak
mkofler96 Jul 19, 2024
187fb6f
added hexaedron element export and updated tests
mkofler96 Jul 19, 2024
3df14cf
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 19, 2024
4ba8844
fixed volumex hexa333 and replaced volume tet with existing fixture
mkofler96 Jul 19, 2024
0207602
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 19, 2024
9ee6790
switched to mesh.whatami
mkofler96 Aug 23, 2024
dc665e1
added tests for 2D MFEM export
mkofler96 Aug 23, 2024
866ce3f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 23, 2024
e0722c6
split internal mfem export functions to 2D and 3D
mkofler96 Aug 23, 2024
4ff9c68
Update tests/test_export.py
j042 May 27, 2025
c5d409a
fixed old hexa333 mesh to new single element mesh
mkofler96 May 29, 2025
d97f923
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 29, 2025
fa4fc74
Chore(pyproject): streamline optional dependencies, change ruff py ve…
clemens-fricke Nov 5, 2025
dc04b61
Add(test): Add MFEM export test 2x2x2 boundaries
clemens-fricke Nov 5, 2025
282a15f
Merge branch 'main' into ft-mfem-3D-export
clemens-fricke Nov 5, 2025
dddb3c6
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 5, 2025
923748d
CI: Remove deprecated macos runner
clemens-fricke Nov 6, 2025
878ffd5
CI(precommit): Moved reference branch to develop
clemens-fricke Nov 6, 2025
2ac920b
Chore: Add pull request template
clemens-fricke Nov 6, 2025
cf11f9a
Chore: Removed self reference in optional dependencies
clemens-fricke Nov 6, 2025
bd7f6fe
Fix: PR suggestion incorperations
clemens-fricke Nov 6, 2025
fbf61c8
Fix: PR suggestion incorperations
clemens-fricke Nov 6, 2025
4cce325
Fix: PR suggestion incorperated
clemens-fricke Nov 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Overview

Description

## Addressed issues

* Issues addressed

## Showcase

A short / one-liner example to highlight the (new) feature

```python
import gustaf

...
```

## Checklists

* [ ] Set target branch to develop
* [ ] Documentations are up-to-date.
* [ ] Added example(s)
* [ ] Added test(s)
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
os: [ubuntu-latest, macos-13, macos-latest, windows-latest]
os: [ubuntu-latest, macos-latest, windows-latest]

steps:
- uses: actions/checkout@v4
Expand Down
23 changes: 7 additions & 16 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
# use default options for ci
ci:
autoupdate_schedule: "weekly"
autoupdate_branch: 'develop'
submodules: false

repos:
Expand All @@ -21,25 +22,21 @@ repos:
- id: check-toml
- id: check-yaml
- id: debug-statements
- id: end-of-file-fixer
- id: mixed-line-ending
- id: requirements-txt-fixer
- id: trailing-whitespace

- repo: https://github.com/psf/black
rev: "25.1.0"
hooks:
- id: black
args: [--line-length=79]

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.12
hooks:
# Run the linter.
- id: ruff-check
args: [
--fix,
--exit-non-zero-on-fix,
]
types_or: [python, pyi]
args: [--fix, --exit-non-zero-on-fix]
# Run the formatter.
- id: ruff-format
types_or: [python, pyi]

- repo: https://github.com/codespell-project/codespell
rev: v2.4.1
Expand All @@ -48,9 +45,3 @@ repos:
additional_dependencies:
- tomli
args: [-w]

- repo: https://github.com/keewis/blackdoc
rev: v0.4.1
hooks:
- id: blackdoc
args: [--line-length=75]
2 changes: 1 addition & 1 deletion gustaf/create/faces.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ def to_simplex(quad, alternate=False):
else:
tri = quad
utils.log.debug(
"Non quadrilateral mesh provided, return original" " mesh."
"Non quadrilateral mesh provided, return original mesh."
)

return tri
Expand Down
227 changes: 153 additions & 74 deletions gustaf/io/mfem.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,97 +107,176 @@ def extract_values(fname, start_index, n_lines, total_lines, dtype):
return mesh


def format_array(array):
"""Format NumPy array into string. Each entry in a row is separated by
a blank space and every row is separated by a new line.

Parameters
------------
array: NumPy array

Returns
------------
str
"""
row_strings = [" ".join(map(str, row)) for row in array]
return "\n".join(row_strings)


def export(fname, mesh):
"""Export mesh in MFEM format. Supports 2D triangle and quadrilateral
meshes. Does not support different element attributes or difference in
vertex dimension and mesh dimension.
meshes as well as 3D tetrahedral and hexahedral meshes. Also extracts
boundary information from the GustafObject.BC variable. Does not
support different element attributes or difference in vertex dimension
and mesh dimension.

Parameters
------------
fname: str
mesh: Faces
mesh: Faces, Volumes

Returns
------------
None
"""
# Basic infos
nvertices, dim = mesh.vertices.shape
# Get dimension info based on vertices and choose corresponding export
dim = mesh.vertices.shape[1]

if dim == 2:
elements_string, boundary_string, vertices_string = _export_2d(mesh)
elif dim == 3:
elements_string, boundary_string, vertices_string = _export_3d(mesh)
else:
raise NotImplementedError(
f"Sorry, we cannot export meshes of dimension {dim}."
)

def format_array(array):
"""Format NumPy array into string. Each entry in a row is separated by
a blank space and every row is separated by a new line.
with open(fname, "w") as f:
f.write("MFEM mesh v1.0\n\n")
f.write(f"dimension\n{dim}\n\n")
f.write(elements_string)
f.write(boundary_string)
f.write(vertices_string)

Parameters
------------
array: NumPy array

Returns
------------
str
"""
row_strings = [" ".join(map(str, row)) for row in array]
return "\n".join(row_strings)
def _export_2d(mesh):
"""Internal function to export 2D meshes.

# Export 2D mesh
if dim == 2:
# Elements
element_attribute = 1 # Other numbers not yet supported
elements = mesh.elements
n_elements, n_element_vertices = elements.shape
if n_element_vertices == 3:
geometry_type = geometry_types["TRIANGLE"]
elif n_element_vertices == 4:
geometry_type = geometry_types["SQUARE"]
else:
raise NotImplementedError(
"Sorry, we cannot export mesh with elements "
f"with {n_element_vertices} vertices."
)
e = np.ones((n_elements, 1), dtype=settings.INT_DTYPE)
elements_array = np.hstack(
(element_attribute * e, geometry_type * e, elements)
Parameters
------------
mesh: Faces

Returns
------------
elements_string, boundary_string, vertices_string
"""
element_attribute = 1 # Other numbers not yet supported
elements = mesh.elements
n_elements = elements.shape[0]
if mesh.whatami == "tri":
geometry_type = geometry_types["TRIANGLE"]
elif mesh.whatami == "quad":
geometry_type = geometry_types["SQUARE"]
else:
raise NotImplementedError(
f"Sorry, we cannot export 2D meshes of type {mesh.whatami}."
)
elements_array_string = format_array(elements_array)
elements_string = f"elements\n{n_elements}\n"
elements_string += f"{elements_array_string}\n\n"

# Boundary
edges = mesh.edges()
nboundary_edges = sum(map(len, mesh.BC.values()))
boundary_array = np.empty(
(nboundary_edges, 4), dtype=settings.INT_DTYPE
e = np.ones((n_elements, 1), dtype=settings.INT_DTYPE)
elements_array = np.hstack(
(element_attribute * e, geometry_type * e, elements)
)
elements_array_string = format_array(elements_array)
elements_string = f"elements\n{n_elements}\n"
elements_string += f"{elements_array_string}\n\n"

# Boundary
edges = mesh.edges()
nboundary_edges = sum(map(len, mesh.BC.values()))
boundary_array = np.empty((nboundary_edges, 4), dtype=settings.INT_DTYPE)
startrow = 0
# Add boundary one by one as SEGMENTs
for bid, edgeids in mesh.BC.items():
nedges = len(edgeids)
e = np.ones(nedges).reshape(-1, 1)
vertex_list = edges[edgeids, :]
boundary_array[startrow : (startrow + nedges), :] = np.hstack(
(int(bid) * e, geometry_types["SEGMENT"] * e, vertex_list)
)
startrow = 0
# Add boundary one by one as SEGMENTs
for bid, edgeids in mesh.BC.items():
nedges = len(edgeids)
e = np.ones(nedges).reshape(-1, 1)
vertex_list = edges[edgeids, :]
boundary_array[startrow : (startrow + nedges), :] = np.hstack(
(int(bid) * e, geometry_types["SEGMENT"] * e, vertex_list)
)
startrow += nedges

boundary_array_string = format_array(boundary_array)
boundary_string = f"boundary\n{nboundary_edges}\n"
boundary_string += f"{boundary_array_string}\n\n"

# Vertices
vdim = 2 # Currently only option
vertices_array_string = format_array(mesh.vertices)
vertices_string = f"vertices\n{nvertices}\n{vdim}\n"
vertices_string += f"{vertices_array_string}"

with open(fname, "w") as f:
f.write("MFEM mesh v1.0\n\n")
f.write(f"dimension\n{dim}\n\n")
f.write(elements_string)
f.write(boundary_string)
f.write(vertices_string)

# Export 3D mesh
startrow += nedges
Comment on lines +194 to +205

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add validation for boundary condition data.

The code assumes mesh.BC exists and is properly formatted, but should validate this to prevent runtime errors.

Add validation before processing boundary conditions:

+    # Validate BC format
+    if not hasattr(mesh, 'BC') or not isinstance(mesh.BC, dict):
+        raise AttributeError("Mesh must have a BC attribute of type dict")
+    
+    for bid, edgeids in mesh.BC.items():
+        if not isinstance(edgeids, (list, np.ndarray)):
+            raise TypeError(f"BC values must be lists or arrays, got {type(edgeids)} for key {bid}")
+        if len(edgeids) == 0:
+            continue

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In gustaf/io/mfem.py around lines 197 to 208, the code assumes mesh.BC exists
and is properly formatted without validation, which can cause runtime errors.
Add a check to confirm mesh.BC is present and is a dictionary or similar mapping
type before processing. If mesh.BC is missing or invalid, raise a clear
exception or handle the case gracefully to prevent errors during iteration and
indexing.


boundary_array_string = format_array(boundary_array)
boundary_string = f"boundary\n{nboundary_edges}\n"
boundary_string += f"{boundary_array_string}\n\n"

# Vertices
vdim = 2 # Currently only option
vertices_array_string = format_array(mesh.vertices)
nvertices = mesh.vertices.shape[0]
vertices_string = f"vertices\n{nvertices}\n{vdim}\n"
vertices_string += f"{vertices_array_string}"
return elements_string, boundary_string, vertices_string


def _export_3d(mesh):
"""Internal function to export 3D meshes.

Parameters
------------
mesh: Volumes

Returns
------------
elements_string, boundary_string, vertices_string
"""
element_attribute = 1 # Other numbers not yet supported
elements = mesh.elements
n_elements = elements.shape[0]
if mesh.whatami == "tet":
body_geometry_type = geometry_types["TETRAHEDRON"]
face_geometry_type = geometry_types["TRIANGLE"]
n_face_vertices = 3
elif mesh.whatami == "hexa":
body_geometry_type = geometry_types["CUBE"]
face_geometry_type = geometry_types["SQUARE"]
n_face_vertices = 4
else:
raise NotImplementedError(
f"Sorry, we cannot export mesh of dimension {dim}."
f"Sorry, we cannot export 3D meshes of type {mesh.whatami}."
)
e = np.ones((n_elements, 1), dtype=settings.INT_DTYPE)
elements_array = np.hstack(
(element_attribute * e, body_geometry_type * e, elements)
)
elements_array_string = format_array(elements_array)
elements_string = f"elements\n{n_elements}\n"
elements_string += f"{elements_array_string}\n\n"

# Boundary
faces = mesh.faces()

nboundary_faces = sum(map(len, mesh.BC.values()))
boundary_array = np.empty(
(nboundary_faces, n_face_vertices + 2), dtype=settings.INT_DTYPE
)
startrow = 0
# Add boundary one by one as TRIANGLEs
for bid, faceids in mesh.BC.items():
nfaces = len(faceids)
e = np.ones(nfaces).reshape(-1, 1)
vertex_list = faces[faceids, :]
boundary_array[startrow : (startrow + nfaces), :] = np.hstack(
(int(bid) * e, face_geometry_type * e, vertex_list)
)
startrow += nfaces
Comment on lines +257 to +270

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add validation for 3D boundary condition data.

Similar to the 2D case, the 3D export should validate boundary condition data format.

Add validation before processing boundary conditions:

+    # Validate BC format
+    if not hasattr(mesh, 'BC') or not isinstance(mesh.BC, dict):
+        raise AttributeError("Mesh must have a BC attribute of type dict")
+    
+    for bid, faceids in mesh.BC.items():
+        if not isinstance(faceids, (list, np.ndarray)):
+            raise TypeError(f"BC values must be lists or arrays, got {type(faceids)} for key {bid}")
+        if len(faceids) == 0:
+            continue
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
nboundary_faces = sum(map(len, mesh.BC.values()))
boundary_array = np.empty(
(nboundary_faces, n_face_vertices + 2), dtype=settings.INT_DTYPE
)
startrow = 0
# Add boundary one by one as TRIANGLEs
for bid, faceids in mesh.BC.items():
nfaces = len(faceids)
e = np.ones(nfaces).reshape(-1, 1)
vertex_list = faces[faceids, :]
boundary_array[startrow : (startrow + nfaces), :] = np.hstack(
(int(bid) * e, face_geometry_type * e, vertex_list)
)
startrow += nfaces
# Validate BC format
if not hasattr(mesh, "BC") or not isinstance(mesh.BC, dict):
raise AttributeError("Mesh must have a BC attribute of type dict")
# Ensure each BC entry is a list or array, skip empty entries
for bid, faceids in mesh.BC.items():
if not isinstance(faceids, (list, np.ndarray)):
raise TypeError(f"BC values must be lists or arrays, got {type(faceids)} for key {bid}")
if len(faceids) == 0:
continue
nboundary_faces = sum(map(len, mesh.BC.values()))
boundary_array = np.empty(
(nboundary_faces, n_face_vertices + 2), dtype=settings.INT_DTYPE
)
startrow = 0
# Add boundary one by one as TRIANGLEs
for bid, faceids in mesh.BC.items():
nfaces = len(faceids)
e = np.ones(nfaces).reshape(-1, 1)
vertex_list = faces[faceids, :]
boundary_array[startrow : (startrow + nfaces), :] = np.hstack(
(int(bid) * e, face_geometry_type * e, vertex_list)
)
startrow += nfaces
🤖 Prompt for AI Agents
In gustaf/io/mfem.py around lines 260 to 273, the code processes 3D boundary
condition data without validating its format, which can lead to errors. Add
validation checks before the loop to ensure that the boundary condition data in
mesh.BC is in the expected format (e.g., correct dimensions and types). If the
data is invalid, raise an appropriate error or handle it gracefully to prevent
incorrect processing.


boundary_array_string = format_array(boundary_array)
boundary_string = f"boundary\n{nboundary_faces}\n"
boundary_string += f"{boundary_array_string}\n\n"

# Vertices
vdim = 3 # Currently only option
vertices_array_string = format_array(mesh.vertices)
nvertices = mesh.vertices.shape[0]
vertices_string = f"vertices\n{nvertices}\n{vdim}\n"
vertices_string += f"{vertices_array_string}"
return elements_string, boundary_string, vertices_string
2 changes: 1 addition & 1 deletion gustaf/utils/tictoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def summary(self, log=True, print_=False):
times for each lap from the timer start (cumulative time).
"""
start = self._laps[0]
cumulative = [f"{lap-start:.10f}" for lap in self._laps[1:]]
cumulative = [f"{lap - start:.10f}" for lap in self._laps[1:]]

if log or print_:
message = [f"\n+++ {self._title} - time logs +++\n"]
Expand Down
11 changes: 6 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,16 @@ test = [
"napf>=0.0.5",
]
dev = [
"pytest",
"funi",
"napf",
"pre-commit",
"vedo>=2023.5.0",
"scipy",
"meshio",
"napf>=0.0.5",
"funi>=0.0.1",
"requests",
"pre-commit",
"ipywidgets",
"k3d",
"pytest",
]

[tool.setuptools]
Expand All @@ -82,7 +82,8 @@ version = {attr = "gustaf._version.version"}

[tool.ruff]
line-length = 79
target-version = "py38"
target-version = "py39"
exclude = ["tests/data"]

[tool.ruff.lint]
select = [
Expand Down
Loading