Page
Library
Module
Module type
Parameter
Class
Class type
Source
In short, State Dimension Logic provides functionality for storing information about a robot (or any other process with similar logic with time steps) in a single location (ie: a determinstic state machine).
For a small example of it's, see https://github.com/zevbo/RobotState/tree/main/simple_test.
If you have opam installed (installation instructrions for opam: https://opam.ocaml.org/doc/Install.html) you can install all other necessary dependencies with:
opam install dune core ppx_jane ounit2
git clone https://github.com/zevbo/RobotState.git
opam install RobotState
We're all trying to become better software engineers and programers all the time. When we say "better," we're mainly thinking about a few things:
Ultimately, this project is an attempt to provide tooling to dramatically help with 1 and 4. In the spirit of this project, I'd like to encourage some extra thought into your choice of tooling with the goal of optimizing these 4 ideals. In additon, there are a couple of tools in specific I would like to recommend for working with robot state:
I also recommend using the auto-formatter provided by ocaml-platform. To do so, you need a .ocamlformat file at the root of your proejct (I suggest copying the one from this repository), and run the below opam command. Alternatively, you can use the ocaml-format extention on VSCode, but I suggest trying to get the auto-formatter to work through ocaml-platform first.
opam install ocamlformat ocaml-lsp-server
This tutorial will be split up into three stand-alone sections:
Let's turn out attention to a simple example, simulaitng a body moving in a single dimension. It starts stationary, and every tick, its linear velocity increases by 0.1 + a random number between 0.5 and -0.5. It also has a light, that will turn on once the robot passes position 50.
The example can be found at the following link, and we will look through the example bottom up: https://github.com/zevbo/RobotState/tree/main/simple_example. To run it, simply cd into the run_simple_example directory, and run:
dune exec ./run_simple_example.exe
Let's first turn our attention towards the simplest file: sds.ml. Aside from some open
s, it only contains three short let declerations:
let (x : float Sd.t) = Sd.create "x" Float.sexp_of_t
let (v : float Sd.t) = Sd.create "v" Float.sexp_of_t
let (light_on : bool Sd.t) = Sd.create "light on" Bool.sexp_of_t
Here, x, v and light_on are each a unique key (called a state dimension or Sd.t) corresponidng to a value being stored corresponding to our robot simulation. The first value to Sd.create is the name of the state dimension, and the second value is a sexp_of function. This sexp_of function specifies what data is stored with that state dimension. The x position and velocity are both floats, so we use Float.sexp_of_t to initialize those state dimensions. Whether or not the light is on is a boolean, so we pass Bool.sexp_of_t to initalize its Sd.t.
Now let's turn our attention towards update_v.ml and update_x.mls. Update_v.ml defines an Sd_node.t instance that corresponds to the logic for updating the velocity of the robot. Update_x.ml does the same for updating the position. We can see the major portion of the two files are the following:
let logic =
[%map_open.Sd_lang
let v = sd_past Sds.v 1 (V 0.0) in
let diff = 0.1 +. Random.float_range (-0.5) 0.5 in
Rs.set Rs.empty Sds.v (v +. diff)]
let logic =
[%map_open.Sd_lang
let x = sd_past Sds.x 1 (V 0.0)
and v = sd Sds.v in
Rs.set Rs.empty Sds.x (x +. v)]
To create the logic for a node, you write the logic inside the %map_open.Sd_lang
syntax. The logic for a node can always be broken up into three parts:
Let's start with "the decleration of required state dimensions." This section is marked by the first let statement, and in it you can use the following functions to retrieve data other nodes have declared about the robot:
val sd : 'a Sd.t -> 'a
val sd_past : 'a Sd.t -> int -> Sd_lang.default -> 'a
The sd function gives you the value of a state dimension that has been estimated in the current tick. sd_past gives you the value of a state dimension that was estimated some number of ticks ago (0 = this tick, 1 is previous tick). The default value says what value to use in the case where there are fewer than the request number of states recorded so far. The decleration for Sd_lang.default is the following:
type 'a default =
| V of 'a (* in case of too few states, return associated value of type 'a *)
| Last (* in case of too few states, use the oldest state *)
| Safe_last of 'a (* like last, except in case of too few states and only current state exists, use 'a *)
| Unsafe (* in case of too few states, fail *)
To get full safety, it is recommended to try and stick to the Safe_last
and V
cases.
If you need multiple state dimensions, you can use the and
keyword as seen in update_x.ml.
The middle section, the logic, is the simplest: you simply write code just the way you normally would.
In the final section, you need to create and then return a RobotState.t (also aliases as Rs.t). An Rs.t maps 'a Sd.t values to 'a values. The Rs.t you return from the Sd_node.t indicates the new values that those Sd.t values should have for this time step. The following functions and values give you all the functionality you need to create an Rs.t:
val empty : Rs.t (* an empty Rs.t *)
val set : Rs.t -> 'a Sd.t -> 'a -> Rs.t (* returns a new Rs.t with all the bindings of the previous one, as well as the new binding *)
Often, you will want to write logic that does not estiamte anything about the state. In this case, you want to simply return Rs.empty
. For an example of this, we can look at
Finally, to create an Sd_node.t, you also need to create a set declaring what Sd.t values your Sd_node.t returns. For example, in the case of update_v.ml we have:
let sds_estiamting = (Set.of_list (module Sd.Packed) [ Sd.pack Sds.v ])
let node = Sd_node.create logic sds_estiamting
Now, I encourage you to take a look at light_on.ml as well as print_est
declared in main.ml, and attempt to understand them on your own. If you run into trouble, come back and look at this tutorial!
At this point, we've written all of the logic of the program. We simply need to run it. To do this, we are going to use a Seq_model.t
at the end of main.ml:
let model = Seq_model.create [ Update_v.node; Update_x.node; Light_on.node; Print.node ]
let run () = Seq_model.run model ~ticks:(Some 100)
Here, Seq_model.run
will take a number of ticks (None for no limit) and run each sd_node, as defined by the list passed to Seq_model.create
one after the other on each tick.
One of the major features of this package is the safety checks it provides. When you create and then run a Seq_model.t
, it is guaranteed that every state dimension requested by each node will be available. To see this check in action, let's try flipping the Update_x.est
and Update_v.est
.
++ let model = Seq_model.create [ Update_x.node; Update_v.node; Light_on.node; Print.node ]
-- let model = Seq_model.create [ Update_v.node; Update_x.node; Light_on.node; Print.node ]
Notably, if we were to run this, because Update_x.est now comes before Update_v.est, it will not know the current velocity. Thus, it should fail. So, what happens when we run dune exec ./run_simple_example.exe
?
Uncaught exception:
Sd_logic.Seq_model.Premature_sd_req("v")
Raised at Sd_logic__Seq_model.create in file "sd_logic/seq_model.ml", line 104, characters 33-82
Called from Simple_example__Main.model in file "simple_example/main.ml", line 5, characters 12-88
Rather than failing after you run the program, the error is caught by the sequential model when the model is created! The Seq_model.t
will also detect whether or not two nodes are attempting to estimate the same Sd.t, or if a node requires a state dimension that is never estiamted (rather than requiring one before it is estimated).
To see one other kind of safety, let's say we forgot to add the value for light_on
in the light on node:
++ let _x = sd Sds.x in
++ Rs.empty
-- let x = sd Sds.x in
-- Rs.set Rs.empty Sds.light_on Float.(x > 50.0)
When we run the example using dune exec ./run_simple_example.exe
we get:
Uncaught exception:
Sd_logic.Sd_node.Missing_sd("light_on")
Raised at Sd_logic__Sd_node.execute in file "sd_logic/sd_node.ml", line 34, characters 26-69
Called from Sd_logic__Seq_model.apply.(fun) in file "sd_logic/seq_model.ml", line 28, characters 28-81
Called from Stdlib__list.fold_left in file "list.ml", line 121, characters 24-34
Called from Sd_logic__Seq_model.tick in file "sd_logic/seq_model.ml", line 124, characters 48-57
Called from Sd_logic__Seq_model.run.tick in file "sd_logic/seq_model.ml", line 129, characters 12-18
Called from Sd_logic__Seq_model.run in file "sd_logic/seq_model.ml", line 137, characters 18-26
Called from Dune__exe__Run_simple_example in file "run_simple_example/run_simple_example.ml", line 1, characters 9-35
This error is unfortunatley not catchable before we run the program. But, if a node ever forgets to return a binding for state dimension it said it was estiamting (or returns an extra state dimension), the program will still catch it.
And that's it! You're now ready to use this package on whatever robot you choose!
This project has a number of layers. Fully understanding how to use the project requires understanding each individual layer, as well as how they fit together. Before we take a look at each layer individually, we're going to take a step back for a quick overview of each major section.
'a Sd.t
is a unique key meant to represent some data about the robot. That can be anything from the position of the robot, to that status of a button, to some intermediate data for estimating state about the robot. The type they are paramterized over represents the type of the data that is stored with them.Rs.t
is a glorified univ map from 'a Sd.t
values to 'a
values.Rsh.t
will store a number of robot states, each one corresponding to a different time stamp.Rsh.t
and outputs some value of type 'a, just like a function might.Sd_node.t
is made up of a Rs.t Sd_lang.t
and a variable representing the Sd.t
values that are expected to be returned by the Sd_lang.t
.Sd_lang.t
s that runs the logic of the entire program. Currently, the only model we offer is a sequential model (Seq_model.t
), which each tick runs the same sequence of Sd_node.t
s one after the other.A state