diff --git a/plots/campbell-basic/implementations/plotnine.py b/plots/campbell-basic/implementations/plotnine.py new file mode 100644 index 0000000000..73d4e8fa2e --- /dev/null +++ b/plots/campbell-basic/implementations/plotnine.py @@ -0,0 +1,227 @@ +""" pyplots.ai +campbell-basic: Campbell Diagram +Library: plotnine 0.15.3 | Python 3.14.3 +Quality: 90/100 | Created: 2026-02-15 +""" + +import numpy as np +import pandas as pd +from plotnine import ( + aes, + annotate, + coord_cartesian, + element_blank, + element_line, + element_rect, + element_text, + geom_line, + geom_point, + geom_rect, + geom_text, + ggplot, + guide_legend, + guides, + labs, + scale_color_manual, + scale_linetype_manual, + scale_size_identity, + scale_x_continuous, + scale_y_continuous, + theme, + theme_minimal, +) + + +# Data — Natural frequencies vs rotational speed for rotating machinery +np.random.seed(42) +speed = np.linspace(0, 6000, 80) + +# Natural frequency modes with pronounced gyroscopic speed dependence +modes = { + "1st Bending": 18 + speed * 0.0015 + np.random.normal(0, 0.12, len(speed)), + "2nd Bending": 45 - speed * 0.002 + np.random.normal(0, 0.12, len(speed)), + "1st Torsional": 52 + speed * 0.0025 + np.random.normal(0, 0.12, len(speed)), + "2nd Torsional": 75 + speed * 0.001 + np.random.normal(0, 0.12, len(speed)), + "Axial": 90 - speed * 0.0004 + np.random.normal(0, 0.12, len(speed)), +} + +# Colorblind-safe palette starting with Python Blue +palette = ["#306998", "#E69F00", "#882D9E", "#D55E00", "#009E73"] +mode_names = list(modes.keys()) +mode_colors = dict(zip(mode_names, palette, strict=True)) +eo_color = "#888888" + +# Long-format DataFrame for natural frequency curves +df_modes = pd.DataFrame( + [ + {"Speed": s, "Frequency": f, "Mode": name} + for name, freqs in modes.items() + for s, f in zip(speed, freqs, strict=True) + ] +) + +# Engine order lines: frequency = order × speed / 60 +engine_orders = [1, 2, 3] +eo_names = [f"{o}x EO" for o in engine_orders] +df_eo = pd.DataFrame( + [{"Speed": s, "Frequency": order * s / 60, "Mode": f"{order}x EO"} for order in engine_orders for s in speed] +) + +# Critical speed intersections (EO line crosses natural frequency curve) +critical_points = [] +for order in engine_orders: + eo_freq = order * speed / 60 + for _mode_name, freq_values in modes.items(): + diff = eo_freq - freq_values + sign_changes = np.where(np.diff(np.sign(diff)))[0] + for idx in sign_changes: + s0, s1 = speed[idx], speed[idx + 1] + f0_eo, f1_eo = eo_freq[idx], eo_freq[idx + 1] + f0_m, f1_m = freq_values[idx], freq_values[idx + 1] + t = (f0_m - f0_eo) / ((f1_eo - f0_eo) - (f1_m - f0_m)) + cs, cf = s0 + t * (s1 - s0), f0_eo + t * (f1_eo - f0_eo) + if 0 < cs < 6000 and 0 < cf < 110: + critical_points.append({"Speed": cs, "Frequency": cf}) +df_critical = pd.DataFrame(critical_points) + +# Storytelling: 1x / 1st Bending critical speed (most operationally significant) +eo1_freq = speed / 60 +diff_1b = eo1_freq - modes["1st Bending"] +sc_idx = np.where(np.diff(np.sign(diff_1b)))[0] +annot_speed = annot_freq = None +if len(sc_idx) > 0: + idx = sc_idx[0] + t = (modes["1st Bending"][idx] - eo1_freq[idx]) / ( + (eo1_freq[idx + 1] - eo1_freq[idx]) - (modes["1st Bending"][idx + 1] - modes["1st Bending"][idx]) + ) + annot_speed = speed[idx] + t * (speed[idx + 1] - speed[idx]) + annot_freq = eo1_freq[idx] + t * (eo1_freq[idx + 1] - eo1_freq[idx]) + +# Combine all line data and add line weight column for size differentiation +df_lines = pd.concat([df_modes, df_eo], ignore_index=True) +df_lines["_lw"] = df_lines["Mode"].apply(lambda m: 2.0 if "EO" not in m else 1.0) + +# Legend mappings — consolidated EO into one entry +color_map = {**mode_colors, **dict.fromkeys(eo_names, eo_color)} +ltype_map = {**dict.fromkeys(mode_names, "solid"), **dict.fromkeys(eo_names, "dashed")} +breaks = mode_names + eo_names[:1] +labels = mode_names + ["Engine Order (1×, 2×, 3×)"] + +# Operating range band (nominal: 2000–4500 RPM) +df_band = pd.DataFrame([{"xmin": 2000, "xmax": 4500, "ymin": 0, "ymax": 110}]) + +# EO labels positioned along lines +eo_labels = pd.DataFrame( + [ + {"Speed": 4500, "Frequency": 4500 / 60 + 3, "label": "1×"}, + {"Speed": 2200, "Frequency": 2 * 2200 / 60 + 3, "label": "2×"}, + {"Speed": 1500, "Frequency": 3 * 1500 / 60 + 3, "label": "3×"}, + ] +) + +# Plot — grammar of graphics layer composition +plot = ( + ggplot(df_lines, aes("Speed", "Frequency", color="Mode", linetype="Mode", group="Mode")) + # Operating range shading + + geom_rect( + df_band, + aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax"), + fill="#306998", + alpha=0.04, + color="none", + inherit_aes=False, + ) + # Natural frequency + EO lines with size-identity for weight differentiation + + geom_line(aes(size="_lw")) + + scale_size_identity() + # Critical speed markers + + geom_point( + df_critical, + aes("Speed", "Frequency"), + color="#C62828", + fill="#EF5350", + size=4.5, + shape="D", + stroke=0.7, + inherit_aes=False, + show_legend=False, + ) + # EO line labels + + geom_text( + eo_labels, + aes("Speed", "Frequency", label="label"), + color="#555555", + size=11, + fontstyle="italic", + fontweight="bold", + inherit_aes=False, + show_legend=False, + ) + # Unified legend via scale_manual with custom breaks/labels + + scale_color_manual(values=color_map, breaks=breaks, labels=labels) + + scale_linetype_manual(values=ltype_map, breaks=breaks, labels=labels) + + guides(color=guide_legend(override_aes={"size": [1.8] * 5 + [1.0]}), linetype=guide_legend()) + # coord_cartesian for zoom without data removal + + scale_x_continuous(breaks=range(0, 7000, 1000)) + + scale_y_continuous(breaks=range(0, 111, 10)) + + coord_cartesian(xlim=(0, 6200), ylim=(0, 108)) + + labs(x="Rotational Speed (RPM)", y="Natural Frequency (Hz)", title="campbell-basic · plotnine · pyplots.ai") + # Publication-quality theme + + theme_minimal(base_size=14) + + theme( + figure_size=(16, 9), + text=element_text(family="sans-serif", color="#333333"), + plot_title=element_text(size=24, ha="center", face="bold", color="#1a1a1a"), + axis_title_x=element_text(size=20, face="bold", color="#222222"), + axis_title_y=element_text(size=20, face="bold", color="#222222"), + axis_text=element_text(size=16, color="#555555"), + legend_text=element_text(size=13), + legend_title=element_blank(), + legend_position="bottom", + legend_direction="horizontal", + legend_background=element_rect(fill="white", alpha=0.9, color="#CCCCCC", size=0.4), + legend_key_width=35, + legend_key_height=18, + panel_grid_major=element_line(color="#E5E5E5", size=0.25), + panel_grid_minor=element_blank(), + plot_background=element_rect(fill="white", color="white"), + panel_background=element_rect(fill="#FAFAFA", color="#E0E0E0", size=0.3), + axis_line=element_line(color="#CCCCCC", size=0.4), + plot_margin=0.02, + ) +) + +# Storytelling: annotate the most significant critical speed +if annot_speed is not None: + plot = ( + plot + + annotate( + "segment", + x=annot_speed, + xend=annot_speed, + y=0, + yend=annot_freq, + color="#C62828", + linetype="dotted", + size=0.7, + alpha=0.6, + ) + + annotate( + "text", + x=annot_speed + 180, + y=annot_freq + 5, + label=f"Critical: {int(round(annot_speed))} RPM", + color="#C62828", + size=9, + ha="left", + fontstyle="italic", + fontweight="bold", + ) + ) + +# Operating range label +plot = plot + annotate( + "text", x=3250, y=104, label="Operating Range", color="#306998", size=8, alpha=0.5, fontweight="bold" +) + +plot.save("plot.png", dpi=300, verbose=False) diff --git a/plots/campbell-basic/metadata/plotnine.yaml b/plots/campbell-basic/metadata/plotnine.yaml new file mode 100644 index 0000000000..9fe5ff78dd --- /dev/null +++ b/plots/campbell-basic/metadata/plotnine.yaml @@ -0,0 +1,240 @@ +library: plotnine +specification_id: campbell-basic +created: '2026-02-15T21:08:00Z' +updated: '2026-02-15T21:40:57Z' +generated_by: claude-opus-4-5-20251101 +workflow_run: 22043026955 +issue: 4241 +python_version: 3.14.3 +library_version: 0.15.3 +preview_url: https://storage.googleapis.com/pyplots-images/plots/campbell-basic/plotnine/plot.png +preview_thumb: https://storage.googleapis.com/pyplots-images/plots/campbell-basic/plotnine/plot_thumb.png +preview_html: null +quality_score: 90 +review: + strengths: + - Excellent use of plotnine grammar of graphics with layered composition (operating + range band, lines, markers, annotations) + - 'Strong data storytelling with Critical: 1180 RPM annotation and vertical dotted + line creating immediate visual focus' + - Colorblind-safe palette with both color AND linetype differentiation between modes + and EO lines + - Consolidated legend entry for all three EO lines reduces clutter while maintaining + clarity + - Publication-quality typography with explicitly set font sizes at all levels + - Realistic turbomachinery data with appropriate mode names, frequency ranges, and + gyroscopic speed dependence + weaknesses: + - Operating range band is nearly invisible at alpha=0.04 — could benefit from slightly + higher opacity + - Duplicate interpolation code for 1x/1st Bending critical speed annotation repeats + the general intersection logic + - 3x EO line extends well beyond the plot useful frequency range creating visual + noise in the upper portion + image_description: 'The plot displays a Campbell diagram for rotating machinery + analysis. Five natural frequency curves are plotted against rotational speed (0–6000 + RPM): 1st Bending (dark blue, ~18 Hz baseline, slightly increasing), 2nd Bending + (amber/yellow, ~45 Hz baseline, decreasing), 1st Torsional (purple, ~52 Hz baseline, + increasing), 2nd Torsional (red-orange, ~75 Hz baseline, roughly flat), and Axial + (teal/green, ~90 Hz baseline, nearly flat). Three dashed gray engine order lines + (1×, 2×, 3×) radiate diagonally from the origin. Red diamond markers are placed + at each intersection of an EO line with a natural frequency curve (approximately + 10 critical speed points). A dotted red vertical line at ~1180 RPM with an italic + annotation "Critical: 1180 RPM" highlights the 1×/1st Bending intersection as + the most operationally significant. A very subtle blue-tinted operating range + band spans 2000–4500 RPM with a faint "Operating Range" label. The bottom legend + is horizontal with a white background and gray border, consolidating all three + EO lines into a single "Engine Order (1×, 2×, 3×)" entry. Title reads "campbell-basic + · plotnine · pyplots.ai" in bold centered text.' + criteria_checklist: + visual_quality: + score: 27 + max: 30 + items: + - id: VQ-01 + name: Text Legibility + score: 8 + max: 8 + passed: true + comment: 'All font sizes explicitly set: title 24pt, axis titles 20pt, ticks + 16pt, legend 13pt' + - id: VQ-02 + name: No Overlap + score: 5 + max: 6 + passed: true + comment: 'Minor: 3x label slightly crowds 2nd Torsional line area' + - id: VQ-03 + name: Element Visibility + score: 6 + max: 6 + passed: true + comment: Mode lines weight 2.0, EO lines 1.0, diamond markers size 4.5 — excellent + differentiation + - id: VQ-04 + name: Color Accessibility + score: 4 + max: 4 + passed: true + comment: Colorblind-safe palette with linetype differentiation + - id: VQ-05 + name: Layout Balance + score: 2 + max: 4 + passed: false + comment: Operating range band extremely subtle (alpha=0.04); 3x EO line extends + beyond useful range; bottom legend compresses plot + - id: VQ-06 + name: Axis Labels & Title + score: 2 + max: 2 + passed: true + comment: 'Descriptive with units: Rotational Speed (RPM) and Natural Frequency + (Hz)' + design_excellence: + score: 16 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 7 + max: 8 + passed: true + comment: Custom colorblind-safe palette, intentional typography hierarchy, + refined panel background, publication-quality polish + - id: DE-02 + name: Visual Refinement + score: 5 + max: 6 + passed: true + comment: Subtle grid, minor grid removed, panel background with border, good + whitespace; legend border slightly heavy + - id: DE-03 + name: Data Storytelling + score: 4 + max: 6 + passed: true + comment: 'Critical: 1180 RPM annotation with dotted vertical line creates + focal point; operating range band adds context; visual hierarchy via line + weights' + spec_compliance: + score: 15 + max: 15 + items: + - id: SC-01 + name: Plot Type + score: 5 + max: 5 + passed: true + comment: Correct Campbell diagram with frequency curves and EO lines + - id: SC-02 + name: Required Features + score: 4 + max: 4 + passed: true + comment: 5 modes, 3 EO lines, critical speed markers, mode labels, EO labels, + operating range shading + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: X=Speed (RPM), Y=Frequency (Hz) correctly mapped + - id: SC-04 + name: Title Format + score: 3 + max: 3 + passed: true + comment: campbell-basic · plotnine · pyplots.ai with consolidated EO legend + entry + data_quality: + score: 14 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 5 + max: 6 + passed: true + comment: 5 modes with varying speed dependence, multiple intersections; could + show more dramatic gyroscopic variation + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: Real turbomachinery terminology and plausible frequency ranges + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 4 + passed: true + comment: Realistic RPM and Hz values for rotating machinery + code_quality: + score: 9 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 3 + max: 3 + passed: true + comment: Imports, data, plot, save — 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 + - id: CQ-04 + name: Code Elegance + score: 1 + max: 2 + passed: false + comment: Duplicate interpolation logic for 1x/1st Bending annotation repeats + general intersection pattern + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: Saves as plot.png with dpi=300 + library_features: + score: 9 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 5 + max: 5 + passed: true + comment: Expert grammar-of-graphics composition with layer stacking, scale_manual, + guide_legend, coord_cartesian, annotate + - id: LM-02 + name: Distinctive Features + score: 4 + max: 5 + passed: true + comment: scale_size_identity for line weight, guide_legend(override_aes), + coord_cartesian zoom, geom_rect band + verdict: APPROVED +impl_tags: + dependencies: [] + techniques: + - annotations + - layer-composition + - custom-legend + patterns: + - data-generation + - iteration-over-groups + dataprep: + - interpolation + styling: + - grid-styling + - alpha-blending