Custom structures

To make your own covariance structure first you should make struct that <: AbstractCovarianceType:

Example:

struct YourCovarianceStruct <: AbstractCovarianceType end

You also can specify additional field if you need to use them inside gmat!/rmat! functions.

Then you can make function for construction random effect matrix (gmat!) and repeated effect (rmat!). Only upper triangular can be updated.

Function gmat! have 3 arguments: mx - zero matrix, θ - theta vector for this effect, and your custom structure object.

Next this function used to make "random" part of variance-covariance matrix: V' = Z * G * Z'

function Metida.gmat!(mx, θ, ::YourCovarianceStruct)
    @inbounds @simd for i = 1:size(mx, 1)
        mx[i, i] = θ[i] ^ 2
    end
    nothing
end

Function rmat! have 5 arguments and add repeated effect to V': V = V' + R (so V = Z * G * Z' + R), mx - V' matrix, θ - theta vector for this effect, rz - subject effect matrix, ct - your covariance type object, sb = block number. For example, rmat! for Heterogeneous Toeplitz Parameterized structure is specified bellow (TOEPHP_ <: AbstractCovarianceType).

function Metida.rmat!(mx, θ, rz, ct::TOEPHP_, ::Int)
    l     = size(rz, 2)
    vec   = rz * (θ[1:l])
    s   = size(mx, 1)
    if s > 1 && ct.p > 1
        for m = 1:s - 1
            for n = m + 1:(m + ct.p - 1 > s ? s : m + ct.p - 1)
                @inbounds  mx[m, n] += vec[m] * vec[n] * θ[n - m + l]
            end
        end
    end
    @inbounds @simd for m = 1:s
        mx[m, m] += vec[m] * vec[m]
    end
    nothing
end

One more function you shoud make is covstrparam, this function need to know how many parameters included in theta vector for optimization. Function returns number of variance parameters and rho parameters for this structure. Where t - number of columns in individual Z matrix for random effect or number of columns in repeated effect matrix (rZ).

Example for Heterogeneous Autoregressive and Heterogeneous Compound Symmetry structures:

function Metida.covstrparam(ct::Union{ARH_, CSH_}, t::Int)::Tuple{Int, Int}
    return (t, 1)
end

For better printing you can add:

function Metida.rcoefnames(s, t, ct::YourCovarianceStruct)
    return ["σ² ", "γ ", "ρ "]
end

Where, s - effect schema, t - number of parameters, this function returns names for your covariance structure for printing in LMM output.

Add this method for better printing:

function Base.show(io::IO, ct::YourCovarianceStruct)
    print(io, "YourCovarianceStruct")
end

Then just make model and fit it:

lmm = Metida.LMM(@formula(response ~ 1 + factor*time), ftdf2;
  random = Metida.VarEffect(Metida.@covstr(factor|subject&factor), YourCovarianceStruct()),
  repeated = Metida.VarEffect(Metida.@covstr(1|subject&factor), YourCovarianceStruct()),
  )
  Metida.fit!(lmm)

Example:

using Metida, DataFrames, CSV, CategoricalArrays

spatdf       = CSV.File(joinpath(dirname(pathof(Metida)), "..", "test", "csv", "spatialdata.csv"); types = [Int, Int, String, Float64, Float64, Float64, Float64, Float64]) |> DataFrame
ftdf = CSV.File(joinpath(dirname(pathof(Metida)), "..", "test", "csv",  "1fptime.csv"); types = [String, String, Float64, Float64]) |> DataFrame
df0 = CSV.File(joinpath(dirname(pathof(Metida)), "..", "test", "csv",  "df0.csv"); types = [String, String, String, String,Float64, Float64, Float64]) |> DataFrame

struct CustomCovarianceStructure <: Metida.AbstractCovarianceType end
function Metida.covstrparam(ct::CustomCovarianceStructure, t::Int)::Tuple{Int, Int}
    return (t, 1)
end
function Metida.gmat!(mx, θ, ct::CustomCovarianceStructure)
    s = size(mx, 1)
    @inbounds @simd for m = 1:s
        mx[m, m] = θ[m]
    end
    if s > 1
        for m = 1:s - 1
            @inbounds @simd for n = m + 1:s
                mx[m, n] = mx[m, m] * mx[n, n] * θ[end]
            end
        end
    end
    @inbounds @simd for m = 1:s
        mx[m, m] = mx[m, m] * mx[m, m]
    end
    nothing
end

lmm = Metida.LMM(@formula(response ~1 + factor*time), ftdf;
random = Metida.VarEffect(Metida.@covstr(1 + time|subject&factor), CustomCovarianceStructure()),
)
Metida.fit!(lmm)

# for R matrix

function Metida.rmat!(mx, θ, rz, ::CustomCovarianceStructure, ::Int)
    vec = Metida.tmul_unsafe(rz, θ)
    rn    = size(mx, 1)
    if rn > 1
        for m = 1:rn - 1
            @inbounds @simd for n = m + 1:rn
                mx[m, n] += vec[m] * vec[n] * θ[end]
            end
        end
    end
        @inbounds  for m ∈ axes(mx, 1)
        mx[m, m] += vec[m] * vec[m]
    end
    nothing
end

lmm = Metida.LMM(@formula(var~sequence+period+formulation), df0;
repeated = Metida.VarEffect(Metida.@covstr(period|subject), CustomCovarianceStructure()),
)
Metida.fit!(lmm)
Linear Mixed Model: var ~ sequence + period + formulation
Random 1: 
   No
Repeated: 
    Model: period|subject
    Type: CustomCovarianceStructure (5)
Blocks: 5, Maximum block size: 4
Status: converged (No Errors)
    -2 logREML: 8.7401    BIC: 22.603

    Fixed-effects parameters:
───────────────────────────────────────────────────────
                     Coef.  Std. Error      z  Pr(>|z|)
───────────────────────────────────────────────────────
(Intercept)      1.31285     0.28779     4.56    <1e-05
sequence: 2      0.283193    0.328706    0.86    0.3889
period: 2        0.152412    0.0881943   1.73    0.0840
period: 3        0.08        0.136257    0.59    0.5571
period: 4        0.152412    0.0986418   1.55    0.1223
formulation: 2  -0.0379392   0.0717112  -0.53    0.5968
───────────────────────────────────────────────────────
    Variance components:
    θ vector: [0.43779, 0.426869, 0.630382, 0.49783, 0.899017]
  Residual   Val    var   0.19166
  Residual   Val    var   0.182217
  Residual   Val    var   0.397381
  Residual   Val    var   0.247835
  Residual   Val    rho   0.899017

Custom distance estimation for spatial structures

If you want to use coordinates or some other structures for distance estimation you can define method Metida.edistance to calculate distance:

function Metida.edistance(mx::AbstractMatrix{<:CartesianIndex}, i::Int, j::Int)
    return sqrt((mx[i, 1][1] - mx[j, 1][1])^2 + (mx[i, 1][2] - mx[j, 1][2])^2)
end

For example this method returns distance between two vectors represented as CartesianIndex.

Make vector of CartesianIndex:

spatdf.ci = map(x -> CartesianIndex(x[:x], x[:y]), eachrow(spatdf))
200-element Vector{CartesianIndex{2}}:
 CartesianIndex(8, 94)
 CartesianIndex(9, 40)
 CartesianIndex(40, 62)
 CartesianIndex(49, 24)
 CartesianIndex(20, 89)
 CartesianIndex(57, 14)
 CartesianIndex(44, 68)
 CartesianIndex(64, 90)
 CartesianIndex(30, 81)
 CartesianIndex(79, 32)
 ⋮
 CartesianIndex(100, 45)
 CartesianIndex(55, 59)
 CartesianIndex(11, 22)
 CartesianIndex(37, 25)
 CartesianIndex(57, 8)
 CartesianIndex(81, 99)
 CartesianIndex(68, 83)
 CartesianIndex(89, 17)
 CartesianIndex(62, 79)

Then use new column as "raw" variable with Metida.RawCoding contrast and fit the model:

lmm = Metida.LMM(@formula(r2 ~ f), spatdf;
    repeated = Metida.VarEffect(Metida.@covstr(ci|1), Metida.SPEXP; coding = Dict(:ci => Metida.RawCoding())),
    )
Metida.fit!(lmm)
Linear Mixed Model: r2 ~ f
Random 1: 
   No
Repeated: 
    Model: ci|1
    Type: SPEXP (2)
Blocks: 1, Maximum block size: 200
Status: converged (No Errors)
    -2 logREML: 1985.34    BIC: 1995.92

    Fixed-effects parameters:
────────────────────────────────────────────────
               Coef.  Std. Error     z  Pr(>|z|)
────────────────────────────────────────────────
(Intercept)  6.26619     8.841    0.71    0.4785
f: 1         3.85137     4.12172  0.93    0.3501
────────────────────────────────────────────────
    Variance components:
    θ vector: [46.8999, 8.28173]
  Residual   σ²    var     2199.6
  Residual   θ     theta   8.28173