#Definitions of the different reference frames used and the relations between them.
#= Frames included by default are;
    - International celastial reference Frame (ICRF)
    - CBCI (CelestialBody Centred inertial/J2000 frame)
    - CBCF (Celestial body centred and fixed)
    - Vertical (vertical/local horizon frame)
    - Trajectory (Trajectory Frame)
    - Aerodynamic (Aerodynamic frame )
    - Body (Body frame)
Simulations will be performed in the ICRF, because this is the pseudo-inertial
reference frame that is considered are as inertial.

+ Preferably you want structs for all frames, but how then to refer them in a
tree-like structure?  Make the frames abstract struct will do the trick!
Supertypes will then determine which transformations needs to be explicetely written!
=#

#Define Supertype of the reference frames
abstract type AbstractFrame end

function NewFrame(name::String)
    FrameName = Symbol(name)
    @eval begin
        struct $FrameName <: AbstractFrame end
    end
end

#Global frames that do not require the position of the vehicle
NewFrame("ICRF")
NewFrame("ECI")
NewFrame("ECEF")

#Frames that requre the state of the vehicle
NewFrame("Vertical")
NewFrame("Trajectory")
NewFrame("Aerodynamic")
#NewFrame("Body") -> should be the vehicle type

#create transformation functions for pre-defined frames
struct TransformationQuaternion
    f1::Type{<:AbstractFrame}
    f2::Type{<:AbstractFrame}
    rel::Function
end

#=
#create transformation quaternion
function q_r2i(theta::Float64)
    return angleaxis2quat(theta, [0. 0. 1.])
end
Q_R2I = TransformationQuaternion(ECEF, ECI, q_r2i)

function q_i2r(theta::Float64)
    return angleaxis2quat(-theta, [0. 0. 1.])
end
Q_I2R = TransformationQuaternion(ECI, ECEF, q_i2r)

function q_r2v()

end

function q_aa2i(y::SimState)

end
Q_AA2I = TransformationQuaternion(Aerodynamic, ECI, q_aa2i)
=#


export Synapse, Segment, Neuron, NeuronOrSegment,NeuronOrSegmentOrOutput,AbstractNetwork,AbstractSynapse, Network, Input, Logger, BernoulliSynapseWeight
using DataFrames: DataFrame

################################################################################
## model component definitions                                                ##
################################################################################

abstract type AbstractComponent{ID,T,WT,IT} end
abstract type NeuronOrSegmentOrOutput{ID,T,WT,IT} <: AbstractComponent{ID,T,WT,IT} end
abstract type NeuronOrSegment{ID,T,WT,IT} <: NeuronOrSegmentOrOutput{ID,T,WT,IT} end
abstract type AbstractNetwork{ID,T,WT,IT} <:AbstractComponent{ID,T,WT,IT} end
abstract type AbstractSynapse{ID,T,WT,IT} <:AbstractComponent{ID,T,WT,IT} end

@enum VoltageLevel voltage_low=0 voltage_elevated=1 voltage_high=2

mutable struct Segment{ID,T,WT,IT} <: NeuronOrSegment{ID,T,WT,IT}
    id::ID
    θ_syn::IT
    θ_seg::Int
    plateau_duration::T

    state_syn::IT
    state_seg::Int
    state::VoltageLevel
    active::Bool
    last_activated::T

    next_downstream::NeuronOrSegment{ID,T,WT,IT}
    next_upstream::Vector{Segment{ID,T,WT,IT}}
    net::AbstractNetwork{ID,T,WT,IT}

    function Segment(id::ID, root::NeuronOrSegment{ID,T,WT,IT}; θ_syn=1, θ_seg=1, plateau_duration=root.net.default_plateau_duration) where {ID,T,WT,IT}
        this = new{ID,T,WT,IT}(id, θ_syn, θ_seg, plateau_duration, zero(IT), 0, voltage_low, false, zero(T), root, Segment{ID,T,WT,IT}[], root.net)
        push!(root.next_upstream, this)
        return this
    end
end

mutable struct Synapse{ID,T,WT,IT} <: AbstractSynapse{ID,T,WT,IT}
    id::ID
    target::NeuronOrSegmentOrOutput{ID,T,WT,IT}
    delay::T
    spike_duration::T
    weight::WT
    magnitude::IT
    state::Bool

    net::AbstractNetwork{ID,T,WT,IT}

    function Synapse(
                id::ID, source, target::NeuronOrSegmentOrOutput{ID,T,WT,IT};
                delay::T = source.net.default_delay, spike_duration::T = source.net.default_spike_duration, weight::WT=one(WT)
            ) where {ID,T,WT,IT}
        this = new{ID,T,WT,IT}(id, target, delay, spike_duration, weight, zero(IT), false, source.net)
        push!(source.synapses, this)
        return this
    end
end
sample_spike_magnitude(weight)=weight

"""
Fallback: let YAML.jl handle it.
"""
serialize(w) = w

mutable struct Neuron{ID,T,WT,IT} <: NeuronOrSegment{ID,T,WT,IT}
    id::ID
    θ_syn::IT
    θ_seg::Int
    refractory_duration::T

    state_syn::IT
    state_seg::Int
    state::VoltageLevel
    active::Bool

    next_upstream::Vector{Segment{ID,T,WT,IT}}
    synapses::Vector{Synapse{ID,T,WT,IT}}
    net::AbstractNetwork{ID,T,WT,IT}

    function Neuron(id::ID, net::AbstractComponent{ID,T,WT,IT}; θ_syn=one(IT), θ_seg=1, refractory_duration::T=net.default_refractory_duration) where {ID,T,WT,IT}
        this = new{ID,T,WT,IT}(id, θ_syn, θ_seg, refractory_duration, zero(IT), 0, voltage_low, false, Segment{ID,T,WT,IT}[], Synapse{ID,T,WT,IT}[], net)
        push!(net.neurons, this)
        return this
    end
end

mutable struct Input{ID,T,WT,IT} <: AbstractComponent{ID,T,WT,IT}
    id::ID
    synapses::Vector{Synapse{ID,T,WT,IT}}
    net::AbstractNetwork{ID,T,WT,IT}

    function Input(id::ID, net::AbstractComponent{ID,T,WT,IT}) where {ID,T,WT,IT}
        this = new{ID,T,WT,IT}(id::ID,Synapse{ID,T,WT,IT}[],net)
        push!(net.inputs, this)
        return this
    end
end

mutable struct Output{ID,T,WT,IT} <: NeuronOrSegmentOrOutput{ID,T,WT,IT}
    id::ID
    net::AbstractNetwork{ID,T,WT,IT}
    state_syn::IT

    function Output(id::ID, net::AbstractComponent{ID,T,WT,IT}) where {ID,T,WT,IT}
        this = new{ID,T,WT,IT}(id::ID, net, zero(IT))
        push!(net.outputs, this)
        return this
    end
end

mutable struct Network{ID,T,WT,IT} <: AbstractNetwork{ID,T,WT,IT}
    inputs::Vector{Input{ID,T,WT,IT}}
    outputs::Vector{Output{ID,T,WT,IT}}
    neurons::Vector{Neuron{ID,T,WT,IT}}

    default_refractory_duration::T
    default_delay::T
    default_spike_duration::T
    default_plateau_duration::T

    Network(;
        id_type::Type=Symbol, time_type::Type=Float64, weight_type::Type=Int, synaptic_input_type::Type=Int,
        default_refractory_duration = 1.0, default_delay = 1.0, default_spike_duration = 5.0, default_plateau_duration = 100.0
    ) = new{id_type, time_type, weight_type, synaptic_input_type}(
            Input{id_type, time_type, weight_type, synaptic_input_type}[],
            Output{id_type, time_type, weight_type, synaptic_input_type}[],
            Neuron{id_type, time_type, weight_type, synaptic_input_type}[],
            default_refractory_duration, default_delay, default_spike_duration, default_plateau_duration
        )
end

################################################################################
## default simulation handling                                                ##
################################################################################

struct Event{T, V}
    type::Symbol
    created::T
    t::T
    target::V
end
Base.isless(ev1::Event, ev2::Event) = isless(ev1.t, ev2.t)

function handle!(event::Event, queue!, logger!)
    if event.type == :input_spikes
        # definitely turn input on
        # -> this will trigger all synapses to fire
        on!(event.target, event.t, queue!, logger!)
    elseif event.type == :epsp_starts
        # definitively turn synapse on -> this may trigger a cascade of segements / soma to emit plateaus / a spike
        on!(event.target, event.t, queue!, logger!)
    elseif event.type == :epsp_ends
        # definitively turn synapse off
        off!(event.target, event.t, queue!, logger!)
    elseif event.type == :plateau_ends
        # if the event is not relevant any more (the current plateau is not the one that triggered this event), ignore
        # if the event is still relevant, definitely turn the segment off
        # -> this may also turn off upstream segments
        if event.created >= event.target.last_activated
            maybe_off!(event.target, event.t, queue!, logger!)
        end
    elseif event.type == :refractory_period_ends
        # definitively enable neuron to fire again
        # -> this may also trigger another spike rightaway
        # -> a spike will trigger all synapses to fire
        maybe_off!(event.target, event.t, queue!, logger!)
    end
    nothing
end

################################################################################
## model behavior                                                             ##
################################################################################

function reset!(net::Network{ID,T,WT,IT}) where {ID,T,WT,IT}
    foreach(reset!, net.inputs)
    foreach(reset!, net.outputs)
    foreach(reset!, net.neurons)
end

function reset!(x::Neuron{ID,T,WT,IT}) where {ID,T,WT,IT}
    x.state_syn = zero(IT)
    x.state_seg = 0
    x.active = false
    x.state = voltage_low

    foreach(reset!, x.synapses)
    foreach(reset!, x.next_upstream)
    update_state!(x)
end

function reset!(x::Segment{ID,T,WT,IT}) where {ID,T,WT,IT}
    x.state_syn = zero(IT)
    x.state_seg = 0
    x.active = false
    x.state = voltage_low
    x.last_activated = zero(T)

    foreach(reset!, x.next_upstream)
    update_state!(x)
end

function reset!(x::Input{ID,T,WT,IT}) where {ID,T,WT,IT}
    foreach(reset!, x.synapses)
end

function reset!(x::Output{ID,T,WT,IT}) where {ID,T,WT,IT}
    x.state_syn = zero(IT)
end

function reset!(x::Synapse{ID,T,WT,IT}) where {ID,T,WT,IT}
    x.state = false
    x.magnitude = zero(IT)
end

"""Recursively update all upstream branches."""
function backprop!(obj::Segment, now, logger!)
    # update this element
    recurse = update_state!(obj)
    logger!(now, :backprop, obj.id, obj.state)

    # if necessary, also update upstream elements
    if recurse
        for s_up in obj.next_upstream
            backprop!(s_up, now, logger!)
        end
    end
    return nothing
end

function update_state!(obj::Segment)
    old_state = obj.state
    obj.state = if obj.active || (isa(obj.next_downstream, Segment) && obj.next_downstream.state == voltage_high)
        # active or passively held high by downstream segment
        voltage_high
    elseif obj.state_seg >= obj.θ_seg
        # elevated voltage due to enough upstream input
        voltage_elevated
    else
        voltage_low
    end

    return old_state != obj.state
end

"""Check if this segment was just turned on; if so, backpropagate!"""
function maybe_on!(obj::Segment, now, queue!, logger!)
    update_state!(obj)
    return if obj.active && obj.state_syn >= obj.θ_syn #&& (isempty(obj.next_upstream) || obj.state == voltage_elevated)
        # if the segment is already on, but there is sufficient EPSP, re-trigger!
        @debug "$(now): Re-triggered segment $(obj.id) and extended its plateau!"
        logger!(now, :plateau_extended, obj.id, obj.state)

        # Re-schedule plateau-end time
        obj.last_activated = now
        queue!(Event(:plateau_ends, now, now+obj.plateau_duration, obj))
        true

    elseif ~obj.active && obj.state_syn >= obj.θ_syn && (isempty(obj.next_upstream) || obj.state == voltage_elevated)
        # if the segment is currently off, but the plateau conditions are satisfied, turn on
        @debug "$(now): Triggered segment $(obj.id) and it turned on!"

        # The segment is now active
        obj.active = true

        # propagate to the upstream segments & update
        backprop!(obj, now, logger!)
        logger!(now, :plateau_starts, obj.id, obj.state)

        # turning on this segment might have also turned on the next_downstream segment
        obj.next_downstream.state_seg += 1
        updated = update_state!(obj.next_downstream)
        if ~maybe_on!(obj.next_downstream, now, queue!, logger!) && updated
            logger!(now, :upstate_start, obj.next_downstream.id, obj.next_downstream.state)
        end

        # Each segment must turn off
        obj.last_activated = now
        queue!(Event(:plateau_ends, now, now+obj.plateau_duration, obj))
        true
    else
        @debug "$(now): Triggered segment $(obj.id), but it didn't turn on!"
        false
    end
end

"""Check if this segment should switch off, since its plateau has timed out"""
function maybe_off!(obj::Segment, now, queue!, logger!)
    # if the segment is currently on and has no reason to stay on, turn off

    if obj.active
        @debug "$(now): Triggered segment $(obj.id) and it turned off!"

        # The segment is now inactive
        obj.active = false

        # propagate to the upstream segments & update
        backprop!(obj, now, logger!)
        logger!(now, :plateau_ends, obj.id, obj.state)

        # update next_downstream segment
        obj.next_downstream.state_seg -= 1
        if update_state!(obj.next_downstream)
            logger!(now, :upstate_ends, obj.next_downstream.id, obj.next_downstream.state)
        end
    else
        @debug "$(now): Triggered segment $(obj.id), but it didn't turn off!"
    end
    return nothing
end

"""Check if this Input was triggered to spike"""
function on!(obj::Input, now, queue!, logger!)
    @debug "$(now): Triggered input $(obj.id) to fire!"
    for s in obj.synapses
        queue!(Event(:epsp_starts, now, now+s.delay, s))
    end
    return nothing
end

function update_state!(obj::Neuron)
    old_state = obj.state
    obj.state = if obj.active
        voltage_high
    elseif obj.state_seg >= obj.θ_seg
        # elevated voltage due to enough upstream input
        voltage_elevated
    else
        voltage_low
    end

    return old_state != obj.state
end

"""Check if this neuron was triggered to spike"""
function maybe_on!(obj::Neuron, now, queue!, logger!)
    update_state!(obj)

    return if ~obj.active && obj.state_syn >= obj.θ_syn && (isempty(obj.next_upstream) || obj.state == voltage_elevated)
        @debug "$(now): Triggered neuron $(obj.id) and it fired a spike!"

        obj.active = true
        obj.state = voltage_high
        logger!(now, :spikes, obj.id, obj.state)

        # trigger all synapses
        for s in obj.synapses
            queue!(Event(:epsp_starts, now, now+s.delay, s))
        end

        # don't forget to recover from refractory period
        queue!(Event(:refractory_period_ends, now, now+obj.refractory_duration, obj))
        true
    else
        @debug "$(now): Triggered neuron $(obj.id), but didn't fire!"
        false
    end
end

"""Check if this neuron was re-triggered to spike after refractoriness"""
function maybe_off!(obj::Neuron, now, queue!, logger!)
    return if obj.active
        @debug "$(now): Neuron $(obj.id) came out of refractoriness!"
        obj.active = false
        update_state!(obj)
        logger!(now, :refractory_period_ends, obj.id, obj.state)

        # check if the neuron should spike again, right away
        maybe_on!(obj::Neuron, now, queue!, logger!)
    else
        @debug "$(now): Triggered Neuron $(obj.id), but was already out of refractoriness!"
        false
    end
end

"""Check if an incoming spike should trigger an EPSP"""
function on!(obj::Synapse, now, queue!, logger!)
    if ~obj.state
        # set the synapse's state for this spike
        obj.magnitude = sample_spike_magnitude(obj.weight)
        if obj.magnitude ≈ zero(obj.magnitude)
            @debug "$(now): Triggered synapse $(obj.id), but didn't start an EPSP!"
        else
            @debug "$(now): Triggered synapse $(obj.id) and started an EPSP!"
            obj.state = true
            # inform the target segment about this new EPSP
            obj.target.state_syn += obj.magnitude
            logger!(now, :epsp_starts, obj.id, obj.state)

            # don't forget to turn off EPSP
            queue!(Event(:epsp_ends, now, now+obj.spike_duration, obj))

            # if the spike was excitatory this EPSP might have triggered a plateau in the target
            # else if it was inhibitory this EPSP may have destroyed any ongoing plateau
            if obj.magnitude > zero(obj.magnitude)
                maybe_on!(obj.target, now,  queue!, logger!)
            elseif obj.magnitude < zero(obj.magnitude)
                maybe_off!(obj.target, now,  queue!, logger!)
            end
        end
    else
        @debug "$(now): Triggered synapse $(obj.id), but synapse was already active!"
    end
    return nothing
end

"""Turn off the EPSP"""
function off!(obj::Synapse, now, queue!, logger!)
    # only update if the synapse isn't already off (shouldn't happen!)
    if obj.state
        @debug "$(now): Triggered synapse $(obj.id) and turned EPSP off!"
        # inform the target, that the EPSP is over
        obj.target.state_syn -= obj.magnitude

        # if the spike was inhibitory, this EPSP's end may trigger a rebound spike
        if obj.magnitude < zero(obj.magnitude)
            maybe_on!(obj.target, now,  queue!, logger!)
        end

        # reset the synapse's state
        obj.magnitude = zero(obj.magnitude)
        obj.state = false
        logger!(now, :epsp_ends, obj.id, obj.state)

    else
        @debug "$(now): Triggered synapse $(obj.id), but didn't turn EPSP off!"
    end
    return nothing
end

"""Trigger output"""
function maybe_on!(obj::Output, now, queue!, logger!)
    @debug "$(now): Triggered output $(obj.id)!"
    logger!(now, :spikes, obj.id, true)
    return nothing
end

################################################################################
## Logging                                                                    ##
################################################################################

struct Logger
    data::DataFrame
    filter::Function

    function Logger(net::Network{ID,T,WT,IT}; filter=(t,tp,id,x)->true, DT=Union{Bool,VoltageLevel}) where {ID,T,WT,IT}
        data = DataFrame(:t=>T[], :event=>Symbol[], :object=>ID[], :state=>DT[])
        new(data, filter)
    end
end

"""Log state of 'Output' object at each event"""
function (l::Logger)(args...)
    if getfield(l, :filter)(args...)
        push!(getfield(l,:data), args)
    end
end

# Base.getproperty(l::Logger, sym::Symbol) = getproperty(getfield(l,:data),sym)
# Base.propertynames(l::Logger) = propertynames(getfield(l,:data))

################################################################################
## pretty printing                                                            ##
################################################################################

Base.show(io::IO, x::Logger)   = show(io, getfield(x,:data))
Base.show(io::IO, x::Network)  = print(io, "Network with $(length(x.inputs)) inputs and $(length(x.neurons)) neurons")
Base.show(io::IO, x::Neuron)   = print(io, "Neuron '$(x.id)' with $(length(x.next_upstream)) child-segments and $(length(x.synapses)) outgoing synapses")
Base.show(io::IO, x::Input)    = print(io, "Input '$(x.id)' with $(length(x.synapses)) outgoing synapses")
Base.show(io::IO, x::Output)   = print(io, "Output '$(x.id)'")
Base.show(io::IO, x::Segment)  = print(io, "Segment '$(x.id)' with $(length(x.next_upstream)) child-segments")
Base.show(io::IO, x::Synapse)  = print(io, "Synapse '$(x.id)' (connects to $(x.target.id))")

################################################################################
## Probabilistic synapse implementation                                       ##
################################################################################

struct BernoulliSynapseWeight{T}
    magnitude::T
    probability::Float64
end
Base.:>=(w::BernoulliSynapseWeight, other::BernoulliSynapseWeight) = w.magnitude >= other.magnitude
Base.:>=(w::BernoulliSynapseWeight, x) = w.magnitude >= x
Base.one(::Type{BernoulliSynapseWeight{T}}) where T = BernoulliSynapseWeight(one(T), 1.0)
# Add parser for just a number
BernoulliSynapseWeight{T}(x::T) where {T} = BernoulliSynapseWeight(x, 1.0)
# Add parser for a tuple or vector (magnitude,probability)
BernoulliSynapseWeight{T}(x::Union{Tuple,Vector}) where {T} = BernoulliSynapseWeight(x...)
# generate a stochastic EPSP magnitude
sample_spike_magnitude(w::BernoulliSynapseWeight{T}) where {T} = (w.probability ≥ 1.0 || rand() ≤ w.probability) ? w.magnitude : zero(T)
# add serialization
serialize(w::BernoulliSynapseWeight{T}) where {T} = [w.magnitude, w.probability]


# Big TODO we are still missing the functionality that does the aligning in the constructors

Assemblage(assm::SEAssemblage) = Assemblage(assm.site, assm.occ) # Not a copy constructor - Just a function that will reduce derived types to the base type

# a constructor that takes occ and coords as one single DataFrame format and separates them
function Assemblage(occ::DataFrames.DataFrame; kwargs...)
  occ, coords = parsesingleDataFrame(occ)
  Assemblage(occ, coords; kwargs...)
end

# a constructor that takes occ as a DataFrame
Assemblage(occ::DataFrames.DataFrame, coords::Union{AbstractMatrix, DataFrames.DataFrame}; sitecolumns = true, kwargs...) = Assemblage(ComMatrix(occ; sitecolumns = sitecolumns), coords; kwargs...)

Assemblage(occ::AbstractMatrix, coords::Union{AbstractMatrix, DataFrames.DataFrame}, sites::Vector{String}, species::Vector{String}; sitecolumns = true, kwargs...) = Assemblage(ComMatrix(occ, species, sites; sitecolumns = sitecolumns), coords; kwargs...)

# a constructor that takes coords as a data.frame
function Assemblage(occ::ComMatrix, coords::DataFrames.DataFrame; kwargs...)

    if DataFrames.ncol(coords) == 2 && all(map(x -> x<:Number, eltypest(coords)))
        coords = convert(Matrix, coords)
    elseif DataFrames.ncol(coords) == 3
        xind, yind = guess_xycols(coords)
        siteind = setdiff(1:3, [xind, yind])[1]
        coords = convert(Matrix, coords[!,[xind, yind]])
    else
        error("coords must be a DataFrame with a column for sites and two columns for coordinates")
    end

    Assemblage(occ, coords; kwargs...)
end

function Assemblage(occ::ComMatrix, coords::AbstractMatrix;
      dropemptyspecies::Bool = false, dropemptysites::Bool = false, match_to_coords = true,
      traits = DataFrames.DataFrame(name = speciesnames(occ)), sitestat = DataFrames.DataFrame(sites = sitenames(occ)),
      cdtype::coordstype = auto)

    if match_to_coords
        occ, coords, sitestat = match_commat_coords(occ, coords, sitestat)
    end

    Assemblage(createLocations(coords, cdtype, sitestat), SpeciesData(occ, traits))
  end

function Assemblage(site::P, occ::SpeciesData{D};
    dropemptyspecies::Bool = false, dropemptysites::Bool = false) where {D <: Real, P <: SELocations}

    if dropemptyspecies
        dropspecies!(occ)
    end
    if dropemptysites
        dropsites!(occ, site)
    end
    Assemblage{D, P}(site, occ)
end

function createLocations(coords::AbstractMatrix, cdtype::coordstype = auto,  #by design, this is not type stable, but maybe that is OK for type constructors
        sitestat = DataFrames.DataFrame(sites = 1:size(coords, 1)))

    cdtype == pointdata && return Locations{PointData}(PointData(coords), sitestat)
    cdtype == griddata && return Locations{GridData}(GridData(coords), sitestat)
    if cdtype == auto
        try
            return Locations{GridData}(GridData(coords), sitestat)
        catch
            return Locations{PointData}(PointData(coords), sitestat)
        end
    end
end

function ComMatrix(occ::DataFrames.DataFrame; sitecolumns = true)
    if DataFrames.ncol(occ) == 3 && eltypest(occ)[3] <: AbstractString
        println("Data format identified as Phylocom")
        sites = unique(occ[!,1])
        species = unique(occ[!,3])
        js = indexin(occ[!,1], sites)
        is = indexin(occ[!,3], species)
        occ = maximum(occ[!,2]) == 1 ? sparse(is, js, true) : sparse(is, js, occ[2])
        return ComMatrix(occ, string.(collect(species)), string.(collect(sites)))
    end

    if eltypet(occ[!,1]) <: AbstractString
        species = string.(occ[:,1])
        occ = occ[!,2:end]
        sites = string.(collect(names(occ)))
    else
        species = string.(1:DataFrames.nrow(occ))
        sites = string.(collect(names(occ)))
    end

    try
        occ = dataFrametoSparseMatrix(occ, Bool)
        println("Matrix data assumed to be presence-absence")
    catch
        try
            occ = dataFrametoSparseMatrix(occ, Int) #TODO These lines mean that this code is not completely type stable.
            println("Matrix data assumed to be abundances, minimum $(minimum(occ)), maximum $(maximum(occ))")
        catch
            occ = dataFrametoSparseMatrix(occ, Float64)
            println("Matrix data assumed to be relative abundances, minimum $(minimum(occ)), maximum $(maximum(occ))")
            (minimum(occ) < 0 || maximum(occ) > 1) && @info("Values don't fall in the 0,1 range")
        end
    end

    if !sitecolumns
        return ComMatrix(collect(occ'), sites, species)
    end

    ComMatrix(occ, species, sites)
end

function ComMatrix(; species::AbstractVector = error("keyword `species` must be specified"), sites::AbstractVector = error("keyword `sites` must be specified"), abundances = 0)
    usites = unique(sites)
    uspecies = unique(species)
    js = indexin(sites, usites)
    is = indexin(species, uspecies)
    occ = abundances isa AbstractVector && length(unique(abundances)) > 1 ? sparse(is, js, abundances) : sparse(is, js, true)
    return ComMatrix(occ, string.(collect(uspecies)), string.(collect(usites)))
end

ComMatrix(occurrences::AbstractMatrix, speciesnames, sitenames; sitecolumns = true) =
    ComMatrix(occurrences; speciesnames = speciesnames, sitenames = sitenames, sitecolumns = sitecolumns)

function ComMatrix(occs; speciesnames = :auto, sitenames = :auto, sitecolumns = true)
    occurrences = sitecolumns ? occs : occs'

    if sitenames == :auto
        sitenames = ["site$i" for i in 1:size(occurrences, 2)]
    end
    if speciesnames == :auto
        speciesnames = ["species$i" for i in 1:size(occurrences, 1)]
    end

    length(speciesnames) == size(occurrences, 1) || throw(ArgumentError("length of speciesnames ($(length(speciesnames))) different from number of species in occurrences ($(size(occurrences, 1)))"))
    length(sitenames) == size(occurrences, 2) || throw(ArgumentError("length of sitenames ($(length(sitenames))) different from number of sites in occurrences ($(size(occurrences, 2)))"))
    ComMatrix{eltype(occurrences)}(sparse(occurrences), string.(speciesnames), string.(sitenames))
end

function GridData(coords::AbstractMatrix{<:Union{AbstractFloat, Missing}})
    grid = creategrid(coords)
    indices = getindices(coords, grid)
    GridData(indices, grid)
end

GridTopology(min_x, max_x, cellsize_x, min_y, max_y, cellsize_y) =
    GridTopology(range(min_x, stop = max_x, step = cellsize_x), range(min_y, stop = max_y, step = cellsize_y))


# MIT License
#
# Copyright (c) 2018 Martin Biel
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

abstract type AbstractSubProblem{T <: AbstractFloat} end
abstract type AbstractSubProblemState end

struct Gradient{T <: AbstractFloat, A <: AbstractVector}
    δQ::A
    Q::T
    id::Int

    function Gradient(δQ::AbstractVector, Q::AbstractFloat, id::Int)
        T = promote_type(eltype(δQ), Float32)
        δQ_ = convert(AbstractVector{T}, δQ)
        new{T, typeof(δQ_)}(δQ_, Q, id)
    end
end
SparseGradient{T <: AbstractFloat} = Gradient{T, SparseVector{T,Int64}}
DenseGradient{T <: AbstractFloat} = Gradient{T, Vector{T}}

"""
    SubProblem

Quasi-gradient subproblem.

"""
struct SubProblem{T <: AbstractFloat} <: AbstractSubProblem{T}
    id::Int
    probability::T
    model::JuMP.Model
    optimizer::MOI.AbstractOptimizer
    linking_constraints::Vector{MOI.ConstraintIndex}
    masterterms::Vector{Vector{Tuple{Int, Int, T}}}

    function SubProblem(model::JuMP.Model,
                        id::Integer,
                        π::AbstractFloat)
        T = typeof(π)
        # Get optimizer backend
        optimizer = backend(model)
        # Collect all constraints with known decision occurances
        constraints, terms =
            collect_linking_constraints(model,
                                        T)
        subproblem =  new{T}(id,
                             π,
                             model,
                             optimizer,
                             constraints,
                             terms)
        return subproblem
    end
end

function collect_linking_constraints(model::JuMP.Model,
                                     ::Type{T}) where T <: AbstractFloat
    linking_constraints = Vector{MOI.ConstraintIndex}()
    masterterms = Vector{Vector{Tuple{Int, Int, T}}}()
    master_indices = index.(all_known_decision_variables(model, 1))
    # Parse single rows
    F = AffineDecisionFunction{T}
    for S in [MOI.EqualTo{Float64}, MOI.LessThan{Float64}, MOI.GreaterThan{Float64}]
        for ci in MOI.get(backend(model), MOI.ListOfConstraintIndices{F,S}())
            coeffs = Vector{Tuple{Int, Int, T}}()
            f = MOI.get(backend(model), MOI.ConstraintFunction(), ci)::AffineDecisionFunction{T}
            aff = JuMP.jump_function(model, f)::DecisionAffExpr{T}
            for (coef, kvar) in linear_terms(aff.decisions)
                # Map known decisions to master decision,
                # assuming sorted order
                if state(kvar) == Known
                    col = master_indices[index(kvar).value].value
                    push!(coeffs, (1, col, T(coef)))
                end
            end
            if !isempty(coeffs)
                push!(masterterms, coeffs)
                push!(linking_constraints, ci)
            end
        end
    end
    # Parse vector rows
    F = VectorAffineDecisionFunction{T}
    for S in [MOI.Zeros, MOI.Nonpositives, MOI.Nonnegatives]
        for ci in MOI.get(backend(model), MOI.ListOfConstraintIndices{F,S}())
            coeffs = Vector{Tuple{Int, Int, T}}()
            f = MOI.get(backend(model), MOI.ConstraintFunction(), ci)::VectorAffineDecisionFunction{T}
            affs = JuMP.jump_function(model, f)::Vector{DecisionAffExpr{T}}
            for (row, aff) in enumerate(affs)
                for (coef, kvar) in linear_terms(aff.decisions)
                    # Map known decisions to master decision,
                    # assuming sorted order
                    if state(kvar) == Known
                        col = master_indices[index(kvar).value].value
                        push!(coeffs, (row, col, T(coef)))
                    end
                end
            end
            if !isempty(coeffs)
                push!(masterterms, coeffs)
                push!(linking_constraints, ci)
            end
        end
    end
    return linking_constraints, masterterms
end

function update_subproblem!(subproblem::AbstractSubProblem)
    update_known_decisions!(subproblem.optimizer)
    return nothing
end

function (subproblem::SubProblem)(x::AbstractVector)
    return solve_subproblem(subproblem, x)
end

function solve_subproblem(subproblem::SubProblem, x::AbstractVector)
    MOI.optimize!(subproblem.optimizer)
    status = MOI.get(subproblem.optimizer, MOI.TerminationStatus())
    if status ∈ AcceptableTermination
        return Subgradient(subproblem, x)
    elseif status == MOI.INFEASIBLE
        return Infeasible(subproblem)
    elseif status == MOI.DUAL_INFEASIBLE
        return Unbounded(subproblem)
    else
        error("Subproblem $(subproblem.id) was not solved properly, returned status code: $status")
    end
end

# Smooth #
# ========================== #
@with_kw mutable struct SmoothingData{T <: AbstractFloat}
    objective::AffineDecisionFunction{T} = zero(AffineDecisionFunction{T})
end

@with_kw mutable struct SmoothingParameters{T <: AbstractFloat}
    μ::T = 1.0
    objective_correction::Bool = false
end

"""
    SmoothSubProblem

Subproblem smoothed using a Moreau envelope.

...
# Parameters
- `μ::AbstractFloat = 1.0`: Moreau smoothing parameter. Controls the smoothing approximation accuracy.
...
"""
struct SmoothSubProblem{T <: AbstractFloat} <: AbstractSubProblem{T}
    data::SmoothingData{T}
    parameters::SmoothingParameters{T}

    id::Int
    probability::T
    model::JuMP.Model
    optimizer::MOI.AbstractOptimizer

    projection_targets::Vector{MOI.VariableIndex}
    penaltyterm::Quadratic

    function SmoothSubProblem(model::JuMP.Model,
                              id::Integer,
                              π::AbstractFloat,
                              ξ::AbstractVector;
                              kw...)
        T = typeof(π)
        # Get optimizer backend
        optimizer = backend(model)
        # Initialize data and params
        data = SmoothingData{T}()
        params = SmoothingParameters{T}(; kw...)
        @unpack μ = params
        # Cache objective
        F = MOI.get(backend(model), MOI.ObjectiveFunctionType())
        data.objective = MOI.get(backend(model), MOI.ObjectiveFunction{F}())
        # Create and return smoothed subproblem
        subproblem = new{T}(data,
                            params,
                            id,
                            π,
                            model,
                            optimizer,
                            Vector{MOI.VariableIndex}(),
                            Quadratic())
        # Setup Moreau envelope
        add_projection_targets!(subproblem, ξ)
        return subproblem
    end
end

function (subproblem::SmoothSubProblem)(x::AbstractVector)
    return solve_subproblem(subproblem, x)
end

function solve_subproblem(subproblem::SmoothSubProblem, x::AbstractVector)
    @unpack μ, objective_correction = subproblem.parameters
    decisions = index.(all_decision_variables(subproblem.model, 1))
    if objective_correction
        unfix.(all_decision_variables(subproblem.model, 1))
        StochasticPrograms.enable_penalty!(subproblem.penaltyterm,
                                           subproblem.optimizer,
                                           1 / (2 * μ),
                                           decisions,
                                           subproblem.projection_targets)
    end
    MOI.optimize!(subproblem.optimizer)
    status = MOI.get(subproblem.optimizer, MOI.TerminationStatus())
    if status ∈ AcceptableTermination
        return Gradient(subproblem, x)
    elseif status == MOI.INFEASIBLE
        return Infeasible(subproblem)
    elseif status == MOI.DUAL_INFEASIBLE
        return Unbounded(subproblem)
    else
        error("Subproblem $(subproblem.id) was not solved properly, returned status code: $status")
    end
end

function add_projection_targets!(subproblem::SmoothSubProblem{T}, ξ::AbstractVector) where T <: AbstractFloat
    @unpack μ = subproblem.parameters
    n = num_decisions(subproblem.model, 1)
    resize!(subproblem.projection_targets, n)
    for i in eachindex(ξ)
        name = add_subscript(:ξ, i)
        set = SingleDecisionSet(1, KnownDecision(ξ[i], T), NoSpecifiedConstraint(), false)
        empty = ScalarVariable(VariableInfo(false, NaN, false, NaN, false, NaN, false, NaN, false, false))
        dref = add_variable(subproblem.model, VariableConstrainedOnCreation(empty, set), name)
        subproblem.projection_targets[i] = index(dref)
    end
    # Initialize quadratic penalty
    x = index.(all_decision_variables(subproblem.model, 1))
    initialize_penaltyterm!(subproblem.penaltyterm,
                            subproblem.optimizer,
                            1 / (2 * μ),
                            x,
                            subproblem.projection_targets)
    return nothing
end

function restore_subproblem!(subproblem::SmoothSubProblem)
    MOI.optimize!(subproblem.optimizer)
    # Delete penalty-term
    remove_penalty!(subproblem.penaltyterm, subproblem.optimizer)
    # Delete projection targets
    for var in subproblem.projection_targets
        MOI.delete(subproblem.optimizer, var)
    end
    empty!(subproblem.projection_targets)
    return nothing
end

# Gradient #
# ========================== #
function Subgradient(subproblem::SubProblem{T}, x::AbstractVector) where T <: AbstractFloat
    π = subproblem.probability
    nterms = if isempty(subproblem.masterterms)
        nterms = 0
    else
        nterms = mapreduce(+, subproblem.masterterms) do terms
            length(terms)
        end
    end
    cols = zeros(Int, nterms)
    vals = zeros(T, nterms)
    j = 1
    for (i, ci) in enumerate(subproblem.linking_constraints)
        λ = MOI.get(subproblem.optimizer, MOI.ConstraintDual(), ci)
        for (row, col, coeff) in subproblem.masterterms[i]
            cols[j] = col
            vals[j] = π * λ[row] * coeff
            j += 1
        end
    end
    # Get sense
    sense = MOI.get(subproblem.optimizer, MOI.ObjectiveSense())
    correction = (sense == MOI.MIN_SENSE || sense == MOI.FEASIBILITY_SENSE) ? 1.0 : -1.0
    # Create sense-corrected subgradient
    δQ = sparsevec(cols, vals, length(x))
    Q = correction * π * MOI.get(subproblem.optimizer, MOI.ObjectiveValue())
    return Gradient(δQ, Q, subproblem.id)
end

function Gradient(subproblem::SmoothSubProblem{T}, x::AbstractVector) where T <: AbstractFloat
    @unpack μ, objective_correction = subproblem.parameters
    π = subproblem.probability
    # Get sense
    sense = MOI.get(subproblem.optimizer, MOI.ObjectiveSense())
    correction = (sense == MOI.MIN_SENSE || sense == MOI.FEASIBILITY_SENSE) ? 1.0 : -1.0
    # Get solution
    u = value.(all_decision_variables(subproblem.model, 1))
    # Sense corrected gradient
    δQ = π * (1/μ) * (x - u)
    # Sense corrected objective
    decisions = index.(all_decision_variables(subproblem.model, 1))
    if objective_correction
        fix.(all_decision_variables(subproblem.model, 1), x)
        StochasticPrograms.disable_penalty!(subproblem.penaltyterm,
                         subproblem.optimizer,
                         decisions,
                         subproblem.projection_targets)
        MOI.optimize!(subproblem.optimizer)
        status = MOI.get(subproblem.optimizer, MOI.TerminationStatus())
        if !(status ∈ AcceptableTermination)
            if status == MOI.INFEASIBLE
                return Infeasible(subproblem)
            elseif status == MOI.DUAL_INFEASIBLE
                return Unbounded(subproblem)
            end
        end
    end
    Q = correction * π * MOIU.eval_variables(subproblem.data.objective) do vi
        MOI.get(subproblem.optimizer, MOI.VariablePrimal(), vi)
    end
    return Gradient(δQ, Q, subproblem.id)
end

function Infeasible(subproblem::AbstractSubProblem)
    # Get sense
    sense = MOI.get(subproblem.optimizer, MOI.ObjectiveSense())
    correction = (sense == MOI.MIN_SENSE || sense == MOI.FEASIBILITY_SENSE) ? 1.0 : -1.0
    return Gradient(sparsevec(Float64[]), correction * Inf, subproblem.id)
end

function Unbounded(subproblem::AbstractSubProblem)
    # Get sense
    sense = MOI.get(subproblem.optimizer, MOI.ObjectiveSense())
    correction = (sense == MOI.MIN_SENSE || sense == MOI.FEASIBILITY_SENSE) ? 1.0 : -1.0
    return Gradient(sparsevec(Float64[]), correction * -Inf, subproblem.id)
end

# API
# ------------------------------------------------------------
"""
    Unaltered

Factory object for using regular [`SubProblem`](@ref) in the quasi-gradient algorithm. Passed by default to `subproblems` to `QuasiGradient.Optimizer`.

"""
struct Unaltered <: AbstractSubProblemState end
"""
    Smoothed

Factory object for using [`SmoothSubProblem`](@ref) through Moreau envelopes in the quasi-gradient algorithm. Pass to `subproblems ` in `QuasiGradient.Optimizer` or by setting the [`SubProblems`](@ref) attribute. See [`SmoothSubProblem`](@ref) for parameter descriptions.

"""
struct Smoothed <: AbstractSubProblemState
    parameters::SmoothingParameters{Float64}
end
Smoothed(; kw...) = Smoothed(SmoothingParameters(; kw...))


## Main function to generate pseudo-Data

function GenPseudoDat(model_def, pseudo_def)

    # First make a folder where to save the results
    cudi = pwd(); #@__DIR__;
    if !isdir(string(cudi,"\\Results"))
        mkdir(string(cudi,"\\Results"))
    end
    if !isdir(string(cudi, "\\Results\\", model_def["NameF"],"_",today()))
        mkdir(string(cudi, "\\Results\\", model_def["NameF"],"_",today()))
    end

    # Check which of the 2 types of simul_def have been given and make a choice on how to proceed
    if haskey(pseudo_def, "switchT")
        pseudo_def = checkStructPseudoDat(model_def, pseudo_def)
    else
        pseudo_def = extractPseudoDatCSV(model_def, pseudo_def)
    end

    simul_def = defSimulStruct();

    for (i,j) in simul_def
        simul_def[i] = pseudo_def[i];
    end
    # Make empty structure where the simulations will be
    simuls = Dict();

    # Get ODEs function
    OEDsFun = Symbol(string(join(model_def["NameF"]),"_SolveAll"));

    # loop over the different experiments to simulate
    for i in 1:simul_def["Nexp"]
        # Define simulation parameters
        global ts = collect(0.0:round(simul_def["finalTime"][i]))'; # 24 hour experiment
        global sp = [convert(Int,v) for v in (round.(simul_def["switchT"][i])')]; # 8 step experiment for now
        global ivss = simul_def["y0"][i]; # Experimental values for Y0 (IPTG, aTc, RFP, GFP)

        if model_def["Y0eqs"] != [] # ON inputs
            global pre = simul_def["preInd"][i];
        else
            global pre = [];
        end
        global samps = convert.(Int, round.(simul_def["tsamps"][i]));
        global theta = simul_def["theta"];

        # Inputs case
        if model_def["nInp"] != 0
            r = convert.(Int, 1:model_def["nInp"]:(length(simul_def["uInd"][i])));
            global inputs = zeros(convert.(Int,length(simul_def["uInd"][i])));
            for j in 1:convert.(Int,length(simul_def["uInd"][i])/model_def["nInp"])
                for k in 0:(model_def["nInp"]-1)
                    inputs[r[j]+k] = simul_def["uInd"][i][j,(k+1)];
                end
            end
        else
            r=0
        inputs=[];
        end

        simul = @eval $OEDsFun(ts, theta, sp, inputs, ivss, samps, pre);

        simuls[string("Exp_", i)] = simul;
    end

    # Now extract the entries to which the pseudodata will be generated
    pseudoDme = Dict();
    pseudoDsd = Dict();
    simObs = Dict();
    for i in 1:simul_def["Nexp"]
        pseudoDme[string("PDExp_", i)] = zeros(size(simuls[string("Exp_", i)])[1], length(pseudo_def["Obs"]), size(simuls[string("Exp_", 1)])[3]);
        pseudoDsd[string("PDExp_", i)] = zeros(size(simuls[string("Exp_", i)])[1], length(pseudo_def["Obs"]), size(simuls[string("Exp_", 1)])[3]);
        simObs[string("PDExp_", i)] = zeros(size(simuls[string("Exp_", i)])[1], length(pseudo_def["Obs"]), size(simuls[string("Exp_", 1)])[3]);
        if typeof(pseudo_def["Obs"])==Array{Int,1}
            simObs[string("PDExp_", i)] = simuls[string("Exp_", i)][:,(pseudo_def["Obs"]),:];
        else
            coun = 1;
            for j in 1:length(pseudo_def["Obs"])
               if !(occursin.("+", pseudo_def["Obs"][j])) && !(occursin.("-", pseudo_def["Obs"][j])) &&
               !(occursin.("/", pseudo_def["Obs"][j])) && !(occursin.("*", pseudo_def["Obs"][j])) &&
                !(occursin.("^", pseudo_def["Obs"][j]))
                    tind = findall(x->x==pseudo_def["Obs"][j], model_def["stName"])[1];
                    simObs[string("PDExp_", i)][:,coun,:] = simuls[string("Exp_", i)][:,tind,:];
                    coun +=1;
                else #This is the complicated stuff where we need to evaluate the function written as a string
                    if (occursin(".+", pseudo_def["Obs"][j]))
                        pseudo_def["Obs"][j] = replace(pseudo_def["Obs"][j], ".+"=>" .+ ")
                    elseif (occursin("+", pseudo_def["Obs"][j]))
                        pseudo_def["Obs"][j] = replace(pseudo_def["Obs"][j], "+"=>" .+ ")
                    end

                    if (occursin(".-", pseudo_def["Obs"][j]))
                        pseudo_def["Obs"][j] = replace(pseudo_def["Obs"][j], ".-"=>" .- ")
                    elseif (occursin("-", pseudo_def["Obs"][j]))
                        pseudo_def["Obs"][j] = replace(pseudo_def["Obs"][j], "-"=>" .- ")
                    end

                    if (occursin("./", pseudo_def["Obs"][j]))
                        pseudo_def["Obs"][j] = replace(pseudo_def["Obs"][j], "./"=>" ./ ")
                    elseif (occursin("/", pseudo_def["Obs"][j]))
                        pseudo_def["Obs"][j] = replace(pseudo_def["Obs"][j], "/"=>" ./ ")
                    end

                    if (occursin(".*", pseudo_def["Obs"][j]))
                        pseudo_def["Obs"][j] = replace(pseudo_def["Obs"][j], ".*"=>" .* ")
                    elseif (occursin("*", pseudo_def["Obs"][j]))
                        pseudo_def["Obs"][j] = replace(pseudo_def["Obs"][j], "*"=>" .* ")
                    end

                    if (occursin(".^", pseudo_def["Obs"][j]))
                        pseudo_def["Obs"][j] = replace(pseudo_def["Obs"][j], ".^"=>" .^ ")
                    elseif (occursin("^", pseudo_def["Obs"][j]))
                        pseudo_def["Obs"][j] = replace(pseudo_def["Obs"][j], "^"=>" .^ ")
                    end

                    tmp1 = pseudo_def["Obs"][j];
                    for k in 1:model_def["nStat"]
                        if occursin(model_def["stName"][k], pseudo_def["Obs"][j])
                            tmp1 = replace(tmp1, model_def["stName"][k]=>string(simuls[string("Exp_",i)][:,k,:]))
                        end
                    end
                    simObs[string("PDExp_", i)][:,coun,:] = eval(Meta.parse(tmp1));
                    coun +=1;
                end
            end
        end
    end

    # Generate pseudo-data with noise

    for j in 1:simul_def["Nexp"]
        for stat in 1:length(pseudo_def["Obs"])
            if length(size(pseudo_def["theta"]))==2
                for p in 1:convert(Int, length(pseudo_def["theta"])/model_def["nPar"])
                    if pseudo_def["NoiseType"] == "hetero"
                        pseudoDsd[string("PDExp_", j)][:,stat,p] = simObs[string("PDExp_", j)][:,stat,p] .* pseudo_def["Noise"][stat];
                    elseif pseudo_def["NoiseType"] == "homo"
                        pseudoDsd[string("PDExp_", j)][:,stat,p] = ones(length(simObs[string("PDExp_", j)][:,stat,p])) .* pseudo_def["Noise"][stat];
                    end
                    pseudoDme[string("PDExp_", j)][:,stat,p] = simObs[string("PDExp_", j)][:,stat,p] .+ rand(MvNormal(zeros(size(simObs[string("PDExp_", j)][:,stat,p])[1]),pseudoDsd[string("PDExp_", j)][:,stat,p][:]))
                    if pseudoDsd[string("PDExp_", j)][1,stat,p] == 0 # This checks for the case wher ethe user gives y0 and this is 0. This will output a mean and standard deviation of 0 which will generate issues in lekelihood computation (MLE and Stan inference)
                        pseudoDsd[string("PDExp_", j)][1,stat,p] = 1e-20;
                    end
                    if pseudoDme[string("PDExp_", j)][1,stat,p] == 0
                        pseudoDme[string("PDExp_", j)][1,stat,p] = 1e-10;
                    end
                    for r in 1:size(pseudoDme[string("PDExp_", j)])[1]
                        if pseudoDme[string("PDExp_", j)][r,stat,p] == 0 # If you have homoscedastic noise and high, this can generate negative values which do not make much sense. A proxi to a truncation of the distribution I suppsose.
                            pseudoDme[string("PDExp_", j)][r,stat,p] = 1e-10;
                        end
                    end
                end
            else
                    p=1;
                    if pseudo_def["NoiseType"] == "hetero"
                        pseudoDsd[string("PDExp_", j)][:,stat,p] = simObs[string("PDExp_", j)][:,stat,p] .* pseudo_def["Noise"][stat];
                    elseif pseudo_def["NoiseType"] == "homo"
                        pseudoDsd[string("PDExp_", j)][:,stat,p] = ones(length(simObs[string("PDExp_", j)][:,stat,p])) .* pseudo_def["Noise"][stat];
                    end
                    pseudoDme[string("PDExp_", j)][:,stat,p] = simObs[string("PDExp_", j)][:,stat,p] .+ rand(MvNormal(zeros(size(simObs[string("PDExp_", j)][:,stat,p])[1]),pseudoDsd[string("PDExp_", j)][:,stat,p][:]))
                    if pseudoDsd[string("PDExp_", j)][1,stat,p] == 0
                        pseudoDsd[string("PDExp_", j)][1,stat,p] = 1e-20;
                    end
                    if pseudoDme[string("PDExp_", j)][1,stat,p] == 0
                        pseudoDme[string("PDExp_", j)][1,stat,p] = 1e-10;
                    end

                    for r in 1:size(pseudoDme[string("PDExp_", j)])[1]
                        if pseudoDme[string("PDExp_", j)][r,stat,p] == 0 # If you have homoscedastic noise and high, this can generate negative values which do not make much sense. A proxi to a truncation of the distribution I suppsose.
                            pseudoDme[string("PDExp_", j)][r,stat,p] = 1e-10;
                        end
                    end
            end
        end
    end
    # Save path
    pseudo_def["savepath"] = string(cudi, "\\Results\\", model_def["NameF"],"_",today());
    pseudo_def["savename"] = string(model_def["NameF"],"_",today(), "_PseudoDataResults_",pseudo_def["flag"],".jld");

    # Put all results into one same structure and save
    pseudo_res = Dict()
    pseudo_res["Sims"] = simuls;
    pseudo_res["SimsObs"] = simObs;
    pseudo_res["PData"] = pseudoDme;
    pseudo_res["PError"] = pseudoDsd;

    save(string(pseudo_def["savepath"], "\\", pseudo_def["savename"]), "PseudoData", pseudo_res, "model_def", model_def, "pseudo_def", pseudo_def)

    println("")
    println("----------------------------------------- RESULTS -----------------------------------------")
    println("Pseudo-Data results are saved in the directory: ")
    println(string("                 ", pseudo_def["savepath"]))
    println(string("Under the name ",pseudo_def["savename"]))
    println("--------------------------------------------------------------------------------------")
    println("")

    if simul_def["plot"] == true
        plotPseudoDatODE(pseudo_res,model_def,pseudo_def)
        println("")
        println("----------------------------------------- PLOTS -----------------------------------------")
        println("Pseudo-Data PLOTS are saved in the directory: ")
        println(string("                 ", pseudo_def["savepath"]))
        println(string("Under the name PlotPseudoDat_Exp(i)_",pseudo_def["flag"],".png"))
        println(string("If more than one sample from the parameters are used, the plot will be the average between all traces."))
        println("--------------------------------------------------------------------------------------")
        println("")
    end

    PDatCSVGen(pseudo_res,model_def,pseudo_def);
    println("")
    println("----------------------------------------- CSVs -----------------------------------------")
    println("Pseudo-Data CSVs with results have been generated in the directory: ")
    println(string("                 ", pseudo_def["savepath"], "\\PseudoDataFiles"))
    println(string("Under the names: "))
    println(string("        Simulations: ", model_def["NameF"],"_EXP(i)", "_",
                    pseudo_def["flag"],"_Simulations.csv"))
    println(string("        Observables: ", model_def["NameF"],"_EXP(i)", "_",
                    pseudo_def["flag"],"_Observables.csv"))
    println(string("        Event Inputs (only if the model has inputs): ", model_def["NameF"],"_EXP(i)", "_",
                    pseudo_def["flag"],"_Event_Inputs.csv"))
    println("--------------------------------------------------------------------------------------")
    println("")

    return pseudo_res, model_def, pseudo_def

end

## Pseudodata general plot function

function plotPseudoDatODE(pseudo_res,model_def,pseudo_def)
    cudi = pwd(); # @__DIR__;
    for i in 1:pseudo_def["Nexp"]
        # Title for each subplot
        tit = "";
        yl1 = "";
        for k in 1:length(pseudo_def["Obs"])
            if typeof(pseudo_def["Obs"]) == Array{Int,1};
                tit = hcat(tit, string(model_def["stName"][pseudo_def["Obs"][k]]));
            else
                tit = hcat(tit, string(pseudo_def["Obs"][k]));
            end

            yl1 = hcat(yl1, "y");
        end
        tit = tit[:,2:end];
        yl1 = yl1[:,2:end];

        titu = "";
        yl2 = "";
        if model_def["nInp"] != 0
            for k in 1:model_def["nInp"]
                titu = hcat(titu, string(model_def["inpName"][k]));
                yl2 = hcat(yl2, "u");
            end
            titu = titu[:,2:end];
            yl2 = yl2[:,2:end];
        end

        tuu = hcat(tit, titu);
        yuu = hcat(yl1, yl2);

        # Elements for the inducers
        tu = Array{Array{Any,1}}(undef, length(pseudo_def["Obs"])+model_def["nInp"])
        [tu[k] = [] for k in 1:length(pseudo_def["Obs"])]
        su = Array{Array{Any,1}}(undef, length(pseudo_def["Obs"])+model_def["nInp"])
        [su[k] = [] for k in 1:length(pseudo_def["Obs"])]
        for k in 1:model_def["nInp"]
            tu[k+length(pseudo_def["Obs"])] = round.(pseudo_def["switchT"][i]);
            su[k+length(pseudo_def["Obs"])] = vcat(pseudo_def["uInd"][i][:,(k)], pseudo_def["uInd"][i][end,(k)])
        end

        t = round.(pseudo_def["tsamps"][i]);
        m = pseudo_res["PData"][string("PDExp_",i)];
        s = pseudo_res["PError"][string("PDExp_",i)];

        if length(size(pseudo_def["theta"])) == 1
            pl = plot(t, m[:,:,1],yerror = s[:,:,1], layout=length(pseudo_def["Obs"])+model_def["nInp"], label = "", title = tit, xlabel = "time", ylabel = yuu,
            color = "blue", size = [2000, 1200])

            pl = plot!(tu, su, layout=model_def["nStat"]+model_def["nInp"], label = "", linetype=:step, title = tuu);
        else
            m2 = mean(m, dims = 3);
            s2 = std(m, dims = 3);
            pl = plot(t, m2[:,:,1],yerror = s2[:,:,1], layout=length(pseudo_def["Obs"])+model_def["nInp"], label = "Average", title = tit, xlabel = "time", ylabel = yuu,
            color = "blue", size = [2000, 1200])

            pl = plot!(tu, su, layout=model_def["nStat"]+model_def["nInp"], label = "", linetype=:step, title = tuu);
        end

        savefig(string(cudi, "\\Results\\", model_def["NameF"],"_",today(), "\\PlotPseudoDat_Exp", i,"_", pseudo_def["flag"], ".png"));

    end

end

## Simulation Data structure Dict
function defPseudoDatStruct()

    # The structure of things user should give
    pseudo_def = Dict()

    pseudo_def["Nexp"] = []; # Integer indicating the number of experiments to be simulated
    pseudo_def["finalTime"] = []; # -> Vector of final times for each simulation (initial time will allways be asumed as 0, so please consider that)
    pseudo_def["switchT"] = []; # Array with the switching times of the inducer in the simulation (time 0 and final time need to be considered)
    pseudo_def["y0"] = []; # Array of Y0s for the simulations for each experiment. If you are computing the steady state this vector might not be used, however you still need to introduce it
    pseudo_def["preInd"] = []; # Level of the inducer in the ON. This entry can be empty if no Steady State equations are given and only the Y0 given by the user is considered.
    pseudo_def["uInd"] = []; # Array with the values for the inducer for each experiment at each step
    pseudo_def["theta"] = []; # Vector/Matrix with the parameter samples or directory and file location of CSV file with them. If a matrix is given, pseudo-data will be generated for each one of the theta samples
    pseudo_def["tsamps"] = []; # Array of Sampling times vectors
    pseudo_def["plot"] = []; # Bollean or yes/no string to save the resulting simulations in the results directory (false will be considered as default)
    pseudo_def["flag"] = []; # String to attach a unique flag to the generated files so it is not overwritten. If empty, nothing will be added.

    pseudo_def["Obs"] = []; # States that are observable. This is either a vecotr of strings (that has the same order asmodel_def["stName"] and model_def["eqns"]), a vector of integers insidating which states are observable or the string "All" if all states are observables. This can also be an experssion combining states (Only +,-,*,\ and ^ will be considered)
    pseudo_def["NoiseType"] = []; # String indicating if the desired noise is homoscedastic or heteroscedastic. The allowed strings are homo, homoscedastic or hetero, heteroscedastic (case independent). If empty, default will be heteroscedastic. Note that for high homoscedastic noise data could become negative (which to me does not make much sense), for now this will be set to a value close to 0.
    pseudo_def["Noise"] = []; # Percentage of heteroscedastic noise (introduced as value from 0 to 1). If empty 10% heteroscedastic noise will be assumed. In the homoscedastic case, if empty a standard deviation of 1 will be assumes. This has to be a vector of noise values for each observable.

    return(pseudo_def)

end

## Check pseudo-data structure function
# Function to check the content of the fields the user has given
function checkStructPseudoDat(model_def, pseudo_def)

    # Check taht all the dictionary entries are correct
    entries = ["Nexp", "finalTime", "switchT", "y0", "preInd", "uInd", "theta", "tsamps", "plot", "flag", "Obs", "NoiseType", "Noise"]
    if symdiff(entries,keys(pseudo_def))!=[] && symdiff(entries,keys(pseudo_def)) != ["savepath", "savename"]
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Please, check the entries of the dictionary, there is something wrong...")
        println(symdiff(entries,keys(pseudo_def)))
        return
    end

    # Check one to be sure there is no empty entry
    nee = ["Nexp", "finalTime", "switchT", "y0", "uInd", "theta", "tsamps", "Obs"]; # No empty entries
    for i in 1:length(nee)
        if pseudo_def[nee[i]] == []
            println("-------------------------- Process STOPPED!!! --------------------------")
            println(string("Plese, check the field ", nee[i], ", this cannot be empty"))
            return
        end
    end

    # Checks to see if the content of the fields is the expected (also consier if single value entries have been given as a number or a vector)
    if ((typeof(pseudo_def["Nexp"][1])!=Int) || length(pseudo_def["Nexp"])!=1) && (typeof(pseudo_def["Nexp"])!=Int)
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Please, check the field Nexp! This should be an integer. ")
        return
    elseif (typeof(pseudo_def["finalTime"]) != Array{Int,1}) && (typeof(pseudo_def["finalTime"]) != Array{Float64,1}) && (typeof(pseudo_def["finalTime"]) != Array{Int32,1})
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Please, check the field finalTime! This should be a vector of final times. ")
        return
    elseif (typeof(pseudo_def["switchT"]) != Array{Array{Int,1},1}) && (typeof(pseudo_def["switchT"]) != Array{Array{Float64,1},1}) && (typeof(pseudo_def["switchT"]) != Array{Array{Float32,1},1})
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Please, check the field switchT! This should be an array of arrays of numbers. ")
        return
    elseif (typeof(pseudo_def["y0"]) != Array{Array{Int,1},1}) && (typeof(pseudo_def["y0"]) != Array{Array{Float64,1},1}) && (typeof(pseudo_def["y0"]) != Array{Array{Float32,1},1}) &&
        (typeof(pseudo_def["y0"]) != Array{Array{Int,N} where N,1}) && (typeof(pseudo_def["y0"]) != Array{Array{Float64,N} where N,1}) && (typeof(pseudo_def["y0"]) != Array{Array{Float32,N} where N,1}) &&
        (typeof(pseudo_def["y0"]) != Array{Array{Int,2},1}) && (typeof(pseudo_def["y0"]) != Array{Array{Float64,2},1}) && (typeof(pseudo_def["y0"]) != Array{Array{Float32,2},1})
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Please, check the field y0! This should be an array of arrays of numbers. ")
        return
    elseif (typeof(pseudo_def["preInd"]) != Array{Array{Int,1},1}) && (typeof(pseudo_def["preInd"]) != Array{Array{Float64,1},1}) && (typeof(pseudo_def["preInd"]) != Array{Array{Float32,1},1}) && pseudo_def["preInd"] != []
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Please, check the field preInd! This should be an array of arrays of numbers. ")
        return
    # elseif (typeof(pseudo_def["uInd"]) != Array{Array{Int,1},1}) && (typeof(pseudo_def["uInd"]) != Array{Array{Float64,1},1}) && (typeof(pseudo_def["uInd"]) != Array{Array{Float32,1},1})
    #     println("-------------------------- Process STOPPED!!! --------------------------")
    #     println("Please, check the field uInd! This should be an array of arrays of numbers. ")
    #     return
    elseif (typeof(pseudo_def["theta"]) != Array{Float64,1}) && (typeof(pseudo_def["theta"]) != Array{Float32,1}) &&
        (typeof(pseudo_def["theta"]) != Array{Float64,2}) && (typeof(pseudo_def["theta"]) != Array{Float32,2}) &&
        (typeof(pseudo_def["theta"]) != Array{String,1}) && (typeof(pseudo_def["theta"]) != String)
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Please, check the field theta! This should be a vector, matrix or string. ")
        return
    elseif (typeof(pseudo_def["tsamps"]) != Array{Array{Int,1},1}) && (typeof(pseudo_def["tsamps"]) != Array{Array{Float64,1},1}) && (typeof(pseudo_def["tsamps"]) != Array{Array{Float32,1},1})
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Please, check the field tsamps! This should be an array of arrays of numbers. ")
        return
    elseif (pseudo_def["plot"] != [])
        try
            if (typeof(pseudo_def["plot"]) != Bool) && (typeof(pseudo_def["plot"]) != Array{Bool,1}) &&
            (typeof(pseudo_def["plot"]) != String) && (typeof(pseudo_def["plot"]) != Array{String,1})
                println("-------------------------- Process STOPPED!!! --------------------------")
                println("Please, check the field plot! This should be true, fals, \"Yes\", \"yes\", \"No\", \"no\" or an empty array ")
                return
            end
        catch
             if (typeof(pseudo_def["plot"]) != Array{Bool,1}) &&
            (typeof(pseudo_def["plot"]) != Array{String,1})
                println("-------------------------- Process STOPPED!!! --------------------------")
                println("Please, check the field plot! This should be true, fals, \"Yes\", \"yes\", \"No\", \"no\" or an empty array ")
                return
            end
        end
    elseif (typeof(pseudo_def["flag"]) != Array{String,1}) && (typeof(pseudo_def["flag"]) != String) && ((pseudo_def["flag"]) != [])
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Please, check the field flag! This should be a vector, matrix or string. ")
        return
    elseif (typeof(pseudo_def["Obs"]) != Array{String,1}) && (typeof(pseudo_def["Obs"]) != Array{Int,1})
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Please, check the field Obs! This should be a vector of strings or integers. ")
        return
    elseif (typeof(pseudo_def["NoiseType"]) != Array{String,1}) && (typeof(pseudo_def["NoiseType"]) != String) && ((pseudo_def["NoiseType"]) != [])

    elseif (typeof(pseudo_def["Noise"]) != Array{Float64,1}) && (typeof(pseudo_def["Noise"]) != Array{Float32,1})  &&
        ((pseudo_def["NoiseType"]) != []) && (typeof(pseudo_def["Noise"]) != Array{Int,1})
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Please, check the field Noise! This should be a vector of error percentages from 0 to 1. ")
        return
    end

    # Extract necessary elements to ease generalisation

    if typeof(pseudo_def["Nexp"]) == Array{Int,1}
        pseudo_def["Nexp"] = pseudo_def["Nexp"][1];
    end

    if pseudo_def["plot"]==[true] || pseudo_def["plot"]==["Yes"] || pseudo_def["plot"]==["yes"] || pseudo_def["plot"]=="Yes" || pseudo_def["plot"]=="yes"
        pseudo_def["plot"]=true
    elseif pseudo_def["plot"]==[false] || pseudo_def["plot"]==["No"] || pseudo_def["plot"]==["no"] || pseudo_def["plot"]=="No" || pseudo_def["plot"]=="no" || pseudo_def["plot"]==[]
        pseudo_def["plot"]=false
    end

    if (typeof(pseudo_def["theta"]) == Array{String,1})
        pseudo_def["theta"] = pseudo_def["theta"][1];
    end

    if typeof(pseudo_def["flag"]) == Array{String,1}
        pseudo_def["flag"] = pseudo_def["flag"][1];
    elseif typeof(pseudo_def["flag"]) == String
        pseudo_def["flag"] = pseudo_def["flag"];
    elseif ((pseudo_def["flag"]) == [])
        pseudo_def["flag"] = "";
    end

    if (typeof(pseudo_def["NoiseType"]) == Array{String,1})
        pseudo_def["NoiseType"] = pseudo_def["NoiseType"][1];
    end

    # Check that all the contents make sense
    if (pseudo_def["Nexp"] != length(pseudo_def["finalTime"])) || (pseudo_def["Nexp"] != length(pseudo_def["switchT"])) ||
        (pseudo_def["Nexp"] != length(pseudo_def["y0"])) || (pseudo_def["Nexp"] != length(pseudo_def["uInd"])) ||
        (pseudo_def["Nexp"] != length(pseudo_def["tsamps"]))
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Sorry, some field does not match the number of experiments selected. Check that the number of")
        println("entries for finalTime, switchT, y0, uInd or tsamps matches the number of experiments in Nexp.")
        return
    end

    if (typeof(pseudo_def["theta"]) == Array{Float64,1}) || (typeof(pseudo_def["theta"]) == Array{Float32,1})
        if length(pseudo_def["theta"]) != model_def["nPar"]
            println("-------------------------- Process STOPPED!!! --------------------------")
            println("Number of parameters introduced does not match the specified")
            return
        end
    elseif (typeof(pseudo_def["theta"]) == Array{Float64,2}) || (typeof(pseudo_def["theta"]) == Array{Float32,2})
        if size(pseudo_def["theta"])[1] != model_def["nPar"] && size(pseudo_def["theta"])[2] != model_def["nPar"]
            println("-------------------------- Process STOPPED!!! --------------------------")
            println("Number of parameters introduced does not match the specified")
            return
        end
    elseif (typeof(pseudo_def["theta"]) == String)
        if pseudo_def["theta"][end-3:end] != ".csv"
            println("-------------------------- Process STOPPED!!! --------------------------")
            println("Please, add the .csv termination to the theta file! And no spaces after!")
            return
        end
        if isfile(pseudo_def["theta"])
            pseudo_def["theta"] = Matrix(CSV.read(pseudo_def["theta"]));
        else
            println("-------------------------- Process STOPPED!!! --------------------------")
            println("Sorry, but the file path you introduced does not exist!")
            return
        end
    end

    for i in 1:pseudo_def["Nexp"]
        if pseudo_def["tsamps"][i][end] > pseudo_def["finalTime"][i]
            println("-------------------------- Process STOPPED!!! --------------------------")
            println("Please, check finalTime. You have selected a sampling point past it.")
            return
        end
        if model_def["nInp"] != 0
            if length(pseudo_def["uInd"][i]) != (length(pseudo_def["switchT"][i])-1)
                println("-------------------------- Process STOPPED!!! --------------------------")
                println("Please, check uInd and switchT. Number of steps does not match the number of values for the inputs.")
                return
            end
        end

        if length(pseudo_def["y0"][i])/model_def["nStat"] == 1
            if length(pseudo_def["y0"][i]) != model_def["nStat"]
                println("-------------------------- Process STOPPED!!! --------------------------")
                println(string("Sorry, but Y0 does not match the number or states in experiment ", i))
                return
            end
        else
            if size(pseudo_def["y0"][i])[1] != model_def["nStat"] && size(pseudo_def["y0"][i])[2] != model_def["nStat"]
                println("-------------------------- Process STOPPED!!! --------------------------")
                println(string("Sorry, but Y0 does not match the number or states in experiment ", i))
                return
            end
            if length(pseudo_def["y0"][i])/model_def["nStat"] != length(pseudo_def["theta"])/model_def["nPar"]
                println("-------------------------- Process STOPPED!!! --------------------------")
                println(string("Sorry, but Y0 does not match the dimensions of theta "))
                return
            end
            if size(pseudo_def["y0"][i])[1] == size(pseudo_def["y0"][i])[2]
                println("-------------------------- WARNING --------------------------")
                println(string("Sorry, but the number of rows and columns of the y0 matrix in experiment ",i," is the same, so the checks on "))
                println("correct orientation will not work. Please make sure that the dimensions follow: ")
                println("y0[samples, states]")
            end
        end
    end

    if (model_def["Y0eqs"] != []) && pseudo_def["preInd"]==[]
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("You specified computation of Y0 as steady-state but you have not introduced an inducer value for it!")
        return
    end

    if pseudo_def["NoiseType"] == []
        pseudo_def["NoiseType"] = "hetero"
    end

    pseudo_def["NoiseType"] = lowercase(pseudo_def["NoiseType"]); # To avoid issues with uper cases and that

    if (pseudo_def["NoiseType"] == "homoscedastic")
        pseudo_def["NoiseType"] = "homo";
    elseif (pseudo_def["NoiseType"] == "heteroscedastic")
        pseudo_def["NoiseType"] = "hetero";
    end

    if pseudo_def["Noise"] == []
        pseudo_def["Noise"] = ones(length(pseudo_def["Obs"])) .* 0.1;
    end

    if (pseudo_def["NoiseType"] == "hetero") && (maximum(pseudo_def["Noise"]) > 1)
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Sorry, but the noise percentage has to be between 0 and 1!")
        return
    end

    if (minimum(pseudo_def["Noise"]) < 0)
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Sorry, but the noise percentage or standard deviation is negative and that CANNOT be!")
        return
    end

    if length(pseudo_def["Obs"]) != length(pseudo_def["Noise"])
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Sorry, but you need to assign an error value for each observable selected!")
        return
    end

    # Check warning in case the matrix introduced is symmetric
    if length(size(pseudo_def["theta"])) == 2
        if size(pseudo_def["theta"])[1] == size(pseudo_def["theta"])[2]
            println("-------------------------- WARNING --------------------------")
            println("Sorry, but the number of rows and columns of the theta matrix is the same, so the checks on ")
            println("correct orientation will not work. Please make sure that the dimensions follow: ")
            println("theta[samples, parameters]")
        end
    end

    # Check that no step is no smaller than 2 unit of time
    for i in 1:pseudo_def["Nexp"]
        for j in 1:length(pseudo_def["uInd"][i])
            if (pseudo_def["switchT"][i][j+1]-pseudo_def["switchT"][i][j])<=4
                println("-------------------------- Process STOPPED!!! --------------------------")
                println(string("Sorry, but 2 of the steps in experiment ", i, " are too close. This package cannot "))
                println("handle this for now. We are working on it!")
                return
            end
        end
    end

    # Check that the inputs for the experiment are given as collumns
    try
        if model_def["nInp"]>1
            for i in 1:pseudo_def["Nexp"]
                if size(pseudo_def["uInd"][i])[2] != model_def["nInp"];
                    pseudo_def["uInd"][i] = pseudo_def["uInd"][i]';
                end
            end
        end
    catch
        println("-------------------------- Process STOPPED!!! --------------------------")
        println(string("Sorry, but there is some issue with the contents of the field uInd."))
        return
    end

    if pseudo_def["Obs"] == Array{Int,1}
        if (maximum(pseudo_def["Obs"]) > model_def["nStat"]) || (length(pseudo_def["Obs"]) > model_def["nStat"])
            println("-------------------------- Process STOPPED!!! --------------------------")
            println(string("Sorry, but there is some issue with the contents of the field Obs. It seems that "))
            println("     you have selected more observables than states or an index higher than the number of observables")
            return
        end
    end

    if typeof(pseudo_def["Obs"]) == Array{String,1}
        if convert(Bool,sum(occursin.("+", pseudo_def["Obs"]))) || convert(Bool,sum(occursin.("-", pseudo_def["Obs"]))) ||
           convert(Bool,sum(occursin.("/", pseudo_def["Obs"]))) ||
           convert(Bool,sum(occursin.("*", pseudo_def["Obs"]))) || convert(Bool,sum(occursin.("^", pseudo_def["Obs"])))
            nothing
        else
            if sum(occursin.(model_def["stName"], pseudo_def["Obs"])) == 0
                println("-------------------------- Process STOPPED!!! --------------------------")
                println(string("Sorry, but there is some issue with the contents of the field Obs."))
                println("It seems that the observable(s) selected do not match any state")
                return
            else
                pseudo_def["Obs"] = sort([findall(x->x==pseudo_def["Obs"][i], model_def["stName"])[1] for i in 1:length(pseudo_def["Obs"])])
            end
        end

    elseif typeof(pseudo_def["Obs"]) == Array{Int,1}
        pseudo_def["Obs"] = sort(pseudo_def["Obs"]);
    end

    return(pseudo_def)
end

## CSV introduction description
# function fileStructInfo()
#
#     println("ALL THE FILES MUST BE CSV FILES!!!")
#     println("")
#     println("---------------------------------------------------------------------------------------------------------")
#     println("")
#     println("The observables file entry should have the following structure: ")
#     println("Column 1: Sampling Times for the simulations")
#     println("Column 2 + Number of states: Y0 value for each each state (in order) in each column. ")
#     println("           Value might need to be repeated across all the column, but only the first row will be considered")
#     println("")
#     println("---------------------------------------------------------------------------------------------------------")
#     println("")
#     println("The event inputs file entry should have the following structure: ")
#     println("Column 1: Each Switching time (final time not considered)")
#     println("Column 2: Final time (can be repeated for as many entrances as column 1 has)")
#     println("Column 3 + number of inducers: Repeated column for the value of the inducers in the ON. ")
#     println("              If no pre-inducer will be used in the simulations, any number could be used")
#     println("Column 4 + number of inducers: Value of each inducer at each switching time.")
#     println("")
#     println("This is an Example of an experiment with 3 steps and 2 inducers: ")
#     println(" |Switching|  |FinalTime|  |IPTGpre|  |aTcPre|  |  IPTG  |  |  aTc  |")
#     println("      0           1350         1          0          1          0    ")
#     println("     400          1350         1          0         0.3         50   ")
#     println("     700          1350         1          0         0.7         35   ")
#     println("     950          1350         1          0         0.1        100   ")
#     println("")
#     println("---------------------------------------------------------------------------------------------------------")
#     println("")
#     println("For the main directory, remember that in Julia you have to type \\ twice!")
#
# end

## CSV introduction structure
function defPseudoDatStructFiles()

    fileStructInfo()

    # The structure of things user should give
    pseudo_def = Dict()

    pseudo_def["ObservablesFile"] = []; # File containing information about the states Y0 and sampling times
    pseudo_def["EventInputsFile"] = []; # -> File containing all the information about the stimuli
    pseudo_def["theta"] = []; # Vector/Matrix with the parameter samples or directory and file location of CSV file with them
    pseudo_def["MainDir"] = []; # Directory path where all files are located (to avoid repetition). This can be empty.
    pseudo_def["plot"] = [];
    pseudo_def["flag"] = [];

    pseudo_def["Obs"] = []; # States that are observable. This is either a vecotr of strings (that has the same order asmodel_def["stName"] and model_def["eqns"]), a vector of integers insidating which states are observable or the string "All" if all states are observables. This can also be an experssion combining states (Only +,-,*,\ and ^ will be considered)
    pseudo_def["NoiseType"] = []; # String indicating if the desired noise is homoscedastic or heteroscedastic. The allowed strings are homo, homoscedastic or hetero, heteroscedastic (case independent). If empty, default will be heteroscedastic. Note that for high homoscedastic noise data could become negative (which to me does not make much sense), for now this will be set to a value close to 0.
    pseudo_def["Noise"] = []; # Percentage of heteroscedastic noise (introduced as value from 0 to 1). If empty 10% heteroscedastic noise will be assumed. In the homoscedastic case, if empty a standard deviation of 1 will be assumes. This has to be a vector of noise values for each observable.

    return(pseudo_def)

end

## Extract data from CSV
# Function to extract contents of CSV files if user sellects that
function extractPseudoDatCSV(model_def, pseudo_def)

    # Check taht all the dictionary entries are correct
    entries = ["ObservablesFile", "EventInputsFile", "theta", "MainDir", "plot", "flag", "Obs", "Noise"]
    if symdiff(entries,keys(pseudo_def))!=[]
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Please, check the entries of the dictionary, there is something wrong...")
        println(symdiff(entries,keys(pseudo_def)))
        return
    end

    # Checks on the different strings introduced
    if pseudo_def["MainDir"] == [] || pseudo_def["MainDir"] == ""
        pseudo_def["MainDir"] = "";
    elseif typeof(pseudo_def["MainDir"])==Array{String,1}
        pseudo_def["MainDir"] = pseudo_def["MainDir"][1]
    elseif typeof(pseudo_def["MainDir"]) == String
        nothing
    else
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Sorry, there is something wrong in the type of the field MainDir")
        return
    end

    if typeof(pseudo_def["ObservablesFile"]) != Array{String,1}
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Sorry, there is something wrong in the type of the field ObservablesFile. This should be a Array{String,1}")
        return
    end

    if length(pseudo_def["ObservablesFile"]) != length(pseudo_def["EventInputsFile"])
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Sorry, there number of entries for ObservablesFile and EventInputsFile should be the same")
        return
    end

    for i in 1:length(pseudo_def["ObservablesFile"])
        if pseudo_def["ObservablesFile"][i][end-3:end] != ".csv"
            println("-------------------------- Process STOPPED!!! --------------------------")
            println("Please, add the .csv termination to the ObservablesFile file! And no spaces after!")
            return
        end
    end

    if typeof(pseudo_def["EventInputsFile"]) != Array{String,1}
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Sorry, there is something wrong in the type of the field EventInputsFile. This should be a Array{String,1}")
        return
    end

    for i in 1:length(pseudo_def["EventInputsFile"])
        if pseudo_def["EventInputsFile"][i][end-3:end] != ".csv"
            println("-------------------------- Process STOPPED!!! --------------------------")
            println("Please, add the .csv termination to the EventInputsFile file! And no spaces after!")
            return
        end
    end

    simstu = defPseudoDatStruct();

    simstu["theta"] = pseudo_def["theta"];

    tmpInp = Array{Any}(undef, 1,length(pseudo_def["EventInputsFile"]))
    for i in 1:length(pseudo_def["EventInputsFile"])
        if pseudo_def["MainDir"] != [] && pseudo_def["MainDir"] != ""
            tmpInp[i] = Matrix(CSV.read(string(pseudo_def["MainDir"], "\\\\",pseudo_def["EventInputsFile"][i])));
        else
            tmpInp[i] = Matrix(CSV.read(pseudo_def["EventInputsFile"][i]));
        end
    end

    tmpObs = Array{Any}(undef, 1,length(pseudo_def["ObservablesFile"]))
    for i in 1:length(pseudo_def["ObservablesFile"])
        if pseudo_def["MainDir"] != [] && pseudo_def["MainDir"] != ""
            tmpObs[i] = Matrix(CSV.read(string(pseudo_def["MainDir"], "\\\\",pseudo_def["ObservablesFile"][i])));
        else
            tmpObs[i] = Matrix(CSV.read(pseudo_def["ObservablesFile"][i]));
        end
    end

    try
        simstu["Nexp"] = length(pseudo_def["EventInputsFile"]);
        simstu["finalTime"] = [tmpInp[i][1,2] for i in 1:length(pseudo_def["EventInputsFile"])];
        simstu["switchT"] = [vcat(tmpInp[i][:,1], tmpInp[i][1,2]) for i in 1:length(pseudo_def["EventInputsFile"])];
        simstu["y0"] = [tmpObs[i][1,2:(1+model_def["nStat"])] for i in 1:length(pseudo_def["ObservablesFile"])];
        simstu["preInd"] = [tmpInp[i][1,3:(2+model_def["nInp"])] for i in 1:length(pseudo_def["EventInputsFile"])];
        simstu["uInd"] = [tmpInp[i][:,(3+model_def["nInp"]):(3+model_def["nInp"]+(model_def["nInp"]-1))] for i in 1:length(pseudo_def["EventInputsFile"])];
        simstu["theta"] = pseudo_def["theta"];
        simstu["tsamps"] = [tmpObs[i][:,1] for i in 1:length(pseudo_def["ObservablesFile"])];
        simstu["plot"] = pseudo_def["plot"];
        simstu["flag"] = pseudo_def["flag"];
        simstu["Obs"] = pseudo_def["Obs"];
        simstu["NoiseType"] = pseudo_def["NoiseType"];
        simstu["Noise"] = pseudo_def["Noise"];
    catch
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Sorry, but there seems to be some issue with the internal structure of the files you have given...")
        return
    end

    simstu = checkStructPseudoDat(model_def, simstu);

    return(simstu)

end

function PDatCSVGen(pseudo_res,model_def,pseudo_def)
    ## Generate CSV Files
    if !isdir(string(pseudo_def["savepath"], "\\PseudoDataFiles"))
        mkdir(string(pseudo_def["savepath"], "\\PseudoDataFiles"))
    end

    for expp in 1:pseudo_def["Nexp"]

        # Simulations Matrix
        a,b,c = size(pseudo_res["Sims"][string("Exp_", expp)]);
        Simulations = zeros(a, (b*c)+1);
        Simulations[:,1] = pseudo_def["tsamps"][expp];

        sims_head = ["time"];

        coun = 2;
        for i in 1:c
            Simulations[:,coun:(coun+b-1)] = pseudo_res["Sims"][string("Exp_", expp)][:,:,i];
            coun = coun+b;
            for j in 1:b
                sims_head = vcat(sims_head, string(model_def["stName"][j], "_Theta_", i))
            end
        end

        df1 = DataFrame[Simulations][1]
        rename!(df1, [sims_head[i] for i in 1:(b*c)+1])
        CSV.write(string(pseudo_def["savepath"], "\\PseudoDataFiles\\",model_def["NameF"],"_EXP",expp, "_",
                pseudo_def["flag"],"_Simulations.csv"), df1);

        # Observables matrix
        a,b,c = size(pseudo_res["PData"][string("PDExp_", expp)]);
        Observables = zeros(a, (3*b)*c);
        obs_head = Array{String,1}(undef,(3*b)*c);

        r = 1:3:(3*b)*c
        for i in 1:length(r)
            Observables[:,r[i]] = pseudo_def["tsamps"][expp];
            obs_head[r[i]] = string("time",i);
        end

        co = 2;
        for i in 1:c
            for j in 1:b
                Observables[:,co] = pseudo_res["PData"][string("PDExp_", expp)][:,j,i];
                Observables[:,co+1] = pseudo_res["PError"][string("PDExp_", expp)][:,j,i]
                obs_head[co] = string(pseudo_def["Obs"][j],"_theta", i, "_Mean");
                obs_head[co+1] = string(pseudo_def["Obs"][j],"_theta", i, "_Std");
                co +=3;
            end
        end

        df2 = DataFrame[Observables][1]
        rename!(df2, [obs_head[i] for i in 1:(3*b)*c])
        CSV.write(string(pseudo_def["savepath"], "\\PseudoDataFiles\\",model_def["NameF"],"_EXP",expp, "_",
                pseudo_def["flag"],"_Observables.csv"), df2);

        # Event Inputs matrix
        if model_def["nInp"] != 0
            EvnInputs = zeros(length(pseudo_def["uInd"][expp]),2+(model_def["nInp"]*2));
            evins_head = Array{String,1}(undef,2+(model_def["nInp"]*2));
            evins_head[1] = "Switchingtimes";
            evins_head[2] = "FinalTime";

            EvnInputs[:,1] = pseudo_def["switchT"][expp][1:end-1];
            EvnInputs[:,2] = repeat([pseudo_def["switchT"][expp][end]], outer = [length(pseudo_def["uInd"][expp])]);

            for i in 1:model_def["nInp"]
                if pseudo_def["preInd"] != []
                    EvnInputs[:,(2+i)] = repeat([pseudo_def["preInd"][expp][i]], outer = [length(pseudo_def["uInd"][expp])]);
                else
                    EvnInputs[:,(2+i)] = repeat([0], outer = [length(pseudo_def["uInd"][expp])]);
                end
                evins_head[(2+i)] = string(model_def["inpName"][i],"_Pre");
                EvnInputs[:,(2+model_def["nInp"])+i] = pseudo_def["uInd"][expp];
                evins_head[(2+model_def["nInp"])+i] = string(model_def["inpName"][i]);
            end

            df3 = DataFrame[EvnInputs][1]
            rename!(df3, [evins_head[i] for i in 1:2+(model_def["nInp"]*2)])
            CSV.write(string(pseudo_def["savepath"], "\\PseudoDataFiles\\",model_def["NameF"],"_EXP",expp, "_",
                    pseudo_def["flag"],"_Events_Inputs.csv"), df3);
        end

    end

end


using BenchmarkTools,Knet

const N=1000

const sizes = (1,10,100,1000)

function f2(x,r)
    nx = length(x)
    if r==0
        for i=1:N; y=ccall(("sum_32_20",Knet.libknet8),Float32,(Cint,Ptr{Float32}),nx,x); end
    elseif r==1
        y = similar(x, 1, size(x,2)); ny = length(y); sy = stride(x,2)
        for i=1:N; ccall(("sum_32_21",Knet.libknet8),Nothing,(Cint,Ptr{Float32},Cint,Cint,Ptr{Float32}),nx,x,sy,ny,y); end
    elseif r==2
        y = similar(x, size(x,1), 1); ny = length(y); sy = stride(x,1)
        for i=1:N; ccall(("sum_32_21",Knet.libknet8),Nothing,(Cint,Ptr{Float32},Cint,Cint,Ptr{Float32}),nx,x,sy,ny,y); end
    else
        error("not supported")
    end
    Knet.cudaDeviceSynchronize()
    return y
end

for region in (0,1,2)
    println("sum(a,$region)")
    for s in sizes; print("\t$s"); end; println()
    for nrows in sizes
        print(nrows)
        for ncols in sizes
            a = KnetArray(rand(Float32,nrows,ncols))
            bm = @benchmark f2($a,$region) seconds=1
            mm = round(Int, minimum(bm.times)/N)
            print("\t$mm")
            Knet.gc()
        end
        println()
    end
end

# commit 359d3646 2018-09-22, julia 1.0.0 vs commit 4aa5f92f 2018-08-14, julia 0.6.4
# GPU:V100, CPU:Intel(R) Xeon(R) CPU E5-2680 v4 @ 2.40GHz
#
# sum(a,0)                      		sum(a,0)
# 	1	10	100	1000            	1	10	100	1000
# 1	17834	17643	18922	19007           1	17134	17165	18364	18395
# 10	18977	18978	18977	19006           10	18448	18362	18411	18404
# 100	18974	18965	18989	19064           100	18406	17920	17924	17543
# 1000	18954	18945	19013	27356           1000	17560	17205	17169	25933
# sum(a,1)                                      sum(a,1)
# 	1	10	100	1000            	1	10	100	1000
# 1	3335	3326	3322	5727            1	3188	3190	3191	5721
# 10	3327	3339	3326	6611            10	3189	3192	3191	6596
# 100	3319	3595	3588	6796            100	3492	3482	3486	6777
# 1000	3615	3637	3630	11536           1000	3501	3503	3380	11583
# sum(a,2)                                      sum(a,2)
# 	1	10	100	1000            	1	10	100	1000
# 1	3641	3615	3623	3620            1	3347	3203	3208	3300
# 10	3533	3441	3458	3842            10	3200	3197	3206	3804
# 100	3535	3447	3341	4300            100	3029	3033	3026	4295
# 1000	5782	5974	6923	24663           1000	5767	5967	7001	24780

# COMMIT 3d32e16 2017-05-17 merge
#
# sum(a,0)
# 	1	10	100	1000
# 1	31804	31817	31814	31822
# 10	31790	31852	31827	32158
# 100	31848	31876	32085	35014
# 1000	31831	32072	35023	65961
# sum(a,1)
# 	1	10	100	1000
# 1	4689	4689	4931	13799
# 10	4936	4938	5436	16528
# 100	4936	4938	5470	17198
# 1000	6457	6608	7302	48299
# sum(a,2)
# 	1	10	100	1000
# 1	4690	4689	4691	6779
# 10	4690	4748	4885	8096
# 100	4935	5208	6864	26480
# 1000	13809	15307	32166	216076

# COMMIT: ccad0cb 2017-04-15 (added general reductions, same as last)
# sum(a,0)
# 	1	10	100	1000
# 1	31741	31853	31864	31853
# 10	31779	31804	31899	32097
# 100	31818	31895	32155	35172
# 1000	31871	32094	35185	66605
# sum(a,1)
# 	1	10	100	1000
# 1	4619	4689	4892	13813
# 10	4885	4905	5404	16503
# 100	4848	4914	5442	17207
# 1000	6390	6579	7300	48356
# sum(a,2)
# 	1	10	100	1000
# 1	4611	4621	4606	6692
# 10	4689	4731	4830	8054
# 100	4887	5104	6839	26471
# 1000	13841	15296	32159	217215

# COMMIT: c78d436 2017-04-04 (fixed large array bug, but slowed last col sum(a,1) and last row sum(a,2))
# sum(a,0)
# 	1	10	100	1000
# 1	32333	32391	32305	32404
# 10	32388	32403	32440	32703
# 100	32411	32407	32668	35571
# 1000	32362	32618	35586	66601
# sum(a,1)
# 	1	10	100	1000
# 1	4690	4690	4924	13812
# 10	4939	4941	5427	16532
# 100	4932	4938	5470	17179
# 1000	6506	6583	7299	48242
# sum(a,2)
# 	1	10	100	1000
# 1	4690	4691	4691	6773
# 10	4690	4750	4884	8085
# 100	4929	5197	6866	26475
# 1000	13841	15303	32141	216153

# COMMIT: 89c2026 2017-04-02 (buggy with large arrays)
# sum(a,0)
# 	1	10	100	1000
# 1	32326	32417	32342	32467
# 10	32390	32346	32232	32597
# 100	32212	32345	32571	35466
# 1000	32363	32547	35519	66645
# sum(a,1)
# 	1	10	100	1000
# 1	4470	4528	4777	10954
# 10	4713	4777	5142	12531
# 100	4699	4778	5200	12915
# 1000	6245	6435	7126	42014
# sum(a,2)
# 	1	10	100	1000
# 1	4480	4481	4483	6546
# 10	4541	4601	4695	7929
# 100	4777	4975	6781	26433
# 1000	10970	12016	28019	208529