Visualization¶
Create plots and visualizations from FDS simulation results.
Overview¶
PyFDS provides tools for creating:
- Time series plots
- Multi-parameter comparisons
- Contour plots
- 3D visualizations
- Animations
- Publication-quality figures
from pyfds.analysis import FDSResults
import matplotlib.pyplot as plt
results = FDSResults('simulation.fds')
temp = results.get_device('TEMP_1')
plt.plot(temp['Time'], temp['Value'])
plt.xlabel('Time (s)')
plt.ylabel('Temperature (°C)')
plt.show()
Basic Plotting¶
Single Device Plot¶
import matplotlib.pyplot as plt
from pyfds.analysis import FDSResults
results = FDSResults('room_fire')
# Get device data
temp = results.get_device('TEMP_CEILING')
# Create plot
plt.figure(figsize=(10, 6))
plt.plot(temp['Time'], temp['Value'], 'b-', linewidth=2)
plt.xlabel('Time (s)', fontsize=12)
plt.ylabel('Temperature (°C)', fontsize=12)
plt.title('Ceiling Temperature vs Time', fontsize=14)
plt.grid(True, alpha=0.3)
plt.savefig('temperature.png', dpi=300, bbox_inches='tight')
plt.show()
Multiple Devices¶
# Plot multiple devices on same axes
devices = ['TEMP_LOW', 'TEMP_MID', 'TEMP_HIGH']
colors = ['blue', 'green', 'red']
labels = ['Floor Level', 'Mid-Height', 'Ceiling']
plt.figure(figsize=(12, 6))
for dev, color, label in zip(devices, colors, labels):
data = results.get_device(dev)
plt.plot(data['Time'], data['Value'], color=color, label=label, linewidth=2)
plt.xlabel('Time (s)', fontsize=12)
plt.ylabel('Temperature (°C)', fontsize=12)
plt.title('Temperature Stratification', fontsize=14)
plt.legend(fontsize=10)
plt.grid(True, alpha=0.3)
plt.savefig('stratification.png', dpi=300, bbox_inches='tight')
plt.show()
Advanced Plots¶
Subplots¶
import matplotlib.pyplot as plt
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# Temperature
temp = results.get_device('TEMP_CEILING')
axes[0, 0].plot(temp['Time'], temp['Value'], 'r-', linewidth=2)
axes[0, 0].set_ylabel('Temperature (°C)')
axes[0, 0].set_title('Temperature')
axes[0, 0].grid(True, alpha=0.3)
# Heat Flux
hf = results.get_device('HF_WALL')
axes[0, 1].plot(hf['Time'], hf['Value'], 'orange', linewidth=2)
axes[0, 1].set_ylabel('Heat Flux (kW/m²)')
axes[0, 1].set_title('Wall Heat Flux')
axes[0, 1].grid(True, alpha=0.3)
# Visibility
vis = results.get_device('VIS')
axes[1, 0].plot(vis['Time'], vis['Value'], 'g-', linewidth=2)
axes[1, 0].set_xlabel('Time (s)')
axes[1, 0].set_ylabel('Visibility (m)')
axes[1, 0].set_title('Visibility')
axes[1, 0].grid(True, alpha=0.3)
# Layer Height
layer = results.get_device('LAYER_HEIGHT')
axes[1, 1].plot(layer['Time'], layer['Value'], 'b-', linewidth=2)
axes[1, 1].set_xlabel('Time (s)')
axes[1, 1].set_ylabel('Layer Height (m)')
axes[1, 1].set_title('Smoke Layer')
axes[1, 1].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('multi_parameter.png', dpi=300, bbox_inches='tight')
plt.show()
Filled Areas¶
# Plot with shaded region
temp = results.get_device('TEMP_CEILING')
plt.figure(figsize=(10, 6))
plt.fill_between(
temp['Time'],
temp['Value'],
60, # Tenability limit
where=(temp['Value'] >= 60),
color='red',
alpha=0.3,
label='Above tenability limit'
)
plt.plot(temp['Time'], temp['Value'], 'b-', linewidth=2, label='Temperature')
plt.axhline(y=60, color='r', linestyle='--', label='Tenability limit (60°C)')
plt.xlabel('Time (s)')
plt.ylabel('Temperature (°C)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.savefig('filled_area.png', dpi=300, bbox_inches='tight')
plt.show()
Parametric Study Visualizations¶
Parameter Sweep Plot¶
import polars as pl
import matplotlib.pyplot as plt
# Collect results from parametric study
hrr_values = [500, 1000, 1500, 2000]
peak_temps = []
for hrr in hrr_values:
res = FDSResults(f'fire_{hrr}')
temp = res.get_device('TEMP_CEILING')
peak_temps.append(temp['Value'].max())
# Create plot
plt.figure(figsize=(10, 6))
plt.plot(hrr_values, peak_temps, 'ro-', linewidth=2, markersize=10)
plt.xlabel('HRRPUA (kW/m²)', fontsize=12)
plt.ylabel('Peak Ceiling Temperature (°C)', fontsize=12)
plt.title('Fire Size Sensitivity', fontsize=14)
plt.grid(True, alpha=0.3)
plt.savefig('sensitivity.png', dpi=300, bbox_inches='tight')
plt.show()
Heatmap¶
import numpy as np
import matplotlib.pyplot as plt
# 2D parameter sweep results
hrr_values = [500, 1000, 1500]
door_widths = [0.75, 1.0, 1.5]
# Collect peak temperatures
results_matrix = np.zeros((len(hrr_values), len(door_widths)))
for i, hrr in enumerate(hrr_values):
for j, width in enumerate(door_widths):
res = FDSResults(f'case_h{hrr}_w{int(width*10)}')
temp = res.get_device('TEMP_CEILING')
results_matrix[i, j] = temp['Value'].max()
# Create heatmap
fig, ax = plt.subplots(figsize=(10, 8))
im = ax.imshow(results_matrix, cmap='hot', aspect='auto')
# Labels
ax.set_xticks(np.arange(len(door_widths)))
ax.set_yticks(np.arange(len(hrr_values)))
ax.set_xticklabels(door_widths)
ax.set_yticklabels(hrr_values)
ax.set_xlabel('Door Width (m)', fontsize=12)
ax.set_ylabel('HRRPUA (kW/m²)', fontsize=12)
ax.set_title('Peak Temperature Heatmap', fontsize=14)
# Colorbar
cbar = plt.colorbar(im, ax=ax)
cbar.set_label('Temperature (°C)', fontsize=12)
# Add values as text
for i in range(len(hrr_values)):
for j in range(len(door_widths)):
text = ax.text(j, i, f'{results_matrix[i, j]:.0f}',
ha="center", va="center", color="white", fontsize=10)
plt.tight_layout()
plt.savefig('heatmap.png', dpi=300, bbox_inches='tight')
plt.show()
3D Visualization¶
Surface Plot¶
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
# Create meshgrid
HRR, WIDTH = np.meshgrid(hrr_values, door_widths)
# 3D surface plot
fig = plt.figure(figsize=(12, 8))
ax = fig.add_subplot(111, projection='3d')
surf = ax.plot_surface(HRR, WIDTH, results_matrix.T, cmap='hot', alpha=0.8)
ax.set_xlabel('HRRPUA (kW/m²)')
ax.set_ylabel('Door Width (m)')
ax.set_zlabel('Peak Temperature (°C)')
ax.set_title('Temperature Response Surface')
fig.colorbar(surf, shrink=0.5)
plt.savefig('surface3d.png', dpi=300, bbox_inches='tight')
plt.show()
Custom Styling¶
Publication Quality¶
import matplotlib.pyplot as plt
import matplotlib as mpl
# Set publication style
mpl.rcParams['font.family'] = 'serif'
mpl.rcParams['font.size'] = 11
mpl.rcParams['axes.linewidth'] = 1.5
mpl.rcParams['xtick.major.width'] = 1.5
mpl.rcParams['ytick.major.width'] = 1.5
mpl.rcParams['legend.frameon'] = True
mpl.rcParams['legend.framealpha'] = 0.9
# Create plot
fig, ax = plt.subplots(figsize=(6, 4))
temp = results.get_device('TEMP_CEILING')
ax.plot(temp['Time'], temp['Value'], 'k-', linewidth=1.5)
ax.set_xlabel('Time (s)')
ax.set_ylabel('Temperature (°C)')
ax.set_title('Ceiling Temperature')
ax.grid(True, linestyle=':', alpha=0.5)
plt.tight_layout()
plt.savefig('publication.pdf', dpi=600, bbox_inches='tight')
plt.savefig('publication.png', dpi=300, bbox_inches='tight')
plt.show()
Custom Color Schemes¶
# Define custom colors
colors = {
'fire': '#FF6B35',
'smoke': '#4A4A4A',
'cool': '#004E89',
'hot': '#F77F00'
}
# Use in plots
fig, ax = plt.subplots(figsize=(10, 6))
for dev, color, label in [('TEMP_LOW', colors['cool'], 'Low'),
('TEMP_MID', colors['smoke'], 'Mid'),
('TEMP_HIGH', colors['hot'], 'High')]:
data = results.get_device(dev)
ax.plot(data['Time'], data['Value'], color=color, label=label, linewidth=2)
ax.set_xlabel('Time (s)')
ax.set_ylabel('Temperature (°C)')
ax.legend()
ax.grid(True, alpha=0.3)
plt.show()
Animations¶
Time-Series Animation¶
import matplotlib.pyplot as plt
import matplotlib.animation as animation
# Get all device data
devices = ['TEMP_1', 'TEMP_2', 'TEMP_3']
data = {dev: results.get_device(dev) for dev in devices}
# Set up figure
fig, ax = plt.subplots(figsize=(10, 6))
lines = {dev: ax.plot([], [], label=dev)[0] for dev in devices}
ax.set_xlim(0, 600)
ax.set_ylim(0, 200)
ax.set_xlabel('Time (s)')
ax.set_ylabel('Temperature (°C)')
ax.legend()
ax.grid(True)
def init():
for line in lines.values():
line.set_data([], [])
return lines.values()
def animate(frame):
for dev, line in lines.items():
subset = data[dev].filter(pl.col('Time') <= frame)
line.set_data(subset['Time'], subset['Value'])
return lines.values()
# Create animation
anim = animation.FuncAnimation(
fig, animate, init_func=init,
frames=range(0, 601, 10), # Every 10 seconds
interval=50, # 50ms between frames
blit=True
)
# Save animation
anim.save('temperature_animation.mp4', writer='ffmpeg', fps=20, dpi=150)
# or anim.save('temperature_animation.gif', writer='pillow', fps=10)
plt.show()
Comparison Plots¶
Before/After Comparison¶
# Compare two simulations
baseline = FDSResults('baseline')
modified = FDSResults('modified')
baseline_temp = baseline.get_device('TEMP_CEILING')
modified_temp = modified.get_device('TEMP_CEILING')
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
# Baseline
ax1.plot(baseline_temp['Time'], baseline_temp['Value'], 'b-', linewidth=2)
ax1.set_xlabel('Time (s)')
ax1.set_ylabel('Temperature (°C)')
ax1.set_title('Baseline')
ax1.grid(True, alpha=0.3)
# Modified
ax2.plot(modified_temp['Time'], modified_temp['Value'], 'r-', linewidth=2)
ax2.set_xlabel('Time (s)')
ax2.set_ylabel('Temperature (°C)')
ax2.set_title('Modified')
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('comparison.png', dpi=300, bbox_inches='tight')
plt.show()
Difference Plot¶
# Calculate difference
import polars as pl
# Merge on time
merged = baseline_temp.join(modified_temp, on='Time', suffix='_mod')
# Calculate difference
diff = merged.with_columns([
(pl.col('Value_mod') - pl.col('Value')).alias('Difference')
])
# Plot difference
plt.figure(figsize=(10, 6))
plt.plot(diff['Time'], diff['Difference'], 'purple', linewidth=2)
plt.axhline(y=0, color='k', linestyle='--', alpha=0.5)
plt.xlabel('Time (s)')
plt.ylabel('Temperature Difference (°C)')
plt.title('Modified - Baseline')
plt.grid(True, alpha=0.3)
plt.savefig('difference.png', dpi=300, bbox_inches='tight')
plt.show()
Best Practices¶
1. Consistent Styling¶
# Define style once, use everywhere
PLOT_STYLE = {
'figure.figsize': (10, 6),
'font.size': 11,
'axes.labelsize': 12,
'axes.titlesize': 14,
'legend.fontsize': 10,
'lines.linewidth': 2,
'grid.alpha': 0.3
}
plt.rcParams.update(PLOT_STYLE)
2. Save Multiple Formats¶
# Save both raster and vector
plt.savefig('plot.png', dpi=300, bbox_inches='tight') # For presentations
plt.savefig('plot.pdf', bbox_inches='tight') # For publications
plt.savefig('plot.svg', bbox_inches='tight') # For editing
3. Add Metadata¶
from datetime import datetime
# Add simulation info to plot
fig, ax = plt.subplots()
# ... create plot ...
fig.text(0.99, 0.01, f'Generated: {datetime.now().strftime("%Y-%m-%d")}',
ha='right', va='bottom', fontsize=8, alpha=0.5)
4. Error Bars¶
# Plot mean with std deviation
import numpy as np
# Multiple runs
runs = [FDSResults(f'run_{i}') for i in range(5)]
# Calculate statistics
times = runs[0].get_device('TEMP')['Time']
values = np.array([run.get_device('TEMP')['Value'] for run in runs])
mean = values.mean(axis=0)
std = values.std(axis=0)
# Plot with error bands
plt.fill_between(times, mean-std, mean+std, alpha=0.3)
plt.plot(times, mean, linewidth=2)
plt.xlabel('Time (s)')
plt.ylabel('Temperature (°C)')
plt.title('Mean ± Std Dev (n=5)')
plt.show()
Next Steps¶
- Analysis - Processing results
- Examples - Complete workflows
- Parametric Studies - Sensitivity analysis
- Job Management - Running simulations