v0.15.4
Loading...
Searching...
No Matches
process_sdfs.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""
3Small helper script for working with SDF functions.
4
5It:
6- parses two arguments: -sdf_filename and -log_filename
7- checks that both paths exist
8- dynamically loads the given SDF Python file
9- verifies that it defines a function:
10
11 def sdf(delta_t, t, x, y, z, tx, ty, tz, block_id)
12"""
13
14import argparse
15import inspect
16import importlib.util
17import sys
18import re
19from pathlib import Path
20from multiprocessing import Pool, cpu_count
21
22import numpy as np
23import pyvista as pv
24
25ISO_VALUE = 0.0
26EXPECTED_SDF_ARGS = ["delta_t", "t", "x", "y", "z", "tx", "ty", "tz", "block_id"]
27
28
30 try:
31 return pv.UniformGrid()
32 except AttributeError:
33 # Fallback for older / minimal PyVista builds
34 from vtkmodules.vtkCommonDataModel import vtkImageData
35 return pv.wrap(vtkImageData())
36
37def make_grid(bounds, n):
38 x_min, x_max, y_min, y_max, z_min, z_max = bounds
39
40 x = np.linspace(x_min, x_max, n, dtype=float)
41 y = np.linspace(y_min, y_max, n, dtype=float)
42 z = np.linspace(z_min, z_max, n, dtype=float)
43
44 X, Y, Z = np.meshgrid(x, y, z, indexing="ij")
45
46 grid = create_uniform_grid()
47 grid.dimensions = (n, n, n)
48 grid.origin = (x[0], y[0], z[0])
49 grid.spacing = (x[1] - x[0], y[1] - y[0], z[1] - z[0])
50
51 return grid, X, Y, Z
52
54 sdf_func,
55 delta_t,
56 t,
57 block_id,
58 grid_template,
59 X,
60 Y,
61 Z,
62 tx=0.0,
63 ty=0.0,
64 tz=0.0,
65):
66 grid = grid_template.copy()
67
68 sdf_vals = sdf_func(delta_t, t, X, Y, Z, tx, ty, tz, block_id)
69 sdf_vals = np.asarray(sdf_vals, dtype=float)
70
71 # Accept either 3D or flat array
72 if sdf_vals.shape != X.shape:
73 if sdf_vals.size == X.size:
74 # reshape to 3D; change order="C"/"F" if your sdf uses different flattening
75 sdf_vals = sdf_vals.reshape(X.shape)
76 else:
77 raise RuntimeError(
78 f"sdf() returned array with shape {sdf_vals.shape}, "
79 f"but expected {X.shape} or flat of size {X.size}"
80 )
81
82 grid["SDF"] = sdf_vals.ravel(order="F")
83
84 try:
85 surface = grid.contour([ISO_VALUE], scalars="SDF")
86 except Exception:
87 return None
88
89 if surface.n_points == 0:
90 return None
91
92 return surface
93
94def chunk_indices(n, n_chunks):
95 """Yield index lists splitting range(n) into n_chunks chunks."""
96 if n_chunks <= 0:
97 n_chunks = 1
98 chunk_size = (n + n_chunks - 1) // n_chunks
99 for i in range(n_chunks):
100 start = i * chunk_size
101 end = min(start + chunk_size, n)
102 if start < end:
103 yield list(range(start, end))
104
106 sdf_path,
107 block_ids,
108 load_steps,
109 total_times,
110 delta_times,
111 output_prefix,
112 surface_only,
113 grid_size,
114 resolution,
115 n_procs,
116):
117 n_steps = len(load_steps)
118 if n_steps == 0:
119 print("No time steps to process.")
120 return
121
122 # n_procs = max(1, min(n_procs, cpu_count()))
123 print(f"Using {n_procs} worker process(es) for {n_steps} time steps.")
124
125 bounds = (-grid_size, grid_size, -grid_size, grid_size, -grid_size, grid_size)
126 sdf_path_str = str(sdf_path)
127
128 tasks = []
129 for indices in chunk_indices(n_steps, n_procs):
130 task_args = (
131 sdf_path_str,
132 bounds,
133 resolution,
134 surface_only,
135 output_prefix,
136 block_ids,
137 load_steps,
138 total_times,
139 delta_times,
140 indices,
141 )
142 tasks.append(task_args)
143
144 if n_procs == 1:
145 # No Pool overhead, just run tasks in this process
146 for targs in tasks:
148 else:
149 with Pool(processes=n_procs) as pool:
150 pool.map(_process_time_steps_chunk, tasks)
151
153 """
154 Worker function for multiprocessing.
155
156 args = (
157 sdf_path_str,
158 bounds,
159 resolution,
160 surface_only,
161 output_prefix,
162 block_ids,
163 load_steps,
164 total_times,
165 delta_times,
166 indices,
167 )
168 """
169 (
170 sdf_path_str,
171 bounds,
172 resolution,
173 surface_only,
174 output_prefix,
175 block_ids,
176 load_steps,
177 total_times,
178 delta_times,
179 indices,
180 ) = args
181
182 sdf_func = load_sdf_function(Path(sdf_path_str))
183
184 # Each worker builds its own grid and coordinate arrays
185 grid, X, Y, Z = make_grid(bounds=bounds, n=resolution)
186
187 combined_sdf = None
188
189 for idx in indices:
190 ls = load_steps[idx]
191 t = total_times[idx]
192 dt = delta_times[idx]
193
194 first_block = True
195
196 for block_id in block_ids:
197 vals = sdf_func(dt, t, X, Y, Z, 0.0, 0.0, 0.0, block_id)
198 vals = np.asarray(vals) # keep native dtype
199
200 # Accept flat or 3D
201 if vals.shape != X.shape:
202 if vals.size == X.size:
203 vals = vals.reshape(X.shape)
204 else:
205 raise RuntimeError(
206 f"sdf() returned shape {vals.shape}, "
207 f"expected {X.shape} or flat size {X.size}"
208 )
209
210 if combined_sdf is None:
211 combined_sdf = np.empty_like(vals)
212
213 if first_block:
214 combined_sdf[...] = vals
215 first_block = False
216 else:
217 np.minimum(combined_sdf, vals, out=combined_sdf)
218
219 if combined_sdf is None or first_block:
220 print(f"[step {ls}] no SDF values, skipping")
221 continue
222
223 # SURFACE-ONLY MODE (STL)
224 if surface_only:
225 grid["SDF"] = combined_sdf.ravel(order="F")
226 try:
227 surface = grid.contour([ISO_VALUE], scalars="SDF")
228 except Exception:
229 print(f"[step {ls}] contour generation failed, skipping")
230 continue
231
232 if surface.n_points == 0:
233 print(f"[step {ls}] empty surface, skipping")
234 continue
235
236 filename = f"{output_prefix}{ls}.stl"
237 surface.save(filename)
238 print(f"[step {ls}] saved STL surface to {filename}")
239
240 # FULL VOLUME MODE (VTI)
241 else:
242 grid["SDF"] = combined_sdf.ravel(order="F")
243 filename = f"{output_prefix}{ls}.vti"
244 grid.save(filename)
245 print(f"[step {ls}] saved VTI volume to {filename}")
246
247def str2bool(v):
248 if isinstance(v, bool):
249 return v
250 if v.lower() in ("yes", "true", "t", "1"):
251 return True
252 if v.lower() in ("no", "false", "f", "0"):
253 return False
254 raise argparse.ArgumentTypeError("Boolean value expected.")
255
256def parse_args() -> argparse.Namespace:
257 parser = argparse.ArgumentParser(
258 description="Load an SDF definition and a log file."
259 )
260 parser.add_argument(
261 "-sdf_filename",
262 required=True,
263 type=Path,
264 help="Path to Python file defining sdf(delta_t, t, x, y, z, tx, ty, tz, block_id).",
265 )
266 parser.add_argument(
267 "-log_filename",
268 required=True,
269 type=Path,
270 help="Path to log text file.",
271 )
272 parser.add_argument(
273 "-surface_only",
274 type=str2bool,
275 default=True,
276 help="Generate only STL surfaces (fast). "
277 "If not set, full VTI volumes are generated.",
278 )
279 parser.add_argument(
280 "-grid_size",
281 type=float,
282 default=50.0,
283 help="Half-size of the domain in each direction. "
284 "Domain will be (-grid_size, grid_size)³. Default = 50.",
285 )
286 parser.add_argument(
287 "-resolution",
288 type=int,
289 default=100,
290 help="Grid resolution in each axis. Default = 100.",
291 )
292 parser.add_argument(
293 "-np",
294 type=int,
295 default=4,
296 help="Number of worker processes (cores) to use. Default = 4 (no multiprocessing).",
297 )
298
299 return parser.parse_args()
300
301
302def check_paths(sdf_path: Path, log_path: Path) -> None:
303 if not sdf_path.is_file():
304 raise FileNotFoundError(f"SDF file does not exist or is not a file: {sdf_path}")
305
306 if not log_path.is_file():
307 raise FileNotFoundError(f"Log file does not exist or is not a file: {log_path}")
308
309
310def extract_time_data(log_path: Path):
311 # Pattern A (ep)
312 pat_loadstep = re.compile(
313 r"Load step\s+(\d+)\s+Time\s+([0-9.+-eE]+)\s+delta time\s+([0-9.+-eE]+)"
314 )
315
316 load_steps = []
317 total_times = []
318 delta_times = []
319
320 with log_path.open("r", encoding="utf-8") as f:
321 for line in f:
322 line = re.sub(r"\x1b\[[0-9;]*m", "", line) # <-- ignore coloring artifacts
323 m = pat_loadstep.search(line)
324 if not m:
325 continue
326 load_steps.append(int(m.group(1)))
327 total_times.append(float(m.group(2)))
328 delta_times.append(float(m.group(3)))
329
330 if load_steps:
331 print(f"Extracted {len(load_steps)} time entries from 'Load step' lines.")
332 return load_steps, total_times, delta_times
333
334 # Fallback Pattern B (PETSc TS)
335 pat_ts = re.compile(
336 r"\[petsc\]\s+(\d+)\s+TS\s+dt\s+([0-9.+-eE]+)\s+time\s+([0-9.+-eE]+)",
337 re.IGNORECASE,
338 )
339
340 load_steps = []
341 total_times = []
342 delta_times = []
343
344 with log_path.open("r", encoding="utf-8") as f:
345 for line in f:
346 line = re.sub(r"\x1b\[[0-9;]*m", "", line) # <-- ignore coloring artifacts
347 m = pat_ts.search(line)
348 if not m:
349 continue
350 load_steps.append(int(m.group(1))) # TS step index
351 delta_times.append(float(m.group(2))) # dt
352 total_times.append(float(m.group(3))) # time
353
354 if not load_steps:
355 raise RuntimeError(
356 "No time-step data found. Tried patterns:\n"
357 " - 'Load step <n> Time <t> delta time <dt>'\n"
358 " - '[petsc] <n> TS dt <dt> time <t>'"
359 )
360
361 print(f"Extracted {len(load_steps)} time entries from PETSc TS lines.")
362 return load_steps, total_times, delta_times
363
364def extract_sdf_block_ids(log_path: Path):
365 """
366 Extract msId values for blocks whose name contains 'SDF',
367 but stop scanning once the log reaches the line:
368 "Initialise MoFEM database <- done"
369 """
370
371 stop_phrase = "Initialise MoFEM database <- done"
372
373 pattern = re.compile(
374 r"type BLOCKSET.*?msId\s+(\d+).*?name\s+([A-Za-z0-9_]+)",
375 re.IGNORECASE,
376 )
377
378 ids = []
379
380 with log_path.open("r", encoding="utf-8") as f:
381 for line in f:
382 if stop_phrase in line:
383 break # stop scanning entirely
384
385 m = pattern.search(line)
386 if not m:
387 continue
388
389 ms_id = int(m.group(1))
390 name = m.group(2)
391
392 if "SDF" in name.upper():
393 ids.append(ms_id)
394 continue
395 if "CONTACT" in name.upper():
396 ids.append(ms_id)
397
398 if not ids:
399 raise RuntimeError("No SDF block IDs found in the log file.")
400 print(f"Extracted SDF block IDs: {ids}")
401
402 return ids
403
404def load_sdf_function(sdf_path: Path):
405
406 module_name = "sdf_module_from_file"
407
408 spec = importlib.util.spec_from_file_location(module_name, sdf_path)
409 if spec is None or spec.loader is None:
410 raise RuntimeError(f"Cannot create import spec for {sdf_path}")
411
412 module = importlib.util.module_from_spec(spec)
413 sys.modules[module_name] = module # optional, but can help with imports inside the SDF file
414 spec.loader.exec_module(module) # type: ignore[union-attr]
415
416 if not hasattr(module, "sdf"):
417 raise RuntimeError(f"The SDF file {sdf_path} does not define a function named 'sdf'.")
418
419 sdf_func = getattr(module, "sdf")
420
421 if not callable(sdf_func):
422 raise RuntimeError(f"'sdf' in {sdf_path} is not callable.")
423
424 # Check the function signature
425 sig = inspect.signature(sdf_func)
426 param_names = list(sig.parameters.keys())
427
428 if param_names != EXPECTED_SDF_ARGS:
429 raise RuntimeError(
430 f"sdf() in {sdf_path} has wrong parameters.\n"
431 f"Expected: ({', '.join(EXPECTED_SDF_ARGS)})\n"
432 f"Got: ({', '.join(param_names)})"
433 )
434
435 return sdf_func
436
437
438def main():
439 args = parse_args()
440
441 try:
442 check_paths(args.sdf_filename, args.log_filename)
443 except FileNotFoundError as exc:
444 print(f"Error: {exc}", file=sys.stderr)
445 sys.exit(1)
446
447 try:
448 sdf = load_sdf_function(args.sdf_filename)
449 except RuntimeError as exc:
450 print(f"Error: {exc}", file=sys.stderr)
451 sys.exit(1)
452
453 print(f"SDF function loaded from: {args.sdf_filename}")
454 print(f"Log file path: {args.log_filename}")
455 print(f"SDF signature: {inspect.signature(sdf)}")
456 print(f"Grid bounds: {(-args.grid_size, args.grid_size)*3}")
457 print(f"Grid resolution: {args.resolution} points per axis")
458 print(f"Surface only mode: {args.surface_only}")
459
460 load_steps, total_times, delta_times = extract_time_data(args.log_filename)
461 block_ids = extract_sdf_block_ids(args.log_filename)
462
463 print("All checks passed.")
464 print("Generating SDF meshes for all time steps...")
465
467 args.sdf_filename,
468 block_ids,
469 load_steps,
470 total_times,
471 delta_times,
472 "out_sdf_",
473 args.surface_only,
474 args.grid_size,
475 args.resolution,
476 args.np,
477 )
478 print("Done.")
479
480if __name__ == "__main__":
481 main()
generate_sdf_meshes_parallel(sdf_path, block_ids, load_steps, total_times, delta_times, output_prefix, surface_only, grid_size, resolution, n_procs)
chunk_indices(n, n_chunks)
load_sdf_function(Path sdf_path)
argparse.Namespace parse_args()
_process_time_steps_chunk(args)
extract_sdf_block_ids(Path log_path)
extract_time_data(Path log_path)
sdf_block_to_surface(sdf_func, delta_t, t, block_id, grid_template, X, Y, Z, tx=0.0, ty=0.0, tz=0.0)
make_grid(bounds, n)
None check_paths(Path sdf_path, Path log_path)