Skip to content

Commit f4e104e

Browse files
Merge pull request #200 from StructuralEquationModels/devel
Release 0.2.4
2 parents 7d46e22 + f1d0b85 commit f4e104e

35 files changed

+497
-677
lines changed

.github/workflows/CI.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ on:
33
push:
44
branches: '*'
55
tags: '*'
6+
pull_request:
7+
branches: '*'
68
workflow_dispatch:
79
jobs:
810
test:

.github/workflows/FormatCheck.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: Format suggestions
22
on:
3-
pull_request:
3+
pull_request_target:
44
# this argument is not required if you don't use the `suggestion-label` input
55
types: [ opened, reopened, synchronize, labeled, unlabeled ]
66
jobs:

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "StructuralEquationModels"
22
uuid = "383ca8c5-e4ff-4104-b0a9-f7b279deed53"
33
authors = ["Maximilian Ernst", "Aaron Peikert"]
4-
version = "0.2.3"
4+
version = "0.2.4"
55

66
[deps]
77
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"

src/StructuralEquationModels.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ using LinearAlgebra,
44
Optim,
55
NLSolversBase,
66
Statistics,
7+
StatsBase,
78
SparseArrays,
89
Symbolics,
910
NLopt,
@@ -18,6 +19,8 @@ using LinearAlgebra,
1819
import DataFrames: DataFrame
1920
export StenoGraphs, @StenoGraph, meld
2021

22+
const SEM = StructuralEquationModels
23+
2124
# type hierarchy
2225
include("types.jl")
2326
include("objective_gradient_hessian.jl")
@@ -139,6 +142,7 @@ export AbstractSem,
139142
update_partable!,
140143
update_estimate!,
141144
update_start!,
145+
update_se_hessian!,
142146
Fixed,
143147
fixed,
144148
Start,

src/additional_functions/helper.jl

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
1-
function neumann_series(mat::SparseMatrixCSC)
1+
# Neumann series representation of (I - mat)⁻¹
2+
function neumann_series(mat::SparseMatrixCSC; maxiter::Integer = size(mat, 1))
23
inverse = I + mat
34
next_term = mat^2
45

6+
n = 1
57
while nnz(next_term) != 0
8+
(n <= maxiter) || error("Neumann series did not converge in $maxiter steps")
69
inverse += next_term
710
next_term *= mat
11+
n += 1
812
end
913

1014
return inverse
1115
end
1216

13-
#=
17+
#=
1418
function make_onelement_array(A)
1519
isa(A, Array) ? nothing : (A = [A])
1620
return A
@@ -37,13 +41,8 @@ function get_observed(rowind, data, semobserved; args = (), kwargs = NamedTuple(
3741
return observed_vec
3842
end
3943

40-
function skipmissing_mean(mat)
41-
means = Vector{Float64}(undef, size(mat, 2))
42-
for i in 1:size(mat, 2)
43-
@views means[i] = mean(skipmissing(mat[:, i]))
44-
end
45-
return means
46-
end
44+
skipmissing_mean(mat::AbstractMatrix) =
45+
[mean(skipmissing(coldata)) for coldata in eachcol(mat)]
4746

4847
function F_one_person(imp_mean, meandiff, inverse, data, logdet)
4948
F = logdet
@@ -52,10 +51,10 @@ function F_one_person(imp_mean, meandiff, inverse, data, logdet)
5251
return F
5352
end
5453

55-
function remove_all_missing(data)
54+
function remove_all_missing(data::AbstractMatrix)
5655
keep = Vector{Int64}()
57-
for i in 1:size(data, 1)
58-
if any(.!ismissing.(data[i, :]))
56+
for (i, coldata) in zip(axes(data, 1), eachrow(data))
57+
if any(!ismissing, coldata)
5958
push!(keep, i)
6059
end
6160
end
@@ -108,11 +107,8 @@ function sparse_outer_mul!(C, A, B::Vector, ind) #computes A*S*B -> C, where ind
108107
end
109108

110109
function cov_and_mean(rows; corrected = false)
111-
data = transpose(reduce(hcat, rows))
112-
size(rows, 1) > 1 ? obs_cov = Statistics.cov(data; corrected = corrected) :
113-
obs_cov = reshape([0.0], 1, 1)
114-
obs_mean = vec(Statistics.mean(data, dims = 1))
115-
return obs_cov, obs_mean
110+
obs_mean, obs_cov = StatsBase.mean_and_cov(reduce(hcat, rows), 2, corrected = corrected)
111+
return obs_cov, vec(obs_mean)
116112
end
117113

118114
function duplication_matrix(nobs)
Lines changed: 61 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
1-
function fill_A_S_M(A, S, M, A_indices, S_indices, M_indices, parameters)
2-
for (iA, iS, par) in zip(A_indices, S_indices, parameters)
1+
# fill A, S, and M matrices with the parameter values according to the parameters map
2+
function fill_A_S_M!(
3+
A::AbstractMatrix,
4+
S::AbstractMatrix,
5+
M::Union{AbstractVector, Nothing},
6+
A_indices::AbstractArrayParamsMap,
7+
S_indices::AbstractArrayParamsMap,
8+
M_indices::Union{AbstractArrayParamsMap, Nothing},
9+
parameters::AbstractVector,
10+
)
11+
@inbounds for (iA, iS, par) in zip(A_indices, S_indices, parameters)
312
for index_A in iA
413
A[index_A] = par
514
end
@@ -10,22 +19,28 @@ function fill_A_S_M(A, S, M, A_indices, S_indices, M_indices, parameters)
1019
end
1120

1221
if !isnothing(M)
13-
for (iM, par) in zip(M_indices, parameters)
22+
@inbounds for (iM, par) in zip(M_indices, parameters)
1423
for index_M in iM
1524
M[index_M] = par
1625
end
1726
end
1827
end
1928
end
2029

21-
function get_parameter_indices(parameters, M; linear = true, kwargs...)
22-
M_indices = [findall(x -> (x == par), M) for par in parameters]
23-
24-
if linear
25-
M_indices = cartesian2linear.(M_indices, [M])
30+
# build the map from the index of the parameter to the linear indices
31+
# of this parameter occurences in M
32+
# returns ArrayParamsMap object
33+
function array_parameters_map(parameters::AbstractVector, M::AbstractArray)
34+
params_index = Dict(param => i for (i, param) in enumerate(parameters))
35+
T = Base.eltype(eachindex(M))
36+
res = [Vector{T}() for _ in eachindex(parameters)]
37+
for (i, val) in enumerate(M)
38+
par_ind = get(params_index, val, nothing)
39+
if !isnothing(par_ind)
40+
push!(res[par_ind], i)
41+
end
2642
end
27-
28-
return M_indices
43+
return res
2944
end
3045

3146
function eachindex_lower(M; linear_indices = false, kwargs...)
@@ -49,9 +64,6 @@ function linear2cartesian(ind_lin, dims)
4964
return ind_cart
5065
end
5166

52-
cartesian2linear(ind_cart, A::AbstractArray) = cartesian2linear(ind_cart, size(A))
53-
linear2cartesian(ind_linear, A::AbstractArray) = linear2cartesian(ind_linear, size(A))
54-
5567
function set_constants!(M, M_pre)
5668
for index in eachindex(M)
5769
δ = tryparse(Float64, string(M[index]))
@@ -74,116 +86,52 @@ function check_constants(M)
7486
return false
7587
end
7688

77-
function get_matrix_derivative(M_indices, parameters, n_long)
78-
∇M = [
79-
sparsevec(M_indices[i], ones(length(M_indices[i])), n_long) for
80-
i in 1:length(parameters)
81-
]
82-
83-
∇M = reduce(hcat, ∇M)
84-
85-
return ∇M
89+
# construct length(M)×length(parameters) sparse matrix of 1s at the positions,
90+
# where the corresponding parameter occurs in the M matrix
91+
function matrix_gradient(M_indices::ArrayParamsMap, M_length::Integer)
92+
rowval = reduce(vcat, M_indices)
93+
colptr =
94+
pushfirst!(accumulate((ptr, M_ind) -> ptr + length(M_ind), M_indices, init = 1), 1)
95+
return SparseMatrixCSC(
96+
M_length,
97+
length(M_indices),
98+
colptr,
99+
rowval,
100+
ones(length(rowval)),
101+
)
86102
end
87103

88-
function fill_matrix(M, M_indices, parameters)
104+
# fill M with parameters
105+
function fill_matrix!(
106+
M::AbstractMatrix,
107+
M_indices::AbstractArrayParamsMap,
108+
parameters::AbstractVector,
109+
)
89110
for (iM, par) in zip(M_indices, parameters)
90111
for index_M in iM
91112
M[index_M] = par
92113
end
93114
end
115+
return M
94116
end
95117

96-
function get_partition(A_indices, S_indices)
97-
n_par = length(A_indices)
98-
99-
first_A = "a"
100-
first_S = "a"
101-
last_A = "a"
102-
last_S = "a"
103-
104-
for i in 1:n_par
105-
if length(A_indices[i]) != 0
106-
first_A = i
107-
break
108-
end
109-
end
110-
111-
for i in 1:n_par
112-
if length(S_indices[i]) != 0
113-
first_S = i
114-
break
115-
end
116-
end
117-
118-
for i in n_par + 1 .- (1:n_par)
119-
if length(A_indices[i]) != 0
120-
last_A = i
121-
break
122-
end
123-
end
124-
125-
for i in n_par + 1 .- (1:n_par)
126-
if length(S_indices[i]) != 0
127-
last_S = i
128-
break
129-
end
130-
end
131-
132-
for i in first_A:last_A
133-
if length(A_indices[i]) == 0
134-
throw(
135-
ErrorException(
136-
"Your parameter vector is not partitioned into directed and undirected effects",
137-
),
138-
)
139-
return nothing
140-
end
141-
end
142-
143-
for i in first_S:last_S
144-
if length(S_indices[i]) == 0
145-
throw(
146-
ErrorException(
147-
"Your parameter vector is not partitioned into directed and undirected effects",
148-
),
149-
)
150-
return nothing
151-
end
152-
end
153-
154-
return first_A:last_A, first_S:last_S
155-
end
156-
157-
function get_partition(M_indices)
158-
n_par = length(M_indices)
159-
160-
first_M = "a"
161-
last_M = "a"
162-
163-
for i in 1:n_par
164-
if length(M_indices[i]) != 0
165-
first_M = i
166-
break
167-
end
168-
end
169-
170-
for i in n_par + 1 .- (1:n_par)
171-
if length(M_indices[i]) != 0
172-
last_M = i
173-
break
174-
end
175-
end
176-
177-
for i in first_M:last_M
178-
if length(M_indices[i]) == 0
179-
throw(
180-
ErrorException(
181-
"Your parameter vector is not partitioned into directed, undirected and mean effects",
182-
),
183-
)
184-
return nothing
118+
# range of parameters that are referenced in the matrix
119+
function param_range(mtx_indices::AbstractArrayParamsMap)
120+
first_i = findfirst(!isempty, mtx_indices)
121+
last_i = findlast(!isempty, mtx_indices)
122+
123+
if !isnothing(first_i) && !isnothing(last_i)
124+
for i in first_i:last_i
125+
if isempty(mtx_indices[i])
126+
# TODO show which parameter is missing in which matrix
127+
throw(
128+
ErrorException(
129+
"Your parameter vector is not partitioned into directed and undirected effects",
130+
),
131+
)
132+
end
185133
end
186134
end
187135

188-
return first_M:last_M
136+
return first_i:last_i
189137
end

0 commit comments

Comments
 (0)