diff --git a/plots/campbell-basic/implementations/matplotlib.py b/plots/campbell-basic/implementations/matplotlib.py new file mode 100644 index 0000000000..4e6846cc7e --- /dev/null +++ b/plots/campbell-basic/implementations/matplotlib.py @@ -0,0 +1,210 @@ +""" pyplots.ai +campbell-basic: Campbell Diagram +Library: matplotlib 3.10.8 | Python 3.14.3 +Quality: 90/100 | Created: 2026-02-15 +""" + +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.lines import Line2D +from matplotlib.patches import Patch +from matplotlib.ticker import FuncFormatter + + +# Data +speed_rpm = np.linspace(0, 6000, 200) +speed_hz = speed_rpm / 60 + +# Natural frequency modes (Hz) - realistic gyroscopic effects +mode_1_bending = 18 + 0.004 * speed_rpm - 1.5e-7 * speed_rpm**2 +mode_2_bending = 48 - 0.003 * speed_rpm + 2.0e-7 * speed_rpm**2 +mode_1_torsional = 58 + 0.0004 * speed_rpm +mode_axial = 78 - 0.005 * speed_rpm + 4.0e-7 * speed_rpm**2 +mode_3_bending = 92 + 0.005 * speed_rpm - 3.5e-7 * speed_rpm**2 + +modes = [mode_1_bending, mode_2_bending, mode_1_torsional, mode_axial, mode_3_bending] +mode_labels = ["1st Bending", "2nd Bending", "1st Torsional", "Axial", "3rd Bending"] +mode_colors = ["#306998", "#E8833A", "#2B9EB3", "#984EA3", "#A65628"] + +engine_orders = [1, 2, 3] +eo_freq = {eo: eo * speed_hz for eo in engine_orders} + +# Find critical speed intersections via sign changes +op_min, op_max = 2500, 4500 +critical_speeds, critical_freqs, critical_mlabels = [], [], [] +for mode, mlabel in zip(modes, mode_labels, strict=True): + for eo in engine_orders: + diff = mode - eo * speed_hz + for idx in np.where(np.diff(np.sign(diff)))[0]: + t = abs(diff[idx]) / (abs(diff[idx]) + abs(diff[idx + 1])) + rpm = speed_rpm[idx] + t * (speed_rpm[idx + 1] - speed_rpm[idx]) + freq = mode[idx] + t * (mode[idx + 1] - mode[idx]) + if 100 < rpm < 5900: + critical_speeds.append(rpm) + critical_freqs.append(freq) + critical_mlabels.append(mlabel) + +# Plot +fig, ax = plt.subplots(figsize=(16, 9)) +y_max = 120 + +# Operating range shading +ax.axvspan(op_min, op_max, alpha=0.07, color="#306998", zorder=0) +ax.axvline(op_min, color="#306998", linewidth=1.2, linestyle=":", alpha=0.5, zorder=1) +ax.axvline(op_max, color="#306998", linewidth=1.2, linestyle=":", alpha=0.5, zorder=1) +ax.text( + (op_min + op_max) / 2, + 3, + "Operating Range", + fontsize=13, + color="#306998", + ha="center", + va="bottom", + fontstyle="italic", + alpha=0.6, +) + +# Mode curves +for mode, _label, color in zip(modes, mode_labels, mode_colors, strict=True): + ax.plot(speed_rpm, mode, linewidth=2.8, color=color, zorder=3, solid_capstyle="round") + +# End-of-line labels with vertical de-collision +end_vals = [(mode[-1], label, color) for mode, label, color in zip(modes, mode_labels, mode_colors, strict=True)] +end_vals.sort(key=lambda x: x[0]) +min_gap = 4.5 # minimum Hz gap between adjacent labels +positions = [v[0] for v in end_vals] +for i in range(1, len(positions)): + if positions[i] - positions[i - 1] < min_gap: + positions[i] = positions[i - 1] + min_gap +for y_pos, (_, label, color) in zip(positions, end_vals, strict=True): + ax.annotate( + label, + xy=(speed_rpm[-1], y_pos), + xytext=(8, 0), + textcoords="offset points", + fontsize=10, + color=color, + fontweight="bold", + va="center", + zorder=4, + ) + +# Engine order lines with rotated labels +for eo in engine_orders: + eo_line = eo_freq[eo] + visible = eo_line <= y_max + ax.plot( + speed_rpm[visible], eo_line[visible], linewidth=1.8, color="#AAAAAA", linestyle=(0, (8, 4)), alpha=0.6, zorder=2 + ) + target_freq = y_max * 0.28 + target_rpm = target_freq * 60 / eo + if target_rpm < 5800: + slope_display = (eo / 60) * (9 / y_max) / (16 / 6000) + angle_deg = np.degrees(np.arctan(slope_display)) + ax.annotate( + f"{eo}×", + xy=(target_rpm, target_freq), + fontsize=14, + color="#777777", + fontweight="bold", + ha="center", + va="bottom", + rotation=angle_deg, + rotation_mode="anchor", + zorder=4, + bbox={"boxstyle": "round,pad=0.15", "facecolor": "white", "edgecolor": "none", "alpha": 0.8}, + ) + +# Critical speed markers +cs_arr, cf_arr = np.array(critical_speeds), np.array(critical_freqs) +in_op = (cs_arr >= op_min) & (cs_arr <= op_max) + +if np.any(~in_op): + ax.scatter( + cs_arr[~in_op], cf_arr[~in_op], s=200, color="#D62728", edgecolors="white", linewidth=1.5, zorder=5, alpha=0.45 + ) + +if np.any(in_op): + ax.scatter( + cs_arr[in_op], cf_arr[in_op], s=350, color="#D62728", edgecolors="white", linewidth=2, zorder=6, marker="D" + ) + # Annotate critical intersections inside operating range with well-separated offsets + op_s, op_f, op_m = cs_arr[in_op], cf_arr[in_op], np.array(critical_mlabels)[in_op] + order = np.argsort(op_f) + n = len(order) + for rank, si in enumerate(order): + # Alternate left/right with increasing vertical spread to avoid overlap + sign = 1 if rank % 2 == 0 else -1 + dx = sign * 35 + dy = -30 + rank * (60 / max(n - 1, 1)) + ax.annotate( + op_m[si], + xy=(op_s[si], op_f[si]), + xytext=(dx, dy), + textcoords="offset points", + fontsize=11, + color="#B71C1C", + fontweight="bold", + arrowprops={"arrowstyle": "-|>", "color": "#B71C1C", "lw": 1.0, "shrinkB": 4}, + zorder=7, + bbox={ + "boxstyle": "round,pad=0.25", + "facecolor": "#FFF3F3", + "edgecolor": "#B71C1C", + "alpha": 0.9, + "linewidth": 0.7, + }, + ) + +# Style +ax.set_xlabel("Rotational Speed (RPM)", fontsize=20) +ax.set_ylabel("Frequency (Hz)", fontsize=20) +ax.set_title("campbell-basic · matplotlib · pyplots.ai", fontsize=24, fontweight="medium", pad=16) +ax.tick_params(axis="both", labelsize=16) +for spine in ("top", "right"): + ax.spines[spine].set_visible(False) +for spine in ("left", "bottom"): + ax.spines[spine].set_linewidth(0.6) + ax.spines[spine].set_color("#555555") +ax.set_xlim(0, 6000) +ax.set_ylim(0, y_max) +ax.yaxis.grid(True, alpha=0.15, linewidth=0.6, color="#CCCCCC") +ax.xaxis.grid(True, alpha=0.08, linewidth=0.4, color="#CCCCCC") + +# Format x-axis with thousand separator for readability +ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: f"{x:,.0f}")) + +# Compact two-column legend positioned in upper-left to avoid covering data +eo_handle = Line2D([0], [0], color="#AAAAAA", linewidth=1.8, linestyle=(0, (8, 4)), alpha=0.6) +crit_outside = Line2D( + [0], [0], marker="o", color="none", markerfacecolor="#D62728", markeredgecolor="white", markersize=10, alpha=0.5 +) +crit_inside = Line2D( + [0], [0], marker="D", color="none", markerfacecolor="#D62728", markeredgecolor="white", markersize=10 +) +op_handle = Patch(facecolor="#306998", alpha=0.12, edgecolor="none") + +handles = [Line2D([0], [0], color=c, linewidth=2.8) for c in mode_colors] + [ + eo_handle, + crit_outside, + crit_inside, + op_handle, +] +labels = mode_labels + ["Engine Order (1×–3×)", "Critical Speed", "Critical (op. range)", "Operating Range"] + +ax.legend( + handles, + labels, + fontsize=11, + loc="upper left", + ncol=2, + framealpha=0.92, + edgecolor="#DDDDDD", + borderpad=0.5, + labelspacing=0.4, + handlelength=1.4, + columnspacing=1.0, +) + +plt.tight_layout() +plt.savefig("plot.png", dpi=300, bbox_inches="tight") diff --git a/plots/campbell-basic/metadata/matplotlib.yaml b/plots/campbell-basic/metadata/matplotlib.yaml new file mode 100644 index 0000000000..0d4c7296ee --- /dev/null +++ b/plots/campbell-basic/metadata/matplotlib.yaml @@ -0,0 +1,237 @@ +library: matplotlib +specification_id: campbell-basic +created: '2026-02-15T21:08:00Z' +updated: '2026-02-15T21:36:08Z' +generated_by: claude-opus-4-5-20251101 +workflow_run: 22043026882 +issue: 4241 +python_version: 3.14.3 +library_version: 3.10.8 +preview_url: https://storage.googleapis.com/pyplots-images/plots/campbell-basic/matplotlib/plot.png +preview_thumb: https://storage.googleapis.com/pyplots-images/plots/campbell-basic/matplotlib/plot_thumb.png +preview_html: null +quality_score: 90 +review: + strengths: + - Excellent differentiation between critical speeds inside vs outside operating + range using marker shape, size, opacity, and annotation styling — creates strong + visual hierarchy + - Sophisticated end-of-line label de-collision algorithm prevents overlap at the + right edge + - Rotated engine order labels with white background boxes are elegant and readable + - Operating range shading with dotted boundaries is tasteful and informative + - Linear interpolation for finding exact intersection points is mathematically sound + - Comprehensive two-column legend covers all visual elements without cluttering + weaknesses: + - Right-edge mode labels for Axial and 1st Torsional are vertically close despite + de-collision logic — the minimum gap could be slightly larger + - Legend font size (11pt) is smaller than the recommended 16pt in library rules, + though appropriate given 9 legend entries + - Imports from three separate matplotlib submodules (lines, patches, ticker) adds + minor verbosity + image_description: The plot displays a Campbell Diagram with 5 natural frequency + mode curves (1st Bending in dark blue, 2nd Bending in orange, 1st Torsional in + cyan, Axial in purple, 3rd Bending in brown) plotted against rotational speed + from 0 to 6,000 RPM on the x-axis and frequency from 0 to 120 Hz on the y-axis. + Three dashed gray engine order lines (1x, 2x, 3x) extend diagonally from the origin + with rotated labels on white background boxes. A light blue shaded band marks + the operating range (2,500-4,500 RPM) with dotted vertical boundary lines and + an italic "Operating Range" label at the bottom. Critical speed intersections + outside the operating range appear as semi-transparent red circles, while those + inside appear as larger red diamond markers with annotated mode labels in red-bordered + boxes connected by arrow lines. End-of-line mode labels in bold matching colors + appear at the right edge with vertical de-collision. The title reads "campbell-basic + · matplotlib · pyplots.ai" in medium weight. A two-column legend in the upper-left + distinguishes all element types. Top and right spines are removed, subtle grids + are applied, and the x-axis uses thousand separators. + 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, labels 20pt, ticks 16pt, + legend 11pt, annotations 10-14pt' + - id: VQ-02 + name: No Overlap + score: 5 + max: 6 + passed: true + comment: De-collision logic for end-of-line labels works well; Axial and 1st + Torsional right-edge labels are close but readable + - id: VQ-03 + name: Element Visibility + score: 6 + max: 6 + passed: true + comment: Line widths of 2.8 well-suited; critical markers appropriately sized + (s=200/350) + - id: VQ-04 + name: Color Accessibility + score: 4 + max: 4 + passed: true + comment: Blue, orange, cyan, purple, brown palette is colorblind-distinguishable + - id: VQ-05 + name: Layout & Canvas + score: 3 + max: 4 + passed: true + comment: Good canvas utilization; right-edge labels extend slightly beyond + plot area + - id: VQ-06 + name: Axis Labels & Title + score: 2 + max: 2 + passed: true + comment: Rotational Speed (RPM) and Frequency (Hz) with units + design_excellence: + score: 16 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 7 + max: 8 + passed: true + comment: 'Near publication-ready: custom palette, rotated EO labels with white + boxes, differentiated critical speed markers' + - id: DE-02 + name: Visual Refinement + score: 5 + max: 6 + passed: true + comment: Subtle grids, spines removed/thinned, generous whitespace, elegant + operating range shading + - id: DE-03 + name: Data Storytelling + score: 4 + max: 6 + passed: true + comment: 'Clear visual hierarchy: diamond markers + annotations emphasize + critical speeds in operating range vs translucent circles outside' + 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 vs RPM overlay + - id: SC-02 + name: Required Features + score: 4 + max: 4 + passed: true + comment: 'All spec features: 5 modes, 3 engine orders, critical speed markers, + labels, legend, operating range' + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: X=RPM, Y=Hz correctly assigned + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 + passed: true + comment: Title format correct; comprehensive legend with all element types + data_quality: + score: 14 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 5 + max: 6 + passed: true + comment: Shows modes with varied gyroscopic behavior; both in-range and out-of-range + critical speeds visible + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: Authentic rotordynamics scenario with standard engineering mode names + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 4 + passed: true + comment: Baseline frequencies 18-92 Hz and 0-6000 RPM are realistic for rotating + machinery + code_quality: + score: 9 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 3 + max: 3 + passed: true + comment: 'Flat script: imports, data, plot, save' + - id: CQ-02 + name: Reproducibility + score: 2 + max: 2 + passed: true + comment: Deterministic data from formulas, no randomness + - id: CQ-03 + name: Clean Imports + score: 1 + max: 2 + passed: true + comment: All imports used; minor verbosity importing from separate matplotlib + submodules + - id: CQ-04 + name: Code Elegance + score: 2 + max: 2 + passed: true + comment: Clean intersection-finding with linear interpolation; Pythonic loops + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: Saves as plot.png, dpi=300, bbox_inches=tight + library_mastery: + score: 9 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 5 + max: 5 + passed: true + comment: Axes methods throughout, FuncFormatter, axvspan, proper tight_layout + - id: LM-02 + name: Distinctive Features + score: 4 + max: 5 + passed: true + comment: Custom legend with Line2D/Patch handles, FuncFormatter, rotation_mode + anchor, axvspan + verdict: APPROVED +impl_tags: + dependencies: [] + techniques: + - annotations + - custom-legend + patterns: + - data-generation + - iteration-over-groups + - explicit-figure + dataprep: + - interpolation + styling: + - alpha-blending + - grid-styling + - edge-highlighting