Files
pinn/fdm/visualizer.py
T

228 lines
7.2 KiB
Python
Raw Normal View History

2026-05-13 21:21:53 +02:00
import sys
import os
import numpy as np
import plotly.graph_objects as go
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import config
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
FDM_ANIM_DIR = os.path.join(BASE_DIR, 'animations', 'fdm')
def _next_path(base_dir, prefix, ext):
i = 1
while True:
path = os.path.join(base_dir, f'{prefix}_{i:03d}{ext}')
if not os.path.exists(path):
return path
i += 1
def visualize_fdm(T_matrix, x_vals, t_vals):
"""Produce three HTML visualisations for the FDM solver output.
Parameters
----------
T_matrix : np.ndarray, shape (NX, NT)
Temperature field: T_matrix[i, n] = T(x_i, t_n).
x_vals : np.ndarray, shape (NX,)
Spatial node positions.
t_vals : np.ndarray, shape (NT,)
Time values.
"""
os.makedirs(FDM_ANIM_DIR, exist_ok=True)
# ------------------------------------------------------------------
# 1. Heatmap
# ------------------------------------------------------------------
zmax = float(np.max(np.abs(T_matrix - config.T0)))
# Use symmetric range centred on T0 if there is any variation,
# otherwise fall back to the raw range.
z_data = T_matrix.T # shape (NT, NX) — rows=time, cols=space
if zmax > 0:
z_center = float(np.mean(T_matrix))
z_abs = float(np.max(np.abs(T_matrix - z_center)))
zmin_sym = z_center - z_abs
zmax_sym = z_center + z_abs
else:
zmin_sym = float(np.min(T_matrix))
zmax_sym = float(np.max(T_matrix))
fig_heatmap = go.Figure(go.Heatmap(
z=z_data,
x=x_vals,
y=t_vals,
colorscale='RdBu_r',
zmin=zmin_sym,
zmax=zmax_sym,
colorbar=dict(title='T [°C]'),
))
fig_heatmap.update_layout(
title='FDM — Heat Equation T(x,t)',
xaxis_title='x [m]',
yaxis_title='t [s]',
height=500,
)
heatmap_path = _next_path(FDM_ANIM_DIR, 'heatmap', '.html')
fig_heatmap.write_html(heatmap_path)
print(f"Heatmap saved → {heatmap_path}")
# ------------------------------------------------------------------
# 2. Animated profile T(x) evolving in time
# ------------------------------------------------------------------
L = config.L
n_frames = len(t_vals)
# Subsample frames for a manageable animation (max ~200 frames)
max_frames = 200
step = max(1, n_frames // max_frames)
frame_indices = list(range(0, n_frames, step))
y_min = float(np.min(T_matrix)) - 1.0
y_max = float(np.max(T_matrix)) + 1.0
vline_shapes = [
dict(type='line', x0=0, x1=0, y0=y_min, y1=y_max,
line=dict(color='grey', dash='dash', width=1)),
dict(type='line', x0=L, x1=L, y0=y_min, y1=y_max,
line=dict(color='grey', dash='dash', width=1)),
]
frames = []
for idx in frame_indices:
frames.append(go.Frame(
data=[go.Scatter(
x=x_vals,
y=T_matrix[:, idx],
mode='lines',
line=dict(color='royalblue', width=2),
name='FDM',
)],
name=str(idx),
layout=go.Layout(
title_text=f'FDM | t = {t_vals[idx]:.3f} s',
),
))
fig_anim = go.Figure(
data=[go.Scatter(
x=x_vals,
y=T_matrix[:, frame_indices[0]],
mode='lines',
line=dict(color='royalblue', width=2),
name='FDM',
)],
layout=go.Layout(
title=f'FDM | t = {t_vals[frame_indices[0]]:.3f} s',
xaxis=dict(title='x [m]', range=[-0.02 * L, 1.02 * L]),
yaxis=dict(title='T [°C]', range=[y_min, y_max]),
shapes=vline_shapes,
updatemenus=[dict(
type='buttons',
showactive=False,
y=1.15,
x=0.5,
xanchor='center',
buttons=[
dict(
label='▶ Play',
method='animate',
args=[None, dict(
frame=dict(duration=40, redraw=False),
fromcurrent=True,
mode='immediate',
)],
),
dict(
label='⏸ Pause',
method='animate',
args=[[None], dict(
frame=dict(duration=0, redraw=False),
mode='immediate',
)],
),
],
)],
sliders=[dict(
steps=[
dict(
method='animate',
args=[[str(idx)], dict(
mode='immediate',
frame=dict(duration=0, redraw=False),
)],
label=f'{t_vals[idx]:.2f}',
)
for idx in frame_indices
],
transition=dict(duration=0),
x=0.05,
y=0,
len=0.9,
currentvalue=dict(prefix='t = ', font=dict(size=14)),
)],
),
frames=frames,
)
anim_path = _next_path(FDM_ANIM_DIR, 'animation', '.html')
fig_anim.write_html(anim_path)
print(f"Animation saved → {anim_path}")
# ------------------------------------------------------------------
# 3. Time evolution at fixed spatial points
# ------------------------------------------------------------------
fixed_fractions = [0.0, 0.25, 0.5, 0.75, 1.0]
colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd']
fig_ts = go.Figure()
for frac, color in zip(fixed_fractions, colors):
x_target = frac * L
idx_x = int(np.argmin(np.abs(x_vals - x_target)))
label = f'x = {x_vals[idx_x]:.2f} m'
fig_ts.add_trace(go.Scatter(
x=t_vals,
y=T_matrix[idx_x, :],
mode='lines',
name=label,
line=dict(color=color, width=2),
))
# "Heat ON" vertical dashed line at t = T_STEP
t_step = config.T_STEP
t_min_val = float(np.min(t_vals))
t_max_val = float(np.max(t_vals))
T_min_val = float(np.min(T_matrix)) - 1.0
T_max_val = float(np.max(T_matrix)) + 1.0
fig_ts.add_shape(
type='line',
x0=t_step, x1=t_step,
y0=T_min_val, y1=T_max_val,
line=dict(color='red', dash='dash', width=1.5),
)
fig_ts.add_annotation(
x=t_step,
y=T_max_val,
text='Heat ON',
showarrow=False,
yanchor='top',
font=dict(color='red', size=11),
)
fig_ts.update_layout(
title='FDM — Temperature evolution at fixed points',
xaxis=dict(title='t [s]', range=[t_min_val, t_max_val]),
yaxis=dict(title='T [°C]', range=[T_min_val, T_max_val]),
legend=dict(x=0.01, y=0.99),
height=480,
)
ts_path = _next_path(FDM_ANIM_DIR, 'time_series', '.html')
fig_ts.write_html(ts_path)
print(f"Time-series saved → {ts_path}")