860 lines
23 KiB
Typst
860 lines
23 KiB
Typst
#import "@preview/cetz:0.3.2"
|
|
#import "@preview/cetz-plot:0.1.1"
|
|
#import "@preview/finite:0.4.1"
|
|
#import "@preview/touying:0.6.1": *
|
|
#import themes.simple: *
|
|
|
|
// TODO: clarify between
|
|
// `solver.step : time * 's -> (time * (time -> 'y) * zin option) * 's`
|
|
// and `sim.step : (time * (time -> 'a)) * 's -> (time * (time -> 'b)) * 's`
|
|
|
|
#show: simple-theme.with(
|
|
aspect-ratio: "16-9",
|
|
config-common(show-bibliography-as-footnote: bibliography("sources.bib")),
|
|
)
|
|
#let cetz-canvas = touying-reducer.with(
|
|
reduce: cetz.canvas,
|
|
cover: cetz.draw.hide.with(bounds: true)
|
|
)
|
|
#let arrow(start, ..steps, stroke: black) = cetz.draw.line(
|
|
..(steps.pos().fold((start,), (acc, step) => {
|
|
let ((.., (x, y)), (d, l)) = (acc, step)
|
|
let (nx, ny) = if d == "d" {
|
|
(x, y - l)
|
|
} else if d == "u" {
|
|
(x, y + l)
|
|
} else if d == "l" {
|
|
(x - l, y)
|
|
} else if d == "r" {
|
|
(x + l, y)
|
|
} else {
|
|
(x, y)
|
|
}
|
|
acc.push((nx, ny))
|
|
acc
|
|
})),
|
|
mark: (end: "straight"),
|
|
stroke: stroke
|
|
)
|
|
#let (d, u, l, r) = ("d", "u", "l", "r")
|
|
#let box(start, dim, double: false, content: none, ..rest) = {
|
|
let (x, y) = start
|
|
let (dx, dy) = dim
|
|
cetz.draw.rect((x, y), (x + dx, y + dy), ..rest)
|
|
if double {
|
|
cetz.draw.rect((x + 0.1, y + 0.1), (x + dx - 0.1, y + dy - 0.1), ..rest)
|
|
}
|
|
if content != none {
|
|
cetz.draw.content((x + dx / 2, y + dy / 2), content)
|
|
}
|
|
}
|
|
#set raw(syntaxes: "zelus.sublime-syntax")
|
|
#show raw: set text(font: "CaskaydiaCove NF")
|
|
|
|
= Executing Hybrid Systems
|
|
|
|
Henri Saudubray
|
|
#footnote(link("https://codeberg.org/17maiga/hsim"), numbering: "*")
|
|
|
|
22/05/2025
|
|
|
|
|
|
= Hybrid Systems <touying:hidden>
|
|
|
|
== Hybrid systems, as seen in Zélus
|
|
|
|
Computation alternates between _discrete_ and _continuous_ phases.
|
|
#pause #linebreak()
|
|
Use of an ODE solver to approximate continuous behaviour.
|
|
#pause #linebreak()
|
|
Side effects and state jumps occur in discrete steps triggered by
|
|
zero-crossings.
|
|
|
|
#meanwhile
|
|
|
|
#align(horizon, {
|
|
columns(2, [
|
|
```zelus
|
|
let hybrid saw() = y where
|
|
rec der y = 1.0
|
|
init 0.0
|
|
reset z -> (0.0)
|
|
and z = up(y -. 1.0)
|
|
```
|
|
#align(center, {
|
|
import cetz: *
|
|
import cetz-plot.plot: *
|
|
canvas({
|
|
plot(
|
|
size: (9, 4), axis-style: "school-book", legend: (7.25, 4.5),
|
|
x-tick-step: 1, x-label: none, y-tick-step: 1, y-label: none,
|
|
y-grid: true, y-min: -0.5, y-max: 1.5, x-min: 0, x-max: 4.5,
|
|
plot-style: (stroke: blue), {
|
|
for i in range(5) {
|
|
add(((i, 0), (i + 1, 1)))
|
|
}
|
|
add(((0, 0),), label: [`saw`])
|
|
})
|
|
})
|
|
})
|
|
])
|
|
})
|
|
|
|
== Hybrid assertions
|
|
|
|
Assertions are themselves hybrid systems.
|
|
#pause #linebreak()
|
|
Adding assertions can change the final result, they are not _transparent_.
|
|
#pause #linebreak()
|
|
Solution: simulate assertions with their own solver.
|
|
#meanwhile
|
|
|
|
#columns(2, [
|
|
#align(horizon, [
|
|
```zelus
|
|
let hybrid saw() = y where
|
|
rec der y = 1.0
|
|
init 0.0
|
|
reset z -> (0.0)
|
|
and z = up(y -. 1.0)
|
|
and assert(0.0 <= y <= 1.0)
|
|
```
|
|
])
|
|
#colbreak()
|
|
#align(center + horizon, {
|
|
import cetz: *
|
|
canvas({
|
|
import cetz.draw: *
|
|
box((0, 0), (8, 4.5))
|
|
box((2, 0.25), (4, 1.5), content: ```zelus assert```)
|
|
box((2, 2.75), (4, 1.5), content: `...`)
|
|
arrow((-1, 3.5), (r, 3))
|
|
arrow((6, 3.5), (r, 3))
|
|
arrow((7, 3.5), (d, 1.25), (l, 6), (d, 1.25), (r, 1))
|
|
content((-1.5, 3.5), [`()`])
|
|
content((9.5, 3.5), [`y`])
|
|
content((1, 5), [`saw`])
|
|
})
|
|
})
|
|
])
|
|
|
|
= Nodes <touying:hidden>
|
|
|
|
== Discrete nodes
|
|
|
|
Nodes take an additional reset parameter `'p` for their reset function:
|
|
|
|
#align(top)[
|
|
#grid(columns: (3fr, 2fr), align: (left, left),
|
|
```ocaml
|
|
type ('p, 'a, 'b) dnode =
|
|
DNode : {
|
|
state : 's;
|
|
step : 's -> 'a -> 'b * 's;
|
|
reset : 'p -> 's -> 's;
|
|
} -> ('p, 'a, 'b) dnode
|
|
```,
|
|
cetz.canvas({
|
|
import cetz.draw: *
|
|
box((0, 0), (4, 4), content: `DNode`)
|
|
arrow((-1, 2), (r, 1))
|
|
arrow((4, 2), (r, 1))
|
|
arrow((2, 5), (d, 1))
|
|
content((-1.6, 2), `'a`)
|
|
content((5.6, 2), `'b`)
|
|
content((2, 5.6), `'p`)
|
|
}))
|
|
]
|
|
|
|
== Hybrid nodes
|
|
|
|
Hybrid nodes are a bit more complex:
|
|
#block([
|
|
```ocaml
|
|
type ('p, 'a, 'b, 'y, 'yder, 'zin, 'zout) hnode =
|
|
HNode : {
|
|
state : 's;
|
|
step : 's -> 'a -> 'b * 's;
|
|
reset : 'p -> 's -> 's;
|
|
fder : 's -> 'a -> 'y -> 'yder;
|
|
fzer : 's -> 'a -> 'y -> 'zout;
|
|
fout : 's -> 'a -> 'y -> 'b;
|
|
(* ... *)
|
|
} -> ('p, 'a, 'b, 'y, 'yder, 'zin, 'zout) hnode
|
|
```
|
|
#pause
|
|
#place(left + top, dx: 75%, dy: 36%, block(grid(
|
|
align: left + horizon, columns: (1fr, 10fr),
|
|
move(scale(y: 200%, $}$), dy: -2%), [Discrete behaviour]
|
|
), width: 40%))
|
|
#place(left + top, dx: 75%, dy: 62.5%, block(grid(
|
|
align: left + horizon, columns: (1fr, 10fr),
|
|
move(scale(y: 300%, $}$), dy: -2%), [Continuous behaviour]
|
|
), width: 40%))
|
|
])
|
|
|
|
= Solvers are nodes <touying:hidden>
|
|
|
|
== ODE solvers
|
|
|
|
#slide(repeat: 3, self => [
|
|
#let (uncover, only) = utils.methods(self)
|
|
|
|
ODE solvers are discrete nodes producing streams of functions defined on
|
|
successive intervals:
|
|
$(h: bb(R)_+) -->^D (h': bb(R)_+) times (u: [0,h'] -> bb(V))$.
|
|
|
|
#linebreak()
|
|
#grid(columns: (1fr, 1fr), align: (center, left), cetz.canvas({
|
|
import cetz.draw: *
|
|
box((0, 0), (4, 4), content: `csolver`)
|
|
arrow((-1, 2), (r, 1))
|
|
arrow((4, 3), (r, 1))
|
|
arrow((4, 1), (r, 1))
|
|
content((-1.5, 2), $h$)
|
|
content((5.5, 3), $h'$)
|
|
content((5.5, 1), $u$)
|
|
arrow((2, 5), (d, 1))
|
|
content((2, 5.6), "IVP")
|
|
}), align(horizon, cetz-canvas({
|
|
import cetz-plot.plot: *
|
|
only(1, plot(
|
|
size: (10, 4.5), axis-style: "school-book", legend: (9, 4.5),
|
|
y-tick-step: none, y-label: none, y-min: 0, y-max: 6,
|
|
x-tick-step: none, x-label: none, x-min: 0, x-max: 5,
|
|
x-ticks: ((5, $h$), (2, $h'$)), {
|
|
add(domain: (0, 2), t => calc.sin(t) + t, label: $u$)
|
|
}))
|
|
only(2, plot(
|
|
size: (10, 4.5), axis-style: "school-book", legend: (9, 4.5),
|
|
y-tick-step: none, y-label: none, y-min: 0, y-max: 6,
|
|
x-tick-step: none, x-label: none, x-min: 0, x-max: 5,
|
|
x-ticks: ((5, $h$), (2, ""), (4, $h'$)), {
|
|
add(domain: (2, 4), t => calc.sin(t) + t, label: $u$)
|
|
add(domain: (0, 2), t => calc.sin(t) + t)
|
|
}))
|
|
only(3, plot(
|
|
size: (10, 4.5), axis-style: "school-book", legend: (9, 4.5),
|
|
y-tick-step: none, y-label: none, y-min: 0, y-max: 6,
|
|
x-tick-step: none, x-label: none, x-min: 0, x-max: 5,
|
|
x-ticks: ((2, ""), (4, ""), (5, $h', h$)), {
|
|
add(domain: (4, 5), t => calc.sin(t) + t, label: $u$)
|
|
add(domain: (0, 4), t => calc.sin(t) + t)
|
|
}))
|
|
})))
|
|
])
|
|
|
|
== Zero-crossing solvers
|
|
|
|
#slide(repeat: 3, self => [
|
|
#let (uncover, only) = utils.methods(self)
|
|
|
|
Zero-crossing solvers are discrete nodes too, looking for zero-crossings on
|
|
functions:
|
|
$(h: bb(R)_+) times (u: [0, h] -> bb(V)) -->^D
|
|
(h': bb(R)_+) times (z: bb(Z)?)$.
|
|
|
|
#linebreak()
|
|
#grid(columns: (1fr, 1fr), align: (center, left), cetz.canvas({
|
|
import cetz.draw: *
|
|
box((0, 0), (4, 4), content: `zsolver`)
|
|
arrow((-1, 1), (r, 1))
|
|
arrow((-1, 3), (r, 1))
|
|
arrow((4, 1), (r, 1))
|
|
arrow((4, 3), (r, 1))
|
|
arrow((2, 5), (d, 1))
|
|
content((-1.5, 3), $h$)
|
|
content((-1.5, 1), $u$)
|
|
content((5.5, 3), $h'$)
|
|
content((5.5, 1), $z$)
|
|
content((2, 5.6), "ZC")
|
|
}), align(horizon, cetz-canvas({
|
|
import cetz-plot.plot: *
|
|
only(1, plot(
|
|
size: (10, 4.5), axis-style: "school-book", legend: (9, 4.5),
|
|
y-tick-step: none, y-label: none, y-min: -2, y-max: 5,
|
|
x-tick-step: none, x-label: none, x-min: 0, x-max: 2,
|
|
x-ticks: ((2/3, $h', h$),), {
|
|
add(domain: (0, 2/3), t => t * t - 1, label: "u")
|
|
}))
|
|
only(2, plot(
|
|
size: (10, 4.5), axis-style: "school-book", legend: (9, 4.5),
|
|
y-tick-step: none, y-label: none, y-min: -2, y-max: 5,
|
|
x-tick-step: none, x-label: none, x-min: 0, x-max: 2,
|
|
x-ticks: ((1, $h'$), (4/3, $h$)), {
|
|
add(domain: (2/3, 4/3), t => t * t - 1, label: "u")
|
|
add(domain: (0, 2/3), t => t * t - 1)
|
|
}))
|
|
only(3, plot(
|
|
size: (10, 4.5), axis-style: "school-book", legend: (9, 4.5),
|
|
y-tick-step: none, y-label: none, y-min: -2, y-max: 5,
|
|
x-tick-step: none, x-label: none, x-min: 0, x-max: 2,
|
|
x-ticks: ((2, $h', h$),), {
|
|
add(domain: (1, 2), t => t * t - 1, label: "u")
|
|
add(domain: (0, 1), t => t * t - 1)
|
|
}))
|
|
only("1, 3", cetz.draw.content((2, 4), $z = bot$))
|
|
only("2", cetz.draw.content((2, 3.9), $z != bot$))
|
|
})))
|
|
])
|
|
|
|
== A full solver
|
|
|
|
Combine an ODE solver `csolver` with a zero-crossing solver `zsolver`:
|
|
|
|
#linebreak()
|
|
|
|
#align(center, {
|
|
import cetz: *
|
|
canvas({
|
|
import cetz.draw: *
|
|
box((0, 0), (12, 4))
|
|
content((1.5, 4.5), `solver`)
|
|
box((1, 0.5), (4, 3), content: `csolver`)
|
|
box((7, 1.5), (4, 2), content: `zsolver`)
|
|
arrow((-1, 2), (r, 2))
|
|
arrow((5, 3), (r, 2))
|
|
arrow((5, 1), (r, 8))
|
|
arrow((6, 1), (u, 1), ("r", 1))
|
|
arrow((11, 2), (r, 2))
|
|
arrow((11, 3), (r, 2))
|
|
content((-1.5, 2), [$h$])
|
|
content((6, 3.5), [$h'$])
|
|
content((6, 0.5), [$u$])
|
|
content((13.5, 1), [$u$])
|
|
content((13.75, 2), [$h''$])
|
|
content((13.5, 3), [$z$])
|
|
})
|
|
})
|
|
|
|
= Signals <touying:hidden>
|
|
|
|
== Semantics
|
|
|
|
Two semantics are often used for hybrid systems:
|
|
- Superdense semantics @opsem_lee_zheng:
|
|
- signals are functions from $bb(R) times bb(N) -> bb(V)$;
|
|
- discrete instants are ordered lexicographically
|
|
- Non-standard semantics @ns_sem_benveniste_bourke_caillaud_pouzet:
|
|
- signals are functions from the hyperreals $""^*bb(R) -> bb(V)$;
|
|
- discrete instants are ordered using infinitesimals
|
|
|
|
== Optional values
|
|
|
|
The solver does not always reach the requested horizon in a single step.
|
|
#linebreak()
|
|
We _wait_ for it to finish computing:
|
|
|
|
#align(center, {
|
|
import cetz: *
|
|
canvas({
|
|
import cetz.draw: *
|
|
content((0, 1.5), "Input:")
|
|
content((0, 0), "Output:")
|
|
content((3.5, 1.5), $a_0,$)
|
|
content((3.5, 0), $bot,$)
|
|
content((5, 1.5), $bot,$)
|
|
content((5, 0), $b_0,$)
|
|
content((6.5, 1.5), $bot,$)
|
|
content((6.5, 0), $b_1,$)
|
|
content((8, 1.5), $bot,$)
|
|
content((8, 0), $b_2,$)
|
|
content((9.5, 1.5), $bot,$)
|
|
content((9.5, 0), $bot,$)
|
|
content((11, 1.5), $a_2,$)
|
|
content((11, 0), $bot,$)
|
|
content((12.5, 1.5), $bot,$)
|
|
content((12.5, 0), $b_3,$)
|
|
content((14, 1.5), $bot,$)
|
|
content((14, 0), $b_4,$)
|
|
content((15.5, 1.5), $...$)
|
|
content((15.5, 0), $...$)
|
|
})
|
|
})
|
|
|
|
This is represented by optional values:
|
|
|
|
$ "Signal"(alpha) = "Stream"("Optional"((h:bb(R)_+) times ([0,h] -> alpha))) $
|
|
|
|
== Discrete instants
|
|
|
|
Discrete instants are represented by constant functions of length 0. Their
|
|
order is the order of apparition in the stream.
|
|
|
|
== Example
|
|
|
|
The stream
|
|
#align(center, ```ocaml
|
|
f = (1.0, λt -> t)::(0.0, λt -> 2.0)::(0.3, λt -> 3*t)::...
|
|
```)
|
|
defines the function $f : ""^*bb(R)_+ -> bb(R)$
|
|
#grid(columns: (1fr, 1fr), align: (center, center), $ & f(t) = cases(
|
|
"st"(t)#footnote([
|
|
$"st"(t)$ denotes the _standard part_ of $t$, that is, the closest real
|
|
number $r$ to $t$.
|
|
]) & "if" t in [0, 1],
|
|
2 & "if" t = 1 + partial,
|
|
3 dot "st"(t) & "if" t in [1 + 2 partial, 1.3],
|
|
...
|
|
) $, cetz.canvas({
|
|
import cetz-plot.plot: *
|
|
plot(
|
|
size: (10, 3), axis-style: "school-book",
|
|
y-label: none, y-tick-step: none,
|
|
x-label: none, x-tick-step: none, x-ticks: (1, 2.3), x-grid: true,
|
|
{
|
|
add(domain: (0, 1), t => t)
|
|
add(((1, 2),), mark: "o", mark-size: 0.05,
|
|
mark-style: (stroke: blue, fill: blue))
|
|
add(domain: (1, 1.3), t => 3 * t, style: (stroke: blue))
|
|
}
|
|
)
|
|
}))
|
|
|
|
= Simulation <touying:hidden>
|
|
|
|
== Simulation
|
|
|
|
The simulation is itself a discrete node on signals:
|
|
|
|
#align(center, cetz.canvas({
|
|
import cetz.draw: *
|
|
box((0, 0), (12, 5), content: `???`)
|
|
arrow((-1, 2.5), (r, 1))
|
|
arrow((12, 2.5), (r, 1))
|
|
content((-3, 2.5), $"Signal"(alpha)$)
|
|
content((15, 2.5), $"Signal"(beta)$)
|
|
content((1, 5.5), `sim`)
|
|
box((0.5, 0.5), (4, 4), content: `model`)
|
|
box((7.5, 0.5), (4, 4), content: `solver`)
|
|
}))
|
|
|
|
It runs according to one of two modes: _discrete_ and _continuous_.
|
|
|
|
== (Re)initialization
|
|
|
|
When receiving a new value, store it
|
|
#alternatives([...], [and reset the solver.])
|
|
#align(center, cetz-canvas({
|
|
import cetz.draw: *
|
|
|
|
// sim
|
|
content((1, 6.5), `sim`)
|
|
box((0, 0), (14, 6))
|
|
|
|
// model
|
|
content((2.5, 5.25), `model`)
|
|
box((1, 1), (4, 3.75))
|
|
|
|
// model.fzer
|
|
box((1.25, 3), (3.5, 1.5), content: `fzer`)
|
|
|
|
// model.fder
|
|
box((1.25, 1.25), (3.5, 1.5), content: `fder`)
|
|
|
|
// sim state
|
|
box((10, 1.25), (3, 1.5), double: true, content: `state`)
|
|
|
|
// (h, u) -> state
|
|
content((-2.25, 3), $(h, u)$)
|
|
arrow((-1, 3), (r, 1.5), (d, 2.5), (r, 5), (u, 1.5), (r, 4.5))
|
|
|
|
// sim -> bot
|
|
arrow((14, 3), (r, 1))
|
|
content((15.5, 3), $bot$)
|
|
|
|
(pause,)
|
|
|
|
// computation
|
|
box((6, 3), (3, 1.5), content: `...`)
|
|
|
|
// solver
|
|
box((10, 3), (3, 1.5), content: `solver`)
|
|
|
|
// (h, u) -> computation
|
|
arrow((5.5, 2), (u, 1.5), (r, 0.5))
|
|
|
|
// fzer -> computation
|
|
arrow((4.75, 4), (r, 1.25))
|
|
|
|
// fder -> computation
|
|
arrow((4.75, 2), (r, 0.5), (u, 1.75), (r, 0.75))
|
|
|
|
// computation -> solver
|
|
arrow((9, 3.75), (r, 0.5), (u, 1.25), (r, 2), (d, 0.5))
|
|
content((10.5, 5.5), `ivp + zc`)
|
|
}))
|
|
|
|
== Discrete step
|
|
|
|
#alternatives(
|
|
[Use the model's `step` function and the stored input.],
|
|
[Build a constant function out of the output value.],
|
|
[Update the running mode as needed.]
|
|
)
|
|
#align(center, cetz-canvas({
|
|
import cetz.draw: *
|
|
// sim
|
|
content((1, 6.5), `sim`)
|
|
box((0, 0), (17, 6))
|
|
|
|
// sim state
|
|
box((1, 1), (3, 1.5), double: true, content: `state`)
|
|
|
|
// model
|
|
content((6.5, 5.5), `model`)
|
|
box((5, 0.5), (7, 4.5))
|
|
|
|
// model step
|
|
box((7, 1), (3, 1.5), content: `step`)
|
|
|
|
// model state
|
|
box((7, 2.75), (3, 1.5), double: true, content: `state`)
|
|
|
|
// bot -> sim
|
|
arrow((-1, 3), (r, 1))
|
|
content((-1.5, 3), $bot$)
|
|
|
|
// sim.state -> model.step
|
|
arrow((4, 1.5), (r, 3))
|
|
content((5.75, 1.25), $alpha$)
|
|
|
|
// model.state -> model.step
|
|
arrow((7, 3.5), (l, 0.5), (d, 1.5), (r, 0.5))
|
|
content((6, 2.875), $sigma$)
|
|
|
|
(pause,)
|
|
|
|
// constant signal
|
|
box((13, 1), (3, 1.5), content: `...`)
|
|
|
|
// model.step -> model.state
|
|
arrow((10, 2), (r, 0.5), (u, 1.5), (l, 0.5))
|
|
content((11, 2.875), $sigma$)
|
|
|
|
// model.step -> ...
|
|
arrow((10, 1.5), (r, 3))
|
|
content((11.25, 1), $beta$)
|
|
|
|
// constant signal -> signal(beta)
|
|
arrow((16, 1.75), (r, 0.5), (u, 1.25), (r, 1.5))
|
|
content((20, 3), $"Signal"(beta)$)
|
|
|
|
(pause,)
|
|
|
|
// computation for mode
|
|
box((1, 3.5), (3, 1.5), content: `...`)
|
|
|
|
// model.step -> mode computations
|
|
arrow((10.5, 3.5), (u, 1), (l, 6.5))
|
|
|
|
// sim.state -> mod.computations
|
|
arrow((4, 2), (r, 0.5), (u, 2), (l, 0.5))
|
|
|
|
// mod.computations -> sim.state
|
|
arrow((1, 4.25), (l, 0.5), (d, 2.5), (r, 0.5))
|
|
}))
|
|
|
|
== Continuous step
|
|
|
|
#alternatives(
|
|
[Call the solver with the current target horizon.],
|
|
[Use the current input and the result of the solver to build the output.],
|
|
[Update the model's continuous state with the new horizon.],
|
|
[Update the simulation mode in case of a zero-crossing.],
|
|
)
|
|
|
|
#align(center, cetz-canvas({
|
|
import cetz.draw: *
|
|
// sim
|
|
content((1, 6.5), `sim`)
|
|
box((0, 0), (17, 6))
|
|
|
|
// sim.state
|
|
box((1, 1), (3, 1.5), double: true, content: `state`)
|
|
|
|
// bot -> sim
|
|
arrow((-1, 3), (r, 1))
|
|
content((-1.5, 3), $bot$)
|
|
|
|
// solver
|
|
content((6.5, 5.25), `solver`)
|
|
box((5, 0.75), (6, 4))
|
|
|
|
// solver.step
|
|
box((6.5, 1), (3, 1.5), content: `step`)
|
|
|
|
// solver.state
|
|
box((6.5, 2.75), (3, 1.5), double: true, content: `state`)
|
|
|
|
// solver.state -> solver.step
|
|
arrow((6.5, 3.5), (l, 0.5), (d, 1.5), (r, 0.5))
|
|
content((5.5, 2.75), $sigma$)
|
|
|
|
// sim.state -> solver.step
|
|
arrow((4, 1.5), (r, 2.5))
|
|
|
|
(pause,)
|
|
|
|
// solver.step -> solver.state
|
|
arrow((9.5, 2.2), (r, 0.5), (u, 1.3), (l, 0.5))
|
|
|
|
// computation
|
|
box((13, 1), (3, 1.5), content: `...`)
|
|
|
|
// solver.step -> computation
|
|
arrow((9.5, 1.6), (r, 3.5))
|
|
arrow((9.5, 1.3), (r, 3.5))
|
|
|
|
// sim.state -> computation
|
|
arrow((4, 2), (r, 0.5), (d, 1.5), (r, 8), (u, 1.55), (r, 0.5))
|
|
|
|
// computation -> beta signal
|
|
arrow((16, 1.75), (r, 2.5))
|
|
content((20.5, 1.75), $"Signal"(beta)$)
|
|
|
|
(pause,)
|
|
|
|
// model
|
|
box((12.5, 2.75), (4, 2))
|
|
content((14, 5.25), `model`)
|
|
|
|
// model.state
|
|
box((13, 3), (3, 1.5), double: true, content: `state`)
|
|
|
|
// step -> model
|
|
arrow((12.25, 1.3), (u, 2.2), (r, 0.75))
|
|
arrow((12, 1.6), (u, 2.4), (r, 1))
|
|
|
|
(pause,)
|
|
|
|
// computation
|
|
box((1, 2.75), (3, 1.5), content: `...`)
|
|
|
|
// solver.step -> computation
|
|
arrow((9.5, 1.9), (r, 1), (u, 2.6), (l, 6), (d, 1), (l, 0.5))
|
|
|
|
// computation -> solver.state
|
|
arrow((1, 3.5), (l, 0.5), (d, 1.75), (r, 0.5))
|
|
}))
|
|
#pause
|
|
Is that it?
|
|
|
|
= States <touying:hidden>
|
|
|
|
== The state problem
|
|
|
|
#slide(self => [
|
|
#let (uncover, only) = utils.methods(self)
|
|
Some solvers modify their state in-place (e.g. #smallcaps("Sundials")).
|
|
#linebreak()
|
|
The function returned depends on this state.
|
|
#pause #linebreak()
|
|
It becomes invalid once we call the solver again.
|
|
|
|
#meanwhile
|
|
#align(center, cetz-canvas({
|
|
import cetz-plot.plot: *
|
|
plot(
|
|
size: (10, 4), axis-style: "school-book",
|
|
x-label: none, x-tick-step: none, x-max: 6,
|
|
y-label: none, y-tick-step: none, y-max: 6,
|
|
x-grid: true, x-ticks: ((4.5, ""),),
|
|
{
|
|
only(1, add(domain: (0, 4.5), t => calc.sin(t) + t))
|
|
only(2, add(domain: (4.5, 6), t => calc.sin(t) + t))
|
|
only(2, add(domain: (0, 4.5), t => calc.sin(t) + t))
|
|
}
|
|
)
|
|
}))
|
|
])
|
|
|
|
== Solver steps
|
|
|
|
Most solvers start off with smaller steps, and increase the step length
|
|
progressively.
|
|
|
|
#align(center, cetz.canvas({
|
|
import cetz-plot.plot: *
|
|
plot(
|
|
size: (10, 4), axis-style: "school-book", x-label: none, x-tick-step: none,
|
|
x-ticks: ((0,""), (0.001,""), (0.01,""), (0.1,""), (0.2,""), (0.4,""),
|
|
(1,""), (1.9,""), (3.9, "")), x-grid: true,
|
|
y-label: none, y-tick-step: none, {
|
|
add(domain: (0, 6), t => calc.sin(t) + t)
|
|
}
|
|
)
|
|
}))
|
|
|
|
Combined with in-place state modifications, resets and composition, this could
|
|
hinder performance.
|
|
|
|
== State copies
|
|
|
|
Copying (part of) the state solves this issue.
|
|
#linebreak()
|
|
If the solver supports state copies, we can call it more than once per
|
|
simulation step.
|
|
|
|
```ocaml
|
|
type ('p, 'a, 'b) dnode_c =
|
|
DNodeC : {
|
|
state : 's;
|
|
step : 's -> 'a -> 'b * 's;
|
|
reset : 'p -> 's -> 's;
|
|
copy : 's -> 's;
|
|
} -> ('p, 'a, 'b) dnode_c
|
|
```
|
|
|
|
== Accelerating the simulation
|
|
|
|
We can then concatenate functions obtained from successive continuous steps.
|
|
If no zero-crossing is found, there is no discontinuity.
|
|
|
|
#linebreak()
|
|
|
|
#grid(align: center + horizon, columns: (10fr, 1fr, 10fr),
|
|
cetz.canvas({
|
|
import cetz-plot.plot: *
|
|
plot(
|
|
size: (8, 4), axis-style: "school-book", x-label: none,
|
|
x-tick-step: none, x-ticks: ((3, $h_1$), (6, $h_2$)), x-grid: true,
|
|
y-label: none, y-tick-step: none, {
|
|
add(domain: (0, 3), t => calc.sin(t) + t)
|
|
add(domain: (3, 6), t => calc.sin(t) + t)
|
|
}
|
|
)
|
|
}), $==>$, cetz.canvas({
|
|
import cetz-plot.plot: *
|
|
plot(
|
|
size: (8, 4), axis-style: "school-book", x-label: none,
|
|
x-tick-step: none, x-ticks: ((6, $h_1$),), x-grid: true, y-label: none,
|
|
y-tick-step: none, { add(domain: (0, 6), t => calc.sin(t) + t) }
|
|
)
|
|
})
|
|
)
|
|
|
|
= Composing simulations <touying:hidden>
|
|
|
|
== Pushing values through
|
|
|
|
#slide(self => [
|
|
#let (uncover, only) = utils.methods(self)
|
|
|
|
Unless we can copy the state, we need to use up all of a value before
|
|
producing the next. We need to push empty values ($bot$) as needed.
|
|
|
|
#align(center, cetz-canvas({
|
|
import cetz.draw: *
|
|
box((0, 0), (3, 3), content: $M_1$)
|
|
box((8, 0), (3, 3), content: $M_2$)
|
|
only("1, 3-4", arrow((-1, 1.5), (r, 1)))
|
|
only("2, 5-", arrow((-1, 1.5), (r, 1), stroke: red))
|
|
only("1, 2, 4-", arrow((3, 1.5), (r, 5)))
|
|
only("3", arrow((3, 1.5), (r, 5), stroke: red))
|
|
only("-3", arrow((11, 1.5), (r, 1)))
|
|
only("4-", arrow((11, 1.5), (r, 1), stroke: red))
|
|
content((-3, 1.5), $"Signal"(alpha)$)
|
|
content((5.5, 2), $"Signal"(beta)$)
|
|
content((14, 1.5), $"Signal"(gamma)$)
|
|
|
|
content((-4, -1), $alpha:$)
|
|
content((-4, -2.5), $beta:$)
|
|
content((-4, -4), $gamma:$)
|
|
(pause,)
|
|
content((-2, -4), $bot$)
|
|
content((-2, -2.5), $bot$)
|
|
content((-2, -1.1), $alpha_1$)
|
|
(pause,)
|
|
content((0, -1), $bot$)
|
|
content((0, -2.6), $beta_1$)
|
|
content((0, -4), $bot$)
|
|
(pause,)
|
|
content((2, -1), $bot$)
|
|
content((2, -2.5), $bot$)
|
|
content((2, -4.1), $gamma_1$)
|
|
(pause,)
|
|
content((4, -1.1), $alpha_2$)
|
|
content((4, -2.5), $bot$)
|
|
content((4, -4.1), $gamma_2$)
|
|
content((5.5, -1), $...$)
|
|
content((5.5, -2.5), $...$)
|
|
content((5.5, -4), $...$)
|
|
(pause,)
|
|
box((3.5, -3), (1, 1), stroke: red)
|
|
box((0.1, 0.1), (2.8, 2.8), stroke: red)
|
|
content((12, -2.6), "Possible early reset!")
|
|
}))
|
|
])
|
|
|
|
== Pulling values from
|
|
|
|
#slide(self => [
|
|
#let (uncover, only) = utils.methods(self)
|
|
|
|
The output signal stops at least as often as the input signal does.
|
|
#linebreak()
|
|
This calls for a "lazy" composition: we request rather than provide data.
|
|
|
|
#align(center, cetz-canvas({
|
|
import cetz.draw: *
|
|
box((0, 0), (3, 3), content: $M_1$)
|
|
box((8, 0), (3, 3), content: $M_2$)
|
|
only("1-3, 5-10, 12-", arrow((-1, 1.5), (r, 1)))
|
|
only("4, 11", arrow((-1, 1.5), (r, 1), stroke: red))
|
|
only("1-2, 4-5, 7-8, 10-11, 13-", arrow((3, 1.5), (r, 5)))
|
|
only("3, 6, 9, 12", arrow((3, 1.5), (r, 5), stroke: red))
|
|
only("1, 3-4, 6, 9, 11-12, 14-", arrow((11, 1.5), (r, 1)))
|
|
only("2, 5, 7, 8, 10, 13", arrow((11, 1.5), (r, 1), stroke: red))
|
|
content((-3, 1.5), $"Signal"(alpha)$)
|
|
content((5.5, 2), $"Signal"(beta)$)
|
|
content((14, 1.5), $"Signal"(gamma)$)
|
|
|
|
content((-4, -1), $alpha:$)
|
|
content((-4, -2.5), $beta:$)
|
|
content((-4, -4), $gamma:$)
|
|
(pause,)
|
|
content((-2, -4), $bot$)
|
|
(pause,)
|
|
content((-2, -2.5), $bot$)
|
|
(pause,)
|
|
content((-2, -1.1), $alpha_1$)
|
|
(pause,)
|
|
content((0, -4), $bot$)
|
|
(pause,)
|
|
content((0, -2.6), $beta_1$)
|
|
(pause,)
|
|
content((2, -4.1), $gamma_1$)
|
|
(pause,)
|
|
content((4, -4.1), $gamma_2$)
|
|
(pause,)
|
|
content((6, -2.6), $beta_2$)
|
|
content((6, -4), $bot$)
|
|
(pause,)
|
|
content((8, -4.1), $gamma_3$)
|
|
(pause,)
|
|
content((10, -4), $bot$)
|
|
content((10, -2.5), $bot$)
|
|
content((10, -1.1), $alpha_2$)
|
|
(pause,)
|
|
content((12, -2.6), $beta_3$)
|
|
content((12, -4), $bot$)
|
|
(pause,)
|
|
content((14, -4.1), $gamma_4$)
|
|
(pause,)
|
|
content((15.5, -1), $...$)
|
|
content((15.5, -2.5), $...$)
|
|
content((15.5, -4), $...$)
|
|
}))
|
|
])
|
|
|
|
= Conclusion <touying:hidden>
|
|
|
|
== What next?
|
|
|
|
#align(horizon, [
|
|
- Can we avoid pointless solver resets?
|
|
- Think about assertions: is the checking of an assertion a composition?
|
|
- Plug to Zélus' output
|
|
])
|
|
|