#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 == 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 == 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 == 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 == 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 == 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 == 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 == 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 == 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 ])