feat: lift runtime into language, start of zelus 2024 compatibility

This commit is contained in:
Henri Saudubray 2025-07-11 11:21:07 +02:00
parent dc8d941b84
commit ffc583985a
Signed by: hms
GPG key ID: 7065F57ED8856128
37 changed files with 1154 additions and 143 deletions

View file

@ -11,10 +11,12 @@
#let simulink = smallcaps[Simulink]
#let sundials = smallcaps[Sundials CVODE]
#let zelus = smallcaps[Zélus]
#let TODO(..what) = {
#let note(color, prefix, ..what) = {
let msg = if what.pos().len() == 0 { "" } else { ": " + what.pos().join("") }
block(fill: red, width: 100%, inset: 5pt, align(center, raw("TODO" + msg)))
block(fill: color, width: 100%, inset: 5pt, align(center, raw(prefix + msg)))
}
#let TODO(..what) = note(red, "TODO", ..what)
#let MENTION(..what) = note(gray, "MENTION", ..what)
#let adot(s) = $accent(#s, dot)$
#let addot(s) = $accent(#s, dot.double)$
@ -97,14 +99,14 @@ physical systems. Continuous phases are described using ordinary differential
equations (ODEs), while discrete phases can be represented as a reactive
program in a synchronous language such as #lustre or #esterel.
As a first example, say we wish to model a bouncing ball. We could start by
describing its distance from the ground $y$ with a second-order differential
equation
$ addot(y) = -9.81 $
As an illustration, say we wished to model an extensively studied system: a
bouncing ball. We could start by describing its distance from the ground $y$ as
a function of time, with a second-order differential equation
$ addot(y) = -9.81, $
where $addot(y)$ denotes the second order derivative of $y$ with
respect to time (the acceleration of the ball), and $9.81$ is the gravitational
constant $g$: the acceleration of the ball is its negation. We now give the
initial position and speed of the ball:
respect to time $(d^2y)/(d t^2)$ (the acceleration of the ball), and $9.81$ is
the gravitational constant $g$: the acceleration of the ball is its negation. We
now give the initial position and speed of the ball:
$ y(0) = 50 space space space adot(y)(0) = 0 $
We have just described an initial value problem: given an ODE and an initial
value for its dependent variable, its solution is a function $y(t)$ returning
@ -113,17 +115,20 @@ this function using an ODE solver, such as #sundials.
As of right now, our ball will fall until the end of time; we have not said
anything about how it bounces when it hits the floor. To do so, we need a notion
of discrete _events_. These are modelled by zero-crossings: we monitor a certain
value and stop when it goes from strictly negative to positive or null. For our
purposes, we choose $-y$ as the monitored value, and call the zero-crossing
event $z$. When $z$ occurs (i.e., when the ball touches the ground), we set the
speed $adot(y)$ to $-k dot "last"(adot(y))$, where $"last"(y)$ denotes the left
limit of $y$ (we cannot specify $adot(y)$ in terms of itself), and $k$ is a
factor modelling the loss of inertia due to the collision (say, $0.8$). We can
then resume the approximation of the solution.
of _events_: we need to identify exactly when the ball hits the ground, so that
we may take action to make it bounce. These events are modelled by
zero-crossings: we monitor a certain value and stop when it goes from strictly
negative to positive or null. For our purposes, we choose $-y$ as the monitored
value, and call the zero-crossing event $z$. When $z$ occurs (i.e., when the
ball touches the ground), we set the speed $adot(y)$ to
$-k dot #raw(lang: "zelus", "last")\(adot(y))$, where
$#raw(lang: "zelus", "last")\(y)$ denotes the left limit of $y$ (we cannot
specify $adot(y)$ in terms of itself), and $k$ is a factor modelling the loss of
inertia due to the collision (say, $0.8$). We can then resume the approximation
of the solution.
@lst:ball.zls shows how such a model might be expressed in the concrete syntax
of #zelus.
of #zelus @cit:zelus_sync_lng_with_ode.
#figure(placement: top, gap: 2em,
```zelus
@ -135,22 +140,68 @@ of #zelus.
caption: [The bouncing ball in #zelus]
) <lst:ball.zls>
More formally, a hybrid system can be described as an automaton
More formally, and as done in @cit:alg_ana_hyb_sys, a hybrid system $cal(H)$ can
be defined as a graph whose nodes represent continuous behaviour, and whose
edges represent discrete transitions:
$ cal(H) = (L o c, V a r, E d g, A c t, I n v, I n i) $
where:
- $L o c$ is a finite set of _locations_;
- $V a r$ is a finite set of _variables_;
- $E d g subset.eq L o c times F times L o c$ is a finite set of _transitions_
== Executing models
Executing such a model is quite simple. There are two modes of execution:
discrete and continuous. In continuous mode, we call the ODE solver in order to
obtain an approximation of the variables defined through ODEs, and monitor for
zero-crossings. If a zero-crossing occurs, we enter the discrete mode, in which
we perform computation steps as needed, until no other zero-crossing occurs, in
which case we go back to the continuous mode, and repeat, as seen in @automaton.
To execute such a model, we first compile it into a synchronous function, as
described in @cit:sync_based_codegen_hyb_sys_lng. The details of this
compilation step are not particularly relevant to our purposes, and can be
ignored. What is more interesting is the output of this compilation step: a
single synchronous function. The simulation loop is then itself described as a
synchronous function operating on
#figure(finite.automaton(
(D: (D: "cascade", C: "no cascade"),
C: (C: "no zero-crossing", D: "zero-crossing")),
initial: "D", final: (), layout: finite.layout.linear.with(spacing: 3)
), caption: [High-level overview of the runtime], placement: top) <automaton>
#MENTION("Use of a single solver")
#pagebreak()
// The compilation of a hybrid model into a synchronous function is described in
// detail in @cit:sync_based_codegen_hyb_sys_lng, but can be summarized quite
// succintly as follows. By pairing this synchronous function with an
// off-the-shelf ODE solver like #sundials, we can then simulate the dynamics of
// the system. This is done by repeatedly performing execution steps according to
// two different modes: discrete and continuous.
// The continuous mode operates as follows: we first call the ODE solver in order
// to approximate the dynamics of the model's continuous state.
// Continuous steps first call the ODE solver to approximate the dynamics of the
// model's continuous variables. The solver will return a function defined on a
// time interval, which we then provide as input to the zero-crossing solver, which
// will monitor the evolution of zero-crossing values along this interval. After
// both solvers have been called, we choose what the next step's mode will be:
// - if no zero-crossings have been detected, we output the entire solution
// provided by the ODE solver, and the next step remains continuous;
// - if a zero-crossing occurs, we return the solution provided by the ODE solver
// up to the zero-crossing instant, and the next step becomes a discrete step.
// Discrete steps perform state changes and side effects. We first call the model's
// step function, which updates the state and outputs a value. We then decide what
// the next step is. If a zero-crossing event occured due to the current step, the
// next step is another discrete step. If no new event occured, we perform a
// continuous step.
// Executing such a model is quite simple. There are two modes of execution:
// discrete and continuous. In continuous mode, we call the ODE solver in order
// to obtain an approximation of the variables defined through ODEs, and monitor
// for zero-crossings. If a zero-crossing occurs, we enter the discrete mode, in
// which we perform computation steps as needed, until no other zero-crossing
// occurs, in which case we go back to the continuous mode, and repeat, as seen
// in @automaton.
// #figure(finite.automaton(
// (D: (D: "cascade", C: "no cascade"),
// C: (C: "no zero-crossing", D: "zero-crossing")),
// initial: "D", final: (), layout: finite.layout.linear.with(spacing: 3)
// ), caption: [High-level overview of the runtime], placement: top) <automaton>
= Runtime
To solve this issue, we need to redefine what the runtime of our hybrid system
@ -180,10 +231,10 @@ required by the assertion becomes a state variable.
== Solvers as synchronous nodes
== Simulations as synchronous nodes
#TODO("talk about the new runtime")
#MENTION("the new runtime")
= Assertions
#TODO("talk about how assertions are done")
#MENTION("how assertions are done")
#pagebreak()
= Annex