lapy 1.6.0__tar.gz → 1.7.0.dev0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {lapy-1.6.0 → lapy-1.7.0.dev0}/PKG-INFO +12 -17
  2. {lapy-1.6.0 → lapy-1.7.0.dev0}/README.md +3 -2
  3. lapy-1.7.0.dev0/lapy/_cholmod.py +102 -0
  4. lapy-1.7.0.dev0/lapy/_gmsh_io.py +266 -0
  5. {lapy-1.6.0 → lapy-1.7.0.dev0}/lapy/_tet_io.py +11 -96
  6. {lapy-1.6.0 → lapy-1.7.0.dev0}/lapy/_tria_io.py +15 -257
  7. {lapy-1.6.0 → lapy-1.7.0.dev0}/lapy/conformal.py +353 -120
  8. {lapy-1.6.0 → lapy-1.7.0.dev0}/lapy/diffgeo.py +26 -32
  9. {lapy-1.6.0 → lapy-1.7.0.dev0}/lapy/heat.py +8 -13
  10. lapy-1.7.0.dev0/lapy/intersect.py +238 -0
  11. lapy-1.7.0.dev0/lapy/remesh.py +434 -0
  12. {lapy-1.6.0 → lapy-1.7.0.dev0}/lapy/shapedna.py +3 -3
  13. {lapy-1.6.0 → lapy-1.7.0.dev0}/lapy/solver.py +24 -20
  14. {lapy-1.6.0 → lapy-1.7.0.dev0}/lapy/tria_mesh.py +289 -18
  15. {lapy-1.6.0 → lapy-1.7.0.dev0}/lapy/utils/_config.py +0 -2
  16. {lapy-1.6.0 → lapy-1.7.0.dev0}/lapy/utils/_imports.py +1 -4
  17. {lapy-1.6.0 → lapy-1.7.0.dev0}/lapy.egg-info/PKG-INFO +12 -17
  18. {lapy-1.6.0 → lapy-1.7.0.dev0}/lapy.egg-info/SOURCES.txt +4 -1
  19. {lapy-1.6.0 → lapy-1.7.0.dev0}/lapy.egg-info/requires.txt +6 -13
  20. {lapy-1.6.0 → lapy-1.7.0.dev0}/pyproject.toml +26 -29
  21. lapy-1.6.0/setup.py +0 -3
  22. {lapy-1.6.0 → lapy-1.7.0.dev0}/LICENSE +0 -0
  23. {lapy-1.6.0 → lapy-1.7.0.dev0}/lapy/__init__.py +0 -0
  24. {lapy-1.6.0 → lapy-1.7.0.dev0}/lapy/_read_geometry.py +0 -0
  25. {lapy-1.6.0 → lapy-1.7.0.dev0}/lapy/_version.py +0 -0
  26. {lapy-1.6.0 → lapy-1.7.0.dev0}/lapy/commands/__init__.py +0 -0
  27. {lapy-1.6.0 → lapy-1.7.0.dev0}/lapy/commands/sys_info.py +0 -0
  28. {lapy-1.6.0 → lapy-1.7.0.dev0}/lapy/io.py +0 -0
  29. {lapy-1.6.0 → lapy-1.7.0.dev0}/lapy/plot.py +0 -0
  30. {lapy-1.6.0 → lapy-1.7.0.dev0}/lapy/polygon.py +0 -0
  31. {lapy-1.6.0 → lapy-1.7.0.dev0}/lapy/tet_mesh.py +0 -0
  32. {lapy-1.6.0 → lapy-1.7.0.dev0}/lapy/utils/__init__.py +0 -0
  33. {lapy-1.6.0 → lapy-1.7.0.dev0}/lapy.egg-info/dependency_links.txt +0 -0
  34. {lapy-1.6.0 → lapy-1.7.0.dev0}/lapy.egg-info/entry_points.txt +0 -0
  35. {lapy-1.6.0 → lapy-1.7.0.dev0}/lapy.egg-info/top_level.txt +0 -0
  36. {lapy-1.6.0 → lapy-1.7.0.dev0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lapy
3
- Version: 1.6.0
3
+ Version: 1.7.0.dev0
4
4
  Summary: A package for differential geometry on meshes (Laplace, FEM)
5
5
  Author-email: Martin Reuter <martin.reuter@dzne.de>
6
6
  Maintainer-email: Martin Reuter <martin.reuter@dzne.de>
@@ -14,43 +14,39 @@ Classifier: Operating System :: Microsoft :: Windows
14
14
  Classifier: Operating System :: Unix
15
15
  Classifier: Operating System :: MacOS
16
16
  Classifier: Programming Language :: Python :: 3 :: Only
17
- Classifier: Programming Language :: Python :: 3.10
18
17
  Classifier: Programming Language :: Python :: 3.11
19
18
  Classifier: Programming Language :: Python :: 3.12
20
19
  Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
21
  Classifier: Natural Language :: English
22
22
  Classifier: Intended Audience :: Science/Research
23
- Requires-Python: >=3.10
23
+ Requires-Python: >=3.11
24
24
  Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
- Requires-Dist: nibabel
27
- Requires-Dist: numpy>=1.21
28
- Requires-Dist: plotly
29
- Requires-Dist: psutil
30
- Requires-Dist: scipy!=1.13.0
26
+ Requires-Dist: nibabel>=4.0
27
+ Requires-Dist: numpy>=1.23.2
28
+ Requires-Dist: plotly>=5.0
29
+ Requires-Dist: psutil>=5.9
30
+ Requires-Dist: scipy!=1.13.0,>=1.9.2
31
31
  Provides-Extra: build
32
32
  Requires-Dist: build; extra == "build"
33
33
  Requires-Dist: twine; extra == "build"
34
34
  Provides-Extra: chol
35
- Requires-Dist: scikit-sparse>=0.4.16; extra == "chol"
35
+ Requires-Dist: cholespy>=2.0; extra == "chol"
36
36
  Provides-Extra: doc
37
37
  Requires-Dist: furo!=2023.8.17; extra == "doc"
38
38
  Requires-Dist: matplotlib; extra == "doc"
39
- Requires-Dist: memory-profiler; extra == "doc"
40
39
  Requires-Dist: numpydoc; extra == "doc"
41
40
  Requires-Dist: sphinx!=7.2.*,>=7.0; extra == "doc"
42
41
  Requires-Dist: sphinxcontrib-bibtex; extra == "doc"
43
42
  Requires-Dist: sphinx-copybutton; extra == "doc"
44
43
  Requires-Dist: sphinx-design; extra == "doc"
45
- Requires-Dist: sphinx-gallery; extra == "doc"
46
- Requires-Dist: sphinx-issues; extra == "doc"
47
44
  Requires-Dist: nbsphinx; extra == "doc"
48
45
  Requires-Dist: IPython; extra == "doc"
49
46
  Requires-Dist: ipykernel; extra == "doc"
50
47
  Provides-Extra: style
51
48
  Requires-Dist: bibclean; extra == "style"
52
49
  Requires-Dist: codespell; extra == "style"
53
- Requires-Dist: pydocstyle[toml]; extra == "style"
54
50
  Requires-Dist: ruff; extra == "style"
55
51
  Provides-Extra: test
56
52
  Requires-Dist: pytest; extra == "test"
@@ -62,8 +58,6 @@ Requires-Dist: lapy[chol]; extra == "all"
62
58
  Requires-Dist: lapy[doc]; extra == "all"
63
59
  Requires-Dist: lapy[style]; extra == "all"
64
60
  Requires-Dist: lapy[test]; extra == "all"
65
- Provides-Extra: full
66
- Requires-Dist: lapy[all]; extra == "full"
67
61
  Dynamic: license-file
68
62
 
69
63
  [![PyPI version](https://badge.fury.io/py/lapy.svg)](https://pypi.org/project/lapy/)
@@ -94,6 +88,8 @@ Key design goals:
94
88
  - `heat`: heat kernel and diffusion utilities, geodesics via heat method.
95
89
  - `shapedna`: compute ShapeDNA (Laplace spectra) for shape descriptors.
96
90
  - `conformal`: conformal mapping methods for genus-0 surfaces.
91
+ - `intersect`: self-intersection tests for triangle meshes, and undoing vertex moves that create intersections.
92
+ - `remesh`: isotropic remeshing to near-equilateral triangles that keeps the topology, and closest points on a surface.
97
93
  - `io`: read/write vertex functions and eigenvector files.
98
94
  - `plot`: lightweight Plotly wrappers for interactive visualization.
99
95
 
@@ -128,8 +124,7 @@ python3 -m pip install --user --src /my/preferred/location --editable git+https:
128
124
 
129
125
  ### Dependencies and optional backends
130
126
  - Core: Python 3, NumPy, SciPy.
131
- - Optional (recommended): scikit-sparse (for CHOLMOD) to accelerate Cholesky sparse solves.
132
- Note: CHOLMOD (via scikit-sparse) is not currently installable via plain `pip` on all platforms; use `conda` when possible. If `use_cholmod=True` is requested, LaPy attempts to import CHOLMOD and will raise if it is unavailable. Install ordering: install `numpy` and `scipy` first, then `scikit-sparse`.
127
+ - Optional: cholespy (for CHOLMOD) to accelerate sparse solves with the Cholesky decomposition. Install it with `pip install lapy[chol]`. If `use_cholmod=True` is requested without cholespy, LaPy raises an ImportError.
133
128
 
134
129
  ## Solver backends
135
130
  - Default: SciPy sparse LU/QR routines.
@@ -26,6 +26,8 @@ Key design goals:
26
26
  - `heat`: heat kernel and diffusion utilities, geodesics via heat method.
27
27
  - `shapedna`: compute ShapeDNA (Laplace spectra) for shape descriptors.
28
28
  - `conformal`: conformal mapping methods for genus-0 surfaces.
29
+ - `intersect`: self-intersection tests for triangle meshes, and undoing vertex moves that create intersections.
30
+ - `remesh`: isotropic remeshing to near-equilateral triangles that keeps the topology, and closest points on a surface.
29
31
  - `io`: read/write vertex functions and eigenvector files.
30
32
  - `plot`: lightweight Plotly wrappers for interactive visualization.
31
33
 
@@ -60,8 +62,7 @@ python3 -m pip install --user --src /my/preferred/location --editable git+https:
60
62
 
61
63
  ### Dependencies and optional backends
62
64
  - Core: Python 3, NumPy, SciPy.
63
- - Optional (recommended): scikit-sparse (for CHOLMOD) to accelerate Cholesky sparse solves.
64
- Note: CHOLMOD (via scikit-sparse) is not currently installable via plain `pip` on all platforms; use `conda` when possible. If `use_cholmod=True` is requested, LaPy attempts to import CHOLMOD and will raise if it is unavailable. Install ordering: install `numpy` and `scipy` first, then `scikit-sparse`.
65
+ - Optional: cholespy (for CHOLMOD) to accelerate sparse solves with the Cholesky decomposition. Install it with `pip install lapy[chol]`. If `use_cholmod=True` is requested without cholespy, LaPy raises an ImportError.
65
66
 
66
67
  ## Solver backends
67
68
  - Default: SciPy sparse LU/QR routines.
@@ -0,0 +1,102 @@
1
+ """Sparse Cholesky factorization with CHOLMOD, provided by the cholespy package."""
2
+
3
+ import numpy as np
4
+ from scipy import sparse
5
+
6
+ from .utils._imports import import_optional_dependency
7
+
8
+ _INT32_MAX = np.iinfo(np.int32).max
9
+
10
+
11
+ def require_cholmod() -> None:
12
+ """Raise an ImportError if cholespy, which provides CHOLMOD, is not installed."""
13
+ import_optional_dependency("cholespy", extra="It is required for use_cholmod=True.", raise_error=True)
14
+
15
+
16
+ class CholeskyFactor:
17
+ """Cholesky factorization of a sparse symmetric positive definite matrix.
18
+
19
+ CHOLMOD reads only one triangle of the matrix, so the matrix has to be
20
+ genuinely symmetric. The factorization and the solves run in double
21
+ precision; solutions are returned in ``float32`` for a ``float32`` matrix
22
+ and in ``float64`` otherwise.
23
+
24
+ Parameters
25
+ ----------
26
+ matrix : scipy.sparse.spmatrix
27
+ Sparse symmetric positive definite matrix of shape (n, n).
28
+
29
+ Raises
30
+ ------
31
+ ImportError
32
+ If cholespy is not installed.
33
+ ValueError
34
+ If the matrix is not square, too large for 32-bit indices, or not
35
+ positive definite.
36
+ """
37
+
38
+ def __init__(self, matrix) -> None:
39
+ cholespy = import_optional_dependency(
40
+ "cholespy", extra="It is required for use_cholmod=True.", raise_error=True
41
+ )
42
+ a = sparse.csc_matrix(matrix)
43
+ a.sum_duplicates()
44
+ a.sort_indices()
45
+ n = a.shape[0]
46
+ if a.shape != (n, n):
47
+ raise ValueError(f"Cholesky factorization needs a square matrix, got shape {a.shape}.")
48
+ # cholespy takes 32-bit indices; scipy switches to 64-bit ones beyond that range
49
+ if n > _INT32_MAX or a.nnz > _INT32_MAX:
50
+ raise ValueError(
51
+ f"Matrix with {n} rows and {a.nnz} non-zeros is too large for CHOLMOD's 32-bit indices."
52
+ )
53
+ # output precision; cholespy factorizes in double precision on the CPU either way
54
+ self.dtype = np.dtype(np.float32) if a.dtype == np.float32 else np.dtype(np.float64)
55
+ # Passed as CSR, which for a symmetric matrix holds the same arrays as CSC.
56
+ # cholespy reallocates the arrays of a CSC input in place, which corrupts the
57
+ # heap when NumPy owns them (it crashes on Windows), but copies a CSR input.
58
+ try:
59
+ self._solver = cholespy.CholeskySolverD(
60
+ n,
61
+ a.indptr.astype(np.int32),
62
+ a.indices.astype(np.int32),
63
+ a.data.astype(np.float64),
64
+ cholespy.MatrixType.CSR,
65
+ )
66
+ except ValueError as err:
67
+ if "positive definite" not in str(err):
68
+ raise
69
+ raise ValueError(
70
+ "Cholesky factorization failed because the matrix is not positive definite, "
71
+ "for example a Laplace operator without boundary conditions. "
72
+ "Use use_cholmod=False for the LU decomposition instead."
73
+ ) from err
74
+ self.shape = (n, n)
75
+
76
+ def solve(self, b) -> np.ndarray:
77
+ """Solve ``A x = b`` with the factorized matrix.
78
+
79
+ Parameters
80
+ ----------
81
+ b : array_like or scipy.sparse.spmatrix
82
+ Right hand side of shape (n,) or (n, n_rhs).
83
+
84
+ Returns
85
+ -------
86
+ np.ndarray
87
+ Solution of the same shape as ``b``, ``float32`` for a ``float32``
88
+ matrix and ``float64`` otherwise.
89
+
90
+ Raises
91
+ ------
92
+ ValueError
93
+ If the first dimension of ``b`` does not match the matrix.
94
+ """
95
+ if sparse.issparse(b):
96
+ b = b.toarray()
97
+ b = np.ascontiguousarray(np.asarray(b), dtype=np.float64)
98
+ if b.ndim not in (1, 2) or b.shape[0] != self.shape[0]:
99
+ raise ValueError(f"Right hand side of shape {b.shape} does not match a matrix of shape {self.shape}.")
100
+ x = np.empty_like(b)
101
+ self._solver.solve(b, x)
102
+ return x.astype(self.dtype, copy=False)
@@ -0,0 +1,266 @@
1
+ """Shared reader for GMSH meshes, MSH 2 ASCII format.
2
+
3
+ Both :mod:`lapy._tria_io` and :mod:`lapy._tet_io` parse the same file format,
4
+ so the parsing lives here once and each of them keeps only the thin wrapper
5
+ that turns the result into its own mesh type.
6
+
7
+ .. moduleauthor:: Nico Schloemer <nico.schloemer@gmail.com>
8
+ LICENSE MIT
9
+ https://github.com/nschloe/meshio
10
+ http://geuz.org/gmsh/doc/texinfo/gmsh.html#MSH-ASCII-file-format
11
+ """
12
+
13
+ import logging
14
+ import os
15
+ import re
16
+ from itertools import islice
17
+
18
+ import numpy as np
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # gmsh element code -> name, and the vertex count of each name.
23
+ GMSH_TO_NAME = {
24
+ 15: "vertex",
25
+ 1: "line",
26
+ 2: "triangle",
27
+ 3: "quad",
28
+ 4: "tetra",
29
+ 5: "hexahedron",
30
+ 6: "wedge",
31
+ 7: "pyramid",
32
+ 8: "line3",
33
+ 9: "triangle6",
34
+ 10: "quad9",
35
+ 11: "tetra10",
36
+ 12: "hexahedron27",
37
+ 13: "prism18",
38
+ 14: "pyramid14",
39
+ 26: "line4",
40
+ 36: "quad16",
41
+ }
42
+ NAME_TO_GMSH = {v: k for k, v in GMSH_TO_NAME.items()}
43
+ NODES_PER_CELL = {
44
+ "vertex": 1,
45
+ "line": 2,
46
+ "triangle": 3,
47
+ "quad": 4,
48
+ "tetra": 4,
49
+ "hexahedron": 8,
50
+ "wedge": 6,
51
+ "pyramid": 5,
52
+ "line3": 3,
53
+ "triangle6": 6,
54
+ "quad9": 9,
55
+ "tetra10": 10,
56
+ "hexahedron27": 27,
57
+ "prism18": 18,
58
+ "pyramid14": 14,
59
+ "line4": 4,
60
+ "quad16": 16,
61
+ }
62
+
63
+
64
+ def _fail(msg: str) -> None:
65
+ """Log and raise in the format the other readers use."""
66
+ msg = f"[{msg}] --> FAILED\n"
67
+ logger.error(msg)
68
+ raise ValueError(msg)
69
+
70
+
71
+ def _expect(line: str, keyword: str) -> None:
72
+ """Fail on a malformed file.
73
+
74
+ Not an assert: these validate untrusted file content, and assert is
75
+ stripped under ``python -O``, which would let a truncated file through and
76
+ only surface much later as a wrong mesh.
77
+ """
78
+ if line.strip() != keyword:
79
+ _fail(f"{keyword} keyword not found")
80
+
81
+
82
+ def _holds_type(block: str, name: str) -> bool:
83
+ """Say whether an element block contains a cell type, without parsing it.
84
+
85
+ Each record starts ``number type ...``, so the type is the second token of
86
+ a line. Scanning the raw text for that is several times cheaper than
87
+ converting the whole block to integers, which lets a caller that asked for
88
+ triangles reject a volume mesh before paying for the conversion.
89
+ """
90
+ return re.search(rf"(?m)^\s*\d+\s+{NAME_TO_GMSH[name]}\s", block) is not None
91
+
92
+
93
+ def _cells_from_block(flat: np.ndarray, num_cells: int) -> dict:
94
+ """Split a flat MSH 2 element block into 0-based node indices per cell type.
95
+
96
+ A record is ``number type n_tags <tags...> <nodes...>``, so records only
97
+ share a width when the block holds a single element type with a single tag
98
+ count. That is the common case and reshapes in one step; a mixed block
99
+ falls back to walking the records.
100
+
101
+ Parameters
102
+ ----------
103
+ flat : np.ndarray
104
+ All integers of the element block, in file order.
105
+ num_cells : int
106
+ Number of records the block declares.
107
+
108
+ Returns
109
+ -------
110
+ dict
111
+ Maps an element name to its (n_cells, n_vertices) index array.
112
+ """
113
+ if num_cells == 0:
114
+ return {}
115
+
116
+ width, remainder = divmod(flat.size, num_cells)
117
+ if remainder == 0:
118
+ rows = flat.reshape(num_cells, width)
119
+ etype, n_tags = int(rows[0, 1]), int(rows[0, 2])
120
+ name = GMSH_TO_NAME[etype]
121
+ uniform = (
122
+ 3 + n_tags + NODES_PER_CELL[name] == width
123
+ and bool((rows[:, 1] == etype).all())
124
+ and bool((rows[:, 2] == n_tags).all())
125
+ )
126
+ if uniform:
127
+ return {name: rows[:, -NODES_PER_CELL[name] :] - 1}
128
+
129
+ # Mixed block: walk the records. tolist() keeps the walk in plain python,
130
+ # which is far cheaper than numpy scalar indexing in a loop, and the node
131
+ # indices are extended into one flat list per type rather than appended as
132
+ # rows, so the final conversion is a single reshape.
133
+ values = flat.tolist()
134
+ chunks: dict = {}
135
+ pos, size = 0, len(values)
136
+ while pos < size:
137
+ name = GMSH_TO_NAME[values[pos + 1]]
138
+ n_nodes = NODES_PER_CELL[name]
139
+ end = pos + 3 + values[pos + 2] + n_nodes
140
+ chunks.setdefault(name, []).extend(values[end - n_nodes : end])
141
+ pos = end
142
+ return {
143
+ k: np.array(v, dtype=np.int64).reshape(-1, NODES_PER_CELL[k]) - 1
144
+ for k, v in chunks.items()
145
+ }
146
+
147
+
148
+ def read_gmsh(filename: str, want: str | None = None) -> tuple[np.ndarray, dict]:
149
+ """Read points and cells from a GMSH file, MSH 2 ASCII format.
150
+
151
+ Parameters
152
+ ----------
153
+ filename : str
154
+ Filename to load, must end in ``.msh``.
155
+ want : str or None, default=None
156
+ Cell type the caller needs, for example ``"triangle"`` or ``"tetra"``.
157
+ When given, a file that cannot supply it is rejected as early as
158
+ possible: the node coordinates are skipped rather than parsed until the
159
+ element block has been checked, and the check itself is a text scan
160
+ rather than a full conversion. Reading a volume mesh as a triangle mesh
161
+ therefore costs a fraction of a full read.
162
+
163
+ Returns
164
+ -------
165
+ points : np.ndarray
166
+ Vertex coordinates, shape (n_points, 3).
167
+ cells : dict
168
+ Maps an element name to its (n_cells, n_vertices) index array.
169
+
170
+ Raises
171
+ ------
172
+ OSError
173
+ If file is not found or not readable.
174
+ ValueError
175
+ If the extension is not ``.msh``, the file is binary or not version 2,
176
+ a section is malformed, or ``want`` is not present.
177
+ """
178
+ if os.path.splitext(filename)[1] != ".msh":
179
+ _fail("no .msh file")
180
+
181
+ logger.debug("--> GMSH format ... ")
182
+
183
+ try:
184
+ f = open(filename)
185
+ except OSError:
186
+ logger.error("[file not found or not readable]")
187
+ raise
188
+
189
+ points = None
190
+ node_offset = None
191
+ num_nodes = 0
192
+ cells: dict = {}
193
+
194
+ with f:
195
+ while True:
196
+ line = f.readline()
197
+ if not line:
198
+ break
199
+ if not line.startswith("$"):
200
+ _fail(f"section keyword expected, found '{line.strip()}'")
201
+ section = line[1:].strip()
202
+
203
+ if section == "MeshFormat":
204
+ larr = list(filter(None, f.readline().split()))
205
+ if larr[0][0] != "2":
206
+ _fail(f"need mesh format 2, found {larr[0]}")
207
+ if larr[1] != "0":
208
+ # The file is opened in text mode, so the binary layout
209
+ # cannot be read from this handle at all.
210
+ _fail("binary format not implemented")
211
+ _expect(f.readline(), "$EndMeshFormat")
212
+
213
+ elif section == "Nodes":
214
+ num_nodes = int(f.readline())
215
+ if want is not None and not cells:
216
+ # Skip for now. If the element block turns out not to hold
217
+ # `want` we never pay to parse these coordinates, and
218
+ # skipping is about thirty times cheaper than parsing.
219
+ node_offset = f.tell()
220
+ for _ in islice(f, num_nodes):
221
+ pass
222
+ else:
223
+ points = _read_points(f, num_nodes)
224
+ _expect(f.readline(), "$EndNodes")
225
+
226
+ elif section == "Elements":
227
+ num_cells = int(f.readline())
228
+ block = "".join(islice(f, num_cells))
229
+ if want is not None:
230
+ first = block.split("\n", 1)[0].split()
231
+ known = len(first) > 1 and int(first[1]) == NAME_TO_GMSH[want]
232
+ if not known and not _holds_type(block, want):
233
+ _fail(f"file holds no {want} elements")
234
+ cells = _cells_from_block(
235
+ np.array(block.split(), dtype=np.int64), num_cells
236
+ )
237
+ _expect(f.readline(), "$EndElements")
238
+
239
+ else:
240
+ # Skip sections we do not use, such as $PhysicalNames,
241
+ # $Periodic or $NodeData. Erroring out instead would reject
242
+ # perfectly good files.
243
+ end = f"$End{section}"
244
+ while True:
245
+ line = f.readline()
246
+ if not line:
247
+ _fail(f"{end} keyword not found")
248
+ if line.strip() == end:
249
+ break
250
+
251
+ if points is None and node_offset is not None:
252
+ f.seek(node_offset)
253
+ points = _read_points(f, num_nodes)
254
+
255
+ if points is None:
256
+ _fail("$Nodes section not found")
257
+ if want is not None and want not in cells:
258
+ _fail(f"file holds no {want} elements")
259
+
260
+ return points, cells
261
+
262
+
263
+ def _read_points(f, num_nodes: int) -> np.ndarray:
264
+ """Read a node block as (n, 3), dropping the leading index column."""
265
+ points = np.fromfile(f, "float32", num_nodes * 4, " ").reshape(num_nodes, 4)
266
+ return np.ascontiguousarray(points[:, 1:])
@@ -4,7 +4,6 @@ Should be called via the TetMesh member functions.
4
4
  """
5
5
 
6
6
  import logging
7
- import os.path
8
7
  from typing import TYPE_CHECKING
9
8
 
10
9
  import numpy as np
@@ -15,7 +14,7 @@ if TYPE_CHECKING:
15
14
  logger = logging.getLogger(__name__)
16
15
 
17
16
  def read_gmsh(filename: str) -> "TetMesh":
18
- """Load GMSH tetrahedron mesh.
17
+ """Load GMSH tetrahedron mesh, MSH 2 ASCII format.
19
18
 
20
19
  Parameters
21
20
  ----------
@@ -32,101 +31,17 @@ def read_gmsh(filename: str) -> "TetMesh":
32
31
  OSError
33
32
  If file is not found or not readable.
34
33
  ValueError
35
- If file format is invalid or binary format is encountered.
34
+ If the extension is not ``.msh``, the file is binary or not version 2,
35
+ a section is malformed, or the file holds no tetrahedra.
36
36
  """
37
- extension = os.path.splitext(filename)[1]
38
- verbose = 1
39
- if verbose > 0:
40
- logger.info("--> GMSH format ... ")
41
- if extension != ".msh":
42
- msg = "[no .msh file] --> FAILED\n"
43
- logger.error(msg)
44
- raise ValueError(msg)
45
- try:
46
- f = open(filename)
47
- except OSError:
48
- logger.error("[file not found or not readable]")
49
- raise
50
- line = f.readline()
51
- if not line.startswith("$MeshFormat"):
52
- msg = "[$MeshFormat keyword not found] --> FAILED\n"
53
- logger.error(msg)
54
- f.close()
55
- raise ValueError(msg)
56
- line = f.readline()
57
- larr = line.split()
58
- ver = float(larr[0])
59
- ftype = int(larr[1])
60
- datatype = int(larr[2])
61
- logger.debug(
62
- "Msh file ver %s, ftype %s, datatype %s",
63
- ver,
64
- ftype,
65
- datatype,
66
- )
67
- if ftype != 0:
68
- msg = "[binary format not implemented] --> FAILED\n"
69
- logger.error(msg)
70
- f.close()
71
- raise ValueError(msg)
72
- line = f.readline()
73
- if not line.startswith("$EndMeshFormat"):
74
- msg = "[$EndMeshFormat keyword not found] --> FAILED\n"
75
- logger.error(msg)
76
- f.close()
77
- raise ValueError(msg)
78
- line = f.readline()
79
- if not line.startswith("$Nodes"):
80
- msg = "[$Nodes keyword not found] --> FAILED\n"
81
- logger.error(msg)
82
- f.close()
83
- raise ValueError(msg)
84
- pnum = int(f.readline())
85
- # read (nodes X 4) matrix as chunk
86
- # drop first column
87
- v = np.fromfile(f, "float32", 4 * pnum, " ")
88
- v.shape = (pnum, 4)
89
- v = np.delete(v, 0, 1)
90
- line = f.readline()
91
- if not line.startswith("$EndNodes"):
92
- msg = "[$EndNodes keyword not found] --> FAILED\n"
93
- logger.error(msg)
94
- f.close()
95
- raise ValueError(msg)
96
- line = f.readline()
97
- if not line.startswith("$Elements"):
98
- msg = "[$Elements keyword not found] --> FAILED\n"
99
- logger.error(msg)
100
- f.close()
101
- raise ValueError(msg)
102
- tnum = int(f.readline())
103
- pos = f.tell()
104
- line = f.readline()
105
- f.seek(pos)
106
- larr = line.split()
107
- if int(larr[1]) != 4:
108
- logger.debug("larr: %s", larr)
109
- msg = "[can only read tetras] --> FAILED\n"
110
- logger.error(msg)
111
- f.close()
112
- raise ValueError(msg)
113
- # read (nodes X ?) matrix
114
- t = np.fromfile(f, "int", tnum * len(larr), " ")
115
- t.shape = (tnum, len(larr))
116
- t = np.delete(t, np.s_[0 : len(larr) - 4], 1)
117
- line = f.readline()
118
- if not line.startswith("$EndElements"):
119
- logger.debug("Line: %s", line)
120
- msg = "[$EndElements keyword not found] --> FAILED\n"
121
- logger.error(msg)
122
- f.close()
123
- raise ValueError(msg)
124
- f.close()
125
- logger.info(" --> DONE ( V: %d , T: %d )", v.shape[0], t.shape[0])
126
37
  from . import TetMesh
38
+ from ._gmsh_io import read_gmsh as _read
127
39
 
128
- return TetMesh(v, t)
129
-
40
+ points, cells = _read(filename, want="tetra")
41
+ logger.info(
42
+ " --> DONE ( V: %d , T: %d )", points.shape[0], cells["tetra"].shape[0]
43
+ )
44
+ return TetMesh(points, cells["tetra"])
130
45
 
131
46
  def read_vtk(filename: str) -> "TetMesh":
132
47
  """Load VTK tetrahedron mesh.
@@ -194,7 +109,7 @@ def read_vtk(filename: str) -> "TetMesh":
194
109
  pnum = int(larr[1])
195
110
  # read points as chunk
196
111
  v = np.fromfile(f, "float32", 3 * pnum, " ")
197
- v.shape = (pnum, 3)
112
+ v = v.reshape(pnum, 3)
198
113
  # expect polygon or tria_strip line
199
114
  line = f.readline()
200
115
  larr = line.split()
@@ -207,7 +122,7 @@ def read_vtk(filename: str) -> "TetMesh":
207
122
  logger.error(msg)
208
123
  raise ValueError(msg)
209
124
  t = np.fromfile(f, "int", ttnum, " ")
210
- t.shape = (tnum, 5)
125
+ t = t.reshape(tnum, 5)
211
126
  if t[tnum - 1][0] != 4:
212
127
  msg = "[can only read tetras] --> FAILED\n"
213
128
  logger.error(msg)