#
Making Component Libraries
#
Terminology
Julia organizes reusable code into "packages". But the term "package" is used generally for anything containing code. In this document, the term "library" will be used to refer to Julia packages that specifically contain MTK models.
#
Model Reuse
In the previous example we saw how to create a collection
of component models and then use these to build a system model composed of
components. But we don't want to repeat the definitions of all of our
component models each time we need to build a system model. Instead, we'd like
to create a reusable collection of component models that we can use across
projects. This is analogous to the package concept in Modelica.
Doing this in Julia is quite straightforward. Not only does the Julia language provide a way to create these reusable modules of Julia code, but it includes a complete package management solution, making it very easy to (re-)use model libraries. These libraries are versioned (using semantic versioning!) so each project can depend on specific versions of the model library.
In this section, we will walk through the creation of a reusable model library and then create a system level model that uses that model library.
#
Creating a Model Library
There is extensive documentation about the Julia Package Manager. This document is not a replacement for that documentation. In this case, we will create a package that is only being used locally. If you want to learn how to publish packages check out this section of the documentation. For a video that walks through the process of setting up a Julia package, you'll find one on this page.
But for our simple case of creating a local package, all we need to do is go to a directory where we want the package to be stored and do the following:
$ cd <dir-to-store-library>
$ julia
_ _ _(_)_ | Documentation: https://docs.julialang.org
(_) | (_) (_) |
_ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help.
| | | | | | |/ _` | |
| | |_| | | | (_| | | Version 1.9.2 (2023-07-05)
_/ |\__'_|_|_|\__'_| | Official https://julialang.org/ release
|__/ |
julia> using Pkg
julia> Pkg.generate("ExampleLibrary")
Generating project ExampleLibrary:
ExampleLibrary/Project.toml
ExampleLibrary/src/ExampleLibrary.jl
Dict{String, Base.UUID} with 1 entry:
"ExampleLibrary" => UUID("XXXXXXXX-XXXXX-XXXX-XXXXX-XXXXXXXXXXXX")
This will create a directory with the structure:
<dir-to-store-library>
/src
ExampleLibrary.jl
Project.toml
Of course, you can name the library anything you like. Furthermore, you can use
the package manager mode of the Julia REPL by pressing ] at the julia> prompt,
e.g.,
$ julia
julia> <press ]>
(@v1.9) pkg> generate ExampleLibrary
Generating project ExampleLibrary:
ExampleLibrary/Project.toml
ExampleLibrary/src/ExampleLibrary.jl
The "entry point" for this library is the file src/ExampleLibrary.jl.
Initially, it will contain:
module ExampleLibrary
greet() = print("Hello World!")
end # module ExampleLibrary
#
Populating Our Library
At this point, we can simply copy remove the initial content from
src/ExampleLibrary.jl and then copy all the definitions we've created into
src/ExampleLibrary.jl. However, we need to be sure to also export the
contents of the library. Without the export, anything created in this file is
assumed to be internal to the library and not available outside.
So for our basic electrical components, our ExampleLibrary.jl would look like:
module ExampleLibrary
using ModelingToolkit
using Unitful
@variables t [unit = u"s"]
D = Differential(t)
@connector Pin begin
...
end
@mtkmodel Ground begin
...
end
@mtkmodel TwoPin begin
...
end
@mtkmodel Resistor begin
...
end
@mtkmodel Capacitor begin
...
end
@mtkmodel Inductor begin
...
end
@mtkmodel ConstantVoltage begin
...
end
export t, D, Pin, Ground, TwoPin, Resistor, Capacitor, Inductor, ConstantVoltage
end
It is very important not to forget the export command.
#
Adding Dependencies
When a Julia package is created, it starts with no external dependencies. This
means that it is assumed that the code in the library doesn't leverage any other
Julia packages. If you look at the Project.toml file associated with
ExampleLibrary, it should look something like this:
name = "ExampleLibrary"
uuid = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX"
authors = ["Michael M. Tiller <michael.tiller@juliahub.com>"]
version = "0.1.0"
But based on the using statements at the top of our ExampleLibrary.jl file,
our library requires the ModelingToolkit and Unitful package to provide us
with a number of macros we need to describe our component models and unit
information. In order for those using clauses to succeed, we must declare
dependencies on these other packages.
One way declare that ExampleLibrary depends on ModelingToolkit and Unitful
is to go to the directory where ExampleLibrary is stored (i.e., the
directory where Project.toml exists). Using the built-in package manager, we
can do something like this:
$ julia
julia> # press ]
(@v1.9) pkg> activate .
(@v1.9) pkg> add ModelingToolkit
(@v1.9) pkg> add Unitful
The same thing can also be done programmatically in Julia with:
julia> using Pkg
julia> Pkg.activate(".")
julia> Pkg.add("ModelingToolkit")
julia> Pkg.add("Unitful")
Either way, the resulting Project.toml file should look something like this:
name = "ExampleLibrary"
uuid = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX"
authors = ["Michael M. Tiller <michael.tiller@juliahub.com>"]
version = "0.1.0"
[deps]
ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78"
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
The activate step is used to indicate which package we want to add the
dependency to. In this case, the "." simply indicates the package we are
operating on is the one defined in the current directory.
#
Project.toml
Let's go through and explain what the contents of the Project.toml file above
mean. First, we have the name of the Julia package. This is the name that
others will use when referring to the package. Next we have the uuid. This
is unique identifier for this package. It will help to avoid any confusion just
in case there are other packages that share the same name.
Then we have the authors which is a list of authors. Finally, we have the
version number. As mentioned previously, the Julia package manager uses
semantic versioning.
The next section in Project.toml is the [deps] section. This is used for
declaring Julia package that this package depends on. Following [deps]
section.
#
using ExampleLibrary
To make use of the code in our ExampleLibrary package, we just need to add a
using ExampleLibrary to the top of any Julia file we are editing or typing
this in the Julia REPL. But, as discussed previously, for this to work we must
add ExampleLibrary as a dependency.
In our case, ExampleLibrary just exists locally on our own machine. So one
easy way to make it available is to add it as a dependency just as we previously
added ModelingToolkit as a dependency. However, there is one problem.
Specifically, ModelingToolkit is published to the global package registry, but
our ExampleLibrary isn't. Fortunately, the Julia package manager supports
exactly this use case of developing a package locally via the dev command
(also available via Pkg.develop("..."))), e.g.
(@v1.9) pkg> dev /path/to/ExampleLibrary
or
julia> using Pkg
julia> Pkg.develop("/path/to/ExampleLibrary")
In this case, we should be able to do the following in the Julia REPL:
julia> using ExampleLibrary
[ Info: Precompiling ExampleLibrary [9675ecc2-7a0c-4983-b502-498a345db439]
julia> D
(::Symbolics.Differential) (generic function with 3 methods)
The fact that D is defined after we run using ExampleLibrary is an
indication that we have successfully wrapped all of that code inside a
reusable Julia package.
#
Organizing Libraries
Recall that so far our entire library is contained in ExampleLibrary.jl. But
such a file can get very large. For this reason, it is a good idea to split the
code up by function. For example, we could put the Resistor model into a file
called resistor.jl, or Capacitor model into a file called capacitor.jl and
so on. In this case, our resistor.jl file might look like this:
@mtkmodel Resistor begin
@extend v, i = twopin = TwoPin()
@parameters begin
R, [unit = u"Ω"]
end
@equations begin
v ~ i * R
end
end
Each of the other files would have similar contents. The overall directory structure might then be:
<dir-to-store-library>
/src
ExampleLibrary.jl
capacitor.jl
resitor.jl
ground.jl
inductor.jl
pin.jl
resistor.jl
twopin.jl
Project.toml
If our models are organized in this way, our ExampleLibrary.jl file would
simply be:
module ExampleLibrary
using ModelingToolkit
using Unitful
@variables t [unit = u"s"]
D = Differential(t)
include("./capacitor.jl")
include("./constant_voltage.jl")
include("./ground.jl")
include("./inductor.jl")
include("./pin.jl")
include("./resistor.jl")
include("./twopin.jl")
export t, D, Pin, Ground, TwoPin, Resistor, Capacitor, Inductor, ConstantVoltage
end # module ExampleLibrary
Looking at resistor.jl, one might wonder why we don't require a using ModelingToolkit statement at the top. This is because we don't load
resistor.jl by itself. Instead, it is only read when ExampleLibrary.jl is
read. So at the point where include("./resistor.jl") is evaluated, the using ModelingToolkit statement has already been processed.
#
Building the System Model
With ExampleLibrary available to us, we can now write a much shorter Julia
program to build and run our system:
using ModelingToolkit
using ExampleLibrary
using DifferentialEquations
using Plots
@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]))