From 21c72d7eb86b0ef47ff45529eea976f4d8cbdfee Mon Sep 17 00:00:00 2001
From: Mahmoud Bentriou <mahmoud.bentriou@centralesupelec.fr>
Date: Fri, 20 Nov 2020 21:19:46 +0100
Subject: [PATCH] Add of several useful functions for data access for
 Trajectory object.

First implementation of LHA.
Synchronized simulation not yet tested.
Reading of trajectories with LHA seems to work with one test basted on
simulations that checks that the last value is well read.
---
 automata/automaton_F.jl                       |  95 +++++++++++
 core/MarkovProcesses.jl                       |  12 +-
 core/lha.jl                                   | 161 ++++++++++++++++++
 core/model.jl                                 |  72 +++++++-
 core/trajectory.jl                            |  51 ++++--
 tests/automata/automaton_F.jl                 |  12 ++
 .../automata/read_trajectory_last_state_F.jl  |  32 ++++
 tests/run_all.jl                              |   1 +
 tests/run_automata.jl                         |   7 +
 tests/run_bench.jl                            |  16 ++
 10 files changed, 436 insertions(+), 23 deletions(-)
 create mode 100644 automata/automaton_F.jl
 create mode 100644 core/lha.jl
 create mode 100644 tests/automata/automaton_F.jl
 create mode 100644 tests/automata/read_trajectory_last_state_F.jl
 create mode 100644 tests/run_automata.jl
 create mode 100644 tests/run_bench.jl

diff --git a/automata/automaton_F.jl b/automata/automaton_F.jl
new file mode 100644
index 0000000..38c2c3e
--- /dev/null
+++ b/automata/automaton_F.jl
@@ -0,0 +1,95 @@
+
+function create_automaton_F(m::ContinuousTimeModel, x1::Float64, x2::Float64, t1::Float64, t2::Float64, str_obs::String)
+    # Locations
+    l_loc = ["l0", "l1", "l2", "l3"]
+
+    ## Invariant predicates
+    true_inv_predicate = (A::LHA, S:: StateLHA) -> return true 
+    Λ_F = Dict("l0" => true_inv_predicate, "l1" => true_inv_predicate,
+               "l2" => true_inv_predicate, "l3" => true_inv_predicate)
+    
+    ## Init and final loc
+    l_loc_init = ["l0"]
+    l_loc_final = ["l2"]
+
+    #S.n <=> S.l_var[A.map_var_automaton_idx["n"]] 
+    #P <=> xn[map_var_model_idx[l_ctes[str_O]] with str_O = "P". On stock str_O dans l_ctes
+    
+    ## Map of automaton variables
+    map_var_automaton_idx = Dict{VariableAutomaton,Int}("n" => 1, "d" => 2)
+
+    ## Flow of variables
+    l_flow = Dict{VariableAutomaton,Vector{Float64}}("l0" => [0.0,0.0], 
+                                                     "l1" => [0.0,0.0], 
+                                                     "l2" => [0.0,0.0], 
+                                                     "l3" => [0.0,0.0])
+
+    ## Edges
+    map_edges = Dict{Tuple{Location,Location}, Vector{Edge}}()
+
+    # l0 loc : we construct  the edges of the form l0 => (..)
+    # "cc" as check_constraints
+    tuple = ("l0", "l1")
+    cc_aut_F_l0l1_1(A::LHA, S::StateLHA) = true
+    us_aut_F_l0l1_1!(A::LHA, S::StateLHA, x::SubArray{Int,1}) = 
+        (S.loc = "l1"; S["d"] = Inf; S["n"] = get_value(A, x, str_obs))
+    edge1 = Edge([nothing], cc_aut_F_l0l1_1, us_aut_F_l0l1_1!)
+    map_edges[tuple] = [edge1]
+
+    # l1 loc : we construct  the edges of the form l1 => (..)
+    tuple = ("l1", "l2")
+    cc_aut_F_l1l2_1(A::LHA, S::StateLHA) = 
+        (A.l_ctes["x1"] <= S["n"] <= A.l_ctes["x2"]) && (A.l_ctes["t1"] <= S.time <= A.l_ctes["t2"])
+    us_aut_F_l1l2_1!(A::LHA, S::StateLHA, x::SubArray{Int,1}) = (S["d"] = 0; S.loc = "l2")
+    edge1 = Edge([nothing], cc_aut_F_l1l2_1, us_aut_F_l1l2_1!)
+
+    cc_aut_F_l1l2_2(A::LHA, S::StateLHA) = (S["d"] > 0 && S.time > A.l_ctes["t2"])
+    us_aut_F_l1l2_2!(A::LHA, S::StateLHA, x::SubArray{Int,1}) = (S.loc = "l2")
+    edge2 = Edge([nothing], cc_aut_F_l1l2_2, us_aut_F_l1l2_2!)
+    
+    cc_aut_F_l1l2_3(A::LHA, S::StateLHA) = (S["d"] == 0 && S.time >= A.l_ctes["t1"])
+    us_aut_F_l1l2_3!(A::LHA, S::StateLHA, x::SubArray{Int,1}) = (S.loc = "l2")
+    edge3 = Edge([nothing], cc_aut_F_l1l2_3, us_aut_F_l1l2_3!)
+    
+    map_edges[tuple] = [edge1, edge2, edge3]
+
+    tuple = ("l1", "l3")
+    cc_aut_F_l1l3_1(A::LHA, S::StateLHA) = (A.l_ctes["x1"] <= S["n"] <= A.l_ctes["x2"])
+    us_aut_F_l1l3_1!(A::LHA, S::StateLHA, x::SubArray{Int,1}) = (S["d"] = 0; S.loc = "l3")
+    edge1 = Edge([nothing], cc_aut_F_l1l3_1, us_aut_F_l1l3_1!)
+    
+    cc_aut_F_l1l3_2(A::LHA, S::StateLHA) = 
+        (A.l_ctes["x1"] > S["n"] || S["n"] > A.l_ctes["x2"]) && (S.time < A.l_ctes["t1"])
+    us_aut_F_l1l3_2!(A::LHA, S::StateLHA, x::SubArray{Int,1}) = 
+        (S["d"] = min(sqrt((S.time - A.l_ctes["t1"])^2 + (S["n"] - A.l_ctes["x2"])^2), 
+                      sqrt((S.time - A.l_ctes["t1"])^2 + (S["n"] - A.l_ctes["x1"])^2)); S.loc = "l3")
+    edge2 = Edge([nothing], cc_aut_F_l1l3_2, us_aut_F_l1l3_2!)
+
+    cc_aut_F_l1l3_3(A::LHA, S::StateLHA) = 
+        (A.l_ctes["x1"] > S["n"] || S["n"] > A.l_ctes["x2"]) && (A.l_ctes["t1"] <= S.time <= A.l_ctes["t2"])
+    us_aut_F_l1l3_3!(A::LHA, S::StateLHA, x::SubArray{Int,1}) = 
+        (S["d"] = min(S["d"], min(abs(S["n"] - A.l_ctes["x1"]), abs(S["n"] - A.l_ctes["x2"]))); S.loc = "l3")
+    edge3 = Edge([nothing], cc_aut_F_l1l3_3, us_aut_F_l1l3_3!)
+    map_edges[tuple] = [edge1, edge2, edge3]
+
+    # l3 loc
+    tuple = ("l3", "l1")
+    cc_aut_F_l3l1_1(A::LHA, S::StateLHA) = true
+    us_aut_F_l3l1_1!(A::LHA, S::StateLHA, x::SubArray{Int,1}) = (S["n"] = get_value(A, x, str_obs); S.loc = "l1")
+    edge1 = Edge(["ALL"], cc_aut_F_l3l1_1, us_aut_F_l3l1_1!)
+    tuple = ("l3", "l2")
+    cc_aut_F_l3l2_1(A::LHA, S::StateLHA) = (S.time >= A.l_ctes["t2"])
+    us_aut_F_l3l2_1!(A::LHA, S::StateLHA, x::SubArray{Int,1}) = (S["n"] = get_value(A, x, str_obs); S.loc = "l2")
+    edge2 = Edge([nothing], cc_aut_F_l3l2_1, us_aut_F_l3l2_1!)
+    map_edges[tuple] = [edge1, edge2]
+
+    ## Constants
+    l_ctes = Dict{String,Float64}("x1" => x1, "x2" => x2, "t1" => t1, "t2" => t2)
+
+    A = LHA(m.l_transitions, l_loc, Λ_F, l_loc_init, l_loc_final, 
+            map_var_automaton_idx, l_flow, map_edges, l_ctes, m.map_var_idx)
+    return A
+end
+
+export create_automaton_F
+
diff --git a/core/MarkovProcesses.jl b/core/MarkovProcesses.jl
index e9d2614..dfb23e6 100644
--- a/core/MarkovProcesses.jl
+++ b/core/MarkovProcesses.jl
@@ -1,6 +1,7 @@
 module MarkovProcesses
 
-import Base: +, -, getfield, getindex, ImmutableDict
+import Base: +, -, *
+import Base: copy, getfield, getindex, lastindex, setindex!, getproperty, setproperty!
 
 export Model, ContinuousTimeModel, DiscreteTimeModel
 export simulate, set_param!, get_param, set_observed_var!
@@ -10,8 +11,15 @@ include("model.jl")
 
 export Observations, AbstractTrajectory, Trajectory
 export +, -, δ, dist_lp
-export get_obs_var, length_states, length_obs_var, is_bounded, times, transitions
+export get_obs_var, length_states, length_obs_var, get_state_from_time 
+export is_bounded, times, transitions
+export check_consistency, is_steadystate
 include("trajectory.jl")
 
+export LHA, StateLHA, Edge
+export init_state, next_state!, read_trajectory
+export load_automaton, get_index, get_value, length_var
+include("lha.jl")
+
 end
 
diff --git a/core/lha.jl b/core/lha.jl
new file mode 100644
index 0000000..c3a1359
--- /dev/null
+++ b/core/lha.jl
@@ -0,0 +1,161 @@
+
+const Location = String
+const VariableAutomaton = String
+
+struct Edge
+    transitions::Vector{Transition}
+    check_constraints::Function
+    update_state!::Function
+end
+
+struct LHA
+    l_transitions::Vector{Transition}
+    l_loc::Vector{Location} 
+    Λ::Dict{Location,Function}
+    l_loc_init::Vector{Location}
+    l_loc_final::Vector{Location}
+    map_var_automaton_idx::Dict{VariableAutomaton,Int} # nvar keys : str_var => idx in l_var
+    l_flow::Dict{Location,Vector{Float64}} # output of length nvar
+    map_edges::Dict{Tuple{Location,Location},Vector{Edge}}
+    l_ctes::Dict{String,Float64}
+    map_var_model_idx::Dict{String,Int} # of dim d (of a model)
+end
+
+LHA(A::LHA, map_var::Dict{String,Int}) = LHA(A.l_transitions, A.l_loc, A.Λ, 
+                                             A.l_loc_init, A.l_loc_final, A.map_var_automaton_idx, A.l_flow,
+                                             A.map_edges, A.l_ctes, map_var)
+length_var(A::LHA) = length(A.map_var_automaton_idx)
+get_value(A::LHA, x::SubArray{Int,1}, var::String) = x[A.map_var_model_idx[var]]
+load_automaton(automaton::String) = include(get_module_path() * "/automata/$(automaton).jl")
+
+mutable struct StateLHA
+    A::LHA
+    loc::Location
+    l_var::Vector{Float64}
+    time::Float64
+end
+
+copy(S::StateLHA) = StateLHA(S.A, S.loc, S.l_var, S.time)
+# Not overring getproperty, setproperty to avoid a conversion Symbol => String for the dict key
+getindex(S::StateLHA, var::VariableAutomaton) = (S.l_var)[(S.A).map_var_automaton_idx[var]]
+setindex!(S::StateLHA, val::Float64, var::VariableAutomaton) = (S.l_var)[(S.A).map_var_automaton_idx[var]] = val
+setindex!(S::StateLHA, val::Int, var::VariableAutomaton) = (S.l_var)[(S.A).map_var_automaton_idx[var]] = convert(Float64, val)
+function Base.show(io::IO, S::StateLHA)
+    print(io, "State of LHA\n")
+    print(io, "- location: $(S.loc)\n")
+    print(io, "- time: $(S.time)\n")
+    print(io, "- variables:\n")
+    for (var, idx) in (S.A).map_var_automaton_idx
+        print(io, "* $var = $(S.l_var[idx])\n")
+    end
+end
+
+function init_state(A::LHA, x0::SubArray{Int,1}, t0::Float64)
+    S0 = StateLHA(A, "", zeros(length_var(A)), t0)
+    for loc in A.l_loc_init
+        if A.Λ[loc](A,S0) 
+            S0.loc = loc
+            break
+        end
+    end
+    return S0
+end
+
+function next_state!(Snplus1::StateLHA, A::LHA, 
+                    xnplus1::SubArray{Int,1}, tnplus1::Float64, tr_nplus1::Transition, 
+                    Sn::StateLHA; verbose = false)
+    for i in eachindex(Snplus1.l_var)
+        Snplus1.l_var[i] += (A.l_flow[Sn.loc])[i]*(tnplus1 - Sn.time) 
+    end
+    Snplus1.time = tnplus1
+
+    # En fait d'apres observation de Cosmos, après qu'on ait lu la transition on devrait stop.
+    edge_candidates = Edge[]
+    tuple_candidates = Tuple{Location, Location}[]
+    first_round = true
+    detected_event = (tr_nplus1 == nothing) ? true : false
+    turns = 1
+    while first_round || !detected_event || length(edge_candidates) > 0
+        edge_candidates = Edge[]
+        tuple_candidates = Tuple{Location,Location}[]
+        current_loc = Snplus1.loc
+        if verbose
+            @show turns
+        end
+        # Save all edges that satisfies transition predicate (synchronous or autonomous)
+        for loc in A.l_loc
+            tuple_edges = (current_loc, loc)
+            if haskey(A.map_edges, tuple_edges)
+                for edge in A.map_edges[tuple_edges]
+                    if edge.check_constraints(A, Snplus1)
+                        push!(edge_candidates, edge)
+                        push!(tuple_candidates, tuple_edges)
+                    end
+                end
+            end
+        end
+        # Search the one we must chose
+        ind_edge = 0
+        for (i, edge) in enumerate(edge_candidates)
+            if edge.transitions[1] == nothing
+                ind_edge = i
+                break
+            end
+            if first_round || !detected_event
+                if (length(edge.transitions) == 1 && tr_nplus1 != nothing && edge.transitions[1] == "ALL") || 
+                    (tr_nplus1 in edge.transitions)
+                    detected_event = true
+                    ind_edge = i
+                end
+            end
+        end
+        # Update the state with the chosen one (if it exists)
+        if ind_edge > 0
+            edge_candidates[ind_edge].update_state!(A, Snplus1, xnplus1)
+            # Should add something like if edges_candidates[ind_edge].transition != nohting break end ??
+        end
+        if verbose
+            @show first_round, detected_event
+            @show tnplus1, tr_nplus1, xnplus1
+            @show ind_edge
+            @show edge_candidates
+            @show tuple_candidates
+            @show Snplus1
+        end
+        if ind_edge == 0 break end
+        # For debug
+        if turns > 10
+            println("Turns, Bad behavior of region2 automaton")
+            @show first_round, detected_event
+            @show length(edge_candidates)
+            @show tnplus1, tr_nplus1, xnplus1
+            @show edge_candidates
+            for edge in edge_candidates
+                @show edge.check_constraints(A, Snplus1)
+            end
+            error("Unpredicted behavior automaton F v2")
+        end
+        turns += 1
+        first_round = false
+    end
+end
+
+# For debug purposes
+function read_trajectory(A::LHA, σ::Trajectory; verbose = false)
+    A_new = LHA(A, (σ.m)._map_obs_var_idx)
+    l_t = times(σ)
+    l_tr = transitions(σ)
+    Sn = init_state(A_new, σ[1], l_t[1])
+    Snplus1 = copy(Sn)
+    if verbose println("Init: ") end
+    if verbose @show Sn end
+    for n in 1:length_states(σ)
+        next_state!(Snplus1, A_new, σ[n], l_t[n], l_tr[n], Sn; verbose = verbose)
+        if Snplus1.loc in A_new.l_loc_final 
+            break 
+        end
+        Sn = Snplus1 
+    end
+    return Sn
+end
+
diff --git a/core/model.jl b/core/model.jl
index e2b23b0..6b46148 100644
--- a/core/model.jl
+++ b/core/model.jl
@@ -3,14 +3,16 @@ import StaticArrays: SVector
 
 abstract type Model end 
 abstract type DiscreteTimeModel <: Model end 
+const Transition = Union{String,Nothing}
 
+# Model types
 mutable struct ContinuousTimeModel <: Model
     d::Int # state space dim
     k::Int # parameter space dim
     map_var_idx::Dict{String,Int} # maps str to full state space
     _map_obs_var_idx::Dict{String,Int} # maps str to observed state space
     map_param_idx::Dict{String,Int} # maps str in parameter space
-    l_name_transitions::Vector{String}
+    l_transitions::Vector{Transition}
     p::Vector{Float64}
     x0::Vector{Int}
     t0::Float64
@@ -22,7 +24,7 @@ mutable struct ContinuousTimeModel <: Model
     buffer_size::Int
 end
 
-function ContinuousTimeModel(d::Int, k::Int, map_var_idx::Dict, map_param_idx::Dict, l_name_transitions::Vector{String}, 
+function ContinuousTimeModel(d::Int, k::Int, map_var_idx::Dict, map_param_idx::Dict, l_transitions::Vector{String}, 
               p::Vector{Float64}, x0::Vector{Int}, t0::Float64, 
               f!::Function, is_absorbing::Function; 
               g::Vector{String} = keys(map_var_idx), time_bound::Float64 = Inf, buffer_size::Int = 10)
@@ -41,9 +43,17 @@ function ContinuousTimeModel(d::Int, k::Int, map_var_idx::Dict, map_param_idx::D
         @warn "You have possibly redefined a function Model.is_absorbing used in a previously instantiated model."
     end
 
-    return ContinuousTimeModel(d, k, map_var_idx, _map_obs_var_idx, map_param_idx, l_name_transitions, p, x0, t0, f!, g, _g_idx, is_absorbing, time_bound, buffer_size)
+    return ContinuousTimeModel(d, k, map_var_idx, _map_obs_var_idx, map_param_idx, l_transitions, p, x0, t0, f!, g, _g_idx, is_absorbing, time_bound, buffer_size)
 end
 
+#=
+mutable struct SynchronizedModel
+    m::ContinuousTimeModel
+    automaton::LHA
+end
+=#
+
+# Simulation
 function simulate(m::ContinuousTimeModel)
     # trajectory fields
     full_values = Matrix{Int}(undef, 1, m.d)
@@ -89,6 +99,62 @@ function simulate(m::ContinuousTimeModel)
     return Trajectory(m, values, times, transitions)
 end
 
+#=
+function simulate(product::SynchronizedModel)
+    # trajectory fields
+    m = product.m
+    A = product.automaton
+    full_values = Matrix{Int}(undef, 1, m.d)
+    full_values[1,:] = m.x0
+    times = Float64[m.t0]
+    transitions = Union{String,Nothing}[nothing]
+    reshaped_x0 = view(reshape(m.x0, 1, m.d), 1, :) # View for type stability
+    S0 = init_state(A, reshaped_x0, t0) 
+    # values at time n
+    n = 0
+    xn = reshaped_x0 
+    tn = m.t0
+    Sn = copy(S0)
+    # at time n+1
+    mat_x = zeros(Int, m.buffer_size, m.d)
+    l_t = zeros(Float64, m.buffer_size)
+    l_tr = Vector{String}(undef, m.buffer_size)
+    is_absorbing::Bool = m.is_absorbing(m.p,xn)
+    Snplus1 = copy(Sn)
+    while !is_absorbing && (tn < m.time_bound)
+        i = 0
+        while i < m.buffer_size && !is_absorbing && (tn < m.time_bound)
+            i += 1
+            m.f!(mat_x, l_t, l_tr, i, xn, tn, m.p)
+            xn = view(mat_x, i, :)
+            tn = l_t[i]
+            tr_n = l_tr[n]
+            A.next_state!(Snplus1, A, xn, tn, tr_n, Sn)
+            Sn = Snplus1
+            is_absorbing = m.is_absorbing(m.p,xn)
+        end
+        full_values = vcat(full_values, view(mat_x, 1:i, :))
+        append!(times, view(l_t, 1:i))
+        append!(transitions,  view(l_tr, 1:i))
+        n += i
+        is_absorbing = m.is_absorbing(m.p,xn)
+    end
+    if is_bounded(m)
+        if times[end] > m.time_bound
+            full_values[end,:] = full_values[end-1,:]
+            times[end] = m.time_bound
+            transitions[end] = nothing
+        else
+            full_values = vcat(full_values, reshape(full_values[end,:], 1, m.d))
+            push!(times, m.time_bound)
+            push!(transitions, nothing)
+        end
+    end
+    values = view(full_values, :, m._g_idx)
+    return Trajectory(m, values, times, transitions)
+end
+=#
+
 function simulate(m::ContinuousTimeModel, n::Int)
     obs = ContinuousObservations(undef, n)
     for i = 1:n
diff --git a/core/trajectory.jl b/core/trajectory.jl
index 6f8ce4a..37e9697 100644
--- a/core/trajectory.jl
+++ b/core/trajectory.jl
@@ -6,7 +6,7 @@ struct Trajectory <: AbstractTrajectory
     m::ContinuousTimeModel
     values::Matrix{Int}
     times::Vector{Float64}
-    transitions::Vector{Union{String,Nothing}}
+    transitions::Vector{Transition}
 end
 
 # Operations
@@ -15,7 +15,7 @@ function -(σ1::AbstractTrajectory,σ2::AbstractTrajectory) end
 
 # Top-level Lp distance function
 """
-    `dist_lp(l_σ1, l_σ2; verbose, p, str_stat_list, str_stat_trajectory)`   
+`dist_lp(l_σ1, l_σ2; verbose, p, str_stat_list, str_stat_trajectory)`   
 
 Function that computes Lp distance between two set of any dimensional trajectories.
 ...
@@ -35,7 +35,7 @@ function dist_lp(l_σ1::Vector{<:AbstractTrajectory}, l_σ2::Vector{<:AbstractTr
 end
 
 """
-    `dist_lp(σ1, σ2; verbose, p, str_stat)`   
+`dist_lp(σ1, σ2; verbose, p, str_stat)`   
 
 Function that computes Lp distance between two trajectories of any dimension.
 It computes Lp distances for each observed variable (contained in `get_obs_var(σ)`). and then apply a statistic on these distances.
@@ -57,7 +57,7 @@ function dist_lp(σ1::AbstractTrajectory, σ2::AbstractTrajectory;
 end
 
 """
-    `dist_lp(σ1, σ2, var; verbose, p, str_stat)`   
+`dist_lp(σ1, σ2, var; verbose, p, str_stat)`   
 
 Function that computes Lp distance between two trajectories of any dimension.
 It computes Lp distances for each observed variable (contained in `get_obs_var(σ)`). and then apply a statistic on these distances.
@@ -131,7 +131,7 @@ function dist_lp_mean(σ1::AbstractTrajectory, σ2::AbstractTrajectory;
 end
 # For a list of trajectories
 function dist_lp_mean(l_σ1::Vector{<:AbstractTrajectory}, l_σ2::Vector{<:AbstractTrajectory};  
-                 verbose::Bool = false, p::Int = 1, str_stat_trajectory::String = "mean")
+                      verbose::Bool = false, p::Int = 1, str_stat_trajectory::String = "mean")
     res = 0.0
     for σ1 in l_σ1
         for σ2 in l_σ2
@@ -164,12 +164,12 @@ function _riemann_sum(f::Function, t_begin::Real, t_end::Real, step::Float64)
 end
 
 function check_consistency(σ::AbstractTrajectory)
-    test_sizes = length(σ.times) == length(σ.transitions) == size(σ.values)[1]
-    test_obs_var = length_obs_var(σ) == length(σ.m.g) == size(σ.values)[2]
-    if !test_sizes error("Issue between sizes of values/times/transitions in this trajectory.") end
-    if !test_obs_var error("Issue between sizes of observed variables in model and values") end
+    @assert length(σ.times) == length(σ.transitions) == size(σ.values)[1]
+    @assert length_obs_var(σ) == length(σ.m.g) == size(σ.values)[2]
     return true
 end
+is_steadystate(σ::AbstractTrajectory) = (σ.m).is_absorbing((σ.m).p, σ[end])
+
 
 # Properties of the trajectory
 length_states(σ::AbstractTrajectory) = length(σ.times)
@@ -178,15 +178,29 @@ get_obs_var(σ::AbstractTrajectory) = (σ.m).g
 is_bounded(σ::AbstractTrajectory) = σ.transitions[end] == nothing 
 
 # Access to trajectory values
-get_var_values(σ::AbstractTrajectory, var::String) = 
-    view(σ.values, :, (σ.m)._map_obs_var_idx[var])
-get_state(σ::AbstractTrajectory, idx::Int) = 
-    view(σ.values, idx, :)
-get_value(σ::AbstractTrajectory, var::String, idx::Int) = 
-    σ.values[idx,(σ.m)._map_obs_var_idx[var]] 
-
-
-δ(σ::AbstractTrajectory,t::Float64) = true
+get_var_values(σ::AbstractTrajectory, var::String) = view(σ.values, :, (σ.m)._map_obs_var_idx[var])
+get_state(σ::AbstractTrajectory, idx::Int) = view(σ.values, idx, :)
+get_value(σ::AbstractTrajectory, var::String, idx::Int) = σ.values[idx,(σ.m)._map_obs_var_idx[var]] 
+# Operation @
+function get_state_from_time(σ::AbstractTrajectory, t::Float64)
+    @assert t >= 0.0
+    l_t = times(σ)
+    if t == l_t[end] return σ[end] end
+    if t > l_t[end]
+        if !is_bounded(σ)
+            return σ[end]
+        else 
+            error("This trajectory is bounded and you're accessing out of the bounds")
+        end
+    end
+    for i in eachindex(l_t)
+        if l_t[i] <= t < l_t[i+1]
+            return σ[i]
+        end
+    end
+    error("Unexpected behavior")
+end
+δ(σ::AbstractTrajectory,idx::Int) = times(σ)[i+1] - times(σ)[i]
 
 states(σ::AbstractTrajectory) = σ.values
 times(σ::AbstractTrajectory) = σ.times
@@ -198,4 +212,5 @@ getindex(σ::AbstractTrajectory, idx::Int) = get_state(σ, idx)
 getindex(σ::AbstractTrajectory, var::String, idx::Int) = get_value(σ, var, idx)
 # Get the path of a variable ["I"]
 getindex(σ::AbstractTrajectory, var::String) = get_var_values(σ, var)
+lastindex(σ::AbstractTrajectory) = length_states(σ)
 
diff --git a/tests/automata/automaton_F.jl b/tests/automata/automaton_F.jl
new file mode 100644
index 0000000..8b70061
--- /dev/null
+++ b/tests/automata/automaton_F.jl
@@ -0,0 +1,12 @@
+
+using MarkovProcesses
+
+load_model("SIR")
+load_automaton("automaton_F")
+SIR.time_bound = 120.0
+x1, x2, t1, t2 = 0.0, Inf, 100.0, 120.0 
+A_F = create_automaton_F(SIR, x1, x2, t1, t2, "I") # <: LHA
+
+#sync_SIR = SIR * A_F
+#σ, state_lha = simulate(sync_SIR)  
+
diff --git a/tests/automata/read_trajectory_last_state_F.jl b/tests/automata/read_trajectory_last_state_F.jl
new file mode 100644
index 0000000..636080d
--- /dev/null
+++ b/tests/automata/read_trajectory_last_state_F.jl
@@ -0,0 +1,32 @@
+
+using MarkovProcesses
+
+load_model("SIR")
+load_automaton("automaton_F")
+SIR.time_bound = 120.0
+x1, x2, t1, t2 = 0.0, Inf, 100.0, 120.0 
+
+A_F = create_automaton_F(SIR, x1, x2, t1, t2, "I") # <: LHA
+
+function test_last_state(A::LHA, m::ContinuousTimeModel)
+    σ = simulate(m)
+    Send = read_trajectory(A, σ)
+    test = (get_state_from_time(σ, Send.time)[1] == Send["n"]) & (Send["d"] == 0)
+    #=
+    if !test
+        @show Send
+        @show get_state_from_time(σ, Send.time)
+        error("tkt")
+    end=#
+    return test
+end
+
+test_all = true
+nbr_sim = 10000
+for i = 1:nbr_sim
+    test = test_last_state(A_F, SIR)
+    global test_all = test_all & test
+end
+
+return test_all
+
diff --git a/tests/run_all.jl b/tests/run_all.jl
index e1433d2..5ed6f29 100644
--- a/tests/run_all.jl
+++ b/tests/run_all.jl
@@ -2,4 +2,5 @@
 include("run_unit.jl")
 include("run_simulation.jl")
 include("run_dist_lp.jl")
+include("run_automata.jl")
 
diff --git a/tests/run_automata.jl b/tests/run_automata.jl
new file mode 100644
index 0000000..abec4d0
--- /dev/null
+++ b/tests/run_automata.jl
@@ -0,0 +1,7 @@
+
+using Test
+
+@testset "Automata tests" begin
+    @test include("automata/read_trajectory_last_state_F.jl")
+end
+
diff --git a/tests/run_bench.jl b/tests/run_bench.jl
new file mode 100644
index 0000000..5b6a694
--- /dev/null
+++ b/tests/run_bench.jl
@@ -0,0 +1,16 @@
+
+using Test
+import MarkovProcesses: get_module_path
+
+path_bench  = get_module_path() * "/bench/"
+function include_arg!(path_file::String, arg::String)
+    global ARGS = [arg]
+    include(path_bench * "array_memory_order/sim.jl")
+    return true
+end
+
+@testset "Benchmark" begin
+    @test include_arg!(path_bench * "array_memory_order/sim.jl", "SIR")
+    @test include_arg!(path_bench * "pygmalion/sim_sir.jl", "SIR")
+end
+
-- 
GitLab