|
| 1 | +""" pyplots.ai |
| 2 | +eye-diagram-basic: Signal Integrity Eye Diagram |
| 3 | +Library: letsplot 4.9.0 | Python 3.14.3 |
| 4 | +Quality: 91/100 | Created: 2026-03-17 |
| 5 | +""" |
| 6 | + |
| 7 | +import numpy as np |
| 8 | +import pandas as pd |
| 9 | +from lets_plot import ( |
| 10 | + LetsPlot, |
| 11 | + aes, |
| 12 | + element_blank, |
| 13 | + element_rect, |
| 14 | + element_text, |
| 15 | + geom_raster, |
| 16 | + geom_segment, |
| 17 | + geom_text, |
| 18 | + ggplot, |
| 19 | + ggsize, |
| 20 | + guide_colorbar, |
| 21 | + labs, |
| 22 | + layer_tooltips, |
| 23 | + scale_fill_gradientn, |
| 24 | + scale_x_continuous, |
| 25 | + scale_y_continuous, |
| 26 | + theme, |
| 27 | +) |
| 28 | +from lets_plot.export import ggsave |
| 29 | + |
| 30 | + |
| 31 | +LetsPlot.setup_html() |
| 32 | + |
| 33 | +# Data - Simulated NRZ eye diagram |
| 34 | +np.random.seed(42) |
| 35 | +n_traces = 400 |
| 36 | +samples_per_ui = 150 |
| 37 | +n_bits = 3 |
| 38 | +n_samples = samples_per_ui * n_bits |
| 39 | +time_full = np.linspace(0, n_bits, n_samples, endpoint=False) |
| 40 | + |
| 41 | +# Signal parameters |
| 42 | +amplitude = 1.0 |
| 43 | +noise_sigma = 0.05 * amplitude |
| 44 | +jitter_sigma = 0.03 |
| 45 | + |
| 46 | +# Sigmoid steepness for bandwidth-limited edges (lower = smoother S-curves) |
| 47 | +steepness = 8.0 / 0.7 |
| 48 | + |
| 49 | +# Generate overlaid traces |
| 50 | +all_time = [] |
| 51 | +all_voltage = [] |
| 52 | + |
| 53 | +for _ in range(n_traces): |
| 54 | + bits = np.random.randint(0, 2, n_bits + 1) |
| 55 | + voltage = np.ones(n_samples) * bits[0] * amplitude |
| 56 | + |
| 57 | + for bit_idx in range(1, n_bits + 1): |
| 58 | + transition_time = bit_idx + np.random.normal(0, jitter_sigma) |
| 59 | + if bits[bit_idx] != bits[bit_idx - 1]: |
| 60 | + direction = (bits[bit_idx] - bits[bit_idx - 1]) * amplitude |
| 61 | + voltage = voltage + direction / (1.0 + np.exp(-steepness * (time_full - transition_time))) |
| 62 | + |
| 63 | + voltage += np.random.normal(0, noise_sigma, n_samples) |
| 64 | + |
| 65 | + # Extract 2 UI window centered on the pattern (from 0.5 to 2.5 UI) |
| 66 | + mask = (time_full >= 0.5) & (time_full < 2.5) |
| 67 | + t_window = time_full[mask] - 0.5 |
| 68 | + v_window = voltage[mask] |
| 69 | + |
| 70 | + all_time.extend(t_window) |
| 71 | + all_voltage.extend(v_window) |
| 72 | +all_time = np.array(all_time) |
| 73 | +all_voltage = np.array(all_voltage) |
| 74 | + |
| 75 | +# Create 2D density heatmap by binning (high resolution for smooth rendering) |
| 76 | +n_time_bins = 300 |
| 77 | +n_voltage_bins = 180 |
| 78 | +time_edges = np.linspace(0, 2.0, n_time_bins + 1) |
| 79 | +voltage_edges = np.linspace(-0.3, 1.3, n_voltage_bins + 1) |
| 80 | + |
| 81 | +density, _, _ = np.histogram2d(all_time, all_voltage, bins=[time_edges, voltage_edges]) |
| 82 | + |
| 83 | +# Normalize density |
| 84 | +density = density / density.max() |
| 85 | + |
| 86 | +# Build long-form DataFrame |
| 87 | +time_centers = (time_edges[:-1] + time_edges[1:]) / 2 |
| 88 | +voltage_centers = (voltage_edges[:-1] + voltage_edges[1:]) / 2 |
| 89 | +time_grid, voltage_grid = np.meshgrid(time_centers, voltage_centers, indexing="ij") |
| 90 | + |
| 91 | +df = pd.DataFrame({"time_ui": time_grid.ravel(), "voltage": voltage_grid.ravel(), "density": density.ravel()}) |
| 92 | + |
| 93 | +# Filter out zero-density cells for cleaner rendering |
| 94 | +df = df[df["density"] > 0].reset_index(drop=True) |
| 95 | + |
| 96 | +# Inferno-inspired perceptually uniform colormap for density |
| 97 | +inferno_colors = [ |
| 98 | + "#0d0887", |
| 99 | + "#2d0594", |
| 100 | + "#46039f", |
| 101 | + "#6a00a8", |
| 102 | + "#8f0da4", |
| 103 | + "#b12a90", |
| 104 | + "#cc4778", |
| 105 | + "#e16462", |
| 106 | + "#f1844b", |
| 107 | + "#fca636", |
| 108 | + "#fcce25", |
| 109 | + "#f0f921", |
| 110 | +] |
| 111 | + |
| 112 | +# Measure eye opening at center (1.0 UI) for annotations |
| 113 | +center_col = n_time_bins // 2 |
| 114 | +center_density = density[center_col, :] |
| 115 | +threshold = 0.05 |
| 116 | +low_density_mask = center_density < threshold |
| 117 | +voltage_center_vals = voltage_centers[low_density_mask] |
| 118 | +eye_region = voltage_center_vals[(voltage_center_vals > 0.15) & (voltage_center_vals < 0.85)] |
| 119 | +eye_bottom = eye_region.min() if len(eye_region) > 0 else 0.25 |
| 120 | +eye_top = eye_region.max() if len(eye_region) > 0 else 0.75 |
| 121 | +eye_height = eye_top - eye_bottom |
| 122 | +eye_mid_v = (eye_top + eye_bottom) / 2 |
| 123 | + |
| 124 | +# Measure eye width at mid-voltage level |
| 125 | +mid_row = np.argmin(np.abs(voltage_centers - eye_mid_v)) |
| 126 | +row_density = density[:, mid_row] |
| 127 | +low_density_time = time_centers[row_density < threshold] |
| 128 | +eye_time_region = low_density_time[(low_density_time > 0.6) & (low_density_time < 1.4)] |
| 129 | +eye_left = eye_time_region.min() if len(eye_time_region) > 0 else 0.75 |
| 130 | +eye_right = eye_time_region.max() if len(eye_time_region) > 0 else 1.25 |
| 131 | +eye_width = eye_right - eye_left |
| 132 | + |
| 133 | +# Annotation DataFrames |
| 134 | +ann_color = "#00e5ff" |
| 135 | +height_x = 1.32 |
| 136 | +height_seg = pd.DataFrame({"x": [height_x], "y": [eye_bottom], "xend": [height_x], "yend": [eye_top]}) |
| 137 | +width_seg = pd.DataFrame({"x": [eye_left], "y": [eye_mid_v], "xend": [eye_right], "yend": [eye_mid_v]}) |
| 138 | +height_label = pd.DataFrame({"x": [height_x + 0.04], "y": [eye_mid_v], "label": [f"Eye Height: {eye_height:.2f} V"]}) |
| 139 | +width_label = pd.DataFrame( |
| 140 | + {"x": [(eye_left + eye_right) / 2], "y": [eye_mid_v - 0.09], "label": [f"Eye Width: {eye_width:.2f} UI"]} |
| 141 | +) |
| 142 | + |
| 143 | +# Plot |
| 144 | +plot = ( |
| 145 | + ggplot(df, aes(x="time_ui", y="voltage", fill="density")) |
| 146 | + + geom_raster( |
| 147 | + tooltips=layer_tooltips() |
| 148 | + .format("@time_ui", ".2f") |
| 149 | + .format("@voltage", ".2f") |
| 150 | + .format("@density", ".3f") |
| 151 | + .line("Time: @time_ui UI") |
| 152 | + .line("Voltage: @voltage V") |
| 153 | + .line("Density: @density") |
| 154 | + ) |
| 155 | + + geom_segment( |
| 156 | + aes(x="x", y="y", xend="xend", yend="yend"), data=height_seg, color=ann_color, size=1.2, inherit_aes=False |
| 157 | + ) |
| 158 | + + geom_segment( |
| 159 | + aes(x="x", y="y", xend="xend", yend="yend"), data=width_seg, color=ann_color, size=1.2, inherit_aes=False |
| 160 | + ) |
| 161 | + + geom_text( |
| 162 | + aes(x="x", y="y", label="label"), data=height_label, color=ann_color, size=11, hjust=0, inherit_aes=False |
| 163 | + ) |
| 164 | + + geom_text( |
| 165 | + aes(x="x", y="y", label="label"), data=width_label, color=ann_color, size=11, hjust=0.5, inherit_aes=False |
| 166 | + ) |
| 167 | + + scale_fill_gradientn( |
| 168 | + colors=inferno_colors, name="Trace\nDensity", guide=guide_colorbar(barwidth=14, barheight=260, nbin=256) |
| 169 | + ) |
| 170 | + + scale_x_continuous(name="Time (UI)", breaks=[0.0, 0.5, 1.0, 1.5, 2.0], expand=[0, 0]) |
| 171 | + + scale_y_continuous(name="Voltage (V)", breaks=[0.0, 0.5, 1.0], labels=["0.0", "0.5", "1.0"], expand=[0, 0]) |
| 172 | + + labs(title="eye-diagram-basic · letsplot · pyplots.ai") |
| 173 | + + theme( |
| 174 | + plot_title=element_text(size=28, face="bold", color="#e0e0e0", margin=[0, 0, 10, 0]), |
| 175 | + axis_title_x=element_text(size=20, color="#cccccc", margin=[10, 0, 0, 0]), |
| 176 | + axis_title_y=element_text(size=20, color="#cccccc", margin=[0, 10, 0, 0]), |
| 177 | + axis_text_x=element_text(size=16, color="#aaaaaa"), |
| 178 | + axis_text_y=element_text(size=16, color="#aaaaaa"), |
| 179 | + axis_ticks=element_blank(), |
| 180 | + axis_line=element_blank(), |
| 181 | + legend_text=element_text(size=14, color="#cccccc"), |
| 182 | + legend_title=element_text(size=16, face="bold", color="#cccccc"), |
| 183 | + panel_grid=element_blank(), |
| 184 | + panel_background=element_rect(fill="#000004", color="#000004"), |
| 185 | + plot_background=element_rect(fill="#0d0d2b", color="#0d0d2b"), |
| 186 | + plot_margin=[40, 30, 20, 20], |
| 187 | + legend_background=element_rect(fill="#0d0d2b", color="#0d0d2b"), |
| 188 | + ) |
| 189 | + + ggsize(1600, 900) |
| 190 | +) |
| 191 | + |
| 192 | +# Save |
| 193 | +ggsave(plot, "plot.png", path=".", scale=3) |
| 194 | +ggsave(plot, "plot.html", path=".") |
0 commit comments