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)))