# Load dependencies

```julia
#load dependencies
#load in dependencies to read data
using CSV, Plots, DataFrames, Dates
using LinearAlgebra,Pkg
#using DifferentialEquations # do not need the whole package necessarilly. Using just the ODE functions will save some memory
using OrdinaryDiffEq
#show versions
Pkg.status()
```

```julia
versioninfo()
```

# Spatial Epidemic Model for GA for COVID-19

Here, we extend the model to look at infection spread through a spatial metapopulation model.
To do this, we consider that individuals spend time in the local community, but also may commute - for work, errands, social activities or other things. Following this we split the population $N_{tot}$ into two compartments $N_{local}$ and $N_{commute}$. $N_{local}$ is the population who live in a particular county, whilst $N_{commute} = N_{local} - N_{leave} + N_{arrive}$. Here $N_{leave}$ are those who leave the county, whilst $N_{arrive}$ are those who travel into the county. We do not explicitly account for those arriving or leaving in the local ODE's.

We assume that those in the county see the local population for a proportion of time $w_{local}$ and the commuter population for a proportion of time $w_{commmute}$, where we assume $w_{local} + w_{commute} = 1$.

The system of nonlinear differential equations governing this patch and age-structured epidemiological for patch $i$ and age $a$ are as follows:

\begin{align}
\frac{dS(i,a)}{dt} & =  -\overset{new\ cases\ from\ symptomatic\ contact}{\overbrace{\kappa\beta_S S(i,a) \left( w_{local}\frac{ I_{S,local}(i)}{N_{local}(i)} + w_{commute}\frac{ I_{S,commute}(i)}{N_{commute}(i)} \right) }}  - \overset{new\ cases\ from\ asymptomatic\ contact}{\overbrace{\kappa\beta_A S(i,a) \left( w_{local}\frac{ I_{A,local}(i)}{N_{local}(i)} + w_{commute}\frac{ I_{A,commute}(i)}{N_{commute}(i)} \right) }}  \\ \\
\frac{dE(i,a)}{dt} & =  \overset{new\ cases\ from\ symptomatic\ contact}{\overbrace{\kappa\beta_S S(i,a) \left( w_{local}\frac{ I_{S,local}(i)}{N_{local}(i)} + w_{commute}\frac{ I_{S,commute}(i)}{N_{commute}(i)} \right) }}  + \overset{new\ cases\ from\ asymptomatic\ contact}{\overbrace{\kappa\beta_A S(i,a) \left( w_{local}\frac{ I_{A,local}(i)}{N_{local}(i))} + w_{commute}\frac{ I_{A,commute}(i)}{N_{commute}(i)} \right) }}  - \overset{onset\ of\ infectiousness}{\overbrace{\gamma_E E(i,a)}} \\
\frac{dI_A(i,a)}{dt} & =  \overset{asymptomatic\ onset}{\overbrace{p\gamma_E E(i,a)}} - \overset{recovery}{\overbrace{\gamma_A I_A(i,a)}} \\
\frac{dI_S(i,a)}{dt} & =  \overset{symptomatic\ onset}{\overbrace{(1-p)\gamma_E E(i,a)}} - \overset{transfer\ from\ I_S}{\overbrace{\gamma_S I_S(i,a)}} \\
\frac{dI_{h,sub}(i,a)}{dt} & =  \overset{subcritical\ cases}{\overbrace{h(a)(1-\xi(a)) \gamma_S I_S(i,a)}} - \overset{transfer\ from\ I_{h,sub}}{\overbrace{\gamma_h I_{h,sub}(i,a)}} \\
\frac{dI_{h,crit}(i,a)}{dt} & =  \overset{critical\ (ICU)\ cases}{\overbrace{h(a)\xi(a) \gamma_S I_S(i,a)}} - \overset{transfer\ from\ I_{h,crit}}{\overbrace{\gamma_h I_{h,crit}(i,a)}} \\
\frac{dR(a)}{dt} & =  \overset{recovery\ from\ I_A}{\overbrace{\gamma_A I_A(i,a)}} + \overset{recovery\ from\ I_S}{\overbrace{(1-h(a))\gamma_S I_S(i,a)}} + \overset{recovery\ from\ I_{h,sub}}{\overbrace{\gamma_h I_{h,sub}(i,a)}} + \overset{recovery\ from\ I_{h,crit}}{\overbrace{(1-\mu)\gamma_h I_{h,crit}(i,a)}} \\
\frac{dD(i,a)}{dt} & = \overset{mortality}{\overbrace{\mu \gamma_h I_{h,crit}(i,a)}}
\end{align}

where $I_{S,local}$ and $I_{S,commute}$ are the total number (across all age classes) of sympomatic infectious individuals; $I_{A,local}$ and $I_{S,commute}$ are the total number of asymptomatic infectious individuals; and $N_{local}$ and $N_{commute}$ are the total number of alive individuals during local and commute times.

# Read in functions called in the scripts

```julia
#ODE model code
#assumes age-dependent journeys between counties
function SpatialAgeStructuredEpidemiologicalModelMixAge!(du,u,par,t)
    SDI,β_a,β_s,p,γ_e,γ_a,γ_s,γ_h,h,ξ,μ,S_ind,E_ind,
    Ia_ind,Is_ind,Ihsub_ind,Ihcrit_ind,R_ind,D_ind,
    NumPatches,NumAgeClasses,patchstates,patchstatesALIVE,
    patchstatesMOVEABLE,w,wy,wo,transport,ISMOVEFACTOR,w1,w2 = par

    #NumPatches is the number of spatial "nodes"
    #w is a matrix of size NxN determining flows between nodes.
    #transport is a matrix to store movements vectors (don't want to recreate this object throughout this function)
    #patchstates - total number of states in a patch
    #patchstatesALIVE - number of non-dead states
    #patchstatesMOVEABLE - number of states that are allowed to move between patches
    #S_ind,E_ind,Ia_ind,Is_ind,Ihsub_ind,Ihcrit_ind,R_ind,D_ind - indexes of the states for the first patch i.e. number 1:80

    #have matrix w, representing movement of commuters i.e. X1 stay in county, X2 go to county 2, X3 go to county3
    #total travel is fraction travelled applied to people in moving states
    #some amounts of these will be IA and IS - keep track!
    #want to know N,Is,IA leaving each county; and arriving at each county
    #w11 S1 + w12 S2  #net change for each state so long as diagonal is -ve.

    #w is those aged 30-59
    #wy is those 0-29
    #wo is those 60+

    #Stage 1:
    #calculate transport between patches

    #transport = transport .* 0.0;
    #want this to be age structured
    for ii = 1:patchstatesMOVEABLE #for each age class of alive states that can move.
        INDS = ii:patchstates:(ii+(NumPatches-1)*patchstates ) #gives 1xpatch indices for transport multiplication for each age class and each state that can move
        MOD = mod(ii,NumAgeClasses-1)
        if MOD>0 & MOD<4
        transport[:,ii] = wy*u[INDS]
        elseif MOD==0  || MOD >=7
        transport[:,ii] = wo*u[INDS]
        else
        transport[:,ii] =  w*u[INDS]  #add the 1*patch vector of movement fluxes to transport
        end
    end

    #modify tranport matrix to account for Is people deciding to move or not.
    transport[:,Is_ind] = ISMOVEFACTOR .* transport[:,Is_ind]

    #calculate net change for each patch for N,Ia and Is
    C_TOT_NETCHANGE = sum(transport,dims=2)
    C_I_A_NETCHANGE = sum(transport[:,Ia_ind],dims=2)
    C_I_S_NETCHANGE = sum(transport[:,Is_ind],dims=2)

    #Stage 2:
    #Go through the ODE system node by node. As have found fluxes between nodes already, this will be a synchronous update of local and spatial dynamics.

    for aa=1:(Int64(NumPatches)) #for each patch node

        #Local patch dynamics depend on patch quantitites
        NTOT_Loc = sum(u[((aa-1)*patchstates+1):((aa-1)*patchstates+patchstatesALIVE)]) #total alive
        TOTI_a_Loc = sum(u[(aa-1)*patchstates .+ Ia_ind]) #total asymptomatic infected
        TOTI_s_Loc = sum(u[(aa-1)*patchstates .+ Is_ind]) #total symptomatic infected

        NTOT_Com = NTOT_Loc + C_TOT_NETCHANGE[aa]#total alive
        TOTI_a_Com = TOTI_a_Loc + C_I_A_NETCHANGE[aa] #total asymptomatic infected
        TOTI_s_Com = TOTI_s_Loc + C_I_S_NETCHANGE[aa] #total symptomatic infected

        LOCAL = w1/(NTOT_Loc)
        COMMUTE = w2/(NTOT_Com)
        propI_A = ( TOTI_a_Loc*LOCAL + TOTI_a_Com*COMMUTE )
        propI_S = ( TOTI_s_Loc*LOCAL + TOTI_s_Com*COMMUTE )

        for bb = 1:(Int64(NumAgeClasses))  #for each age class in this patch
        INDS = (aa-1)*patchstates .+ [S_ind[bb],E_ind[bb],Ia_ind[bb],Is_ind[bb],R_ind[bb],Ihsub_ind[bb],Ihcrit_ind[bb],D_ind[bb]]

            S,E,Ia,Is,R,Ihsub,Ihcrit,D = u[INDS]

            du[INDS[1]] =  -β_a*SDI*S*propI_A - β_s*SDI*S*propI_S   #dS/dt
            du[INDS[2]] =  +β_a*SDI*S*propI_A + β_s*SDI*S*propI_S - γ_e*E  #dE/dt
            du[INDS[3]] = p[bb]*γ_e*E - γ_a*Ia   #dI_a /dt
            du[INDS[4]] = (1-p[bb])*γ_e*E - γ_s*Is  #dI_s/dt
            du[INDS[5]] = γ_a*Ia + (1-h[bb])*γ_s*Is + γ_h*Ihsub + (1-μ[bb])*γ_h*Ihcrit #dR/dt
            du[INDS[6]] = h[bb]*(1-ξ[bb])*γ_s*Is - γ_h*Ihsub #dIhsub/dt
            du[INDS[7]] = h[bb]*ξ[bb]*γ_s*Is -  γ_h*Ihcrit #dIhcrit/dt
            du[INDS[8]] = μ[bb]*γ_h*Ihcrit #dD/dt
        end
    end

end

```

```julia
#ODE model code - this is used to model at the level of the whole of Georgia! (helpful in setting initial conditions)
function SingleNode!(du,u,par,t)
    SDI,β_a,β_s,p,γ_e,γ_a,γ_s,γ_h,h,ξ,μ,S_ind,E_ind,Ia_ind,Is_ind,R_ind,Ihsub_ind,Ihcrit_ind,D_ind = par

    TOTI_a = sum(u[Ia_ind]) #Total asymptomatic infectious individuals
    TOTI_s = sum(u[Is_ind]) #Total symptomatic infectious individuals
    NTOT = sum(u[1:63]) #All individuals not in the dead (D) state

    for aa = 1:length(S_ind)#for each age class
        INDS = [S_ind[aa],E_ind[aa],Ia_ind[aa],Is_ind[aa],R_ind[aa],Ihsub_ind[aa],Ihcrit_ind[aa],D_ind[aa]]
        S,E,Ia,Is,R,Ihsub,Ihcrit,D = u[INDS]
        du[INDS[1]] = -β_a*SDI*S*TOTI_a/(NTOT) - β_s*SDI*S*TOTI_s/(NTOT) #dS/dt
        du[INDS[2]] = +β_a*SDI*S*TOTI_a/(NTOT) + β_s*SDI*S*TOTI_s/(NTOT) -  γ_e*E   #dE/dt
        du[INDS[3]] = p[aa]*γ_e*E - γ_a*Ia  #dI_a /dt
        du[INDS[4]] = (1-p[aa])*γ_e*E - γ_s*Is  #dI_s/dt
        du[INDS[5]] = γ_a*Ia + (1-h[aa])*γ_s*Is + γ_h*Ihsub + (1-μ[aa])*γ_h*Ihcrit  #dR/dt
        du[INDS[6]] = h[aa]*(1-ξ[aa])*γ_s*Is - γ_h*Ihsub #dIhsub/dt
        du[INDS[7]] = h[aa]*ξ[aa]*γ_s*Is -  γ_h*Ihcrit #dIhcrit/dt
        du[INDS[8]] = μ[aa]*γ_h*Ihcrit #dD/dt
    end

end
```

```julia
#reads in ODE solution; and saves output summary figures and CSV files
function WriteOutput!(PatchFIPS,PatchNames,Regions,GAPOP,TODAY,TIMERESOLTION,T_END,sol,pars,SaveName)
    SDI,β_a,β_s,p,γ_e,γ_a,γ_s,γ_h,h,ξ,μ,S_ind,E_ind,
        NumPatches,NumAgeClasses,patchstates,patchstatesALIVE,
        patchstatesMOVEABLE,w,wy,wo,transport,ISMOVEFACTOR,w1,w2 = pars

    Gath = gatherStates!(sol,pars)
    STATES = GAPOP * Gath[1] #organise the solution
    Times = sol.t[Gath[2]]

    #patches  x EpiStates x AgeClasses x Time
    SIZES = size(STATES)

    #Make summary tables for each epi state, and total hospitalised
    #S,E,Ia,Is,R,Ihsub,Ihcrit,D, Ihsub+Ihcrit
    S = zeros(SIZES[1],SIZES[4]) # pacthes x time
    E = zeros(SIZES[1],SIZES[4]) # pacthes x time
    Ia = zeros(SIZES[1],SIZES[4]) # pacthes x time
    Is = zeros(SIZES[1],SIZES[4]) # pacthes x time
    R = zeros(SIZES[1],SIZES[4]) # pacthes x time
    Ihsub = zeros(SIZES[1],SIZES[4]) # pacthes x time
    Ihcrit = zeros(SIZES[1],SIZES[4]) # pacthes x time
    D = zeros(SIZES[1],SIZES[4]) # pacthes x time
    Htot = zeros(SIZES[1],SIZES[4]) # pacthes x time
    Dperday = zeros(SIZES[1],SIZES[4]) # pacthes x time
    NewCperday = zeros(SIZES[1],SIZES[4]) # pacthes x time

    for aa = 1:SIZES[1] #for each patch
        S[aa,:] = dropdims(sum(STATES[aa,1,1:9,:],dims=1),dims=1)
        E[aa,:] = dropdims(sum(STATES[aa,2,1:9,:],dims=1),dims=1)
        Ia[aa,:] = dropdims(sum(STATES[aa,3,1:9,:],dims=1),dims=1)
        Is[aa,:] = dropdims(sum(STATES[aa,4,1:9,:],dims=1),dims=1)
        R[aa,:] = dropdims(sum(STATES[aa,5,1:9,:],dims=1),dims=1)
        Ihsub[aa,:] = dropdims(sum(STATES[aa,6,1:9,:],dims=1),dims=1)
        Ihcrit[aa,:] = dropdims(sum(STATES[aa,7,1:9,:],dims=1),dims=1)
        D[aa,:] = dropdims(sum(STATES[aa,8,1:9,:],dims=1),dims=1)
        Htot[aa,:] =  Ihsub[aa,:] .+ Ihcrit[aa,:]
        for bb = 1:SIZES[3] #for age classes
            #Dperday[aa,:] .+=  μ[bb].*γ_h .*STATES[aa,7,bb,:].*1.0;  #deaths per day
            Dperday[aa,:] .+= [( STATES[aa,8,bb,2:end] .- STATES[aa,8,bb,1:(end-1)]) ./ (Times[2:end] - Times[1:(end-1)]); 0]
            NewCperday[aa,:] .+= [( STATES[aa,1,bb,1:(end-1)] .- STATES[aa,1,bb,2:end]) ./ (Times[2:end] - Times[1:(end-1)]); 0]
        end
    end

    #build Time column names
    colnames = Array{Date,1}(undef,1)
    colnames[1] = TODAY
    for aa = 2:(SIZES[4])#for each time
        push!(colnames,colnames[aa-1]+Dates.Day(convert(Int,Times[aa]-Times[aa-1])))
    end

    #build dataframes
    SSdata = DataFrame(S)
    rename!(SSdata , Symbol.(colnames))
    insertcols!(SSdata, 1, :FIP => PatchFIPS)
    insertcols!(SSdata, 2, :County => PatchNames)

    EEdata = DataFrame(E)
    rename!(EEdata , Symbol.(colnames))
    insertcols!(EEdata, 1, :FIP => PatchFIPS)
    insertcols!(EEdata, 2, :County => PatchNames)

    Iadata = DataFrame(Ia)
    rename!(Iadata , Symbol.(colnames))
    insertcols!(Iadata, 1, :FIP => PatchFIPS)
    insertcols!(Iadata, 2, :County => PatchNames)

    Isdata = DataFrame(Is)
    rename!(Isdata , Symbol.(colnames))
    insertcols!(Isdata, 1, :FIP => PatchFIPS)
    insertcols!(Isdata, 2, :County => PatchNames)

    RRdata = DataFrame(R)
    rename!(RRdata , Symbol.(colnames))
    insertcols!(RRdata, 1, :FIP => PatchFIPS)
    insertcols!(RRdata, 2, :County => PatchNames)

    Ihsubdata = DataFrame(Ihsub)
    rename!(Ihsubdata , Symbol.(colnames))
    insertcols!(Ihsubdata, 1, :FIP => PatchFIPS)
    insertcols!(Ihsubdata, 2, :County => PatchNames)

    Ihcritdata = DataFrame(Ihcrit)
    rename!(Ihcritdata , Symbol.(colnames))
    insertcols!(Ihcritdata, 1, :FIP => PatchFIPS)
    insertcols!(Ihcritdata, 2, :County => PatchNames)

    DDdata = DataFrame(D)
    rename!(DDdata , Symbol.(colnames))
    insertcols!(DDdata, 1, :FIP => PatchFIPS)
    insertcols!(DDdata, 2, :County => PatchNames)

    HHdata = DataFrame(Htot)
    rename!(HHdata , Symbol.(colnames))
    insertcols!(HHdata, 1, :FIP => PatchFIPS)
    insertcols!(HHdata, 2, :County => PatchNames)

    DpDdata = DataFrame(Dperday)
    rename!(DpDdata , Symbol.(colnames))
    insertcols!(DpDdata, 1, :FIP => PatchFIPS)
    insertcols!(DpDdata, 2, :County => PatchNames)

    CpDdata = DataFrame(NewCperday)
    rename!(CpDdata , Symbol.(colnames))
    insertcols!(CpDdata, 1, :FIP => PatchFIPS)
    insertcols!(CpDdata, 2, :County => PatchNames)

    #make a directory to store files
    mkdir(SaveName)

    #write data to file
    CSV.write(string(SaveName,"/GA_county_Susceptibles.csv"),SSdata)
    CSV.write(string(SaveName,"/GA_county_Exposed.csv"),EEdata)
    CSV.write(string(SaveName,"/GA_county_AsymptInfected.csv"),Iadata)
    CSV.write(string(SaveName,"/GA_county_SymptInfected.csv"),Isdata)
    CSV.write(string(SaveName,"/GA_county_Recovered.csv"),RRdata)
    CSV.write(string(SaveName,"/GA_county_subacuteHospitalised.csv"),Ihsubdata)
    CSV.write(string(SaveName,"/GA_county_criticalHospitalised.csv"),Ihcritdata)
    CSV.write(string(SaveName,"/GA_county_Killed.csv"),DDdata)
    CSV.write(string(SaveName,"/GA_county_totalHospitalised.csv"),HHdata)
    CSV.write(string(SaveName,"/GA_county_deathrateperday.csv"),DpDdata)
    CSV.write(string(SaveName,"/GA_county_newcaserateperday.csv"),CpDdata)

    ##save summary data at RegionalLevel

    RegionList = sort(unique(Regions))
    SSdataR = zeros(length(RegionList),SIZES[4])
    EEdataR = zeros(length(RegionList),SIZES[4])
    IadataR = zeros(length(RegionList),SIZES[4])
    IsdataR = zeros(length(RegionList),SIZES[4])
    IhsubdataR = zeros(length(RegionList),SIZES[4])
    IhcritdataR = zeros(length(RegionList),SIZES[4])
    RRdataR = zeros(length(RegionList),SIZES[4])
    DDdataR = zeros(length(RegionList),SIZES[4])
    HHdataR = zeros(length(RegionList),SIZES[4])
    DpDdataR = zeros(length(RegionList),SIZES[4])
    CpDdataR = zeros(length(RegionList),SIZES[4])

    for aa = 1:length(RegionList) #for each region
        RegionInd = findall(Regions.== RegionList[aa])
        SSdataR[aa,:] = sum(S[RegionInd,:],dims=1)
        EEdataR[aa,:] = sum(E[RegionInd,:],dims=1)
        IadataR[aa,:] = sum(Ia[RegionInd,:],dims=1)
        IsdataR[aa,:] = sum(Is[RegionInd,:],dims=1)
        RRdataR[aa,:] = sum(R[RegionInd,:],dims=1)
        DDdataR[aa,:] = sum(D[RegionInd,:],dims=1)
        IhsubdataR[aa,:] = sum(Ihsub[RegionInd,:],dims=1)
        IhcritdataR[aa,:] = sum(Ihcrit[RegionInd,:],dims=1)
        HHdataR[aa,:] = sum(Htot[RegionInd,:],dims=1)
        DpDdataR[aa,:] = sum(Dperday[RegionInd,:],dims=1)
        CpDdataR[aa,:] = sum(NewCperday[RegionInd,:],dims=1)
    end

    SSdataRR = DataFrame(SSdataR)
    rename!(SSdataRR , Symbol.(colnames))
    insertcols!(SSdataRR, 1, :Region => RegionList)

    EEdataRR = DataFrame(EEdataR)
    rename!(EEdataRR , Symbol.(colnames))
    insertcols!(EEdataRR, 1, :Region => RegionList)

    IadataRR = DataFrame(IadataR)
    rename!(IadataRR , Symbol.(colnames))
    insertcols!(IadataRR, 1, :Region => RegionList)

    IsdataRR = DataFrame(IsdataR)
    rename!(IsdataRR , Symbol.(colnames))
    insertcols!(IsdataRR, 1, :Region => RegionList)

    RRdataRR = DataFrame(RRdataR)
    rename!(RRdataRR , Symbol.(colnames))
    insertcols!(RRdataRR, 1, :Region => RegionList)

    IhsubdataRR = DataFrame(IhsubdataR)
    rename!(IhsubdataRR , Symbol.(colnames))
    insertcols!(IhsubdataRR, 1, :Region => RegionList)

    IhcritdataRR = DataFrame(IhcritdataR)
    rename!(IhcritdataRR , Symbol.(colnames))
    insertcols!(IhcritdataRR, 1, :Region => RegionList)

    DDdataRR = DataFrame(DDdataR)
    rename!(DDdataRR , Symbol.(colnames))
    insertcols!(DDdataRR, 1, :Region => RegionList)

    HHdataRR = DataFrame(HHdataR)
    rename!(HHdataRR , Symbol.(colnames))
    insertcols!(HHdataRR, 1, :Region => RegionList)

    DpDdataRR = DataFrame(DpDdataR)
    rename!(DpDdataRR , Symbol.(colnames))
    insertcols!(DpDdataRR, 1, :Region => RegionList)

    CpDdataRR = DataFrame(CpDdataR)
    rename!(CpDdataRR , Symbol.(colnames))
    insertcols!(CpDdataRR, 1, :Region => RegionList)

    CSV.write(string(SaveName,"/GA_region_Susceptibles.csv"),SSdataRR)
    CSV.write(string(SaveName,"/GA_region_Exposed.csv"),EEdataRR)
    CSV.write(string(SaveName,"/GA_region_AsymptInfected.csv"),IadataRR)
    CSV.write(string(SaveName,"/GA_region_SymptInfected.csv"),IsdataRR)
    CSV.write(string(SaveName,"/GA_region_Recovered.csv"),RRdataRR)
    CSV.write(string(SaveName,"/GA_region_subacuteHospitalised.csv"),IhsubdataRR)
    CSV.write(string(SaveName,"/GA_region_criticalHospitalised.csv"),IhcritdataRR)
    CSV.write(string(SaveName,"/GA_region_Killed.csv"),DDdataRR)
    CSV.write(string(SaveName,"/GA_region_totalHospitalised.csv"),HHdataRR)
    CSV.write(string(SaveName,"/GA_region_deathrateperday.csv"),DpDdataRR)
    CSV.write(string(SaveName,"/GA_region_newcaserateperday.csv"),CpDdataRR)

    #Save summary data at County Level
    #sum across counties
    GASummary = zeros(11,SIZES[4])
    GASummary[1,:] = sum(S,dims=1)
    GASummary[2,:] = sum(E,dims=1)
    GASummary[3,:] = sum(Ia,dims=1)
    GASummary[4,:] = sum(Is,dims=1)
    GASummary[5,:] = sum(R,dims=1)
    GASummary[6,:] = sum(Ihsub,dims=1)
    GASummary[7,:] = sum(Ihcrit,dims=1)
    GASummary[8,:] = sum(D,dims=1)
    GASummary[9,:] = sum(Htot,dims=1)
    GASummary[10,:] = sum(Dperday,dims=1)
    GASummary[11,:] = sum(NewCperday,dims=1)

    GASum = DataFrame(GASummary)
    rename!(GASum , Symbol.(colnames))
    insertcols!(GASum, 1,:EpiState => ["S","E","Ia","Is","R","Ihsub","Ihcrit","D","Htot","Dperday","NCperday"])

    CSV.write(string(SaveName,"/GA_Summary.csv"),GASum)

end
```

```julia
#Used to turn the solution variable 1x(159*9*8) into an array that can be more easily parsed
function gatherStates!(sol,par)
    SDI,β_a,β_s,p,γ_e,γ_a,γ_s,γ_h,h,ξ,μ,S_ind,E_ind,
    Ia_ind,Is_ind,Ihsub_ind,Ihcrit_ind,R_ind,D_ind,
    NumPatches,NumAgeClasses,patchstates,patchstatesALIVE,
    patchstatesMOVEABLE,w,wy,wo,transport,ISMOVEFACTOR,w1,w2 = par

    #find time indices (in case of duplicates)
    UU = unique(sol.t)
    TS = Array{Int64,1}(undef,0)
    for aa = 1:length(UU)
       push!(TS,findall(sol.t .== UU[aa])[1])
    end

    Sols =  hcat(sol.u...)

    ARRAYSTORE = zeros(NumPatches,8,NumAgeClasses,length(TS)) #array of patches x epi states x ages x time

    for aa = 1:NumPatches
        for bb = 1:NumAgeClasses
        INDS = (aa-1)*patchstates .+ [S_ind[bb],E_ind[bb],Ia_ind[bb],Is_ind[bb],R_ind[bb],Ihsub_ind[bb],Ihcrit_ind[bb],D_ind[bb]]
            for cc=1:8
                for dd = 1:length(TS)
                    ARRAYSTORE[aa,cc,bb,dd] = Sols[INDS[cc],TS[dd]]
                end
            end
        end
    end
    return ARRAYSTORE,TS
end
```

```julia
#summary plot - plots the summed epi. states at the Georgia level i.e. dropping spatial dimension
function SummaryPlot!(JAM,T)
SZ = size(JAM)
#AA=dropdims(sum(JAM[aa,1,1:9,:],dims=1),dims=1)[end]
S = zeros(SZ[4])
E = zeros(SZ[4])
Ia = zeros(SZ[4])
Is = zeros(SZ[4])
R = zeros(SZ[4])
Ihs = zeros(SZ[4])
Ihc = zeros(SZ[4])
D = zeros(SZ[4])

for aa = 1:SZ[1]
    S .+= dropdims(sum(JAM[aa,1,1:9,:],dims=1),dims=1)
    E .+= dropdims(sum(JAM[aa,2,1:9,:],dims=1),dims=1)
    Ia .+= dropdims(sum(JAM[aa,3,1:9,:],dims=1),dims=1)
    Is .+= dropdims(sum(JAM[aa,4,1:9,:],dims=1),dims=1)
    R .+= dropdims(sum(JAM[aa,5,1:9,:],dims=1),dims=1)
    Ihs .+= dropdims(sum(JAM[aa,6,1:9,:],dims=1),dims=1)
    Ihc .+= dropdims(sum(JAM[aa,7,1:9,:],dims=1),dims=1)
    D .+= dropdims(sum(JAM[aa,8,1:9,:],dims=1),dims=1)
end

pl = plot(T,S,label="S",yguidefontsize=10,xlabel="Time (days)",ylabel="People",legend=:outertopright)
plot!(T,E,label="E")
plot!(T,Ia,label="Ia")
plot!(T,Is,label="Is")
plot!(T,R,label="R")
plot!(T,Ihs,label="Ihsub")
plot!(T,Ihc,label="Ihcrit")
plot!(T,D,label="D")

return pl
end
```

# Set up the model scripts

```julia
#######################################STUFFTO EDIT ######################
#Time stepping stuff
TIMERESOLUTION = 1 #MAKE THIS AN INTEGER IN DAYS!!!!!! REQUIRED FOR SAVING LATER ON
T_END = 150
tspanrun = (0.0,T_END*1.0) #run the simulations between 0 days and 400 days
TODAY = Date(2020,3,28) #Date format of time 0

#Load in Data used to set time 0
CasesbyCounty = CSV.read("./data/28March2020.csv");
HospitalRegions = CSV.read("./data/ga_hosp_regions.csv")
HospRegions = HospitalRegions.HospRegion

#String denoting where to save output

#SAVEFOLD = "SimulationOutput/March28_sim"
SAVEFOLD = "SimulationOutput/March28_eig"

#initial condition setting to use (1 - uses a simulation method county by county; 2 - uses the state level model to partition to counties)
#other choices are archives right now from previous attempts

#CHOICES = 1
CHOICES = 2

#######################################STUFFTO EDIT ######################
```

```julia
#Model states
NumPatches = 159 ; #159 counties
NumAgeClasses = 9; # 0-9,10-19,20-29,30-39,40-49,50-59,60-69,70-79,80+
NumEpiStates = 8; # S,E,Ia,Is,R,Ihsub,Ihcrit,D

patchstates = NumAgeClasses*NumEpiStates
patchstatesMOVEABLE = (NumEpiStates-3)*NumAgeClasses  #D cannot move. Ihsub, Ihcrit cannot move.
#Say that I_s may be able to move - can modify this assumption later.
patchstatesALIVE = (NumEpiStates-1)*NumAgeClasses #all but D are alive
transport = zeros(NumPatches,patchstatesMOVEABLE);

#Epidemiological state indexing - just for within one patch.
S_ind = 1:NumAgeClasses
E_ind = (1*NumAgeClasses+1):2*NumAgeClasses
Ia_ind = (2*NumAgeClasses+1):3*NumAgeClasses
Is_ind = (3*NumAgeClasses+1):4*NumAgeClasses
R_ind = (4*NumAgeClasses+1):5*NumAgeClasses
Ihsub_ind = (5*NumAgeClasses+1):6*NumAgeClasses
Ihcrit_ind = (6*NumAgeClasses+1):7*NumAgeClasses
D_ind = (7*NumAgeClasses+1):8*NumAgeClasses

#Age class information
MinAge = [0,10,20,30,40,50,60,70,80]
MiddleAge = [5,15,25,35,45,55,65,75,90];
```

```julia
#read in the GA level data

ODL29 = CSV.read("./data/ga_countyOD_29_younger.csv") # transport matrix for 0-29
OD30_55 = CSV.read("./data/ga_countyOD_30_54.csv") #transport matrix for 30-55 (assume till 59)
OD55_up = CSV.read("./data/ga_countyOD_55_older.csv") #transport matrix for 55+ (assume for 60+)
GA_county_size = CSV.read("./data/GA_county_decade_age.csv") #age distributions by county

#FIP is a way of designating counties (https://en.wikipedia.org/wiki/List_of_FIPS_codes_for_Georgia_(U.S._state))
PatchFIPS = GA_county_size.FIP
PatchCounty = GA_county_size.County
#NOTE - will assign initial states by GA_county_size
#USE THE FIP indices here to keep everything ordered.

#Here we load in origin-destination data (159x159) showing trip numbers between places (including self-loops) which is sampled (not normalised)
#we need to translate into a form useful to the model
#to do this we want to:
#a) normalise such that we know the proportion of trips from an origin to a destination
#b) replace the diagonal (self trips) with negative elements that sum to the proportion of those leaving
#c) transpose the data such that we can use it in a matrix multiplication to calculate net pop. change (- leavers + incomers) for each type of state e.g. multiply transport matrix by all S individuals in age class 20-29 (159x1) [ then repeat for all epi states and age classes]
#we are going to get transport matrices w for each of the age structured commuting data

#load and create commuter matrix
## Commuter data for those aged 30-55
COMMRULES = OD30_55[:,2:end] #matrix of data showing origin (row) and destination (col) patterns by county.
#data is total number of trips from using a field sampling approach. Want to turn into a proportion.
#Note 1: this is a sample. Needs normalising. Note 2: first column was FIP
JJ = 1.0*convert(Array,COMMRULES)#convert to matrix of Floats
sJJ = sum(JJ,dims=1) #sum over columns - this is the total arrivals (including self)
for aa =1:159
    JJ[:,aa] = JJ[:,aa] ./ sJJ[aa]
end
#find proportion of those staying in county
PROPSTAY = JJ[1:size(JJ, 1)+1:end]
#reassign diagonal elements as those who will be leaving county
JJ[1:size(JJ, 1)+1:end] = -(1.0 .- PROPSTAY)
#transpose matrix as we require destination to be the rows and origin to be the columns
w = JJ';

## Commuter data for those aged less than 30
COMMRULES = ODL29[:,2:end] #matrix of data showing origin (row) and destination (col) patterns by county.
#Note 1: this is a sample. Needs normalising. Note 2: first column was FIP
JJ = 1.0*convert(Array,COMMRULES)#convert to matrix of Floats
sJJ = sum(JJ,dims=1) #sum over columns - this is the total arrivals (including self)
for aa =1:159
    JJ[:,aa] = JJ[:,aa] ./ sJJ[aa]
end
#find proportion of those staying in county
PROPSTAY = JJ[1:size(JJ, 1)+1:end]
#reassign diagonal elements as those who will be leaving county
JJ[1:size(JJ, 1)+1:end] = -(1.0 .- PROPSTAY)
#transpose matrix as we require destination to be the rows and origin to be the columns
wy = JJ';

## Commuter data for those 55 and older
COMMRULES = OD55_up[:,2:end] #matrix of data showing origin (row) and destination (col) patterns by county.
#Note 1: this is a sample. Needs normalising. Note 2: first column was FIP
JJ = 1.0*convert(Array,COMMRULES)#convert to matrix of Floats
sJJ = sum(JJ,dims=1) #sum over columns - this is the total arrivals (including self)
for aa =1:159
    JJ[:,aa] = JJ[:,aa] ./ sJJ[aa]
end
#find proportion of those staying in county
PROPSTAY = JJ[1:size(JJ, 1)+1:end]
#reassign diagonal elements as those who will be leaving county
JJ[1:size(JJ, 1)+1:end] = -(1.0 .- PROPSTAY)
#transpose matrix as we require destination to be the rows and origin to be the columns
wo = JJ';

```

```julia
#Make plot of the age-distribution across counties
makeplot=1
if makeplot==1
gr()
plot(MiddleAge,100 .* convert(Array,(GA_county_size[1,3:11])),xlim=(0,100),title="Age distribution by county",
    ylabel="Percent of county population (%)",xlabel="Age",legend=false,yguidefontsize=8,markershape = :auto,markersize=2)
for aa=2:159
    plot!(MiddleAge,100 .* convert(Array,(GA_county_size[aa,3:11])),markershape = :auto,markersize=2)
end
savefig("./figures/GAagestructure.pdf")
current()
end
```

```julia
#Basic epidemiology to apply across all patches
SocialDistancingInterventions=[1.0,0.75,0.5,0.25] #proportionalities to apply to both β_A and β_S (1.0 is no distancing)
β_A = 0.3 #asymptomatic transmission rate (per day)
β_S = 0.6 #symptomatic transmission rate (per day)
γ_e = (1.0/4.0) #mean exposed period (days)
γ_a = (1.0/6.0) #mean asymptomatic period (days)
γ_s = (1.0/6.0) #mean symptomatic period (days)
γ_h = (1.0/10.0) # mean hospitalised period (days)

p = [0.95, 0.95,0.9,0.8,0.7,0.6,0.4,0.2,0.2]  #fraction of cases that are asymptomatic
h = [0.1, 0.3, 1.2, 3.2, 4.9, 10.2, 16.6, 24.3, 27.3]./100; #fraction of cases that require a hospital visit (by age class)
ξ = [5, 5, 5, 5, 6.3, 12.2, 27.4, 43.2, 70.9]./100; #fraction of hospitalised cases that become critical and require critical care (by age class)
μ = 0.5.*ones(NumAgeClasses) #mortality fraction

ISMOVEFACTOR = 1.0 # how much to let Is people commute (must be 0-1)
w1 = 16/24 ; #fraction of time mixing with local pop.
w2 = 1.0 - w1 ;#fraction of time mixing with commuter pop.

#set parameter sets to use to initialise/drive the model
param = [1.0,β_A,β_S,p,γ_e,γ_a,γ_s,γ_h,h,ξ,μ,S_ind,E_ind,
    Ia_ind,Is_ind,Ihsub_ind,Ihcrit_ind,R_ind,D_ind,
    NumPatches,NumAgeClasses,patchstates,patchstatesALIVE,
    patchstatesMOVEABLE,w,wy,wo,transport,ISMOVEFACTOR,w1,w2]

singlenodepar =[1.0,β_A,β_S,p,γ_e,γ_a,γ_s,γ_h,h,ξ,μ,S_ind,E_ind,Ia_ind,Is_ind,R_ind,Ihsub_ind,Ihcrit_ind,D_ind]

```

 ### Set the initial conditions

We now have the ingredients. Now we need to mix in the ingredients.
Want to set initial conditions, being cognisant of the GA data.
Two main ways right now are: a simulation approach (CHOICE=5); and an "eigenvector" approach (CHOICE=7)

Simulation starts an epidemic in Fulton & Dougherty, then runs the model forward until deaths match up those recorded in GA data for each county one-by-one. The states in each county at these times are then patched together into an initial condition.

Eigenvector approach runs the model at the Georgia level till total deaths are matched. Then assigns the final vector of values proportionally across counties as an intial condition.

```julia
### Setting initial conditions
#Assign county age structured data to initial state
GAPOP = sum(GA_county_size.POPSIZE) #total population of GA

MAT = convert(Array, (GA_county_size[:,3:(end-1)] .* GA_county_size.POPSIZE))
MAT2 = MAT ./ GAPOP
#sum(MAT2)#should = 1. Yes!
#MAT2 is the fraction of GA in eaah age class in each county.

#want to now assign MAT2 to initial vector u00
u00 = zeros(NumPatches*NumAgeClasses*NumEpiStates)

for aa = 1:NumPatches
        u00[(aa-1)*patchstates .+ S_ind] = MAT2[aa,:]
end

# load case data and figure out initial conditions
tspan = (0.0,300.0)

#start epidemic with 1 exposed individual ages 20-29 in Fulton county ...
INDFult = findall(GA_county_size.FIP .== 13121)
INDFult=INDFult[1]#patch index
u00[S_ind[3] + patchstates*(INDFult-1)] = u00[S_ind[3] + patchstates*(INDFult-1)] - 1.0/GAPOP #minus from S
u00[E_ind[3] + patchstates*(INDFult-1)] = u00[E_ind[3] + patchstates*(INDFult-1)] + 1.0/GAPOP #add to E
#...and with 1 exposed individual ages 20-29 in Dougherty county
INDDougherty = findall(GA_county_size.FIP .== 13095) #dougherty county (Albany)
INDDougherty=INDDougherty[1]#patch index
u00[S_ind[3] + patchstates*(INDDougherty-1)] = u00[S_ind[3] + patchstates*(INDDougherty-1)] - 1.0/GAPOP #minus from S
u00[E_ind[3] + patchstates*(INDDougherty-1)] = u00[E_ind[3] + patchstates*(INDDougherty-1)] + 1.0/GAPOP #add to E
initu = u00

#set the ODE problem using this initial condition (1 infection in Dougherty and 1 infection in Fulton from people aged 20-29)
prob_init = ODEProblem(SpatialAgeStructuredEpidemiologicalModelMixAge!,initu,tspan,param)

#set an terminate integration affect.
affect!(integrator) = terminate!(integrator)

#state wide deaths and cases
TOT_deaths = sum(CasesbyCounty.deaths)/GAPOP
TOT_cases = sum(CasesbyCounty.cases)/GAPOP

#now use this information to initialise the model.
#Currently done with two options:
###CHOICES = 1 :  County simulation method (CSM)
#uses the initial outbreak defined above to find epi. distributions
#in each county that hit death data in the GA data input county-by-county.
#These conditions are then merged together to make the final initial condition.
###CHOICES = 2 :  State level model (SLM)
#uses a non-spatial model for the whole of GA to find epi. distributions
# that hit deaths for the whole of GA. Then apportions these out to counties
# given the proportions of deaths recorded in each county.

if  CHOICES == 1
    ## County Simulation Method (CSM) ##

    #in general should use hospitalisations rather than cases, unless death available
    #case numbers highly likely to be underestimates
    Hprop = 0.2 #20.48% proportion of cases that are hospitalised - appears consistently around 20% (April 15)
    #- this is taken from Georgia Department of Public Health COVID-19 Daily Status Report
    #https://dph.georgia.gov/covid-19-daily-status-report

    #cases county by county
    for aa = 1:length(GA_county_size.FIP) #for each county in GA
    print(aa) #print index to help show progress

        IND = findall(CasesbyCounty.FIP .== GA_county_size.FIP[aa]) #find patch index
        DEATH = 0.0
        if length(IND) == 1 #ignore unknown category in case data
            DEATH = CasesbyCounty.deaths[IND[1]] #deaths recorded in this patch
            HOSP = Hprop * CasesbyCounty.cases[IND[1]] #hsopitalised assumed in this patch
        else
            HOSP = 0.5 # if no cases recorded. Assign some proportion to be hospitalised. Most likely non-zero.
        end

        #use this info. to simulate to epi. distributions in this patch

        if DEATH >0.0
            #If there are deaths simulate until the number of deaths is reached
             #set up callback to stop integration when number of cases reaches a threshold
            CaseThreshold = DEATH/GAPOP
            #sum of S in patch - (all in patch - thresh)
            condition(u,t,integrator)  = sum(u[D_ind .+ patchstates*(aa-1)]) - CaseThreshold  #this must be equal to zero at stopping condition
            affect!(integrator) = terminate!(integrator)
            cb = ContinuousCallback(condition,affect!)
            #solve the initial spin up, checking for positivity and using callback definition
            sol_init = solve(prob_init,Tsit5(), abstol=1e-8,reltol=1e-8,saveat=1,isoutofdomain=(y,p,t)->any(x->x<0,y), callback = cb)
            #assign end spinup states per county into new initial condition.
            END = sol_init.u[end]
            u00[(1+patchstates*(aa-1)):(patchstates*aa)] = END[(1+patchstates*(aa-1)):(patchstates*aa)]
        else
            #If no deaths simulate until the number hospitalised (both Ihsub and Ihcrit) is reached
            #set up callback to stop integration when number of cases reaches a threshold
            CaseThreshold = HOSP/GAPOP
            #sum of S in patch - (all in patch - thresh)
            condition2(u,t,integrator)  = sum(u[[Ihsub_ind;Ihcrit_ind] .+ patchstates*(aa-1)]) - CaseThreshold  #this must be equal to zero at stopping condition
            affect2!(integrator) = terminate!(integrator)
            cb2 = ContinuousCallback(condition2,affect2!)
            #solve the initial spin up, checking for positivity and using callback definition
            sol_init = solve(prob_init,Tsit5(), abstol=1e-8,reltol=1e-8,saveat=1,isoutofdomain=(y,p,t)->any(x->x<0,y), callback = cb2)
            #assign end spinup states per county into new initial condition.
            END = sol_init.u[end]
            u00[(1+patchstates*(aa-1)):(patchstates*aa)] = END[(1+patchstates*(aa-1)):(patchstates*aa)]
        end
    end

elseif CHOICES == 2
    ## State Level Model (SLM) ##

    #1.set up initial conditions for a GA level model
    #Simple model to total deaths at state level. Then reapportion in the county model.
    Singleu0 = zeros(NumAgeClasses*NumEpiStates)
    Singleu0[S_ind] = sum(MAT2,dims=1)
    Singleu0[S_ind[3]] = Singleu0[S_ind[3]] - 1.0/GAPOP
    Singleu0[E_ind[3]] = 1.0/GAPOP  #seed the epidemic (1 person 20-29).

    prob_s_init = ODEProblem(SingleNode!,Singleu0,tspan,singlenodepar)

    CaseThreshold = TOT_deaths #as a fraction of GA pop.

    condition3(u,t,integrator)  = CaseThreshold - sum(u[D_ind])   #this must be equal to zero at stopping condition
    affect3!(integrator) = terminate!(integrator)
    cb3 = ContinuousCallback(condition3,affect3!)

    #solve the initial spin up, checking for positivity and using callback definition
    sol_s_init = solve(prob_s_init,Tsit5(), abstol=1e-8,reltol=1e-8,saveat=0.1,isoutofdomain=(y,p,t)->any(x->x<0,y), callback = cb3)

    #2. Now use this epi. distribution and assign it to each county
    START = sol_s_init.u[end] #initial distributions at state scale (age*epi classes)
    SumD = sum(START[D_ind])*GAPOP #sum of all deaths in model
    TOTD =  sum(CasesbyCounty.deaths) #total deaths in data
    TOTU =  sum(CasesbyCounty.deaths[CasesbyCounty.FIP.==-999]) #total deaths with unknown assignment
    L1 = length(GA_county_size.FIP) #all counties
    NUMUNKNOWN = sum(CasesbyCounty.FIP.==-999)
    L2 = length(CasesbyCounty.FIP)- NUMUNKNOWN - sum(CasesbyCounty.deaths .== 0) #num counties with cases

    for aa = 1:159
        countypop = GA_county_size.POPSIZE[aa]

        IND = findall(CasesbyCounty.FIP .== GA_county_size.FIP[aa]) #find patch index
        if length(IND) == 1 #ignore unknown category in case data
            D_county = 1.0 * CasesbyCounty.deaths[IND[1]]
            if D_county == 0.0
                D_county = TOTU/(L1-L2) #if this county has no recorded cases assign from the unknown pool.
            end
        else
            D_county = TOTU/(L1-L2) #if this county has no recorded cases assign from the unknown pool.
        end

        RATIO = 1.0*(D_county/TOTD) #county ratio - ratio of deaths in county to total

        NEW = START .* RATIO  .* GAPOP
        JAM = zeros(NumAgeClasses)
        Ages = MAT[aa,:] # age distribution in this county

        for bb=1:NumAgeClasses
           #calculate sum of all but S this age class
            JAM[bb] = sum(NEW[[E_ind[bb];Ia_ind[bb];Is_ind[bb];R_ind[bb];Ihsub_ind[bb];Ihcrit_ind[bb];D_ind[bb]]])
           #Now find what S for this age class should be
            NEW[S_ind[bb]] = Ages[bb] - JAM[bb]
        end

        NEW = NEW ./ GAPOP

        #check if any element in NEW is less than 0 - would indicate problems in preserving county demographics.
        if sum(isless.(NEW,0)) < 0
            print("WARNING. u LESS THAN 0")
            NEW[isless.(NEW,0)] = 0.0
        end

        #assign to intiial condition
        u00[(1+patchstates*(aa-1)):(patchstates*aa)] = NEW

    end

end

#assign end spinup states as new initial condition.

```

```julia
#clean up memory by removing tables and objects no longer of interest. Julia has no explicit removal of variables (that I know of).
#assigning large matrices to integers should save a fair amount of memory.
#varinfo()
COMMRULES = 0
ODL29 = 0
OD30_55 = 0
OD55_up = 0
#GA_county_size = 0
#CasesbyCounty = 0
MAT = 0
MAT2 = 0
JJ =0
```

### Run the simulations

Now that the initial conditions are set, we are ready to bake the model.
Note that in the above conditions are set without any social distancing interventions. Currently, these are applied at time =0. That is here.
Here, we have a loop that goes through different social distancing levels (that reduce βS and βA); runs the model and saves the output to file.

```julia
#NOW DO INTERVENTIONS IN A LOOP

mkdir(SAVEFOLD)
for bb = 1:length(SocialDistancingInterventions)

    SAVEFOLD2 = string(SAVEFOLD,"/Social_",bb)
    SAVEFOLD3 = string(SAVEFOLD,"/Social_",bb,"relaxed")

    #set parameters for this intervention
    pars = [SocialDistancingInterventions[bb],β_A,β_S,p,γ_e,γ_a,γ_s,γ_h,h,ξ,μ,S_ind,E_ind,
    Ia_ind,Is_ind,Ihsub_ind,Ihcrit_ind,R_ind,D_ind,
    NumPatches,NumAgeClasses,patchstates,patchstatesALIVE,
    patchstatesMOVEABLE,w,wy,wo,transport,ISMOVEFACTOR,w1,w2]

    ## NO RELAXATION
    #define ODE problems to solve
    prob = ODEProblem(SpatialAgeStructuredEpidemiologicalModelMixAge!,u00,tspanrun,pars)
    #set New Problem
    #solve the ODEs
    SOL = solve(prob,Tsit5(),abstol=1e-8,reltol=1e-8,saveat=TIMERESOLUTION,isoutofdomain=(y,p,t)->any(x->x<0,y))

    ##RELAXATION ON MAY 15th
    prob_a = ODEProblem(SpatialAgeStructuredEpidemiologicalModelMixAge!,u00,tspanrun,pars)
    condition(u,t,integrator) = t== 48  #48 days to May 15th
    affect!(integrator) = integrator.p[1] = 1.0
    cb = DiscreteCallback(condition,affect!)
    SOL_a = solve(prob_a,Tsit5(),abstol=1e-8,reltol=1e-8,saveat=TIMERESOLUTION,isoutofdomain=(y,p,t)->any(x->x<0,y),callback = cb, tstops=[48.0],save_positions=(false,false))

    #save output

    WriteOutput!(PatchFIPS,PatchCounty,HospRegions,GAPOP,TODAY,TIMERESOLUTION,T_END,SOL,pars,SAVEFOLD2)
    WriteOutput!(PatchFIPS,PatchCounty,HospRegions,GAPOP,TODAY,TIMERESOLUTION,T_END,SOL_a,pars,SAVEFOLD3)

    JAM = GAPOP * gatherStates!(SOL,param)[1]
    Gath = gatherStates!(SOL_a,param)
    JAM2 = GAPOP  * Gath[1]
    time2 = SOL_a.t[Gath[2]]

    #county-level projected deaths
    gr()
    plot(SOL.t,dropdims(sum(JAM[1,8,1:9,:],dims=1),dims=1),legend=false,ylabel="D per county",xlabel="Time (days)")

    for aa = 2:159
        plot!(SOL.t,dropdims(sum(JAM[aa,8,1:9,:],dims=1),dims=1))
    end
    savefig(string(SAVEFOLD2,"/deaths.png"))
    #current()

    #county-level projected ICU
    plot(SOL.t,dropdims(sum(JAM[1,7,1:9,:],dims=1),dims=1),legend=false,ylabel="ICU per county",xlabel="Time (days)")

    for aa = 2:159
        plot!(SOL.t,dropdims(sum(JAM[aa,7,1:9,:],dims=1),dims=1))
    end
    savefig(string(SAVEFOLD2,"/ICU.png"))
    #current()

    #county-level projected hosp.s
    plot(SOL.t,dropdims(sum(JAM[1,6,1:9,:],dims=1),dims=1).+dropdims(sum(JAM[1,7,1:9,:],dims=1),dims=1),legend=false,ylabel="Hosp. per county",xlabel="Time (days)")

    for aa = 2:159
        plot!(SOL.t,dropdims(sum(JAM[aa,6,1:9,:],dims=1),dims=1).+dropdims(sum(JAM[1,7,1:9,:],dims=1),dims=1))
    end
    savefig(string(SAVEFOLD2,"/totalHosp.png"))
    #current()

    p1 = SummaryPlot!(JAM,SOL.t)
    savefig(string(SAVEFOLD2,"/GAscenarios.pdf"))
    #current()
    p2 = SummaryPlot!(JAM2,time2)
    savefig(string(SAVEFOLD3,"/GAscenarios.pdf"))
    #current()

end
```


# Estrategias Evolutivas

La librería **Pyristic** incluye una clase llamada `EvolutionStrategy`,  inspirada en la metaheurística de *Estrategias evolutivas* (EE), para resolver problemas de minimización. Para trabajar con esta clase se requiere hacer lo siguiente:

1. Definir:
    * La función objetivo $f$.
    * La lista de restricciones.
    * Lista de límites inferiores y superiores.
    * Configuración de operadores de la metaheurística (opcional).

2. Crear una clase que herede de `EvolutionStrategy`.

3. Sobreescribir las siguientes funciones de la clase `EvolutionStrategy`:

    * initialize_step_weights (opcional)
    * initialize_population (opcional)
    * fixer (opcional)
    * mutation_operator (opcional)
    * crossover_operator (opcional)
    * adaptive_crossover (opcional)
    * adaptive_mutation (opcional)
    * survivor_selection (opcional)

A continuación se mostrará cómo resolver dos problemas de optimización continua usando  la clase `EvolutionStrategy`. El primer paso es importar la librería y los módulos que se van a utilizar.

### Librerías externas

```python
from pprint import pprint
import math
import numpy as np
import copy
from IPython.display import Image
from IPython.core.display import HTML
```

### Componentes de `pyristic`
La estructura que está organizada la librería es:
 * Las metaheurísticas están ubicadas en `heuristic`.
 * Las funciones de prueba están ubicadas en `utils.test_function`.
 * Las clases auxiliares para mantener la información de los operadores que serán empleados para alguna de las metaheurísticas basadas en los paradigmas del cómputo evolutivo están ubicadas en `utils.helpers`.
 * Las metaheurísticas basadas en los paradigmas del cómputo evolutivo dependen de un conjunto de operadores (selección, mutación y cruza). Estos operadores están ubicados en `utils.operators`.

Para demostrar el uso de nuestra metaheurística basada en *algoritmos geneticos* tenemos que importar la clase llamada `Genetic` que se encuentra en `heuristic.GeneticAlgorithm_search`.

```python
from pyristic.heuristic.EvolutionStrategy_search import EvolutionStrategy
from pyristic.utils.operators import selection, mutation, crossover
from pyristic.utils.test_function import beale_, ackley_
from pyristic.utils.helpers import EvolutionStrategyConfig,get_stats
```

## Función de Beale
\begin{equation}
  \label{eq:BF}
  \begin{array}{rll}
  \text{minimizar:} & f(x_1, x_2) = (1.5 - x_1 + x_1x_2)^2 + (2.25 - x_1 + x_1x_2^2)^2 + (2.625 - x_1 + x_1x_2^3)^2
  &  \\
  \text{Tal que: } & -4.5 \leq x_1,x_2 \leq 4.5 &
  \end{array}
\end{equation}

El mínimo global se encuentra en $x^* = (3, 0.5)$ y $f(x^*) = 0$.

```python
Image(filename="include/beale.png", width=500, height=300)
```

La librería **Pyristic** tiene implementados algunos problemas de prueba en **utils.helpers.test_function**, entre ellos la función de Beale. Los problemas de prueba están definidos como diccionarios con las siguientes llaves:

* `function.` Función objetivo.
* `constraints.` Lista con las funciones que validan las diferentes restricciones del problema. Todas las funciones deben regresar un valor booleano.
* `bounds.` Límites para cada una de las variables del problema. En el caso de que todas las variables del problema compartan el mismo intervalo de búsqueda, se utiliza una lista con dos valores numéricos. El primero indica el límite inferior y el segundo el límite superior. En caso contrario, se tendrá una lista con dos listas internas. La primera lista  almacena los límites inferiores y la segunda los límites superiores.
* `decision_variables.` Número de variables de decisión.

```python
beale_
```

    {'function': CPUDispatcher(<function beale_function at 0x7f018684ac20>),
     'constraints': [<function pyristic.utils.test_function.constraint1_beale(X: numpy.ndarray) -> bool>],
     'bounds': [-4.5, 4.5],
     'decision_variables': 2}

### Función de aptitud
Los algoritmos evolutivos, se define una función llamada **aptitud** que describe a cada individuo que tan bueno es en el problema a resolver. Los valores más grandes de aptitud corresponden a las mejores soluciones. Para el problema de Beale, la función de aptitud es:
$F_a(i) = \frac{1}{f(x^{(i)}_1, x^{(i)}_2) + 1}$

```python
def aptitudeBeale(X: np.array) -> np.array:
    return 1/ ( beale_['function'](X)+1)
```

### Declaración de `EvolutionStrategy`
La metaheurística de EE implementada en la librería de **Pyristic** se puede utilizar de las siguientes maneras:

* Crear una clase que herede de la clase `EvolutionStrategy` y sobreescribir alguno de los métodos.
* Declarar un objeto de tipo *EvolutionStrategyConfig*, ubicado en *utils.helpers*, y utilizarlo para inicializar un objeto de tipo `EvolutionStrategy`.
* Realizar una combinación de las dos anteriores.

Es importante resaltar que se puede hacer uso de la metaheurística sin modificar los operadores que tiene por defecto.

## Ejecución de la metaheurística
Como mencionamos antes, una forma de utilizar la metaheurística es hacer una instancia de la clase `EvolutionStrategy` dejando su configuración por defecto.

Los argumentos que se deben indicar al momento de inicializar son:
* función objetivo
* restricciones del problema
* límites inferior y superior (por cada variable de decisión)
* número de variables que tiene el problema.

```python
Beale = EvolutionStrategy(
                    function= aptitudeBeale,\
                    decision_variables=beale_['decision_variables'],\
                    constraints= beale_['constraints'],\
                    bounds= beale_['bounds'])
```

Recordemos que `beale_` es un diccionario con la información requerida por el constructor de la clase `EvolutionStrategy`.

### ($\mu + \lambda$) - EE
Por defecto, la metaheurística trabaja de la siguiente forma:
* Para la selección de sobrevivientes utiliza un esquema $(\mu + \lambda)$.
* El operador de cruza  para las variables de decisión es *recombinación discreta* (operador llamado `discrete`).
* El operador de cruza para los tamaños de paso es *recombinación intermedia* (operador llamado `intermediate`).
* Cada individuo tiene un tamaño de paso por variable. (operador llamado `mult_sigma_adaptive_mutator`).

Una vez creado el objeto, se manda a llamar a su método `optimize` el cual recibe los  siguientes parámetros:
   * **generations** = $300$
   * **population_size** = $80$
   * **offspring_size** = $160$

```python
Beale.optimize(300,80,160,verbose=True)
```

    100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 300/300 [00:02<00:00, 121.70it/s]

```python
print(Beale)
```

    Evolution Strategy search:
     F_a(X) = 1.0
     X = [2.99999999 0.5       ]
     Constraints:
     x1: -4.5 <= 3.00 <= 4.5
     x2: -4.5 <= 0.50 <= 4.5

Para realizar un estudio estadístico del comportamiento de la metaheurística, la librería **Pyristic** cuenta con una función llamada `get_stats`. Esta función se encuentra en `utils.helpers` y recibe como parámetros:

   * Objeto que realiza la ejecución de la metaheurística.
   * Número de veces que se quiere ejecutar la metaheurística.
   * Tupla con los argumentos que recibe la función `optimize`.
   * Argumentos adicionales a la búsqueda (opcional).

La función **get_stats** considera la solución devuelta por la metaheurística en cada ejecución y retorna un diccionario con la mejor y peor solución encontrada y la media y desviación estándar del valor de la función objetivo.

```python
args = (200, 80, 160, 0.0001, False)
statistics = get_stats(Beale, 21, args, transformer=beale_['function'])
```

```python
pprint(statistics)
```

    {'Best solution': {'f': 5.232655460094757e-18, 'x': array([3. , 0.5])},
     'Mean': 7.500236923765416e-13,
     'Median': 6.295559358095275e-17,
     'Standard deviation': 3.1450540830911966e-12,
     'Worst solution': {'f': 1.47876718588774e-11,
                        'x': array([3.00000693, 0.50000227])},
     'averageTime': 0.19353040059407553,
     'objectiveFunction': [6.974537963159639e-17,
                           1.5829469345976208e-17,
                           5.232655460094757e-18,
                           3.55142232876361e-17,
                           3.659867353206695e-14,
                           6.295559358095275e-17,
                           9.368025066127844e-17,
                           2.3280831331374633e-17,
                           9.248440100693195e-13,
                           5.228335842212396e-17,
                           9.484958935479025e-18,
                           4.58002399942093e-17,
                           7.223764853904115e-17,
                           2.4479100052993973e-17,
                           1.411336805961557e-17,
                           9.124874382780466e-17,
                           1.47876718588774e-11,
                           4.83541251795498e-16,
                           3.808890680308518e-17,
                           1.58427128576792e-16,
                           8.705432027964274e-17]}

### (1+1) - EE
A continuación vamos a implementar la EE más simple conocida como $(1+1)-$EE para resolver la función de Beale. $(1+1)-$EE trabaja de la siguiente forma:

 1. Utiliza un único valor de $\sigma$ para mutar todas las variables de decisión.
 2. El valor de $\sigma$ se autoadapta utilizando la regla del éxito del 1/5.
 \begin{equation}
  \sigma =
  \begin{cases}
   \sigma / c & \text{Si } p_s > 1/5, \\
   \sigma \cdot c & \text{Si } p_s < 1/5, \\
   \sigma  & \text{Si } p_s = 1/5 \\
  \end{cases}
\end{equation}
 3. Se utiliza un solo individuo, éste es mutado para generar un hijo.

\begin{equation}
  \label{eq:mutarVariables}
   x'_i = x_i + \sigma'_i \cdot N_i(0, 1)
\end{equation}

 4. El individuo más apto es el que pasará a la siguiente generación.

Esta versión no considera un operador de cruza. Por este motivo, vamos a sobreescribir `crossover_operator` y `adaptive_crossover` para que retornen el individuo original sin cambio alguno. Las funciones que debemos sobreescribir son:
* crossover_operator
* adaptive_crossover
* adaptive_mutation
* mutation_operator
* initializing_step_weights

```python
class ESBasic(EvolutionStrategy):
    def __init__(self, function  ,\
                 decision_variables:int,\
                 constraints: list,\
                 bounds: np.ndarray):
        super().__init__(function, decision_variables, constraints, bounds)

        self.successful = 0

    def crossover_operator(self, parent_ind1,\
                                 parent_ind2,\
                                 **kargs):
        return self.logger['parent_population_x']

    def adaptive_crossover(self, parent_ind1,\
                                 parent_ind2,\
                                 **kargs):
        return self.logger['parent_population_sigma']

    def adaptive_mutation(self, **kargs):

            if self.logger['current_iter'] % kargs['k'] != 0:
                return self.logger['offspring_population_sigma']

            ps = self.successful / kargs['k']
            self.successful = 0
            if( ps > 1/5):
                return self.logger['offspring_population_sigma']/kargs['c']
            elif( ps < 1/5):
                return self.logger['offspring_population_sigma']*kargs['c']
            else:
                return self.logger['offspring_population_sigma']

    def mutation_operator(self, **kargs):
        X_mutated = copy.deepcopy(self.logger['offspring_population_x'])
        X_mutated += self.logger['offspring_population_sigma'] * np.random.normal(0,1, size=X_mutated.shape)

        if(self.f(self.logger['offspring_population_x'][0]) > self.f(X_mutated[0])):
            self.successful+=1

        return X_mutated

    def initializing_step_weights(self, eps_sigma:int, **kwargs) -> np.ndarray:
        return np.random.uniform(0,1, size=(self.logger['parent_population_size'], 1))
```

```python
Beale_basic = ESBasic(
                    function= aptitudeBeale,\
                    decision_variables=beale_['decision_variables'],\
                    constraints= beale_['constraints'],\
                    bounds= beale_['bounds'])
```

Como se puede observar, estamos haciendo uso del argumento llamado `kargs`, el cual es un diccionario que contiene información adicional. En este caso `kargs` contiene dos constantes: la primera es el parámetro $k$ que indica cada cuantas generaciones se va a actualizar el valor de $\sigma$ y la segunda es el valor del parámetro $c$ que se utiliza  para actualizar el valor de $\sigma$. El diccionario tiene que ser incluido siempre con `**` antes del nombre de la variable que lo almacena.

```python
additional_arguments = {'k':20,'c':0.83}
```

```python
Beale_basic.optimize(generations=200,\
                     population_size=1,\
                     offspring_size=1,\
                     **additional_arguments)
```

    100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 200/200 [00:00<00:00, 1739.90it/s]

```python
print(Beale_basic)
```

    Evolution Strategy search:
     F_a(X) = 0.9969506868038376
     X = [2.98335859 0.50729749]
     Constraints:
     x1: -4.5 <= 2.98 <= 4.5
     x2: -4.5 <= 0.51 <= 4.5

```python
args = (500, 1, 1, 0.001,False)
statistics = get_stats(Beale_basic, 30, args, additional_arguments, aptitudeBeale)
```

```python
pprint(statistics)
```

    {'Best solution': {'f': 0.09378523089864498,
                       'x': array([ 0.03287226, -3.80633412])},
     'Mean': 0.7862535058145801,
     'Median': 0.9967553646899534,
     'Standard deviation': 0.3029875418531697,
     'Worst solution': {'f': 0.9999381625668178,
                        'x': array([2.9834752 , 0.49499717])},
     'averageTime': 0.043139831225077314,
     'objectiveFunction': [0.9990169200052803,
                           0.09378523089864498,
                           0.5672019868347583,
                           0.9981812538791579,
                           0.9979996162555546,
                           0.9977846479786782,
                           0.9937017740501066,
                           0.5163703703126666,
                           0.5472588628918187,
                           0.49558365225972467,
                           0.9959051556100147,
                           0.9972898297178926,
                           0.9987279969858004,
                           0.5645729629595143,
                           0.09596677560117627,
                           0.996932108411761,
                           0.9993865654887663,
                           0.9991981127606973,
                           0.9983601706985583,
                           0.9970646756951872,
                           0.5675010831608579,
                           0.9982544529717703,
                           0.9999381625668178,
                           0.9965786209681458,
                           0.5395095891789323,
                           0.9892498155559739,
                           0.9985622337077901,
                           0.09551406458177113,
                           0.5544542372737761,
                           0.9977542451758096]}

## Función de Ackley

\begin{equation}
  \min f(\vec{x}) = -20\exp \left( -0.2 \sqrt{\frac{1}{n} \sum_{i=1}^n x_i^2} \right)
  - exp \left( \frac{1}{n} \sum_{i=1}^n \cos (2\pi x_i) \right)
  + 20 + e
\end{equation}

El mínimo global está en $x^* = 0 $, $f(\vec{x}) = 0$ y su dominio es $|x_{i}| < 30$.

```python
Image(filename="include/ackley.jpg", width=500, height=300)
```

Al igual que la función de Beale, la librería **Pyristic** tiene implementada la función de Ackley en `utils.helpers.test_function`.

```python
ackley_
```

    {'function': CPUDispatcher(<function ackley_function at 0x7f018561c950>),
     'constraints': [<function pyristic.utils.test_function.constraint1_ackley(X: numpy.ndarray) -> bool>],
     'bounds': [-30.0, 30.0],
     'decision_variables': 10}

### Función de aptitud
Los algoritmos evolutivos, se define una función llamada **aptitud** que describe a cada individuo que tan bueno es en el problema a resolver. Los valores más grandes de aptitud corresponden a las mejores soluciones. Para el problema de Ackley, la función de aptitud es: $F_a(\vec{X}) = -f(\vec{X})$

```python
def aptitudeAckley(x):
    return -1* ackley_['function'](x)
```

Este problema no se encuentra restringido a un número de variables de decisión. Para  modificar el número de variables de decisión hacemos:
```python
ackley_['decision_variables'] = 5
```

### ($\mu+\lambda$)-EE

Emplearemos la metaheurística de EE, con la configuración por defecto, para resolver la función de Ackley con 10 variables de decisión.

```python
Ackley = EvolutionStrategy(
                    function= aptitudeAckley,\
                    decision_variables=ackley_['decision_variables'],\
                    constraints= ackley_['constraints'],\
                    bounds= ackley_['bounds'])
```

```python
'''
Número de generaciones: 250
Tamaño de población de padres: 100
Tamaño de población de hijos: 200
'''
Ackley.optimize(250,100,200)
```

    100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 250/250 [00:00<00:00, 289.85it/s]

```python
print(Ackley)
```

    Evolution Strategy search:
     F_a(X) = -0.28845250921960686
     X = [-0.05837    -0.10053953 -0.00708623 -0.03774568 -0.03316804 -0.03318999
      0.02397432 -0.00125536 -0.00243173 -0.0546666 ]
     Constraints:
     x1: -30 <= -0.06 <= 30
     x2: -30 <= -0.10 <= 30
     x3: -30 <= -0.01 <= 30
     x4: -30 <= -0.04 <= 30
     x5: -30 <= -0.03 <= 30
     x6: -30 <= -0.03 <= 30
     x7: -30 <= 0.02 <= 30
     x8: -30 <= -0.00 <= 30
     x9: -30 <= -0.00 <= 30
     x10: -30 <= -0.05 <= 30

```python
args = (250, 100, 200, 0.001 ,False)
statistics = get_stats(Ackley, 30, args, transformer=ackley_['function'])
```

```python
pprint(statistics)
```

    {'Best solution': {'f': 0.010249799629580725,
                       'x': array([-0.00086015, -0.00161265, -0.00215718,  0.00065416, -0.00228198,
           -0.00559011,  0.00025284, -0.00344554,  0.00059995, -0.0020872 ])},
     'Mean': 0.1644989262654607,
     'Median': 0.07446471752602557,
     'Standard deviation': 0.28754471244926094,
     'Worst solution': {'f': 1.628041385916028,
                        'x': array([-0.26780161, -0.21837777, -0.1178678 , -0.09132986, -0.26427773,
            0.10544494, -0.13091937,  0.10397274, -0.12819718,  0.03343717])},
     'averageTime': 0.8125235954920451,
     'objectiveFunction': [0.034206526420007766,
                           1.628041385916028,
                           0.12596106222010706,
                           0.22109914950204912,
                           0.046531228197921326,
                           0.04410535059719267,
                           0.09213135864640565,
                           0.2619662918114547,
                           0.015350036094688324,
                           0.0419903892174287,
                           0.031469512589055615,
                           0.11911705593879462,
                           0.27359047848434104,
                           0.2391134627754572,
                           0.06432248377842997,
                           0.21975810167185772,
                           0.07006921608206129,
                           0.0369945594033827,
                           0.031840050386872765,
                           0.2748363430488152,
                           0.010249799629580725,
                           0.03167359762541766,
                           0.09315407594888869,
                           0.0777059003499656,
                           0.07122353470208553,
                           0.33507869438961224,
                           0.020629669655939598,
                           0.036820161759220316,
                           0.1742926822490216,
                           0.21164562887173988]}

### ($\mu,\lambda$)-EE

Ahora vamos a utilizar una configuración del tipo `EvolutionStrategyConfig` para definir los métodos que serán empleados en nuestro objeto del tipo `EvolutionStrategy`.

```python
configuration_ackley = (EvolutionStrategyConfig()
                       .survivor_selection(selection.replacement_selector())
                       .adaptive_mutation(
                           mutation.single_sigma_adaptive_mutator(
                                           ackley_['decision_variables'])
                                         )
                       )
```

En este caso estamos modificando el esquema de selección de sobrevivientes y emplearemos un único tamaño de paso para cada individuo, donde, el argumento que tiene `single_sigma_adaptive_mutator` es el número de variables de decisión del problema.

```python
print(configuration_ackley)
```

    --------------------------------
    	Configuration
    --------------------------------
    Survivor selection: Replacement population
    Adaptive mutation: Single Sigma

    --------------------------------

```python
solver_ackley_custom = EvolutionStrategy(
                    function= aptitudeAckley,\
                    decision_variables=ackley_['decision_variables'],\
                    constraints= ackley_['constraints'],\
                    bounds= ackley_['bounds'],config=configuration_ackley)
```

```python
solver_ackley_custom.optimize(250,100,200)
```

    100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 250/250 [00:01<00:00, 179.18it/s]

```python
print(solver_ackley_custom)
```

    Evolution Strategy search:
     F_a(X) = -21.55196699129465
     X = [27.23771294 27.23771294 27.23771294 27.23771294 27.23771294 27.23771294
     27.23771294 27.23771294 27.23771294 27.23771294]
     Constraints:
     x1: -30 <= 27.24 <= 30
     x2: -30 <= 27.24 <= 30
     x3: -30 <= 27.24 <= 30
     x4: -30 <= 27.24 <= 30
     x5: -30 <= 27.24 <= 30
     x6: -30 <= 27.24 <= 30
     x7: -30 <= 27.24 <= 30
     x8: -30 <= 27.24 <= 30
     x9: -30 <= 27.24 <= 30
     x10: -30 <= 27.24 <= 30

```python
args = (250, 100, 200,0.001 ,False)
statistics = get_stats(solver_ackley_custom, 30, args, transformer=ackley_['function'])
```

```python
pprint(statistics)
```

    {'Best solution': {'f': 3.914908634326306,
                       'x': array([ 0.22236554, -0.78155697, -0.90025871,  0.27553164,  1.10221801,
           -0.91454935,  0.07856534,  0.78949362, -0.93589365,  0.36879387])},
     'Mean': 17.869116590215647,
     'Median': 19.950424956466673,
     'Standard deviation': 5.142625044782836,
     'Worst solution': {'f': 22.13188156750359,
                        'x': array([-29.6417392, -29.6417392, -29.6417392, -29.6417392, -29.6417392,
           -29.6417392, -29.6417392, -29.6417392, -29.6417392, -29.6417392])},
     'averageTime': 1.0821852445602418,
     'objectiveFunction': [10.214464573662518,
                           21.68444010317522,
                           3.914908634326306,
                           19.950424956466673,
                           19.950424956466673,
                           21.773356498999437,
                           22.13188156750359,
                           19.950424956466673,
                           20.745762410131494,
                           7.161152173042002,
                           19.950424956466673,
                           19.647852175141484,
                           19.950424956466673,
                           19.950424956466673,
                           20.08725543332284,
                           19.950424956466673,
                           20.22782779904124,
                           19.950424956466673,
                           19.950424956466673,
                           19.950424956466673,
                           19.950424956466673,
                           5.0998160615647485,
                           19.950424956466673,
                           19.950424956466673,
                           20.368000242171377,
                           20.506531700594646,
                           19.950424956466673,
                           8.02794632911078,
                           14.300142756300238,
                           20.876209857848135]}

## (1+1)-EE

A continación usaremos la EE más simple para intentar resolver la función de Ackley con 10 variables de decisión.

```python
Ackley_basic = ESBasic(
                    function= aptitudeAckley,\
                    decision_variables=ackley_['decision_variables'],\
                    constraints= ackley_['constraints'],\
                    bounds= ackley_['bounds'])
```

```python
Ackley_basic.optimize(10000,1,1,**additional_arguments )
```

    100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 10000/10000 [00:01<00:00, 7829.57it/s]

```python
print(Ackley_basic)
```

    Evolution Strategy search:
     F_a(X) = -19.713742965447526
     X = [-30.          -6.06618645  28.00763708  -4.01995382   9.98348065
      10.95156463 -12.98341022  30.          -0.06114509 -19.18521633]
     Constraints:
     x1: -30 <= -30.00 <= 30
     x2: -30 <= -6.07 <= 30
     x3: -30 <= 28.01 <= 30
     x4: -30 <= -4.02 <= 30
     x5: -30 <= 9.98 <= 30
     x6: -30 <= 10.95 <= 30
     x7: -30 <= -12.98 <= 30
     x8: -30 <= 30.00 <= 30
     x9: -30 <= -0.06 <= 30
     x10: -30 <= -19.19 <= 30

```python
args = (10000, 1, 1, 0.001,False)
statistics = get_stats(Ackley_basic, 30, args, additional_arguments, transformer=ackley_['function'])
```

```python
pprint(statistics)
```

    {'Best solution': {'f': 10.310536059391417,
                       'x': array([ 0.03343847, -0.78729196, -0.13713295, -0.07919603, -9.08132812,
            2.9751608 ,  1.23007554, -0.91011728,  0.84557144, -2.29842616])},
     'Mean': 19.29082310550591,
     'Median': 19.663565578248548,
     'Standard deviation': 1.7281086637591332,
     'Worst solution': {'f': 20.26451261448206,
                        'x': array([ 22.10012059, -14.99663106,  20.77574185,  22.88045243,
           -19.97996349,  11.1043873 ,  27.08426041, -27.00899638,
            16.85222655,  27.07655944])},
     'averageTime': 0.9502643664677938,
     'objectiveFunction': [19.766594434620053,
                           19.69738945159391,
                           20.087764430839382,
                           19.532432501793185,
                           19.2855996760406,
                           19.476403133473898,
                           19.81576868565518,
                           19.34851356423621,
                           19.35177435923925,
                           19.76326438683161,
                           19.64038637966428,
                           20.21808540377747,
                           20.02292698192497,
                           18.808488740334017,
                           20.05904845457353,
                           19.123067934794904,
                           19.44640999478022,
                           19.44180567590634,
                           19.93611334585588,
                           19.70559439438051,
                           19.06727228643281,
                           20.125716751767744,
                           17.976236933257045,
                           19.576789328814215,
                           19.551319019341047,
                           19.701331874439493,
                           19.936801590103148,
                           10.310536059391417,
                           19.686744776832814,
                           20.26451261448206]}


# NODDI-Watson in crossings using MIX optimization

Estimation of microstructure parameters in crossing tissue configuration is currently still a challenge in microstructure imaging. To tackle this problem, *(Farooq et al. 2016)* proposed a stochastic optimization approach called MIX to fit known microstructure models such as NODDI-Watson *(Zhang et al. 2012)* in crossings, which they refer to NODDIx. In this example, we use MIX to reproduce their example application of NODDIx, whose configuration simply an extension of regular NODDI-Watson with multiple bundles:

\begin{align}
 E^{\textrm{NODDIx
}}_{\textrm{Watson}}= \underbrace{f_{\textrm{CSF}}\overbrace{E_{\textrm{iso}}(\cdot|\lambda_{\textrm{CSF}})}^{\textrm{Ball}}}_{\textrm{CSF}}
                             +\sum_{i=1}^{2}\overbrace{W(\kappa_i,\boldsymbol{\mu}_i)}^{\textrm{Watson}}\,*_{\mathbb{S}^2}\, \left[\underbrace{f_{h,i}\overbrace{E_{\textrm{h}}(\cdot|\lambda_\perp^{\textrm{tort}},\lambda_\parallel)}^{\textrm{Zeppelin}}}_{\textrm{Hindered Extra-Axonal}}+\underbrace{f_{r,i}\overbrace{E_r(\cdot|\lambda_\parallel)}^{\textrm{Stick}}}_{\textrm{Intra-Axonal}}\right].
\end{align}

MIX addresses the issue of robustly finding a global minimum for models with many parameters. The approach involves taking multiple optimization steps, which separately optimize the linear parameters (volume fractions) and non-linear parameters (all the others).

## Generate NODDIx model

We generate the model the same way as in the previous example - only now we include two instead of one Watson-dispersed bundles. First we import the separate pieces:

```python
from dmipy.signal_models import cylinder_models, gaussian_models
ball = gaussian_models.G1Ball()
stick = cylinder_models.C1Stick()
zeppelin = gaussian_models.G2Zeppelin()
```

Combine them into one Watson dispersed bundle and set NODDI's parameter links and fixes:

```python
from dmipy.distributions.distribute_models import SD1WatsonDistributed
watson_dispersed_bundle1 = SD1WatsonDistributed(models=[stick, zeppelin])
watson_dispersed_bundle1.parameter_names
```

    ['G2Zeppelin_1_lambda_perp',
     'SD1Watson_1_odi',
     'G2Zeppelin_1_lambda_par',
     'SD1Watson_1_mu',
     'C1Stick_1_lambda_par',
     'partial_volume_0']

```python
watson_dispersed_bundle1.set_tortuous_parameter('G2Zeppelin_1_lambda_perp','C1Stick_1_lambda_par','partial_volume_0')
watson_dispersed_bundle1.set_equal_parameter('G2Zeppelin_1_lambda_par', 'C1Stick_1_lambda_par')
watson_dispersed_bundle1.set_fixed_parameter('G2Zeppelin_1_lambda_par', 1.7e-9)
```

We can create a second instance of the first bundle by using the .copy() function.

```python
watson_dispersed_bundle2 = watson_dispersed_bundle1.copy()
```

```python
from dmipy.core.modeling_framework import MultiCompartmentModel
NODDIx_mod = MultiCompartmentModel(models=[ball, watson_dispersed_bundle1, watson_dispersed_bundle2])
```

Visualize the model:

```python
from IPython.display import Image
NODDIx_mod.visualize_model_setup(view=False, cleanup=False)
Image('Model Setup.png')
```

As the last step we fix the isotropic diffusivity of the model

```python
NODDIx_mod.parameter_names
```

    ['G1Ball_1_lambda_iso',
     'SD1WatsonDistributed_1_partial_volume_0',
     'SD1WatsonDistributed_1_SD1Watson_1_mu',
     'SD1WatsonDistributed_2_SD1Watson_1_odi',
     'SD1WatsonDistributed_2_partial_volume_0',
     'SD1WatsonDistributed_2_SD1Watson_1_mu',
     'SD1WatsonDistributed_1_SD1Watson_1_odi',
     'partial_volume_0',
     'partial_volume_1',
     'partial_volume_2']

```python
NODDIx_mod.set_fixed_parameter('G1Ball_1_lambda_iso', 3e-9)
```

## Human Connectome Project Example

```python
from dmipy.data import saved_data
scheme_hcp, data_hcp = saved_data.wu_minn_hcp_coronal_slice()
sub_image = data_hcp[70:90,: , 70:90]
```

    This data slice originates from Subject 100307 of the Human Connectome Project, WU-Minn Consortium (Principal Investigators: David Van Essen and Kamil Ugurbil; 1U54MH091657) funded by the 16 NIH Institutes and Centers that support the NIH Blueprint for Neuroscience Research; and by the McDonnell Center for Systems Neuroscience at Washington University.

```python
import matplotlib.pyplot as plt
import matplotlib.patches as patches
%matplotlib inline

fig, ax = plt.subplots(1)
ax.imshow(data_hcp[:, 0, :, 0].T, origin=True)
rect = patches.Rectangle((70,70),20,20,linewidth=1,edgecolor='r',facecolor='none')
ax.add_patch(rect)
ax.set_axis_off()
ax.set_title('HCP coronal slice B0 with ROI');
```

## Fit NODDIx to the HCP subsection

To use MIX instead of the standard Brute2Fine the solver needs to set to 'mix'. The option 'maxiter' states the threshold amount of iterations the stochastic algorithms takes.

#### Note: Fitting NODDIx using MIX even on this small patch takes close to 90 minutes!

```python
NODDIx_fit = NODDIx_mod.fit(scheme_hcp, sub_image, solver='mix', maxiter=300)
```

    Using parallel processing with 8 workers.
    Setup MIX optimizer in 3.2901763916e-05 seconds
    Fitting of 400 voxels complete in 5606.92030287 seconds.
    Average of 14.0173007572 seconds per voxel.

## Visualizing NODDIx FODs

We can now visualize the FODs of the fitted NODDIx model near the corpus callosum and centrum semiovale. We expect to find a single-bundle moving in from the left, leading to a crossing area on the right.

```python
from dipy.data import get_sphere
from dipy.viz.actor import slicer
sphere = get_sphere(name='symmetric724').subdivide()
fods = NODDIx_fit.fod(sphere.vertices, visual_odi_lower_bound=0.05)
```

```python
import numpy as np
affine = np.eye(4)
affine[0,3] = -10
affine[1,3] = -10

volume_res = fitted_parameters['partial_volume_0']
volume_im = slicer(volume_res[:, 0, :, None], interpolation='nearest', affine=affine, opacity=0.7)
```

```python
from dipy.viz import fvtk
ren = fvtk.ren()
fod_spheres = fvtk.sphere_funcs(fods, sphere, scale=1., norm=False)
fod_spheres.RotateX(90)
fod_spheres.RotateZ(180)
fod_spheres.RotateY(180)
fvtk.add(ren, fod_spheres)
fvtk.add(ren, volume_im)
fvtk.record(ren=ren, size=[700, 700])
```

```python
import matplotlib.image as mpimg
img = mpimg.imread('dipy.png')

plt.figure(figsize=[10, 10])
plt.imshow(img[100:-97, 100:-85])
plt.title('NODDIx FODs with isotropic volume fraction background', fontsize=20)
plt.axis('off');
```

Indeed, MIX managed to estimate a smooth FOD map, finding consistent crossing areas on the right and a smooth bundle continuation out of the corpus callosum - which is impressive. However, from the background we can see that the isotropic compartment is underestimated above the corpus callosum.

## Visualizing parameters maps

Visualizing the estimated parameter maps as before we can see they are much less interpretable than those of simpler single bundle models. Only partial_volume_0 (the Ball volume fraction) is smooth, showing the CSF on the bottom, but the ordering of the other parameter values is randomly distribution among the two Watson-dispersed bundles. Furtermore, it doesn't look like only a single Watson bundle was active at any point in the corpus callosum - the model found a global minimum where both bundles contributed to the single bundle with some optimal model parameters.

```python
fitted_parameters = NODDIx_fit.fitted_parameters

fig, axs = plt.subplots(3, 3, figsize=[15, 10])
axs = axs.ravel()

counter = 0
for name, values in fitted_parameters.items():
    if values.squeeze().ndim != 2:
        continue
    cf = axs[counter].imshow(values.squeeze().T, origin=True, interpolation='nearest')
    axs[counter].set_title(name)
    fig.colorbar(cf, ax=axs[counter], shrink=0.5)
    counter += 1
```

While MIX still produced in good FOD fields, this means that in its current form MIX lacks a way to automatically select the best (actual) number of bundles in a voxel. This means model parameters do not have a microstructural interpretation when the number of defined bundles does not match the actual number of bundles. A sparsity regularization on the volume fractions such as used by *(Zhu et al. 2013)* could be solution.

## References
- Farooq, Hamza, et al. "Microstructure imaging of crossing (MIX) white matter fibers from diffusion MRI." Scientific reports 6 (2016): 38927.
- Zhang, Hui, et al. "NODDI: practical in vivo neurite orientation dispersion and density imaging of the human brain." Neuroimage 61.4 (2012): 1000-1016.
- Zhu, Xinghua, et al. "Model selection and estimation of multi-compartment models in diffusion mri with a rician noise model." International Conference on Information Processing in Medical Imaging. Springer, Berlin, Heidelberg, 2013.


# LTI

Linear Time-Invariant systems

System is time invariant if when
\begin{align}
x(t)=x(t-t_{0}),
y(t)=y(t-t_{0})
\end{align}
LTI if it is linear and time invariant

x(t)=x_{1}(t) output y(t)=y_{1}(t)
x(t)=x_{2}(t) output y(t)=y_{2}(t)

System is linear if when $x(t)=a_{1}x_{1}(t)+a_{2}x_{2}(t)$

$y(t)=a_{1}y_{1}(t)+a_{2}y_{2}(t)$

```python

```


Consider a ball of mass $m$ with horizontal coordinate $x$ rolling in a double-well potential $V(x) = x^4/4 - x^2/2$. (This is sometimes called the "sombrero" potential. Plot it to see why, for $x\in[-1.5,1.5]$.) This potential produces a force $f_{\text{hat}}(x) = -V'(x) = -x^3 + x$ on the rolling ball. Suppose the ball also has slight friction, so experiences a drag force $f_{\text{drag}}(\dot{x}) = -\nu \dot{x}$. With these forces we thus expect the ball to roll down the sides of the sombrero potential and settle in one of the two stable wells. However, this is boring, so instead we are going to shake the hat back and force periodically with a driving force $f_{\text{drive}}(t) = F\cos(\omega t)$. For small driving forces $F$, this should simply jiggle the ball back and forth at the bottom of one of the stable wells. Our task will be to explore what happens for larger driving forces $F$.

```python
import numpy as np
import sympy as sp
import matplotlib.pyplot as plt
```

```python

```

```python
x = np.linspace(-1.5,1.5,1000)
plt.plot(x, temp(x,0.25))
plt.plot(x, sombrero(x))
```

```python
@np.vectorize
def sombrero(x):
    return (x**4)/4 - (x**2)/2

@np.vectorize
def f_hat(x):
    return -1*(x**3)+x

@np.vectorize
def f_drag(x, v):
    return -v*x

#TEMP
@np.vectorize
def temp(x,v):
    return f_hat(x)# + f_drag(x,v)

#@np.vectorize
#def m_dotdot(x):
```

```python
x,y = sp.symbols('x y')
```

```python
f_hat =
```

```python
acceleration = 1
velocity = 2

time = np.linspace(0,1,100)
dt = time[1]-time[0]

positions = [1]
prev_position = positions[0]
prev_velocity = 2

for time in time:
    new_velocity = prev_velocity + acceleration * dt
    new_position = prev_position + new_velocity * dt
    positions.append(new_position)
    prev_time = time
    prev_velocity = new_velocity
    pev_position = new_position
```

    [1,
     1.0203040506070808,
     1.0204060810121416,
     1.0205081114172023,
     1.020610141822263,
     1.0207121722273238,
     1.0208142026323845,
     1.020916233037445,
     1.0210182634425058,
     1.0211202938475665,
     1.0212223242526273,
     1.021324354657688,
     1.0214263850627487,
     1.0215284154678095,
     1.02163044587287,
     1.0217324762779307,
     1.0218345066829915,
     1.0219365370880522,
     1.022038567493113,
     1.0221405978981737,
     1.0222426283032344,
     1.022344658708295,
     1.0224466891133557,
     1.0225487195184164,
     1.0226507499234772,
     1.022752780328538,
     1.0228548107335986,
     1.0229568411386594,
     1.0230588715437199,
     1.0231609019487806,
     1.0232629323538414,
     1.023364962758902,
     1.0234669931639628,
     1.0235690235690236,
     1.0236710539740843,
     1.0237730843791448,
     1.0238751147842056,
     1.0239771451892663,
     1.024079175594327,
     1.0241812059993878,
     1.0242832364044485,
     1.0243852668095093,
     1.0244872972145698,
     1.0245893276196305,
     1.0246913580246912,
     1.024793388429752,
     1.0248954188348127,
     1.0249974492398735,
     1.0250994796449342,
     1.0252015100499947,
     1.0253035404550555,
     1.0254055708601162,
     1.025507601265177,
     1.0256096316702377,
     1.0257116620752984,
     1.0258136924803591,
     1.0259157228854199,
     1.0260177532904804,
     1.0261197836955411,
     1.0262218141006019,
     1.0263238445056626,
     1.0264258749107233,
     1.026527905315784,
     1.0266299357208448,
     1.0267319661259053,
     1.026833996530966,
     1.0269360269360268,
     1.0270380573410876,
     1.0271400877461483,
     1.027242118151209,
     1.0273441485562698,
     1.0274461789613303,
     1.027548209366391,
     1.0276502397714518,
     1.0277522701765125,
     1.0278543005815732,
     1.027956330986634,
     1.0280583613916947,
     1.0281603917967552,
     1.028262422201816,
     1.0283644526068767,
     1.0284664830119374,
     1.0285685134169982,
     1.028670543822059,
     1.0287725742271197,
     1.0288746046321802,
     1.028976635037241,
     1.0290786654423016,
     1.0291806958473624,
     1.0292827262524231,
     1.0293847566574839,
     1.0294867870625446,
     1.029588817467605,
     1.0296908478726658,
     1.0297928782777266,
     1.0298949086827873,
     1.029996939087848,
     1.0300989694929088,
     1.0302009998979695,
     1.03030303030303,
     1.0304050607080908]

```python
def r_plus(r, v, a)
```


# Chapter 2. Training Machine Learning Algorithms for Classication.

```python
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sbn
```

We will use `Perceptron` algorithm for binary classification problem, based on Iris dataset.

```python
df = pd.read_csv('https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data', header=None)
df.tail()
```

      0
      1
      2
      3
      4

      145
      6.7
      3.0
      5.2
      2.3
      Iris-virginica

      146
      6.3
      2.5
      5.0
      1.9
      Iris-virginica

      147
      6.5
      3.0
      5.2
      2.0
      Iris-virginica

      148
      6.2
      3.4
      5.4
      2.3
      Iris-virginica

      149
      5.9
      3.0
      5.1
      1.8
      Iris-virginica

```python
df.describe()
```

      0
      1
      2
      3

      count
      150.000000
      150.000000
      150.000000
      150.000000

      mean
      5.843333
      3.054000
      3.758667
      1.198667

      std
      0.828066
      0.433594
      1.764420
      0.763161

      min
      4.300000
      2.000000
      1.000000
      0.100000

      25%
      5.100000
      2.800000
      1.600000
      0.300000

      50%
      5.800000
      3.000000
      4.350000
      1.300000

      75%
      6.400000
      3.300000
      5.100000
      1.800000

      max
      7.900000
      4.400000
      6.900000
      2.500000

### Dataset Descripion

* **Column 0** - x0 sepal length
* **Column 2** - x1 petal lenght
* **Column 4** - y class of flower (label)

The feature vector `x` would contain of only two features: `sepal length` and `petal length`.

### Dataset Preparation and Visualization

Let's plot features on 2D graph.

```python
# prepare labels array
y = df.iloc[0:100, 4].values
# transform label array from text to [-1, 1] values
y_train = np.where(y == 'Iris-setosa', -1, 1)
```

```python
# prepare feature matrix
X_train = df.iloc[0:100, [0, 2]].values
```

```python
# prepare visualization
plt.scatter(X_train[:50, 0], X_train[:50, 1], color='red', marker='o', label='setosa')
plt.scatter(X_train[50:100, 0], X_train[50:100, 1], color='blue', marker='x', label='versicolor')
plt.xlabel('petal length')
plt.ylabel('sepal length')
plt.legend(loc='upper left')
plt.show()
```

From the plot we can see, that classes are *linearly separable*, so we can apply **Perceptron** model.

## Perceptron

Let's implement simple percepton model.

```python
def activation_function(z):
    return np.where(z >= 0, 1, -1)

def add_bias_column(X):
    (m, n) = X.shape
    X = np.concatenate((np.ones((m, 1)), X), 1)
    return X

def perceptron(X, w):
    X = add_bias_column(X)
    z = np.dot(X, w)

    return activation_function(z)
```

Try it with a randomly generated weights:

```python
def random_weights(features_dimention):
    return np.random.rand(1 + features_dimention) / 32

def zero_weights(features_dimention):
    return np.zeros(1 + features_dimention)

w_random = random_weights(2)

print('random small weights', w_random)
```

    random small weights [ 0.01829631  0.03121418  0.02712658]

Run perceptron and check it's current predictions:

```python
prediction = perceptron(X_train, w_random)
print(prediction)
```

    [1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
     1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
     1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1]

As you can see, current classifier predicts everything as `virginica`, which is not true. Let's introduce some functions, we can measure errors and accuracy of classifirer:

```python
def error(predicted, expected):
    return np.subtract(predicted, expected)

def error_count(predicted, expected):
    return np.count_nonzero(error(predicted, expected))

def minimal_square_error(predicted, expected):
    m = predicted.size
    return (np.sum(np.power(np.subtract(predicted, expected), 2)) / 2 * (m))

def classifier_accuracy(predicted, expected):
    m = predicted.size
    return error_count(predicted, expected) / m
```

### Misclassified samples

```python
print(error_count(prediction, y_train))
```

    50

### Minimal squares error

```python
print(minimal_square_error(prediction, y_train))
```

    10000.0

### Classifier accuracy

```python
print(classifier_accuracy(prediction, y_train))
```

    0.5

## Perceptron Training

As we can see from figures above, the performance on given model is very low. Our goal, is to adjust model parameters `w`, for better classification results. The learning algorithm is based on adjustion of `weights` after training session, based on error given by classifier.

\begin{align}
&\omega_j := \omega_j  + \Delta\omega_j
\end{align}

\begin{align}
&\Delta\omega_j := \alpha (y^i - \hat{y}^i) x_j^j
\end{align}

Where $\alpha$ - is a constant, called learning rate.

We run our training routine until we see good enought prediction results. Each cycle of training is called `epoch`.

```python
def run_perceptron(X, y, w):
    predicted = perceptron(X, w)
    return (
        w,
        error(y, predicted),
        error_count(y, predicted)
    )

def adjust_weigths(alpha, X, w, error):
    X = add_bias_column(X)
    delta_w = np.dot(np.dot(error, X), alpha)

    return np.add(w, delta_w)

def select_model(train_results):
    return [result for result in train_results if result[2] == 0]

def train(alpha, epochs_count, X, y):
    results = []

    w_local = random_weights(X.shape[1])

    for epoch in range(epochs_count):
        result = run_perceptron(X, y, w_local)
        # adjust the model, based on currents results and start next epoch..
        w_local = adjust_weigths(alpha, X, w_local, result[1])
        results.append(result)

    return results
```

```python
def plot_training_results(results, dim, label):
    errors = list(map(lambda r: r[dim], results))

    plt.plot(range(1, len(errors) + 1), errors, marker='o')
    plt.xlabel('Epochs')
    plt.ylabel(label)
    plt.show()
```

### Alpha = 0.01, Epochs = 10

```python
results_1 = train(0.01, 10, X_train, y_train);
model_1 = select_model(results_1)
print(model_1[0][0])

plot_training_results(results_1, 2, 'Misclassification Errors')
```

### Alpha = 0.03, Epochs = 10

```python
results_2 = train(0.03, 10, X_train, y_train)
model_2 = select_model(results_3)
print(model_2[0][0])

plot_training_results(results_2, 2, 'Misclassification Errors')
```

## Alpha = 0.1, Epochs = 10

```python
results_3 = train(0.1, 10, X_train, y_train)
model_3 = select_model(results_3)
print(model_3[0][0])

plot_training_results(results_3, 2, 'Misclassification Errors')
```