Skip to content

Fix Nyquist frequency and the P(s)Z(s) exactness claim in the ZoH example - #1068

Merged
baggepinnen merged 1 commit into
masterfrom
docs/zoh-nyquist-freq
Jul 31, 2026
Merged

Fix Nyquist frequency and the P(s)Z(s) exactness claim in the ZoH example#1068
baggepinnen merged 1 commit into
masterfrom
docs/zoh-nyquist-freq

Conversation

@baggepinnen

Copy link
Copy Markdown
Member

Draft. Three related fixes to docs/src/examples/zoh.md.

1. The Nyquist frequency marker was off by a factor of 2π

Both Bode plots on the page draw the marker with vline!([0.5 0.5], lab="Nyquist freq."). The bodeplot x-axis is rad/s (hz=false by default), so with Ts = 1 the Nyquist frequency is π/Ts ≈ 3.14 rad/s. 0.5 is that frequency in Hz.

The page was already internally inconsistent about this — further down it says "(Nyquist frequency is π rad/s)", and _default_freq_vector in lib/ControlSystemsBase/src/freqresp.jl uses log10(π/sys.Ts) with the comment "Draw up to Nyquist frequency for discrete systems".

Both markers now use π/Ts.

2. P(s)Z(s) does not match P_d(z) exactly

The first plot only extended to ω = 1 rad/s — 0.32× the actual Nyquist frequency. Over that range the two agree to 0.4%, invisible on a log-magnitude plot, and the text concluded they "match exactly".

They don't. ZoH sampling folds the response at every ω + kω_s down onto ω, so the exact relation is the aliasing sum

P_d(exp(iωTs)) = Σ_k  Z(i(ω + kω_s)) · P(i(ω + kω_s)),   ω_s = 2π/Ts

and P(s)Z(s) is only the k = 0 term. Verified against c2d to ~1e-14 relative error. For this page's P = tf(0.1,[1,0.1,0.1]), Ts = 1, the k = 0 truncation error is:

ω vs. Nyquist error of P·Z
1.0 (old plot limit) 0.32× 0.4%
1.22 0.39× 1%
2.0 0.64× 10%
2.985 0.95× 269%

The frequency axis now runs up to the Nyquist frequency, which makes the divergence plainly visible, and the text states the aliasing sum and points to d2c_exact for work near Nyquist.

Both plots become considerably more informative as a side effect — in the Discrete to continuous plot the marker now lands exactly on the fold, with the aliased image above it, which is what that section's prose is actually describing.

3. Same overstatement in the time domain

The continuous-sinusoid comparison said the output of Pz "matches that of Pd exactly". At the input frequency used there the steady-state amplitudes differ by about 1% — the same aliasing contribution. Softened, and phrased without a hard-coded number so it does not go stale if the input frequency is changed.

Left alone

The d2c_exact claim in the same file is correct and unchanged: it reproduces the discrete frequency response to ~1e-14 across the whole axis including above Nyquist, since it is an algebraic z^{-1} = e^{-sTs} substitution.

Verification

Both edited @example blocks were executed in a real session with ControlSystems + Plots and render as expected; the aliasing sum, the truncation errors, and the d2c_exact exactness were each checked numerically. I did not run a full docs/make.jl build (that environment needs Ipopt/GCMAES/etc.), but the only new Documenter construct is a same-page section @ref, a pattern already used elsewhere in these docs, and makedocs here sets warnonly = [:missing_docs, :cross_references].

🤖 Generated with Claude Code

https://claude.ai/code/session_01VaQczXaXuF3KW5Lu9yMMD8

…mple

The "Nyquist freq." marker was drawn at 0.5 in both Bode plots on this page,
but the bodeplot x-axis is rad/s, so for Ts = 1 the Nyquist frequency is
pi/Ts ~ 3.14 rad/s. 0.5 is the Nyquist frequency expressed in Hz, off by a
factor 2*pi. Elsewhere on the same page the text already states "Nyquist
frequency is pi rad/s", so the page was internally inconsistent. Both markers
now use pi/Ts.

The first plot also only extended to omega = 1 rad/s, i.e. 0.32x the actual
Nyquist frequency. Over that range P(s)Z(s) and Pd(z) agree to within 0.4%,
which is invisible on a log-magnitude Bode plot, and the page concluded from
this that the two "match exactly". They do not. ZoH sampling folds the
response at all frequencies omega + k*omega_s down onto omega, so the exact
relation is the aliasing sum

    Pd(exp(i*omega*Ts)) = sum_k Z(i(omega + k*omega_s)) P(i(omega + k*omega_s))

of which P(s)Z(s) is only the k = 0 term. For the example on this page the
k = 0 truncation is within 1% up to 0.39x the Nyquist frequency, is off by
10% at omega = 2, and by 269% at 0.95x Nyquist. The frequency axis now runs
up to the Nyquist frequency so the divergence is visible, and the text states
the aliasing sum and points to d2c_exact for accurate work near Nyquist.

The same overstatement applied to the continuous-sinusoid time-domain
comparison further down (the amplitudes differ by ~1% at the input frequency
used there), so that claim is softened too.

The corresponding claim about d2c_exact is correct and is left unchanged --
it matches the discrete frequency response to ~1e-14 over the whole axis,
including above Nyquist, since it is an algebraic substitution.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VaQczXaXuF3KW5Lu9yMMD8
@JuliaControlBot

Copy link
Copy Markdown

This is an automated message.
Plots were compared to references. 4/11 images have changed, see differences below.
After pulling this PR, please update the reference images by creating a PR to ControlExamplePlots.jl here.

Difference Reference Image New Image
✔️ 0.0 Reference New
✔️ 0.0 Reference New
✔️ 0.0 Reference New
✔️ 0.0 Reference New

@baggepinnen
baggepinnen marked this pull request as ready for review July 31, 2026 10:12
@baggepinnen
baggepinnen merged commit 4784e94 into master Jul 31, 2026
2 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants