Level 2 · Signals, Systems & Control: Systems and Responses
Level 1 simulated systems one step at a time. This level asks what can be known about a system as a whole. For the linear, time-invariant (LTI) systems the answer is remarkable: one list of numbers, the system's answer to a single short tap, predicts its answer to everything. Combining that list with an input is convolution, and the first half builds on it: echoes, filters, step responses, stability and chains of systems.
The second half writes systems as difference equations, which compute each output from a few earlier outputs and inputs, and reads their step responses: the time constant, rise time and final value of a first-order system, then a mass on a spring with a damper, which can swing past its target. Level 1's moving average turns out to be one of this level's filters, its Euler step for the tea a difference equation, and its percent overshoot a measure of damping.
The other paths meet these ideas too. A convolution is a pair of nested loops, O(n · m) in the language of the Algorithms path; fitting a model to a recorded response is the least-squares line of the Statistics path; and in the Machine Learning path a convolutional network learns its own short kernels, while gradient descent with momentum, v = beta * v + slope, is a first-order difference equation.
Linear and time-invariant
A system turns an input list x into an output list y. Two properties make it easy to understand:
- Linear: twice the input gives twice the output, and two inputs together give the sum of their separate outputs (superposition). In particular, silence in gives silence out.
- Time-invariant: it behaves the same today as tomorrow, so delaying the input only delays the output.
A test can disprove a property but never prove it, so a real check tries many inputs; here one input catches two of three systems out:
def fahrenheit(x): # a thermometer showing °F
return [1.8 * v + 32 for v in x]
def fade_in(x): # a DJ's fade-in over 4 samples
return [min(k / 4, 1) * v for k, v in enumerate(x)]
def echo(x): # y[k] = x[k] + 0.5 * x[k - 2]
return [v + (0.5 * x[k - 2] if k >= 2 else 0) for k, v in enumerate(x)]
x = [1, 2, 0, -1]
for name, s in (("fahrenheit", fahrenheit), ("fade_in", fade_in), ("echo", echo)):
scales = s([2 * v for v in x]) == [2 * v for v in s(x)]
shifts = s([0] + x)[1:] == s(x)
print(f"{name:10} doubles with the input: {scales!s:5} a delay only delays: {shifts}")
fahrenheit doubles with the input: False a delay only delays: True
fade_in doubles with the input: True a delay only delays: False
echo doubles with the input: True a delay only delays: True
The converter's + 32 turns 0 °C into 32 °F: not linear. The fade only multiplies, so it is linear, but the same note at sample 0 and at sample 6 comes out at different volumes: not time-invariant. The echo passes both. Real systems are LTI only approximately: a loudspeaker at modest volume, a bridge vibrating a little.
Common mistakes: concluding "linear" from one passing test. Comparing floats with == when the arithmetic can round.
Practice: Testing the Sealed Boxes
The impulse response
The unit impulse [1, 0, 0, ...] is the shortest possible tap, and what comes back is the impulse response h: for the echo above, [1, 0, 0.5, 0, ...]. For an LTI system h is a complete fingerprint: any input is a row of scaled, delayed impulses, so the output is the same row of scaled, delayed copies of h, added up. After one tablet of a medicine, the concentration in the blood follows h; a second tablet three hours later adds a second copy:
h = [0, 4, 6, 5, 3.5, 2, 1, 0.5] # mg/L in the blood, hour by hour, after one tablet
n = 12
first = h + [0] * (n - len(h))
second = [0] * 3 + h + [0] * (n - 3 - len(h))
both = [a + b for a, b in zip(first, second)]
print(both)
plot(first, kind="stem", label="tablet at hour 0")
plot(second, kind="stem", label="tablet at hour 3")
plot(both, label="both", xlabel="hour", ylabel="mg/L")
[0, 4, 6, 5, 7.5, 8, 6, 4.0, 2, 1, 0.5, 0]
Press Run: the line is the two stem patterns added. The peak, 8 mg/L at hour 5, is more than one tablet gives, because the second tablet's rise lands on the first one's tail. A pharmacist checks a dosing plan with this sum, needing only h.
Practice: Footsteps on a Footbridge
Convolution
Adding all the copies at once is convolution, written y = x * h (not multiplication). Output k collects each input x_j times the part of the response that has had k - j samples to develop:
the sum running over every j for which both x_j and h_{k-j} exist. In Python the sum is a generator with that condition as its if, and the list of outputs is a comprehension over k, one sum per output:
In a car park, arrivals[j] cars drive in during 10-minute interval j and stay[i] is the share still parked i intervals later, so the number of parked cars is a convolution (x is arrivals, h is stay):
arrivals = [5, 8, 3, 0, 2] # cars arriving in each 10-minute interval
stay = [1, 0.8, 0.5, 0.2] # share of a car still parked 0, 1, 2, 3 intervals after arriving
n = len(arrivals) + len(stay) - 1
parked = [sum(arrivals[j] * stay[k - j] for j in range(len(arrivals)) if 0 <= k - j < len(stay))
for k in range(n)]
print(n, "values:", [round(p, 2) for p in parked])
print("interval 2:", arrivals[0] * stay[2], "+", arrivals[1] * stay[1], "+", arrivals[2] * stay[0])
8 values: [5, 12.0, 11.9, 7.4, 5.1, 2.2, 1.0, 0.4]
interval 2: 2.5 + 6.4 + 3
The second line is "flip and slide": write stay backwards under arrivals with stay[0] under arrivals[2], multiply the overlapping pairs and add; slide one place for the next sample. The full output has len(x) + len(h) - 1 samples, because the last cars stay after the arrivals stop. Swapping the lists changes nothing, x * h = h * x. When h is mostly zeros (an echo a second late), only its non-zero entries need visiting.
Same length. A smoothed photo must keep its number of pixels, but the full output is longer, with features late by the kernel's centre position. The same-length output drops that many samples from the front and keeps len(x). For an odd kernel the centre is tap (len(h) - 1) // 2, here 2:
row = [10, 10, 10, 90, 10, 10, 10] # brightness along one row of a photo: a thin bright line
kernel = [1/9, 2/9, 3/9, 2/9, 1/9] # a blur
full = [sum(row[j] * kernel[k - j] for j in range(len(row)) if 0 <= k - j < len(kernel))
for k in range(len(row) + len(kernel) - 1)]
same = full[2:2 + len(row)]
print("same:", [round(v, 1) for v in same])
print("brightest pixel: row", row.index(max(row)), " full", full.index(max(full)), " same", same.index(max(same)))
same: [6.7, 17.8, 27.8, 36.7, 27.8, 17.8, 6.7]
brightest pixel: row 3 full 5 same 3
The blurred line stays at pixel 3. The edge pixels came out at 6.7, not 10, because the pixels beyond the photo counted as 0: the edge effect of zero padding. An even kernel has no centre, so a same-length rule must say whether its output is half a sample early or late.
Common mistakes: stopping the full output at len(x) samples. Forgetting the condition on k - j: a negative index silently reads from the end. Writing h[j - k].
Practice: From Rainfall to River Flow, Smoothing a Trail Profile Without Moving the Summit, A Chapel's Echoes as an Impulse Response
FIR filters
A finite impulse response (FIR) filter makes each output a fixed weighted sum of the present and a few past inputs,
with m fixed weights b_i. Feed it the impulse and out come the weights, so h = b: an FIR filter is a convolution with its coefficients. Level 1's moving average was one, b = [1/w] * w. The difference filter b = [1, -1] gives x_k - x_{k-1}, zero where the signal is flat and large where it changes. A train's position, logged once a second, becomes its speed:
pos = [0, 12, 26, 41, 55, 66, 74, 79, 81] # metres past the signal, one reading per second
b = [1, -1]
speed = [b[0] * pos[k] + b[1] * pos[k - 1] for k in range(1, len(pos))]
print(speed, "m/s")
plot(range(1, len(pos)), speed, kind="stem", xlabel="second", ylabel="speed (m/s)")
[12, 14, 15, 14, 11, 8, 5, 2] m/s
The output starts at k = 1, the first moment both readings exist: a filter with m weights has a complete input only from sample m - 1. Differences amplify noise, since every jitter is a change, so a real speedometer smooths first, with a second FIR filter.
Practice: Boxes Passing the Light Gate
The step response is a running sum
The other standard test is the step: switch the input on and leave it on. A step is an impulse at every sample, so output k collects
the running sum of the impulse response. One hour of a drip raises a patient's concentration by h; a drip left running adds a copy every hour:
from itertools import accumulate
h = [0.5, 1.2, 0.9, 0.5, 0.2] # mg/L, hour by hour, from ONE hour of drip
step = [sum(h[k - j] for j in range(k + 1) if k - j < len(h)) for k in range(8)]
print([round(s, 2) for s in step])
print([round(s, 2) for s in accumulate(h)], "and sum(h) =", round(sum(h), 2))
plot(h, kind="stem", label="one hour of drip")
plot(step, kind="step", label="drip left on", xlabel="hour", ylabel="mg/L")
[0.5, 1.7, 2.6, 3.1, 3.3, 3.3, 3.3, 3.3]
[0.5, 1.7, 2.6, 3.1, 3.3] and sum(h) = 3.3
The convolution with a step and the running sum agree, and once h has run out the response stays at sum(h), the steady gain (or DC gain): how much the system scales a constant input. The difference filter turns a step response back into the impulse response.
Common mistakes: expecting the step response to settle at the peak of h rather than its sum.
Practice: Switching the Hob On and Leaving It
Causality and memory
A filter running live, in a hearing aid or a guitar pedal, must produce output k when input k arrives: it must be causal, using the present and the past but not the future. Write a filter as taps (d, c), each adding c * x[k - d]: positive d looks back, d = 0 is the present, negative d looks ahead. A weather station's live average {0: 0.5, 1: 0.5} needs samples 9 and 10 to report sample 10: it must remember one past input. A centred average {-1: 1/3, 0: 1/3, 1: 1/3} also needs sample 11, so it is not causal; run live, it must answer one sample late, a latency of one sample. A calibration {0: 2} uses only the present and is memoryless. A hearing aid's filters are short because much more than 10 ms of latency makes the wearer's own voice sound wrong.
Practice: Can the Effects Pedal Run It Live?
BIBO stability
A system is BIBO stable (bounded input, bounded output) if every input within a limit M gives an output within some limit. An FIR filter always is: no output can exceed M\sum_i |b_i| (M * sum(abs(v) for v in b)), reached when the input's signs match the weights'.
Feedback changes that. Adding back a share a of the last output, y_k = a\,y_{k-1} + x_k, gives the impulse response 1, a, a^2, \dots, which never ends: an infinite impulse response (IIR). A savings account with interest has a slightly above 1. Pay in 1 every month:
for a in (0.6, 0.95, 1.01):
y, ys = 0.0, []
for month in range(120):
y = a * y + 1
ys.append(y)
note = f"limit 1 / (1 - a) = {1 / (1 - a):.2f}" if a < 1 else "no limit"
print(f"a = {a}: after 10 years {y:.2f} ({note})")
plot(ys, label=f"a = {a}", xlabel="month")
a = 0.6: after 10 years 2.50 (limit 1 / (1 - a) = 2.50)
a = 0.95: after 10 years 19.96 (limit 1 / (1 - a) = 20.00)
a = 1.01: after 10 years 230.04 (no limit)
When |a| < 1 the echoes shrink and add up to
so no input bounded by M can push the output past M / (1 - |a|). When |a| >= 1 the sum grows for ever. Press Run: a = 1.01 has no ceiling, good news for a saver and, on a stage, the howl of a microphone hearing its own speaker. For a <= -1 the input 1, -1, 1, ... runs away.
Common mistakes: judging stability from one test input. Thinking a = 1 is safe: it adds up for ever.
Practice: Will the Stage Monitor Howl?
Cascades
A sensor, a cable and a display in a row form a cascade. The impulse leaves the first stage as h1, which the second convolves with h2, so the chain's impulse response is h1 * h2. Convolution ignores order, so the chain does too, and the steady gains multiply:
conv = lambda x, h: [sum(x[j] * h[k - j] for j in range(len(x)) if 0 <= k - j < len(h))
for k in range(len(x) + len(h) - 1)]
sensor = [0.8, 0.4, 0.2] # a seismometer's pickup, volts per millimetre
cable = [0, 0, 1] # two samples of delay
display = [0.5, 0.5] # the display averages the last two values
print([round(v, 3) for v in conv(conv(sensor, cable), display)])
print([round(v, 3) for v in conv(conv(display, sensor), cable)])
print("steady gain:", round(sum(sensor) * sum(cable) * sum(display), 3))
[0.0, 0.0, 0.4, 0.6, 0.3, 0.1]
[0.0, 0.0, 0.4, 0.6, 0.3, 0.1]
steady gain: 1.4
A feedback stage with |a| < 1 contributes its gain 1 / (1 - a), and one unstable stage makes the chain unstable. Real amplifiers clip, so in practice a big gain placed before a filter may saturate where the same gain after it would not.
Practice: The Microphone Chain's Fingerprint
Difference equations
A feedback system's impulse response never ends, but a difference equation describes it in a few numbers:
With no a terms it is FIR; with them, IIR. The outputs before sample 0, the initial conditions, must be given: a display already showing 12 m/s starts differently from one showing 0. A wind gauge's display keeps 80 % of what it showed and moves 20 % of the way to each new reading, y_k = 0.8\,y_{k-1} + 0.2\,x_k. In code the recurrence is a loop that updates one variable, y_prev, from its old value. One gust:
x = [0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0] # m/s, one gust at sample 2
y_prev = 0.0 # y[-1]: the display started at 0
y = []
for xk in x:
y_prev = 0.8 * y_prev + 0.2 * xk
y.append(y_prev)
print([round(v, 3) for v in y])
plot(x, kind="stem", label="gauge")
plot(y, kind="stem", label="display", xlabel="sample", ylabel="m/s")
[0.0, 0.0, 2.0, 1.6, 1.28, 1.024, 0.819, 0.655, 0.524, 0.419, 0.336, 0.268]
The gust shows as 2 m/s and fades by 0.8 each sample, never quite to zero: two coefficients replace an endless list. This exponential moving average stores one number instead of a window, and it is the tea's Euler step rearranged: T = (1 - dt / tau) * T + (dt / tau) * T_room. A second-order equation, with y[k-2], can swing.
Common mistakes: putting the initial conditions y[-2] and y[-1] in the wrong order.
Practice: The Ride-Height Filter, Sample by Sample
First-order step response and the time constant
Hold the input constant and a first-order equation becomes y_k = a\,y_{k-1} + b: keep a share a (between 0 and 1), gain b per sample. It settles where y = a y + b, so the final value is y_\infty = b / (1 - a). The gap to it shrinks by a each sample, so from 0,
A camera flash charging its capacitor, one sample per millisecond:
import math
a, b, dt = 0.9, 30, 0.001 # share kept per sample, volts gained per sample, seconds
final = b / (1 - a)
print("final:", round(final, 1), "V tau:", round(-dt / math.log(a) * 1000, 2), "ms")
y, ys = 0.0, [0.0]
for k in range(50):
y = a * y + b
ys.append(y)
for k in (10, 30, 50):
print(f"after {k} ms: {ys[k]:5.1f} V = {100 * ys[k] / final:4.1f} % of the way")
plot([k * dt * 1000 for k in range(len(ys))], ys, xlabel="ms", ylabel="volts")
final: 300.0 V tau: 9.49 ms
after 10 ms: 195.4 V = 65.1 % of the way
after 30 ms: 287.3 V = 95.8 % of the way
after 50 ms: 298.5 V = 99.5 % of the way
In Level 1 the gap shrank as exp(-t / tau); here, after t = k * dt, it is a ** k = exp(k * ln(a)). They agree when a^k = e^{-k\Delta t/\tau}, so the time constant is \tau = -\Delta t / \ln a (-dt / math.log(a)), 9.49 ms for the flash. One tau covers about 63.2 % of the way (1 - e ** -1), three about 95 %, five over 99 %, and the ready light comes on. A share a close to 1 means a long tau.
Common mistakes: math.log10 instead of math.log. Giving tau in samples when seconds were asked for.
Practice: How Many Samples to 63 %?
Identifying a model from data
System identification finds a model's numbers from a recorded test. If y[k] = a * y[k-1] + c fits, every pair of consecutive samples (y[k-1], y[k]) lies on the line q = a p + c, with p = y_{k-1} and q = y_k, and two pairs fix a line. A fan spinning up:
rpm = [0, 600, 1080, 1464, 1771.2] # one reading per second at full voltage
a = (rpm[2] - rpm[1]) / (rpm[1] - rpm[0])
c = rpm[1] - a * rpm[0]
print("a =", round(a, 4), " c =", round(c, 4), " final speed:", round(c / (1 - a), 1), "rpm")
print("predicted next reading:", round(a * rpm[3] + c, 1), " logged:", rpm[4])
a = 0.8 c = 600.0 final speed: 3000.0 rpm
predicted next reading: 1771.2 logged: 1771.2
The fan keeps 80 % of its speed each second (a time constant of -1 / ln(0.8), about 4.5 s) and will settle at 3000 rpm; the last line tests the model on a sample it was not built from. Real readings are noisy, so fit the least-squares line through all the pairs. If they lie near no line, the system is not first-order.
Practice: Learning the Print Bed From One Warm-Up
Rise time
A third number joins Level 1's overshoot and settling time: the rise time, from 10 % to 90 % of the way from the start to the final value, leaving out a sticky start and a slow final creep. A drone climbing to hover at 20 m, logged every half second:
sample: 0 1 2 3 4 5 6 7 8 9 10
height (m): 0 0.4 2.1 5.0 8.6 12.0 14.9 17.0 18.4 19.2 19.6
The levels are 2 m and 18 m, first reached at samples 2 and 8, so the rise time is (8 - 2) × 0.5 = 3 s, only as fine as the sample spacing. A first-order system reaches 10 % at tau * ln(10 / 9) and 90 % at tau * ln(10), so its rise time is \tau\ln 9, about 2.2 time constants, whatever the size of the step.
Practice: The E-Bike's Rise Time
The mass-spring-damper
A first-order system only creeps towards its target; a mass on a spring can swing past it. Hang a 20 kg suitcase on a spring luggage scale and let go. With x the stretch (m) and v its speed (m/s), three forces act: the weight F = m * g pulling down, the spring -k * x pulling back harder the further it stretches (k in N/m), and the damping -c * v, friction in proportion to the speed (c in N·s/m). The acceleration is the total force over the mass. There are now two states, and the order of the update matters. Number the steps n (the letter k is taken by the spring). With the acceleration \alpha_n = (F - c v_n - k x_n) / m, acc in the code, semi-implicit Euler updates the velocity first and moves with the new one,
while plain Euler moves with the old one, x_{n+1} = x_n + \Delta t\,v_n, and adds energy every step:
def hang(c, semi, m=20, k=4000, F=196.2, dt=0.005, steps=600):
x, v, xs = 0.0, 0.0, [0.0]
for _ in range(steps):
acc = (F - c * v - k * x) / m
if semi:
v = v + dt * acc
x = x + dt * v # moves with the new velocity
else:
x, v = x + dt * v, v + dt * acc # plain Euler: both from the old values
xs.append(x)
return xs
for semi in (True, False):
xs = hang(0, semi)
name = "semi-implicit" if semi else "plain Euler"
print(f"{name:13} largest stretch {max(xs[:100]):.4f} m at first, {max(xs[-100:]):.4f} m after 3 s")
plot([j * 0.005 for j in range(len(xs))], xs, label=name, xlabel="seconds", ylabel="stretch (m)")
semi-implicit largest stretch 0.0981 m at first, 0.0981 m after 3 s
plain Euler largest stretch 0.1039 m at first, 0.2568 m after 3 s
With no damping (c = 0) the true motion swings for ever between 0 and 9.8 cm, twice F / k. Press Run: semi-implicit Euler keeps that swing, plain Euler lets it grow, energy from nowhere. Try hang(40, True): the suitcase bounces and settles where the spring holds the whole weight, x = F / k, which is how a spring scale weighs.
Common mistakes: a wrong sign on the spring or damper force, which makes the simulation explode. Dividing by the weight in newtons instead of the mass in kilograms.
Practice: A Passenger Gets In: the Suspension's Step Response
Three kinds of damping
Two numbers summarise a mass-spring-damper. The natural frequency \omega_n = \sqrt{k / m} (wn), in radians per second, is how fast it would swing undamped: a stiffer spring or lighter mass swings faster. The damping ratio
(zeta) compares the damping with the amount that just prevents a swing:
zeta < 1, underdamped: it overshoots and oscillates while the swings die away;zeta = 1, critically damped: the fastest return without overshoot;zeta > 1, overdamped: no overshoot, but a slow crawl.
An analogue meter's needle is a small mass on a hair spring, with damping the designer chooses. Apply a full-scale signal:
import math
m, k, dt = 1, 100, 0.001
for c in (4, 20, 60):
x, v, xs = 0.0, 0.0, [0.0]
for _ in range(1500):
v = v + dt * (k * 1.0 - c * v - k * x) / m # pushed towards full scale, x = 1
x = x + dt * v
xs.append(x)
zeta = c / (2 * math.sqrt(k * m))
print(f"c = {c:2}: zeta = {zeta:.1f}, highest {max(xs):.3f}, after 1.5 s {xs[-1]:.3f}")
plot([j * dt for j in range(len(xs))], xs, label=f"zeta = {zeta:.1f}", xlabel="seconds")
c = 4: zeta = 0.2, highest 1.526, after 1.5 s 1.019
c = 20: zeta = 1.0, highest 1.000, after 1.5 s 1.000
c = 60: zeta = 3.0, highest 0.921, after 1.5 s 0.921
Press Run. At zeta = 0.2 the needle shoots past 1.5 and wobbles; at 3 it has not arrived after 1.5 s; critical damping arrives cleanly. Meter and suspension designers often choose about 0.7: a few per cent of overshoot buys a faster response.
Practice: Tuning a Door Closer
Damping ratio from overshoot, and the natural frequency
The overshoot of an underdamped mass-spring-damper depends only on zeta. With it as a fraction and L its natural logarithm, the exact solution gives
The time from one peak to the next is the damped period T_d, with angular frequency \omega_d = 2\pi / T_d; damping slows the swing a little, so the natural frequency is \omega_n = \omega_d / \sqrt{1 - \zeta^2}. A wind starts suddenly against a tall tower; its top sways out to 19 cm around a final lean of 10 cm, with peaks 6.0 s apart:
import math
start, peak, final, Td = 0, 19, 10, 6.0 # cm, cm, cm, seconds
os = (peak - final) / (final - start)
L = math.log(os)
zeta = -L / math.sqrt(math.pi ** 2 + L ** 2)
wd = 2 * math.pi / Td
wn = wd / math.sqrt(1 - zeta ** 2)
print("zeta =", round(zeta, 4), " wd =", round(wd, 4), "rad/s wn =", round(wn, 4), "rad/s")
zeta = 0.0335 wd = 1.0472 rad/s wn = 1.0478 rad/s
A 90 % overshoot means a damping ratio of about 0.03, typical of a steel tower and the reason tall buildings add tuned mass dampers, heavy pendulums near the top; with so little damping wd and wn almost agree. Since \omega_n^2 = k / m, a known mass gives k = m\,\omega_n^2 and c = 2\zeta m\,\omega_n: one bounce identifies the model. No overshoot means zeta >= 1, by an unknown amount.
Common mistakes: measuring the overshoot against the setpoint instead of the final value. Taking Td from a peak to the next trough, half a period.
Practice: Is the Shock Absorber Worn? Damping From Overshoot, Spring and Damper From One Bounce
The final value
A final value needs no long simulation. Once settled, every y[k], y[k-1], ... equals one number y and every input the constant x; substitute and solve. The wind gauge's y = 0.8 * y + 0.2 * x gives y = x, a steady gain of 1, as a display should have. For the mass-spring-damper, settled means v = 0 and acc = 0, leaving F = k * x.
The substitution assumes the system settles. With too much feedback, y[k] = 1.25 * y[k-1] + 0.2 * x[k], a steady 7 m/s makes the formula claim 0.2 * 7 / (1 - 1.25) = -5.6, while the output passes 4500 within 30 samples. Check stability first (for first order, |a| < 1).
Practice: Where Does the Scale's Reading Settle?, The Suspension Simulation Is a Difference Equation
Practice
These problems are required on this path at this level; the path adds one problem from each of the level's topics, picked for you and kept.
- From Rainfall to River Flow
- Switching the Hob On and Leaving It
- Will the Stage Monitor Howl?
- The Ride-Height Filter, Sample by Sample
- How Many Samples to 63 %?
- Tuning a Door Closer
Checklist
You can now:
- Test a system for linearity and time invariance, and predict its output from its impulse response.
- Compute a full and a same-length convolution, and write an FIR filter.
- Build a step response as a running sum and read its steady gain.
- Decide whether a filter is causal or memoryless, and what it must remember or wait for.
- Decide BIBO stability of FIR filters and first-order feedback, and find a cascade's impulse response and gain.
- Run a difference equation from its initial conditions.
- Find the final value, time constant and rise time of a first-order system, and identify it from a recorded step.
- Simulate a mass-spring-damper with semi-implicit Euler and classify its damping.
- Find the damping ratio from an overshoot and the natural frequency from a period.