Step Response of a Second Order System (Control Theory)ΒΆ

../_images/light-impulse.png ../_images/dark-impulse.png
  1. Prepare data and second_order function

from collections import namedtuple
from math import exp, sin, sqrt
import detroit as d3


Margin = namedtuple("Margin", ("top", "right", "bottom", "left"))

# Arbitrary parameters for second order system
K = 1
omega_0 = 2
e0 = 1


def second_order(t, zeta):
    if t <= 0:
        return 0
    if zeta > 1:
        p1 = -zeta * omega_0 + omega_0 * sqrt(zeta * zeta - 1)
        p2 = -zeta * omega_0 - omega_0 * sqrt(zeta * zeta - 1)
        return K * omega_0 / (2 * sqrt(zeta * zeta - 1)) * (exp(p1 * t) - exp(p2 * t))
    else:
        return (
            K
            * omega_0
            / sqrt(1 - zeta * zeta)
            * exp(-zeta * omega_0 * t)
            * sin(omega_0 * sqrt(1 - zeta * zeta) * t)
        )


xmax = 10
steps = 1000

xvalues = [(xmax * i / steps) for i in range(steps)]
allzeta = [2, 1.7, 1.5, 1.3, 1.1, 0.9, 0.7, 0.6, 0.4, 0.2]
data = {
    "allzeta": allzeta,
    "allvalues": [
        {"key": zeta, "values": [{"x": t, "y": second_order(t, zeta)} for t in xvalues]}
        for zeta in allzeta
    ],
}
  1. Make the impulse chart

# Specify the chart's dimensions
margin = Margin(10, 30, 30, 60)
width = 960 - margin.left - margin.right
height = 800 - margin.top - margin.bottom

# Create the SVG container.
svg = (
    d3.create("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
    .append("g")
    .attr("transform", f"translate({margin.left}, {margin.top})")
)

# Create the horizontal x scale.
x = (
    d3.scale_linear()
    .set_domain(d3.extent(data["allvalues"][0]["values"], lambda d: d["x"]))
    .set_range([0, width])
)


svg.append("g").attr("transform", f"translate(0, {height})").call(
    d3.axis_bottom(x).set_tick_format(d3.format(".0f"))
)

# Create the vertical y scale.
ymax = max(data["allvalues"][-1]["values"], key=lambda d: d["y"])["y"]

y = d3.scale_linear().set_domain([-ymax, ymax]).set_range([height, -1]).nice()

svg.append("g").call(d3.axis_left(y).set_tick_format(d3.format(".1f")))

# Create the color scale given zeta values.
color = (
    d3.scale_linear()
    .set_domain([max(data["allzeta"]), min(data["allzeta"])])
    .set_range(["gold", "deepskyblue"])
)

# Append the pathes for the lines.
line = d3.line().x(lambda d: x(d["x"])).y(lambda d: y(d["y"]))
(
    svg.select_all(".line")
    .data(data["allvalues"])
    .enter()
    .append("path")
    .attr("fill", "none")
    .attr("stroke", lambda d: color(d["key"]))
    .attr("d", lambda d: line(d["values"]))
)
  1. Save your chart

with open("impulse.svg", "w") as file:
    file.write(str(svg))