Skip to content

Commit 040560d

Browse files
sbryngelsonclaude
andcommitted
Fix multi-processor assembly and Silo data ordering
Three issues fixed: 1. Silo reader: reinterpret HDF5 data from C row-major to Fortran column-major order so data[i,j,k] maps to (x_i, y_j, z_k) 2. Multi-processor assembly: use per-cell searchsorted + np.ix_ indexing instead of contiguous block placement, correctly handling ghost/buffer cell overlap between processors 3. Renderer: fall back to GIF via Pillow when ffmpeg is unavailable Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3bc0bbe commit 040560d

3 files changed

Lines changed: 84 additions & 139 deletions

File tree

toolchain/mfc/viz/reader.py

Lines changed: 24 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -319,58 +319,22 @@ def assemble(case_dir: str, step: int, fmt: str = 'binary', # pylint: disable=t
319319
z_cc = (pd.z_cb[:-1] + pd.z_cb[1:]) / 2.0 if pd.p > 0 else np.array([0.0])
320320
proc_centers.append((rank, pd, x_cc, y_cc, z_cc))
321321

322-
# Build global coordinate arrays
323-
# For each unique origin in each dimension, accumulate sizes
324-
x_chunks: Dict[float, Tuple[int, np.ndarray]] = {}
325-
y_chunks: Dict[float, Tuple[int, np.ndarray]] = {}
326-
z_chunks: Dict[float, Tuple[int, np.ndarray]] = {}
327-
328-
for rank, pd, x_cc, y_cc, z_cc in proc_centers:
329-
x_key = round(x_cc[0], 12)
330-
y_key = round(y_cc[0], 12) if ndim >= 2 else 0.0
331-
z_key = round(z_cc[0], 12) if ndim >= 3 else 0.0
332-
if x_key not in x_chunks:
333-
x_chunks[x_key] = (len(x_cc), x_cc)
334-
if y_key not in y_chunks:
335-
y_chunks[y_key] = (len(y_cc), y_cc)
336-
if z_key not in z_chunks:
337-
z_chunks[z_key] = (len(z_cc), z_cc)
338-
339-
# Build global coordinate arrays by concatenating sorted chunks
340-
sorted_x_keys = sorted(x_chunks.keys())
341-
sorted_y_keys = sorted(y_chunks.keys())
342-
sorted_z_keys = sorted(z_chunks.keys())
343-
344-
global_x = np.concatenate([x_chunks[k][1] for k in sorted_x_keys])
345-
global_y = np.concatenate([y_chunks[k][1] for k in sorted_y_keys]) if ndim >= 2 else np.array([0.0])
346-
global_z = np.concatenate([z_chunks[k][1] for k in sorted_z_keys]) if ndim >= 3 else np.array([0.0])
347-
348-
# Compute offsets for each origin
349-
x_offsets: Dict[float, int] = {}
350-
off = 0
351-
for k in sorted_x_keys:
352-
x_offsets[k] = off
353-
off += x_chunks[k][0]
354-
355-
y_offsets: Dict[float, int] = {}
356-
off = 0
357-
for k in sorted_y_keys:
358-
y_offsets[k] = off
359-
off += y_chunks[k][0]
360-
361-
z_offsets: Dict[float, int] = {}
362-
off = 0
363-
for k in sorted_z_keys:
364-
z_offsets[k] = off
365-
off += z_chunks[k][0]
366-
367-
# Get all variable names from first processor
368-
varnames = list(proc_data[0][1].variables.keys())
322+
# Build unique sorted global coordinate arrays (handles ghost overlap)
323+
all_x = np.concatenate([xc for _, _, xc, _, _ in proc_centers])
324+
global_x = np.unique(np.round(all_x, 12))
325+
if ndim >= 2:
326+
all_y = np.concatenate([yc for _, _, _, yc, _ in proc_centers])
327+
global_y = np.unique(np.round(all_y, 12))
328+
else:
329+
global_y = np.array([0.0])
330+
if ndim >= 3:
331+
all_z = np.concatenate([zc for _, _, _, _, zc in proc_centers])
332+
global_z = np.unique(np.round(all_z, 12))
333+
else:
334+
global_z = np.array([0.0])
369335

370-
# Allocate global arrays
371-
nx = len(global_x)
372-
ny = len(global_y)
373-
nz = len(global_z)
336+
varnames = list(proc_data[0][1].variables.keys())
337+
nx, ny, nz = len(global_x), len(global_y), len(global_z)
374338

375339
global_vars: Dict[str, np.ndarray] = {}
376340
for vn in varnames:
@@ -381,25 +345,22 @@ def assemble(case_dir: str, step: int, fmt: str = 'binary', # pylint: disable=t
381345
else:
382346
global_vars[vn] = np.zeros(nx)
383347

384-
# Place each processor's data at the correct offset
385-
for rank, pd, x_cc, y_cc, z_cc in proc_centers:
386-
x_key = round(x_cc[0], 12)
387-
y_key = round(y_cc[0], 12) if ndim >= 2 else 0.0
388-
z_key = round(z_cc[0], 12) if ndim >= 3 else 0.0
389-
390-
xi = x_offsets[x_key]
391-
yi = y_offsets[y_key] if ndim >= 2 else 0
392-
zi = z_offsets[z_key] if ndim >= 3 else 0
348+
# Place each processor's data using per-cell coordinate lookup
349+
# (handles ghost/buffer cell overlap between processors)
350+
for _rank, pd, x_cc, y_cc, z_cc in proc_centers:
351+
xi = np.searchsorted(global_x, np.round(x_cc, 12))
352+
yi = np.searchsorted(global_y, np.round(y_cc, 12)) if ndim >= 2 else np.array([0])
353+
zi = np.searchsorted(global_z, np.round(z_cc, 12)) if ndim >= 3 else np.array([0])
393354

394355
for vn, data in pd.variables.items():
395356
if vn not in global_vars:
396357
continue
397358
if ndim == 3:
398-
global_vars[vn][xi:xi + pd.m + 1, yi:yi + pd.n + 1, zi:zi + pd.p + 1] = data
359+
global_vars[vn][np.ix_(xi, yi, zi)] = data
399360
elif ndim == 2:
400-
global_vars[vn][xi:xi + pd.m + 1, yi:yi + pd.n + 1] = data
361+
global_vars[vn][np.ix_(xi, yi)] = data
401362
else:
402-
global_vars[vn][xi:xi + pd.m + 1] = data
363+
global_vars[vn][xi] = data
403364

404365
return AssembledData(
405366
ndim=ndim, x_cc=global_x, y_cc=global_y, z_cc=global_z,

toolchain/mfc/viz/renderer.py

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ def render_mp4(varname, steps, output, fps=10, # pylint: disable=too-many-argum
198198
elif assembled.ndim == 3:
199199
render_3d_slice(assembled, varname, step, frame_path, **opts)
200200

201-
# Combine frames into MP4 using ffmpeg
201+
# Combine frames into MP4 using ffmpeg, or fall back to GIF via Pillow
202202
frame_pattern = os.path.join(viz_dir, '%06d.png')
203203
ffmpeg_cmd = [
204204
'ffmpeg', '-y',
@@ -210,20 +210,41 @@ def render_mp4(varname, steps, output, fps=10, # pylint: disable=too-many-argum
210210
output,
211211
]
212212

213+
success = False
213214
try:
214215
subprocess.run(ffmpeg_cmd, check=True, capture_output=True)
216+
success = True
215217
except FileNotFoundError:
216-
print(f"ffmpeg not found. Frames saved to {viz_dir}/")
217-
print(f"To create video manually: ffmpeg -framerate {fps} "
218-
f"-i {frame_pattern} -c:v libx264 -pix_fmt yuv420p {output}")
219-
return False
218+
pass
220219
except subprocess.CalledProcessError as e:
221220
print(f"ffmpeg failed: {e.stderr.decode()}")
222-
print(f"Frames saved to {viz_dir}/")
223-
return False
221+
222+
if not success:
223+
# Fall back to GIF via Pillow
224+
gif_output = output.rsplit('.', 1)[0] + '.gif'
225+
try:
226+
from PIL import Image # pylint: disable=import-outside-toplevel
227+
frames = []
228+
frame_files = sorted(f for f in os.listdir(viz_dir) if f.endswith('.png'))
229+
for fname in frame_files:
230+
img = Image.open(os.path.join(viz_dir, fname))
231+
frames.append(img.copy())
232+
img.close()
233+
if frames:
234+
duration = max(int(1000 / fps), 1)
235+
frames[0].save(gif_output, save_all=True, append_images=frames[1:],
236+
duration=duration, loop=0)
237+
output = gif_output
238+
success = True
239+
print(f"ffmpeg not found; saved GIF to {gif_output}")
240+
except ImportError:
241+
print(f"Neither ffmpeg nor Pillow available. Frames saved to {viz_dir}/")
242+
print(f"To create video: ffmpeg -framerate {fps} "
243+
f"-i {frame_pattern} -c:v libx264 -pix_fmt yuv420p {output}")
224244

225245
# Clean up frames
226-
for fname in os.listdir(viz_dir):
227-
os.remove(os.path.join(viz_dir, fname))
228-
os.rmdir(viz_dir)
229-
return True
246+
if success:
247+
for fname in os.listdir(viz_dir):
248+
os.remove(os.path.join(viz_dir, fname))
249+
os.rmdir(viz_dir)
250+
return success

toolchain/mfc/viz/silo_reader.py

Lines changed: 28 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,12 @@ def read_silo_file( # pylint: disable=too-many-locals
123123
data_path = attr["value0"]
124124
data = _resolve_path(f, data_path).astype(np.float64)
125125

126-
# Silo stores zone-centered data as (ny, nx) for 2-D — but MFC's
127-
# DBPUTQV1 call passes the array in Fortran column-major order,
128-
# which HDF5 writes row-major. The resulting shape in the file
129-
# is (dims[0], dims[1]) = (nx, ny). We keep it that way so it
130-
# matches the binary reader's (m, n) convention.
126+
# MFC's DBPUTQV1 passes the Fortran column-major array as a
127+
# flat buffer. HDF5 stores it row-major. Reinterpret the
128+
# bytes in Fortran order so data[i,j,k] = value at (x_i,y_j,z_k),
129+
# matching the binary reader convention.
130+
if data.ndim >= 2:
131+
data = np.ascontiguousarray(data).ravel().reshape(data.shape, order='F')
131132
variables[key] = data
132133

133134
return ProcessorData(
@@ -204,6 +205,7 @@ def assemble_silo( # pylint: disable=too-many-locals,too-many-statements,too-ma
204205
sample = proc_data[0][1]
205206
ndim = 1 + (sample.n > 0) + (sample.p > 0)
206207

208+
# Compute cell centers for each processor
207209
proc_centers: list = []
208210
for rank, pd in proc_data:
209211
x_cc = (pd.x_cb[:-1] + pd.x_cb[1:]) / 2.0
@@ -215,52 +217,19 @@ def assemble_silo( # pylint: disable=too-many-locals,too-many-statements,too-ma
215217
)
216218
proc_centers.append((rank, pd, x_cc, y_cc, z_cc))
217219

218-
# Build global coordinate arrays from unique chunks
219-
x_chunks: dict = {}
220-
y_chunks: dict = {}
221-
z_chunks: dict = {}
222-
223-
for _rank, _pd, x_cc, y_cc, z_cc in proc_centers:
224-
xk = round(float(x_cc[0]), 12)
225-
yk = round(float(y_cc[0]), 12) if ndim >= 2 else 0.0
226-
zk = round(float(z_cc[0]), 12) if ndim >= 3 else 0.0
227-
if xk not in x_chunks:
228-
x_chunks[xk] = x_cc
229-
if yk not in y_chunks:
230-
y_chunks[yk] = y_cc
231-
if zk not in z_chunks:
232-
z_chunks[zk] = z_cc
233-
234-
global_x = np.concatenate([x_chunks[k] for k in sorted(x_chunks)])
235-
global_y = (
236-
np.concatenate([y_chunks[k] for k in sorted(y_chunks)])
237-
if ndim >= 2
238-
else np.array([0.0])
239-
)
240-
global_z = (
241-
np.concatenate([z_chunks[k] for k in sorted(z_chunks)])
242-
if ndim >= 3
243-
else np.array([0.0])
244-
)
245-
246-
# Compute offsets for each chunk
247-
x_offsets: dict = {}
248-
off = 0
249-
for k in sorted(x_chunks):
250-
x_offsets[k] = off
251-
off += len(x_chunks[k])
252-
253-
y_offsets: dict = {}
254-
off = 0
255-
for k in sorted(y_chunks):
256-
y_offsets[k] = off
257-
off += len(y_chunks[k])
258-
259-
z_offsets: dict = {}
260-
off = 0
261-
for k in sorted(z_chunks):
262-
z_offsets[k] = off
263-
off += len(z_chunks[k])
220+
# Build unique sorted global coordinate arrays (handles ghost overlap)
221+
all_x = np.concatenate([xc for _, _, xc, _, _ in proc_centers])
222+
global_x = np.unique(np.round(all_x, 12))
223+
if ndim >= 2:
224+
all_y = np.concatenate([yc for _, _, _, yc, _ in proc_centers])
225+
global_y = np.unique(np.round(all_y, 12))
226+
else:
227+
global_y = np.array([0.0])
228+
if ndim >= 3:
229+
all_z = np.concatenate([zc for _, _, _, _, zc in proc_centers])
230+
global_z = np.unique(np.round(all_z, 12))
231+
else:
232+
global_z = np.array([0.0])
264233

265234
varnames = list(proc_data[0][1].variables.keys())
266235
nx, ny, nz = len(global_x), len(global_y), len(global_z)
@@ -274,28 +243,22 @@ def assemble_silo( # pylint: disable=too-many-locals,too-many-statements,too-ma
274243
else:
275244
global_vars[vn] = np.zeros(nx)
276245

246+
# Place each processor's data using per-cell coordinate lookup
247+
# (handles ghost/buffer cell overlap between processors)
277248
for _rank, pd, x_cc, y_cc, z_cc in proc_centers:
278-
xk = round(float(x_cc[0]), 12)
279-
yk = round(float(y_cc[0]), 12) if ndim >= 2 else 0.0
280-
zk = round(float(z_cc[0]), 12) if ndim >= 3 else 0.0
281-
282-
xi = x_offsets[xk]
283-
yi = y_offsets[yk] if ndim >= 2 else 0
284-
zi = z_offsets[zk] if ndim >= 3 else 0
285-
286-
lx = len(x_cc)
287-
ly = len(y_cc) if ndim >= 2 else 1
288-
lz = len(z_cc) if ndim >= 3 else 1
249+
xi = np.searchsorted(global_x, np.round(x_cc, 12))
250+
yi = np.searchsorted(global_y, np.round(y_cc, 12)) if ndim >= 2 else np.array([0])
251+
zi = np.searchsorted(global_z, np.round(z_cc, 12)) if ndim >= 3 else np.array([0])
289252

290253
for vn, data in pd.variables.items():
291254
if vn not in global_vars:
292255
continue
293256
if ndim == 3:
294-
global_vars[vn][xi : xi + lx, yi : yi + ly, zi : zi + lz] = data
257+
global_vars[vn][np.ix_(xi, yi, zi)] = data
295258
elif ndim == 2:
296-
global_vars[vn][xi : xi + lx, yi : yi + ly] = data
259+
global_vars[vn][np.ix_(xi, yi)] = data
297260
else:
298-
global_vars[vn][xi : xi + lx] = data
261+
global_vars[vn][xi] = data
299262

300263
return AssembledData(
301264
ndim=ndim,

0 commit comments

Comments
 (0)