diffusion-cartogram 0.2.0__py3-none-any.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.
- diffusion_cartogram/__init__.py +202 -0
- diffusion_cartogram/core.py +1329 -0
- diffusion_cartogram/core_2d.py +831 -0
- diffusion_cartogram/visualization.py +1490 -0
- diffusion_cartogram/visualization_2d.py +534 -0
- diffusion_cartogram-0.2.0.dist-info/METADATA +287 -0
- diffusion_cartogram-0.2.0.dist-info/RECORD +10 -0
- diffusion_cartogram-0.2.0.dist-info/WHEEL +5 -0
- diffusion_cartogram-0.2.0.dist-info/licenses/LICENSE +21 -0
- diffusion_cartogram-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1490 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Visualization and animation tools for VDERM exports.
|
|
3
|
+
|
|
4
|
+
This module provides functions to create animations and visualizations from
|
|
5
|
+
exported VDERM data (grids, surfaces, and meshes).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import matplotlib.pyplot as plt
|
|
10
|
+
from matplotlib.animation import FuncAnimation, PillowWriter
|
|
11
|
+
from mpl_toolkits.mplot3d import Axes3D
|
|
12
|
+
import glob
|
|
13
|
+
import os
|
|
14
|
+
from tqdm import tqdm
|
|
15
|
+
import warnings
|
|
16
|
+
|
|
17
|
+
from .core import read_xyz, HAS_PYMESHLAB
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def animate_grid_deformation(export_folder='vderm_exports',
|
|
21
|
+
subfolder='vderm_grid',
|
|
22
|
+
output_file='grid_animation.gif',
|
|
23
|
+
fps=5,
|
|
24
|
+
subsample=5000,
|
|
25
|
+
cmap='plasma',
|
|
26
|
+
figsize=(15, 5),
|
|
27
|
+
alpha=0.3):
|
|
28
|
+
"""
|
|
29
|
+
Create animated GIF of grid deformation showing three orthogonal 2D projections.
|
|
30
|
+
|
|
31
|
+
Creates three side-by-side 2D plots showing XY, XZ, and YZ projections of the
|
|
32
|
+
grid with density coloring. Points are overlaid with transparency to show
|
|
33
|
+
the full 3D structure.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
export_folder : str, default='vderm_exports'
|
|
38
|
+
Base export folder
|
|
39
|
+
subfolder : str, default='vderm_grid'
|
|
40
|
+
Subfolder containing grid exports
|
|
41
|
+
output_file : str, default='grid_animation.gif'
|
|
42
|
+
Output animation filename (.gif or .mp4)
|
|
43
|
+
fps : int, default=5
|
|
44
|
+
Frames per second
|
|
45
|
+
subsample : int or None, default=5000
|
|
46
|
+
Subsample to this many points for faster rendering.
|
|
47
|
+
If None, use all points (may be slow for large grids).
|
|
48
|
+
cmap : str, default='plasma'
|
|
49
|
+
Matplotlib colormap name for density visualization
|
|
50
|
+
figsize : tuple, default=(15, 5)
|
|
51
|
+
Figure size in inches (width, height)
|
|
52
|
+
alpha : float, default=0.3
|
|
53
|
+
Opacity for overlaid points (0=transparent, 1=opaque)
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
None
|
|
58
|
+
Saves animation to output_file
|
|
59
|
+
|
|
60
|
+
Examples
|
|
61
|
+
--------
|
|
62
|
+
>>> # Basic usage
|
|
63
|
+
>>> animate_grid_deformation('my_exports', output_file='deform.gif')
|
|
64
|
+
|
|
65
|
+
>>> # More opaque points
|
|
66
|
+
>>> animate_grid_deformation('my_exports', alpha=0.5)
|
|
67
|
+
|
|
68
|
+
>>> # Higher frame rate
|
|
69
|
+
>>> animate_grid_deformation('my_exports', fps=10, subsample=3000)
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
# Find all grid files
|
|
73
|
+
pattern = os.path.join(export_folder, subfolder, 'grid_iteration_*.xyz')
|
|
74
|
+
files = sorted(glob.glob(pattern))
|
|
75
|
+
|
|
76
|
+
# Also check for final file
|
|
77
|
+
final_pattern = os.path.join(export_folder, subfolder, 'grid_final_*.xyz')
|
|
78
|
+
final_files = glob.glob(final_pattern)
|
|
79
|
+
if final_files:
|
|
80
|
+
files.append(final_files[0])
|
|
81
|
+
|
|
82
|
+
if len(files) == 0:
|
|
83
|
+
raise FileNotFoundError(
|
|
84
|
+
f"No grid files found matching {pattern}\n"
|
|
85
|
+
f"Make sure you ran run_VDERM_with_tracking with export_grid=True"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
print(f"Found {len(files)} frames to animate")
|
|
89
|
+
|
|
90
|
+
# Load first frame to determine subsample indices
|
|
91
|
+
pos_first, norm_first, dens_first = read_xyz(files[0])
|
|
92
|
+
|
|
93
|
+
# Determine subsample indices
|
|
94
|
+
if subsample and len(pos_first) > subsample:
|
|
95
|
+
subsample_indices = np.random.choice(len(pos_first), subsample, replace=False)
|
|
96
|
+
subsample_indices.sort() # Sort for better cache locality
|
|
97
|
+
print(f"Subsampling {len(pos_first)} points to {subsample} points")
|
|
98
|
+
else:
|
|
99
|
+
subsample_indices = None
|
|
100
|
+
print(f"Using all {len(pos_first)} points (no subsampling)")
|
|
101
|
+
|
|
102
|
+
# Load all data
|
|
103
|
+
all_positions = []
|
|
104
|
+
all_densities = []
|
|
105
|
+
|
|
106
|
+
for f in tqdm(files, desc="Loading data"):
|
|
107
|
+
pos, norm, dens = read_xyz(f)
|
|
108
|
+
|
|
109
|
+
# Subsample if requested
|
|
110
|
+
if subsample_indices is not None:
|
|
111
|
+
pos = pos[subsample_indices]
|
|
112
|
+
dens = dens[subsample_indices]
|
|
113
|
+
|
|
114
|
+
all_positions.append(pos)
|
|
115
|
+
all_densities.append(dens)
|
|
116
|
+
|
|
117
|
+
# Get global bounds for consistent axes
|
|
118
|
+
all_pos = np.vstack(all_positions)
|
|
119
|
+
pos_min = all_pos.min(axis=0)
|
|
120
|
+
pos_max = all_pos.max(axis=0)
|
|
121
|
+
|
|
122
|
+
all_dens = np.hstack(all_densities)
|
|
123
|
+
dens_min = all_dens.min()
|
|
124
|
+
dens_max = all_dens.max()
|
|
125
|
+
|
|
126
|
+
print(f"Position range: [{pos_min[0]:.3f}, {pos_max[0]:.3f}] × "
|
|
127
|
+
f"[{pos_min[1]:.3f}, {pos_max[1]:.3f}] × "
|
|
128
|
+
f"[{pos_min[2]:.3f}, {pos_max[2]:.3f}]")
|
|
129
|
+
print(f"Density range: [{dens_min:.3f}, {dens_max:.3f}]")
|
|
130
|
+
|
|
131
|
+
# Create figure with three 2D subplots
|
|
132
|
+
fig, (ax_xy, ax_xz, ax_yz) = plt.subplots(1, 3, figsize=figsize)
|
|
133
|
+
|
|
134
|
+
# Create colormap normalizer
|
|
135
|
+
from matplotlib.colors import Normalize
|
|
136
|
+
norm = Normalize(vmin=dens_min, vmax=dens_max)
|
|
137
|
+
|
|
138
|
+
def update(frame):
|
|
139
|
+
"""Update function for animation"""
|
|
140
|
+
# Clear all axes
|
|
141
|
+
ax_xy.clear()
|
|
142
|
+
ax_xz.clear()
|
|
143
|
+
ax_yz.clear()
|
|
144
|
+
|
|
145
|
+
positions = all_positions[frame]
|
|
146
|
+
densities = all_densities[frame]
|
|
147
|
+
|
|
148
|
+
# XY plane (top view - looking down Z axis)
|
|
149
|
+
ax_xy.scatter(positions[:, 0], positions[:, 1],
|
|
150
|
+
c=densities, cmap=cmap, s=1, alpha=alpha,
|
|
151
|
+
vmin=dens_min, vmax=dens_max)
|
|
152
|
+
ax_xy.set_xlim(pos_min[0], pos_max[0])
|
|
153
|
+
ax_xy.set_ylim(pos_min[1], pos_max[1])
|
|
154
|
+
ax_xy.set_xlabel('X')
|
|
155
|
+
ax_xy.set_ylabel('Y')
|
|
156
|
+
ax_xy.set_title('XY Plane (Top View)')
|
|
157
|
+
ax_xy.set_aspect('equal')
|
|
158
|
+
ax_xy.grid(True, alpha=0.3)
|
|
159
|
+
|
|
160
|
+
# XZ plane (front view - looking along Y axis)
|
|
161
|
+
ax_xz.scatter(positions[:, 0], positions[:, 2],
|
|
162
|
+
c=densities, cmap=cmap, s=1, alpha=alpha,
|
|
163
|
+
vmin=dens_min, vmax=dens_max)
|
|
164
|
+
ax_xz.set_xlim(pos_min[0], pos_max[0])
|
|
165
|
+
ax_xz.set_ylim(pos_min[2], pos_max[2])
|
|
166
|
+
ax_xz.set_xlabel('X')
|
|
167
|
+
ax_xz.set_ylabel('Z')
|
|
168
|
+
ax_xz.set_title('XZ Plane (Front View)')
|
|
169
|
+
ax_xz.set_aspect('equal')
|
|
170
|
+
ax_xz.grid(True, alpha=0.3)
|
|
171
|
+
|
|
172
|
+
# YZ plane (side view - looking along X axis)
|
|
173
|
+
scatter_yz = ax_yz.scatter(positions[:, 1], positions[:, 2],
|
|
174
|
+
c=densities, cmap=cmap, s=1, alpha=alpha,
|
|
175
|
+
vmin=dens_min, vmax=dens_max)
|
|
176
|
+
ax_yz.set_xlim(pos_min[1], pos_max[1])
|
|
177
|
+
ax_yz.set_ylim(pos_min[2], pos_max[2])
|
|
178
|
+
ax_yz.set_xlabel('Y')
|
|
179
|
+
ax_yz.set_ylabel('Z')
|
|
180
|
+
ax_yz.set_title('YZ Plane (Side View)')
|
|
181
|
+
ax_yz.set_aspect('equal')
|
|
182
|
+
ax_yz.grid(True, alpha=0.3)
|
|
183
|
+
|
|
184
|
+
# Extract iteration number and set main title
|
|
185
|
+
filename = os.path.basename(files[frame])
|
|
186
|
+
if 'final' in filename:
|
|
187
|
+
fig.suptitle('Final Grid State (Converged)',
|
|
188
|
+
fontsize=16, fontweight='bold')
|
|
189
|
+
else:
|
|
190
|
+
iter_num = int(filename.split('_')[-1].replace('.xyz', ''))
|
|
191
|
+
fig.suptitle(f'Grid Deformation - Iteration {iter_num}',
|
|
192
|
+
fontsize=16)
|
|
193
|
+
|
|
194
|
+
return scatter_yz, # Return one for blit
|
|
195
|
+
|
|
196
|
+
# Create initial frame to set up colorbar
|
|
197
|
+
positions = all_positions[0]
|
|
198
|
+
densities = all_densities[0]
|
|
199
|
+
|
|
200
|
+
scatter_yz = ax_yz.scatter(positions[:, 1], positions[:, 2],
|
|
201
|
+
c=densities, cmap=cmap, s=1, alpha=alpha,
|
|
202
|
+
vmin=dens_min, vmax=dens_max)
|
|
203
|
+
|
|
204
|
+
# Add colorbar (shared for all three plots)
|
|
205
|
+
cbar = fig.colorbar(scatter_yz, ax=[ax_xy, ax_xz, ax_yz],
|
|
206
|
+
label='Density', shrink=0.8, pad=0.02)
|
|
207
|
+
|
|
208
|
+
# Create animation
|
|
209
|
+
print(f"Creating animation with {fps} fps...")
|
|
210
|
+
anim = FuncAnimation(fig, update, frames=len(files),
|
|
211
|
+
interval=1000//fps, blit=False)
|
|
212
|
+
|
|
213
|
+
# Save
|
|
214
|
+
print(f"Saving to {output_file}...")
|
|
215
|
+
if output_file.endswith('.gif'):
|
|
216
|
+
writer = PillowWriter(fps=fps)
|
|
217
|
+
anim.save(output_file, writer=writer)
|
|
218
|
+
elif output_file.endswith('.mp4'):
|
|
219
|
+
try:
|
|
220
|
+
from matplotlib.animation import FFMpegWriter
|
|
221
|
+
writer = FFMpegWriter(fps=fps, bitrate=1800)
|
|
222
|
+
anim.save(output_file, writer=writer)
|
|
223
|
+
except Exception as e:
|
|
224
|
+
raise RuntimeError(
|
|
225
|
+
f"Failed to save MP4. Make sure ffmpeg is installed.\n"
|
|
226
|
+
f"Error: {e}\n"
|
|
227
|
+
f"Try saving as .gif instead, or install ffmpeg."
|
|
228
|
+
)
|
|
229
|
+
else:
|
|
230
|
+
raise ValueError(
|
|
231
|
+
f"output_file must end with .gif or .mp4, got: {output_file}"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
plt.close()
|
|
235
|
+
print(f"✓ Animation saved to: {output_file}")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def animate_surface_deformation(export_folder='vderm_exports',
|
|
239
|
+
subfolder='vderm_surface',
|
|
240
|
+
output_file='surface_animation.gif',
|
|
241
|
+
fps=5,
|
|
242
|
+
subsample=5000,
|
|
243
|
+
show_normals=False,
|
|
244
|
+
alpha=0.6,
|
|
245
|
+
figsize=(10, 8)):
|
|
246
|
+
"""
|
|
247
|
+
Create animated GIF of surface deformation from exported surface states.
|
|
248
|
+
|
|
249
|
+
Parameters
|
|
250
|
+
----------
|
|
251
|
+
export_folder : str, default='vderm_exports'
|
|
252
|
+
Base export folder
|
|
253
|
+
subfolder : str, default='vderm_surface'
|
|
254
|
+
Subfolder containing surface exports
|
|
255
|
+
output_file : str, default='surface_animation.gif'
|
|
256
|
+
Output animation filename (.gif or .mp4)
|
|
257
|
+
fps : int, default=5
|
|
258
|
+
Frames per second
|
|
259
|
+
subsample : int or None, default=5000
|
|
260
|
+
Subsample to this many points for faster rendering
|
|
261
|
+
show_normals : bool, default=False
|
|
262
|
+
If True, draw normal vectors (slower, may clutter visualization)
|
|
263
|
+
figsize : tuple, default=(10, 8)
|
|
264
|
+
Figure size in inches
|
|
265
|
+
|
|
266
|
+
Returns
|
|
267
|
+
-------
|
|
268
|
+
None
|
|
269
|
+
Saves animation to output_file
|
|
270
|
+
|
|
271
|
+
Examples
|
|
272
|
+
--------
|
|
273
|
+
>>> # Basic surface animation
|
|
274
|
+
>>> animate_surface_deformation('my_exports')
|
|
275
|
+
|
|
276
|
+
>>> # Show normal vectors
|
|
277
|
+
>>> animate_surface_deformation('my_exports',
|
|
278
|
+
... show_normals=True,
|
|
279
|
+
... subsample=1000) # Use fewer points with normals
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
# Find all surface files
|
|
283
|
+
pattern = os.path.join(export_folder, subfolder, 'surface_iteration_*.xyz')
|
|
284
|
+
files = sorted(glob.glob(pattern))
|
|
285
|
+
|
|
286
|
+
# Check for final
|
|
287
|
+
final_pattern = os.path.join(export_folder, subfolder, 'surface_final_*.xyz')
|
|
288
|
+
final_files = glob.glob(final_pattern)
|
|
289
|
+
if final_files:
|
|
290
|
+
files.append(final_files[0])
|
|
291
|
+
|
|
292
|
+
if len(files) == 0:
|
|
293
|
+
raise FileNotFoundError(
|
|
294
|
+
f"No surface files found matching {pattern}\n"
|
|
295
|
+
f"Make sure you ran run_VDERM_with_tracking with export_surface=True"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
print(f"Found {len(files)} frames to animate")
|
|
299
|
+
|
|
300
|
+
# Load first frame to determine subsample indices
|
|
301
|
+
pos_first, norm_first, dense_first = read_xyz(files[0])
|
|
302
|
+
|
|
303
|
+
min_dens = min(dense_first)
|
|
304
|
+
max_dens = max(dense_first)
|
|
305
|
+
|
|
306
|
+
# Determine subsample indices
|
|
307
|
+
if subsample and len(pos_first) > subsample:
|
|
308
|
+
subsample_indices = np.random.choice(len(pos_first), subsample, replace=False)
|
|
309
|
+
subsample_indices.sort()
|
|
310
|
+
print(f"Subsampling {len(pos_first)} points to {subsample} points")
|
|
311
|
+
else:
|
|
312
|
+
subsample_indices = None
|
|
313
|
+
print(f"Using all {len(pos_first)} points (no subsampling)")
|
|
314
|
+
|
|
315
|
+
# Load all data
|
|
316
|
+
all_points = []
|
|
317
|
+
all_normals = []
|
|
318
|
+
all_dens = []
|
|
319
|
+
|
|
320
|
+
for f in tqdm(files, desc="Loading data"):
|
|
321
|
+
pts, norms, dens = read_xyz(f)
|
|
322
|
+
|
|
323
|
+
# Subsample if requested
|
|
324
|
+
if subsample_indices is not None:
|
|
325
|
+
pts = pts[subsample_indices]
|
|
326
|
+
norms = norms[subsample_indices]
|
|
327
|
+
dens = dens[subsample_indices]
|
|
328
|
+
|
|
329
|
+
all_points.append(pts)
|
|
330
|
+
all_normals.append(norms)
|
|
331
|
+
all_dens.append(dens)
|
|
332
|
+
|
|
333
|
+
# Get global bounds
|
|
334
|
+
all_pts = np.vstack(all_points)
|
|
335
|
+
pts_min = all_pts.min(axis=0)
|
|
336
|
+
pts_max = all_pts.max(axis=0)
|
|
337
|
+
|
|
338
|
+
print(f"Surface bounds: [{pts_min[0]:.3f}, {pts_max[0]:.3f}] × "
|
|
339
|
+
f"[{pts_min[1]:.3f}, {pts_max[1]:.3f}] × "
|
|
340
|
+
f"[{pts_min[2]:.3f}, {pts_max[2]:.3f}]")
|
|
341
|
+
|
|
342
|
+
# Create figure
|
|
343
|
+
fig = plt.figure(figsize=figsize)
|
|
344
|
+
ax = fig.add_subplot(111, projection='3d')
|
|
345
|
+
|
|
346
|
+
def update(frame):
|
|
347
|
+
ax.clear()
|
|
348
|
+
|
|
349
|
+
points = all_points[frame]
|
|
350
|
+
normals = all_normals[frame]
|
|
351
|
+
densities = all_dens[frame]
|
|
352
|
+
|
|
353
|
+
# Plot surface
|
|
354
|
+
box = ax.scatter(points[:, 0], points[:, 1], points[:, 2],
|
|
355
|
+
c=densities,cmap='viridis', s=1, alpha=alpha,
|
|
356
|
+
vmin=min_dens,vmax=max_dens)
|
|
357
|
+
|
|
358
|
+
# Optionally show normals
|
|
359
|
+
if show_normals:
|
|
360
|
+
# Only show every Nth normal for clarity
|
|
361
|
+
step = max(1, len(points) // 100)
|
|
362
|
+
normal_scale = (pts_max - pts_min).mean() * 0.02 # Scale normals to 2% of domain
|
|
363
|
+
ax.quiver(points[::step, 0], points[::step, 1], points[::step, 2],
|
|
364
|
+
normals[::step, 0], normals[::step, 1], normals[::step, 2],
|
|
365
|
+
length=normal_scale, color='red', alpha=0.5,
|
|
366
|
+
arrow_length_ratio=0.3)
|
|
367
|
+
|
|
368
|
+
ax.set_xlim(pts_min[0], pts_max[0])
|
|
369
|
+
ax.set_ylim(pts_min[1], pts_max[1])
|
|
370
|
+
ax.set_zlim(pts_min[2], pts_max[2])
|
|
371
|
+
|
|
372
|
+
ax.set_box_aspect([
|
|
373
|
+
pts_max[0] - pts_min[0],
|
|
374
|
+
pts_max[1] - pts_min[1],
|
|
375
|
+
pts_max[2] - pts_min[2]
|
|
376
|
+
])
|
|
377
|
+
|
|
378
|
+
ax.set_xlabel('X')
|
|
379
|
+
ax.set_ylabel('Y')
|
|
380
|
+
ax.set_zlabel('Z')
|
|
381
|
+
|
|
382
|
+
# Extract iteration
|
|
383
|
+
filename = os.path.basename(files[frame])
|
|
384
|
+
if 'final' in filename:
|
|
385
|
+
ax.set_title('Surface: Final (Converged)', fontsize=14, fontweight='bold')
|
|
386
|
+
else:
|
|
387
|
+
iter_num = int(filename.split('_')[-1].replace('.xyz', ''))
|
|
388
|
+
ax.set_title(f'Surface: Iteration {iter_num}', fontsize=14)
|
|
389
|
+
|
|
390
|
+
return ax,
|
|
391
|
+
|
|
392
|
+
# initial frame to set up colorbar
|
|
393
|
+
|
|
394
|
+
box = ax.scatter(pos_first[:, 0], pos_first[:, 1], pos_first[:, 2],
|
|
395
|
+
c=dense_first,cmap='viridis', s=1, alpha=alpha,
|
|
396
|
+
vmin=min_dens,vmax=max_dens)
|
|
397
|
+
|
|
398
|
+
cbar = plt.colorbar(box, ax=ax, label='Density', shrink=0.5)
|
|
399
|
+
|
|
400
|
+
# Create animation
|
|
401
|
+
print(f"Creating animation with {fps} fps...")
|
|
402
|
+
anim = FuncAnimation(fig, update, frames=len(files),
|
|
403
|
+
interval=1000//fps, blit=False)
|
|
404
|
+
|
|
405
|
+
# Save
|
|
406
|
+
print(f"Saving to {output_file}...")
|
|
407
|
+
if output_file.endswith('.gif'):
|
|
408
|
+
writer = PillowWriter(fps=fps)
|
|
409
|
+
anim.save(output_file, writer=writer)
|
|
410
|
+
elif output_file.endswith('.mp4'):
|
|
411
|
+
try:
|
|
412
|
+
from matplotlib.animation import FFMpegWriter
|
|
413
|
+
writer = FFMpegWriter(fps=fps, bitrate=1800)
|
|
414
|
+
anim.save(output_file, writer=writer)
|
|
415
|
+
except Exception as e:
|
|
416
|
+
raise RuntimeError(
|
|
417
|
+
f"Failed to save MP4. Make sure ffmpeg is installed.\n"
|
|
418
|
+
f"Error: {e}"
|
|
419
|
+
)
|
|
420
|
+
else:
|
|
421
|
+
raise ValueError("output_file must end with .gif or .mp4")
|
|
422
|
+
|
|
423
|
+
plt.close()
|
|
424
|
+
print(f"✓ Animation saved to: {output_file}")
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def interactive_pcd_plot(export_folder, pattern='grid_iteration_*.xyz',
|
|
428
|
+
subsample=True, max_points=10_000):
|
|
429
|
+
"""
|
|
430
|
+
Visualize a sequence of exported grid states to see deformation over time.
|
|
431
|
+
|
|
432
|
+
Parameters
|
|
433
|
+
----------
|
|
434
|
+
export_folder : str
|
|
435
|
+
Folder containing exported grid files
|
|
436
|
+
pattern : str
|
|
437
|
+
Glob pattern to match grid files (default: 'grid_iteration_*.xyz')
|
|
438
|
+
subsample : bool
|
|
439
|
+
Whether to downsample the number of points plotted
|
|
440
|
+
max_points :
|
|
441
|
+
max points to plot if subsampling enabled
|
|
442
|
+
|
|
443
|
+
Examples
|
|
444
|
+
--------
|
|
445
|
+
>>> visualize_grid_sequence('deformation_sequence')
|
|
446
|
+
>>> visualize_grid_sequence('exports', save_animation=True, output_file='deform.gif')
|
|
447
|
+
|
|
448
|
+
Notes
|
|
449
|
+
-----
|
|
450
|
+
ipympl and ipywidgets must be installed, and the jupyter backend must be set
|
|
451
|
+
to %matplotlib widgets for this function to work. Otherwise it will produce a
|
|
452
|
+
static plot of the first frame
|
|
453
|
+
"""
|
|
454
|
+
|
|
455
|
+
# Find all matching files
|
|
456
|
+
file_pattern = os.path.join(export_folder, pattern)
|
|
457
|
+
files = sorted(glob.glob(file_pattern))
|
|
458
|
+
|
|
459
|
+
if len(files) == 0:
|
|
460
|
+
raise FileNotFoundError(f"No files matching pattern '{file_pattern}'")
|
|
461
|
+
|
|
462
|
+
print(f"Found {len(files)} files to visualize")
|
|
463
|
+
|
|
464
|
+
# Read all files to get global bounds and density range
|
|
465
|
+
all_positions = []
|
|
466
|
+
all_densities = []
|
|
467
|
+
for f in files:
|
|
468
|
+
pos, norms, dens = read_xyz(f)
|
|
469
|
+
all_positions.append(pos)
|
|
470
|
+
all_densities.append(dens)
|
|
471
|
+
|
|
472
|
+
# Compute global bounds for consistent axes
|
|
473
|
+
all_pos_concat = np.vstack(all_positions)
|
|
474
|
+
all_dens_concat = np.hstack(all_densities)
|
|
475
|
+
|
|
476
|
+
pos_min = all_pos_concat.min(axis=0)
|
|
477
|
+
pos_max = all_pos_concat.max(axis=0)
|
|
478
|
+
dens_min = all_dens_concat.min()
|
|
479
|
+
dens_max = all_dens_concat.max()
|
|
480
|
+
|
|
481
|
+
# Interactive visualization
|
|
482
|
+
from matplotlib.widgets import Slider
|
|
483
|
+
|
|
484
|
+
fig = plt.figure(figsize=(12, 8))
|
|
485
|
+
ax = fig.add_subplot(111, projection='3d')
|
|
486
|
+
|
|
487
|
+
# Initial plot
|
|
488
|
+
positions, densities = all_positions[0], all_densities[0]
|
|
489
|
+
|
|
490
|
+
# Subsample if needed
|
|
491
|
+
if subsample and len(positions) > max_points:
|
|
492
|
+
indices = np.random.choice(len(positions), 10000, replace=False)
|
|
493
|
+
positions_sub = positions[indices]
|
|
494
|
+
densities_sub = densities[indices]
|
|
495
|
+
else:
|
|
496
|
+
positions_sub = positions
|
|
497
|
+
densities_sub = densities
|
|
498
|
+
|
|
499
|
+
scatter = ax.scatter(positions_sub[:, 0], positions_sub[:, 1], positions_sub[:, 2],
|
|
500
|
+
c=densities_sub, cmap='viridis', s=1,
|
|
501
|
+
vmin=dens_min, vmax=dens_max)
|
|
502
|
+
|
|
503
|
+
ax.set_xlim(pos_min[0], pos_max[0])
|
|
504
|
+
ax.set_ylim(pos_min[1], pos_max[1])
|
|
505
|
+
ax.set_zlim(pos_min[2], pos_max[2])
|
|
506
|
+
|
|
507
|
+
ax.set_box_aspect([
|
|
508
|
+
pos_max[0] - pos_min[0],
|
|
509
|
+
pos_max[1] - pos_min[1],
|
|
510
|
+
pos_max[2] - pos_min[2]
|
|
511
|
+
])
|
|
512
|
+
|
|
513
|
+
ax.set_xlabel('X')
|
|
514
|
+
ax.set_ylabel('Y')
|
|
515
|
+
ax.set_zlabel('Z')
|
|
516
|
+
ax.set_title('Iteration 0')
|
|
517
|
+
|
|
518
|
+
cbar = plt.colorbar(scatter, ax=ax, label='Density', shrink=0.5)
|
|
519
|
+
|
|
520
|
+
# Add slider
|
|
521
|
+
ax_slider = plt.axes([0.2, 0.02, 0.6, 0.03])
|
|
522
|
+
slider = Slider(ax_slider, 'Iteration', 0, len(files)-1,
|
|
523
|
+
valinit=0, valstep=1)
|
|
524
|
+
|
|
525
|
+
def update_plot(val):
|
|
526
|
+
frame = int(slider.val)
|
|
527
|
+
positions, densities = all_positions[frame], all_densities[frame]
|
|
528
|
+
|
|
529
|
+
# Subsample if needed
|
|
530
|
+
if subsample and len(positions) > max_points:
|
|
531
|
+
positions_sub = positions[indices]
|
|
532
|
+
densities_sub = densities[indices]
|
|
533
|
+
else:
|
|
534
|
+
positions_sub = positions
|
|
535
|
+
densities_sub = densities
|
|
536
|
+
|
|
537
|
+
scatter._offsets3d = (positions_sub[:, 0],
|
|
538
|
+
positions_sub[:, 1],
|
|
539
|
+
positions_sub[:, 2])
|
|
540
|
+
scatter.set_array(densities_sub)
|
|
541
|
+
|
|
542
|
+
ax.set_title(f'Iteration {frame * 10}') # Adjust based on export_frequency
|
|
543
|
+
fig.canvas.draw_idle()
|
|
544
|
+
|
|
545
|
+
slider.on_changed(update_plot)
|
|
546
|
+
plt.show()
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def create_side_by_side_animation(export_folder='vderm_exports',
|
|
550
|
+
grid_folder='vderm_grid',
|
|
551
|
+
surface_folder='vderm_surface',
|
|
552
|
+
output_file='comparison.gif',
|
|
553
|
+
fps=5,
|
|
554
|
+
subsample=3000,
|
|
555
|
+
alpha_grid=1,
|
|
556
|
+
alpha_surface=0.6,
|
|
557
|
+
figsize=(15, 10)):
|
|
558
|
+
"""
|
|
559
|
+
Create side-by-side animation showing grid and surface evolution in 2D projections.
|
|
560
|
+
|
|
561
|
+
Creates a 2×3 grid of subplots:
|
|
562
|
+
- Top row: Grid XY, XZ, YZ projections (with density coloring)
|
|
563
|
+
- Bottom row: Surface XY, XZ, YZ projections (solid color)
|
|
564
|
+
|
|
565
|
+
Grid and surface use their own separate bounds for better visualization.
|
|
566
|
+
|
|
567
|
+
Parameters
|
|
568
|
+
----------
|
|
569
|
+
export_folder : str, default='vderm_exports'
|
|
570
|
+
Base export folder
|
|
571
|
+
grid_folder : str, default='vderm_grid'
|
|
572
|
+
Subfolder containing grid exports
|
|
573
|
+
surface_folder : str, default='vderm_surface'
|
|
574
|
+
Subfolder containing surface exports
|
|
575
|
+
output_file : str, default='comparison.gif'
|
|
576
|
+
Output animation filename (.gif or .mp4)
|
|
577
|
+
fps : int, default=5
|
|
578
|
+
Frames per second
|
|
579
|
+
subsample : int or None, default=3000
|
|
580
|
+
Subsample each dataset to this many points
|
|
581
|
+
alpha : float, default=0.3
|
|
582
|
+
Opacity for grid points
|
|
583
|
+
figsize : tuple, default=(15, 10)
|
|
584
|
+
Figure size in inches (width, height)
|
|
585
|
+
|
|
586
|
+
Returns
|
|
587
|
+
-------
|
|
588
|
+
None
|
|
589
|
+
Saves animation to output_file
|
|
590
|
+
|
|
591
|
+
Notes
|
|
592
|
+
-----
|
|
593
|
+
Grid and surface exports must have matching iteration numbers.
|
|
594
|
+
If they don't match, only overlapping iterations will be animated.
|
|
595
|
+
|
|
596
|
+
Examples
|
|
597
|
+
--------
|
|
598
|
+
>>> create_side_by_side_animation('my_exports', output_file='both.gif')
|
|
599
|
+
|
|
600
|
+
>>> # More opaque grid, faster playback
|
|
601
|
+
>>> create_side_by_side_animation('my_exports', alpha=0.5, fps=10)
|
|
602
|
+
"""
|
|
603
|
+
|
|
604
|
+
# Find files
|
|
605
|
+
grid_pattern = os.path.join(export_folder, grid_folder, 'grid_iteration_*.xyz')
|
|
606
|
+
surface_pattern = os.path.join(export_folder, surface_folder, 'surface_iteration_*.xyz')
|
|
607
|
+
|
|
608
|
+
grid_files = sorted(glob.glob(grid_pattern))
|
|
609
|
+
surface_files = sorted(glob.glob(surface_pattern))
|
|
610
|
+
|
|
611
|
+
if len(grid_files) == 0:
|
|
612
|
+
raise FileNotFoundError(f"No grid files found at {grid_pattern}")
|
|
613
|
+
if len(surface_files) == 0:
|
|
614
|
+
raise FileNotFoundError(f"No surface files found at {surface_pattern}")
|
|
615
|
+
|
|
616
|
+
# Must have matching counts
|
|
617
|
+
if len(grid_files) != len(surface_files):
|
|
618
|
+
print(f"Warning: Found {len(grid_files)} grid files but "
|
|
619
|
+
f"{len(surface_files)} surface files")
|
|
620
|
+
n_frames = min(len(grid_files), len(surface_files))
|
|
621
|
+
grid_files = grid_files[:n_frames]
|
|
622
|
+
surface_files = surface_files[:n_frames]
|
|
623
|
+
|
|
624
|
+
print(f"Creating {len(grid_files)} frame side-by-side animation")
|
|
625
|
+
|
|
626
|
+
# Load first frame to determine subsample indices
|
|
627
|
+
pos_first, norm_first, dense_first = read_xyz(surface_files[0])
|
|
628
|
+
|
|
629
|
+
# Determine subsample indices ONCE (use for all frames)
|
|
630
|
+
if subsample and len(pos_first) > subsample:
|
|
631
|
+
subsample_surface_indices = np.random.choice(len(pos_first), subsample, replace=False)
|
|
632
|
+
subsample_surface_indices.sort() # Sort for better cache locality
|
|
633
|
+
print(f"Subsampling {len(pos_first)} points to {subsample} points")
|
|
634
|
+
else:
|
|
635
|
+
subsample_surface_indices = None
|
|
636
|
+
print(f"Using all {len(pos_first)} points (no subsampling)")
|
|
637
|
+
|
|
638
|
+
# Load first frame to determine subsample indices
|
|
639
|
+
pos_first, norm_first, dense_first = read_xyz(grid_files[0])
|
|
640
|
+
|
|
641
|
+
# Determine subsample indices ONCE (use for all frames)
|
|
642
|
+
if subsample and len(pos_first) > subsample:
|
|
643
|
+
subsample_grid_indices = np.random.choice(len(pos_first), subsample, replace=False)
|
|
644
|
+
subsample_grid_indices.sort() # Sort for better cache locality
|
|
645
|
+
print(f"Subsampling {len(pos_first)} points to {subsample} points")
|
|
646
|
+
else:
|
|
647
|
+
subsample_grid_indices = None
|
|
648
|
+
print(f"Using all {len(pos_first)} points (no subsampling)")
|
|
649
|
+
|
|
650
|
+
# Load all data
|
|
651
|
+
all_grid_pos, all_grid_dens = [], []
|
|
652
|
+
all_surf_pts, all_surf_norms = [], []
|
|
653
|
+
|
|
654
|
+
for gf, sf in tqdm(zip(grid_files, surface_files),
|
|
655
|
+
total=len(grid_files), desc="Loading"):
|
|
656
|
+
gp, gn, gd = read_xyz(gf)
|
|
657
|
+
sp, sn, sd = read_xyz(sf)
|
|
658
|
+
|
|
659
|
+
# Subsample
|
|
660
|
+
if subsample:
|
|
661
|
+
gp, gd = gp[subsample_grid_indices], gd[subsample_grid_indices]
|
|
662
|
+
sp, sn = sp[subsample_surface_indices], sn[subsample_surface_indices]
|
|
663
|
+
|
|
664
|
+
all_grid_pos.append(gp)
|
|
665
|
+
all_grid_dens.append(gd)
|
|
666
|
+
all_surf_pts.append(sp)
|
|
667
|
+
all_surf_norms.append(sn)
|
|
668
|
+
|
|
669
|
+
# Get separate bounds for grid and surface
|
|
670
|
+
all_grid = np.vstack(all_grid_pos)
|
|
671
|
+
grid_min = all_grid.min(axis=0)
|
|
672
|
+
grid_max = all_grid.max(axis=0)
|
|
673
|
+
|
|
674
|
+
all_surf = np.vstack(all_surf_pts)
|
|
675
|
+
surf_min = all_surf.min(axis=0)
|
|
676
|
+
surf_max = all_surf.max(axis=0)
|
|
677
|
+
|
|
678
|
+
all_dens = np.hstack(all_grid_dens)
|
|
679
|
+
dens_min, dens_max = all_dens.min(), all_dens.max()
|
|
680
|
+
|
|
681
|
+
print(f"Grid bounds: [{grid_min[0]:.3f}, {grid_max[0]:.3f}] × "
|
|
682
|
+
f"[{grid_min[1]:.3f}, {grid_max[1]:.3f}] × "
|
|
683
|
+
f"[{grid_min[2]:.3f}, {grid_max[2]:.3f}]")
|
|
684
|
+
print(f"Surface bounds: [{surf_min[0]:.3f}, {surf_max[0]:.3f}] × "
|
|
685
|
+
f"[{surf_min[1]:.3f}, {surf_max[1]:.3f}] × "
|
|
686
|
+
f"[{surf_min[2]:.3f}, {surf_max[2]:.3f}]")
|
|
687
|
+
|
|
688
|
+
# Create figure with 2 rows, 3 columns
|
|
689
|
+
fig, axes = plt.subplots(2, 3, figsize=figsize)
|
|
690
|
+
ax_g_xy, ax_g_xz, ax_g_yz = axes[0] # Top row: grid
|
|
691
|
+
ax_s_xy, ax_s_xz, ax_s_yz = axes[1] # Bottom row: surface
|
|
692
|
+
|
|
693
|
+
def update(frame):
|
|
694
|
+
"""Update function for animation"""
|
|
695
|
+
# Clear all axes
|
|
696
|
+
for ax_row in axes:
|
|
697
|
+
for ax in ax_row:
|
|
698
|
+
ax.clear()
|
|
699
|
+
|
|
700
|
+
grid_pos = all_grid_pos[frame]
|
|
701
|
+
grid_dens = all_grid_dens[frame]
|
|
702
|
+
surf_pts = all_surf_pts[frame]
|
|
703
|
+
|
|
704
|
+
# ==========================================
|
|
705
|
+
# Top row: Grid projections (with density)
|
|
706
|
+
# ==========================================
|
|
707
|
+
|
|
708
|
+
# Grid XY
|
|
709
|
+
ax_g_xy.scatter(grid_pos[:, 0], grid_pos[:, 1],
|
|
710
|
+
c=grid_dens, cmap='plasma', s=1, alpha=alpha_grid,
|
|
711
|
+
vmin=dens_min, vmax=dens_max)
|
|
712
|
+
ax_g_xy.set_xlim(grid_min[0], grid_max[0])
|
|
713
|
+
ax_g_xy.set_ylim(grid_min[1], grid_max[1])
|
|
714
|
+
ax_g_xy.set_xlabel('X')
|
|
715
|
+
ax_g_xy.set_ylabel('Y')
|
|
716
|
+
ax_g_xy.set_title('Grid XY (Top View)', fontsize=10)
|
|
717
|
+
ax_g_xy.set_aspect('equal')
|
|
718
|
+
ax_g_xy.grid(True, alpha=0.2)
|
|
719
|
+
|
|
720
|
+
# Grid XZ
|
|
721
|
+
ax_g_xz.scatter(grid_pos[:, 0], grid_pos[:, 2],
|
|
722
|
+
c=grid_dens, cmap='plasma', s=1, alpha=alpha_grid,
|
|
723
|
+
vmin=dens_min, vmax=dens_max)
|
|
724
|
+
ax_g_xz.set_xlim(grid_min[0], grid_max[0])
|
|
725
|
+
ax_g_xz.set_ylim(grid_min[2], grid_max[2])
|
|
726
|
+
ax_g_xz.set_xlabel('X')
|
|
727
|
+
ax_g_xz.set_ylabel('Z')
|
|
728
|
+
ax_g_xz.set_title('Grid XZ (Front View)', fontsize=10)
|
|
729
|
+
ax_g_xz.set_aspect('equal')
|
|
730
|
+
ax_g_xz.grid(True, alpha=0.2)
|
|
731
|
+
|
|
732
|
+
# Grid YZ
|
|
733
|
+
scatter_grid = ax_g_yz.scatter(grid_pos[:, 1], grid_pos[:, 2],
|
|
734
|
+
c=grid_dens, cmap='plasma', s=1, alpha=alpha_grid,
|
|
735
|
+
vmin=dens_min, vmax=dens_max)
|
|
736
|
+
ax_g_yz.set_xlim(grid_min[1], grid_max[1])
|
|
737
|
+
ax_g_yz.set_ylim(grid_min[2], grid_max[2])
|
|
738
|
+
ax_g_yz.set_xlabel('Y')
|
|
739
|
+
ax_g_yz.set_ylabel('Z')
|
|
740
|
+
ax_g_yz.set_title('Grid YZ (Side View)', fontsize=10)
|
|
741
|
+
ax_g_yz.set_aspect('equal')
|
|
742
|
+
ax_g_yz.grid(True, alpha=0.2)
|
|
743
|
+
|
|
744
|
+
# ==========================================
|
|
745
|
+
# Bottom row: Surface projections
|
|
746
|
+
# ==========================================
|
|
747
|
+
|
|
748
|
+
# Surface XY
|
|
749
|
+
ax_s_xy.scatter(surf_pts[:, 0], surf_pts[:, 1],
|
|
750
|
+
c='dodgerblue', s=1, alpha=alpha_surface)
|
|
751
|
+
ax_s_xy.set_xlim(surf_min[0], surf_max[0])
|
|
752
|
+
ax_s_xy.set_ylim(surf_min[1], surf_max[1])
|
|
753
|
+
ax_s_xy.set_xlabel('X')
|
|
754
|
+
ax_s_xy.set_ylabel('Y')
|
|
755
|
+
ax_s_xy.set_title('Surface XY (Top View)', fontsize=10)
|
|
756
|
+
ax_s_xy.set_aspect('equal')
|
|
757
|
+
ax_s_xy.grid(True, alpha=0.2)
|
|
758
|
+
|
|
759
|
+
# Surface XZ
|
|
760
|
+
ax_s_xz.scatter(surf_pts[:, 0], surf_pts[:, 2],
|
|
761
|
+
c='dodgerblue', s=1, alpha=alpha_surface)
|
|
762
|
+
ax_s_xz.set_xlim(surf_min[0], surf_max[0])
|
|
763
|
+
ax_s_xz.set_ylim(surf_min[2], surf_max[2])
|
|
764
|
+
ax_s_xz.set_xlabel('X')
|
|
765
|
+
ax_s_xz.set_ylabel('Z')
|
|
766
|
+
ax_s_xz.set_title('Surface XZ (Front View)', fontsize=10)
|
|
767
|
+
ax_s_xz.set_aspect('equal')
|
|
768
|
+
ax_s_xz.grid(True, alpha=0.2)
|
|
769
|
+
|
|
770
|
+
# Surface YZ
|
|
771
|
+
ax_s_yz.scatter(surf_pts[:, 1], surf_pts[:, 2],
|
|
772
|
+
c='dodgerblue', s=1, alpha=alpha_surface)
|
|
773
|
+
ax_s_yz.set_xlim(surf_min[1], surf_max[1])
|
|
774
|
+
ax_s_yz.set_ylim(surf_min[2], surf_max[2])
|
|
775
|
+
ax_s_yz.set_xlabel('Y')
|
|
776
|
+
ax_s_yz.set_ylabel('Z')
|
|
777
|
+
ax_s_yz.set_title('Surface YZ (Side View)', fontsize=10)
|
|
778
|
+
ax_s_yz.set_aspect('equal')
|
|
779
|
+
ax_s_yz.grid(True, alpha=0.2)
|
|
780
|
+
|
|
781
|
+
# Extract iteration and set main title
|
|
782
|
+
filename = os.path.basename(grid_files[frame])
|
|
783
|
+
if 'final' in filename:
|
|
784
|
+
fig.suptitle('Grid vs Surface: Final (Converged)',
|
|
785
|
+
fontsize=14, fontweight='bold')
|
|
786
|
+
else:
|
|
787
|
+
iter_num = int(filename.split('_')[-1].replace('.xyz', ''))
|
|
788
|
+
fig.suptitle(f'Grid vs Surface: Iteration {iter_num}',
|
|
789
|
+
fontsize=14)
|
|
790
|
+
|
|
791
|
+
return scatter_grid,
|
|
792
|
+
|
|
793
|
+
# Create initial frame for colorbar
|
|
794
|
+
grid_pos = all_grid_pos[0]
|
|
795
|
+
grid_dens = all_grid_dens[0]
|
|
796
|
+
scatter_grid = ax_g_yz.scatter(grid_pos[:, 1], grid_pos[:, 2],
|
|
797
|
+
c=grid_dens, cmap='plasma', s=1, alpha=alpha_grid,
|
|
798
|
+
vmin=dens_min, vmax=dens_max)
|
|
799
|
+
|
|
800
|
+
# Add colorbar for grid density (spans top row)
|
|
801
|
+
cbar = fig.colorbar(scatter_grid, ax=axes[0, :],
|
|
802
|
+
label='Grid Density', shrink=0.8, pad=0.02,
|
|
803
|
+
orientation='vertical', location='right')
|
|
804
|
+
|
|
805
|
+
print(f"Creating animation with {fps} fps...")
|
|
806
|
+
anim = FuncAnimation(fig, update, frames=len(grid_files),
|
|
807
|
+
interval=1000//fps, blit=False)
|
|
808
|
+
|
|
809
|
+
print(f"Saving to {output_file}...")
|
|
810
|
+
writer = PillowWriter(fps=fps)
|
|
811
|
+
anim.save(output_file, writer=writer)
|
|
812
|
+
plt.close()
|
|
813
|
+
print(f"✓ Animation saved to: {output_file}")
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def plot_density_evolution(export_folder='vderm_exports',
|
|
817
|
+
grid_folder='vderm_grid',
|
|
818
|
+
output_file='density_evolution.png'):
|
|
819
|
+
"""
|
|
820
|
+
Plot how mean and max density evolve over iterations.
|
|
821
|
+
|
|
822
|
+
Parameters
|
|
823
|
+
----------
|
|
824
|
+
export_folder : str
|
|
825
|
+
Base export folder
|
|
826
|
+
grid_folder : str
|
|
827
|
+
Subfolder containing grid exports
|
|
828
|
+
output_file : str
|
|
829
|
+
Output plot filename
|
|
830
|
+
|
|
831
|
+
Examples
|
|
832
|
+
--------
|
|
833
|
+
>>> plot_density_evolution('my_exports')
|
|
834
|
+
"""
|
|
835
|
+
|
|
836
|
+
pattern = os.path.join(export_folder, grid_folder, 'grid_iteration_*.xyz')
|
|
837
|
+
files = sorted(glob.glob(pattern))
|
|
838
|
+
|
|
839
|
+
if len(files) == 0:
|
|
840
|
+
raise FileNotFoundError(f"No grid files found at {pattern}")
|
|
841
|
+
|
|
842
|
+
iterations = []
|
|
843
|
+
mean_densities = []
|
|
844
|
+
max_densities = []
|
|
845
|
+
min_densities = []
|
|
846
|
+
std_densities = []
|
|
847
|
+
|
|
848
|
+
for f in tqdm(files, desc="Analyzing density"):
|
|
849
|
+
pos, norm, densities = read_xyz(f)
|
|
850
|
+
|
|
851
|
+
# Extract iteration number
|
|
852
|
+
filename = os.path.basename(f)
|
|
853
|
+
iter_num = int(filename.split('_')[-1].replace('.xyz', ''))
|
|
854
|
+
|
|
855
|
+
iterations.append(iter_num)
|
|
856
|
+
mean_densities.append(densities.mean())
|
|
857
|
+
max_densities.append(densities.max())
|
|
858
|
+
min_densities.append(densities.min())
|
|
859
|
+
std_densities.append(densities.std())
|
|
860
|
+
|
|
861
|
+
# Create plot
|
|
862
|
+
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8))
|
|
863
|
+
|
|
864
|
+
# Top: Mean, min, max
|
|
865
|
+
ax1.plot(iterations, mean_densities, 'b-', label='Mean', linewidth=2)
|
|
866
|
+
ax1.plot(iterations, max_densities, 'r--', label='Max', linewidth=1.5)
|
|
867
|
+
ax1.plot(iterations, min_densities, 'g--', label='Min', linewidth=1.5)
|
|
868
|
+
ax1.fill_between(iterations, min_densities, max_densities, alpha=0.2)
|
|
869
|
+
ax1.set_xlabel('Iteration')
|
|
870
|
+
ax1.set_ylabel('Density')
|
|
871
|
+
ax1.set_title('Density Statistics Over Time')
|
|
872
|
+
ax1.legend()
|
|
873
|
+
ax1.grid(True, alpha=0.3)
|
|
874
|
+
|
|
875
|
+
# Bottom: Standard deviation
|
|
876
|
+
ax2.plot(iterations, std_densities, 'purple', linewidth=2)
|
|
877
|
+
ax2.set_xlabel('Iteration')
|
|
878
|
+
ax2.set_ylabel('Density Std Dev')
|
|
879
|
+
ax2.set_title('Density Variation Over Time')
|
|
880
|
+
ax2.grid(True, alpha=0.3)
|
|
881
|
+
|
|
882
|
+
plt.tight_layout()
|
|
883
|
+
plt.savefig(output_file, dpi=150, bbox_inches='tight')
|
|
884
|
+
plt.close()
|
|
885
|
+
|
|
886
|
+
print(f"✓ Density evolution plot saved to: {output_file}")
|
|
887
|
+
|
|
888
|
+
def export_grid_to_paraview(export_folder='vderm_exports',
|
|
889
|
+
subfolder='vderm_grid',
|
|
890
|
+
output_folder='paraview_grid'):
|
|
891
|
+
"""
|
|
892
|
+
Export grid data to VTK format for visualization in ParaView.
|
|
893
|
+
|
|
894
|
+
Exports grid node positions with all available attributes:
|
|
895
|
+
velocity vectors and density values (if present).
|
|
896
|
+
|
|
897
|
+
Grid files are expected to have format: x y z v_x v_y v_z rho
|
|
898
|
+
|
|
899
|
+
Parameters
|
|
900
|
+
----------
|
|
901
|
+
export_folder : str, default='vderm_exports'
|
|
902
|
+
Base export folder
|
|
903
|
+
subfolder : str, default='vderm_grid'
|
|
904
|
+
Subfolder containing grid exports
|
|
905
|
+
output_folder : str, default='paraview_grid'
|
|
906
|
+
Folder to save VTK files
|
|
907
|
+
|
|
908
|
+
Returns
|
|
909
|
+
-------
|
|
910
|
+
None
|
|
911
|
+
VTK files saved to output_folder
|
|
912
|
+
|
|
913
|
+
Examples
|
|
914
|
+
--------
|
|
915
|
+
>>> export_grid_to_paraview('my_exports')
|
|
916
|
+
>>> # Then in ParaView: File → Open → select .vtk files
|
|
917
|
+
>>> # Color by 'density' to see density field evolution
|
|
918
|
+
>>> # Apply 'Glyph' filter with 'velocity' to visualize flow field
|
|
919
|
+
"""
|
|
920
|
+
|
|
921
|
+
pattern = os.path.join(export_folder, subfolder, '*.xyz')
|
|
922
|
+
files = sorted(glob.glob(pattern))
|
|
923
|
+
|
|
924
|
+
if len(files) == 0:
|
|
925
|
+
raise FileNotFoundError(
|
|
926
|
+
f"No grid files found at {pattern}\n"
|
|
927
|
+
f"Make sure you ran run_VDERM_with_tracking with export_grid=True"
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
os.makedirs(output_folder, exist_ok=True)
|
|
931
|
+
|
|
932
|
+
for f in tqdm(files, desc="Converting grid to VTK"):
|
|
933
|
+
# Read grid data (should have positions + velocities + densities)
|
|
934
|
+
positions, velocities, densities = read_xyz(f)
|
|
935
|
+
|
|
936
|
+
# Check what data is available
|
|
937
|
+
has_velocities = velocities is not None
|
|
938
|
+
has_densities = densities is not None
|
|
939
|
+
|
|
940
|
+
if not has_velocities:
|
|
941
|
+
warnings.warn(
|
|
942
|
+
f"File {f} does not contain velocity vectors. "
|
|
943
|
+
"Grid exports should have 7 columns (x y z v_x v_y v_z rho)."
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
if not has_densities:
|
|
947
|
+
warnings.warn(
|
|
948
|
+
f"File {f} does not contain density data. "
|
|
949
|
+
"Grid exports should have 7 columns (x y z v_x v_y v_z rho)."
|
|
950
|
+
)
|
|
951
|
+
|
|
952
|
+
# Create output filename
|
|
953
|
+
basename = os.path.basename(f).replace('.xyz', '.vtk')
|
|
954
|
+
output_path = os.path.join(output_folder, basename)
|
|
955
|
+
|
|
956
|
+
# Write VTK file with all available attributes
|
|
957
|
+
with open(output_path, 'w') as vtk:
|
|
958
|
+
vtk.write('# vtk DataFile Version 3.0\n')
|
|
959
|
+
vtk.write('VDERM Grid Data\n')
|
|
960
|
+
vtk.write('ASCII\n')
|
|
961
|
+
vtk.write('DATASET POLYDATA\n')
|
|
962
|
+
|
|
963
|
+
# Write vertices
|
|
964
|
+
vtk.write(f'POINTS {len(positions)} float\n')
|
|
965
|
+
for pos in positions:
|
|
966
|
+
vtk.write(f'{pos[0]:.6e} {pos[1]:.6e} {pos[2]:.6e}\n')
|
|
967
|
+
|
|
968
|
+
# Write point data (only if we have velocities or densities)
|
|
969
|
+
if has_velocities or has_densities:
|
|
970
|
+
vtk.write(f'\nPOINT_DATA {len(positions)}\n')
|
|
971
|
+
|
|
972
|
+
# Add velocities as vector data
|
|
973
|
+
if has_velocities:
|
|
974
|
+
vtk.write('VECTORS velocity float\n')
|
|
975
|
+
for vel in velocities:
|
|
976
|
+
vtk.write(f'{vel[0]:.6e} {vel[1]:.6e} {vel[2]:.6e}\n')
|
|
977
|
+
|
|
978
|
+
# Add density as scalar data
|
|
979
|
+
if has_densities:
|
|
980
|
+
vtk.write('\nSCALARS density float 1\n')
|
|
981
|
+
vtk.write('LOOKUP_TABLE default\n')
|
|
982
|
+
for dens in densities:
|
|
983
|
+
vtk.write(f'{dens:.6e}\n')
|
|
984
|
+
|
|
985
|
+
# Print helpful summary
|
|
986
|
+
print(f"✓ Exported {len(files)} grid files to {output_folder}/")
|
|
987
|
+
print(f" Open ParaView and load the .vtk files to visualize")
|
|
988
|
+
|
|
989
|
+
if has_densities:
|
|
990
|
+
print(f" Tip: Color by 'density' to see density field evolution")
|
|
991
|
+
if has_velocities:
|
|
992
|
+
print(f" Tip: Apply 'Glyph' filter with 'velocity' to visualize flow field")
|
|
993
|
+
print(f" Or use 'Stream Tracer' to show particle trajectories")
|
|
994
|
+
|
|
995
|
+
def export_surface_to_paraview(export_folder='vderm_exports',
|
|
996
|
+
subfolder='vderm_surface',
|
|
997
|
+
output_folder='paraview_surface'):
|
|
998
|
+
"""
|
|
999
|
+
Export surface point clouds to VTK format for ParaView.
|
|
1000
|
+
|
|
1001
|
+
Exports surface point clouds with all available attributes:
|
|
1002
|
+
normal vectors and density values (if present).
|
|
1003
|
+
|
|
1004
|
+
Surface files are expected to have format: x y z n_x n_y n_z rho
|
|
1005
|
+
|
|
1006
|
+
Parameters
|
|
1007
|
+
----------
|
|
1008
|
+
export_folder : str, default='vderm_exports'
|
|
1009
|
+
Base export folder
|
|
1010
|
+
subfolder : str, default='vderm_surface'
|
|
1011
|
+
Subfolder containing surface exports
|
|
1012
|
+
output_folder : str, default='paraview_surface'
|
|
1013
|
+
Folder to save VTK files
|
|
1014
|
+
|
|
1015
|
+
Returns
|
|
1016
|
+
-------
|
|
1017
|
+
None
|
|
1018
|
+
VTK files saved to output_folder
|
|
1019
|
+
|
|
1020
|
+
Examples
|
|
1021
|
+
--------
|
|
1022
|
+
>>> export_surface_to_paraview('my_exports')
|
|
1023
|
+
>>> # In ParaView: Load .vtk files
|
|
1024
|
+
>>> # Color by 'density' to see density distribution on surface
|
|
1025
|
+
>>> # Apply 'Glyph' filter to show normal vectors
|
|
1026
|
+
"""
|
|
1027
|
+
|
|
1028
|
+
pattern = os.path.join(export_folder, subfolder, '*.xyz')
|
|
1029
|
+
files = sorted(glob.glob(pattern))
|
|
1030
|
+
|
|
1031
|
+
if len(files) == 0:
|
|
1032
|
+
raise FileNotFoundError(
|
|
1033
|
+
f"No surface files found at {pattern}\n"
|
|
1034
|
+
f"Make sure you ran run_VDERM_with_tracking with export_surface=True"
|
|
1035
|
+
)
|
|
1036
|
+
|
|
1037
|
+
os.makedirs(output_folder, exist_ok=True)
|
|
1038
|
+
|
|
1039
|
+
for f in tqdm(files, desc="Converting surface to VTK"):
|
|
1040
|
+
# Read surface data (should have positions + normals + densities)
|
|
1041
|
+
positions, normals, densities = read_xyz(f)
|
|
1042
|
+
|
|
1043
|
+
# Check what data is available
|
|
1044
|
+
has_normals = normals is not None
|
|
1045
|
+
has_densities = densities is not None
|
|
1046
|
+
|
|
1047
|
+
if not has_normals:
|
|
1048
|
+
warnings.warn(
|
|
1049
|
+
f"File {f} does not contain normal vectors. "
|
|
1050
|
+
"Surface exports should have 7 columns (x y z n_x n_y n_z rho)."
|
|
1051
|
+
)
|
|
1052
|
+
|
|
1053
|
+
if not has_densities:
|
|
1054
|
+
warnings.warn(
|
|
1055
|
+
f"File {f} does not contain density data. "
|
|
1056
|
+
"Surface exports should have 7 columns (x y z n_x n_y n_z rho)."
|
|
1057
|
+
)
|
|
1058
|
+
|
|
1059
|
+
# Create output filename
|
|
1060
|
+
basename = os.path.basename(f).replace('.xyz', '.vtk')
|
|
1061
|
+
output_path = os.path.join(output_folder, basename)
|
|
1062
|
+
|
|
1063
|
+
# Write VTK file with all available attributes
|
|
1064
|
+
with open(output_path, 'w') as vtk:
|
|
1065
|
+
vtk.write('# vtk DataFile Version 3.0\n')
|
|
1066
|
+
vtk.write('VDERM Surface Point Cloud\n')
|
|
1067
|
+
vtk.write('ASCII\n')
|
|
1068
|
+
vtk.write('DATASET POLYDATA\n')
|
|
1069
|
+
|
|
1070
|
+
# Write vertices
|
|
1071
|
+
vtk.write(f'POINTS {len(positions)} float\n')
|
|
1072
|
+
for pos in positions:
|
|
1073
|
+
vtk.write(f'{pos[0]:.6e} {pos[1]:.6e} {pos[2]:.6e}\n')
|
|
1074
|
+
|
|
1075
|
+
# Write point data (only if we have normals or densities)
|
|
1076
|
+
if has_normals or has_densities:
|
|
1077
|
+
vtk.write(f'\nPOINT_DATA {len(positions)}\n')
|
|
1078
|
+
|
|
1079
|
+
# Add normals as vector data
|
|
1080
|
+
if has_normals:
|
|
1081
|
+
vtk.write('NORMALS normals float\n')
|
|
1082
|
+
for norm in normals:
|
|
1083
|
+
vtk.write(f'{norm[0]:.6e} {norm[1]:.6e} {norm[2]:.6e}\n')
|
|
1084
|
+
|
|
1085
|
+
# Add density as scalar data
|
|
1086
|
+
if has_densities:
|
|
1087
|
+
vtk.write('\nSCALARS density float 1\n')
|
|
1088
|
+
vtk.write('LOOKUP_TABLE default\n')
|
|
1089
|
+
for dens in densities:
|
|
1090
|
+
vtk.write(f'{dens:.6e}\n')
|
|
1091
|
+
|
|
1092
|
+
# Print summary
|
|
1093
|
+
print(f"✓ Exported {len(files)} surface files to {output_folder}/")
|
|
1094
|
+
print(f" Open ParaView and load the .vtk files to visualize")
|
|
1095
|
+
|
|
1096
|
+
if has_densities:
|
|
1097
|
+
print(f" Tip: Color by 'density' to see density distribution on surface")
|
|
1098
|
+
if has_normals:
|
|
1099
|
+
print(f" Tip: Apply 'Glyph' filter with 'normals' to visualize normal vectors")
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
def export_meshes_to_paraview(export_folder='vderm_exports',
|
|
1103
|
+
subfolder='vderm_mesh',
|
|
1104
|
+
output_folder='paraview_meshes'):
|
|
1105
|
+
"""
|
|
1106
|
+
Convert STL meshes to VTK format for ParaView.
|
|
1107
|
+
|
|
1108
|
+
**Note:** This function converts existing STL files to VTK but cannot include
|
|
1109
|
+
density data. For ParaView visualization with density vertex attributes,
|
|
1110
|
+
use run_VDERM_with_tracking() with mesh_format='vtk' instead.
|
|
1111
|
+
|
|
1112
|
+
Exports reconstructed surface meshes with vertex normals. ParaView can render
|
|
1113
|
+
these with better quality than matplotlib and allows interactive exploration.
|
|
1114
|
+
|
|
1115
|
+
Parameters
|
|
1116
|
+
----------
|
|
1117
|
+
export_folder : str, default='vderm_exports'
|
|
1118
|
+
Base export folder
|
|
1119
|
+
subfolder : str, default='vderm_mesh'
|
|
1120
|
+
Subfolder containing mesh exports (.stl files)
|
|
1121
|
+
output_folder : str, default='paraview_meshes'
|
|
1122
|
+
Folder to save VTK mesh files
|
|
1123
|
+
|
|
1124
|
+
Returns
|
|
1125
|
+
-------
|
|
1126
|
+
None
|
|
1127
|
+
VTK files saved to output_folder
|
|
1128
|
+
|
|
1129
|
+
See Also
|
|
1130
|
+
--------
|
|
1131
|
+
run_VDERM_with_tracking : Use mesh_format='vtk' for meshes with density data
|
|
1132
|
+
|
|
1133
|
+
Examples
|
|
1134
|
+
--------
|
|
1135
|
+
>>> # Convert existing STL meshes to VTK (without density)
|
|
1136
|
+
>>> export_meshes_to_paraview('my_exports')
|
|
1137
|
+
>>> # In ParaView: File → Open → select .vtk files → Apply
|
|
1138
|
+
|
|
1139
|
+
>>> # Preferred: Generate VTK meshes with density directly
|
|
1140
|
+
>>> run_VDERM_with_tracking(grid, surface_pts, normals,
|
|
1141
|
+
... export_mesh=True,
|
|
1142
|
+
... mesh_format='vtk') # Better for ParaView!
|
|
1143
|
+
"""
|
|
1144
|
+
|
|
1145
|
+
# Print recommendation
|
|
1146
|
+
print("\n" + "="*70)
|
|
1147
|
+
print("NOTE: For ParaView visualization with density vertex attributes,")
|
|
1148
|
+
print("the recommended approach is to use run_VDERM_with_tracking()")
|
|
1149
|
+
print("with mesh_format='vtk' instead of converting STL files.")
|
|
1150
|
+
print()
|
|
1151
|
+
print("Example:")
|
|
1152
|
+
print(" run_VDERM_with_tracking(grid, surface_pts, normals,")
|
|
1153
|
+
print(" export_mesh=True,")
|
|
1154
|
+
print(" mesh_format='vtk')")
|
|
1155
|
+
print()
|
|
1156
|
+
print("This function converts existing STL → VTK but cannot include density.")
|
|
1157
|
+
print("="*70 + "\n")
|
|
1158
|
+
|
|
1159
|
+
if not HAS_PYMESHLAB:
|
|
1160
|
+
raise ImportError(
|
|
1161
|
+
"This function requires pymeshlab for mesh conversion.\n"
|
|
1162
|
+
"Install with: pip install pymeshlab"
|
|
1163
|
+
)
|
|
1164
|
+
import pymeshlab as ml
|
|
1165
|
+
|
|
1166
|
+
pattern = os.path.join(export_folder, subfolder, '*.stl')
|
|
1167
|
+
files = sorted(glob.glob(pattern))
|
|
1168
|
+
|
|
1169
|
+
if len(files) == 0:
|
|
1170
|
+
raise FileNotFoundError(
|
|
1171
|
+
f"No mesh files found at {pattern}\n"
|
|
1172
|
+
f"Make sure you ran run_VDERM_with_tracking with export_mesh=True"
|
|
1173
|
+
)
|
|
1174
|
+
|
|
1175
|
+
os.makedirs(output_folder, exist_ok=True)
|
|
1176
|
+
|
|
1177
|
+
for f in tqdm(files, desc="Converting STL to VTK"):
|
|
1178
|
+
# Load mesh with pymeshlab
|
|
1179
|
+
ms = ml.MeshSet()
|
|
1180
|
+
ms.load_new_mesh(f)
|
|
1181
|
+
|
|
1182
|
+
# Get mesh data
|
|
1183
|
+
mesh = ms.current_mesh()
|
|
1184
|
+
vertices = mesh.vertex_matrix()
|
|
1185
|
+
faces = mesh.face_matrix()
|
|
1186
|
+
|
|
1187
|
+
# Compute normals
|
|
1188
|
+
ms.compute_normal_for_point_clouds()
|
|
1189
|
+
normals = mesh.vertex_normal_matrix()
|
|
1190
|
+
|
|
1191
|
+
# Create output filename
|
|
1192
|
+
basename = os.path.basename(f).replace('.stl', '.vtk')
|
|
1193
|
+
output_path = os.path.join(output_folder, basename)
|
|
1194
|
+
|
|
1195
|
+
# Write VTK POLYDATA file (geometry + normals only, no density)
|
|
1196
|
+
with open(output_path, 'w') as vtk:
|
|
1197
|
+
vtk.write('# vtk DataFile Version 3.0\n')
|
|
1198
|
+
vtk.write('VDERM Mesh (converted from STL, no density data)\n')
|
|
1199
|
+
vtk.write('ASCII\n')
|
|
1200
|
+
vtk.write('DATASET POLYDATA\n')
|
|
1201
|
+
|
|
1202
|
+
# Write vertices
|
|
1203
|
+
vtk.write(f'POINTS {len(vertices)} float\n')
|
|
1204
|
+
for v in vertices:
|
|
1205
|
+
vtk.write(f'{v[0]:.6e} {v[1]:.6e} {v[2]:.6e}\n')
|
|
1206
|
+
|
|
1207
|
+
# Write triangular faces
|
|
1208
|
+
vtk.write(f'\nPOLYGONS {len(faces)} {len(faces) * 4}\n')
|
|
1209
|
+
for face in faces:
|
|
1210
|
+
vtk.write(f'3 {face[0]} {face[1]} {face[2]}\n')
|
|
1211
|
+
|
|
1212
|
+
# Write normals as vector data
|
|
1213
|
+
vtk.write(f'\nPOINT_DATA {len(vertices)}\n')
|
|
1214
|
+
vtk.write('NORMALS normals float\n')
|
|
1215
|
+
for n in normals:
|
|
1216
|
+
vtk.write(f'{n[0]:.6e} {n[1]:.6e} {n[2]:.6e}\n')
|
|
1217
|
+
|
|
1218
|
+
print(f"\n✓ Exported {len(files)} mesh files to {output_folder}/")
|
|
1219
|
+
print(f" Open ParaView and load the .vtk files to visualize")
|
|
1220
|
+
print(f" Tip: In ParaView, use 'Surface With Edges' for best visualization")
|
|
1221
|
+
print(f"\n⚠ These meshes do NOT contain density data.")
|
|
1222
|
+
print(f" To get density coloring in ParaView, use mesh_format='vtk'")
|
|
1223
|
+
print(f" in run_VDERM_with_tracking() instead.\n")
|
|
1224
|
+
|
|
1225
|
+
def export_all_to_paraview(export_folder='vderm_exports',
|
|
1226
|
+
output_base='paraview_exports'):
|
|
1227
|
+
"""
|
|
1228
|
+
Export all available data (grid, surface, meshes) to ParaView format.
|
|
1229
|
+
|
|
1230
|
+
Convenience function that exports everything that's available.
|
|
1231
|
+
|
|
1232
|
+
**Note:** If meshes were exported as .stl files, they will be converted
|
|
1233
|
+
to VTK but will not contain density data. For meshes with density coloring
|
|
1234
|
+
in ParaView, use run_VDERM_with_tracking() with mesh_format='vtk'.
|
|
1235
|
+
|
|
1236
|
+
Parameters
|
|
1237
|
+
----------
|
|
1238
|
+
export_folder : str, default='vderm_exports'
|
|
1239
|
+
Base export folder from run_VDERM_with_tracking
|
|
1240
|
+
output_base : str, default='paraview_exports'
|
|
1241
|
+
Base folder for ParaView exports
|
|
1242
|
+
|
|
1243
|
+
Examples
|
|
1244
|
+
--------
|
|
1245
|
+
>>> export_all_to_paraview('my_deformation')
|
|
1246
|
+
>>> # Creates paraview_exports/grid/, surface/, and meshes/
|
|
1247
|
+
"""
|
|
1248
|
+
|
|
1249
|
+
if not HAS_PYMESHLAB:
|
|
1250
|
+
raise ImportError(
|
|
1251
|
+
"This function requires pymeshlab for mesh conversion.\n"
|
|
1252
|
+
"Install with: pip install pymeshlab"
|
|
1253
|
+
)
|
|
1254
|
+
import pymeshlab as ml
|
|
1255
|
+
|
|
1256
|
+
exported = []
|
|
1257
|
+
|
|
1258
|
+
# Check what's available and export
|
|
1259
|
+
grid_path = os.path.join(export_folder, 'vderm_grid')
|
|
1260
|
+
if os.path.exists(grid_path) and len(glob.glob(os.path.join(grid_path, '*.xyz'))) > 0:
|
|
1261
|
+
output = os.path.join(output_base, 'grid')
|
|
1262
|
+
export_grid_to_paraview(export_folder, 'vderm_grid', output)
|
|
1263
|
+
exported.append('grid')
|
|
1264
|
+
|
|
1265
|
+
surface_path = os.path.join(export_folder, 'vderm_surface')
|
|
1266
|
+
if os.path.exists(surface_path) and len(glob.glob(os.path.join(surface_path, '*.xyz'))) > 0:
|
|
1267
|
+
output = os.path.join(output_base, 'surface')
|
|
1268
|
+
export_surface_to_paraview(export_folder, 'vderm_surface', output)
|
|
1269
|
+
exported.append('surface')
|
|
1270
|
+
|
|
1271
|
+
# Check for VTK meshes first, then STL
|
|
1272
|
+
mesh_path = os.path.join(export_folder, 'vderm_mesh')
|
|
1273
|
+
if os.path.exists(mesh_path):
|
|
1274
|
+
vtk_meshes = glob.glob(os.path.join(mesh_path, '*.vtk'))
|
|
1275
|
+
stl_meshes = glob.glob(os.path.join(mesh_path, '*.stl'))
|
|
1276
|
+
|
|
1277
|
+
if len(vtk_meshes) > 0:
|
|
1278
|
+
# VTK meshes already exist - just copy them
|
|
1279
|
+
output = os.path.join(output_base, 'meshes')
|
|
1280
|
+
os.makedirs(output, exist_ok=True)
|
|
1281
|
+
import shutil
|
|
1282
|
+
for vtk_file in vtk_meshes:
|
|
1283
|
+
shutil.copy(vtk_file, output)
|
|
1284
|
+
print(f"✓ Copied {len(vtk_meshes)} VTK mesh files (with density data)")
|
|
1285
|
+
exported.append('meshes (VTK with density)')
|
|
1286
|
+
|
|
1287
|
+
elif len(stl_meshes) > 0:
|
|
1288
|
+
# Convert STL to VTK (without density)
|
|
1289
|
+
output = os.path.join(output_base, 'meshes')
|
|
1290
|
+
export_meshes_to_paraview(export_folder, 'vderm_mesh', output)
|
|
1291
|
+
exported.append('meshes (converted from STL, no density)')
|
|
1292
|
+
|
|
1293
|
+
if not exported:
|
|
1294
|
+
print("No VDERM exports found to convert!")
|
|
1295
|
+
else:
|
|
1296
|
+
print(f"\n✓ Exported {', '.join(exported)} to {output_base}/")
|
|
1297
|
+
print("\nTo visualize in ParaView:")
|
|
1298
|
+
print(" 1. Open ParaView")
|
|
1299
|
+
print(" 2. File → Open → Navigate to paraview_exports/")
|
|
1300
|
+
print(" 3. Select all .vtk files in a folder")
|
|
1301
|
+
print(" 4. Click 'Apply' in the Properties panel")
|
|
1302
|
+
print(" 5. Use the time slider to animate through iterations")
|
|
1303
|
+
|
|
1304
|
+
if 'meshes (converted from STL, no density)' in exported:
|
|
1305
|
+
print("\n⚠ Meshes converted from STL do not contain density data.")
|
|
1306
|
+
print(" For density coloring, re-run with mesh_format='vtk'")
|
|
1307
|
+
|
|
1308
|
+
|
|
1309
|
+
def plot_pcd(positions, densities=None, normals=None,
|
|
1310
|
+
title='Point Cloud',
|
|
1311
|
+
cmap='plasma',
|
|
1312
|
+
figsize=(12, 9),
|
|
1313
|
+
show_normals=False,
|
|
1314
|
+
normal_scale=0.05,
|
|
1315
|
+
point_size=1,
|
|
1316
|
+
alpha=0.6,
|
|
1317
|
+
view='3d',
|
|
1318
|
+
save_file=None):
|
|
1319
|
+
"""
|
|
1320
|
+
Plot a single point cloud with optional density coloring and normal vectors.
|
|
1321
|
+
|
|
1322
|
+
Can display as 3D view or three 2D orthogonal projections.
|
|
1323
|
+
|
|
1324
|
+
Parameters
|
|
1325
|
+
----------
|
|
1326
|
+
positions : ndarray, shape (n_points, 3)
|
|
1327
|
+
Point positions [x, y, z]
|
|
1328
|
+
densities : ndarray, shape (n_points,), optional
|
|
1329
|
+
Density values for color coding. If None, uses uniform color.
|
|
1330
|
+
normals : ndarray, shape (n_points, 3), optional
|
|
1331
|
+
Normal or velocity vectors to display
|
|
1332
|
+
title : str, default='Point Cloud'
|
|
1333
|
+
Plot title
|
|
1334
|
+
cmap : str, default='plasma'
|
|
1335
|
+
Colormap for density visualization
|
|
1336
|
+
figsize : tuple, default=(12, 9)
|
|
1337
|
+
Figure size in inches
|
|
1338
|
+
show_normals : bool, default=False
|
|
1339
|
+
If True and normals provided, draw vector arrows
|
|
1340
|
+
normal_scale : float, default=0.05
|
|
1341
|
+
Scale factor for normal vector arrows (as fraction of domain size)
|
|
1342
|
+
point_size : float, default=1
|
|
1343
|
+
Size of points in scatter plot
|
|
1344
|
+
alpha : float, default=0.6
|
|
1345
|
+
Point transparency (0=transparent, 1=opaque)
|
|
1346
|
+
view : str, default='3d'
|
|
1347
|
+
View type: '3d' for 3D plot, '2d' for three orthogonal projections
|
|
1348
|
+
save_file : str, optional
|
|
1349
|
+
If provided, save plot to this file instead of showing
|
|
1350
|
+
|
|
1351
|
+
Returns
|
|
1352
|
+
-------
|
|
1353
|
+
fig : matplotlib Figure
|
|
1354
|
+
The figure object
|
|
1355
|
+
|
|
1356
|
+
Examples
|
|
1357
|
+
--------
|
|
1358
|
+
>>> # Simple 3D plot
|
|
1359
|
+
>>> positions = np.random.rand(1000, 3)
|
|
1360
|
+
>>> plot_pcd(positions)
|
|
1361
|
+
|
|
1362
|
+
>>> # With density coloring
|
|
1363
|
+
>>> densities = np.random.rand(1000)
|
|
1364
|
+
>>> plot_pcd(positions, densities=densities, cmap='viridis')
|
|
1365
|
+
|
|
1366
|
+
>>> # 2D projections with density
|
|
1367
|
+
>>> plot_pcd(positions, densities=densities, view='2d')
|
|
1368
|
+
|
|
1369
|
+
>>> # Show normal vectors
|
|
1370
|
+
>>> normals = np.random.randn(1000, 3)
|
|
1371
|
+
>>> plot_pcd(positions, normals=normals, show_normals=True)
|
|
1372
|
+
|
|
1373
|
+
>>> # Save to file
|
|
1374
|
+
>>> plot_pcd(positions, densities=densities, save_file='myplot.png')
|
|
1375
|
+
"""
|
|
1376
|
+
|
|
1377
|
+
# Get bounds
|
|
1378
|
+
pos_min = positions.min(axis=0)
|
|
1379
|
+
pos_max = positions.max(axis=0)
|
|
1380
|
+
ranges = pos_max - pos_min
|
|
1381
|
+
|
|
1382
|
+
# Compute normal scale if showing normals
|
|
1383
|
+
if show_normals and normals is not None:
|
|
1384
|
+
arrow_length = normal_scale * ranges.mean()
|
|
1385
|
+
|
|
1386
|
+
if view == '3d':
|
|
1387
|
+
# Single 3D plot
|
|
1388
|
+
fig = plt.figure(figsize=figsize)
|
|
1389
|
+
ax = fig.add_subplot(111, projection='3d')
|
|
1390
|
+
|
|
1391
|
+
# Plot points
|
|
1392
|
+
if densities is not None:
|
|
1393
|
+
scatter = ax.scatter(positions[:, 0], positions[:, 1], positions[:, 2],
|
|
1394
|
+
c=densities, cmap=cmap, s=point_size, alpha=alpha)
|
|
1395
|
+
cbar = plt.colorbar(scatter, ax=ax, label='Density', shrink=0.6)
|
|
1396
|
+
else:
|
|
1397
|
+
ax.scatter(positions[:, 0], positions[:, 1], positions[:, 2],
|
|
1398
|
+
c='dodgerblue', s=point_size, alpha=alpha)
|
|
1399
|
+
|
|
1400
|
+
# Show normals if requested
|
|
1401
|
+
if show_normals and normals is not None:
|
|
1402
|
+
# Subsample for clarity
|
|
1403
|
+
step = max(1, len(positions) // 100)
|
|
1404
|
+
ax.quiver(positions[::step, 0], positions[::step, 1], positions[::step, 2],
|
|
1405
|
+
normals[::step, 0], normals[::step, 1], normals[::step, 2],
|
|
1406
|
+
length=arrow_length, color='red', alpha=0.5, arrow_length_ratio=0.3)
|
|
1407
|
+
|
|
1408
|
+
# Set limits and aspect
|
|
1409
|
+
ax.set_xlim(pos_min[0], pos_max[0])
|
|
1410
|
+
ax.set_ylim(pos_min[1], pos_max[1])
|
|
1411
|
+
ax.set_zlim(pos_min[2], pos_max[2])
|
|
1412
|
+
ax.set_box_aspect(ranges)
|
|
1413
|
+
|
|
1414
|
+
ax.set_xlabel('X')
|
|
1415
|
+
ax.set_ylabel('Y')
|
|
1416
|
+
ax.set_zlabel('Z')
|
|
1417
|
+
ax.set_title(title, fontsize=14, fontweight='bold')
|
|
1418
|
+
|
|
1419
|
+
elif view == '2d':
|
|
1420
|
+
# Three orthogonal projections
|
|
1421
|
+
fig, (ax_xy, ax_xz, ax_yz) = plt.subplots(1, 3, figsize=figsize)
|
|
1422
|
+
|
|
1423
|
+
# XY plane
|
|
1424
|
+
if densities is not None:
|
|
1425
|
+
scatter = ax_xy.scatter(positions[:, 0], positions[:, 1],
|
|
1426
|
+
c=densities, cmap=cmap, s=point_size, alpha=alpha)
|
|
1427
|
+
else:
|
|
1428
|
+
ax_xy.scatter(positions[:, 0], positions[:, 1],
|
|
1429
|
+
c='dodgerblue', s=point_size, alpha=alpha)
|
|
1430
|
+
|
|
1431
|
+
ax_xy.set_xlim(pos_min[0], pos_max[0])
|
|
1432
|
+
ax_xy.set_ylim(pos_min[1], pos_max[1])
|
|
1433
|
+
ax_xy.set_xlabel('X')
|
|
1434
|
+
ax_xy.set_ylabel('Y')
|
|
1435
|
+
ax_xy.set_title('XY Plane (Top View)')
|
|
1436
|
+
ax_xy.set_aspect('equal')
|
|
1437
|
+
ax_xy.grid(True, alpha=0.3)
|
|
1438
|
+
|
|
1439
|
+
# XZ plane
|
|
1440
|
+
if densities is not None:
|
|
1441
|
+
ax_xz.scatter(positions[:, 0], positions[:, 2],
|
|
1442
|
+
c=densities, cmap=cmap, s=point_size, alpha=alpha)
|
|
1443
|
+
else:
|
|
1444
|
+
ax_xz.scatter(positions[:, 0], positions[:, 2],
|
|
1445
|
+
c='dodgerblue', s=point_size, alpha=alpha)
|
|
1446
|
+
|
|
1447
|
+
ax_xz.set_xlim(pos_min[0], pos_max[0])
|
|
1448
|
+
ax_xz.set_ylim(pos_min[2], pos_max[2])
|
|
1449
|
+
ax_xz.set_xlabel('X')
|
|
1450
|
+
ax_xz.set_ylabel('Z')
|
|
1451
|
+
ax_xz.set_title('XZ Plane (Front View)')
|
|
1452
|
+
ax_xz.set_aspect('equal')
|
|
1453
|
+
ax_xz.grid(True, alpha=0.3)
|
|
1454
|
+
|
|
1455
|
+
# YZ plane
|
|
1456
|
+
if densities is not None:
|
|
1457
|
+
scatter = ax_yz.scatter(positions[:, 1], positions[:, 2],
|
|
1458
|
+
c=densities, cmap=cmap, s=point_size, alpha=alpha)
|
|
1459
|
+
else:
|
|
1460
|
+
ax_yz.scatter(positions[:, 1], positions[:, 2],
|
|
1461
|
+
c='dodgerblue', s=point_size, alpha=alpha)
|
|
1462
|
+
|
|
1463
|
+
ax_yz.set_xlim(pos_min[1], pos_max[1])
|
|
1464
|
+
ax_yz.set_ylim(pos_min[2], pos_max[2])
|
|
1465
|
+
ax_yz.set_xlabel('Y')
|
|
1466
|
+
ax_yz.set_ylabel('Z')
|
|
1467
|
+
ax_yz.set_title('YZ Plane (Side View)')
|
|
1468
|
+
ax_yz.set_aspect('equal')
|
|
1469
|
+
ax_yz.grid(True, alpha=0.3)
|
|
1470
|
+
|
|
1471
|
+
# Add colorbar if using densities
|
|
1472
|
+
if densities is not None:
|
|
1473
|
+
cbar = fig.colorbar(scatter, ax=[ax_xy, ax_xz, ax_yz],
|
|
1474
|
+
label='Density', shrink=0.8, pad=0.02)
|
|
1475
|
+
|
|
1476
|
+
fig.suptitle(title, fontsize=16, fontweight='bold')
|
|
1477
|
+
# plt.tight_layout()
|
|
1478
|
+
|
|
1479
|
+
else:
|
|
1480
|
+
raise ValueError(f"view must be '3d' or '2d', got '{view}'")
|
|
1481
|
+
|
|
1482
|
+
# Save or show
|
|
1483
|
+
if save_file:
|
|
1484
|
+
plt.savefig(save_file, dpi=150, bbox_inches='tight')
|
|
1485
|
+
plt.close()
|
|
1486
|
+
print(f"✓ Plot saved to: {save_file}")
|
|
1487
|
+
else:
|
|
1488
|
+
plt.show()
|
|
1489
|
+
|
|
1490
|
+
return fig
|