1. Signal Generation & Fourier Analysis¶
We build a composite signal from three sinusoids plus noise, then recover the frequency content with the FFT. Use the slider below to adjust the noise level interactively.
In [1]:
import numpy as np
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
np.random.seed(42)
# --- Signal parameters ---
fs = 1000 # sampling rate (Hz)
t = np.linspace(0, 1, fs, endpoint=False)
freqs = [50, 120, 300] # Hz
amplitudes = [1.0, 0.6, 0.3]
noise_levels = [0.0, 0.2, 0.5, 1.0, 2.0]
clean = sum(a * np.sin(2 * np.pi * f * t) for f, a in zip(freqs, amplitudes))
# --- FFT helper ---
def compute_fft(signal):
N = len(signal)
fft = np.fft.rfft(signal)
freq = np.fft.rfftfreq(N, d=1/fs)
mag = np.abs(fft) / N * 2
return freq, mag
# --- Build figure with slider ---
fig = make_subplots(
rows=2, cols=1,
subplot_titles=("Time domain", "Frequency spectrum"),
vertical_spacing=0.15
)
traces_per_step = 2
all_traces = []
for nl in noise_levels:
noisy = clean + nl * np.random.randn(len(t))
freq, mag = compute_fft(noisy)
all_traces.append(go.Scatter(
x=t[:300], y=noisy[:300],
mode='lines', line=dict(width=1, color='#1e69d8'),
name=f'Signal (noise={nl})',
visible=False, showlegend=False
))
all_traces.append(go.Scatter(
x=freq, y=mag,
mode='lines', fill='tozeroy',
line=dict(color='#d85a7b', width=1.5),
fillcolor='rgba(216,90,123,0.15)',
name=f'Spectrum (noise={nl})',
visible=False, showlegend=False
))
# Show first step
all_traces[0].visible = True
all_traces[1].visible = True
for tr in all_traces:
row = 2 if 'Spectrum' in tr.name else 1
fig.add_trace(tr, row=row, col=1)
# Slider steps
steps = []
for i, nl in enumerate(noise_levels):
vis = [False] * len(noise_levels) * traces_per_step
vis[i*traces_per_step] = True
vis[i*traces_per_step+1] = True
steps.append(dict(
method='update',
args=[{'visible': vis}],
label=str(nl)
))
fig.update_layout(
sliders=[dict(
active=0,
currentvalue=dict(prefix='Noise amplitude: ', font=dict(size=13)),
pad=dict(t=50),
steps=steps
)],
height=600,
title_text='Composite signal + FFT with adjustable noise',
template='plotly_white'
)
fig.update_xaxes(title_text='Time (s)', row=1, col=1)
fig.update_xaxes(title_text='Frequency (Hz)', row=2, col=1, range=[0, 400])
fig.update_yaxes(title_text='Amplitude', row=1, col=1)
fig.update_yaxes(title_text='|FFT|', row=2, col=1)
from IPython.display import HTML, display
display(HTML(fig.to_html(full_html=False, include_plotlyjs='cdn')))
2. Statistical Summary with Pandas¶
We generate a dataset of material properties drawn from normal distributions and explore their correlations.
In [2]:
n = 200
E = np.random.normal(70e3, 5e3, n) # Young's modulus (MPa) — aluminium-like
nu = np.random.normal(0.33, 0.02, n).clip(0.1, 0.49)
rho = np.random.normal(2700, 100, n) # density (kg/m³)
G = E / (2 * (1 + nu)) # shear modulus (derived)
df = pd.DataFrame({'E (MPa)': E, 'nu': nu, 'rho (kg/m³)': rho, 'G (MPa)': G})
df.describe().round(1)
Out[2]:
| E (MPa) | nu | rho (kg/m³) | G (MPa) | |
|---|---|---|---|---|
| count | 200.0 | 200.0 | 200.0 | 200.0 |
| mean | 69982.2 | 0.3 | 2697.0 | 26330.3 |
| std | 4830.9 | 0.0 | 96.5 | 1844.8 |
| min | 59476.3 | 0.3 | 2426.1 | 22262.1 |
| 25% | 66988.4 | 0.3 | 2637.0 | 25107.2 |
| 50% | 69937.2 | 0.3 | 2694.1 | 26313.9 |
| 75% | 72684.6 | 0.3 | 2765.7 | 27402.0 |
| max | 84590.9 | 0.4 | 2950.1 | 32239.8 |
In [3]:
# Scatter matrix — interactive
import plotly.express as px
fig2 = px.scatter_matrix(
df,
dimensions=['E (MPa)', 'nu', 'rho (kg/m³)', 'G (MPa)'],
title='Scatter matrix: randomly sampled material properties',
template='plotly_white',
opacity=0.5,
color=df['E (MPa)'],
color_continuous_scale='Viridis',
labels={'color': 'E (MPa)'}
)
fig2.update_traces(marker=dict(size=4))
fig2.update_layout(height=600)
from IPython.display import HTML, display
display(HTML(fig2.to_html(full_html=False, include_plotlyjs=False)))
3. 3D Surface — Stress in a Biaxial Plate¶
Von Mises stress $\sigma_\text{vm} = \sqrt{\sigma_x^2 - \sigma_x\sigma_y + \sigma_y^2 + 3\tau_{xy}^2}$ evaluated on a grid of $\sigma_x$–$\sigma_y$ combinations at fixed shear $\tau_{xy} = 20\,\text{MPa}$.
In [4]:
sx = np.linspace(-200, 200, 120)
sy = np.linspace(-200, 200, 120)
SX, SY = np.meshgrid(sx, sy)
tau = 20.0
vm = np.sqrt(SX**2 - SX*SY + SY**2 + 3*tau**2)
fig3 = go.Figure(go.Surface(
x=sx, y=sy, z=vm,
colorscale='Plasma',
colorbar=dict(title='σ_vm (MPa)')
))
fig3.update_layout(
title='Von Mises stress surface (τ_xy = 20 MPa)',
scene=dict(
xaxis_title='σ_x (MPa)',
yaxis_title='σ_y (MPa)',
zaxis_title='σ_vm (MPa)'
),
template='plotly_white',
height=550
)
from IPython.display import HTML, display
display(HTML(fig3.to_html(full_html=False, include_plotlyjs=False)))
4. Animated Chart — Damped Oscillation¶
A damped harmonic oscillator $x(t) = e^{-\zeta\omega_0 t}\cos(\omega_d t)$ animated over increasing damping ratios $\zeta$.
In [5]:
omega0 = 2 * np.pi * 5 # 5 Hz natural frequency
t_osc = np.linspace(0, 2, 500)
zetas = np.round(np.linspace(0.01, 0.99, 30), 2)
frames = []
for z in zetas:
wd = omega0 * np.sqrt(max(1 - z**2, 1e-9))
x = np.exp(-z * omega0 * t_osc) * np.cos(wd * t_osc)
frames.append(go.Frame(
data=[go.Scatter(x=t_osc, y=x, mode='lines',
line=dict(color='#1e69d8', width=2))],
name=str(z),
layout=go.Layout(title_text=f'Damped oscillation ζ = {z:.2f}')
))
# Initial frame
z0 = zetas[0]
wd0 = omega0 * np.sqrt(1 - z0**2)
x0 = np.exp(-z0 * omega0 * t_osc) * np.cos(wd0 * t_osc)
fig4 = go.Figure(
data=[go.Scatter(x=t_osc, y=x0, mode='lines',
line=dict(color='#1e69d8', width=2))],
frames=frames
)
fig4.update_layout(
title=f'Damped oscillation ζ = {z0:.2f}',
xaxis_title='Time (s)', yaxis_title='x(t)',
template='plotly_white',
height=420,
updatemenus=[dict(
type='buttons', showactive=False,
y=1.1, x=0.0, xanchor='left',
buttons=[
dict(label='▶ Play',
method='animate',
args=[None, dict(frame=dict(duration=80, redraw=True),
fromcurrent=True)]),
dict(label='⏸ Pause',
method='animate',
args=[[None], dict(frame=dict(duration=0, redraw=False),
mode='immediate')])
]
)],
sliders=[dict(
steps=[dict(method='animate',
args=[[str(z)],
dict(mode='immediate',
frame=dict(duration=80, redraw=True))],
label=str(z)) for z in zetas],
currentvalue=dict(prefix='ζ = ', font=dict(size=13)),
pad=dict(t=50)
)]
)
from IPython.display import HTML, display
display(HTML(fig4.to_html(full_html=False, include_plotlyjs=False)))