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.
Files changed (109) hide show
  1. mmgpy/__init__.py +296 -0
  2. mmgpy/__main__.py +13 -0
  3. mmgpy/_io.py +535 -0
  4. mmgpy/_logging.py +290 -0
  5. mmgpy/_mesh.py +2286 -0
  6. mmgpy/_mmgpy.cpython-311-x86_64-linux-gnu.so +0 -0
  7. mmgpy/_mmgpy.pyi +2140 -0
  8. mmgpy/_options.py +304 -0
  9. mmgpy/_progress.py +850 -0
  10. mmgpy/_pyvista.py +410 -0
  11. mmgpy/_result.py +143 -0
  12. mmgpy/_transfer.py +273 -0
  13. mmgpy/_validation.py +669 -0
  14. mmgpy/_version.py +3 -0
  15. mmgpy/_version.py.in +3 -0
  16. mmgpy/bin/mmg2d_O3 +0 -0
  17. mmgpy/bin/mmg3d_O3 +0 -0
  18. mmgpy/bin/mmgs_O3 +0 -0
  19. mmgpy/interactive/__init__.py +24 -0
  20. mmgpy/interactive/sizing_editor.py +790 -0
  21. mmgpy/lagrangian.py +394 -0
  22. mmgpy/lib/libmmg2d.so +0 -0
  23. mmgpy/lib/libmmg2d.so.5 +0 -0
  24. mmgpy/lib/libmmg2d.so.5.8.0 +0 -0
  25. mmgpy/lib/libmmg3d.so +0 -0
  26. mmgpy/lib/libmmg3d.so.5 +0 -0
  27. mmgpy/lib/libmmg3d.so.5.8.0 +0 -0
  28. mmgpy/lib/libmmgs.so +0 -0
  29. mmgpy/lib/libmmgs.so.5 +0 -0
  30. mmgpy/lib/libmmgs.so.5.8.0 +0 -0
  31. mmgpy/lib/libvtkCommonColor-9.5.so.1 +0 -0
  32. mmgpy/lib/libvtkCommonComputationalGeometry-9.5.so.1 +0 -0
  33. mmgpy/lib/libvtkCommonCore-9.5.so.1 +0 -0
  34. mmgpy/lib/libvtkCommonDataModel-9.5.so.1 +0 -0
  35. mmgpy/lib/libvtkCommonExecutionModel-9.5.so.1 +0 -0
  36. mmgpy/lib/libvtkCommonMath-9.5.so.1 +0 -0
  37. mmgpy/lib/libvtkCommonMisc-9.5.so.1 +0 -0
  38. mmgpy/lib/libvtkCommonSystem-9.5.so.1 +0 -0
  39. mmgpy/lib/libvtkCommonTransforms-9.5.so.1 +0 -0
  40. mmgpy/lib/libvtkDICOMParser-9.5.so.1 +0 -0
  41. mmgpy/lib/libvtkFiltersCellGrid-9.5.so.1 +0 -0
  42. mmgpy/lib/libvtkFiltersCore-9.5.so.1 +0 -0
  43. mmgpy/lib/libvtkFiltersExtraction-9.5.so.1 +0 -0
  44. mmgpy/lib/libvtkFiltersGeneral-9.5.so.1 +0 -0
  45. mmgpy/lib/libvtkFiltersGeometry-9.5.so.1 +0 -0
  46. mmgpy/lib/libvtkFiltersHybrid-9.5.so.1 +0 -0
  47. mmgpy/lib/libvtkFiltersHyperTree-9.5.so.1 +0 -0
  48. mmgpy/lib/libvtkFiltersModeling-9.5.so.1 +0 -0
  49. mmgpy/lib/libvtkFiltersParallel-9.5.so.1 +0 -0
  50. mmgpy/lib/libvtkFiltersReduction-9.5.so.1 +0 -0
  51. mmgpy/lib/libvtkFiltersSources-9.5.so.1 +0 -0
  52. mmgpy/lib/libvtkFiltersStatistics-9.5.so.1 +0 -0
  53. mmgpy/lib/libvtkFiltersTexture-9.5.so.1 +0 -0
  54. mmgpy/lib/libvtkFiltersVerdict-9.5.so.1 +0 -0
  55. mmgpy/lib/libvtkIOCellGrid-9.5.so.1 +0 -0
  56. mmgpy/lib/libvtkIOCore-9.5.so.1 +0 -0
  57. mmgpy/lib/libvtkIOGeometry-9.5.so.1 +0 -0
  58. mmgpy/lib/libvtkIOImage-9.5.so.1 +0 -0
  59. mmgpy/lib/libvtkIOLegacy-9.5.so.1 +0 -0
  60. mmgpy/lib/libvtkIOParallel-9.5.so.1 +0 -0
  61. mmgpy/lib/libvtkIOParallelXML-9.5.so.1 +0 -0
  62. mmgpy/lib/libvtkIOXML-9.5.so.1 +0 -0
  63. mmgpy/lib/libvtkIOXMLParser-9.5.so.1 +0 -0
  64. mmgpy/lib/libvtkImagingCore-9.5.so.1 +0 -0
  65. mmgpy/lib/libvtkImagingSources-9.5.so.1 +0 -0
  66. mmgpy/lib/libvtkParallelCore-9.5.so.1 +0 -0
  67. mmgpy/lib/libvtkParallelDIY-9.5.so.1 +0 -0
  68. mmgpy/lib/libvtkRenderingCore-9.5.so.1 +0 -0
  69. mmgpy/lib/libvtkdoubleconversion-9.5.so.1 +0 -0
  70. mmgpy/lib/libvtkexpat-9.5.so.1 +0 -0
  71. mmgpy/lib/libvtkfmt-9.5.so.1 +0 -0
  72. mmgpy/lib/libvtkjpeg-9.5.so.1 +0 -0
  73. mmgpy/lib/libvtkjsoncpp-9.5.so.1 +0 -0
  74. mmgpy/lib/libvtkkissfft-9.5.so.1 +0 -0
  75. mmgpy/lib/libvtkloguru-9.5.so.1 +0 -0
  76. mmgpy/lib/libvtklz4-9.5.so.1 +0 -0
  77. mmgpy/lib/libvtklzma-9.5.so.1 +0 -0
  78. mmgpy/lib/libvtkmetaio-9.5.so.1 +0 -0
  79. mmgpy/lib/libvtkpng-9.5.so.1 +0 -0
  80. mmgpy/lib/libvtkpugixml-9.5.so.1 +0 -0
  81. mmgpy/lib/libvtksys-9.5.so.1 +0 -0
  82. mmgpy/lib/libvtktiff-9.5.so.1 +0 -0
  83. mmgpy/lib/libvtktoken-9.5.so.1 +0 -0
  84. mmgpy/lib/libvtkverdict-9.5.so.1 +0 -0
  85. mmgpy/lib/libvtkzlib-9.5.so.1 +0 -0
  86. mmgpy/metrics.py +596 -0
  87. mmgpy/progress.py +69 -0
  88. mmgpy/py.typed +0 -0
  89. mmgpy/repair/__init__.py +37 -0
  90. mmgpy/repair/_core.py +226 -0
  91. mmgpy/repair/_elements.py +241 -0
  92. mmgpy/repair/_vertices.py +219 -0
  93. mmgpy/sizing.py +370 -0
  94. mmgpy/ui/__init__.py +97 -0
  95. mmgpy/ui/__main__.py +87 -0
  96. mmgpy/ui/app.py +1837 -0
  97. mmgpy/ui/parsers.py +501 -0
  98. mmgpy/ui/remeshing.py +448 -0
  99. mmgpy/ui/samples.py +249 -0
  100. mmgpy/ui/utils.py +280 -0
  101. mmgpy/ui/viewer.py +587 -0
  102. mmgpy-0.5.0.dist-info/METADATA +186 -0
  103. mmgpy-0.5.0.dist-info/RECORD +109 -0
  104. mmgpy-0.5.0.dist-info/WHEEL +6 -0
  105. mmgpy-0.5.0.dist-info/entry_points.txt +13 -0
  106. mmgpy-0.5.0.dist-info/licenses/LICENSE +38 -0
  107. share/man/man1/mmg2d.1.gz +0 -0
  108. share/man/man1/mmg3d.1.gz +0 -0
  109. 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)