Skip to content
Menu

3. Laplace and Z Transforms

The Laplace and Z transforms turn hard differential/difference-equation problems into simpler algebraic ones, in continuous and discrete time respectively — the essential toolkit before building state-space models.

PowerPoint handouts: Laplace Transform (light) · Inverse Laplace Transform (dark) · Inverse Laplace Transform (light) · Z Transform (dark) · Z Transform (light)

link to download code

Using sympy’s symbolic engine, this verifies the basic Laplace-transform pairs derived earlier in the chapter: the exponential e^(-at), a specific exponential e^(-2t), the ramp 2t, and the unit impulse — each matching the closed-form results worked out by hand.

import sympy
t, s = sympy.symbols('t, s')
a = sympy.symbols('a', real=True, positive=True)
f = sympy.exp(-a * t)
F = sympy.laplace_transform(f, t, s, noconds=True)
print("L{exp(-a*t)} =", F)
g = sympy.exp(-2 * t)
G = sympy.laplace_transform(g, t, s, noconds=True)
print("L{exp(-2*t)} =", G)
h = 2 * t
H = sympy.laplace_transform(h, t, s, noconds=True)
print("L{2*t} =", H)
d = sympy.DiracDelta(t)
D = sympy.laplace_transform(d, t, s, noconds=True)
print("L{delta(t)} =", D)

Basic Laplace transforms (mcimp/codes/laplaceZtransforms/simplelaplace.py)

Click "Run" to execute.
link to download code

A time delay in the time domain corresponds to multiplication by e^(-sτ) in the s-domain. This confirms that a unit impulse delayed by 4 seconds, δ(t−4), transforms to e^(−4s), exactly as the shift property predicts.

import sympy
t, s = sympy.symbols('t, s')
d = sympy.DiracDelta(t - 4)
D = sympy.laplace_transform(d, t, s, noconds=True)
print("L{delta(t - 4)} =", D)

Time-shift property (mcimp/codes/laplaceZtransforms/laplaceTimeDelay.py)

Click "Run" to execute.
link to download code

Breaking F(s) = 32 / [s(s+4)(s+8)] into simple first-order terms lets you read off the inverse Laplace transform term by term from a lookup table, rather than evaluating the inversion integral directly. sympy.apart() confirms the by-hand residues K1=1, K2=-2, K3=1 from the text.

import sympy
s = sympy.symbols('s')
G = 32 / s / (s + 4) / (s + 8)
print("F(s) =", G)
print("Partial fraction expansion:", sympy.apart(G))

Partial fraction expansion (mcimp/codes/laplaceZtransforms/partial_fraction_expansion.py)

Click "Run" to execute.
link to download code

Builds a second-order transfer function G(s) = (s+2)/(s²+2s+3), reports its poles and zeros, then plots its step response, impulse response, and its response to two different forced inputs (a constant and a sine wave) side by side — illustrating that a transfer function’s poles set the response’s natural modes regardless of which input drives it.

import control as ct
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import io, base64
num = [1, 2] # Numerator coefficients
den = [1, 2, 3] # Denominator coefficients
sys_tf = ct.tf(num, den)
print(sys_tf)
poles = ct.poles(sys_tf)
zeros = ct.zeros(sys_tf)
print("System Poles =", poles, "\nSystem Zeros =", zeros)
T, yout = ct.step_response(sys_tf)
T, yout_i = ct.impulse_response(sys_tf)
u1 = np.full(len(T), 2)
u2 = np.sin(T)
_, yout_u1 = ct.forced_response(sys_tf, T, u1)
_, yout_u2 = ct.forced_response(sys_tf, T, u2)
fig, axs = plt.subplots(1, 3, figsize=(12, 3.5))
axs[0].plot(T, yout); axs[0].set_title("Step response"); axs[0].grid(True)
axs[1].plot(T, yout_i); axs[1].set_title("Impulse response"); axs[1].grid(True)
axs[2].plot(T, yout_u1, label="Input 1 (const = 2)")
axs[2].plot(T, yout_u2, label="Input 2 (sin)")
axs[2].set_title("Forced response"); axs[2].legend(); axs[2].grid(True)
for ax in axs:
ax.set_xlabel("Time (sec)")
plt.tight_layout()
buf = io.BytesIO()
plt.savefig(buf, format="png", dpi=100)
plot_b64 = base64.b64encode(buf.getvalue()).decode()

Transfer function analysis (mcimp/codes/laplaceZtransforms/transfer_fun.py)

Click "Run" to execute.
link to download code

The DC gain is the steady-state ratio of output to a constant input, equal to G(s) evaluated at s=0 (or, via the Final Value Theorem, lim_(s→0) sG(s)/s). For G(s)=(2s+3)/(4s²+3s+1), that’s b₀/a₀ = 3/1 = 3.

import control as ct
s = ct.tf('s')
G = (2 * s + 3) / (4 * s**2 + 3 * s + 1)
print("DC gain of G(s) =", ct.dcgain(G))

DC gain (mcimp/codes/laplaceZtransforms/CTDCgain.py)

Click "Run" to execute.
link to download code

The DC-gain formula is only meaningful for a stable system. H(s)=3/(s−2) has a pole at s=2 (right-half plane), so even though the formula still returns a number (−1.5), the actual step response never converges to it — it diverges, as the growing output samples below make clear.

import control as ct
H = ct.tf([0, 3], [1, -2])
print("DC gain of H(s) =", ct.dcgain(H))
T, yout = ct.step_response(H)
print("\nH(s) is unstable, so the step response never actually settles at the DC gain:")
print(" first 5 samples:", [round(float(v), 3) for v in yout[:5]])
print(" last 5 samples: ", [round(float(v), 3) for v in yout[-5:]])
print(f" magnitude keeps growing -- by t={T[-1]:.2f}s it reaches", round(float(max(abs(yout))), 1))

DC gain caution: unstable systems (mcimp/codes/laplaceZtransforms/CTDCgain_caution.py)

Click "Run" to execute.
link to download code

The Z transform of the geometric sequence (a^k) is derived directly from its definition, ∑ a^k z^-k, a geometric series converging to 1/(1−az⁻¹) = z/(z−a) whenever |a/z| < 1. Setting a=1 recovers the Z transform of the discrete-time unit step.

import sympy
z, k, a = sympy.symbols('z k a', positive=True)
# Z{a^k} straight from the definition, sum_{k=0}^inf a^k z^-k = 1/(1 - a/z),
# valid for |a/z| < 1 -- the same geometric-series convergence condition
# used in the text. (Computed via sympy directly rather than lcapy, so this
# demo only needs pyodide-native packages.)
gamma = a / z
F = sympy.simplify(1 / (1 - gamma))
print("Z{a^k} =", F)
# Special case a = 1: the discrete-time unit step sequence
print("Z{1(k)} = Z{a^k}|a=1 =", F.subs(a, 1))

Z transform of a geometric sequence (mcimp/codes/laplaceZtransforms/simpleZtransform.py)

Click "Run" to execute.
link to download code

Multiplying a sequence by k in the time domain corresponds to differentiating its Z transform: Z(k f(k)) = −z dF(z)/dz. Applied to f(k)=a^k and checked at the book’s worked value a=0.5, this reproduces the time-scaled geometric sequence’s transform.

import sympy
z, k, a = sympy.symbols('z k a', positive=True)
# Base transform: Z{a^k} = z/(z - a)
gamma = a / z
F = sympy.simplify(1 / (1 - gamma))
print("Z{a^k} =", F)
# Differentiation property: Z{k f(k)} = -z dF(z)/dz
F1 = sympy.simplify(-z * sympy.diff(F, z))
print("Z{k a^k} =", F1)
print("\nWith a = 0.5 (the book's worked example):")
print(" Z{0.5^k} =", F.subs(a, sympy.Rational(1, 2)))
print(" Z{k*0.5^k} =", F1.subs(a, sympy.Rational(1, 2)))

Z-domain differentiation property (mcimp/codes/laplaceZtransforms/timescaledGeometricSeqZ.py)

Click "Run" to execute.
link to download code

The discrete-time counterpart of a transfer function needs an explicit sampling time Ts alongside its numerator/denominator coefficients. This builds a sampled second-order system, reports its poles/zeros, and plots its step response as a staircase — reflecting that a discrete-time system’s output only changes at each sampling instant.

import control as ct
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import io, base64
Ts = 0.1 # sampling time
num = [0.09952, -0.08144]
den = [1, -1.792, 0.8187]
sys_tf = ct.tf(num, den, Ts)
print(sys_tf)
poles = ct.poles(sys_tf)
zeros = ct.zeros(sys_tf)
print("System Poles =", poles, "\nSystem Zeros =", zeros)
T, yout = ct.step_response(sys_tf)
fig, ax = plt.subplots(figsize=(6, 4))
# a stairs plot shows the discrete-time nature of the response; shifting by
# one sample makes the initial one-step delay visible, matching the book
ax.step(T, np.append(0, yout[0:-1]))
ax.grid(True)
ax.set_xlabel("Time (sec)")
ax.set_ylabel("y")
plt.tight_layout()
buf = io.BytesIO()
plt.savefig(buf, format="png", dpi=100)
plot_b64 = base64.b64encode(buf.getvalue()).decode()

Discrete-time transfer function (mcimp/codes/laplaceZtransforms/transfer_fun_dt.py)

Click "Run" to execute.

← All chapters