Building models for JuliaSim

The JuliaSim software is available for preview only. Please contact us for access, by emailing [email protected], if you are interested in evaluating JuliaSim.

In this tutorial, we demonstrate how the user can build models with ModelingToolkit.jl and the Functional Mock-up Interface.

Circuit simulation

ModelingToolkit.jl (MTK) is a modeling language for high-performance symbolic-numeric computation in scientific computing and scientific machine learning. It combines ideas from symbolic computational algebra systems with causal and acausal equation-based modeling frameworks to give an extendable and parallel modeling system. It allows for users to give a high-level description of a model for symbolic preprocessing to analyze and enhance the model. Automatic transformations, such as index reduction, can be applied to the model before solving in order to make it easily handle equations that could not be solved when modeled without symbolic intervention.

As with Modelica, MTK allows for building models hierarchically in a component-based fashion. For example, defining a component in MTK is to define a function which generates an ODESystem:

@parameters t

function Pin(;name)
    @variables v(t) i(t)
    ODESystem(Equation[], t, [v, i], [], name=name, defaults=[v=>1.0, i=>1.0])
end

function Capacitor(;name, C = 1.0)
    val = C
    @named p = Pin(); @named n = Pin()
    @variables v(t); @parameters C
    D = Differential(t)
    eqs = [v ~ p.v - n.v
           0 ~ p.i + n.i
           D(v) ~ p.i / C]
    ODESystem(eqs, t, [v], [C],
        systems=[p, n],
        defaults=Dict(C => val),
        name=name)
end

We can now build a subsystem from the above primitive - in this case, the classic RC circuit:

using JuliaSim

@named resistor = Resistor(R=1.0)
@named capacitor = Capacitor(C=1.0)
@named source = ConstantVoltage(V=1.0)
@named ground = Ground()
rc_eqs = [
          connect(source.p, resistor.p)
          connect(resistor.n, capacitor.p)
          connect(capacitor.n, source.n, ground.g)
         ]
@named rc_model = ODESystem(rc_eqs, systems=[resistor, capacitor, source, ground])
sys = structural_simplify(rc_model)
u0 = [
      capacitor.v => 0.0
      capacitor.p.i => 0.0
     ]
prob = ODAEProblem(sys, u0, (0, 10.0))
sol = solve(prob, Tsit5())

The solution can be plotted by appending the following command to the above code:

using Plots
plot(sol)

We then obtain the following plot:

MTKPlot

The composition of the ModelingToolkit.jl component with trained machine learning models (from the JuliaSim Standard Library) is performed by representing the trained model as a set of differential-algebraic equations. As already mentioned, JuliaSim uses the novel architecture, CTESNs, as a class of surrogate algorithms. A demonstration follows in the next tutorial.

JuliaSim and the Functional Mock-up Interface

In order to facilitate the exchange of simulation models, JuliaSim supports the composition of surrogatized CTESNs with models from external languages, such as the Functional Mock-up Interface, which represents a tool-independent standard. The exported model, Functional Mock-up Unit (FMU), which consists of sets of differential-algebraic equations, can then be used for coupled simulation. The coupled simulation is performed either via model exchange (with a centralized time-integration algorithm) or by means of co-simulation, that is, a method where FMUs export their own simulation routine.

To illustrate FMU simulation via JuliaSim, we construct the following example. Suppose we have an FMU which describes a particular heating system. The following JuliaSim code performs the necessary FMU simulation in a few lines:

using JuliaSim

fmu_filename = joinpath(@__DIR__, "my_fmus", "heating.fmu")
keynames = ["Td0", "Tu0"]
test = [293.15, 343.15]
param_space = [(290.16, 296.15), (338.15, 348.15)]
ts = 0:100.0:5000.0 # timestamps

setup = FMUs.FMUSimSetup(fmu_filename, keynames)
surralg = LPCTESN(1000)
truth = JuliaSim.simulate(setup, test; ts=ts)

The surrogatization of the above FMU is performed in the following steps:

surr = JuliaSim.surrogatize(
    setup,
    param_space,
    surralg,
    1000; # n_sample_pts
    component_name = :heating,
    hybrid_sim_kwargs = (;ts=ts),
    ensemble_kwargs = (;ts=ts),
    verbose=true
)
pred = surr(test, ts)
@show JuliaSim.relerror(truth, pred)

logpath = joinpath(@__DIR__, "logs")
mkpath(logpath) #local path
save_loc = joinpath(logpath, "Heating.pdf")
JuliaSim.weave_diagnostics(surr; header="Heating", save_location=save_loc)

The weave_diagnostics function is used for producing a detailed report. More information about visualization and generating summaries can be found in subsequent tutorials.