# 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 the steps for adding dependencies is what populates the [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]))