Diokle docs GitHub Open the workbench

Scientific machine learning

daedalus.sciml combines Modelica models with machine learning, in NumPy only:

  • Missing physics. Keep the physics you trust in Modelica, give the part you cannot explain a top-level input, and recover that input's values from measurements.
  • Sparse regression. Turn the recovered values into a short formula built from a few candidate terms, such as products of the variables (SINDy), and put the formula back into the model as an equation.
  • Neural surrogates. Run the model for many parameter values, then train a small neural network that predicts the quantities you care about from those parameters. Export the network as a Modelica block that simulates anywhere and compiles to embedded C.

In short: you keep the equations you trust, and let the data fill in the rest. It needs the Python API and NumPy, nothing else: no PyTorch or JAX, and every network is small enough to train in seconds on a laptop.

The worked example

Daedalus.Examples.SciML holds a DC motor whose friction is missing from the model. python/examples/sciml_motor.py runs the whole workflow:

data: 2501 samples of speed and current over 5 s, noise 0.01 rad/s
missing torque recovered in 0.28 s: RMS error 2.46e-04 N.m (range 1.79e-02)
sparse regression: tau_missing = 2.00314e-07*w*abs(w) + 0.00399621*tanh(w/2.0)   (R2 0.99813, 2 of 8 terms)
  as Modelica:  MotorKnown m(tau_missing=2e-07*m.w*abs(m.w) + 0.004*tanh(m.w/2.0));
  true law:     4e-3*tanh(w/2) + 2e-7*w*abs(w)
  speed error vs the rig: 0.0017 rad/s with the discovered law, 4.1 rad/s without

neural surrogate in 0.7 s
surrogate of 2 outputs over 2 parameters, 120 runs (24 held out)
  w_peak: R2 1.0000, max error 0.212 (range 31.56)
  i_rms: R2 0.9999, max error 0.01005 (range 1.307)

Timings depend on the machine. The speed check simulates the fitted coefficients at full precision. The Modelica line rounds them to three digits, and MotorDiscovered in the library uses that rounded form. With the fitted law the speed stays within 0.0017 rad/s of the rig over 5 s, against 4.1 rad/s without the friction term.

Left: the friction torque recovered from noisy speed data, the true law and the SINDy formula. Right: the speed error against the rig, without the friction and with the discovered law

Missing physics

The known model, with the unknown torque as an input:

model MotorKnown
  parameter Real R=1.2, L=5e-3, k=0.05, J=2e-4, b=1e-5;
  input Real tau_missing "Unknown torque, found from data";
  Real u, i(start=0, fixed=true), w(start=0, fixed=true);
equation
  u = 6*sin(2*pi*0.4*time) + 3*sin(2*pi*1.3*time);
  L*der(i) = u - R*i - k*w;
  J*der(w) = k*i - b*w - tau_missing;
end MotorKnown;
import daedalus
from daedalus import sciml

known = daedalus.Model("library/Daedalus", "Daedalus.Examples.SciML.MotorKnown")
tau = sciml.missing_term(known, t, {"w": w_measured, "i": i_measured}, "tau_missing")

missing_term estimates the time derivatives of the measured states (local polynomial fits, smooth_derivative), then at every sample finds the value of the input for which the model's derivatives match them best. The model's right-hand side comes from Model.derivatives, which evaluates der(x) = f(t, x, u) at any points, so any Modelica model works, with its algebraic loops, tables and events.

This answers the question a universal differential equation answers, by collocation instead of differentiating through the solver: it is fast (thousands of points in a fraction of a second) and needs every state measured. When some states are not measured, fit the parameters of a candidate law with daedalus calibrate instead.

Symbolic regression

theta, terms = sciml.candidate_library({"w": w_measured}, degree=2, tanh_scales=(0.5, 2.0, 8.0))
fit = sciml.sindy(theta, tau, terms, threshold=0.1)
print(fit.expression(prefix="m.", digits=3))
# 2e-07*m.w*abs(m.w) + 0.004*tanh(m.w/2.0)

candidate_library builds monomials up to a degree and, for friction-like terms, sign(x), x*abs(x) and tanh(x/s); add your own columns as needed. sindy keeps the fewest terms that explain the data (sequentially thresholded least squares on normalised columns, so threshold is relative). The expression is Modelica text, ready for a modifier:

model MotorDiscovered
  MotorKnown m(tau_missing=0.004*tanh(m.w/2.0) + 2e-07*m.w*abs(m.w));
end MotorDiscovered;

Neural surrogates

truth = daedalus.Model("library/Daedalus", "Daedalus.Examples.SciML.MotorTruth")

def qoi(r):
    return {"w_peak": r["w"].max(), "i_rms": (r["i"] ** 2).mean() ** 0.5}

s = sciml.fit_surrogate(truth, {"Tc": (0, 1e-2), "J": (1e-4, 4e-4)}, qoi, n=120,
                        simulate={"vars": ["w", "i"]})
print(s.report())                 # R2 and largest error on held-out runs
s.predict(Tc=4e-3, J=2e-4)        # {'w_peak': 155.2, 'i_rms': 1.344}
open("MotorSurrogate.mo", "w").write(s.to_modelica("MotorSurrogate"))

fit_surrogate draws a Latin hypercube over the bounds, simulates the runs in parallel threads, holds a fifth of them out, and trains a multilayer perceptron (tanh layers, Adam). qoi is a list of variables (their final values) or a function of the result. The exported block has one input per parameter and one output per quantity, written with scalar equations only, so it runs in any Modelica tool and through daedalus codegen-c on a microcontroller.

For a network trained elsewhere, sciml.MLP can be filled with its weights and exported the same way.

Reference

Function What it does
Model.derivatives(t, states, inputs=, params=, read=) the model's der(x) (and any variables in read) at given points
smooth_derivative(t, y, window=11, order=3) derivative of noisy samples by local polynomial fits
missing_term(model, t, data, term, ...) values of a top-level input along measured states
candidate_library(variables, degree, ...) candidate terms and their Modelica text
sindy(theta, y, terms, threshold=0.05) sparse regression; SparseFit.expression() gives Modelica
latin_hypercube(bounds, n, seed) stratified samples
MLP(n_in, n_out, hidden) a small network: fit, predict, to_modelica
fit_surrogate(model, bounds, qoi, n=200, ...) sample, simulate, train and validate a surrogate

Edit this page on GitHub