An Elixir GenServer module based on the popular Python package cadCAD: "Design, test and validate complex systems through simulation in Python".
This is an exercise in better understanding system modelling, simulation, complex systems, and the design of cadCAD.
It also serves as a learning exercise in the Erlang/Elixir OTP actor model and GenServer based programming constructs! In fact, it is the OTP actor model that originally made me think of the similarities to graph theory and the way state is managed in complex system modelling.
Designing a differential games engine also uses all the best features of the Elixir language: it is functional, expressive, has great data constructs, pattern matching, scalability, it enables creating maintainable code, and has incredible tooling.
Example robots & marbles model:
defmodule Marbles7 do
@behaviour Cadex.Behaviour
@initial_conditions %{
box_A: 10,
box_B: 0
}
@partial_state_update_blocks [
%Cadex.Types.PartialStateUpdateBlock{
policies: [
:robot_1,
:robot_2
],
variables: [
:box_A,
:box_B
]
}
]
@simulation_parameters %Cadex.Types.SimulationParameters{
T: 50,
N: 30
}
@impl true
def config do
%Cadex.Types.State{
sim: %{
simulation_parameters: @simulation_parameters,
partial_state_update_blocks: @partial_state_update_blocks
},
current_state: @initial_conditions
}
end
@robots_probabilities [0.5, 1/3]
def policy_helper(_params, _substep, _previous_states, current_state, capacity \\ 1) do
add_to_A = cond do
current_state[:box_A] > current_state[:box_B] -> -capacity
current_state[:box_A] < current_state[:box_B] -> capacity
true -> 0
end
{:ok, %{add_to_A: add_to_A, add_to_B: -add_to_A}}
end
@impl true
def policy(:robot_1, params, substep, previous_states, current_state) do
robot_ID = 1
cond do
:rand.uniform < @robots_probabilities |> Enum.at(robot_ID - 1) ->
policy_helper(params, substep, previous_states, current_state)
true -> {:ok, %{add_to_A: 0, add_to_B: 0}}
end
end
@impl true
def policy(:robot_2, params, substep, previous_states, current_state) do
robot_ID = 2
cond do
:rand.uniform < @robots_probabilities |> Enum.at(robot_ID - 1) ->
policy_helper(params, substep, previous_states, current_state)
true -> {:ok, %{add_to_A: 0, add_to_B: 0}}
end
end
@impl true
def update(_var = :box_A, _params, _substep, _previous_states, _current_state, input) do
%{add_to_A: add_to_A} = input
increment = &(&1 + add_to_A)
{:ok, increment}
end
@impl true
def update(_var = :box_B, _params, _substep, _previous_states, _current_state, input) do
%{add_to_B: add_to_B} = input
increment = &(&1 + add_to_B)
{:ok, increment}
end
end
Running the simulation:
{:ok, _pid} = Cadex.start(Marbles7)
{:ok, %Cadex.Types.State{} = state} = Cadex.run()
Generate plot via export
:
{:ok, runs} = Cadex.run(debug=false)
box_A_plots = runs |> Enum.map(fn %{run: _run, result: result} ->
result |> Enum.map(fn timestep ->
%{state: state} = timestep |> List.last
%{box_A: box_A} = state |> List.last
box_A
end)
end)
box_B_plots = runs |> Enum.map(fn %{run: _run, result: result} ->
result |> Enum.map(fn timestep ->
%{state: state} = timestep |> List.last
%{box_B: box_B} = state |> List.last
box_B
end)
end)
PythonInterface.plot_marble_runs(box_A_plots, box_B_plots)
State variables can be atoms (:foo
) or tuples ({:foo, :bar}
). This allows us to pattern match, and have multiple state update functions for each variable, for example:
@partial_state_update_blocks [
%Cadex.Types.PartialStateUpdateBlock{
policies: [
:robot_1,
:robot_2
],
variables: [
{:box_A, :function_1},
{:box_B, :function_1}
]
},
%Cadex.Types.PartialStateUpdateBlock{
policies: [
:robot_1,
:robot_2
],
variables: [
{:box_A, :function_2},
{:box_B, :function_2}
]
}
]
The above partial state update blocks would then pattern match to the following state update functions:
def update({:box_A, :function_1}, _params, _substep, _previous_states, _current_state, input) do ...
def update({:box_A, :function_2}, _params, _substep, _previous_states, _current_state, input) do ...
def update({:box_B, :function_1}, _params, _substep, _previous_states, _current_state, input) do ...
def update({:box_B, :function_2}, _params, _substep, _previous_states, _current_state, input) do ...
I've created a Nix shell for development, shell.nix
. This includes all the dependencies you should need, and should work on both Linux and Mac - I've only tested it on Linux.
- Install Nix:
curl -L --proto '=https' --tlsv1.2 https://nixos.org/nix/install | sh
- Enter Nix shell:
nix-shell
The test test/marbles_test.exs
provides a good example of a basic model, the cadCAD robots and marbles model.
To run this test, enter the development environment, and run mix test test/marbles_test.exs
.
You should see output as follows:
[%PartialStateUpdateBlock{policies: [], variables: [:box_A, :box_B]}]
.%State{
current: %{box_A: 11, box_B: 0},
delta: %{
box_A: #Function<2.72390911/1 in Cadex.handle_cast/2>,
box_B: #Function<1.72390911/1 in Cadex.handle_cast/2>
},
previous: %{},
sim: %{
partial_state_update_blocks: [
%PartialStateUpdateBlock{policies: [], variables: [:box_A, :box_B]}
],
simulation_parameters: %SimulationParameters{M: %{}, N: 1, T: 10}
}
}
%State{
current: %{box_A: 10, box_B: 1},
delta: %{
box_A: #Function<2.72390911/1 in Cadex.handle_cast/2>,
box_B: #Function<1.72390911/1 in Cadex.handle_cast/2>
},
previous: %{box_A: 11, box_B: 0},
sim: %{
partial_state_update_blocks: [
%PartialStateUpdateBlock{policies: [], variables: [:box_A, :box_B]}
],
simulation_parameters: %SimulationParameters{M: %{}, N: 1, T: 10}
}
}
%State{
current: %{box_A: 9, box_B: 2},
delta: %{
box_A: #Function<2.72390911/1 in Cadex.handle_cast/2>,
box_B: #Function<1.72390911/1 in Cadex.handle_cast/2>
},
previous: %{box_A: 10, box_B: 1},
sim: %{
partial_state_update_blocks: [
%PartialStateUpdateBlock{policies: [], variables: [:box_A, :box_B]}
],
simulation_parameters: %SimulationParameters{M: %{}, N: 1, T: 10}
}
}
...
Finished in 0.08 seconds
5 tests, 0 failures
Randomized with seed 249053
This package is not available on Hex yet.
If available in Hex, the package can be installed
by adding cadex
to your list of dependencies in mix.exs
:
def deps do
[
{:cadex, "~> 0.1.0"}
]
end
Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/cadex.
- Basic state updates & policy functions (Robots and marbles tutorial 7)
- Monte carlo simulations
- Parameter sweeps
- Keep experimenting
- Introduce concurrency with
flow
- Visualization with Phoenix LiveView and Scenic