# RLC Circuit Using Components

So far, we've created models strictly from equations. Now we are going to change gears a bit and create the same RLC circuit but this time using acausal component models.

# "Standard Stuff"

As with the other examples, there a few bits we always need to do, i.e.,

using ModelingToolkit
using DifferentialEquations
using Plots

In addition, we need to add one more package called Unitful (which you'll need to add as a dependency) for assigning units to variables and values...

using Unitful

And, of course, we define the time, t, and the differentiation operator, D.

@variables t [unit = u"s"]
D = Differential(t)

Note that these will eventually be moving into a package so you won't need to define them for every model. But, for now, we still need to define them.

# Connectors

As in Modelica, ModelingToolkit uses connectors to define the interactions between components. These need to be defined once for each domain and then can be reused. Connectors are the foundation for modeling a given domain. For an electrical system, the connectors are defined as follows:

@connector Pin begin
    v(t), [unit = u"V"]
    i(t), [connect = Flow, unit = u"A"]
end

The @connector macro is used to define a connector (in this case named Pin). Within the connector we have two variables v(t) and i(t). Both of these have units associated with them and the current, i(t), is also marked with connect = Flow. This is analogous to the flow keyword in Modelica.

With the connector definition completed, we've laid the foundation for creating electrical components.

# Components

With the connectors defined, we can go through and implement all the components we require.

# Ground

Our Ground model will be the simplest model we create because it requires only a single equation:

@mtkmodel Ground begin
    @components begin
        g = Pin()
    end
    @equations begin
        g.v ~ 0
    end
end

Because this is the definition of a component model, it uses the @mtkmodel macros (unlike the connectors which start with @connector). The @components section lists all the subcomponents of this model. In this case, there is only a single Pin instance named g. In the equation section, we have a single equation which simply pins the voltage to zero.

# Resistor

Our Resistor model is slightly more sophisticated than our Ground model and it adds a few more complications:

@mtkmodel Resistor begin
    @components begin
        p = Pin()
        n = Pin()
    end
    @variables begin
        v(t), [unit = u"V"]
        i(t), [unit = u"A"]
    end
    @parameters begin
        R, [unit = u"Ω"]
    end
    @equations begin
        v ~ p.v - n.v
        0 ~ p.i + n.i
        i ~ p.i
        v ~ i * R
    end
end

One complication is that it has two subcomponents, the two pins p and n. In addition, it has local variables v(t) and i(t). Finally, in addition to having multiple equations, this component also has parameters defined in the @parameters section.

# Capacitor

Our Capacitor model is quite similar to our Resistor model. This will become important in the next section. But for now, just notice that the Capacitor model is pretty much as you would expect given the way our Resistor is defined:

@mtkmodel Capacitor begin
    @components begin
        p = Pin()
        n = Pin()
    end
    @variables begin
        v(t), [unit = u"V"]
        i(t), [unit = u"A"]
    end
    @parameters begin
        C, [unit = u"F"]
    end
    @equations begin
        v ~ p.v - n.v
        0 ~ p.i + n.i
        i ~ p.i
        C * D(v) ~ i
    end
end

# TwoPin

Now from a software engineering perspective, there is something very bad about our previous Resistor and Capacitor models. Specifically, there is way too much duplicate code. One of the key principles in software development is the DRY principle which stands for "Don't repeat yourself".

Fortunately, MTK has a way for us to avoid repeating ourselves. We do this by defining a special model which we will call TwoPin that represents all the common bits between Resistor and Capacitor (and, as we will see shortly, Inductor).

@mtkmodel TwoPin begin
    @components begin
        p = Pin()
        n = Pin()
    end
    @variables begin
        v(t), [unit = u"V"]
        i(t), [unit = u"A"]
    end
    @equations begin
        v ~ p.v - n.v
        0 ~ p.i + n.i
        i ~ p.i
    end
end

Note how the @components, @variables and @equations sections here contain the common bits from Resistor and Capacitor. In subsequent sections we will see how this makes the definition of our component models much simpler.

# Resistor Redux

Now that we've defined TwoPin, lets make another pass at our Resistor model:

@mtkmodel Resistor begin
    @extend v, i = twopin = TwoPin()
    @parameters begin
        R, [unit = u"Ω"]
    end
    @equations begin
        v ~ i * R
    end
end

Note how much simpler the model is in this case. We use the @extend macro to extract the variables v and i from the TwoPin instance so we can reference them locally in our equations. All the other sections of TwoPin get merged into the sections in Resistor so that or Resistor also has the connectors and equations of TwoPin.

# Capacitor Redux

We can give our Capacitor model the same treatment:

@mtkmodel Capacitor begin
    @extend v, i = twopin = TwoPin()
    @parameters begin
        C, [unit = u"F"]
    end
    @equations begin
        C * D(v) ~ i
    end
end

From a software engineering point, the essential point here is that this model is primarily composed of things that are unique to the capacitor model and avoids repeating anything that it shares with other models.

# Inductor

By leveraging the TwoPin model, we are able to make an Inductor model just as succinctly as we did our latest Resistor and Capacitor models:

@mtkmodel Inductor begin
    @extend v, i = twopin = TwoPin()
    @parameters begin
        L, [unit = u"H"]
    end
    @equations begin
        L * D(i) ~ v
    end
end

# VoltageSource

In order to complete a component oriented version of the model we created from equations, we need a VoltageSource:

@mtkmodel ConstantVoltage begin
    @extend (v,) = twopin = TwoPin()
    @parameters begin
        V, [unit = u"V"]
    end
    @equations begin
        V ~ v
    end
end

Just as with our previous models, we leverage the TwoPin model. But, in this case, we don't actually need the current variable from TwoPin because the behavior of the current source doesn't depend on current (unlike all the other components previous derived from TwoPin)

# System Model

With our component models all defined, we can finally assemble our system level model. It uses the same set of macros (e.g., @mtkmodel, @equations, @components) as our other models:

@mtkmodel RLCModel begin
    @components begin
        resistor = Resistor(R = 100.0)
        capacitor = Capacitor(C = 1.0e-3)
        inductor = Inductor(L = 1.0)
        source = ConstantVoltage(V = 24.0)
        ground = Ground()
    end
    @equations begin
        connect(source.p, inductor.p)
        connect(inductor.n, resistor.p)
        connect(inductor.n, capacitor.p)
        connect(resistor.n, ground.g)
        connect(capacitor.n, ground.g)
        connect(source.n, ground.g)
    end
end

# Results

With our system model defined, a few more lines are all that is required to simulate the system:

@mtkbuild rlc_model = RLCModel()
u0 = [
    rlc_model.capacitor.v => 0.0,
    rlc_model.inductor.i => 0.0
]
prob = ODEProblem(rlc_model, u0, (0, 1.0))
sol = solve(prob)
display(plot(sol, idxs=[rlc_model.capacitor.v, rlc_model.inductor.i]))

The results from our component oriented model:

Component Oriented Model Results
Component Oriented Model Results

...agree exactly with our equation oriented model:

Equation Oriented Model Results
Equation Oriented Model Results

# Benefits

Granted, we had to write a lot more code for the component oriented models. But the difference here is that we didn't have to apply all the conservation laws ourselves and then formulate the resulting ordinary differential equations. All that work was taken care of here. This means that we can combine the models above and any others that we chose to create and not have to worry about errors resulting from improperly formulating the conservation equations. These conservation equations can be derived automatically from the connect equations in our model.

What this means in practice is that our system results in the following equations:

\begin{align}
capacitor_{+}p_{+}v\left( t \right) =& capacitor_{+}v\left( t \right) \\
source_{+}v\left( t \right) =& source_{+}V \\
inductor_{+}p_{+}i\left( t \right) =& inductor_{+}i\left( t \right) \\
ground_{+}g_{+}v\left( t \right) =& 0 \\
capacitor_{+}n_{+}v\left( t \right) =& 0 \\
source_{+}n_{+}v\left( t \right) =& 0 \\
resistor_{+}n_{+}v\left( t \right) =& 0 \\
resistor_{+}v\left( t \right) =& capacitor_{+}p_{+}v\left( t \right) \\
inductor_{+}v\left( t \right) =&  - capacitor_{+}v\left( t \right) + source_{+}v\left( t \right) \\
source_{+}p_{+}v\left( t \right) =& source_{+}v\left( t \right) \\
inductor_{+}p_{+}v\left( t \right) =& source_{+}v\left( t \right) \\
inductor_{+}n_{+}i\left( t \right) =&  - inductor_{+}p_{+}i\left( t \right) \\
source_{+}i\left( t \right) =&  - inductor_{+}p_{+}i\left( t \right) \\
resistor_{+}p_{+}v\left( t \right) =& resistor_{+}v\left( t \right) \\
resistor_{+}i\left( t \right) =& \frac{resistor_{+}v\left( t \right)}{resistor_{+}R} \\
inductor_{+}n_{+}v\left( t \right) =& resistor_{+}v\left( t \right) \\
source_{+}n_{+}i\left( t \right) =&  - source_{+}i\left( t \right) \\
resistor_{+}n_{+}i\left( t \right) =&  - resistor_{+}i\left( t \right) \\
capacitor_{+}i\left( t \right) =& inductor_{+}i\left( t \right) - resistor_{+}i\left( t \right) \\
source_{+}p_{+}i\left( t \right) =&  - source_{+}n_{+}i\left( t \right) \\
resistor_{+}p_{+}i\left( t \right) =&  - resistor_{+}n_{+}i\left( t \right) \\
capacitor_{+}n_{+}i\left( t \right) =&  - capacitor_{+}i\left( t \right) \\
ground_{+}g_{+}i\left( t \right) =&  - inductor_{+}p_{+}i\left( t \right) + resistor_{+}i\left( t \right) + capacitor_{+}i\left( t \right) \\
capacitor_{+}p_{+}i\left( t \right) =&  - capacitor_{+}n_{+}i\left( t \right)
\end{align}

...but MTK automatically reduces this down to:

\begin{align}
\frac{\mathrm{d} capacitor_{+}v\left( t \right)}{\mathrm{d}t} =& \frac{capacitor_{+}i\left( t \right)}{capacitor_{+}C} \\
\frac{\mathrm{d} inductor_{+}i\left( t \right)}{\mathrm{d}t} =& \frac{inductor_{+}v\left( t \right)}{inductor_{+}L}
\end{align}

So we get the best of both worlds. The process of building the models removes many of the tedious, time consuming and error prone aspects of model development thanks to our component oriented approach but MTK's symbolic processing means we get an optimized system of equations to solve.

Note, the complete system of equations for our system can be extracted by executing latexify(full_equations(rlc_model)) and the reduce system of equations can be extracted with latexify(rlc_model), in case you were curious.

# Standard Library

Note, much of what we have defined here is already defined in the MTK standard library. But the goal of this example was to demonstrate creation of a simple electrical component library from scratch to drive home the point that these component models are not somehow "built in" to MTK but that anybody can create such components for any engineering domain.

# Complete Example

using ModelingToolkit
using DifferentialEquations
using Plots
using Unitful

@variables t [unit = u"s"]
D = Differential(t)

@connector Pin begin
    v(t), [unit = u"V"]
    i(t), [connect = Flow, unit = u"A"]
end

@mtkmodel Ground begin
    @components begin
        g = Pin()
    end
    @equations begin
        g.v ~ 0
    end
end

@mtkmodel TwoPin begin
    @components begin
        p = Pin()
        n = Pin()
    end
    @variables begin
        v(t), [unit = u"V"]
        i(t), [unit = u"A"]
    end
    @equations begin
        v ~ p.v - n.v
        0 ~ p.i + n.i
        i ~ p.i
    end
end

@mtkmodel Resistor begin
    @extend v, i = twopin = TwoPin()
    @parameters begin
        R, [unit = u"Ω"]
    end
    @equations begin
        v ~ i * R
    end
end

@mtkmodel Capacitor begin
    @extend v, i = twopin = TwoPin()
    @parameters begin
        C, [unit = u"F"]
    end
    @equations begin
        C * D(v) ~ i
    end
end

@mtkmodel Inductor begin
    @extend v, i = twopin = TwoPin()
    @parameters begin
        L, [unit = u"H"]
    end
    @equations begin
        L * D(i) ~ v
    end
end

@mtkmodel ConstantVoltage begin
    @extend (v,) = twopin = TwoPin()
    @parameters begin
        V, [unit = u"V"]
    end
    @equations begin
        V ~ v
    end
end

@mtkmodel RLCModel begin
    @components begin
        resistor = Resistor(R = 100.0)
        capacitor = Capacitor(C = 1.0e-3)
        inductor = Inductor(L = 1.0)
        source = ConstantVoltage(V = 24.0)
        ground = Ground()
    end
    @equations begin
        connect(source.p, inductor.p)
        connect(inductor.n, resistor.p)
        connect(inductor.n, capacitor.p)
        connect(resistor.n, ground.g)
        connect(capacitor.n, ground.g)
        connect(source.n, ground.g)
    end
end

@mtkbuild rlc_model = RLCModel()
u0 = [
    rlc_model.capacitor.v => 0.0,
    rlc_model.inductor.i => 0.0
]
prob = ODEProblem(rlc_model, u0, (0, 1.0))
sol = solve(prob)
display(plot(sol, idxs=[rlc_model.capacitor.v, rlc_model.inductor.i]))