24 KiB
Examples of usage of dds
Introduction
This document shows some examples of usage of the modules in dds
with Org-mode. It relies on emacs-ob-racket.
The following section describes how Org-mode can interact with
Racket, and how this interaction can be used for a fluid workflow
with dds
. In particular, the code block munch-sexp
is defined
in this section.
The subsequent sections show off some the functionalities of the
submodules of dds
.
Org-mode, Racket, and dds
<<intro>>
Installing dds
locally
To install dds
locally, you can simply run the following command
in dds
.
raco pkg install
After this installation, you can import dds
modules by simply
doing the following:
#lang racket
(file dds/networks)
(file dds/utils)
Importing a module from file old
To require the modules from the files of dds
, you can use the
following code (I only reset the prelude here because I set at the
top of this file):
#lang racket
(require (file "~/Candies/prj/racket/dds/networks.rkt"))
(require (file "~/Candies/prj/racket/dds/utils.rkt"))
Note that this code will not work with :results value
. I think it
is because in this case the code is not really evaluated at top
level.
These initialisation lines can be put into the prologue of every
code block in a subtree by setting :prologue
via
:header-args:racket:
in the properties drawer. Check out the
properties drawer of this section for an example.
Alternatively, this property can be set via a #+PROPERTY
line at
the top the file. For example, this file has such a line. Whenever
this property line changes, refresh the setup of the file by hitting
C-c C-c
on the property line. This will update the prologue for
all racket code blocks.
Finally, you can also set :prologue
(and other properties with
long values) in the following way:
(make-state '((a . 1)))
'#hash((a . 1))
Output formats for results of evaluation of code blocks
This section of the Org manual describes various different formats
for presenting the results of code blocks. I find the following
three particularly useful as of [2020-02-22 Sat]: output
, list
,
and table
.
The output
result format is the simplest and the most natural
ones. It works as if the code block were inserted into a module
which would then be evaluated.
(println "This is the first line of output.")
(println (+ 1 2))
(println "This the third line of output.")
"This is the first line of output." 3 "This the third line of output."
The list
result format typesets the result of the last line in the
code block as a list:
'(1 "hello" (and x y))
- 1
- "hello"
- (and x y)
Note how nested lists are not recursively shown as nested Org-mode lists.
For some reason, the list
output format does not work with the
result drawer:
'(1 "hello" (and x y))
- (1 "\"hello\"" (and x y))
Finally, the table
result format typesets the output as a table:
'((a . #t) (b . #f))
a | #t |
b | #f |
This is clearly very useful for printing states (and hash tables, more generally):
(make-state '((a . 1) (b . #f) (c . "hello")))
a | 1 |
b | #f |
c | "hello" |
A note about printing update function forms
Automatic table typesetting may go in the way of readability for hash tables whose values are lists, as the following example shows:
#hash((a . (and a b)) (b . (not b)))
a | and | a | b |
b | not | b |
To tackle this issue, dds/utils
provides
stringify-variable-mapping
(with the shortcut sgfy
) which
converts all the values of a given variable mapping to strings:
(stringify-variable-mapping #hash((a . (and a b)) (b . (not b))))
a | "(and a b)" |
b | "(not b)" |
Reading Org-mode tables<<tabread>>
Org-mode allows supplying tables as arguments for code blocks.
a | "(and a b)" |
b | (or b (not a)) |
tab
((a (and a b)) (b (or b (not a))))
Unfortunately, the same trick does not work with Racket directly, because Racket interprets the first elements in the parentheses as function applications:
tab
application: not a procedure; expected a procedure that can be applied to arguments given: "a" arguments…: "(and a b)" context…: "/tmp/babel-qkvrRR/org-babel-c4wuju.rkt": [running body] temp37_0 for-loop run-module-instance!125 perform-require!78
Fortunately, we can easily remedy this problem by creating a named parameterised Elisp source block which will explicitly convert the table to a string:
(prin1 sexp)
(("a" "(and a b)") ("b" "(or b (not a))"))
We can now correctly receive this table in a Racket source code
block by threading it through munch-sexp
:
(println tab)
"((\"a\" \"(and a b)\") (\"b\" \"(or b (not a))\"))"
dds/utils
has several functions for parsing such strings, and
notably read-org-variable-mapping
, with the shortcut unorgv
:
(unorgv tab)
'#hash((a . (and a b)) (b . (or b (not a))))
Of course, we can use munch-sexp
to prepare any other table than
test-table
for use with Racket:
a | (not a) |
b | (and a c) |
c | (and a (not b)) |
(unorgv tab)
'#hash((a . (not a)) (b . (and a c)) (c . (and a (not b))))
Inline graph visualisation with Graphviz
Some functions in dds
build graphs:
(build-interaction-graph (unorgv bf))
#<unweighted-graph>
The graph
library allows building a Graphviz description of the
constructed graph. (Note that you have to install the graph
library by running raco pkg install graph
and require it. The
long property line at the top of this file defining the prologue for
racket source code blocks takes care of requiring graph
.)
(display (graphviz (build-interaction-graph (unorgv bf))))
digraph G { node0 [label="c"]; node1 [label="b"]; node2 [label="a"]; subgraph U { edge [dir=none]; node0 -> node1; node2 -> node2; } subgraph D { node2 -> node0; node2 -> node1; } }
You can have an inline drawing of this graph by calling the previous
code block (igraph
) via a noweb reference in Graphviz/DOT source
block:
<<igraph()>>
Note that the graph
library draws self-loops as undirected edges.
It also draws double-sided edges as undirected edges (e.g., in the
preceding graph, b depends on c and c depends on b).
dds/networks
The dds/networks
is a module for working with different network
models. A network is a set of variables which are updated according
to their corresponding update functions. The variables to be
updated at each step are given by the mode. This model can
generalise Boolean networks, TBANs, multivalued networks, etc.
Boolean networks
Consider the following Boolean network:
a | b |
b | (and (not a) c) |
c | (not c) |
Note that if you define the formula of a as 0, it will set a to 1,
because 0 is not #f. For example, (if 0 1 2)
evaluates to 1, and
not to 2.
Here's the unsigned interaction graph of this network:
(dotit (build-interaction-graph (unorgv simple-bn)))
<<simple-bn-ig()>>
Here's the signed interaction graph of this network:
(dotit (build-boolean-signed-interaction-graph/form (unorgv simple-bn)))
<<simple-bn-sig()>>
For the interaction a → b, note indeed that when c is #f, b is always #f (positive interaction). On the other hand, when c is #t, b becomes (not a) (negative interaction). Therefore, the influence of a on b is neither activating nor inhibiting.
Here is the full state graph of this network under the asynchronous dynamics:
(let* ([bn (network-form->network (unorgv simple-bn))]
[bn-asyn (make-asyn-dynamics bn)])
(dotit (pretty-print-state-graph (build-full-boolean-state-graph bn-asyn))))
<<simple-bn-sg()>>
Alternatively, you may prefer a slighty more compact representation of Boolean values as 0 and 1:
(let* ([bn (network-form->network (unorgv simple-bn))]
[bn-asyn (make-asyn-dynamics bn)])
(dotit (pretty-print-boolean-state-graph (build-full-boolean-state-graph bn-asyn))))
<<simple-bn-sg-bool()>>
Consider the following state (appearing in the upper left corner of the state graph):
a | 0 |
c | 1 |
b | 1 |
These are the states which can be reached from it in at most 2 steps:
(let* ([bn (network-form->network (unorgv simple-bn))]
[bn-asyn (make-asyn-dynamics bn)]
[s0 (booleanize-state (unorgv some-state))])
(dotit (pretty-print-boolean-state-graph (dds-build-n-step-state-graph bn-asyn (set s0) 2))))
<<simple-bn-some-state()>>
Here is the complete state graph with edges annotated with the modality leading to the update.
(let* ([bn (network-form->network (unorgv simple-bn))]
[bn-asyn (make-asyn-dynamics bn)])
(dotit (pretty-print-boolean-state-graph (build-full-boolean-state-graph-annotated bn-asyn))))
<<simple-bn-sg-bool-ann()>>
For some networks, a single transition between two states may be due to different modalities. Consider the following network:
a | (not b) |
b | b |
(let* ([bn (network-form->network (unorgv input-bn))]
[bn-asyn (make-asyn-dynamics bn)])
(dotit (pretty-print-boolean-state-graph (build-full-boolean-state-graph-annotated bn-asyn))))
<<bn2-sgr()>>
Tabulating functions and networks
Here's how you can tabulate a function. The domain of x is {1, 2}, and the domain of y is {0, 2, 4}. The first column in the output corresponds to x, the second to y, and the third corresponds to the value of the function.
(tabulate (λ (x y) (+ x y)) '(1 2) '(0 2 4))
1 | 0 | 1 |
1 | 2 | 3 |
1 | 4 | 5 |
2 | 0 | 2 |
2 | 2 | 4 |
2 | 4 | 6 |
Here's how you tabulate a Boolean function:
(tabulate/boolean (λ (x y) (and x y)))
#f | #f | #f |
#f | #t | #f |
#t | #f | #f |
#t | #t | #t |
Here's how to tabulate the network simple-bn
, defined at the top
of this section:
(tabulate-boolean-network (network-form->network (unorgv in-bn)))
a | b | c | f-a | f-b | f-c |
#f | #f | #f | #f | #f | #t |
#f | #f | #t | #f | #t | #f |
#f | #t | #f | #t | #f | #t |
#f | #t | #t | #t | #t | #f |
#t | #f | #f | #f | #f | #t |
#t | #f | #t | #f | #f | #f |
#t | #t | #f | #t | #f | #t |
#t | #t | #t | #t | #f | #f |
Random functions and networks
To avoid having different results every time a code block in this section is run, every code block seeds the random number generator to 0.
dds/networks
can generate random functions, given a domain for
each of its arguments and for the function itself. Consider the
following domains:
a | (#f #t) |
b | (1 2) |
c | (cold hot) |
Here's a random function taking values in the codomain (4 5 6)
:
(random-seed 0)
(define rnd-func (random-function/state (unorgv simple-domains) '(4 5 6)))
(tabulate-state rnd-func (unorgv simple-domains))
a | b | c | f |
#f | 1 | cold | 4 |
#f | 1 | hot | 5 |
#f | 2 | cold | 4 |
#f | 2 | hot | 4 |
#t | 1 | cold | 5 |
#t | 1 | hot | 6 |
#t | 2 | cold | 4 |
#t | 2 | hot | 5 |
We can build an entire random network over these domains:
(random-seed 0)
(define n (random-network (unorgv simple-domains)))
(tabulate-network n (unorgv simple-domains))
a | b | c | f-a | f-b | f-c |
#f | 1 | cold | #f | 2 | hot |
#f | 1 | hot | #f | 2 | cold |
#f | 2 | cold | #t | 1 | cold |
#f | 2 | hot | #t | 2 | hot |
#t | 1 | cold | #f | 2 | cold |
#t | 1 | hot | #t | 1 | cold |
#t | 2 | cold | #f | 2 | hot |
#t | 2 | hot | #t | 1 | cold |
Let's snapshot this random network and give it a name.
a | b | c | f-a | f-b | f-c |
#f | 1 | cold | #f | 2 | hot |
#f | 1 | hot | #f | 2 | cold |
#f | 2 | cold | #t | 1 | cold |
#f | 2 | hot | #t | 2 | hot |
#t | 1 | cold | #f | 2 | cold |
#t | 1 | hot | #t | 1 | cold |
#t | 2 | cold | #f | 2 | hot |
#t | 2 | hot | #t | 1 | cold |
Here's how we can read back this table as a Boolean network:
(string->any rnd-network)
'(("a" "b" "c" "f-a" "f-b" "f-c") ("#f" 1 "cold" "#f" 2 "hot") ("#f" 1 "hot" "#f" 2 "cold") ("#f" 2 "cold" "#t" 1 "cold") ("#f" 2 "hot" "#t" 2 "hot") ("#t" 1 "cold" "#f" 2 "cold") ("#t" 1 "hot" "#t" 1 "cold") ("#t" 2 "cold" "#f" 2 "hot") ("#t" 2 "hot" "#t" 1 "cold"))
You can use table->network
to convert a table such as rnd-network
to a network.
(table->network (unorg rnd-network))
'#hash((a . #<procedure:…dds/networks.rkt:518:4>) (b . #<procedure:…dds/networks.rkt:518:4>) (c . #<procedure:…dds/networks.rkt:518:4>))
Here's the state graph of rnd-network.
(define n (table->network (unorg rnd-network)))
(define rnd-asyn (make-asyn-dynamics n))
(define states (list->set (build-all-states (unorgv simple-domains))))
(dotit (pretty-print-state-graph (dds-build-state-graph-annotated rnd-asyn states)))
<<rnd-network-sg()>>
Here's the signed interaction graph of rnd-network.
(define n (table->network (unorg rnd-network)))
(dotit (build-signed-interaction-graph n (unorgv simple-domains)))
<<rnd-network-ig()>>
Note that build-signed-interaction-graph
only includes the + and
the - arcs in the graph, as it does not have access to the symbolic
description of the function.
Reaction systems
Consider the following reaction system:
a | x t | y | z |
b | x | q | z |
Here is how we read this reaction into Racket code:
(read-org-rs input-rs)
(hash 'a (reaction (set 'x 't) (set 'y) (set 'z)) 'b (reaction (set 'x) (set 'q) (set 'z)))
Here is how we can put it back into an Org-mode table:
(rs->ht-str-triples (read-org-rs input-rs))
a | "t x" | "y" | "z" |
b | "x" | "q" | "z" |
Here is how we can apply this reaction system to a state:
(let ([rs (read-org-rs input-rs)])
(apply-rs rs (set 'x 't)))
(set 'z)
Let's see which reactions got applied:
(let ([rs (read-org-rs input-rs)])
(list-enabled rs (set 'x 't)))
- a
- b
You can also give a name to a list and read it with munch-sexp
:
- x y
- z
- t
(read-context-sequence input-ctx)
(list (set 'x 'y) (set 'z) (set) (set 't))
Let's see what the evolution of rs1
looks like with the context
sequence ctx1
.
(dotit (pretty-print-state-graph (build-interactive-process-graph (read-org-rs input-rs) (read-context-sequence input-ctx))))
digraph G { node0 [label="C:{z}{}{t}\nD:{z}"]; node1 [label="C:{}{t}\nD:{}"]; node2 [label="C:{x y}{z}{}{t}\nD:{}"]; node3 [label="C:{t}\nD:{}"]; node4 [label="C:\nD:{}"]; subgraph U { edge [dir=none]; node4 -> node4 [label="{}"]; } subgraph D { node0 -> node1 [label="{}"]; node1 -> node3 [label="{}"]; node2 -> node0 [label="{b}"]; node3 -> node4 [label="{}"]; } }
<<rs1-sgr()>>
Note that we need to keep the full context sequence in the name of each state to avoid merging states with the same result and contexts, but which occur at different steps of the evolution.
The graphical presentation for interactive processes is arguably less readable than just listing the contexts and the results explicitly. Here is how you can do it.
(build-interactive-process (read-org-rs input-rs) (read-context-sequence input-ctx))
(y x) | nil |
(z) | (z) |
nil | nil |
(t) | nil |
nil | nil |
The first column of this table shows the current context. The second column shows the result of application of the reactions to the previous state. The interactive process contains one more step with respect to the context sequence. This is to show the effect of the last context.
Note that empty sets are printed as nil
.