feat: lift runtime into language, start of zelus 2024 compatibility
This commit is contained in:
parent
dc8d941b84
commit
ffc583985a
37 changed files with 1154 additions and 143 deletions
115
doc/rep.typ
115
doc/rep.typ
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue