diff --git a/plots/eye-diagram-basic/implementations/letsplot.py b/plots/eye-diagram-basic/implementations/letsplot.py new file mode 100644 index 0000000000..da1c747c63 --- /dev/null +++ b/plots/eye-diagram-basic/implementations/letsplot.py @@ -0,0 +1,194 @@ +""" pyplots.ai +eye-diagram-basic: Signal Integrity Eye Diagram +Library: letsplot 4.9.0 | Python 3.14.3 +Quality: 91/100 | Created: 2026-03-17 +""" + +import numpy as np +import pandas as pd +from lets_plot import ( + LetsPlot, + aes, + element_blank, + element_rect, + element_text, + geom_raster, + geom_segment, + geom_text, + ggplot, + ggsize, + guide_colorbar, + labs, + layer_tooltips, + scale_fill_gradientn, + scale_x_continuous, + scale_y_continuous, + theme, +) +from lets_plot.export import ggsave + + +LetsPlot.setup_html() + +# Data - Simulated NRZ eye diagram +np.random.seed(42) +n_traces = 400 +samples_per_ui = 150 +n_bits = 3 +n_samples = samples_per_ui * n_bits +time_full = np.linspace(0, n_bits, n_samples, endpoint=False) + +# Signal parameters +amplitude = 1.0 +noise_sigma = 0.05 * amplitude +jitter_sigma = 0.03 + +# Sigmoid steepness for bandwidth-limited edges (lower = smoother S-curves) +steepness = 8.0 / 0.7 + +# Generate overlaid traces +all_time = [] +all_voltage = [] + +for _ in range(n_traces): + bits = np.random.randint(0, 2, n_bits + 1) + voltage = np.ones(n_samples) * bits[0] * amplitude + + for bit_idx in range(1, n_bits + 1): + transition_time = bit_idx + np.random.normal(0, jitter_sigma) + if bits[bit_idx] != bits[bit_idx - 1]: + direction = (bits[bit_idx] - bits[bit_idx - 1]) * amplitude + voltage = voltage + direction / (1.0 + np.exp(-steepness * (time_full - transition_time))) + + voltage += np.random.normal(0, noise_sigma, n_samples) + + # Extract 2 UI window centered on the pattern (from 0.5 to 2.5 UI) + mask = (time_full >= 0.5) & (time_full < 2.5) + t_window = time_full[mask] - 0.5 + v_window = voltage[mask] + + all_time.extend(t_window) + all_voltage.extend(v_window) +all_time = np.array(all_time) +all_voltage = np.array(all_voltage) + +# Create 2D density heatmap by binning (high resolution for smooth rendering) +n_time_bins = 300 +n_voltage_bins = 180 +time_edges = np.linspace(0, 2.0, n_time_bins + 1) +voltage_edges = np.linspace(-0.3, 1.3, n_voltage_bins + 1) + +density, _, _ = np.histogram2d(all_time, all_voltage, bins=[time_edges, voltage_edges]) + +# Normalize density +density = density / density.max() + +# Build long-form DataFrame +time_centers = (time_edges[:-1] + time_edges[1:]) / 2 +voltage_centers = (voltage_edges[:-1] + voltage_edges[1:]) / 2 +time_grid, voltage_grid = np.meshgrid(time_centers, voltage_centers, indexing="ij") + +df = pd.DataFrame({"time_ui": time_grid.ravel(), "voltage": voltage_grid.ravel(), "density": density.ravel()}) + +# Filter out zero-density cells for cleaner rendering +df = df[df["density"] > 0].reset_index(drop=True) + +# Inferno-inspired perceptually uniform colormap for density +inferno_colors = [ + "#0d0887", + "#2d0594", + "#46039f", + "#6a00a8", + "#8f0da4", + "#b12a90", + "#cc4778", + "#e16462", + "#f1844b", + "#fca636", + "#fcce25", + "#f0f921", +] + +# Measure eye opening at center (1.0 UI) for annotations +center_col = n_time_bins // 2 +center_density = density[center_col, :] +threshold = 0.05 +low_density_mask = center_density < threshold +voltage_center_vals = voltage_centers[low_density_mask] +eye_region = voltage_center_vals[(voltage_center_vals > 0.15) & (voltage_center_vals < 0.85)] +eye_bottom = eye_region.min() if len(eye_region) > 0 else 0.25 +eye_top = eye_region.max() if len(eye_region) > 0 else 0.75 +eye_height = eye_top - eye_bottom +eye_mid_v = (eye_top + eye_bottom) / 2 + +# Measure eye width at mid-voltage level +mid_row = np.argmin(np.abs(voltage_centers - eye_mid_v)) +row_density = density[:, mid_row] +low_density_time = time_centers[row_density < threshold] +eye_time_region = low_density_time[(low_density_time > 0.6) & (low_density_time < 1.4)] +eye_left = eye_time_region.min() if len(eye_time_region) > 0 else 0.75 +eye_right = eye_time_region.max() if len(eye_time_region) > 0 else 1.25 +eye_width = eye_right - eye_left + +# Annotation DataFrames +ann_color = "#00e5ff" +height_x = 1.32 +height_seg = pd.DataFrame({"x": [height_x], "y": [eye_bottom], "xend": [height_x], "yend": [eye_top]}) +width_seg = pd.DataFrame({"x": [eye_left], "y": [eye_mid_v], "xend": [eye_right], "yend": [eye_mid_v]}) +height_label = pd.DataFrame({"x": [height_x + 0.04], "y": [eye_mid_v], "label": [f"Eye Height: {eye_height:.2f} V"]}) +width_label = pd.DataFrame( + {"x": [(eye_left + eye_right) / 2], "y": [eye_mid_v - 0.09], "label": [f"Eye Width: {eye_width:.2f} UI"]} +) + +# Plot +plot = ( + ggplot(df, aes(x="time_ui", y="voltage", fill="density")) + + geom_raster( + tooltips=layer_tooltips() + .format("@time_ui", ".2f") + .format("@voltage", ".2f") + .format("@density", ".3f") + .line("Time: @time_ui UI") + .line("Voltage: @voltage V") + .line("Density: @density") + ) + + geom_segment( + aes(x="x", y="y", xend="xend", yend="yend"), data=height_seg, color=ann_color, size=1.2, inherit_aes=False + ) + + geom_segment( + aes(x="x", y="y", xend="xend", yend="yend"), data=width_seg, color=ann_color, size=1.2, inherit_aes=False + ) + + geom_text( + aes(x="x", y="y", label="label"), data=height_label, color=ann_color, size=11, hjust=0, inherit_aes=False + ) + + geom_text( + aes(x="x", y="y", label="label"), data=width_label, color=ann_color, size=11, hjust=0.5, inherit_aes=False + ) + + scale_fill_gradientn( + colors=inferno_colors, name="Trace\nDensity", guide=guide_colorbar(barwidth=14, barheight=260, nbin=256) + ) + + scale_x_continuous(name="Time (UI)", breaks=[0.0, 0.5, 1.0, 1.5, 2.0], expand=[0, 0]) + + scale_y_continuous(name="Voltage (V)", breaks=[0.0, 0.5, 1.0], labels=["0.0", "0.5", "1.0"], expand=[0, 0]) + + labs(title="eye-diagram-basic · letsplot · pyplots.ai") + + theme( + plot_title=element_text(size=28, face="bold", color="#e0e0e0", margin=[0, 0, 10, 0]), + axis_title_x=element_text(size=20, color="#cccccc", margin=[10, 0, 0, 0]), + axis_title_y=element_text(size=20, color="#cccccc", margin=[0, 10, 0, 0]), + axis_text_x=element_text(size=16, color="#aaaaaa"), + axis_text_y=element_text(size=16, color="#aaaaaa"), + axis_ticks=element_blank(), + axis_line=element_blank(), + legend_text=element_text(size=14, color="#cccccc"), + legend_title=element_text(size=16, face="bold", color="#cccccc"), + panel_grid=element_blank(), + panel_background=element_rect(fill="#000004", color="#000004"), + plot_background=element_rect(fill="#0d0d2b", color="#0d0d2b"), + plot_margin=[40, 30, 20, 20], + legend_background=element_rect(fill="#0d0d2b", color="#0d0d2b"), + ) + + ggsize(1600, 900) +) + +# Save +ggsave(plot, "plot.png", path=".", scale=3) +ggsave(plot, "plot.html", path=".") diff --git a/plots/eye-diagram-basic/metadata/letsplot.yaml b/plots/eye-diagram-basic/metadata/letsplot.yaml new file mode 100644 index 0000000000..5aacc257f4 --- /dev/null +++ b/plots/eye-diagram-basic/metadata/letsplot.yaml @@ -0,0 +1,230 @@ +library: letsplot +specification_id: eye-diagram-basic +created: '2026-03-17T23:34:53Z' +updated: '2026-03-17T23:57:30Z' +generated_by: claude-opus-4-5-20251101 +workflow_run: 23221365710 +issue: 4561 +python_version: 3.14.3 +library_version: 4.9.0 +preview_url: https://storage.googleapis.com/pyplots-images/plots/eye-diagram-basic/letsplot/plot.png +preview_thumb: https://storage.googleapis.com/pyplots-images/plots/eye-diagram-basic/letsplot/plot_thumb.png +preview_html: https://storage.googleapis.com/pyplots-images/plots/eye-diagram-basic/letsplot/plot.html +quality_score: 91 +review: + strengths: + - Excellent dark oscilloscope aesthetic perfectly suited for the engineering domain + - Comprehensive signal generation with realistic noise, jitter, and bandwidth-limited + sigmoid transitions + - Eye height/width annotations add quantitative value and tell the signal quality + story + - High-resolution 300x180 bin density heatmap for smooth rendering + - Well-crafted inferno-inspired colormap with perceptually uniform progression + weaknesses: + - Minor vertical streak artifacts at time window boundaries from edge effects in + data extraction windowing + - Could leverage more distinctive lets-plot features + image_description: 'The plot displays an eye diagram on a dark background (#000004 + panel, #0d0d2b outer) mimicking an oscilloscope display. A density heatmap rendered + via geom_raster shows overlaid NRZ signal traces using an inferno-inspired colormap + — dark blue/purple for low trace density, through pink/magenta, to bright yellow/orange + for high density regions. The characteristic eye-shaped opening is clearly visible + centered around 1.0 UI. The high-density (yellow) regions appear at the stable + 0V and 1V signal levels and along the transition paths. The X-axis is labeled + "Time (UI)" with ticks at 0.0, 0.5, 1.0, 1.5, 2.0. The Y-axis is labeled "Voltage + (V)" with ticks at 0.0, 0.5, 1.0. Two cyan annotation lines mark the eye height + (vertical, 0.68 V) and eye width (horizontal, 0.79 UI) with corresponding labels. + A colorbar on the right shows "Trace Density" from 0 to 1. Title reads "eye-diagram-basic + · letsplot · pyplots.ai" in light gray bold text.' + criteria_checklist: + visual_quality: + score: 28 + max: 30 + items: + - id: VQ-01 + name: Text Legibility + score: 8 + max: 8 + passed: true + comment: 'All font sizes explicitly set: title 28pt, axis titles 20pt, axis + text 16pt, legend title 16pt, legend text 14pt' + - id: VQ-02 + name: No Overlap + score: 6 + max: 6 + passed: true + comment: No overlapping text elements, annotations well-positioned + - id: VQ-03 + name: Element Visibility + score: 5 + max: 6 + passed: true + comment: Good density rendering at 300x180 bins, minor vertical streak artifacts + at time boundaries + - id: VQ-04 + name: Color Accessibility + score: 4 + max: 4 + passed: true + comment: Inferno colormap is perceptually uniform and colorblind-safe, cyan + annotations contrast well + - id: VQ-05 + name: Layout & Canvas + score: 3 + max: 4 + passed: true + comment: Good proportions, minor edge artifacts create visual noise at boundaries + - id: VQ-06 + name: Axis Labels & Title + score: 2 + max: 2 + passed: true + comment: Time (UI) and Voltage (V) with appropriate units + design_excellence: + score: 17 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 7 + max: 8 + passed: true + comment: Dark oscilloscope aesthetic, custom inferno palette, cyan annotations, + professional polish + - id: DE-02 + name: Visual Refinement + score: 5 + max: 6 + passed: true + comment: Spines removed, ticks removed, grid removed, dark themed backgrounds, + generous margins + - id: DE-03 + name: Data Storytelling + score: 5 + max: 6 + passed: true + comment: Eye height/width annotations communicate signal quality, density + creates natural visual hierarchy + spec_compliance: + score: 15 + max: 15 + items: + - id: SC-01 + name: Plot Type + score: 5 + max: 5 + passed: true + comment: Correct density heatmap of overlaid NRZ signal traces + - id: SC-02 + name: Required Features + score: 4 + max: 4 + passed: true + comment: 'All spec features present: density coloring, UI axis, voltage levels, + noise, jitter, sigmoid transitions, annotations' + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: X=time in UI, Y=voltage in volts, correct mapping + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 + passed: true + comment: Correct title format, descriptive colorbar legend + data_quality: + score: 14 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 5 + max: 6 + passed: true + comment: Shows both signal levels, transitions, noise, jitter, and eye opening; + minor edge artifacts + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: NRZ signal integrity is a genuine engineering application with realistic + parameters + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 4 + passed: true + comment: Voltage 0-1V, 2 UI window, 5% noise, 3% jitter are physically sensible + code_quality: + score: 10 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 3 + max: 3 + passed: true + comment: Clean imports-data-plot-save structure, no functions or classes + - id: CQ-02 + name: Reproducibility + score: 2 + max: 2 + passed: true + comment: np.random.seed(42) set + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: All imports used, specific imports from lets_plot + - id: CQ-04 + name: Code Elegance + score: 2 + max: 2 + passed: true + comment: Appropriate complexity, clean vectorized operations + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: Saves as plot.png via ggsave with scale=3 + library_mastery: + score: 7 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 4 + max: 5 + passed: true + comment: 'Good ggplot grammar: aes, geom_raster, scale_fill_gradientn, comprehensive + theme' + - id: LM-02 + name: Distinctive Features + score: 3 + max: 5 + passed: true + comment: Uses layer_tooltips() for interactive hover, guide_colorbar with + custom params, HTML export + verdict: APPROVED +impl_tags: + dependencies: [] + techniques: + - annotations + - hover-tooltips + - html-export + - layer-composition + patterns: + - data-generation + - iteration-over-groups + - matrix-construction + dataprep: + - binning + - normalization + styling: + - dark-theme + - custom-colormap