feat (sim): greedy simulation

This commit is contained in:
Henri Saudubray 2025-04-22 17:57:10 +02:00
parent b4a29bbb97
commit 3d317f65a0
Signed by: hms
GPG key ID: 7065F57ED8856128
5 changed files with 421 additions and 262 deletions

View file

@ -1,145 +0,0 @@
\documentclass[a4paper]{article}
\usepackage{fullpage}
\usepackage{listings}
\usepackage{xcolor}
\title{A formalization of a simulation engine for hybrid systems}
\author{}
\date{}
\setlength{\parindent}{0pt}
\lstdefinelanguage{ml}{
basicstyle=\ttfamily,
morekeywords=[1]{
type, float, option, let, fun, with, if, else, then, in, as, match,
},
morekeywords=[2]{
HNode, DNode, Idle, Running, Discrete, Continuous
},
keywordstyle=[1]{\color{blue}},
keywordstyle=[2]{\color{red}},
commentstyle=\itshape,
columns=[l]fullflexible,
sensitive=true,
morecomment=[s]{(*}{*)},
keepspaces=true,
literate=
{'a}{$\alpha$}{1}
{'b}{$\beta$}{1}
{'p}{$\rho$}{1}
{'s}{$\sigma$}{1}
{'y}{$y$}{1}
{'yder}{$\dot{y}$}{1}
{'zin}{$z_{in}$}{1}
{'zout}{$z_{out}$}{1}
{fun\ }{$\lambda$}{1}
{->}{$\to$}{1}
{+.}{$+$}{1}
{-.}{$-$}{1}
{=}{$=$}{1}
{>=}{$\geq$}{1}
{<=}{$\leq$}{1}
}
\lstnewenvironment{ml}{\lstset{language=ml}}{}
\newcommand{\mlf}[1]{\lstinline[language=ml]{#1}}
\begin{document}
\maketitle
A discrete synchronous function, or node, can be seen as a pair of a step and
reset function, which operate on an inner state:
\begin{ml}
type ('p, 'a, 'b) dnode =
DNode : { s : 's;
step : 's -> 'a -> 'b * 's;
reset : 'p -> 's -> 's } -> ('p, 'a, 'b) dnode
\end{ml}
A hybrid node is quite similar: it has an inner state, a step and a reset
function; but the step function is decomposed into multiple distinct elements
for the purpose of the simulation:
\begin{ml}
type ('p, 'a, 'b, 'y, 'yder, 'zin, 'zout) hnode =
HNode : { s : 's;
step : 's -> 'a -> 'b * 's;
fder : 's -> 'a -> 'y -> 'yder;
fzer : 's -> 'a -> 'y -> 'zout;
fout : 's -> 'a -> 'y -> 'b;
reset : 'p -> 's -> 's;
horizon : 's -> time;
jump : 's -> bool;
cget : 's -> 'y;
cset : 's -> 'y -> 's;
zset : 's -> 'zin -> 's;
} -> ('p, 'a, 'b, 'y, 'yder, 'zin, 'zout) hnode
\end{ml}
\mlf{step} and \mlf{reset} are on discrete steps, and behave in the same way as
for a discrete node. \mlf{fder}, \mlf{fzer} and \mlf{fout} are used during
integration phases with a solver. \mlf{fder} represents the derivative function
in an initial value problem. \mlf{fzer} is the zero-crossing function, which
computes the values of all zero-crossing function we wish to monitor. \mlf{fout}
computes the output of the system at a particular instant.
\begin{figure}
\begin{ml}
let sim (HNode model) (DNode solver) =
let s = { status = Idle; mstate = model.state; sstate = solver.state } in
let step state input =
match input, state.status with
| Some i, _ ->
let status = Running
{ mode = Continuous; input = i; now = 0.0; stop = i.length } in
None, { state with status }
| None, Idle -> None, state
| None, Running ({ mode = Discrete; _ } as r) ->
let o, mstate = model.step state.mstate (r.input.u r.now) in
let state =
let h = model.horizon mstate in
if h <= 0.0 then { state with mstate }
else if r.now >= r.stop then s
else if model.jump mstate then
let y = model.cget mstate in
let fder t = model.fder mstate (offset r.input r.now t) in
let fzer t = model.fzer mstate (offset r.input r.now t) in
let ivp = { fder; stop = r.stop -. r.now; init = y } in
let zc = { yc = y; fzer } in
let sstate = solver.reset (ivp, zc) state.sstate in
let status = Running { r with mode = Continuous } in
{ status; mstate; sstate }
else { state with status = Running { r with mode = Continuous } }
in Some { start = r.now; length = 0.0; u = fun _ -> o }, state
| None, Running ({ mode = Continuous; _ } as r) ->
let (h, f, z), sstate = solver.step state.sstate r.stop in
let mstate = model.cset state.mstate (f h) in
let now = r.input.start +. h in
let state = match z with
| None ->
let status =
if h >= r.stop then Running { r with mode = Discrete; now }
else Running { r with now } in
{ status; mstate; sstate }
| Some z ->
let status = Running { r with mode = Discrete; now } in
{ status; mstate = model.zset mstate z; sstate } in
let fout t =
model.fout mstate (r.input.u (r.now +. t)) (f (r.now +. t)) in
let out =
{ start = r.input.start +. r.now; length = h -. r.now; u = fout } in
Some out, state in
let reset (m, s) { mstate; sstate; _ } =
let mstate = model.reset m mstate in
let sstate = solver.reset s sstate in
{ status = Idle; mstate; sstate } in
DNode { state = s; step; reset }
\end{ml}
\label{fig:ml:sim}
\caption{Hybrid System Simulation}
\end{figure}
\end{document}

View file

@ -1,17 +1,36 @@
#import "@preview/cetz:0.3.2"
#import "@preview/cetz-plot:0.1.1"
#import "@preview/finite:0.4.1"
#set page(paper: "a4")
#set par(justify: true)
#let breakl(b) = {
raw(b.text.replace(regex(" *\\\\\n *"), " "), lang: b.lang, block: b.block)
}
#show raw: set text(font: "CaskaydiaCove NF")
#show link: underline
= Notes on hybrid system simulation
== TODO
Multiple different axes of research:
- abstract over the notion of internal state and of our ability to copy some or
part of it
- make free variables contained in solver closures explicit, and require the
ability to copy them for greedy simulation (without the solver itself knowing
about them?)
- think about the notion of input values with overlapping domains
- think about successive input values carrying information about possible
discontinuities
== The problem
The initial problem is _transparent assertions_. We want to simulate assertions
with their own solvers, because simulating assertions with the same solver as
the parent system changes the final results, when assertions are supposed to be
transparent.
The overarching problem is _transparent assertions_. We want to simulate
assertions with their own solvers, because simulating assertions with the same
solver as their parent system changes the final results, when assertions are
supposed to be transparent.
Assertions are themselves hybrid systems, and can contain their own assertions.
We thus need a semantics that is both *independent* of the solver, which becomes
@ -29,69 +48,333 @@ An ODE solver is, in fact, a special case of a discrete node, which is
initialized with an initial value problem, takes as input a time horizon to
reach, and returns as output an actual time reached and a dense solution.
```ocaml
type ('y, 'yder) csolver =
(('y, 'yder) ivp, time, time * (time -> 'y)) dnode
type ('y, 'yder) ivp = { init : 'y; fder : time -> 'y -> 'yder; stop : time }
type ('y, 'yder) csolver = (('y, 'yder) ivp, time, time * (time -> 'y)) dnode
```
Similarly, a zero-crossing solver is initialized with a zero-crossing problem,
takes as input a horizon and dense function, and returns an actual time reached
and optional zero-crossing.
```ocaml
type ('y, 'zout) zc = { fzer : time -> 'y -> 'zout; yc : 'y }
type ('y, 'zin, 'zout) zsolver =
(('y, 'zout) zc, time * (time -> 'y), time * 'zin option) dnode
```
A complete solver is then a composition of these two: it is initialized with
both an `ivp` and `zc`, takes in a horizon, and returns an actual time reached,
dense solution and optional zero-crossing.
```ocaml
#breakl(```ocaml
type ('y, 'yder, 'zin, 'zout) solver =
(('y, 'yder) ivp * ('y, 'zout) zc, time, time * (time -> 'y) * 'zin option) dnode
```
(('y, 'yder) ivp * ('y, 'zout) zc, time, time * (time -> 'y) * 'zin option) \
dnode
```)
The construction of a full solver from an ODE and a zero-crossing solver is
available in the file `src/lib/hsim/solver.ml`.
The simulation of a hybrid system with a solver can itself be seen as a discrete
node, operating on streams of functions defined on successive intervals.
== Output format
```ocaml
type ('p, 'a, 'b) sim = ('p, 'a signal, 'b signal) dnode
```
In "lazy" mode, the output has format ```ocaml 'b signal = 'b value option```.
== The simulation
In "greedy" mode, the output has format ```ocaml 'b value list```.
=== Modes and output format
Indeed, we cannot concatenate a discrete step (i.e. a function defined on a
singleton interval $[t,t]$) with anything else, as it may be hidden by the
(inclusive) border of the other function:
There are two simulation modes, depending on whether the solver provides a way
to copy its internal state as part of its interface, or more precisely, whether
the dense solution returned by the solver's step function remains valid after we
modify the solver's state (through another integration step or a full reset).
In *_lazy_* mode, the output has format ```ocaml 'b signal = 'b value option```.
The input stream first provides a value for the simulation to run, and must then
provide as many `None` values as needed for the solver to finish integrating as
much as possible (that is, until it reaches a zero-crossing or the horizon
provided by the current input value). This mode is the one used when the solver
modifies its solution in place (for example, #smallcaps([Sundials])). Each step
of the simulation runs _one_ step of the solver, and then runs all subsystems as
much as they need to in order for them to finish using the output of the solver.
The solver output is then not needed anymore, and we can call the solver again
to progress with the simulation.
In *_greedy_* mode, the output has format ```ocaml 'b value list```. The input
stream provides a value for the simulation to run, and the simulation then calls
the solver as much as it needs to until it reaches the end of the possible
integration period (zero-crossing or horizon). We concatenate the list of
results (all functions returned from the solver steps are continuous at their
edges and defined on more than a single instant, and we can thus create a large,
piecewise-defined function from all the results up to this point). The solver
then performs as many discrete steps as needed (i.e. we cascade). These are kept
in the output list _as is_. We cannot concatenate a discrete step (i.e. a
function defined on a singleton interval $[t,t]$) with anything else, as it may
be hidden by the (inclusive) border of the other function. For instance,
#align(center, ```ocaml
f = concat { start = 0.0; length = 1.0; u = fun t -> t }
{ start = 1.0; length = 0.0; u = fun t -> 2.0 }
```)
#columns(2, [
#align(center, cetz.canvas({
cetz-plot.plot.plot(
size: (1, 1), x-tick-step: 1, y-tick-step: 1, axis-style: "left", {
cetz-plot.plot.add(((0,0),(1,1)))
cetz-plot.plot.add(((1,2),), mark: "o", mark-size: .03)})}))
#colbreak()
#linebreak()
#columns(2, [#{
align(center, cetz.canvas({
import cetz-plot.plot: *
plot(size: (1, 0.5), axis-style: "left", x-tick-step: 1, x-label: "time",
y-tick-step: 1, y-label: none, {
add(((0,0),(1,1)))
add(((1,2),), mark: "o", mark-size: .03)
})
}))
colbreak()
$ f(t) = cases(
t & "if" 0.0 <= t <= 1.0,
2.0 & "if" t = 1.0 + partial,
bot & "otherwise",
) $
}])
Here, what is `f.u 1.0`? We cannot differentiate between the multiple
infinitesimal steps at a given real time `t` using only floating-point numbers
as input. Therefore, we keep the list of inputs separated, apart from the
adjacent continuous functions, which we can concatenate together. Until we reach
the horizon given by the input function, we keep alternating continuous and
discrete steps. The output is a list of functions, which we can interpret as a
stream, and the subsystems (the assertions, for instance) can then be called
repeatedly until they reach the same horizon as the parent system. This has the
advantage of not needing/having optional values in the input and output streams,
and the calling system does not need to guess how many steps the simulation will
need to reach the requested horizon.
=== Steps
A simulation step is either a discrete step or a continuous step.
Discrete steps call the `model.step` function, build a constant output function
defined on a singleton interval `[now, now]`, and end in one of four possible
cases:
- We need to cascade (perform another discrete step)
- We reached the horizon (reset the solver? idle mode?)
- We can still integrate, but there has been a state jump (reset solver)
- We can still integrate, and there has been no state jump (perform a continuous
step)
#columns(4, [#align(center, {
import cetz: *
import cetz-plot.plot: *
let p = palette.new(colors: (blue,blue, blue)).with(stroke: true, fill: true)
canvas({
plot(
size: (2,2), axis-style: "left", x-tick-step: none, x-label: none,
x-ticks: ((1,none),), x-grid: "both", y-tick-step: none, y-label: none,
y-min: 1, y-ticks: ((4, none),), y-grid: "both",
style: p, {
add(((0,2),(.3,2.1),(.7,2.9),(1,3)), line: "spline")
add(((1,4),), mark: "o", mark-size: .03)
add(((1,5),(1.3,4.7),(1.7,3.2),(2,3)), line: "spline")
})
})
colbreak(); canvas({
plot(
size: (2,2), axis-style: "left", x-tick-step: none, x-label: none,
x-ticks: ((3,none),), x-grid: "both", y-tick-step: none, y-label: none,
y-min: 1, y-ticks: ((3, none),), y-grid: "both", {
add(((0,2),(.5,3.2),(1,5.5),(2,4),(3,3)), line: "spline")
})
})
colbreak(); canvas({
plot(
size: (2,2), axis-style: "left", x-tick-step: none, x-label: none,
x-ticks: ((1, none),), x-grid: "both", y-tick-step: none, y-label: none,
y-min: 1, y-ticks: ((5, none),), y-grid: "both", {
add(((0,2),(0.5,2.3),(1,3)), line: "spline")
add(((1,5),(1.5,4.7),(2,4)), line: "spline")
})
})
colbreak(); canvas({
plot(
size: (2,2), axis-style: "left", x-tick-step: none, x-label: none,
x-ticks: ((2,none),), x-grid: "both", y-tick-step: none, y-label: none,
y-min: 1, y-ticks: ((4, none),), y-grid: "both", {
add(((0,2),(0.5,2.3),(1,3),(1.3,5),(2,4),(3,3)), line: "spline")
})
})
})])
Continuous steps call the `solver.step` function, parameterized by `model.fder`
and `model.fzer`, build an output function using `model.fout` defined on the
interval `[now, now + h]`, and end in one of three possible cases:
- We found a zero-crossing (perform a discrete step)
- We found no zero-crossing, but have reached the horizon (perform a discrete
step)
- We found no zero-crossing and have not reached the horizon (perform another
continuous step)
#columns(3, [#align(center, {
import cetz: *
import cetz-plot.plot: *
let p = palette.new(colors: (blue,blue, blue)).with(stroke: true, fill: true)
canvas({
plot(
size: (2,2), axis-style: "left", x-tick-step: none, x-label: none,
x-ticks: ((.7,none),), x-grid: "both", y-tick-step: none, y-label: none,
y-min: -2, y-ticks: ((0,$0$),), y-grid: "both", {
add(((0,-1),(.5,-.6),(.7,0),(1,2.5),(2,1),(3,.5)), line: "spline")
})
})
colbreak(); canvas({
plot(
size: (2,2), axis-style: "left", x-tick-step: none, x-label: none,
x-ticks: ((3,none),), x-grid: "both", y-tick-step: none, y-label: none,
y-min: 1, y-ticks: ((3, none),), y-grid: "both", {
add(((0,2),(.5,3.2),(1,5.5),(2,4),(3,3)), line: "spline")
})
})
colbreak(); canvas({
plot(
size: (2,2), axis-style: "left", x-tick-step: none, x-label: none,
x-ticks: ((2,none),), x-grid: "both", y-tick-step: none, y-label: none,
y-min: 1, y-ticks: ((4, none),), y-grid: "both", {
add(((0,2),(0.5,2.3),(1,3),(1.3,5),(2,4),(3,3)), line: "spline")
})
})
})])
=== Resets
==== Solver reset
When do we need to reset the solver itself?
If we receive a new input that is not continuous w.r.t. the previous input, we
should reset the solver before starting the integration. This implies that an
input value is tagged with information relating it to the previous input value.
If this is not the case, we can reset whenever we receive a new input value,
although this is less satisfactory.
If a discrete step results in a state jump, we should reset the solver before
continuing the integration.
==== Simulation reset
Two possible options for the simulation reset:
- ask for reset parameters for both the model and solver:
```ocaml
'p * (('y, 'yder) ivp * ('y, 'zin, 'zout) zc
```
- ask for only what is needed for the reset of the model, then build a new
initial value problem and zero-crossing problem using the new model state and
reset the solver using them. We can build `fder` and `fzer` quite easily. We
still need to build `init`, `yc` and `time`.
`init` and `yc` need to use `model.cget` in order to obtain a continuous state
element. However, the usual simulation process first starts by executing a
discrete step to initialize the model state. Does `model.cget` make sense
before this first step has been executed?
#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)
)) <automaton>
Tagged values that indicate whether they are discontinuous w.r.t. the previous
input value do not help us here. Indeed, we could consider that the first
value in the stream is always considered discontinuous, which would force a
reset as we receive an input value. However, the initialization of the solver
still depends on the initialization of the model, which (might) need a
discrete step, and we are back to square one.
Additionally, what stop time do we use for the initial state?
If possible, this would have the advantage of making the reset function only
dependent on the model, since the solver is updated to match the model's
simulation requirements.
```ocaml
val sim : ('p, 'a, 'b, 'y, 'yder, 'zin, 'zout) hnode ->
('y, 'yder, 'zin, 'zout) solver ->
('p, 'a, 'b) sim
```
== Mathematical model
#link("https://zelus.di.ens.fr/cc2015/fullpaper.pdf")[[CC'15]] defines the
simulation loop as follows:
#block(fill: rgb(230, 230, 230), stroke: black, inset: 5pt, [
The simulation loop of a hybrid system is a _synchronous function_ defining
four streams $t(n)$, $l x(n)$, $y(n)$ and $z(n)$ with $n in bb(N)$.
$t(n) in bb(R)$ is the increasing sequence of instants at which the solver
stops. $l x(n)$ is the value at time $t(n)$ of the _continuous state
variables_, that is, of all variables defined by their derivatives in the
original model. $y(n)$ is the value at time $t(n)$ of the _discrete state_.
$z(n)$ indicates any _zero-crossings_ at instant $t(n)$ on signals monitored
by the solver, that is, any signals that become equal to or pass through zero.
The synchronous function has two modes: the discrete mode ($D$) contains all
computations that may change the discrete state or that have side effects. The
continuous mode ($C$) is where ODEs are solved. The two modes alternate
according to the execution scheme summarized in @automaton.
In the _continuous mode_, the solver computes an approximation of the solution
of the ODEs and monitors a set of expressions for zero-crossings. The solver
is abstracted by a function $s o l v e(f)(g)$ parameterized by $f$ and $g$
where:
- $x'(tau)=f(y(n),tau,x(tau))$ defines the derivatives of continuous state
variables $x$ at instant $tau in bb(R)$;
- $u p z(tau)=g(y(n),tau,x(tau))$ defines the current values of a set of
zero-crossing signals $u p z$, indexed by $i in {1, ..., k}$.
The continuous mode $C$ computes four sequences $s$, $l x$, $z$ and $t$ such
that $ (l x, z, t, s)(n+1) = s o l v e(f)(g)(s,y,l x,t,s t e p)(n) $ where
- $s(n)$ is the internal state of the solver at instant $t(n) in bb(R)$.
Calling $s o l v e(f)(g)$ updates the state to $s(n+1)$.
- $x$ is an approximation of a solution of the ODE
$ x'(tau)=f(y(n),tau,x(tau)) $
It is parameterized by the current discrete state $y(n)$ and initialized at
instant $t(n)$ with the value of $l x(n)$, that is, $x(t(n)) = l x(n)$.
- $l x(n+1)$ is bounded by the horizon $t(n) + s t e p(n)$ that the solver has
been asked to reach, that is
$ t(n) <= t(n+1) <= t(n) + s t e p(n) $
- $z(n+1)$ signals any zero-crossings detected at time $t(n+1)$. An event
occurs with a transition to the discrete mode $D$ when horizon
$t(n) + s t e p(n)$ is reached, or when at least one of the zero-crossing
signals $u p z(i)$, for $i in {1, ..., k}$ crosses zero, which is indicated
by a true value for the corresponding boolean output $z(n+1)(i)$. $
e v e n t = z(n+1)(0) or ... or z(n+1)(k) or (t(n+1) = t(n) + s t e p(n)) $
If the solver raises an error (for example, a division by zero or an inability
to find a solution), we consider that the simulation fails.
All discrete changes occur in the _discrete mode_ $D$. It is entered when an
event is raised during integration. During a discrete phase, the function
$n e x t$ defines $y$, $l x$, $s t e p$, $e n c o r e$, $z$ and $t$:
$ (y, l x, s t e p, e n c o r e)(n+1) & = n e x t(y, l x, z, t)(n) \
z(n+1) & = f a l s e \
t(n+1) & = t(n) $
where
- $y(n+1)$ is the new discrete state; outside of mode $D$, $y(n+1) = y(n)$.
- $l x(n+1)$ is the new continuous state, which may be changed directly in the
discrete mode.
- $s t e p(n+1)$ is the new step size.
- $e n c o r e(n+1)$ is true if an additional discrete step must be performed.
Function $n e x t$ can decide to trigger instantaneously another discrete
event, causing an _event cascade_.
- $t(n)$ (the simulation time) is unchanged during discrete phases.
The initial values for $y(0)$, $l x(0)$ and $s(0)$ are given by an
initialization function $i n i t$. Finally, $s o l v e(f)(g)$ may decide to
reset its internal state if the continuous state changes. If
$i n i t \_ s o l v e$ initializes the solver state, we have
$ r e i n i t & = (l x(n+1) != l x(n)) \
s(n+1) & = "if" r e i n i t "then" i n i t \_ s o l v e(l x(n+1),s(n))
"else" s(n) $
Taken together, the definitions from both modes give a synchronous
interpretation of the simulation loop as a stream function that computes the
sequences $l x$, $y$ and $t$ at instant $n+1$ according to their values at
instant $n$ and an internal state. Writing $s o l v e(f)(g)$ abstracts from
the actual choice of integration method and zero-crossing detection algorithm.
A more detailed description of $s o l v e(f)(g)$ would be possible (for
example, an automaton with two states: one that integrates, and one that
detects zero-crossings) but with no influence on the code generation problem
which must be independent of such simulation details.
Given a program written in a high-level language, we must produce the
functions $i n i t$, $f$, $g$, and $n e x t$. In practice, they are
implemented in an imperative language like C. Code generation for hybrid
models has much in common with code generation for synchronous languages.
])
How do we represent infinitesimal steps in floating-point numbers ?
A possible representation of the above could be
#align(center, ```ocaml
f = [{ start = 0.0; length = 1.0; u = fun t -> t };
{ start = 1.0; length = 0.0; u = fun _ -> 2.0 }]
```)
but returning a list is problematic, however, as it cannot be passed easily as
input to the underlying models. Unless the input can itself be considered as a
list, in which case the simulation simply operates over the whole list,
resetting the solver as needed. This has the advantage of not having to wait
several steps, providing nothing, until the solver is done integrating the
input, before we can provide it with another input.

View file

@ -6,19 +6,28 @@ open State
let offset (input : 'a value) (now : time) : time -> 'a =
fun t -> input.u ((now -. input.start) +. t)
let rec compose = function
| [] -> assert false
| [f] -> f
| { start=sl; u=ul; _ } :: l ->
let { start=sr; length=lr; u=ur } = compose l in
let length = sr +. lr -. sl in
{ start=sl; length; u=fun t -> if t <= sr then ur t else ul t }
module LazySim (S : SimState) =
struct
(* TODO: figure out where we initialize the solvers; the initialization
function already supposes an initialized solver state, but could we
parameterize [LazySim] with a solver state module that provides its
own initialization function ? *)
(* TODO: figure out where we initialize the solvers; the initialization and
reset functions already suppose an initialized solver state, but
could we parameterize simulation with a solver state module that
provides its own initialization function ? *)
let sim
(HNode model : ('p, 'a, 'b, 'y, 'yder, 'zin, 'zout) hnode)
(DNode solver : ('y, 'yder, 'zin, 'zout) solver)
: ('p, 'a, 'b) sim
= let s = S.init ~mstate:model.state ~sstate:solver.state in
: ('p, 'a, 'b) lazy_sim
= let state = S.init ~mstate:model.state ~sstate:solver.state in
let step state input =
let mstate = S.mstate state and sstate = S.sstate state
and status = S.status state in
@ -39,8 +48,9 @@ module LazySim (S : SimState) =
let h = model.horizon mstate in
if h <= 0.0 then S.update ~mstate state
else if now >= stop then
(* TODO: equivalent of [s] (initial state of model and
solvers) ? *)
(* TODO: Build an initial state with the initial states for
both the solver and model - an equivalent of [s] in
the original version. *)
raise Common.Utils.TODO
else if model.jump mstate then
let y = model.cget mstate in
@ -75,77 +85,97 @@ module LazySim (S : SimState) =
let mstate = model.zset mstate z in
S.update ~status ~mstate ~sstate state in
Some out, state in
let reset m s =
let mstate = model.reset m (S.mstate s) in
let reset p s =
(* TODO: does [model.cget mstate] make sense before the first discrete
step - can we use [mstate] to reinitialize [sstate] ? *)
let mstate = model.reset p (S.mstate s) in
let y = model.cget mstate in
(* TODO: what initial stop time do we use ? *)
let stop = raise Common.Utils.TODO in
let ivp = { fder = model.fder mstate; stop; init = y } in
let zc = { fzer = model.fzer mstate; yc = y } in
let sstate = solver.reset (ivp, zc) (S.sstate s) in
let status = S.idle (S.status s) in
S.update ~status ~mstate ~sstate s in
DNode { state = s; step; reset }
DNode { state; step; reset }
end
module GreedySim (S : SimState) =
struct
(* TODO: greedy simulation *)
(* TODO: greedy simulation: call the solvers and the subsystems as often as
needed until we reach the horizon. *)
let sim
(HNode model : ('p, 'a, 'b, 'y, 'yder, 'zin, 'zout) hnode)
(DNode solver : ('y, 'yder, 'zin, 'zout) solver)
: ('p, 'a, 'b) sim
: ('p, 'a, 'b) greedy_sim
= let state = S.init ~mstate:model.state ~sstate:solver.state in
let rec step state input =
let status = S.status state and mstate = S.mstate state
and sstate = S.sstate state in
match input, S.is_running state with
| Some input, _ ->
let mode = Discrete and now = 0.0 and stop = input.length in
let status = S.running ~mode ~input ~now ~stop (S.status state) in
let state = S.update ~status state in
step state None
| None, false -> None, state
| None, true ->
let input = S.input state and now = S.now state
and stop = S.stop state in
match S.mode state with
| Discrete ->
let o, mstate = model.step mstate (input.u now) in
let state =
let h = model.horizon mstate in
if h <= 0.0 then S.update ~mstate state
else if now >= stop then raise Common.Utils.TODO
else if model.jump mstate then
let y = model.cget mstate in
let fder t = model.fder mstate (offset input now t) in
let fzer t = model.fzer mstate (offset input now t) in
let ivp = { fder; stop = stop -. now; init = y } in
let zc = { yc = y; fzer } in
let sstate = solver.reset (ivp, zc) sstate in
let status = S.running ~mode:Continuous status in
S.update ~status ~mstate ~sstate state
else
let status = S.running ~mode:Continuous status in
S.update ~status state in
let start = input.start +. now in
Some { start; length = 0.0; u = fun _ -> o }, state
| Continuous ->
let (h, f, z), sstate = solver.step sstate stop in
let mstate = model.cset mstate (f h) in
let h' = input.start +. h in
let fout t =
model.fout mstate (input.u (now +. t)) (f (now +. t)) in
let out =
{ start = input.start +. now; length = h -. now; u = fout } in
match z with
| None ->
let status =
if h >= stop then S.running ~mode:Discrete ~now:h' status
in
if not (S.is_running state) then
let mode = Discrete and now = 0.0 and stop = input.length in
let status = S.running ~mode ~input ~now ~stop (S.status state) in
let state = S.update ~status state in
step state input
else let now = S.now state and stop = S.stop state in
match S.mode state with
| Discrete ->
let o, mstate = model.step mstate (input.u now) in
let h = model.horizon mstate in
let rest, state =
if h <= 0.0 then step (S.update ~mstate state) input
else if now >= stop then [], state
else if model.jump mstate then
let y = model.cget mstate in
let fder t = model.fder mstate (offset input now t) in
let fzer t = model.fzer mstate (offset input now t) in
let ivp = { fder; stop = stop -. now; init = y } in
let zc = { yc = y; fzer } in
let sstate = solver.reset (ivp, zc) sstate in
let status = S.running ~mode:Continuous status in
step (S.update ~status ~mstate ~sstate state) input
else
let status = S.running ~mode:Continuous status in
step (S.update ~status state) input in
let start = input.start +. now in
{ start; length = 0.0; u = fun _ -> o }::rest, state
| Continuous ->
let (h, f, z), _sstate = solver.step sstate stop in
let mstate = model.cset mstate (f h) in
let h' = input.start +. h in
let fout t =
model.fout mstate (input.u (now +. t)) (f (now +. t)) in
let out =
{ start = input.start +. now; length = h -. now; u = fout } in
match z with
| None ->
if h >= stop then
let status = S.running ~mode:Discrete ~now:h' status in
let rest, state =
step (S.update ~status ~mstate ~sstate state) input in
out::rest, state
else
let status = S.running ~now:h' status in
let rest, state =
step (S.update ~status ~mstate ~sstate state) input in
(match rest with
| [] -> [out], state
| f::rest -> compose [out;f] :: rest, state)
| Some z ->
let status = S.running ~mode:Discrete ~now:h' status in
let mstate = model.zset mstate z in
let rest, state =
step (S.update ~status ~mstate ~sstate state) input in
out::rest, state in
let reset = assert false in
DNode { state; step; reset }
end

View file

@ -1,20 +1,6 @@
open Types
(* Model *)
module type ModelState =
sig
type state
end
(* Solvers *)
module type SolverState =
sig
type state
end
(* Simulation *)
(** Step mode: discrete or continuous *)

View file

@ -81,5 +81,10 @@ type ('y, 'yder, 'zin, 'zout) solver =
(** The simulation of a hybrid system is a synchronous function on streams of
functions. *)
type ('p, 'a, 'b) sim =
type ('p, 'a, 'b) lazy_sim =
('p, 'a signal, 'b signal) dnode
(** Greedy simulation takes in an input and computes as many solver and
subsystem steps as needed to reach the input's horizon. *)
type ('p, 'a, 'b) greedy_sim =
('p, 'a value, 'b value list) dnode