mmgpy 0.5.0__cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
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.
- mmgpy/__init__.py +296 -0
- mmgpy/__main__.py +13 -0
- mmgpy/_io.py +535 -0
- mmgpy/_logging.py +290 -0
- mmgpy/_mesh.py +2286 -0
- mmgpy/_mmgpy.cpython-311-x86_64-linux-gnu.so +0 -0
- mmgpy/_mmgpy.pyi +2140 -0
- mmgpy/_options.py +304 -0
- mmgpy/_progress.py +850 -0
- mmgpy/_pyvista.py +410 -0
- mmgpy/_result.py +143 -0
- mmgpy/_transfer.py +273 -0
- mmgpy/_validation.py +669 -0
- mmgpy/_version.py +3 -0
- mmgpy/_version.py.in +3 -0
- mmgpy/bin/mmg2d_O3 +0 -0
- mmgpy/bin/mmg3d_O3 +0 -0
- mmgpy/bin/mmgs_O3 +0 -0
- mmgpy/interactive/__init__.py +24 -0
- mmgpy/interactive/sizing_editor.py +790 -0
- mmgpy/lagrangian.py +394 -0
- mmgpy/lib/libmmg2d.so +0 -0
- mmgpy/lib/libmmg2d.so.5 +0 -0
- mmgpy/lib/libmmg2d.so.5.8.0 +0 -0
- mmgpy/lib/libmmg3d.so +0 -0
- mmgpy/lib/libmmg3d.so.5 +0 -0
- mmgpy/lib/libmmg3d.so.5.8.0 +0 -0
- mmgpy/lib/libmmgs.so +0 -0
- mmgpy/lib/libmmgs.so.5 +0 -0
- mmgpy/lib/libmmgs.so.5.8.0 +0 -0
- mmgpy/lib/libvtkCommonColor-9.5.so.1 +0 -0
- mmgpy/lib/libvtkCommonComputationalGeometry-9.5.so.1 +0 -0
- mmgpy/lib/libvtkCommonCore-9.5.so.1 +0 -0
- mmgpy/lib/libvtkCommonDataModel-9.5.so.1 +0 -0
- mmgpy/lib/libvtkCommonExecutionModel-9.5.so.1 +0 -0
- mmgpy/lib/libvtkCommonMath-9.5.so.1 +0 -0
- mmgpy/lib/libvtkCommonMisc-9.5.so.1 +0 -0
- mmgpy/lib/libvtkCommonSystem-9.5.so.1 +0 -0
- mmgpy/lib/libvtkCommonTransforms-9.5.so.1 +0 -0
- mmgpy/lib/libvtkDICOMParser-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersCellGrid-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersCore-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersExtraction-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersGeneral-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersGeometry-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersHybrid-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersHyperTree-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersModeling-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersParallel-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersReduction-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersSources-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersStatistics-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersTexture-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersVerdict-9.5.so.1 +0 -0
- mmgpy/lib/libvtkIOCellGrid-9.5.so.1 +0 -0
- mmgpy/lib/libvtkIOCore-9.5.so.1 +0 -0
- mmgpy/lib/libvtkIOGeometry-9.5.so.1 +0 -0
- mmgpy/lib/libvtkIOImage-9.5.so.1 +0 -0
- mmgpy/lib/libvtkIOLegacy-9.5.so.1 +0 -0
- mmgpy/lib/libvtkIOParallel-9.5.so.1 +0 -0
- mmgpy/lib/libvtkIOParallelXML-9.5.so.1 +0 -0
- mmgpy/lib/libvtkIOXML-9.5.so.1 +0 -0
- mmgpy/lib/libvtkIOXMLParser-9.5.so.1 +0 -0
- mmgpy/lib/libvtkImagingCore-9.5.so.1 +0 -0
- mmgpy/lib/libvtkImagingSources-9.5.so.1 +0 -0
- mmgpy/lib/libvtkParallelCore-9.5.so.1 +0 -0
- mmgpy/lib/libvtkParallelDIY-9.5.so.1 +0 -0
- mmgpy/lib/libvtkRenderingCore-9.5.so.1 +0 -0
- mmgpy/lib/libvtkdoubleconversion-9.5.so.1 +0 -0
- mmgpy/lib/libvtkexpat-9.5.so.1 +0 -0
- mmgpy/lib/libvtkfmt-9.5.so.1 +0 -0
- mmgpy/lib/libvtkjpeg-9.5.so.1 +0 -0
- mmgpy/lib/libvtkjsoncpp-9.5.so.1 +0 -0
- mmgpy/lib/libvtkkissfft-9.5.so.1 +0 -0
- mmgpy/lib/libvtkloguru-9.5.so.1 +0 -0
- mmgpy/lib/libvtklz4-9.5.so.1 +0 -0
- mmgpy/lib/libvtklzma-9.5.so.1 +0 -0
- mmgpy/lib/libvtkmetaio-9.5.so.1 +0 -0
- mmgpy/lib/libvtkpng-9.5.so.1 +0 -0
- mmgpy/lib/libvtkpugixml-9.5.so.1 +0 -0
- mmgpy/lib/libvtksys-9.5.so.1 +0 -0
- mmgpy/lib/libvtktiff-9.5.so.1 +0 -0
- mmgpy/lib/libvtktoken-9.5.so.1 +0 -0
- mmgpy/lib/libvtkverdict-9.5.so.1 +0 -0
- mmgpy/lib/libvtkzlib-9.5.so.1 +0 -0
- mmgpy/metrics.py +596 -0
- mmgpy/progress.py +69 -0
- mmgpy/py.typed +0 -0
- mmgpy/repair/__init__.py +37 -0
- mmgpy/repair/_core.py +226 -0
- mmgpy/repair/_elements.py +241 -0
- mmgpy/repair/_vertices.py +219 -0
- mmgpy/sizing.py +370 -0
- mmgpy/ui/__init__.py +97 -0
- mmgpy/ui/__main__.py +87 -0
- mmgpy/ui/app.py +1837 -0
- mmgpy/ui/parsers.py +501 -0
- mmgpy/ui/remeshing.py +448 -0
- mmgpy/ui/samples.py +249 -0
- mmgpy/ui/utils.py +280 -0
- mmgpy/ui/viewer.py +587 -0
- mmgpy-0.5.0.dist-info/METADATA +186 -0
- mmgpy-0.5.0.dist-info/RECORD +109 -0
- mmgpy-0.5.0.dist-info/WHEEL +6 -0
- mmgpy-0.5.0.dist-info/entry_points.txt +13 -0
- mmgpy-0.5.0.dist-info/licenses/LICENSE +38 -0
- share/man/man1/mmg2d.1.gz +0 -0
- share/man/man1/mmg3d.1.gz +0 -0
- share/man/man1/mmgs.1.gz +0 -0
mmgpy/ui/parsers.py
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
"""Parsers for solution files and safe formula evaluation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import logging
|
|
7
|
+
import operator
|
|
8
|
+
import re
|
|
9
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def parse_sol_file(content: str) -> dict[str, dict]:
|
|
20
|
+
"""Parse a Medit .sol file and return solution fields.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
content : str
|
|
25
|
+
Content of the .sol file.
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
dict[str, dict]
|
|
30
|
+
Dictionary mapping field names to dicts with:
|
|
31
|
+
- "data": numpy array
|
|
32
|
+
- "location": "vertices", "triangles", or "tetrahedra"
|
|
33
|
+
|
|
34
|
+
Examples
|
|
35
|
+
--------
|
|
36
|
+
>>> content = '''
|
|
37
|
+
... MeshVersionFormatted 2
|
|
38
|
+
... Dimension 3
|
|
39
|
+
... SolAtVertices
|
|
40
|
+
... 3
|
|
41
|
+
... 1 1
|
|
42
|
+
... 0.5
|
|
43
|
+
... 0.3
|
|
44
|
+
... 0.1
|
|
45
|
+
... End
|
|
46
|
+
... '''
|
|
47
|
+
>>> fields = parse_sol_file(content)
|
|
48
|
+
>>> "solution@vertices" in fields
|
|
49
|
+
True
|
|
50
|
+
>>> len(fields["solution@vertices"]["data"])
|
|
51
|
+
3
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
lines = content.strip().split("\n")
|
|
55
|
+
fields: dict[str, dict] = {}
|
|
56
|
+
|
|
57
|
+
i = 0
|
|
58
|
+
dimension = 3
|
|
59
|
+
|
|
60
|
+
# Map keyword to location name
|
|
61
|
+
location_map = {
|
|
62
|
+
"SolAtVertices": "vertices",
|
|
63
|
+
"SolAtTriangles": "triangles",
|
|
64
|
+
"SolAtTetrahedra": "tetrahedra",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
while i < len(lines):
|
|
68
|
+
line = lines[i].strip()
|
|
69
|
+
|
|
70
|
+
if line.startswith("Dimension"):
|
|
71
|
+
match = re.search(r"\d+", line)
|
|
72
|
+
if match:
|
|
73
|
+
dimension = int(match.group())
|
|
74
|
+
elif i + 1 < len(lines):
|
|
75
|
+
i += 1
|
|
76
|
+
dimension = int(lines[i].strip())
|
|
77
|
+
i += 1
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
# Check for any SolAt* keyword
|
|
81
|
+
location = None
|
|
82
|
+
for keyword, loc_name in location_map.items():
|
|
83
|
+
if line.startswith(keyword):
|
|
84
|
+
location = loc_name
|
|
85
|
+
break
|
|
86
|
+
|
|
87
|
+
if location is not None:
|
|
88
|
+
i += 1
|
|
89
|
+
if i >= len(lines):
|
|
90
|
+
break
|
|
91
|
+
|
|
92
|
+
n_entities = int(lines[i].strip())
|
|
93
|
+
i += 1
|
|
94
|
+
if i >= len(lines):
|
|
95
|
+
break
|
|
96
|
+
|
|
97
|
+
type_line = lines[i].strip().split()
|
|
98
|
+
n_solutions = int(type_line[0])
|
|
99
|
+
sol_types = [int(t) for t in type_line[1 : 1 + n_solutions]]
|
|
100
|
+
|
|
101
|
+
i += 1
|
|
102
|
+
values: list[list[float]] = []
|
|
103
|
+
while len(values) < n_entities and i < len(lines):
|
|
104
|
+
line = lines[i].strip()
|
|
105
|
+
if line == "End" or line.startswith(("Mesh", "Sol")):
|
|
106
|
+
break
|
|
107
|
+
if line == "":
|
|
108
|
+
i += 1
|
|
109
|
+
continue
|
|
110
|
+
row_values = [float(v) for v in line.split()]
|
|
111
|
+
values.append(row_values)
|
|
112
|
+
i += 1
|
|
113
|
+
|
|
114
|
+
if values:
|
|
115
|
+
data = np.array(values, dtype=np.float64)
|
|
116
|
+
col_idx = 0
|
|
117
|
+
for sol_idx, sol_type in enumerate(sol_types):
|
|
118
|
+
if sol_type == 1:
|
|
119
|
+
base = f"solution_{sol_idx}" if n_solutions > 1 else "solution"
|
|
120
|
+
name = f"{base}@{location}"
|
|
121
|
+
if data.ndim == 1:
|
|
122
|
+
fields[name] = {"data": data, "location": location}
|
|
123
|
+
else:
|
|
124
|
+
fields[name] = {
|
|
125
|
+
"data": data[:, col_idx],
|
|
126
|
+
"location": location,
|
|
127
|
+
}
|
|
128
|
+
col_idx += 1
|
|
129
|
+
elif sol_type == 2:
|
|
130
|
+
base = f"vector_{sol_idx}" if n_solutions > 1 else "vector"
|
|
131
|
+
name = f"{base}@{location}"
|
|
132
|
+
fields[name] = {
|
|
133
|
+
"data": data[:, col_idx : col_idx + dimension],
|
|
134
|
+
"location": location,
|
|
135
|
+
}
|
|
136
|
+
col_idx += dimension
|
|
137
|
+
elif sol_type == 3:
|
|
138
|
+
tensor_size = 6 if dimension == 3 else 3
|
|
139
|
+
base = f"tensor_{sol_idx}" if n_solutions > 1 else "tensor"
|
|
140
|
+
name = f"{base}@{location}"
|
|
141
|
+
fields[name] = {
|
|
142
|
+
"data": data[:, col_idx : col_idx + tensor_size],
|
|
143
|
+
"location": location,
|
|
144
|
+
}
|
|
145
|
+
col_idx += tensor_size
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
i += 1
|
|
149
|
+
|
|
150
|
+
return fields
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class SafeFormulaEvaluator:
|
|
154
|
+
"""Safely evaluate mathematical formulas without using eval().
|
|
155
|
+
|
|
156
|
+
This evaluator parses mathematical expressions using Python's AST module
|
|
157
|
+
and only allows a restricted set of safe operations. It prevents arbitrary
|
|
158
|
+
code execution while supporting common mathematical operations needed for
|
|
159
|
+
levelset formulas.
|
|
160
|
+
|
|
161
|
+
Examples
|
|
162
|
+
--------
|
|
163
|
+
>>> evaluator = SafeFormulaEvaluator()
|
|
164
|
+
>>> x = np.array([0, 1, 2])
|
|
165
|
+
>>> y = np.array([0, 0, 0])
|
|
166
|
+
>>> z = np.array([0, 0, 0])
|
|
167
|
+
>>> result = evaluator.evaluate("x**2 + y**2 + z**2 - 0.25", x, y, z)
|
|
168
|
+
>>> result[0] # 0**2 + 0**2 + 0**2 - 0.25
|
|
169
|
+
-0.25
|
|
170
|
+
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
# Safe binary operators
|
|
174
|
+
SAFE_BINOPS: ClassVar[dict[type, Callable]] = {
|
|
175
|
+
ast.Add: operator.add,
|
|
176
|
+
ast.Sub: operator.sub,
|
|
177
|
+
ast.Mult: operator.mul,
|
|
178
|
+
ast.Div: operator.truediv,
|
|
179
|
+
ast.FloorDiv: operator.floordiv,
|
|
180
|
+
ast.Mod: operator.mod,
|
|
181
|
+
ast.Pow: operator.pow,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# Safe unary operators
|
|
185
|
+
SAFE_UNARYOPS: ClassVar[dict[type, Callable]] = {
|
|
186
|
+
ast.UAdd: operator.pos,
|
|
187
|
+
ast.USub: operator.neg,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# Safe comparison operators
|
|
191
|
+
SAFE_CMPOPS: ClassVar[dict[type, Callable]] = {
|
|
192
|
+
ast.Lt: operator.lt,
|
|
193
|
+
ast.LtE: operator.le,
|
|
194
|
+
ast.Gt: operator.gt,
|
|
195
|
+
ast.GtE: operator.ge,
|
|
196
|
+
ast.Eq: operator.eq,
|
|
197
|
+
ast.NotEq: operator.ne,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# Safe numpy functions that can be called
|
|
201
|
+
SAFE_NP_FUNCS: ClassVar[set[str]] = {
|
|
202
|
+
"sin",
|
|
203
|
+
"cos",
|
|
204
|
+
"tan",
|
|
205
|
+
"arcsin",
|
|
206
|
+
"arccos",
|
|
207
|
+
"arctan",
|
|
208
|
+
"arctan2",
|
|
209
|
+
"sinh",
|
|
210
|
+
"cosh",
|
|
211
|
+
"tanh",
|
|
212
|
+
"exp",
|
|
213
|
+
"log",
|
|
214
|
+
"log10",
|
|
215
|
+
"log2",
|
|
216
|
+
"sqrt",
|
|
217
|
+
"abs",
|
|
218
|
+
"absolute",
|
|
219
|
+
"sign",
|
|
220
|
+
"floor",
|
|
221
|
+
"ceil",
|
|
222
|
+
"round",
|
|
223
|
+
"clip",
|
|
224
|
+
"minimum",
|
|
225
|
+
"maximum",
|
|
226
|
+
"where",
|
|
227
|
+
"pi",
|
|
228
|
+
"e",
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
def __init__(self) -> None:
|
|
232
|
+
"""Initialize the safe formula evaluator."""
|
|
233
|
+
self._variables: dict[str, np.ndarray] = {}
|
|
234
|
+
|
|
235
|
+
def evaluate(
|
|
236
|
+
self,
|
|
237
|
+
formula: str,
|
|
238
|
+
x: np.ndarray,
|
|
239
|
+
y: np.ndarray,
|
|
240
|
+
z: np.ndarray,
|
|
241
|
+
) -> np.ndarray:
|
|
242
|
+
"""Safely evaluate a formula with x, y, z variables.
|
|
243
|
+
|
|
244
|
+
Parameters
|
|
245
|
+
----------
|
|
246
|
+
formula : str
|
|
247
|
+
Mathematical formula using x, y, z variables and numpy functions.
|
|
248
|
+
x : np.ndarray
|
|
249
|
+
X coordinates array.
|
|
250
|
+
y : np.ndarray
|
|
251
|
+
Y coordinates array.
|
|
252
|
+
z : np.ndarray
|
|
253
|
+
Z coordinates array.
|
|
254
|
+
|
|
255
|
+
Returns
|
|
256
|
+
-------
|
|
257
|
+
np.ndarray
|
|
258
|
+
Result of evaluating the formula.
|
|
259
|
+
|
|
260
|
+
Raises
|
|
261
|
+
------
|
|
262
|
+
ValueError
|
|
263
|
+
If the formula contains unsafe operations or syntax errors.
|
|
264
|
+
|
|
265
|
+
"""
|
|
266
|
+
self._variables = {"x": x, "y": y, "z": z}
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
tree = ast.parse(formula, mode="eval")
|
|
270
|
+
except SyntaxError as e:
|
|
271
|
+
msg = f"Invalid formula syntax: {e}"
|
|
272
|
+
raise ValueError(msg) from e
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
result = self._eval_node(tree.body)
|
|
276
|
+
except (TypeError, KeyError, AttributeError) as e:
|
|
277
|
+
msg = f"Error evaluating formula: {e}"
|
|
278
|
+
raise ValueError(msg) from e
|
|
279
|
+
|
|
280
|
+
return np.asarray(result, dtype=np.float64)
|
|
281
|
+
|
|
282
|
+
def _eval_node(self, node: ast.AST) -> np.ndarray | float:
|
|
283
|
+
"""Recursively evaluate an AST node.
|
|
284
|
+
|
|
285
|
+
Parameters
|
|
286
|
+
----------
|
|
287
|
+
node : ast.AST
|
|
288
|
+
The AST node to evaluate.
|
|
289
|
+
|
|
290
|
+
Returns
|
|
291
|
+
-------
|
|
292
|
+
np.ndarray | float
|
|
293
|
+
The result of evaluating the node.
|
|
294
|
+
|
|
295
|
+
Raises
|
|
296
|
+
------
|
|
297
|
+
ValueError
|
|
298
|
+
If the node type is not allowed.
|
|
299
|
+
|
|
300
|
+
"""
|
|
301
|
+
if isinstance(node, ast.Constant):
|
|
302
|
+
# Numbers and constants
|
|
303
|
+
if isinstance(node.value, (int, float)):
|
|
304
|
+
return node.value
|
|
305
|
+
msg = f"Unsupported constant type: {type(node.value)}"
|
|
306
|
+
raise ValueError(msg)
|
|
307
|
+
|
|
308
|
+
if isinstance(node, ast.Name):
|
|
309
|
+
# Variables: x, y, z
|
|
310
|
+
if node.id in self._variables:
|
|
311
|
+
return self._variables[node.id]
|
|
312
|
+
msg = f"Unknown variable: {node.id}. Only x, y, z are allowed."
|
|
313
|
+
raise ValueError(msg)
|
|
314
|
+
|
|
315
|
+
if isinstance(node, ast.BinOp):
|
|
316
|
+
# Binary operations: +, -, *, /, **, etc.
|
|
317
|
+
op_type = type(node.op)
|
|
318
|
+
if op_type not in self.SAFE_BINOPS:
|
|
319
|
+
msg = f"Unsupported binary operator: {op_type.__name__}"
|
|
320
|
+
raise ValueError(msg)
|
|
321
|
+
left = self._eval_node(node.left)
|
|
322
|
+
right = self._eval_node(node.right)
|
|
323
|
+
return self.SAFE_BINOPS[op_type](left, right)
|
|
324
|
+
|
|
325
|
+
if isinstance(node, ast.UnaryOp):
|
|
326
|
+
# Unary operations: +, -
|
|
327
|
+
op_type = type(node.op)
|
|
328
|
+
if op_type not in self.SAFE_UNARYOPS:
|
|
329
|
+
msg = f"Unsupported unary operator: {op_type.__name__}"
|
|
330
|
+
raise ValueError(msg)
|
|
331
|
+
operand = self._eval_node(node.operand)
|
|
332
|
+
return self.SAFE_UNARYOPS[op_type](operand)
|
|
333
|
+
|
|
334
|
+
if isinstance(node, ast.Compare):
|
|
335
|
+
# Comparison operations
|
|
336
|
+
if len(node.ops) != 1 or len(node.comparators) != 1:
|
|
337
|
+
msg = "Only simple comparisons are supported"
|
|
338
|
+
raise ValueError(msg)
|
|
339
|
+
op_type = type(node.ops[0])
|
|
340
|
+
if op_type not in self.SAFE_CMPOPS:
|
|
341
|
+
msg = f"Unsupported comparison operator: {op_type.__name__}"
|
|
342
|
+
raise ValueError(msg)
|
|
343
|
+
left = self._eval_node(node.left)
|
|
344
|
+
right = self._eval_node(node.comparators[0])
|
|
345
|
+
return self.SAFE_CMPOPS[op_type](left, right)
|
|
346
|
+
|
|
347
|
+
if isinstance(node, ast.Call):
|
|
348
|
+
return self._eval_call(node)
|
|
349
|
+
|
|
350
|
+
if isinstance(node, ast.Attribute):
|
|
351
|
+
return self._eval_attribute(node)
|
|
352
|
+
|
|
353
|
+
if isinstance(node, ast.IfExp):
|
|
354
|
+
# Ternary: a if condition else b -> np.where(condition, a, b)
|
|
355
|
+
test = self._eval_node(node.test)
|
|
356
|
+
body = self._eval_node(node.body)
|
|
357
|
+
orelse = self._eval_node(node.orelse)
|
|
358
|
+
return np.where(test, body, orelse)
|
|
359
|
+
|
|
360
|
+
msg = f"Unsupported expression type: {type(node).__name__}"
|
|
361
|
+
raise ValueError(msg)
|
|
362
|
+
|
|
363
|
+
def _eval_call(self, node: ast.Call) -> np.ndarray | float:
|
|
364
|
+
"""Evaluate a function call node.
|
|
365
|
+
|
|
366
|
+
Parameters
|
|
367
|
+
----------
|
|
368
|
+
node : ast.Call
|
|
369
|
+
The function call AST node.
|
|
370
|
+
|
|
371
|
+
Returns
|
|
372
|
+
-------
|
|
373
|
+
np.ndarray | float
|
|
374
|
+
The result of the function call.
|
|
375
|
+
|
|
376
|
+
Raises
|
|
377
|
+
------
|
|
378
|
+
ValueError
|
|
379
|
+
If the function is not allowed.
|
|
380
|
+
|
|
381
|
+
"""
|
|
382
|
+
# Handle np.function() calls
|
|
383
|
+
if isinstance(node.func, ast.Attribute):
|
|
384
|
+
if isinstance(node.func.value, ast.Name) and node.func.value.id == "np":
|
|
385
|
+
func_name = node.func.attr
|
|
386
|
+
if func_name not in self.SAFE_NP_FUNCS:
|
|
387
|
+
msg = f"Unsupported numpy function: np.{func_name}"
|
|
388
|
+
raise ValueError(msg)
|
|
389
|
+
func = getattr(np, func_name)
|
|
390
|
+
args = [self._eval_node(arg) for arg in node.args]
|
|
391
|
+
return func(*args)
|
|
392
|
+
msg = "Only np.function() calls are allowed"
|
|
393
|
+
raise ValueError(msg)
|
|
394
|
+
|
|
395
|
+
# Handle direct function calls like abs()
|
|
396
|
+
if isinstance(node.func, ast.Name):
|
|
397
|
+
func_name = node.func.id
|
|
398
|
+
# Map Python builtins to numpy equivalents
|
|
399
|
+
builtin_map = {
|
|
400
|
+
"abs": np.abs,
|
|
401
|
+
"min": np.minimum,
|
|
402
|
+
"max": np.maximum,
|
|
403
|
+
}
|
|
404
|
+
if func_name in builtin_map:
|
|
405
|
+
args = [self._eval_node(arg) for arg in node.args]
|
|
406
|
+
return builtin_map[func_name](*args)
|
|
407
|
+
if func_name in self.SAFE_NP_FUNCS:
|
|
408
|
+
func = getattr(np, func_name)
|
|
409
|
+
args = [self._eval_node(arg) for arg in node.args]
|
|
410
|
+
return func(*args)
|
|
411
|
+
msg = f"Unsupported function: {func_name}"
|
|
412
|
+
raise ValueError(msg)
|
|
413
|
+
|
|
414
|
+
msg = "Invalid function call"
|
|
415
|
+
raise ValueError(msg)
|
|
416
|
+
|
|
417
|
+
def _eval_attribute(self, node: ast.Attribute) -> float:
|
|
418
|
+
"""Evaluate an attribute access node.
|
|
419
|
+
|
|
420
|
+
Parameters
|
|
421
|
+
----------
|
|
422
|
+
node : ast.Attribute
|
|
423
|
+
The attribute access AST node.
|
|
424
|
+
|
|
425
|
+
Returns
|
|
426
|
+
-------
|
|
427
|
+
float
|
|
428
|
+
The attribute value.
|
|
429
|
+
|
|
430
|
+
Raises
|
|
431
|
+
------
|
|
432
|
+
ValueError
|
|
433
|
+
If the attribute is not allowed.
|
|
434
|
+
|
|
435
|
+
"""
|
|
436
|
+
# Handle np.pi, np.e
|
|
437
|
+
if isinstance(node.value, ast.Name) and node.value.id == "np":
|
|
438
|
+
attr_name = node.attr
|
|
439
|
+
if attr_name in {"pi", "e"}:
|
|
440
|
+
return getattr(np, attr_name)
|
|
441
|
+
msg = f"Unsupported numpy attribute: np.{attr_name}"
|
|
442
|
+
raise ValueError(msg)
|
|
443
|
+
msg = "Only np.pi and np.e attributes are allowed"
|
|
444
|
+
raise ValueError(msg)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
# Module-level instance for convenience
|
|
448
|
+
_evaluator = SafeFormulaEvaluator()
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def evaluate_levelset_formula(
|
|
452
|
+
formula: str,
|
|
453
|
+
x: np.ndarray,
|
|
454
|
+
y: np.ndarray,
|
|
455
|
+
z: np.ndarray,
|
|
456
|
+
) -> np.ndarray:
|
|
457
|
+
"""Safely evaluate a levelset formula.
|
|
458
|
+
|
|
459
|
+
This is a convenience function that uses the SafeFormulaEvaluator
|
|
460
|
+
to safely evaluate mathematical formulas without using eval().
|
|
461
|
+
|
|
462
|
+
Parameters
|
|
463
|
+
----------
|
|
464
|
+
formula : str
|
|
465
|
+
Mathematical formula using x, y, z variables.
|
|
466
|
+
Supported operations:
|
|
467
|
+
- Arithmetic: +, -, *, /, **, //, %
|
|
468
|
+
- Comparisons: <, <=, >, >=, ==, !=
|
|
469
|
+
- Numpy functions: np.sin, np.cos, np.sqrt, np.exp, np.log, etc.
|
|
470
|
+
- Constants: np.pi, np.e
|
|
471
|
+
- Ternary expressions: a if condition else b
|
|
472
|
+
|
|
473
|
+
x : np.ndarray
|
|
474
|
+
X coordinates array.
|
|
475
|
+
y : np.ndarray
|
|
476
|
+
Y coordinates array.
|
|
477
|
+
z : np.ndarray
|
|
478
|
+
Z coordinates array.
|
|
479
|
+
|
|
480
|
+
Returns
|
|
481
|
+
-------
|
|
482
|
+
np.ndarray
|
|
483
|
+
Result of evaluating the formula, shaped as (-1, 1).
|
|
484
|
+
|
|
485
|
+
Raises
|
|
486
|
+
------
|
|
487
|
+
ValueError
|
|
488
|
+
If the formula contains unsafe operations or syntax errors.
|
|
489
|
+
|
|
490
|
+
Examples
|
|
491
|
+
--------
|
|
492
|
+
>>> x = np.array([0, 1, 0])
|
|
493
|
+
>>> y = np.array([0, 0, 1])
|
|
494
|
+
>>> z = np.array([0, 0, 0])
|
|
495
|
+
>>> result = evaluate_levelset_formula("x**2 + y**2 + z**2 - 0.25", x, y, z)
|
|
496
|
+
>>> result.shape
|
|
497
|
+
(3, 1)
|
|
498
|
+
|
|
499
|
+
"""
|
|
500
|
+
result = _evaluator.evaluate(formula, x, y, z)
|
|
501
|
+
return result.reshape(-1, 1)
|