postProcess/contour.py
contour.py
Render per-snapshot panels of the species concentration, the deformation-rate norm and the speed for one ActiveDrops case, in parallel over snapshots.
Run from postProcess/ with a case
directory that contains
intermediate/snapshot-<t> files:
python3 contour.py --caseToProcess ../simulationCases/c1000 --cpus 4The getFacets and getData
helpers are compiled once with qcc before
any snapshot is processed; workers share the read-only
executables. Frames are named by the snapshot time so
serial and parallel runs produce the same set.
Author
Vatsal Sanjay Email: [email protected] Computational Multiphase Physics (CoMPhy) Lab, Durham University Last updated: Sep 9, 2026
import numpy as np
import os
import subprocess as sp
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from matplotlib.collections import LineCollection
from matplotlib.ticker import StrMethodFormatter
import pandas as pd
import multiprocessing as mp
from functools import partial
import sys
import argparse
import shutil
matplotlib.rcParams['font.family'] = 'serif'
# Use LaTeX text rendering only when a latex executable is available; otherwise mathtext.
matplotlib.rcParams['text.usetex'] = shutil.which('latex') is not None
def execute_process(exe):
p = sp.Popen(exe, stdout=sp.PIPE, stderr=sp.PIPE)
stdout, stderr = p.communicate()
return stderr.decode("utf-8").split("\n")
def compile_helpers(helpers=("getFacets", "getData")):Compile the Basilisk snapshot readers once, before any snapshot is processed.
if not shutil.which("qcc"):
sys.exit("qcc not found; source the repository .project_config first.")
# qcc resolves its intermediate files relative to the working directory, so
# compile with relative names from the postProcess directory.
here = os.path.dirname(os.path.abspath(__file__))
for name in helpers:
src = os.path.join(here, f"{name}.c")
exe = os.path.join(here, name)
if os.path.exists(exe) and os.path.getmtime(exe) >= os.path.getmtime(src):
continue
cmd = ["qcc", "-O2", "-Wall", "-disable-dimensions", f"{name}.c", "-o", name, "-lm"]
print("Compiling:", " ".join(cmd), flush=True)
sp.check_call(cmd, cwd=here)
def get_segs(place):
temp2 = execute_process(["./getFacets", place])
segs = []
temp2 = list(filter(None, temp2))
# getFacets prints segment endpoints in pairs of lines; ignore a trailing odd line.
for n1 in range(0, len(temp2) - 1, 2):
temp3 = temp2[n1].split()
temp4 = temp2[n1+1].split()
if len(temp3) < 2 or len(temp4) < 2:
continue
x1, y1 = map(float, temp3[:2])
x2, y2 = map(float, temp4[:2])
segs.append(((x1, y1), (x2, y2)))
return segs
def get_field_values(place, xmin, xmax, ymin, ymax, ny):
temp2 = list(filter(None, execute_process(["./getData", place, str(xmin), str(ymin), str(xmax), str(ymax), str(ny)])))
rows = [line.split() for line in temp2]
if not rows or any(len(r) != 5 for r in rows) or len(rows) % ny != 0:
return None
data = np.array(rows, dtype=float)
nx = data.shape[0] // ny
X = data[:,0].reshape((nx, ny)).transpose()
Y = data[:,1].reshape((nx, ny)).transpose()
T = data[:,2].reshape((nx, ny)).transpose()
D2 = data[:,3].reshape((nx, ny)).transpose()
Vel = data[:,4].reshape((nx, ny)).transpose()
return X, Y, T, D2, Vel, nx
def plot_graphics(t, name, xmin, xmax, ymin, ymax, segs, T, D2, Vel):
fig, axs = plt.subplots(1, 3, figsize=(19.20, 10.80))
# Common attributes for all subplots
for ax in axs:
rect = matplotlib.patches.Rectangle((xmin, ymin), xmax-xmin, ymax-ymin, linewidth=2, edgecolor='k', facecolor='none')
ax.add_patch(rect)
line_segments = LineCollection(segs, linewidths=4, colors='green', linestyle='solid')
ax.add_collection(line_segments)
ax.set_aspect('equal')
ax.set_xlim(xmin, xmax)
ax.set_ylim(ymin, ymax)
ax.axis('off')
# Individual subplots
im0 = axs[0].imshow(T, cmap="coolwarm", interpolation='Bilinear', origin='lower', extent=[xmin, xmax, ymin, ymax], vmax=10.0, vmin=0.0)
axs[0].set_title(r"$\phi, t = %5.4f$" % t, fontsize=20)
fig.colorbar(im0, ax=axs[0], fraction=0.046, pad=0.04)
im1 = axs[1].imshow(D2, cmap="hot", interpolation='Bilinear', origin='lower', extent=[xmin, xmax, ymin, ymax], vmax=1.0, vmin=-3.0)
axs[1].set_title(r"$\|\mathcal{D}_{ij}\|, t = %5.4f$" % t, fontsize=20)
fig.colorbar(im1, ax=axs[1], fraction=0.046, pad=0.04)
im2 = axs[2].imshow(Vel, cmap="Blues", interpolation='Bilinear', origin='lower', extent=[xmin, xmax, ymin, ymax], vmax=10.0, vmin=0.0)
axs[2].set_title(r"$\|V_i\|, t = %5.4f$" % t, fontsize=20)
fig.colorbar(im2, ax=axs[2], fraction=0.046, pad=0.04)
plt.savefig(name, bbox_inches='tight', dpi=300)
plt.close()
def process_file(ti, params):Process a single timestep with all parameters passed as a dictionary
t = params['tSnap'] * ti
place = f"{params['caseToProcess']}/intermediate/snapshot-{t:5.4f}"
name = f"{params['folder']}/{int(t*1000):08d}.png"
if not os.path.exists(place):
print(f"{place} File not found!")
return None
elif os.path.exists(name):
print(f"{name} Image present!")
return None
segs = get_segs(place)
fields = get_field_values(
place,
params['xmin'],
params['xmax'],
params['ymin'],
params['ymax'],
params['ny']
)
if fields is None:
print(f"{place} incomplete field output; skipped")
return None
X, Y, T, D2, Vel, nz = fields
plot_graphics(
t, name,
params['xmin'], params['xmax'],
params['ymin'], params['ymax'],
segs, T, D2, Vel
)
print(f"Processed timestep {ti}")
return nz
def main():
# Set up command-line argument parser
parser = argparse.ArgumentParser(description='Render concentration, deformation-rate and speed panels per snapshot')
parser.add_argument('--cpus', '--CPUs', '--num_workers', dest='cpus', type=int, default=4,
help='Number of parallel workers (default 4)')
parser.add_argument('--tSnap', type=float, default=0.1, help='Snapshot time interval')
parser.add_argument('--L0', type=float, default=10.0, help='Length of the domain')
parser.add_argument('--caseToProcess', type=str, default='../simulationCases/c1000',
help='Case directory containing intermediate/ (default ../simulationCases/c1000)')
parser.add_argument('--folderToSave', type=str, default='Video', help='Folder for the rendered frames')
parser.add_argument('--max-frames', type=int, default=500,
help='Maximum number of snapshots to consider (default 500)')
args = parser.parse_args()
if args.cpus <= 0:
parser.error('--cpus must be a positive integer')
if args.max_frames <= 0:
parser.error('--max-frames must be a positive integer')
os.environ.setdefault('OMP_NUM_THREADS', '1')
compile_helpers()
here = os.path.dirname(os.path.abspath(__file__))
os.chdir(here)
nGFS = args.max_frames
num_workers = min(args.cpus, mp.cpu_count())
folder = args.folderToSave
os.makedirs(folder, exist_ok=True)
# Parameters dictionary
params = {
'ny': 128,
'xmin': -args.L0/2.,
'xmax': args.L0/2.,
'ymin': -args.L0/2.,
'ymax': args.L0/2.,
'lw': 2,
'tSnap': args.tSnap,
'folder': folder,
'caseToProcess': args.caseToProcess
}
# Create a partial function with the parameters
process_func = partial(process_file, params=params)
# Create a pool of workers and process the files
with mp.Pool(processes=num_workers) as pool:
results = pool.map(process_func, range(nGFS))
# Filter out None results and print completion
completed = [r for r in results if r is not None]
print(f"Completed {len(completed)} out of {nGFS} files")
if completed:
print(f"Last known nx value: {completed[-1]}")
if __name__ == "__main__":
main()