# DiffEqBiological Tutorial II: Network Properties API
### Samuel Isaacson

The [DiffEqBiological
API](http://docs.juliadiffeq.org/latest/apis/diffeqbio.html) provides a
collection of functions for easily accessing network properties, and for
incrementally building and extending a network. In this tutorial we'll go
through the API, and then illustrate how to programmatically construct a
network.

We'll illustrate the API using a toggle-switch like network that contains a
variety of different reaction types:

```julia
using DifferentialEquations, DiffEqBiological, Latexify, Plots
fmt = :svg
pyplot(fmt=fmt)
rn = @reaction_network begin
    hillr(D₂,α,K,n), ∅ --> m₁
    hillr(D₁,α,K,n), ∅ --> m₂
    (δ,γ), m₁ ↔ ∅
    (δ,γ), m₂ ↔ ∅
    β, m₁ --> m₁ + P₁
    β, m₂ --> m₂ + P₂
    μ, P₁ --> ∅
    μ, P₂ --> ∅
    (k₊,k₋), 2P₁ ↔ D₁
    (k₊,k₋), 2P₂ ↔ D₂
    (k₊,k₋), P₁+P₂ ↔ T
end α K n δ γ β μ k₊ k₋;
```

This corresponds to the chemical reaction network given by

```julia
latexify(rn; env=:chemical)
```

\begin{align}
\require{mhchem}
\ce{ \varnothing &->[\frac{\alpha \cdot K^{n}}{K^{n} + D_2^{n}}] m_{1}}\\
\ce{ \varnothing &->[\frac{\alpha \cdot K^{n}}{K^{n} + D_1^{n}}] m_{2}}\\
\ce{ m_{1} &<=>[\delta][\gamma] \varnothing}\\
\ce{ m_{2} &<=>[\delta][\gamma] \varnothing}\\
\ce{ m_{1} &->[\beta] m_{1} + P_{1}}\\
\ce{ m_{2} &->[\beta] m_{2} + P_{2}}\\
\ce{ P_{1} &->[\mu] \varnothing}\\
\ce{ P_{2} &->[\mu] \varnothing}\\
\ce{ 2 \cdot P_1 &<=>[k_{+}][k_{-}] D_{1}}\\
\ce{ 2 \cdot P_2 &<=>[k_{+}][k_{-}] D_{2}}\\
\ce{ P_{1} + P_{2} &<=>[k_{+}][k_{-}] T}
\end{align}

---
## Network Properties
[Basic
properties](http://docs.juliadiffeq.org/latest/apis/diffeqbio.html#Basic-properties-1)
of the generated network include the `speciesmap` and `paramsmap` functions we
examined in the last tutorial, along with the corresponding `species` and
`params` functions:

```julia
species(rn)
```

    7-element Array{Symbol,1}:
     :m₁
     :m₂
     :P₁
     :P₂
     :D₁
     :D₂
     :T

```julia
params(rn)
```

    9-element Array{Symbol,1}:
     :α
     :K
     :n
     :δ
     :γ
     :β
     :μ
     :k₊
     :k₋

The numbers of species, parameters and reactions can be accessed using
`numspecies(rn)`, `numparams(rn)` and `numreactions(rn)`.

A number of functions are available to access [properties of
reactions](http://docs.juliadiffeq.org/latest/apis/diffeqbio.html#Reaction-Properties-1)
within the generated network, including `substrates`, `products`, `dependents`,
`ismassaction`, `substratestoich`, `productstoich`, and `netstoich`. Each of
these functions takes two arguments, the reaction network `rn` and the index of
the reaction to query information about. For example, to find the substrate
symbols and their corresponding stoichiometries for the 11th reaction, `2P₁ -->D₁`,
we would use

```julia
substrates(rn, 11)
```

    1-element Array{DiffEqBiological.ReactantStruct,1}:
     DiffEqBiological.ReactantStruct(:P₁, 2)

Broadcasting works on all these functions, allowing the construction of a vector
holding the queried information across all reactions, i.e.

```julia
substrates.(rn, 1:numreactions(rn))
```

    16-element Array{Array{DiffEqBiological.ReactantStruct,1},1}:
     []
     []
     [ReactantStruct(:m₁, 1)]
     []
     [ReactantStruct(:m₂, 1)]
     []
     [ReactantStruct(:m₁, 1)]
     [ReactantStruct(:m₂, 1)]
     [ReactantStruct(:P₁, 1)]
     [ReactantStruct(:P₂, 1)]
     [ReactantStruct(:P₁, 2)]
     [ReactantStruct(:D₁, 1)]
     [ReactantStruct(:P₂, 2)]
     [ReactantStruct(:D₂, 1)]
     [ReactantStruct(:P₁, 1), ReactantStruct(:P₂, 1)]
     [ReactantStruct(:T, 1)]

To see the net stoichiometries for all reactions we would use

```julia
netstoich.(rn, 1:numreactions(rn))
```

    16-element Array{Array{Pair{Int64,Int64},1},1}:
     [1=>1]
     [2=>1]
     [1=>-1]
     [1=>1]
     [2=>-1]
     [2=>1]
     [3=>1]
     [4=>1]
     [3=>-1]
     [4=>-1]
     [3=>-2, 5=>1]
     [3=>2, 5=>-1]
     [4=>-2, 6=>1]
     [4=>2, 6=>-1]
     [3=>-1, 4=>-1, 7=>1]
     [3=>1, 4=>1, 7=>-1]

Here the first integer in each pair corresponds to the index of the species
(with symbol `species(rn)[index]`). The second integer corresponds to the net
stoichiometric coefficient of the species within the reaction. `substratestoich`
and `productstoich` are defined similarly.

Several functions are also provided that calculate different types of
[dependency
graphs](http://docs.juliadiffeq.org/latest/apis/diffeqbio.html#Dependency-Graphs-1).
These include `rxtospecies_depgraph`, which provides a mapping from reaction
index to the indices of species whose population changes when the reaction
occurs:

```julia
rxtospecies_depgraph(rn)
```

    16-element Array{Array{Int64,1},1}:
     [1]
     [2]
     [1]
     [1]
     [2]
     [2]
     [3]
     [4]
     [3]
     [4]
     [3, 5]
     [3, 5]
     [4, 6]
     [4, 6]
     [3, 4, 7]
     [3, 4, 7]

Here the last row indicates that the species with indices `[3,4,7]` will change
values when the reaction `T --> P₁ + P₂` occurs. To confirm these are the
correct species we can look at

```julia
species(rn)[[3,4,7]]
```

    3-element Array{Symbol,1}:
     :P₁
     :P₂
     :T

The `speciestorx_depgraph` similarly provides a mapping from species to reactions
for which their *rate laws* depend on that species. These correspond to all reactions
for which the given species is in the `dependent` set of the reaction. We can verify this
for the first species, `m₁`:

```julia
speciestorx_depgraph(rn)[1]
```

    2-element Array{Int64,1}:
     3
     7

```julia
findall(depset -> in(:m₁, depset), dependents.(rn, 1:numreactions(rn)))
```

    2-element Array{Int64,1}:
     3
     7

Finally, `rxtorx_depgraph` provides a mapping that shows when a given reaction
occurs, which other reactions have rate laws that involve species whose value
would have changed:

```julia
rxtorx_depgraph(rn)
```

    16-element Array{Array{Int64,1},1}:
     [1, 3, 7]
     [2, 5, 8]
     [3, 7]
     [3, 4, 7]
     [5, 8]
     [5, 6, 8]
     [7, 9, 11, 15]
     [8, 10, 13, 15]
     [9, 11, 15]
     [10, 13, 15]
     [2, 9, 11, 12, 15]
     [2, 9, 11, 12, 15]
     [1, 10, 13, 14, 15]
     [1, 10, 13, 14, 15]
     [9, 10, 11, 13, 15, 16]
     [9, 10, 11, 13, 15, 16]

#### Note on Using Network Property API Functions
The basic network query and reaction property functions are simply accessors,
returning information that is already stored within the generated
`reaction_network`. In particular, modifying the returned data structures may
lead to inconsistent internal state within the network. As such, they should be
used for accessing, but not modifying, network properties. In contrast, the
dependency graph functions *generate* a new dependency graph each time they are
called, assuming the user will take ownership of the returned graph.

---
## Incremental Construction of Networks
The `@reaction_network` macro is monolithic, in that it not only constructs and
stores basic network properties such as the reaction stoichiometries, but also
generates **everything** needed to immediately solve ODE, SDE and jump models
using the network. This includes Jacobian functions, noise functions, and jump
functions for each reaction. While this allows for a compact interface to the
DifferentialEquations.jl solvers, it can also be computationally expensive for
large networks, where a user may only wish to solve one type of problem and/or
have fine-grained control over what is generated. In addition, some types of
reaction network structures are more amenable to being constructed
programmatically, as opposed to writing out all reactions by hand within one
macro. For these reasons DiffEqBiological provides two additional macros that
only *initially* setup basic reaction network properties, and which can be
extended through a programmatic interface: `@min_reaction_network` and
`@empty_reaction_network`. We now give an introduction to constructing these
more minimal network representations, and how they can be programmatically
extended. See also the relevant [API
section](http://docs.juliadiffeq.org/latest/apis/diffeqbio.html#Reaction-Network-Generation-Macros-1).

The `@min_reaction_network` macro works identically to the `@reaction_network`
macro, but the generated network will only be complete with respect to its
representation of chemical network properties (i.e. species, parameters and
reactions). No ODE, SDE or jump models are generated during the macro call. It
can subsequently be extended with the addition of new species, parameters or
reactions. The `@empty_reaction_network` allocates an empty network structure
that can also be extended using the programmatic interface. For example, consider
a partial version of the toggle-switch like network we defined above:

```julia
rnmin = @min_reaction_network begin
    (δ,γ), m₁ ↔ ∅
    (δ,γ), m₂ ↔ ∅
    β, m₁ --> m₁ + P₁
    β, m₂ --> m₂ + P₂
    μ, P₁ --> ∅
    μ, P₂ --> ∅
end δ γ β μ;
```

Here we have left out the first two, and last three, reactions from the original
`reaction_network`. To expand the network until it is functionally equivalent to
the original model we add back in the missing species, parameters, and *finally*
the missing reactions. Note, it is required that species and parameters be
defined before any reactions using them are added. The necessary network
extension functions are given by `addspecies!`, `addparam!` and `addreaction!`,
and described in the
[API](http://docs.juliadiffeq.org/latest/apis/diffeqbio.html#Functions-to-Add-Species,-Parameters-and-Reactions-to-a-Network-1). To complete `rnmin` we first add the relevant
species:

```julia
addspecies!(rnmin, :D₁)
addspecies!(rnmin, :D₂)
addspecies!(rnmin, :T)
```

Next we add the needed parameters

```julia
addparam!(rnmin, :α)
addparam!(rnmin, :K)
addparam!(rnmin, :n)
addparam!(rnmin, :k₊)
addparam!(rnmin, :k₋)
```

Note, both `addspecies!` and `addparam!` also accept strings encoding the
variable names (which are then converted to `Symbol`s internally).

We are now ready to add the missing reactions. The API provides two forms of the
`addreaction!` function, one takes expressions analogous to what one would write
in the macro:

```julia
addreaction!(rnmin, :(hillr(D₁,α,K,n)), :(∅ --> m₂))
addreaction!(rnmin, :((k₊,k₋)), :(2P₂ ↔ D₂))
addreaction!(rnmin, :k₊, :(2P₁ --> D₁))
addreaction!(rnmin, :k₋, :(D₁ --> 2P₁))
```

The rate can be an expression or symbol as above, but can also just be a
numeric value. The second form of `addreaction!` takes tuples of
`Pair{Symbol,Int}` that encode the stoichiometric coefficients of substrates and
reactants:

```julia
# signature is addreaction!(rnmin, paramexpr, substratestoich, productstoich)
addreaction!(rnmin, :(hillr(D₂,α,K,n)), (), (:m₁ => 1,))
addreaction!(rnmin, :k₊, (:P₁=>1, :P₂=>1), (:T=>1,))
addreaction!(rnmin, :k₋, (:T=>1,), (:P₁=>1, :P₂=>1))
```

Let's check that `rn` and `rnmin` have the same set of species:

```julia
setdiff(species(rn), species(rnmin))
```

    0-element Array{Symbol,1}

the same set of params:

```julia
setdiff(params(rn), params(rnmin))
```

    0-element Array{Symbol,1}

and the final reaction has the same substrates, reactions, and rate expression:

```julia
rxidx = numreactions(rn)
setdiff(substrates(rn, rxidx), substrates(rnmin, rxidx))
```

    0-element Array{DiffEqBiological.ReactantStruct,1}

```julia
setdiff(products(rn, rxidx), products(rnmin, rxidx))
```

    0-element Array{DiffEqBiological.ReactantStruct,1}

```julia
rateexpr(rn, rxidx) == rateexpr(rnmin, rxidx)
```

    true

---
## Extending Incrementally Generated Networks to Include ODEs, SDEs or Jumps
Once a network generated from `@min_reaction_network` or
`@empty_reaction_network` has had all the associated species, parameters and
reactions filled in, corresponding ODE, SDE or jump models can be constructed.
The relevant API functions are `addodes!`, `addsdes!` and `addjumps!`. One
benefit to contructing models with these functions is that they offer more
fine-grained control over what actually gets constructed. For example,
`addodes!` has the optional keyword argument, `build_jac`, which if set to
`false` will disable construction of symbolic Jacobians and functions for
evaluating Jacobians. For large networks this can give a significant speed-up in
the time required for constructing an ODE model. Each function and its
associated keyword arguments are described in the API section, [Functions to add
ODEs, SDEs or Jumps to a
Network](http://docs.juliadiffeq.org/latest/apis/diffeqbio.html#Functions-to-Add-ODEs,-SDEs-or-Jumps-to-a-Network-1).

Let's extend `rnmin` to include the needed functions for use in ODE
solvers:

```julia
addodes!(rnmin)
```

The [Generated Functions for
Models](http://docs.juliadiffeq.org/latest/apis/diffeqbio.html#Generated-Functions-for-Models-1)
section of the API shows what functions have been generated. For ODEs these
include `oderhsfun(rnmin)`, which returns a function of the form `f(du,u,p,t)`
which evaluates the ODEs (i.e. the time derivatives of `u`) within `du`. For
each generated function, the corresponding expressions from which it was
generated can be retrieved using accessors from the [Generated
Expressions](http://docs.juliadiffeq.org/latest/apis/diffeqbio.html#Generated-Expressions-1)
section of the API. The equations within `du` can be retrieved using the
`odeexprs(rnmin)` function. For example:

```julia
odeexprs(rnmin)
```

    7-element Array{Union{Float64, Int64, Expr, Symbol},1}:
     :((-(δ * m₁) + γ) + (α * K ^ n) / (K ^ n + D₂ ^ n))
     :((-(δ * m₂) + γ) + (α * K ^ n) / (K ^ n + D₁ ^ n))
     :(((((β * m₁ - μ * P₁) + -2 * k₊ * (P₁ ^ 2 / 2)) + 2 * k₋ * D₁) - k₊ * P₁ * P₂) + k₋ * T)
     :(((((β * m₂ - μ * P₂) + -2 * k₊ * (P₂ ^ 2 / 2)) + 2 * k₋ * D₂) - k₊ * P₁ * P₂) + k₋ * T)
     :(k₊ * (P₁ ^ 2 / 2) - k₋ * D₁)
     :(k₊ * (P₂ ^ 2 / 2) - k₋ * D₂)
     :(k₊ * P₁ * P₂ - k₋ * T)

Using Latexify we can see the ODEs themselves to compare with these expressions:

```julia
latexify(rnmin)
```

\begin{align}
\frac{dm_1}{dt} =&  - \delta \cdot m_1 + \gamma + \frac{\alpha \cdot K^{n}}{K^{n} + D_2^{n}} \\
\frac{dm_2}{dt} =&  - \delta \cdot m_2 + \gamma + \frac{\alpha \cdot K^{n}}{K^{n} + D_1^{n}} \\
\frac{dP_1}{dt} =& \beta \cdot m_1 - \mu \cdot P_1 -2 \cdot k_+ \cdot \frac{P_1^{2}}{2} + 2 \cdot k_- \cdot D_1 - k_+ \cdot P_1 \cdot P_2 + k_- \cdot T \\
\frac{dP_2}{dt} =& \beta \cdot m_2 - \mu \cdot P_2 -2 \cdot k_+ \cdot \frac{P_2^{2}}{2} + 2 \cdot k_- \cdot D_2 - k_+ \cdot P_1 \cdot P_2 + k_- \cdot T \\
\frac{dD_1}{dt} =& k_+ \cdot \frac{P_1^{2}}{2} - k_- \cdot D_1 \\
\frac{dD_2}{dt} =& k_+ \cdot \frac{P_2^{2}}{2} - k_- \cdot D_2 \\
\frac{dT}{dt} =& k_+ \cdot P_1 \cdot P_2 - k_- \cdot T \\
\end{align}

For ODEs two other functions are generated by `addodes!`. `jacfun(rnmin)` will
return the generated Jacobian evaluation function, `fjac(dJ,u,p,t)`, which given
the current solution `u` evaluates the Jacobian within `dJ`.
`jacobianexprs(rnmin)` gives the corresponding matrix of expressions, which can
be used with Latexify to see the Jacobian:

```julia
latexify(jacobianexprs(rnmin))
```

\begin{equation}
\left[
\begin{array}{ccccccc}
 - \delta & 0 & 0 & 0 & 0 & \frac{ - K^{n} \cdot n \cdot \alpha \cdot D_2^{-1 + n}}{\left( K^{n} + D_2^{n} \right)^{2}} & 0 \\
0 &  - \delta & 0 & 0 & \frac{ - K^{n} \cdot n \cdot \alpha \cdot D_1^{-1 + n}}{\left( K^{n} + D_1^{n} \right)^{2}} & 0 & 0 \\
\beta & 0 &  - \mu - 2 \cdot k_+ \cdot P_1 - k_+ \cdot P_2 &  - k_+ \cdot P_1 & 2 \cdot k_- & 0 & k_{-} \\
0 & \beta &  - k_+ \cdot P_2 &  - \mu - k_+ \cdot P_1 - 2 \cdot k_+ \cdot P_2 & 0 & 2 \cdot k_- & k_{-} \\
0 & 0 & k_+ \cdot P_1 & 0 &  - k_- & 0 & 0 \\
0 & 0 & 0 & k_+ \cdot P_2 & 0 &  - k_- & 0 \\
0 & 0 & k_+ \cdot P_2 & k_+ \cdot P_1 & 0 & 0 &  - k_- \\
\end{array}
\right]
\end{equation}

`addodes!` also generates a function that evaluates the Jacobian of the ODE
derivative functions with respect to the parameters. `paramjacfun(rnmin)` then
returns the generated function. It has the form `fpjac(dPJ,u,p,t)`, which
given the current solution `u` evaluates the Jacobian matrix with respect to
parameters `p` within `dPJ`. For use in DifferentialEquations.jl solvers, an
[`ODEFunction`](http://docs.juliadiffeq.org/latest/features/performance_overloads.html)
representation of the ODEs is available from `odefun(rnmin)`.

`addsdes!` and `addjumps!` work similarly to complete the network for use in
StochasticDiffEq and DiffEqJump solvers.

#### Note on Using Generated Function and Expression API Functions
The generated functions and expressions accessible through the API require first
calling the appropriate `addodes!`, `addsdes` or `addjumps` function. These are
responsible for actually constructing the underlying functions and expressions.
The API accessors simply return already constructed functions and expressions
that are stored within the `reaction_network` structure.

---
## Example of Generating a Network Programmatically
For a user directly typing in a reaction network, it is generally easier to use
the `@min_reaction_network` or `@reaction_network` macros to fully specify
reactions. However, for large, structured networks it can be much easier to
generate the network programmatically. For very large networks, with tens of
thousands of reactions, the form of `addreaction!` that uses stoichiometric
coefficients should be preferred as it offers substantially better performance.
To put together everything we've seen, let's generate the network corresponding
to a 1D continuous time random walk, approximating the diffusion of molecules
within an interval.

The basic "reaction" network we wish to study is

$$
u_1 \leftrightarrows u_2 \leftrightarrows u_3 \cdots \leftrightarrows u_{N}
$$

for $N$ lattice sites on $[0,1]$. For $h = 1/N$ the lattice spacing, we'll
assume the rate molecules hop from their current site to any particular neighbor
is just $h^{-2}$. We can interpret this hopping process as a collection of
$2N-2$ "reactions", with the form $u_i \to u_j$ for $j=i+1$ or $j=i-1$. We construct
the corresponding reaction network as follows. First we set values for the basic
parameters:

```julia
N = 64
h = 1 / N
```

    0.015625

then we create an empty network, and add each species

```julia
rn = @empty_reaction_network

for i = 1:N
    addspecies!(rn, Symbol(:u, i))
end
```

We next add one parameter `β`, which we will set equal to the hopping rate
of molecules, $h^{-2}$:

```julia
addparam!(rn, :β)
```

Finally, we add in the $2N-2$ possible hopping reactions:

```julia
for i = 1:N
    (i < N) && addreaction!(rn, :β, (Symbol(:u,i)=>1,), (Symbol(:u,i+1)=>1,))
    (i > 1) && addreaction!(rn, :β, (Symbol(:u,i)=>1,), (Symbol(:u,i-1)=>1,))
end
```

Let's first construct an ODE model for the network

```julia
addodes!(rn)
```

We now need to specify the initial condition, parameter vector and time interval
to solve on. We start with 10000 molecules placed at the center of the domain,
and setup an `ODEProblem` to solve:

```julia
u₀ = zeros(N)
u₀[div(N,2)] = 10000
p = [1/(h*h)]
tspan = (0.,.01)
oprob = ODEProblem(rn, u₀, tspan, p)
```

    ODEProblem with uType Array{Float64,1} and tType Float64. In-place: true
    timespan: (0.0, 0.01)
    u0: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]

We are now ready to solve the problem and plot the solution. Since we have
essentially generated a method of lines discretization of the diffusion equation
with a discontinuous initial condition, we'll use an A-L stable implicit ODE
solver, `KenCarp4`, and plot the solution at a few times:

```julia
sol = solve(oprob, KenCarp4())
times = [0., .0001, .001, .01]
plt = plot()
for time in times
    plot!(plt, 1:N, sol(time), fmt=fmt, xlabel="i", ylabel="uᵢ", label=string("t = ", time), lw=3)
end
plot(plt, ylims=(0.,10000.))
```

Here we see the characteristic diffusion of molecules from the center of the
domain, resulting in a shortening and widening of the solution as $t$ increases.

Let's now look at a stochastic chemical kinetics jump process version of the
model, where β gives the probability per time each molecule can hop from its
current lattice site to an individual neighboring site. We first add in the
jumps, disabling `regular_jumps` since they are not needed, and using the
`minimal_jumps` flag to construct a minimal representation of the needed jumps.
We then construct a `JumpProblem`, and use the Composition-Rejection Direct
method, `DirectCR`, to simulate the process of the molecules hopping about on
the lattice:

```julia
addjumps!(rn, build_regular_jumps=false, minimal_jumps=true)

# make the initial condition integer valued
u₀ = zeros(Int, N)
u₀[div(N,2)] = 10000

# setup and solve the problem
dprob = DiscreteProblem(rn, u₀, tspan, p)
jprob = JumpProblem(dprob, DirectCR(), rn, save_positions=(false,false))
jsol = solve(jprob, SSAStepper(), saveat=times)
```

    retcode: Default
    Interpolation: Piecewise constant interpolation
    t: 4-element Array{Float64,1}:
     0.0
     0.0001
     0.001
     0.01
    u: 4-element Array{Array{Int64,1},1}:
     [0, 0, 0, 0, 0, 0, 0, 0, 0, 0  …  0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
     [0, 0, 0, 0, 0, 0, 0, 0, 0, 0  …  0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
     [0, 0, 0, 0, 0, 0, 0, 0, 0, 0  …  0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
     [1, 3, 2, 4, 5, 11, 11, 18, 12, 23  …  22, 18, 11, 4, 6, 1, 3, 0, 2, 0]

We can now plot bar graphs showing the locations of the molecules at the same
set of times we examined the ODE solution. For comparison, we also plot the
corresponding ODE solutions (red lines) that we found:

```julia
times = [0., .0001, .001, .01]
plts = []
for i = 1:4
    b = bar(1:N, jsol[i], legend=false, fmt=fmt, xlabel="i", ylabel="uᵢ", title=string("t = ", times[i]))
    plot!(b,sol(times[i]))
    push!(plts,b)
end
plot(plts...)
```

Similar to the ODE solutions, we see that the molecules spread out and become
more and more well-mixed throughout the domain as $t$ increases. The simulation
results are noisy due to the finite numbers of molecules present in the
stochsatic simulation, but since the number of molecules is large they agree
well with the ODE solution at each time.

---
## Getting Help
Have a question related to DiffEqBiological or this tutorial? Feel free to ask
in the DifferentialEquations.jl [Gitter](https://gitter.im/JuliaDiffEq/Lobby).
If you think you've found a bug in DiffEqBiological, or would like to
request/discuss new functionality, feel free to open an issue on
[Github](https://github.com/JuliaDiffEq/DiffEqBiological.jl) (but please check
there is no related issue already open). If you've found a bug in this tutorial,
or have a suggestion, feel free to open an issue on the [DiffEqTutorials Github
site](https://github.com/JuliaDiffEq/DiffEqTutorials.jl). Or, submit a pull
request to DiffEqTutorials updating the tutorial!

---


```python
import grouptesting
from grouptesting.model import *
from grouptesting.algorithms import *
import autograd.numpy as np
from autograd import grad
import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns
import math
from scipy.stats import bernoulli
from scipy.optimize import minimize, rosen, rosen_der
from sympy import *

def nCr(n,k):
    return math.factorial(n) // math.factorial(k) // math.factorial(n - k)

np.random.seed(4000)

```

## Plot ideas:

1. NCOMP vs. NDD for some q, or for three different qs (just to show empirical performance of NDD vs NCOMP) x
2. NCOMP with an achievability bound for some q x
4. NDD with achievability found for some q x

### 2. NCOMP with an achievability bound for some q

```python
# Define initial parameters
num_tests = 100
n = 1000 # population size
theta = 0.5
C = 2
n_theta = C * (n**theta)
k = round(n_theta) # number of infected
alpha = 0.5
p = alpha/k # Bernoulli test design probability parameter
eta = 0.001
q_results = []

############ RUN EXPERIMENT #####################
for q in [0.10]:
    NCOMP_ber_acc = []

    ############## Achievable bound 1 for T ################
    ## Choose delta according to z
    T_array = np.linspace(1, n, 25)
    z = ((1- alpha/(k) * (1-q))**(k-1) + (1/q)*(1 - alpha/(k) * (1-q))**k) / 2
    l = (1 - alpha / k * (1-q))**(k-1)
    u = (1 - alpha / k * (1-q))**(k) * (1/q)
    print("l-1, u-1:", l-1, u-1)
    delta = z - 1
    assert (l-1) < delta < (u-1)

    ########### Achievability Bound 2 Method 2 #######
    eta_1, alpha_1, theta_1, q_1 = eta, alpha, theta, q
    n_1 = n
    n_theta_1 = n_theta
    d_1 = round(n_theta_1)
    eps = 1e-12

    # Objective
    Tminus = lambda delta_1: ((1 + eta_1) * theta_1 * (1/(q_1**2)) * (d_1) * (np.log(n_1))) / (alpha_1 * (1 - np.exp(-2)) * (eps + (1+delta_1) - (1- alpha_1/(d_1) * (1-q_1))**(d_1-1))**2)
    Tplus = lambda delta_1: ((1 + eta_1) * (1/(q_1**2)) * (d_1) * (np.log(n_1))) / (alpha_1 * (1 - np.exp(-2)) * (eps + (1+delta_1) - (1/q_1) * (1- alpha_1/(d_1) * (1-q_1))**d_1)**2)
    ff = lambda delta_1: max(Tminus(delta_1), Tplus(delta_1))

    # Constraints
    l = (1 - alpha_1 / d_1 * (1-q_1))**(d_1-1)
    u = (1 - alpha_1 / d_1 * (1-q_1))**(d_1) * (1/q_1)
    bound = (l-1, u-1)

    x0 = delta
    options={'disp': None, 'maxcor': 100, 'ftol': 1e-14, 'gtol': 1e-012, 'eps': 1e-08, 'maxfun': 15000, 'maxiter': 15000, 'iprint': 1, 'maxls': 20}
    gff = grad(ff)
    res = minimize(ff, x0, tol=1e-12, bounds=[bound], jac=gff)
    achiev_bound = res.fun[0]

    ###### Main Experiment loop #####

    for T in T_array:
        NCOMP_ber_error = []
        NDD_ber_error = []
        COMP_ber_error = []
        print("T: ", int(round(T)))
        for test in range(num_tests):
            sigma = D(n, k) # Generate the vector of defectives
            X_ber = Ber(n, int(round(T)), p)
            y_ber = Y(dilution_noise(X_ber, q), sigma)

            # NCOMP - Bernoulli
            sigma_hat_ber = NCOMP(X_ber, y_ber, q=q, delta=delta)
            err = error(sigma, sigma_hat_ber)
            NCOMP_ber_error.append(error(sigma, sigma_hat_ber))

        acc = (num_tests - np.sum(np.array(NCOMP_ber_error))) / num_tests
        NCOMP_ber_acc.append(acc)

    ################ Plot ####################
    PAL = ['#fef0d9','#fdcc8a','#fc8d59','#d7301f']
    plt.plot(T_array, NCOMP_ber_acc,'o-', c=PAL[-2], label="NCOMP - q = " + str(round(q, 2)))

#     use LaTeX fonts in the plot
    plt.rc('text', usetex=True)
    plt.rc('font', family='serif')
    matplotlib.rcParams['text.usetex'] = True

    plt.axvline(x=float(achiev_bound), ymin=0, ymax=1, color='black', linestyle='dotted', label="Achievability Bound", linewidth=2)
    plt.xticks(ticks=[10, round((achiev_bound)), round(n)])
    plt.yticks(ticks=[0.0, 1.0])
    plt.xlabel(r'Number of tests (T)', fontsize=11)
    plt.ylabel(r'Success Probability', fontsize=11)
    plt.legend()
    plt.savefig('plots/NCOMP Report Nico 2 today' + str(round(C, 2)) + 'qs, q = ' + str(round(q, 2)) + ', delta = ' + str(round(delta, 2)) + ', n = ' + str(n) + ', theta = ' + str(round(theta, 2)) + '.png')
    plt.savefig('plots/NCOMP Report Nico 2 today' + str(round(C, 2)) + 'qs, q = ' + str(round(q, 2)) + ', delta = ' + str(round(delta, 2)) + ', n = ' + str(n) + ', theta = ' + str(round(theta, 2)) + '.eps')
    plt.savefig('plots/NCOMP Report Nico 2 today' + str(round(C, 2)) + 'qs, q = ' + str(round(q, 2)) + ', delta = ' + str(round(delta, 2)) + ', n = ' + str(n) + ', theta = ' + str(round(theta, 2)) + '.pdf')
    plt.show()
```

```python
## Redo the plot because the latex font only comes in the second time you save it for some reason.
PAL = ['#fef0d9','#fdcc8a','#fc8d59','#d7301f']
plt.plot(T_array, NCOMP_ber_acc,'o-', c=PAL[-2], label="NCOMP - q = " + str(round(q, 2)))

#     use LaTeX fonts in the plot
plt.rc('text', usetex=True)
plt.rc('font', family='serif')
matplotlib.rcParams['text.usetex'] = True

plt.axvline(x=float(achiev_bound), ymin=0, ymax=1, color='black', linestyle='dotted', label="Achievability Bound", linewidth=2)
plt.xticks(ticks=[10, round(float(achiev_bound)), round(n)])
plt.yticks(ticks=[0.0, 1.0])
plt.xlabel(r'Number of tests (T)', fontsize=11)
plt.ylabel(r'Success Probability', fontsize=11)
plt.legend()
plt.savefig('plots/NCOMP Report Nico 2 today' + str(round(C, 2)) + 'qs, q = ' + str(round(q, 2)) + ', delta = ' + str(round(delta, 2)) + ', n = ' + str(n) + ', theta = ' + str(round(theta, 2)) + '.png')
plt.savefig('plots/NCOMP Report Nico 2 today' + str(round(C, 2)) + 'qs, q = ' + str(round(q, 2)) + ', delta = ' + str(round(delta, 2)) + ', n = ' + str(n) + ', theta = ' + str(round(theta, 2)) + '.eps')
plt.savefig('plots/NCOMP Report Nico 2 today' + str(round(C, 2)) + 'qs, q = ' + str(round(q, 2)) + ', delta = ' + str(round(delta, 2)) + ', n = ' + str(n) + ', theta = ' + str(round(theta, 2)) + '.pdf')
plt.show()
```

### 3. NDD with Achievability bound for some q

```python
# Define initial para200
n = 1000 # population size
theta = 0.15
C = 2
n_theta = C * (n**theta)
k = round(n_theta) # number of infected
eta = 0.001
q_results = []

np.random.seed(23455324)

############ RUN EXPERIMENT #####################
for q in [0.10]:
    alpha = 0.5
    p = alpha/k # Bernoulli test design probability parameter
    print("q: ", q)
    NDD_ber_acc = []
    NCOMP_ber_acc = []

    T_array = np.linspace(1, n, 25)
    pi_init = (q+0.07)

    ########### Achievability Bound 2 Method 2 #######
    eta_1, alpha_1, theta_1, q_1 = eta, alpha, theta, q
    n_1 = n
    n_theta_1 = n_theta
    d_1 = round(n_theta_1)
    eps = 1e-12

    # Objective
    D_ = lambda _eps: (_eps)*np.log(_eps) - _eps + 1
    T1 = lambda pi: (d_1 * np.log(d_1) * np.exp(alpha_1 * (1-q_1)) / ((alpha_1 * q_1)*D_(pi / (q_1 * np.exp(-alpha_1 * (1-q_1))))))
    T2 = lambda pi: ((1 - (theta_1 - eps)) * d_1 * np.log(n) * np.exp(alpha_1*(1-q_1)) / (alpha_1 * D_(pi * np.exp(alpha_1 * (1-q_1)))))
    T3 = lambda pi: (np.exp(alpha_1) * d_1 * np.log(d_1) / (alpha_1 * (1-q_1)))
    ff = lambda pi: max(T1(pi), T2(pi), T3(pi))

    bound = (q, np.exp(-alpha * (1-q))-0.01)

    x0 = pi_init
    gff = grad(ff)
    res = minimize(ff, x0, tol=1e-12, bounds=[bound], jac=gff)
    achiev_bound = res.fun[0]
    pi_NDD = res.x[0]
    assert q <= pi_NDD <= 1

    ###### Main Experiment loop #####
    for T in T_array:
        NDD_ber_error = []
        DD_ber_error = []
        NCOMP_ber_error = []
        print("T: ", int(round(T)))
        for test in range(num_tests):
            sigma = D(n, k) # Generate the vector of defectives
            X_ber = Ber(n, int(round(T)), p)
            y_ber = Y(dilution_noise(X_ber, q), sigma)

            # NDD - Bernoulli
            sigma_hat_ber = NDD(X_ber, y_ber, pi=(pi_NDD), alpha=alpha, T = T, d = k)
            err = error(sigma, sigma_hat_ber)
            NDD_ber_error.append(error(sigma, sigma_hat_ber))

        acc = (num_tests - np.sum(np.array(NDD_ber_error))) / num_tests
        NDD_ber_acc.append(acc)

    ################ Plot ####################
    PAL = ['#f1eef6','#bdc9e1','#74a9cf','#0570b0']
    plt.plot(T_array, NDD_ber_acc,'o-', c=PAL[-1], label="NDD - q = " + str(round(q, 2)))

    # use LaTeX fonts in the plot
    plt.rc('text', usetex=True)
    plt.rc('font', family='serif')
    matplotlib.rcParams['text.usetex'] = True

    plt.axvline(x=ff(pi_NDD), ymin=0, ymax=1, color='black', linestyle='dotted', label="Achievability bound", linewidth=3)
    plt.xticks(ticks=[round(10.0), round(ff(pi_NDD)), round(n)])
    plt.yticks(ticks=[0.0, 1.0])
    plt.xlabel(r'Number of tests (T)', fontsize=11)
    plt.ylabel(r'Success Probability', fontsize=11)
    plt.legend()
    plt.savefig('plots/NDD Report Nico 2 today' + str(round(C, 2)) + 'qs, q = ' + str(round(q, 2)) + ', pi = ' + str(round(pi_NDD, 2)) + ', n = ' + str(n) + ', theta = ' + str(round(theta, 2)) + '.png')
    plt.savefig('plots/NDD Report Nico 2 today' + str(round(C, 2)) + 'qs, q = ' + str(round(q, 2)) + ', pi = ' + str(round(pi_NDD, 2)) + ', n = ' + str(n) + ', theta = ' + str(round(theta, 2)) + '.eps')
    plt.savefig('plots/NDD Report Nico 2 today' + str(round(C, 2)) + 'qs, q = ' + str(round(q, 2)) + ', pi = ' + str(round(pi_NDD, 2)) + ', n = ' + str(n) + ', theta = ' + str(round(theta, 2)) + '.pdf')
    plt.show()
```

### 1. NDD vs NCOMP for some/different qs

```python
# Define initial parameters
num_tests = 300
n = 1000 # population size
theta = 0.15
C = 2
n_theta = C * (n**theta)
k = round(n_theta) # number of infected
eta = 0.001
q_results = []

############ RUN EXPERIMENT #####################
for q in [0.000001, 0.15, 0.35, 0.50]:
    alpha = 0.5
    p = alpha/k # Bernoulli test design probability parameter
    print("q: ", q)
    NDD_ber_acc = []
    DD_ber_acc = []
    NCOMP_ber_acc = []

    T_array = np.linspace(1, n, 25)
    pi_init = (q+0.07)

    ########### Achievability Bound 2 Method 2 #######
    eta_1, alpha_1, theta_1, q_1 = eta, alpha, theta, q
    n_1 = n
    n_theta_1 = n_theta
    d_1 = round(n_theta_1)
    eps = 1e-12

    # Objective
    D_ = lambda _eps: (_eps)*np.log(_eps) - _eps + 1
    T1 = lambda pi: (d_1 * np.log(d_1) * np.exp(alpha_1 * (1-q_1)) / ((alpha_1 * q_1)*D_(pi / (q_1 * np.exp(-alpha_1 * (1-q_1))))))
    T2 = lambda pi: ((1 - (theta_1 - eps)) * d_1 * np.log(n) * np.exp(alpha_1*(1-q_1)) / (alpha_1 * D_(pi * np.exp(alpha_1 * (1-q_1)))))
    T3 = lambda pi: (np.exp(alpha_1) * d_1 * np.log(d_1) / (alpha_1 * (1-q_1)))
    ff = lambda pi: max(T1(pi), T2(pi), T3(pi))

    bound = (q, np.exp(-alpha * (1-q))-0.01)
    x0 = pi_init
    gff = grad(ff)
    res = minimize(ff, x0, tol=1e-6, bounds=[bound], jac=gff)
    achiev_bound = res.fun[0]
    pi_NDD = res.x[0]
    assert q <= pi_NDD <= 1

    ###### Main Experiment loop #####
    for T in T_array:
        NDD_ber_error = []
        DD_ber_error = []
        NCOMP_ber_error = []
        print("T: ", int(round(T)))
        for test in range(num_tests):
            sigma = D(n, k) # Generate the vector of defectives
            X_ber = Ber(n, int(round(T)), p)
            y_ber = Y(dilution_noise(X_ber, q), sigma)

            # NCOMP - Bernoulli
            # Choose delta according to z
            z = ((1- alpha/(k) * (1-q))**(k-1) + (1/q)*(1 - alpha/(k) * (1-q))**k) / 2
            l = (1 - alpha / k * (1-q))**(k-1)
            u = (1 - alpha / k * (1-q))**(k) * (1/q)
            delta_NCOMP = z - 1
            assert (l-1) < delta_NCOMP < (u-1)
            sigma_hat_ber = NCOMP(X_ber, y_ber, q=q, delta=delta_NCOMP)
            err = error(sigma, sigma_hat_ber)
            NCOMP_ber_error.append(error(sigma, sigma_hat_ber))

            # NDD - Bernoulli
            sigma_hat_ber = NDD(X_ber, y_ber, pi=(pi_NDD), alpha=alpha, T = T, d = k)
            err = error(sigma, sigma_hat_ber)
            NDD_ber_error.append(error(sigma, sigma_hat_ber))

        acc = (num_tests - np.sum(np.array(NDD_ber_error))) / num_tests
        NDD_ber_acc.append(acc)
        acc = (num_tests - np.sum(np.array(NCOMP_ber_error))) / num_tests
        NCOMP_ber_acc.append(acc)

    q_results.append([q, T_array, NCOMP_ber_acc, NDD_ber_acc])
```

```python
PAL1 = ['#eff3ff','#bdd7e7','#6baed6','#3182bd','#08519c']
PAL2 = ['#feedde','#fdbe85','#fd8d3c','#e6550d','#a63603']
for i in range(len(q_results)):
    print(i)
    q, T_array, NCOMP_ber_acc, NDD_ber_acc = q_results[i]

    if (i==0):
        plt.plot(T_array, NDD_ber_acc,'o-', c=PAL1[-1-i], label="NDD - q = 0.0 to 0.5", linewidth=2)
        plt.plot(T_array, NCOMP_ber_acc,'--', c=PAL2[-1-i], label="NCOMP - q = 0.0 to 0.5", linewidth=2)
    else:
        plt.plot(T_array, NDD_ber_acc,'o-', c=PAL1[-1-i], linewidth=2)
        plt.plot(T_array, NCOMP_ber_acc,'--', c=PAL2[-1-i], linewidth=2)

    # use LaTeX fonts in the plot
    plt.rc('text', usetex=True)
    plt.rc('font', family='serif')
    matplotlib.rcParams['text.usetex'] = True

    plt.yticks(ticks=[0.0, 1.0])
    plt.xlabel(r'Number of tests (T)', fontsize=11)
    plt.ylabel(r'Success Probability', fontsize=11)

plt.legend()
plt.savefig('plots/NCOMP vs NDD Report Nico 2 today' + str(round(C, 2)) + 'qs, q = ' + str(round(q, 2)) + ', delta = ' + str(round(delta, 2)) + ', n = ' + str(n) + ', theta = ' + str(round(theta, 2)) + '.png')
plt.savefig('plots/NCOMP vs NDD Report Nico 2 today' + str(round(C, 2)) + 'qs, q = ' + str(round(q, 2)) + ', delta = ' + str(round(delta, 2)) + ', n = ' + str(n) + ', theta = ' + str(round(theta, 2)) + '.pdf')
plt.show()
```

```python

```

notebooks/.ipynb_checkpoints/Semester Project Plots Nico-checkpoint.ipynb


# Exercise session nº 3
---
# Geometry, epistasis, and developmental patterning

__*Sacha Ichbiah, 07/02/22, ENS Paris*__

This subject is extracted from :
> Françis Corson, Eric Siggia, *Geometry, epistasis, and developmental patterning*, PNAS, 2012.
> https://doi.org/10.1073/pnas.1201505109

And the implementation is inspired from :
> Lisa Bedin https://colab.research.google.com/drive/1X7hAimJ12D7iA98kBdbmEGOnWw5VAYHP?usp=sharing

 During development, cells or tissues become competent when they can sense signals that direct them toward a particular fate; they are specified or committed to a fate when the signal can be removed with no change in outcome. Cells are determined when they cannot be directed to alternative fates by other signals. Differentiation ensues when specialized gene batteries are induced along with characteristic morphology. None of these concepts can be tested in a completely controlled way in vivo, and the recent creation of induced pluripotent stem cells shows that even differentiation can be reversed.

These concepts admit a natural __geometric representation__, which can be formalized in the language of dynamical systems, also called the geometric theory of differential equations. When the molecular details are not accessible, a system’s __effective behavior__ may be represented in terms of a small number of aggregate variables, and qualitatively different behaviors enumerated according to the geometrical structure of __trajectories__ or __topology__. The fates that are accessible to a cell are associated with __attractors__ — the valleys in Waddington’s “epigenetic landscape” - to which neighboring trajectories converge.

The description of cellular differentiation with energy landscapes dates from __Conrad Hal Waddington__, a founding father of __epigenetics__, who viewed the evolution as a process happening via mutations of the genes involved in the developpment. The energy landscape in which the cells lived is determined by the genome of the individual. Thus mutation would affect the landscapes, and therefore affect cellular states.

>__Epigenetics :__ The study of changes in cells caused by modification of gene expression rather than alteration of the genetic code itself.

>__Epistasis :__ When one or several genes avoid the expression of other genes

>__Hypomorph (hypermorph) :__ A type of mutation in which the altered gene product possesses a reduced (enhanced) level of activity

>__Autocrine :__ Denoting or relating to a cell-produced substance that has an effect on the cell by which it is secreted.

>__Differentiation :__ Process through which cells specialized themselves into a given cell-type

Developmental signaling networks are indeed composed of dozens of components whose interactions are very difficult to quantify in an embryo. The authors of the paper model the discrete hierarchy of phenotypic models using energy landscapes. This model encompasses both signaling and commitment, and allows to predict the correlated cell fate probabilities for a cross between any two backgrounds/conditions.  The two signaling pathways are combined additively, without interactions, and epistasis only arises from the nonlinear dynamical flow in the landscape defined by the geometric model.

The biological example considered here is the vulva of the nematode C. Elegans, in which two signaling pathways are integrated : Induction by __EGF__ and lateral signaling through __Notch__. The genetic experiments seems to indicate a mutual pathway repression. This model fits these experimental data, but also quantifies the contributions of extrinsic vs. intrinsic sources of noise in the penetrance of mutant phenotypes in signaling hypomorphs.

During this session, we will model the energy landscape described in the paper and make several predictions from this model, linked to biological data.

---

# 0 - Landscape topology

There are three possible topologies of the landscape. The landscape of the right is choosen to model vulval patterning based on experimental data.
All transitions between two fates are permissible, there is a central source (__open circle__) and saddles (__crosses__) between each pair of fate.

### Governing equations :
The state of each cell is parametrized by a two-dimensional vector $\vec{r} = x\vec{e}_x + y \vec{e}_y$ and its dynamics are defined as follows :

$
\begin{align}
\vec{f}(\vec{r}) &= 2 \vec{r} + c_2[-2x\vec{e}_x + (y^2 + x^2)\vec{e}_y] \newline
\frac{d\vec{r}}{dt} &= \frac{1}{\tau}[\vec{\sigma}_1(\vec{f}(\vec{r}) + \vec{m}) - \vec{r}] + \vec{\eta}(t) \newline
\vec{\sigma}_1 &= tanh(||\vec{f}||)\frac{\vec{f}}{||\vec{f}||} \newline
\vec{m} &= \vec{m}_0 + l_1 \vec{m}_1 + l_2 \vec{m}_2 \newline
\left< \eta_i(t) \eta_j(t')\right> &= 2D \delta_{ij} \delta(t-t')
\end{align}
$

$\vec{f}(\vec{r})$ is the term in the equations that make the dynamics multistable.

### Initial conditions :

We assume that the initial state is monostable. Thus we remove the term $\vec{f}$ from the equations that maked them multistable. The dynamics are given by :

$
\dfrac{d\vec{r}}{dt} = \frac{1}{\tau}[\vec{\sigma}_1(\vec{m}_0) - \vec{r}] + \vec{\eta}(t) $

### __Question 1__
> What is the steady state of the initial distribution of $\vec{r}$ ?

__*Correction*__ at steady state, $\vec{r} = \vec{\sigma}_1(\vec{m}_0) + \tau \vec{\eta}$, It is thus drawn from a gaussian distribution $\mathcal{N}(\vec{\sigma}_1(\vec{m}_0), 2D \tau^2)$

### __Question 2__
> Discretize the equation with a forward euler-scheme. from which distribution do you draw $\eta$ at discrete times ?

__*Correction*__ We call the timestep $\Delta t$. Thus $t_n = n\Delta t$

$\vec{r}(t_{n+1}) = \vec{r}(t_n)(1-\frac{\Delta t}{\tau}) + \frac{\Delta t}{\tau}(\vec{\sigma}_1(\vec{f}(t_n) + \vec{m}) + \vec{\eta}_n)$

We have $\eta_i^n = \int_{(n-1)\Delta t}^{n\Delta t}\eta_i(t)dt$, thus :

$
\begin{align}
\left<\eta_i^n \right> &= \left<\int_{(n-1)\Delta t}^{n\Delta t}\eta_i(t)dt \right> = \int_{(n-1)\Delta t}^{n\Delta t}\left< \eta_i(t) \right> = 0 \newline
\left<\eta_i^n \eta_j^m \right> &= \left<  \int_{(n-1)\Delta t}^{n\Delta t}\eta_i(t)dt \int_{(m-1)\Delta t}^{m\Delta t}\eta_j(t')dt' \right>  \newline
\left<\eta_i^n \eta_j^m \right> &= \left<  \int_{(n-1)\Delta t}^{n\Delta t} \int_{(m-1)\Delta t}^{m\Delta t} \eta_i(t)\eta_j(t')dtdt' \right>  \newline
\left<\eta_i^n \eta_j^m \right> &=  \int_{(n-1)\Delta t}^{n\Delta t} \int_{(m-1)\Delta t}^{m\Delta t} \left<\eta_i(t)\eta_j(t') \right> dtdt' \newline
\left<\eta_i^n \eta_j^m \right> &=  \int_{(n-1)\Delta t}^{n\Delta t} \int_{(m-1)\Delta t}^{m\Delta t} 2D \delta(t-t') \delta_{ij} dtdt' \newline
\left<\eta_i^n \eta_j^m \right> &=  \int_{(n-1)\Delta t}^{n\Delta t}  2D \delta(m-n) \delta_{ij} dt \newline
\left<\eta_i^n \eta_j^m \right> &=  2D \Delta t \delta(m-n) \delta_{ij} \newline
\end{align}
$

Thus $\eta \mapsto \mathcal{N}(0,2D \Delta t)$

```python
from utils import *
from tqdm import tqdm
import numpy as np # Numerical computing library
import matplotlib
import matplotlib.pyplot as plt # Plotting library
```

```python
#The given parameters are infered from biological data.
#A big part of the study was to determine all these parameters from the experiments.
#We will only give the phenotypical landscapes

tau = 1./2.18
D = 0.16**2/4.
c2 = 1

def sigmaonefunc(x):
    norm = np.linalg.norm(x)
    sigma = np.tanh(norm)*x/norm
    return(sigma)

def f(r):
    s = 2*r
    x,y = r
    e1 = np.array([1,0])
    e2 = np.array([0,1])
    s-=e1*c2*(2*x*y)
    s+=e2*c2*(y**2-x**2)
    return(s)
```

# I - Unbiased Dynamics

We want to find the fixed points of the energy landscape. We will consider the situation without noise $(\eta = 0)$ and without bias $(\vec{m}=0)$

$
\dfrac{d\vec{r}}{dt} = \dfrac{1}{\tau}[\vec{\sigma}_1(\vec{f}(\vec{r}) - \vec{r}]$

### __Question 3__
> Integrate and plot these dynamics with $t\in[0,20]$ from the given initial conditions. How many basins do you find ? Find the coordinates of the basins from these dynamics.

__Tip :__ You can do an euler scheme or use _scipy.integrate.odeint_ to find the trajectories.

__*Correction*__

```python
import scipy.integrate

#This function works by defining a function func(r,t) giving the value of dr/dt, and the timepoints.

def dynamic_unbiased(r, t):
    f_value = f(r)
    sigma1 = sigmaonefunc(f_value)
    return (sigma1 - r) / tau

time = np.linspace(0, 20, 1000)
initial_conditions = [(.0001, .0001), (.0001, -.0001), (-.0001, .0001), (-.0001, -.0001), (-0.001,0),(0.001,0),(1e-6,-0.001),(-1e-6,-0.001)]

# Do the simulations.
# Remember that we define f as the partial application of cellular_switch.
trajectory = {}
for j, ic in enumerate(initial_conditions):
    trajectory[j] = scipy.integrate.odeint(dynamic_unbiased, y0=ic, t=time)
```

```python
# Draw the trajectories.

fig,ax = plot_voronoi_setup()
for j,ic in enumerate(initial_conditions):
    l=plt.plot(trajectory[j][:,0],trajectory[j][:,1],label = ic)
    add_arrow(l[0])

plt.xlim(-np.sqrt(2),np.sqrt(2))
plt.ylim(-np.sqrt(2),np.sqrt(2))
plt.gca().set_aspect('equal', adjustable='box')
plt.legend(title = "Initial conditions",loc="lower center", bbox_to_anchor=(0.5, 1.05), ncol=8)
plt.title("Trajectories in the unbiased case")

converging_points = np.array([trajectory[k][-1] for k in range(len(trajectory))])
converging_points = np.unique(converging_points.round(5),axis=0)
print("Converging points :",converging_points)
```

### Phase Diagram : Basins of attraction

We now want to have a representation of the energy landscape by computing a vector field of the dynamics at each point. The vector field should lead to one of the basins established before. More concretely, we will plot the vector field $\dfrac{dr}{dt}$ on the grid $[-\sqrt{2},\sqrt{2}]^2$.

To do that, we will evaluate the vector field at each point of a discretized square grid Z , and use matplotlib.pyplot.streamplot to plot it.

```python
# We determine the x and y points of the grid

xlist20 = np.linspace(-np.sqrt(2), np.sqrt(2), 20)
ylist20 = np.linspace(-np.sqrt(2), np.sqrt(2), 20)

# We determine the values of the vector field on this grid
X20, Y20 = np.meshgrid(xlist20, ylist20)
Z20 = np.zeros((20,20,2))

for a,b in np.ndindex(X20.shape):
    x, y = X20[a,b],Y20[a,b]
    Z20[a,b] = dynamic_unbiased(np.array([x,y]), 0)
```

```python
fig,ax = plot_voronoi_setup(plot_bassins=True,markersize=200)
plt.streamplot(X20, Y20, Z20[:,:,0], Z20[:,:,1],color = 'black', linewidth = 1.5,density = 1)
plt.show()
```

# II - Biased Dynamics

Now we work with biased dynamics $\vec{m} \neq \vec{0}$. We have :

$
\begin{align}
\frac{d\vec{r}}{dt} &= \frac{1}{\tau}[\vec{\sigma}_1(\vec{f}(\vec{r}) + \vec{m}) - \vec{r}] + \vec{\eta}(t) \newline
\vec{m} &= \vec{m}_0 + l_1 \vec{m}_1 + l_2 \vec{m}_2 \newline
\end{align}
$

Where $\vec{m}_0$ represent the natural tendency to go to a 3° fate, $\vec{m}_1$ represent the EGF signaling, leading to the 1° fate, and $\vec{m}_2$ represent the Notch signaling, leading to the 2° fate.

These biased dynamics obviously change the topological landscape that was observed previously. We will determine these new landscapes by plotting the vector field of the dynamics, as before.

```python
stationary = np.array([[-0.86156317, -0.49742373],[ 0.86156317, -0.49742373],[1.60591136e-14, 9.94847458e-01]])
```

```python
m0_norm = 0.39
m1_norm = 4.60
m2_norm = 5.97

m0 = m0_norm * stationary[0] / np.linalg.norm(stationary[0])  # fate 3°
m1 = m1_norm * stationary[1] / np.linalg.norm(stationary[1])  # EGF signaling
m2 = m2_norm * stationary[2] / np.linalg.norm(stationary[2])  # Notch signaling
m0, np.linalg.norm(m0), np.linalg.norm(stationary[0]), np.linalg.norm(stationary[1]), np.linalg.norm(stationary[2])
m_norm_max = np.max([m0_norm, m1_norm, m2_norm])
```

```python
fig,ax = plot_voronoi_setup(plot_bassins=True, figsize=(5,5))
plt.streamplot(X20, Y20, Z20[:,:,0], Z20[:,:,1],color = "k",linewidth=0.75)

lw = 6

plt.arrow(0., 0.,m0[0]/m_norm_max, m0[1]/m_norm_max, head_width=0.1, head_length=0.1, color='b',linewidth=lw) # fate 3°
plt.arrow(0., 0.,m1[0]/m_norm_max, m1[1]/m_norm_max, head_width=0.1, head_length=0.1, color='r',linewidth=lw) # EGF signaling
plt.arrow(0., 0.,m2[0]/m_norm_max, m2[1]/m_norm_max, head_width=0.1, head_length=0.1, color='g',linewidth=lw) # Notch signaling
```

### Phase Diagram : Basins of attraction

### __Question 4__

> Write the derivative function in the case of the biased dynamics. Plot the vector field of the dynamics for each scenario, as was done before un the case of unbiased dynamics. What do you observe ?

```python
def dynamic_biased(r, t, l1, l2):
    f_value = f(r)
    m_value = m0 + l1 * m1 + l2 * m2
    sigma1 = sigmaonefunc(f_value + m_value)
    return (sigma1 - r) / tau

scenarios = [
    {'l1': 0, 'l2': 0},
    {'l1': 0.16, 'l2': 0},
    {'l1': 0.5, 'l2': 0},
    {'l1': 0., 'l2': 0.16},
    {'l1': 0., 'l2': 0.5}
    ]

fig, ax = plt.subplots(1,5,figsize=(25,10))

points_plotted = [[0,1,2],[0,1,2],[1],[0,1,2],[2]]

for j,params in enumerate(scenarios) :

    l1, l2 = [params[key] for key in ["l1","l2"]]

    Z20 = np.zeros((20,20,2))

    for a,b in np.ndindex(X20.shape):
        x, y = X20[a,b],Y20[a,b]
        Z20[a,b] = dynamic_biased(np.array([x,y]), 0,l1,l2)

    ax[j].streamplot(X20, Y20, Z20[:,:,0], Z20[:,:,1],color = 'black', linewidth = 1.5,density = 1)
    ax[j].scatter(stationary[points_plotted[j]][:,0],stationary[points_plotted[j]][:,1],color='red',s=100)
    ax[j].arrow(0., 0.,2*m0[0]/m_norm_max, 2*m0[1]/m_norm_max, head_width=0.05, head_length=0.05, color='b',linewidth=lw) # fate 3°
    if l1>0 :
        ax[j].arrow(0., 0.,2*l1*m1[0]/m_norm_max, 2*l1*m1[1]/m_norm_max, head_width=0.05, head_length=0.05, color='r',linewidth=lw) # EGF signaling
    if l2>0 :
        ax[j].arrow(0., 0.,2*l2*m2[0]/m_norm_max, 2*l2*m2[1]/m_norm_max, head_width=0.05, head_length=0.05, color='g',linewidth=lw) # Notch signaling
    ax[j].set_aspect('equal')
    ax[j].axis('off')
    ax[j].set_title(params)

```

# III - Vulval Precursor Cells (VPCs) Trajectories

C-Elegans is a nematode that has an invariange lineage pattern : the cells always divide in the same way to form the same lineage tree in the normal development. Thus, mutations alterating this lineage provide important informations about the mechanisms involved to obtain these fates.

The vulva of Caenorhabditis elegans forms from a row of vulval precursor cells (VPCs), which are competent to adopt three possible fates :

- 1° in red, induced by EGF from the neighboring anchor cell (AC)
- 2° in green, induced by Notch lateraly among VPCs
- 3° in blue, in absence of induction

The pattern of fates among the daugthers of these precursors cells is shown on the figure bellow. When development is perturbed, other patterns of the three fates are obtained, often with variable results for genetically identical animals. Thus experimental outcomes are described as probabilities. We will show how modifying the previous landscape according to these mutations allows to retrieve experimental features.

We will integrate the dynamics of the five cells P4,P5,P6,P7,P8 at each time points.
From the previous situation, we now need to integrate the interaction between several cells through EGF and Notch. for EGF :

#### EGF signaling

The EGF signal is fixed and decreases exponentially with the distance from the anchor cell (AC):

$l_1 = l_1^0 \times [\gamma^2, \gamma, 1, \gamma, \gamma^2]$ respectively, for $[P4.p,P5.p,P6.p,P7.p,P8.p]$

#### Notch signaling

On the other hand, the lateral signal emitted by each VPC depends on its current state. The lateral signal is defined by a function $L_2(\vec{r})$ which varies as a sigmoid from zero to one when $\vec{r}$ crosses a threshold line that runs diagonally through the 3°, 1° domains.

$\begin{align}
L_2(\vec{r}) &= \sigma_2(n_0 + \vec{n}_1 \cdot \vec{r}) \newline
\sigma_2(u) &= \dfrac{1+\text{tanh}2u}{2}
\end{align}$

To relate the signal, L2, produced by a cell and its neighbors to Notch signaling, l2, in that cell, we introduce a ratio $\alpha$ to parameterize the relative importance of autocrine and paracrine signaling (Notch ligands or Deltas include both transmembrane and diffusible ligands). Thus for P6.p:

$l_2(P6.p) = \alpha L_2(P6.p) + L_2(P5.p) + L_2(P7.p)$

### __Question 5__

> With a forward-Euler scheme, integrate and plot the trajectories of the five VPC in the case of normal (WT) development.

Discretizing with the Euler method a stochastic differential equation is called a the Euler-Maruyama method.

https://en.wikipedia.org/wiki/Euler–Maruyama_method This is quite natural to do so but also theoretically grounded !

```python
gamma = 0.16
alpha = 1.14
n0 = -1.23
rot1 = -48.28 * np.pi / 180.
n1_norm = 3
n1 = n1_norm * np.array([np.cos(rot1), np.sin(rot1)])
n1_normal = np.array([np.sin(rot1), -np.cos(rot1)])
l1_over = 7.39
l1_lin = 0.55
```

### Notch Ligand line :

$$l_2 = \frac{\alpha}{2}(1+\tanh(2 (n_0 +\overrightarrow{n_1} \cdot \overrightarrow{r}))$$
Thus, to reach the half expression of Notch ligand (cf. definition of "induction of half" in S2.4.1 single cell response):
$$\tanh(2 (n_0 +\overrightarrow{n_1} \cdot \overrightarrow{r})=\frac{4 m_0}{\alpha m_2} \Leftrightarrow \overrightarrow{n_1} \cdot \overrightarrow{r} = \frac{1}{2} \tanh^{-1}(\frac{4 m_0}{\alpha m_2}) -n_0:=h$$
Thus, we obtain the following line equation:
$$\overrightarrow{r}=\frac{h}{\|n_1\|^2}\overrightarrow{n_1}+\lambda \overrightarrow{n'_1}$$ where $\overrightarrow{n'_1}$ is the unit normal vector to $\overrightarrow{n_1}$, and $\lambda \in \mathbb{R}$ is a scalar defined such as $\|r\|$ is in the definition bounds

```python
def notch_line_fn(lambd_min, lambd_max):
    half_value = np.arctanh(4*m0_norm / (alpha * m2_norm)) / 2 - n0
    r_min = half_value / n1_norm ** 2 * n1 + lambd_min * n1_normal
    r_max = half_value / n1_norm ** 2 * n1 + lambd_max * n1_normal
    return np.stack([r_min, r_max], axis=0)
```

```python
def sigmatwofunc(u):
    sigma = (1 + np.tanh(2*u)) / 2
    return(sigma)

def L2(u):
    return(sigmatwofunc(n0+np.dot(n1,u)))

nsteps = 100
nit = 200

timepoints = np.linspace(0,1,nsteps)
dt = timepoints[1]

L1 = np.ones(5)
L1[0]=L1[4]=gamma**2
L1[1]=L1[3]=gamma
L1*=l1_lin

Startpoints = np.zeros((nit,5,2))
Endpoints   = np.zeros((nit,5,2))
Trajectories= np.zeros((nit,nsteps,5,2))

R4 = np.zeros((nsteps,2))
R5 = np.zeros((nsteps,2))
R6 = np.zeros((nsteps,2))
R7 = np.zeros((nsteps,2))
R8 = np.zeros((nsteps,2))

for i in tqdm(range(nit)):
    for n in range(0,nsteps):

        if n ==0 :
            sigma1 = sigmaonefunc(m0)
            R4[n] = np.random.normal(0,np.sqrt(2*D*tau**2),2) + sigma1
            R5[n] = np.random.normal(0,np.sqrt(2*D*tau**2),2) + sigma1
            R6[n] = np.random.normal(0,np.sqrt(2*D*tau**2),2) + sigma1
            R7[n] = np.random.normal(0,np.sqrt(2*D*tau**2),2) + sigma1
            R8[n] = np.random.normal(0,np.sqrt(2*D*tau**2),2) + sigma1
            #print(R4[0],R5[0],R6[0],R7[0],R8[0],sigma1)
            continue

        #Notch signaling
        l2_p4 = alpha * L2(R4[n-1]) + L2(R5[n-1])
        l2_p5 = alpha * L2(R5[n-1]) + L2(R4[n-1]) + L2(R6[n-1])
        l2_p6 = alpha * L2(R6[n-1]) + L2(R5[n-1]) + L2(R7[n-1])
        l2_p7 = alpha * L2(R7[n-1]) + L2(R6[n-1]) + L2(R8[n-1])
        l2_p8 = alpha * L2(R8[n-1]) + L2(R7[n-1])

        sigma1 = sigmaonefunc(f(R4[n-1])+m0+m1*L1[0]+m2*l2_p4)
        eta = np.random.normal(0,np.sqrt(2*D*dt),2)
        R4[n]=R4[n-1]*(1-(dt/tau))+(dt/tau)*(sigma1) + eta

        sigma1 = sigmaonefunc(f(R5[n-1])+m0+m1*L1[1]+m2*l2_p5)
        eta = np.random.normal(0,np.sqrt(2*D*dt),2)
        R5[n]=R5[n-1]*(1-(dt/tau))+(dt/tau)*(sigma1) + eta

        sigma1 = sigmaonefunc(f(R6[n-1])+m0+m1*L1[2]+m2*l2_p6)
        eta = np.random.normal(0,np.sqrt(2*D*dt),2)
        R6[n]=R6[n-1]*(1-(dt/tau))+(dt/tau)*(sigma1) + eta

        sigma1 = sigmaonefunc(f(R7[n-1])+m0+m1*L1[3]+m2*l2_p7)
        eta = np.random.normal(0,np.sqrt(2*D*dt),2)
        R7[n]=R7[n-1]*(1-(dt/tau))+(dt/tau)*(sigma1) + eta

        sigma1 = sigmaonefunc(f(R8[n-1])+m0+m1*L1[4]+m2*l2_p8)
        eta = np.random.normal(0,np.sqrt(2*D*dt),2)
        R8[n]=R8[n-1]*(1-(dt/tau))+(dt/tau)*(sigma1) + eta

    Startpoints[i] = np.stack((R4[0],R5[0],R6[0],R7[0],R8[0]))
    Endpoints[i] = np.stack((R4[-1],R5[-1],R6[-1],R7[-1],R8[-1]))
    Trajectories[i] = np.stack((R4,R5,R6,R7,R8),axis=2).transpose(0,2,1)

fig,ax = plot_voronoi_setup()
plt.scatter(Startpoints[:,:,0],Startpoints[:,:,1],color = 'k')
mean_traj = np.mean(Trajectories,axis=0)
colors = ["blue","green","red","green","blue"]
for i in range(5):
    plt.plot(mean_traj[:,i,0],mean_traj[:,i,1],color = colors[i])
    plt.scatter(Endpoints[:,i,0],Endpoints[:,i,1],color = "tab:"+colors[i])

notch_line = notch_line_fn(-3,3)
plt.plot(notch_line[:,0], notch_line[:, 1], linestyle='--', color='k')
```

# IV - Predictions :

### EGF overexpression :

EGF overexpression can be implemented easily by changing the multiplicative coefficient $l_1^0$ in the expression :

$l_1 = l_1^0 \times [\gamma^2, \gamma, 1, \gamma, \gamma^2]$ respectively, for $[P4.p,P5.p,P6.p,P7.p,P8.p]$

### __Question 6__

> Integrate and plot the trajectories of the five VPC in the case of EGF overexpressed mutants, replacing $l_1^0$ by $l_1^{\text{over}}$. Compare to the wild-type trajectories.

```python
nsteps = 100
nit = 200
timepoints = np.linspace(0,1,nsteps)
dt = timepoints[1]

L1 = np.ones(5)
L1[0]=L1[4]=gamma**2
L1[1]=L1[3]=gamma
L1*=l1_over

Startpoints = np.zeros((nit,5,2))
Endpoints   = np.zeros((nit,5,2))
Trajectories= np.zeros((nit,nsteps,5,2))

R4 = np.zeros((nsteps,2))
R5 = np.zeros((nsteps,2))
R6 = np.zeros((nsteps,2))
R7 = np.zeros((nsteps,2))
R8 = np.zeros((nsteps,2))

for i in tqdm(range(nit)):
    for n in range(0,nsteps):

        if n ==0 :
            sigma1 = sigmaonefunc(m0)
            R4[n] = np.random.normal(0,np.sqrt(2*D*tau**2),2) + sigma1
            R5[n] = np.random.normal(0,np.sqrt(2*D*tau**2),2) + sigma1
            R6[n] = np.random.normal(0,np.sqrt(2*D*tau**2),2) + sigma1
            R7[n] = np.random.normal(0,np.sqrt(2*D*tau**2),2) + sigma1
            R8[n] = np.random.normal(0,np.sqrt(2*D*tau**2),2) + sigma1
            #print(R4[0],R5[0],R6[0],R7[0],R8[0],sigma1)
            continue

        #Notch signaling
        l2_p4 = alpha * L2(R4[n-1]) + L2(R5[n-1])
        l2_p5 = alpha * L2(R5[n-1]) + L2(R4[n-1]) + L2(R6[n-1])
        l2_p6 = alpha * L2(R6[n-1]) + L2(R5[n-1]) + L2(R7[n-1])
        l2_p7 = alpha * L2(R7[n-1]) + L2(R6[n-1]) + L2(R8[n-1])
        l2_p8 = alpha * L2(R8[n-1]) + L2(R7[n-1])

        sigma1 = sigmaonefunc(f(R4[n-1])+m0+m1*L1[0]+m2*l2_p4)
        eta = np.random.normal(0,np.sqrt(2*D*dt),2)
        R4[n]=R4[n-1]*(1-(dt/tau))+(dt/tau)*(sigma1) + eta

        sigma1 = sigmaonefunc(f(R5[n-1])+m0+m1*L1[1]+m2*l2_p5)
        eta = np.random.normal(0,np.sqrt(2*D*dt),2)
        R5[n]=R5[n-1]*(1-(dt/tau))+(dt/tau)*(sigma1) + eta

        sigma1 = sigmaonefunc(f(R6[n-1])+m0+m1*L1[2]+m2*l2_p6)
        eta = np.random.normal(0,np.sqrt(2*D*dt),2)
        R6[n]=R6[n-1]*(1-(dt/tau))+(dt/tau)*(sigma1) + eta

        sigma1 = sigmaonefunc(f(R7[n-1])+m0+m1*L1[3]+m2*l2_p7)
        eta = np.random.normal(0,np.sqrt(2*D*dt),2)
        R7[n]=R7[n-1]*(1-(dt/tau))+(dt/tau)*(sigma1) + eta

        sigma1 = sigmaonefunc(f(R8[n-1])+m0+m1*L1[4]+m2*l2_p8)
        eta = np.random.normal(0,np.sqrt(2*D*dt),2)
        R8[n]=R8[n-1]*(1-(dt/tau))+(dt/tau)*(sigma1) + eta

    Startpoints[i] = np.stack((R4[0],R5[0],R6[0],R7[0],R8[0]))
    Endpoints[i] = np.stack((R4[-1],R5[-1],R6[-1],R7[-1],R8[-1]))
    Trajectories[i] = np.stack((R4,R5,R6,R7,R8),axis=2).transpose(0,2,1)

fig,ax = plot_voronoi_setup()
plt.scatter(Startpoints[:,:,0],Startpoints[:,:,1],color = 'k')
mean_traj = np.mean(Trajectories,axis=0)
colors = ["blue","green","red","green","blue"]
for i in range(5):
    plt.plot(mean_traj[:,i,0],mean_traj[:,i,1],color = colors[i])
    plt.scatter(Endpoints[:,i,0],Endpoints[:,i,1],color = "tab:"+colors[i])
```

### Anchor cell Ablation :

An in-vivo experiment to test our model is to remove the anchor cell (AC) responsible for EGF induction during the development. This is implemented in our model by putting $l_1^0 = 0$ at one moment during the development (corresponding to the moment where the anchor cell is ablated)

### __Question 7__

> Integrate and plot the trajectories of the five VPC and ablate the anchor cell (AC) at $t=0.4$, replacing $l_1^0$ by $0$. Compare to the wild-type trajectories.

```python
nsteps = 100
nit = 200
timepoints = np.linspace(0,1,nsteps)
dt = timepoints[1]

Startpoints = np.zeros((nit,5,2))
Endpoints   = np.zeros((nit,5,2))
Trajectories= np.zeros((nit,nsteps,5,2))

R4 = np.zeros((nsteps,2))
R5 = np.zeros((nsteps,2))
R6 = np.zeros((nsteps,2))
R7 = np.zeros((nsteps,2))
R8 = np.zeros((nsteps,2))

for i in tqdm(range(nit)):

    L1 = np.ones(5)
    L1[0]=L1[4]=gamma**2
    L1[1]=L1[3]=gamma
    L1*=l1_lin

    for n in range(0,nsteps):

        ##Anchor cell ablation at t = 0.4
        if timepoints[n] > 0.4 :
            L1*=0

        if n ==0 :
            sigma1 = sigmaonefunc(m0)
            R4[n] = np.random.normal(0,np.sqrt(2*D*tau**2),2) + sigma1
            R5[n] = np.random.normal(0,np.sqrt(2*D*tau**2),2) + sigma1
            R6[n] = np.random.normal(0,np.sqrt(2*D*tau**2),2) + sigma1
            R7[n] = np.random.normal(0,np.sqrt(2*D*tau**2),2) + sigma1
            R8[n] = np.random.normal(0,np.sqrt(2*D*tau**2),2) + sigma1
            #print(R4[0],R5[0],R6[0],R7[0],R8[0],sigma1)
            continue

        #Notch signaling
        l2_p4 = alpha * L2(R4[n-1]) + L2(R5[n-1])
        l2_p5 = alpha * L2(R5[n-1]) + L2(R4[n-1]) + L2(R6[n-1])
        l2_p6 = alpha * L2(R6[n-1]) + L2(R5[n-1]) + L2(R7[n-1])
        l2_p7 = alpha * L2(R7[n-1]) + L2(R6[n-1]) + L2(R8[n-1])
        l2_p8 = alpha * L2(R8[n-1]) + L2(R7[n-1])

        sigma1 = sigmaonefunc(f(R4[n-1])+m0+m1*L1[0]+m2*l2_p4)
        eta = np.random.normal(0,np.sqrt(2*D*dt),2)
        R4[n]=R4[n-1]*(1-(dt/tau))+(dt/tau)*(sigma1) + eta

        sigma1 = sigmaonefunc(f(R5[n-1])+m0+m1*L1[1]+m2*l2_p5)
        eta = np.random.normal(0,np.sqrt(2*D*dt),2)
        R5[n]=R5[n-1]*(1-(dt/tau))+(dt/tau)*(sigma1) + eta

        sigma1 = sigmaonefunc(f(R6[n-1])+m0+m1*L1[2]+m2*l2_p6)
        eta = np.random.normal(0,np.sqrt(2*D*dt),2)
        R6[n]=R6[n-1]*(1-(dt/tau))+(dt/tau)*(sigma1) + eta

        sigma1 = sigmaonefunc(f(R7[n-1])+m0+m1*L1[3]+m2*l2_p7)
        eta = np.random.normal(0,np.sqrt(2*D*dt),2)
        R7[n]=R7[n-1]*(1-(dt/tau))+(dt/tau)*(sigma1) + eta

        sigma1 = sigmaonefunc(f(R8[n-1])+m0+m1*L1[4]+m2*l2_p8)
        eta = np.random.normal(0,np.sqrt(2*D*dt),2)
        R8[n]=R8[n-1]*(1-(dt/tau))+(dt/tau)*(sigma1) + eta

    Startpoints[i] = np.stack((R4[0],R5[0],R6[0],R7[0],R8[0]))
    Endpoints[i] = np.stack((R4[-1],R5[-1],R6[-1],R7[-1],R8[-1]))
    Trajectories[i] = np.stack((R4,R5,R6,R7,R8),axis=2).transpose(0,2,1)

fig,ax = plot_voronoi_setup()
plt.scatter(Startpoints[:,:,0],Startpoints[:,:,1],color = 'k')
mean_traj = np.mean(Trajectories,axis=0)
colors = ["blue","green","red","green","blue"]
for i in range(5):
    plt.plot(mean_traj[:,i,0],mean_traj[:,i,1],color = colors[i])
    plt.scatter(Endpoints[:,i,0],Endpoints[:,i,1],color = "tab:"+colors[i])
```

### Influence of EGF/Notch Hypomorphs on cell fates :

We now study the joint influence of reduced level of EGF and Notch. This can again be tested in-vivo, where gene expression can be halved by removing a single allele of the gene. In our model, this can be done by changing the norm of $\vec{m}_1$ (for EGF) and of $\vec{m}_2$ (for Notch).

$\vec{m} = \vec{m}_0 + l_1 \vec{m}_1 + l_2 \vec{m}_2$

### __Question 8__

> Integrate and plot the trajectories of the five VPC for each hypomorph config listed bellow. Compare to the wild-type trajectories.

```python
from scipy.spatial import cKDTree

nsteps = 100
nit = 20
timepoints = np.linspace(0,1,nsteps)
dt = timepoints[1]

Startpoints = np.zeros((nit,5,2))
Endpoints   = np.zeros((nit,5,2))
Trajectories= np.zeros((nit,nsteps,5,2))

R4 = np.zeros((nsteps,2))
R5 = np.zeros((nsteps,2))
R6 = np.zeros((nsteps,2))
R7 = np.zeros((nsteps,2))
R8 = np.zeros((nsteps,2))

m1_norm_0=4.60
m2_norm_0=5.97

configs = [{'m1_norm':m1_norm_0,'m2_norm':m2_norm_0},
           {'m1_norm':m1_norm_0/2,'m2_norm':m2_norm_0},
           {'m1_norm':m1_norm_0,'m2_norm':m2_norm_0/2},
           {'m1_norm':m1_norm_0/2,'m2_norm':m2_norm_0/2}]

fig,ax = plt.subplots(1,4,figsize=(20,5))

for j,params in enumerate(configs) :
    m1_norm, m2_norm = params['m1_norm'], params['m2_norm']
    m1 = m1_norm * stationary[1] / np.linalg.norm(stationary[1])  # EGF signaling
    m2 = m2_norm * stationary[2] / np.linalg.norm(stationary[2])  # Notch signaling

    for i in range(nit):
        L1 = np.ones(5)
        L1[0]=L1[4]=gamma**2
        L1[1]=L1[3]=gamma
        L1*=l1_lin

        for n in range(0,nsteps):

            if n ==0 :
                sigma1 = sigmaonefunc(m0)
                R4[n] = np.random.normal(0,np.sqrt(2*D*tau**2),2) + sigma1
                R5[n] = np.random.normal(0,np.sqrt(2*D*tau**2),2) + sigma1
                R6[n] = np.random.normal(0,np.sqrt(2*D*tau**2),2) + sigma1
                R7[n] = np.random.normal(0,np.sqrt(2*D*tau**2),2) + sigma1
                R8[n] = np.random.normal(0,np.sqrt(2*D*tau**2),2) + sigma1
                continue

            #Notch signaling
            l2_p4 = alpha * L2(R4[n-1]) + L2(R5[n-1])
            l2_p5 = alpha * L2(R5[n-1]) + L2(R4[n-1]) + L2(R6[n-1])
            l2_p6 = alpha * L2(R6[n-1]) + L2(R5[n-1]) + L2(R7[n-1])
            l2_p7 = alpha * L2(R7[n-1]) + L2(R6[n-1]) + L2(R8[n-1])
            l2_p8 = alpha * L2(R8[n-1]) + L2(R7[n-1])

            sigma1 = sigmaonefunc(f(R4[n-1])+m0+m1*L1[0]+m2*l2_p4)
            eta = np.random.normal(0,np.sqrt(2*D*dt),2)
            R4[n]=R4[n-1]*(1-(dt/tau))+(dt/tau)*(sigma1) + eta

            sigma1 = sigmaonefunc(f(R5[n-1])+m0+m1*L1[1]+m2*l2_p5)
            eta = np.random.normal(0,np.sqrt(2*D*dt),2)
            R5[n]=R5[n-1]*(1-(dt/tau))+(dt/tau)*(sigma1) + eta

            sigma1 = sigmaonefunc(f(R6[n-1])+m0+m1*L1[2]+m2*l2_p6)
            eta = np.random.normal(0,np.sqrt(2*D*dt),2)
            R6[n]=R6[n-1]*(1-(dt/tau))+(dt/tau)*(sigma1) + eta

            sigma1 = sigmaonefunc(f(R7[n-1])+m0+m1*L1[3]+m2*l2_p7)
            eta = np.random.normal(0,np.sqrt(2*D*dt),2)
            R7[n]=R7[n-1]*(1-(dt/tau))+(dt/tau)*(sigma1) + eta

            sigma1 = sigmaonefunc(f(R8[n-1])+m0+m1*L1[4]+m2*l2_p8)
            eta = np.random.normal(0,np.sqrt(2*D*dt),2)
            R8[n]=R8[n-1]*(1-(dt/tau))+(dt/tau)*(sigma1) + eta

        Startpoints[i] = np.stack((R4[0],R5[0],R6[0],R7[0],R8[0]))
        Endpoints[i] = np.stack((R4[-1],R5[-1],R6[-1],R7[-1],R8[-1]))
        Trajectories[i] = np.stack((R4,R5,R6,R7,R8),axis=2).transpose(0,2,1)

    Tree = cKDTree(stationary)
    pts = Endpoints[:,[1,3],:].reshape(nit*2,2)
    dist, idx = Tree.query(pts)
    ax[j].hist(idx)
    ax[j].set_xlim(-1,3)
```

We can even go further and plot the frequency of \#2 fates of P5/7 progrenitors with Notch and EGF levels going from the WT levels to zero. This gives us a mutational landscape indicating the frequency of changes of fates of progenitors.

```python
from scipy.spatial import cKDTree
from tqdm import tqdm

nsteps = 100
nit = 20
timepoints = np.linspace(0,1,nsteps)
dt = timepoints[1]

Startpoints = np.zeros((nit,5,2))
Endpoints   = np.zeros((nit,5,2))
Trajectories= np.zeros((nit,nsteps,5,2))

R4 = np.zeros((nsteps,2))
R5 = np.zeros((nsteps,2))
R6 = np.zeros((nsteps,2))
R7 = np.zeros((nsteps,2))
R8 = np.zeros((nsteps,2))

m1_norm_0=4.60
m2_norm_0=5.97

configs = [{'m1_norm':m1_norm_0,'m2_norm':m2_norm_0},
           {'m1_norm':m1_norm_0/2,'m2_norm':m2_norm_0},
           {'m1_norm':m1_norm_0,'m2_norm':m2_norm_0/2},
           {'m1_norm':m1_norm_0/2,'m2_norm':m2_norm_0/2}]

xlist20 = np.linspace(0, 1, 20)
ylist20 = np.linspace(0, 1, 20)
X20, Y20 = np.meshgrid(xlist20, ylist20)
Z20 = np.zeros(X20.shape)

for a, b in tqdm(np.ndindex(X20.shape), total=X20.shape[0]*X20.shape[1]) :

    m1_norm, m2_norm = m1_norm_0*X20[a,b], m2_norm_0*Y20[a,b]
    m1 = m1_norm * stationary[1] / np.linalg.norm(stationary[1])  # EGF signaling
    m2 = m2_norm * stationary[2] / np.linalg.norm(stationary[2])  # Notch signaling

    for i in range(nit):
        L1 = np.ones(5)
        L1[0]=L1[4]=gamma**2
        L1[1]=L1[3]=gamma
        L1*=l1_lin

        for n in range(0,nsteps):

            if n ==0 :
                sigma1 = sigmaonefunc(m0)
                R4[n] = np.random.normal(0,np.sqrt(2*D*tau**2),2) + sigma1
                R5[n] = np.random.normal(0,np.sqrt(2*D*tau**2),2) + sigma1
                R6[n] = np.random.normal(0,np.sqrt(2*D*tau**2),2) + sigma1
                R7[n] = np.random.normal(0,np.sqrt(2*D*tau**2),2) + sigma1
                R8[n] = np.random.normal(0,np.sqrt(2*D*tau**2),2) + sigma1
                continue

            #Notch signaling
            l2_p4 = alpha * L2(R4[n-1]) + L2(R5[n-1])
            l2_p5 = alpha * L2(R5[n-1]) + L2(R4[n-1]) + L2(R6[n-1])
            l2_p6 = alpha * L2(R6[n-1]) + L2(R5[n-1]) + L2(R7[n-1])
            l2_p7 = alpha * L2(R7[n-1]) + L2(R6[n-1]) + L2(R8[n-1])
            l2_p8 = alpha * L2(R8[n-1]) + L2(R7[n-1])

            sigma1 = sigmaonefunc(f(R4[n-1])+m0+m1*L1[0]+m2*l2_p4)
            eta = np.random.normal(0,np.sqrt(2*D*dt),2)
            R4[n]=R4[n-1]*(1-(dt/tau))+(dt/tau)*(sigma1) + eta

            sigma1 = sigmaonefunc(f(R5[n-1])+m0+m1*L1[1]+m2*l2_p5)
            eta = np.random.normal(0,np.sqrt(2*D*dt),2)
            R5[n]=R5[n-1]*(1-(dt/tau))+(dt/tau)*(sigma1) + eta

            sigma1 = sigmaonefunc(f(R6[n-1])+m0+m1*L1[2]+m2*l2_p6)
            eta = np.random.normal(0,np.sqrt(2*D*dt),2)
            R6[n]=R6[n-1]*(1-(dt/tau))+(dt/tau)*(sigma1) + eta

            sigma1 = sigmaonefunc(f(R7[n-1])+m0+m1*L1[3]+m2*l2_p7)
            eta = np.random.normal(0,np.sqrt(2*D*dt),2)
            R7[n]=R7[n-1]*(1-(dt/tau))+(dt/tau)*(sigma1) + eta

            sigma1 = sigmaonefunc(f(R8[n-1])+m0+m1*L1[4]+m2*l2_p8)
            eta = np.random.normal(0,np.sqrt(2*D*dt),2)
            R8[n]=R8[n-1]*(1-(dt/tau))+(dt/tau)*(sigma1) + eta

        Startpoints[i] = np.stack((R4[0],R5[0],R6[0],R7[0],R8[0]))
        Endpoints[i] = np.stack((R4[-1],R5[-1],R6[-1],R7[-1],R8[-1]))

    Tree = cKDTree(stationary)
    pts = Endpoints[:,[1,3],:].reshape(nit*2,2)
    dist, idx = Tree.query(pts)
    freq = len(idx[idx==2])/len(idx)
    Z20[a,b]=freq

fig = plt.figure(figsize=(11,8))
heatmap = plt.imshow(Z20,plt.cm.jet)
plt.gca().set_aspect('equal', adjustable='box')
cbar = plt.colorbar(heatmap)
cbar.set_label('Frequency of #2 fate among P5/7 progenitors',**{'fontsize':'xx-large'},rotation=90)
plt.xlabel("EGF induction level $\Rightarrow$", {'fontsize':'xx-large'})
plt.ylabel("$\Leftarrow$ Notch induction level", {'fontsize':'xx-large'})
plt.xticks([], [])
plt.yticks([], [])
plt.title("Influence of the mutational landscape among Notch/EGF \n hypomorphs on fate frequencies of P5/7 progenitors",**{'fontsize':'x-large'})

```

# Conclusion

Development is a dynamical process with competing fates, and it is often hard to visualize the consequences of genetic perturbations. The phenotypic model presented here effectively codifies the genotype-to-phenotype map because the influence of each gene is generally limited to one parameter. Geometric reasoning provides a compact, non-redundant parameterization of signaling and fate specification.

The philosophy of starting with a simplified but global model for developmental patterning meshes well with basic biology : Signaling, commitment, and determination are discrete terms applied to a continuous process. It is unnatural to imagine one model for signaling and another for commitment and determination : the same genes are involved, and the regulation of competence by external signals would fit naturally within our description. A unified model also makes it very simple to incorporate noise, which is a prominent feature of any interesting mutant and of course affects the average trajectories as well as the probabilities of the final states. The existence of a predictive dynamic model should encourage experiments to measure crosses among the available alleles as well as temporal signaling perturbations in mutant backgrounds. Variability in the timing of fate determination is a strong consequence of the flow geometry and should also be measured.

The model studied describes the experiments and time window of C-Elegans VPC differentiation but not much more. There are no parameters that up-regulate receptors at the beginning of competence or shut down pathways after the first VPC division. From this model, we can say nothing about the differentiation that ensues near the fixed points, which clearly requires additional dimensions. However the fixed points are merely compact ways of creating the simplest parameterizations for the boundaries between them and enumerating the various flow topologies.


# Semantics: PrefScLTL.

In this notebook, we ensure that semantics of our proposed preference logic are sound.

Proposed semantics:
* $(w_1, w_2) \models \alpha_1~\trianglerighteq~\alpha_2$ iff either ($w_1 \models \alpha_1$ and $w_2 \models \alpha_2 \land \neg \alpha_1$) or ($w_1 \models \neg \alpha_1 \land \alpha_2$ and $w_2 \models \neg \alpha_1 \land \neg \alpha_2$)

We expect the remaining operator semantics to follow from this definition.
* $(w_1, w_2) \models \alpha_1~\triangleright~\alpha_2$ iff $(w_1, w_2) \models \alpha_1~\trianglerighteq~\alpha_2$ and $(w_1, w_2) \not\models \alpha_2~\trianglerighteq~\alpha_1$

* $(w_1, w_2) \models \alpha_1~\sim~\alpha_2$ iff $(w_1, w_2) \models \alpha_1~\trianglerighteq~\alpha_2$ and $(w_1, w_2) \models \alpha_2~\trianglerighteq~\alpha_1$

In what follows, we derive these semantics to ensure soundness of our definitions.

## Formulation

Every atomic preference formula induces four partitions of $\Sigma^\omega$. Correspondingly, we define 4 propositions each for $w_1, w_2$ to denote which scLTL formulas the words satisfy.

* `w1_00` means $w1$ satisfies $\neg \alpha_1$,  $\neg \alpha_2$.
* `w1_01` means $w1$ satisfies $\neg \alpha_1$,  $\alpha_2$.
* `w1_10` means $w1$ satisfies $\alpha_1$,  $\neg \alpha_2$.
* `w1_11` means $w1$ satisfies $\alpha_1$,  $\alpha_2$.

* `w2_00` means $w2$ satisfies $\neg \alpha_1$,  $\neg \alpha_2$.
* `w2_01` means $w2$ satisfies $\neg \alpha_1$,  $\alpha_2$.
* `w2_10` means $w2$ satisfies $\alpha_1$,  $\neg \alpha_2$.
* `w2_11` means $w2$ satisfies $\alpha_1$,  $\alpha_2$.

## Strict Preference

```python
from sympy import *
from sympy.logic import simplify_logic
from sympy.logic.boolalg import to_cnf, to_dnf, And, Or, Not, Implies, Equivalent
```

```python
w1_00, w1_01, w1_10, w1_11, w2_00, w2_01, w2_10, w2_11 = symbols('w1_00 w1_01 w1_10 w1_11 w2_00 w2_01 w2_10 w2_11')
```

**Constraint 1:** $w_1, w_2$ must be exactly one of the classes.

```python
w1_constraint1 = (w1_00 & ~w1_01 & ~w1_10 & ~w1_11) | \
                    (~w1_00 & w1_01 & ~w1_10 & ~w1_11) | \
                    (~w1_00 & ~w1_01 & w1_10 & ~w1_11) | \
                    (~w1_00 & ~w1_01 & ~w1_10 & w1_11)

w2_constraint1 = (w2_00 & ~w2_01 & ~w2_10 & ~w2_11) | \
                    (~w2_00 & w2_01 & ~w2_10 & ~w2_11) | \
                    (~w2_00 & ~w2_01 & w2_10 & ~w2_11) | \
                    (~w2_00 & ~w2_01 & ~w2_10 & w2_11)

w1_constraint1, w2_constraint1
```

    ((w1_00 & ~w1_01 & ~w1_10 & ~w1_11) | (w1_01 & ~w1_00 & ~w1_10 & ~w1_11) | (w1_10 & ~w1_00 & ~w1_01 & ~w1_11) | (w1_11 & ~w1_00 & ~w1_01 & ~w1_10),
     (w2_00 & ~w2_01 & ~w2_10 & ~w2_11) | (w2_01 & ~w2_00 & ~w2_10 & ~w2_11) | (w2_10 & ~w2_00 & ~w2_01 & ~w2_11) | (w2_11 & ~w2_00 & ~w2_01 & ~w2_10))

### Semantics (Attempt 1)

Proposed semantics:
* $(w_1, w_2) \models \alpha_1~\trianglerighteq~\alpha_2$ iff either ($w_1 \models \alpha_1$ and $w_2 \models \neg \alpha_1$) or ($w_1 \models \neg \alpha_1 \land \alpha_2$ and $w_2 \models \neg \alpha_1 $)

```python
weak_12 = (w1_10 | w1_11) | (w1_01 & (w2_01 | w2_00))
weak_12
```

$\displaystyle w_{1 10} \vee w_{1 11} \vee \left(w_{1 01} \wedge \left(w_{2 00} \vee w_{2 01}\right)\right)$

By definition, we have semantics when weak preference does not hold.

* $(w_1, w_2) \models \alpha_2~\trianglerighteq~\alpha_2$ iff either ($w_1 \models \alpha_2$ and $w_2 \models \neg \alpha_2$) or ($w_1 \models \neg \alpha_2 \land \alpha_1$ and $w_2 \models \neg \alpha_2 $)

* $(w_1, w_2) \models \alpha_2~\not\trianglerighteq~\alpha_2$ iff neither ($w_1 \models \alpha_2$ and $w_2 \models \neg \alpha_2$) nor ($w_1 \models \neg \alpha_2 \land \alpha_1$ and $w_2 \models \neg \alpha_2 $)

```python
weak_21 = (w1_01 | w1_11) | (w1_10 & (w2_00 | w2_10))
weak_21
```

$\displaystyle w_{1 01} \vee w_{1 11} \vee \left(w_{1 10} \wedge \left(w_{2 00} \vee w_{2 10}\right)\right)$

```python
strict_12 = weak_12 & ~weak_21 & w1_constraint1 & w2_constraint1
to_dnf(strict_12, simplify=True)
```

$\displaystyle \left(w_{1 10} \wedge w_{2 01} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 11} \wedge \neg w_{2 00} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 10} \wedge w_{2 11} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 11} \wedge \neg w_{2 00} \wedge \neg w_{2 01} \wedge \neg w_{2 10}\right)$

```python
indiff_12 = weak_12 & weak_21 & w1_constraint1 & w2_constraint1
to_dnf(indiff_12, simplify=True)
```

$\displaystyle \left(w_{1 01} \wedge w_{2 00} \wedge \neg w_{1 00} \wedge \neg w_{1 10} \wedge \neg w_{1 11} \wedge \neg w_{2 01} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 01} \wedge w_{2 01} \wedge \neg w_{1 00} \wedge \neg w_{1 10} \wedge \neg w_{1 11} \wedge \neg w_{2 00} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 10} \wedge w_{2 00} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 11} \wedge \neg w_{2 01} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 10} \wedge w_{2 10} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 11} \wedge \neg w_{2 00} \wedge \neg w_{2 01} \wedge \neg w_{2 11}\right) \vee \left(w_{1 11} \wedge w_{2 00} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 10} \wedge \neg w_{2 01} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 11} \wedge w_{2 01} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 10} \wedge \neg w_{2 00} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 11} \wedge w_{2 10} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 10} \wedge \neg w_{2 00} \wedge \neg w_{2 01} \wedge \neg w_{2 11}\right) \vee \left(w_{1 11} \wedge w_{2 11} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 10} \wedge \neg w_{2 00} \wedge \neg w_{2 01} \wedge \neg w_{2 10}\right)$

```python
incomp_12 = ~weak_12 & ~weak_21 & w1_constraint1 & w2_constraint1
to_dnf(incomp_12, simplify=True)
```

$\displaystyle \left(w_{1 00} \wedge w_{2 00} \wedge \neg w_{1 01} \wedge \neg w_{1 10} \wedge \neg w_{1 11} \wedge \neg w_{2 01} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 00} \wedge w_{2 01} \wedge \neg w_{1 01} \wedge \neg w_{1 10} \wedge \neg w_{1 11} \wedge \neg w_{2 00} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 00} \wedge w_{2 10} \wedge \neg w_{1 01} \wedge \neg w_{1 10} \wedge \neg w_{1 11} \wedge \neg w_{2 00} \wedge \neg w_{2 01} \wedge \neg w_{2 11}\right) \vee \left(w_{1 00} \wedge w_{2 11} \wedge \neg w_{1 01} \wedge \neg w_{1 10} \wedge \neg w_{1 11} \wedge \neg w_{2 00} \wedge \neg w_{2 01} \wedge \neg w_{2 10}\right)$

## Semantics (Attempt 2)

Under semantics of attempt 1, any word $w_1$ that satisfies $\alpha_1 \land \neg \alpha_2$ is considered indifferent to any word $w_2$ that satisfies $\alpha_1 \land \alpha_2$. In the second attempt, we treat them incomparable.

Proposed semantics:
* $(w_1, w_2) \models \alpha_1~\trianglerighteq~\alpha_2$ iff
    either ($w_1 \models \alpha_1 \land \neg \alpha_2$ and $w_2 \models \neg \alpha_1 \lor \neg \alpha_2$)
    or ($w_1 \models \alpha_1 \land \alpha_2$ and $w_2 \models \neg \alpha_1 \lor \alpha_2$)
    or ($w_1 \models \neg \alpha_1 \land \alpha_2$ and $w_2 \models \neg \alpha_1$)

Hence, the relation holds under three cases:
* $w_1 \in \{10\}$ and $w_2 \in \{10, 01, 00\}$
* $w_1 \in \{11\}$ and $w_2 \in \{11, 01, 00\}$
* $w_1 \in \{01\}$ and $w_2 \in \{01, 00\}$

```python
weak_12 = (w1_10 & (w2_10 | w2_01 | w2_00)) | \
          (w1_11 & (w2_11 | w2_01 | w2_00)) | \
          (w1_01 & (w2_01 | w2_00))
weak_12
```

$\displaystyle \left(w_{1 01} \wedge \left(w_{2 00} \vee w_{2 01}\right)\right) \vee \left(w_{1 10} \wedge \left(w_{2 00} \vee w_{2 01} \vee w_{2 10}\right)\right) \vee \left(w_{1 11} \wedge \left(w_{2 00} \vee w_{2 01} \vee w_{2 11}\right)\right)$

* $(w_1, w_2) \models \alpha_2~\trianglerighteq~\alpha_1$ iff
    either ($w_1 \models \alpha_2 \land \neg \alpha_1$ and $w_2 \models \neg \alpha_2 \lor \neg \alpha_1$)
    or ($w_1 \models \alpha_2 \land \alpha_1$ and $w_2 \models \neg \alpha_2 \lor \alpha_1$)
    or ($w_1 \models \neg \alpha_2 \land \alpha_1$ and $w_2 \models \neg \alpha_2$)

Hence, the relation holds under three cases:
* $w_1 \in \{01\}$ and $w_2 \in \{01, 10, 00\}$
* $w_1 \in \{11\}$ and $w_2 \in \{11, 10, 00\}$
* $w_1 \in \{10\}$ and $w_2 \in \{10, 00\}$

```python
weak_21 = (w1_01 & (w2_01 | w2_10 | w2_00)) | \
          (w1_11 & (w2_11 | w2_10 | w2_00)) | \
          (w1_10 & (w2_10 | w2_00))
weak_21
```

$\displaystyle \left(w_{1 01} \wedge \left(w_{2 00} \vee w_{2 01} \vee w_{2 10}\right)\right) \vee \left(w_{1 10} \wedge \left(w_{2 00} \vee w_{2 10}\right)\right) \vee \left(w_{1 11} \wedge \left(w_{2 00} \vee w_{2 10} \vee w_{2 11}\right)\right)$

```python
strict_12 = weak_12 & ~weak_21 & w1_constraint1 & w2_constraint1
to_dnf(strict_12, simplify=True)
```

$\displaystyle \left(w_{1 10} \wedge w_{2 01} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 11} \wedge \neg w_{2 00} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 11} \wedge w_{2 01} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 10} \wedge \neg w_{2 00} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right)$

```python
indiff_12 = weak_12 & weak_21 & w1_constraint1 & w2_constraint1
to_dnf(indiff_12, simplify=True)
```

$\displaystyle \left(w_{1 01} \wedge w_{2 00} \wedge \neg w_{1 00} \wedge \neg w_{1 10} \wedge \neg w_{1 11} \wedge \neg w_{2 01} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 01} \wedge w_{2 01} \wedge \neg w_{1 00} \wedge \neg w_{1 10} \wedge \neg w_{1 11} \wedge \neg w_{2 00} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 10} \wedge w_{2 00} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 11} \wedge \neg w_{2 01} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 10} \wedge w_{2 10} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 11} \wedge \neg w_{2 00} \wedge \neg w_{2 01} \wedge \neg w_{2 11}\right) \vee \left(w_{1 11} \wedge w_{2 00} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 10} \wedge \neg w_{2 01} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 11} \wedge w_{2 11} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 10} \wedge \neg w_{2 00} \wedge \neg w_{2 01} \wedge \neg w_{2 10}\right)$

```python
incomp_12 = ~weak_12 & ~weak_21 & w1_constraint1 & w2_constraint1
to_dnf(incomp_12, simplify=True)
```

$\displaystyle \left(w_{1 00} \wedge w_{2 00} \wedge \neg w_{1 01} \wedge \neg w_{1 10} \wedge \neg w_{1 11} \wedge \neg w_{2 01} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 00} \wedge w_{2 01} \wedge \neg w_{1 01} \wedge \neg w_{1 10} \wedge \neg w_{1 11} \wedge \neg w_{2 00} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 00} \wedge w_{2 10} \wedge \neg w_{1 01} \wedge \neg w_{1 10} \wedge \neg w_{1 11} \wedge \neg w_{2 00} \wedge \neg w_{2 01} \wedge \neg w_{2 11}\right) \vee \left(w_{1 00} \wedge w_{2 11} \wedge \neg w_{1 01} \wedge \neg w_{1 10} \wedge \neg w_{1 11} \wedge \neg w_{2 00} \wedge \neg w_{2 01} \wedge \neg w_{2 10}\right) \vee \left(w_{1 01} \wedge w_{2 11} \wedge \neg w_{1 00} \wedge \neg w_{1 10} \wedge \neg w_{1 11} \wedge \neg w_{2 00} \wedge \neg w_{2 01} \wedge \neg w_{2 10}\right) \vee \left(w_{1 10} \wedge w_{2 11} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 11} \wedge \neg w_{2 00} \wedge \neg w_{2 01} \wedge \neg w_{2 10}\right)$

## Semantics (Attempt 3)

The semantics of attempt 2 are ill-formed. Two words can simultaneously be strictly preferred `and` indifferent!

We now propose new semantics directly for strict and indifference. And, derive that for weak and incomparability.

Proposed semantics:
* $(w_1, w_2) \models \alpha_1~\triangleright~\alpha_2$ iff
    either ($w_1 \models \alpha_1 \land \neg \alpha_2$ and $w_2 \models \neg \alpha_1$)
    or ($w_1 \models \alpha_1 \land \alpha_2$ and $w_2 \models \neg \alpha_1 \land \neg \alpha_2$)
    or ($w_1 \models \neg \alpha_1 \land \alpha_2$ and $w_2 \models \neg \alpha_1 \land \neg \alpha_2$)

Hence, the relation holds under three cases:
* $w_1 \in \{10\}$ and $w_2 \in \{01, 00\}$
* $w_1 \in \{11\}$ and $w_2 \in \{00\}$
* $w_1 \in \{01\}$ and $w_2 \in \{00\}$

```python
strict_12 = (w1_10 & (w2_01 | w2_00)) | \
              (w1_11 & (w2_00)) | \
              (w1_01 & (w2_00))
to_dnf(strict_12 & w1_constraint1 & w2_constraint1, simplify=True)
```

$\displaystyle \left(w_{1 01} \wedge w_{2 00} \wedge \neg w_{1 00} \wedge \neg w_{1 10} \wedge \neg w_{1 11} \wedge \neg w_{2 01} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 10} \wedge w_{2 00} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 11} \wedge \neg w_{2 01} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 10} \wedge w_{2 01} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 11} \wedge \neg w_{2 00} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 11} \wedge w_{2 00} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 10} \wedge \neg w_{2 01} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right)$

```python
indiff_12 = (w1_00 & w2_00) | \
              (w1_01 & w2_01) | \
              (w1_10 & w2_10) | \
              (w1_11 & w2_11)
to_dnf(indiff_12 & w1_constraint1 & w2_constraint1, simplify=True)
```

$\displaystyle \left(w_{1 00} \wedge w_{2 00} \wedge \neg w_{1 01} \wedge \neg w_{1 10} \wedge \neg w_{1 11} \wedge \neg w_{2 01} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 01} \wedge w_{2 01} \wedge \neg w_{1 00} \wedge \neg w_{1 10} \wedge \neg w_{1 11} \wedge \neg w_{2 00} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 10} \wedge w_{2 10} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 11} \wedge \neg w_{2 00} \wedge \neg w_{2 01} \wedge \neg w_{2 11}\right) \vee \left(w_{1 11} \wedge w_{2 11} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 10} \wedge \neg w_{2 00} \wedge \neg w_{2 01} \wedge \neg w_{2 10}\right)$

```python
weak_12 = strict_12 | indiff_12
to_dnf(weak_12 & w1_constraint1 & w2_constraint1, simplify=True)
```

$\displaystyle \left(w_{1 00} \wedge w_{2 00} \wedge \neg w_{1 01} \wedge \neg w_{1 10} \wedge \neg w_{1 11} \wedge \neg w_{2 01} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 01} \wedge w_{2 00} \wedge \neg w_{1 00} \wedge \neg w_{1 10} \wedge \neg w_{1 11} \wedge \neg w_{2 01} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 01} \wedge w_{2 01} \wedge \neg w_{1 00} \wedge \neg w_{1 10} \wedge \neg w_{1 11} \wedge \neg w_{2 00} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 10} \wedge w_{2 00} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 11} \wedge \neg w_{2 01} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 10} \wedge w_{2 01} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 11} \wedge \neg w_{2 00} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 10} \wedge w_{2 10} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 11} \wedge \neg w_{2 00} \wedge \neg w_{2 01} \wedge \neg w_{2 11}\right) \vee \left(w_{1 11} \wedge w_{2 00} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 10} \wedge \neg w_{2 01} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 11} \wedge w_{2 11} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 10} \wedge \neg w_{2 00} \wedge \neg w_{2 01} \wedge \neg w_{2 10}\right)$

```python
strict_21 = (w1_01 & (w2_10 | w2_00)) | \
              (w1_11 & (w2_00)) | \
              (w1_10 & (w2_00))
to_dnf(strict_21 & w1_constraint1 & w2_constraint1, simplify=True)
```

$\displaystyle \left(w_{1 01} \wedge w_{2 00} \wedge \neg w_{1 00} \wedge \neg w_{1 10} \wedge \neg w_{1 11} \wedge \neg w_{2 01} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 01} \wedge w_{2 10} \wedge \neg w_{1 00} \wedge \neg w_{1 10} \wedge \neg w_{1 11} \wedge \neg w_{2 00} \wedge \neg w_{2 01} \wedge \neg w_{2 11}\right) \vee \left(w_{1 10} \wedge w_{2 00} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 11} \wedge \neg w_{2 01} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 11} \wedge w_{2 00} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 10} \wedge \neg w_{2 01} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right)$

```python
incomp_12 = ~strict_12 & ~strict_21 & ~indiff_12 & w1_constraint1 & w2_constraint1
to_dnf(incomp_12, simplify=True)
```

$\displaystyle \left(w_{1 00} \wedge w_{2 01} \wedge \neg w_{1 01} \wedge \neg w_{1 10} \wedge \neg w_{1 11} \wedge \neg w_{2 00} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 00} \wedge w_{2 10} \wedge \neg w_{1 01} \wedge \neg w_{1 10} \wedge \neg w_{1 11} \wedge \neg w_{2 00} \wedge \neg w_{2 01} \wedge \neg w_{2 11}\right) \vee \left(w_{1 00} \wedge w_{2 11} \wedge \neg w_{1 01} \wedge \neg w_{1 10} \wedge \neg w_{1 11} \wedge \neg w_{2 00} \wedge \neg w_{2 01} \wedge \neg w_{2 10}\right) \vee \left(w_{1 01} \wedge w_{2 11} \wedge \neg w_{1 00} \wedge \neg w_{1 10} \wedge \neg w_{1 11} \wedge \neg w_{2 00} \wedge \neg w_{2 01} \wedge \neg w_{2 10}\right) \vee \left(w_{1 10} \wedge w_{2 11} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 11} \wedge \neg w_{2 00} \wedge \neg w_{2 01} \wedge \neg w_{2 10}\right) \vee \left(w_{1 11} \wedge w_{2 01} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 10} \wedge \neg w_{2 00} \wedge \neg w_{2 10} \wedge \neg w_{2 11}\right) \vee \left(w_{1 11} \wedge w_{2 10} \wedge \neg w_{1 00} \wedge \neg w_{1 01} \wedge \neg w_{1 10} \wedge \neg w_{2 00} \wedge \neg w_{2 01} \wedge \neg w_{2 11}\right)$