|
| 1 | +import matplotlib.pyplot as plt |
| 2 | +import numpy as np |
| 3 | +from scipy.signal import lsim |
| 4 | +from scipy.signal import lti |
| 5 | + |
| 6 | +from pulse_transitions import detect_edges |
| 7 | +from pulse_transitions import detect_signal_levels |
| 8 | +from pulse_transitions import get_edge_metrics |
| 9 | + |
| 10 | + |
| 11 | +# Example: synthetic underdamped step response |
| 12 | +def generate_step_response(t, damping=0.2, freq=10): |
| 13 | + # Second-order system parameters |
| 14 | + wn = freq # Natural frequency (rad/s) |
| 15 | + zeta = damping # Damping ratio |
| 16 | + |
| 17 | + # Transfer function: H(s) = wn^2 / (s^2 + 2*zeta*wn*s + wn^2) |
| 18 | + num = [wn**2] |
| 19 | + den = [1, 2*zeta*wn, wn**2] |
| 20 | + |
| 21 | + # Create the system |
| 22 | + system = lti(num, den) |
| 23 | + |
| 24 | + u = [0]*len(t[t<0]) + [1]*len(t[t>=0]) |
| 25 | + t_shift = t-min(t) |
| 26 | + t_out, y_out, _ = lsim(system, U=u, T=t_shift) |
| 27 | + |
| 28 | + return t, y_out |
| 29 | + |
| 30 | + |
| 31 | +def make_annotations_plot(t, y, edge, metrics): |
| 32 | + t_low, t_high = edge.start, edge.end |
| 33 | + trise = t_high - t_low |
| 34 | + mid_level = np.mean(metrics["levels"]) |
| 35 | + |
| 36 | + fractional_thresholds = metrics["fractional_thresholds"] |
| 37 | + absolute_thresholds = metrics["absolute_thresholds"] |
| 38 | + threshold_colors = ["C1", "C2"] |
| 39 | + edge_color = "black" |
| 40 | + signal_color = "C0" |
| 41 | + |
| 42 | + fig, ax = plt.subplots(figsize=(8, 4)) |
| 43 | + ax.plot(t, y, label="Signal", color=signal_color) |
| 44 | + |
| 45 | + # Draw absolute thresholds and level lines |
| 46 | + for frac, abs_val, color in zip(fractional_thresholds, absolute_thresholds, threshold_colors, strict=False): |
| 47 | + ax.axhline(abs_val, linestyle="--", color=color, label=f"{int(frac * 100)}% Threshold") |
| 48 | + ax.annotate(f"{int(frac * 100)}%", |
| 49 | + xy=(t[0], abs_val), |
| 50 | + xytext=(20, 2.5), textcoords="offset points", |
| 51 | + color=color) |
| 52 | + |
| 53 | + # Edge start/end lines |
| 54 | + edge_label = metrics.get("edge_label", "Edge") |
| 55 | + ax.axvline(t_low, linestyle=":", color=edge_color, label=f"{edge_label} start") |
| 56 | + ax.axvline(t_high, linestyle=":", color=edge_color, label=f"{edge_label} end") |
| 57 | + ax.plot([metrics["midcross"]], [mid_level], "kx", label="Mid Cross") |
| 58 | + |
| 59 | + # Annotate rise time & slew rate |
| 60 | + slewrate = metrics.get("slewrate", np.nan) |
| 61 | + ax.annotate(f"Rise: {trise * 1e3:.0f} ps\nSlew: {slewrate:.1f} V/ns", |
| 62 | + xy=(t_high, mid_level), |
| 63 | + xytext=(20, 0), textcoords="offset points", |
| 64 | + ha="left", va="center") |
| 65 | + |
| 66 | + idx, overshoot = metrics["overshoot"] |
| 67 | + ax.plot(t[idx], y[idx], "ro", label="Overshoot") |
| 68 | + ax.annotate(f"Overshoot: {overshoot * 100:.0f}%", |
| 69 | + xy=(t[idx], y[idx]), |
| 70 | + xytext=(20, 0), textcoords="offset points", |
| 71 | + ha="left", va="center") |
| 72 | + |
| 73 | + idx, undershoot = metrics["undershoot"] |
| 74 | + ax.plot(t[idx], y[idx], "go", label="Undershoot") |
| 75 | + ax.annotate(f"Undershoot: {undershoot * 100:.0f}%", |
| 76 | + xy=(t[idx], y[idx]), |
| 77 | + xytext=(20, 0), textcoords="offset points", |
| 78 | + ha="left", va="center") |
| 79 | + |
| 80 | + ax.set_xlabel("Time (ns)") |
| 81 | + ax.set_ylabel("Amplitude (V)") |
| 82 | + ax.set_title("Step Response with Edge Detection") |
| 83 | + ax.legend() |
| 84 | + ax.grid(True, linestyle="--", linewidth=0.5, color="lightgray") # noqa: FBT003 |
| 85 | + plt.tight_layout() |
| 86 | + return fig, ax |
| 87 | + |
| 88 | + |
| 89 | + |
| 90 | +# Generate data |
| 91 | +t = np.linspace(-0.25, 1, 1000) |
| 92 | +t_out, y = generate_step_response(t, freq=25/(max(t))) |
| 93 | + |
| 94 | + |
| 95 | +#plt.plot(t, y) |
| 96 | +#plt.show() |
| 97 | + |
| 98 | +threshold_fractions=(0.1, 0.9) |
| 99 | + |
| 100 | +# Estimate levels (e.g. using histogram or endpoints) |
| 101 | +levels = detect_signal_levels(x=t, y=y, method="histogram") |
| 102 | +low, high = levels |
| 103 | + |
| 104 | +# Detect edges |
| 105 | +edge = detect_edges(t, y, levels=levels, thresholds=threshold_fractions)[0] |
| 106 | + |
| 107 | +# Compute overshoot / undershoot |
| 108 | +metrics = get_edge_metrics(x=t, y=y, thresholds=threshold_fractions, levels=levels) |
| 109 | + |
| 110 | +fig, ax = make_annotations_plot(t, y, edge, metrics) |
| 111 | +fig.savefig("second-order-annotations.png", dpi=300) |
| 112 | +plt.show() |
0 commit comments