Skip to content

Commit 7012118

Browse files
feat(seaborn): implement nyquist-basic (#5141)
## Implementation: `nyquist-basic` - seaborn Implements the **seaborn** version of `nyquist-basic`. **File:** `plots/nyquist-basic/implementations/seaborn.py` **Parent Issue:** #4412 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/23365453301)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent dbf6dff commit 7012118

2 files changed

Lines changed: 370 additions & 0 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
""" pyplots.ai
2+
nyquist-basic: Nyquist Plot for Control Systems
3+
Library: seaborn 0.13.2 | Python 3.14.3
4+
Quality: 90/100 | Created: 2026-03-20
5+
"""
6+
7+
import matplotlib.pyplot as plt
8+
import numpy as np
9+
import pandas as pd
10+
import seaborn as sns
11+
from scipy import signal
12+
13+
14+
# Data — open-loop transfer function: G(s) = 10 / ((s+1)(0.5s+1)(0.2s+1))
15+
num = [10.0]
16+
den = np.polymul(np.polymul([1.0, 1.0], [0.5, 1.0]), [0.2, 1.0])
17+
system = signal.TransferFunction(num, den)
18+
19+
omega = np.logspace(-2, 2, 800)
20+
_, H = signal.freqresp(system, omega)
21+
22+
real_part = H.real
23+
imag_part = H.imag
24+
25+
# Build DataFrame for seaborn-idiomatic plotting
26+
df_pos = pd.DataFrame({"Real": real_part, "Imaginary": imag_part, "Branch": "G(jω), ω ≥ 0"})
27+
df_neg = pd.DataFrame({"Real": real_part, "Imaginary": -imag_part, "Branch": "G(jω), ω < 0"})
28+
df = pd.concat([df_pos, df_neg], ignore_index=True)
29+
30+
# Seaborn theme and palette
31+
palette = sns.color_palette(["#306998", "#306998"])
32+
sns.set_theme(
33+
style="whitegrid",
34+
context="talk",
35+
font_scale=1.2,
36+
rc={"grid.alpha": 0.15, "grid.linewidth": 0.8, "axes.edgecolor": "#bbbbbb"},
37+
)
38+
39+
fig, ax = plt.subplots(figsize=(10, 10))
40+
41+
# Plot both branches using hue-based seaborn lineplot
42+
sns.lineplot(
43+
data=df,
44+
x="Real",
45+
y="Imaginary",
46+
hue="Branch",
47+
palette=palette,
48+
linewidth=2.5,
49+
sort=False,
50+
estimator=None,
51+
style="Branch",
52+
dashes={"G(jω), ω ≥ 0": "", "G(jω), ω < 0": (5, 3)},
53+
ax=ax,
54+
legend=True,
55+
)
56+
57+
# Adjust mirror branch alpha via the line artist
58+
lines = ax.get_lines()
59+
for line in lines:
60+
if line.get_linestyle() != "-":
61+
line.set_alpha(0.4)
62+
63+
# Unit circle
64+
theta = np.linspace(0, 2 * np.pi, 200)
65+
ax.plot(np.cos(theta), np.sin(theta), color="#999999", linewidth=1.2, linestyle=":", alpha=0.6, zorder=1)
66+
67+
# Critical point (-1, 0)
68+
crit_palette = sns.color_palette(["#cc3333"])
69+
ax.plot(-1, 0, marker="x", color=crit_palette[0], markersize=16, markeredgewidth=3, zorder=5)
70+
ax.annotate(
71+
"Critical point (-1, 0)",
72+
xy=(-1, 0),
73+
xytext=(-1.8, 2.5),
74+
fontsize=14,
75+
color=crit_palette[0],
76+
fontweight="bold",
77+
arrowprops={"arrowstyle": "->", "color": crit_palette[0], "lw": 1.5},
78+
)
79+
80+
# Direction arrows along the curve
81+
arrow_indices = [80, 250, 450]
82+
for idx in arrow_indices:
83+
ax.annotate(
84+
"",
85+
xy=(real_part[idx + 8], imag_part[idx + 8]),
86+
xytext=(real_part[idx], imag_part[idx]),
87+
arrowprops={"arrowstyle": "->", "color": "#306998", "lw": 2.5},
88+
)
89+
90+
# Frequency annotations at key points — use seaborn scatterplot for markers
91+
freq_labels = [0.1, 0.5, 1.0, 3.0, 10.0]
92+
freq_indices = [np.argmin(np.abs(omega - f)) for f in freq_labels]
93+
freq_df = pd.DataFrame(
94+
{
95+
"Real": [real_part[i] for i in freq_indices],
96+
"Imaginary": [imag_part[i] for i in freq_indices],
97+
"Frequency": [f"ω = {f} rad/s" for f in freq_labels],
98+
}
99+
)
100+
sns.scatterplot(
101+
data=freq_df,
102+
x="Real",
103+
y="Imaginary",
104+
color="#306998",
105+
s=120,
106+
zorder=4,
107+
ax=ax,
108+
legend=False,
109+
edgecolor="white",
110+
linewidth=1.5,
111+
)
112+
113+
# Annotate frequency points with manual offsets to avoid crowding
114+
offsets = {0.1: (0.5, -0.8), 0.5: (0.6, -0.9), 1.0: (0.6, -0.8), 3.0: (0.8, -1.2), 10.0: (1.0, 0.8)}
115+
for i, (_, row) in enumerate(freq_df.iterrows()):
116+
x, y = row["Real"], row["Imaginary"]
117+
f = freq_labels[i]
118+
ox, oy = offsets[f]
119+
ax.annotate(
120+
row["Frequency"],
121+
xy=(x, y),
122+
xytext=(x + ox, y + oy),
123+
fontsize=12,
124+
color="#444444",
125+
arrowprops={"arrowstyle": "->", "color": "#aaaaaa", "lw": 1},
126+
)
127+
128+
# Style — use seaborn's despine
129+
ax.set_xlabel("Real Part of G(jω)", fontsize=20)
130+
ax.set_ylabel("Imaginary Part of G(jω)", fontsize=20)
131+
ax.set_title("nyquist-basic · seaborn · pyplots.ai", fontsize=24, fontweight="medium")
132+
ax.tick_params(axis="both", labelsize=16)
133+
ax.set_aspect("equal")
134+
ax.axhline(y=0, color="#cccccc", linewidth=0.8, zorder=0)
135+
ax.axvline(x=0, color="#cccccc", linewidth=0.8, zorder=0)
136+
sns.despine(ax=ax)
137+
138+
# Refine legend — position in upper right away from data
139+
ax.legend(loc="upper right", framealpha=0.9, edgecolor="#cccccc", fontsize=14)
140+
141+
plt.tight_layout()
142+
plt.savefig("plot.png", dpi=300, bbox_inches="tight")
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
library: seaborn
2+
specification_id: nyquist-basic
3+
created: '2026-03-20T22:46:01Z'
4+
updated: '2026-03-20T23:25:04Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 23365453301
7+
issue: 4412
8+
python_version: 3.14.3
9+
library_version: 0.13.2
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/nyquist-basic/seaborn/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/nyquist-basic/seaborn/plot_thumb.png
12+
preview_html: null
13+
quality_score: 90
14+
review:
15+
strengths:
16+
- All specification requirements fully implemented with careful attention to detail
17+
- 'Excellent visual hierarchy: critical point prominently marked, mirror branch
18+
de-emphasized, directional arrows guide the viewer'
19+
- Realistic and well-chosen transfer function producing a pedagogically clear Nyquist
20+
curve
21+
- Clean deterministic code with proper scipy.signal usage for frequency response
22+
computation
23+
- Cohesive color scheme and professional polish with subtle grid, removed spines,
24+
and balanced layout
25+
weaknesses:
26+
- Minor annotation crowding near the origin where multiple frequency labels converge
27+
- Seaborn role is somewhat limited since Nyquist plots require specialized matplotlib
28+
features for annotations, arrows, and geometric references
29+
image_description: The plot displays a Nyquist diagram on the complex plane. The
30+
positive-frequency branch (solid dark blue line, labeled "G(jω), ω ≥ 0") sweeps
31+
from approximately (10, 0) at low frequencies downward through the lower half-plane,
32+
curving left and back toward the origin at high frequencies. The mirror (negative-frequency)
33+
branch (dashed lighter blue, labeled "G(jω), ω < 0") reflects symmetrically through
34+
the upper half-plane with reduced opacity. A dotted gray unit circle is centered
35+
at the origin for reference. The critical point at (-1, 0) is marked with a bold
36+
red X and annotated with a red arrow and "Critical point (-1, 0)" label. Five
37+
frequency points (ω = 0.1, 0.5, 1.0, 3.0, 10.0 rad/s) are marked with teal dots
38+
and labeled via gray annotation arrows. Three blue directional arrows along the
39+
positive branch indicate increasing frequency. Title reads "nyquist-basic · seaborn
40+
· pyplots.ai". Axes are labeled "Real Part of G(jω)" and "Imaginary Part of G(jω)".
41+
Legend in upper right with both branch labels. Top and right spines removed. Subtle
42+
whitegrid with low-alpha gridlines. Square aspect ratio maintained.
43+
criteria_checklist:
44+
visual_quality:
45+
score: 29
46+
max: 30
47+
items:
48+
- id: VQ-01
49+
name: Text Legibility
50+
score: 8
51+
max: 8
52+
passed: true
53+
comment: 'All font sizes explicitly set: title 24pt, labels 20pt, ticks 16pt,
54+
annotations 12-14pt'
55+
- id: VQ-02
56+
name: No Overlap
57+
score: 5
58+
max: 6
59+
passed: true
60+
comment: Manual offsets prevent most collisions; minor crowding near origin
61+
between frequency labels
62+
- id: VQ-03
63+
name: Element Visibility
64+
score: 6
65+
max: 6
66+
passed: true
67+
comment: Lines at 2.5 width, markers s=120 with white edges, directional arrows
68+
prominent
69+
- id: VQ-04
70+
name: Color Accessibility
71+
score: 4
72+
max: 4
73+
passed: true
74+
comment: Blue/red/gray scheme is colorblind-safe with no red-green ambiguity
75+
- id: VQ-05
76+
name: Layout & Canvas
77+
score: 4
78+
max: 4
79+
passed: true
80+
comment: Square 10x10 figure with equal aspect ratio, good canvas utilization
81+
- id: VQ-06
82+
name: Axis Labels & Title
83+
score: 2
84+
max: 2
85+
passed: true
86+
comment: 'Descriptive labels with mathematical notation: Real Part of G(jω)'
87+
design_excellence:
88+
score: 15
89+
max: 20
90+
items:
91+
- id: DE-01
92+
name: Aesthetic Sophistication
93+
score: 6
94+
max: 8
95+
passed: true
96+
comment: Cohesive color scheme, intentional hierarchy with alpha-reduced mirror
97+
branch, professional typography
98+
- id: DE-02
99+
name: Visual Refinement
100+
score: 5
101+
max: 6
102+
passed: true
103+
comment: Spines removed, grid alpha 0.15, axis reference lines, generous whitespace
104+
- id: DE-03
105+
name: Data Storytelling
106+
score: 4
107+
max: 6
108+
passed: true
109+
comment: 'Good visual hierarchy: critical point emphasized, mirror branch
110+
de-emphasized, directional arrows guide viewer'
111+
spec_compliance:
112+
score: 15
113+
max: 15
114+
items:
115+
- id: SC-01
116+
name: Plot Type
117+
score: 5
118+
max: 5
119+
passed: true
120+
comment: Correct Nyquist plot on complex plane
121+
- id: SC-02
122+
name: Required Features
123+
score: 4
124+
max: 4
125+
passed: true
126+
comment: 'All features present: critical point, unit circle, freq annotations,
127+
arrows, 1:1 aspect'
128+
- id: SC-03
129+
name: Data Mapping
130+
score: 3
131+
max: 3
132+
passed: true
133+
comment: Real on X, imaginary on Y, both branches correctly computed
134+
- id: SC-04
135+
name: Title & Legend
136+
score: 3
137+
max: 3
138+
passed: true
139+
comment: Correct title format, legend labels match branch descriptions
140+
data_quality:
141+
score: 15
142+
max: 15
143+
items:
144+
- id: DQ-01
145+
name: Feature Coverage
146+
score: 6
147+
max: 6
148+
passed: true
149+
comment: Full frequency sweep, both branches, gain/phase characteristics visible
150+
- id: DQ-02
151+
name: Realistic Context
152+
score: 5
153+
max: 5
154+
passed: true
155+
comment: Realistic 3rd-order control system G(s) = 10/((s+1)(0.5s+1)(0.2s+1))
156+
- id: DQ-03
157+
name: Appropriate Scale
158+
score: 4
159+
max: 4
160+
passed: true
161+
comment: Frequency range 0.01-100 rad/s appropriate for this system
162+
code_quality:
163+
score: 10
164+
max: 10
165+
items:
166+
- id: CQ-01
167+
name: KISS Structure
168+
score: 3
169+
max: 3
170+
passed: true
171+
comment: 'Linear flow: imports, data, plot, save. No functions or classes'
172+
- id: CQ-02
173+
name: Reproducibility
174+
score: 2
175+
max: 2
176+
passed: true
177+
comment: Fully deterministic via scipy.signal.freqresp with fixed coefficients
178+
- id: CQ-03
179+
name: Clean Imports
180+
score: 2
181+
max: 2
182+
passed: true
183+
comment: 'All five imports used: matplotlib, numpy, pandas, seaborn, scipy'
184+
- id: CQ-04
185+
name: Code Elegance
186+
score: 2
187+
max: 2
188+
passed: true
189+
comment: Clean, well-organized, no over-engineering or fake functionality
190+
- id: CQ-05
191+
name: Output & API
192+
score: 1
193+
max: 1
194+
passed: true
195+
comment: Saves as plot.png at dpi=300 with bbox_inches=tight
196+
library_mastery:
197+
score: 6
198+
max: 10
199+
items:
200+
- id: LM-01
201+
name: Idiomatic Usage
202+
score: 4
203+
max: 5
204+
passed: true
205+
comment: 'Good seaborn usage: lineplot with hue/style, scatterplot, set_theme,
206+
despine, color_palette'
207+
- id: LM-02
208+
name: Distinctive Features
209+
score: 2
210+
max: 5
211+
passed: true
212+
comment: Uses seaborn theming and hue-based styling but bulk requires matplotlib
213+
for specialized features
214+
verdict: APPROVED
215+
impl_tags:
216+
dependencies:
217+
- scipy
218+
techniques:
219+
- annotations
220+
- custom-legend
221+
patterns:
222+
- data-generation
223+
- explicit-figure
224+
dataprep: []
225+
styling:
226+
- grid-styling
227+
- alpha-blending
228+
- edge-highlighting

0 commit comments

Comments
 (0)