From 5818694bbe32b4cddb00447df8d64c82b18a0ddb Mon Sep 17 00:00:00 2001 From: Maximilian Ernst Date: Fri, 2 Oct 2026 12:12:49 +0200 Subject: [PATCH 1/9] add minus2ll to defautl fit measures --- src/frontend/fit/fitmeasures/fit_measures.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/fit/fitmeasures/fit_measures.jl b/src/frontend/fit/fitmeasures/fit_measures.jl index 7fabc950..c35ec9ca 100644 --- a/src/frontend/fit/fitmeasures/fit_measures.jl +++ b/src/frontend/fit/fitmeasures/fit_measures.jl @@ -1,4 +1,4 @@ -const DEFAULT_FIT_MEASURES = [AIC, BIC, dof, χ², p_value, nparams, RMSEA, CFI] +const DEFAULT_FIT_MEASURES = [minus2ll, AIC, BIC, dof, χ², p_value, nparams, RMSEA, CFI] fit_measures(fit, measures::AbstractVector) = Dict(Symbol(fn) => fn(fit) for fn in measures) fit_measures(fit, measures...) = fit_measures(fit, measures) From 367a63aac121db8c846532aae43b10a21b3e26b8 Mon Sep 17 00:00:00 2001 From: Maximilian Ernst Date: Fri, 2 Oct 2026 12:27:21 +0200 Subject: [PATCH 2/9] add rand method for SemFiniteDiff --- src/additional_functions/simulation.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/additional_functions/simulation.jl b/src/additional_functions/simulation.jl index e85e9d5c..2c7d5335 100644 --- a/src/additional_functions/simulation.jl +++ b/src/additional_functions/simulation.jl @@ -34,6 +34,8 @@ Distributions.rand(loss::SemLoss, params, n::Integer) = rand(SEM.implied(loss), Distributions.rand(model::Sem, params, n::Integer) = rand(sem_term(model), params, n) +Distributions.rand(wrapper::SemFiniteDiff, params, n::Integer) = rand(wrapper.model, params, n) + # rand() overloads without SEM params Distributions.rand(implied::Union{SemImplied, SemLoss, Sem}, n::Integer) = Distributions.rand(implied, nothing, n) From c4ea138ec023701e9de1f299c3aeaebbb19cbed5 Mon Sep 17 00:00:00 2001 From: Maximilian Ernst Date: Fri, 2 Oct 2026 12:58:02 +0200 Subject: [PATCH 3/9] set default partable value to NaN --- src/frontend/specification/ParameterTable.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/specification/ParameterTable.jl b/src/frontend/specification/ParameterTable.jl index c9b9dc24..72158d1b 100644 --- a/src/frontend/specification/ParameterTable.jl +++ b/src/frontend/specification/ParameterTable.jl @@ -277,7 +277,7 @@ function update_partable!( for (i, par) in enumerate(partable.columns[:label]) if par == :const - coldata[i] = !isnothing(default) ? (isvec_def ? default[i] : default) : zero(T) + coldata[i] = !isnothing(default) ? (isvec_def ? default[i] : default) : T(NaN) elseif haskey(params, par) coldata[i] = params[par] else From e6368efd5cd97623f34e24b033313d0591e94668 Mon Sep 17 00:00:00 2001 From: Maximilian Ernst Date: Fri, 2 Oct 2026 19:59:38 +0200 Subject: [PATCH 4/9] add minus2ll scaling to fitmeasure test helper --- test/examples/helper.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/examples/helper.jl b/test/examples/helper.jl index fed95f3c..6b2ac647 100644 --- a/test/examples/helper.jl +++ b/test/examples/helper.jl @@ -59,6 +59,7 @@ fitmeasure_semjl_to_lavaan = Dict( :nparams => "npar", :RMSEA => "rmsea", :CFI => "cfi", + :minus2ll => "logl" ) function test_fitmeasures( @@ -81,6 +82,7 @@ function test_fitmeasures( @test ismissing(measure) else measure_lav = measures_lav.x[lav_ix] + measure_lav = name == :minus2ll ? -2measure_lav : measure_lav @test measure ≈ measure_lav rtol = rtol atol = atol end end From 2f759c41feea161c9787e6bd640aaa14c7ad1454 Mon Sep 17 00:00:00 2001 From: Maximilian Ernst Date: Fri, 2 Oct 2026 22:34:05 +0200 Subject: [PATCH 5/9] fix Newton convergence issues with different linesearch algo --- test/examples/political_democracy/constructor.jl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/examples/political_democracy/constructor.jl b/test/examples/political_democracy/constructor.jl index 2efa5abe..1376cfe3 100644 --- a/test/examples/political_democracy/constructor.jl +++ b/test/examples/political_democracy/constructor.jl @@ -191,8 +191,13 @@ if opt_engine == :Optim end @testset "ml_solution_hessian" begin - solution = fit(SemOptimizer(engine = :Optim, algorithm = Newton()), model_ml) - + solution = fit( + SemOptimizer( + engine = :Optim, + algorithm = Newton(linesearch = BackTracking(order = 3)), + ), + model_ml, + ) update_estimate!(partable, solution) test_estimates(partable, solution_lav[:parameter_estimates_ml]; atol = 1e-2) end From 5f15aa5ed2b4cc7e23aa8550fb0d2f6fbace0865 Mon Sep 17 00:00:00 2001 From: Maximilian Ernst Date: Sat, 3 Oct 2026 11:27:46 +0200 Subject: [PATCH 6/9] fix infinity norm in testing --- test/examples/helper.jl | 16 +++++++++------- test/examples/multigroup/build_models.jl | 4 ++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/test/examples/helper.jl b/test/examples/helper.jl index 6b2ac647..d461c225 100644 --- a/test/examples/helper.jl +++ b/test/examples/helper.jl @@ -59,7 +59,7 @@ fitmeasure_semjl_to_lavaan = Dict( :nparams => "npar", :RMSEA => "rmsea", :CFI => "cfi", - :minus2ll => "logl" + :minus2ll => "logl", ) function test_fitmeasures( @@ -88,6 +88,10 @@ function test_fitmeasures( end end +# LinearAlgebra v1.13 ignores`norm` keyword for isapprox on arrays (issue #1675) +isapprox_infnorm(x::AbstractArray, y::AbstractArray; atol::Real = 0, rtol::Real = 0) = + norm(x - y, Inf) <= max(atol, rtol * max(norm(x, Inf), norm(y, Inf))) + function test_estimates( partable::ParameterTable, partable_lav; @@ -105,10 +109,9 @@ function test_estimates( @test !any(isnan, expected) if skip # workaround skip=false not supported in earlier versions - @test actual ≈ expected rtol = rtol atol = atol norm = Base.Fix2(norm, Inf) skip = - skip + @test isapprox_infnorm(actual, expected; atol = atol, rtol = rtol) skip = skip else - @test actual ≈ expected rtol = rtol atol = atol norm = Base.Fix2(norm, Inf) + @test isapprox_infnorm(actual, expected; atol = atol, rtol = rtol) end end @@ -138,10 +141,9 @@ function test_estimates( @test !any(isnan, expected) if skip # workaround skip=false not supported in earlier versions - @test actual ≈ expected rtol = rtol atol = atol norm = Base.Fix2(norm, Inf) skip = - skip + @test isapprox_infnorm(actual, expected; atol = atol, rtol = rtol) skip = skip else - @test actual ≈ expected rtol = rtol atol = atol norm = Base.Fix2(norm, Inf) + @test isapprox_infnorm(actual, expected; atol = atol, rtol = rtol) end end diff --git a/test/examples/multigroup/build_models.jl b/test/examples/multigroup/build_models.jl index 47bdea22..a706e966 100644 --- a/test/examples/multigroup/build_models.jl +++ b/test/examples/multigroup/build_models.jl @@ -58,7 +58,7 @@ end test_estimates( partable, solution_lav[:parameter_estimates_ml]; - atol = 1e-3, + atol = 1e-4, col = :se, lav_col = :se, lav_groups = Dict(:Pasteur => 1, :Grant_White => 2), @@ -118,7 +118,7 @@ end test_estimates( partable_s, solution_lav[:parameter_estimates_ml]; - atol = 1e-3, + atol = 1e-4, col = :se, lav_col = :se, lav_groups = Dict(:Pasteur => 1, :Grant_White => 2), From c88dba15dc970bac27585572b5adc74fbd572655 Mon Sep 17 00:00:00 2001 From: Maximilian Ernst Date: Sat, 3 Oct 2026 11:49:46 +0200 Subject: [PATCH 7/9] adapt hessian scaling for standard errors --- src/frontend/fit/standard_errors/hessian.jl | 23 +++++++++------------ 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/frontend/fit/standard_errors/hessian.jl b/src/frontend/fit/standard_errors/hessian.jl index 80b96d33..7d67b59e 100644 --- a/src/frontend/fit/standard_errors/hessian.jl +++ b/src/frontend/fit/standard_errors/hessian.jl @@ -34,22 +34,19 @@ function se_hessian(fit::SemFit; method = :finitediff) return [sqrt(c * H_inv[i]) for i in diagind(H_inv)] end -# Addition functions ------------------------------------------------------------- -H_scaling(loss::SemML) = 2 / (nsamples(loss) - 1) +# Additional functions ------------------------------------------------------------- -function H_scaling(loss::SemWLS) - @warn "Standard errors for WLS are only correct if a GLS weight matrix (the default) is used." - return 2 / (nsamples(loss) - 1) -end - -H_scaling(loss::SemFIML) = 2 / nsamples(loss) +H_nsamples(loss::SemML) = nsamples(loss) - 1 +H_nsamples(loss::SemWLS) = nsamples(loss) - 1 +H_nsamples(loss::SemFIML) = nsamples(loss) +H_nsamples(loss::SemLoss) = nsamples(loss) +H_nsamples(wrapper::SemLossFiniteDiff) = H_nsamples(_unwrap(wrapper)) function H_scaling(model::AbstractSem) semterms = SEM.sem_terms(model) - if length(semterms) > 1 - #@warn "Hessian scaling for multiple loss functions is not implemented yet" - return 2 / nsamples(model) - else - return length(semterms) >= 1 ? H_scaling(loss(semterms[1])) : 1.0 + isempty(semterms) && return 1.0 + if any(term -> _unwrap(loss(term)) isa SemWLS, semterms) + @warn "Standard errors for WLS are only correct if a GLS weight matrix (the default) is used." end + return 2 / sum(term -> H_nsamples(loss(term)), semterms) end From d5db5d991f35e3e67823831634647053b8927569 Mon Sep 17 00:00:00 2001 From: Maximilian Ernst Date: Sat, 3 Oct 2026 11:56:31 +0200 Subject: [PATCH 8/9] bump package and julia version --- Project.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index cfbecee3..6690ecbc 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "StructuralEquationModels" uuid = "383ca8c5-e4ff-4104-b0a9-f7b279deed53" authors = ["Maximilian Ernst", "Aaron Peikert"] -version = "0.5.0" +version = "0.5.1" [deps] DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" @@ -27,7 +27,7 @@ Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7" SymbolicUtils = "d1185830-fcd6-423d-90d6-eec64667417b" [compat] -julia = "1.10, 1.11, 1.12" +julia = "1.10, 1.11, 1.12, 1.13" StenoGraphs = "0.5" DataFrames = "1" Distributions = "0.25" From 680a1428b7ee7075905b44db5d9d4b184797a4cd Mon Sep 17 00:00:00 2001 From: Maximilian Ernst <34346372+Maximilian-Stefan-Ernst@users.noreply.github.com> Date: Sat, 3 Oct 2026 13:46:22 +0200 Subject: [PATCH 9/9] formatting Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/additional_functions/simulation.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/additional_functions/simulation.jl b/src/additional_functions/simulation.jl index 2c7d5335..d5bdbe91 100644 --- a/src/additional_functions/simulation.jl +++ b/src/additional_functions/simulation.jl @@ -34,7 +34,8 @@ Distributions.rand(loss::SemLoss, params, n::Integer) = rand(SEM.implied(loss), Distributions.rand(model::Sem, params, n::Integer) = rand(sem_term(model), params, n) -Distributions.rand(wrapper::SemFiniteDiff, params, n::Integer) = rand(wrapper.model, params, n) +Distributions.rand(wrapper::SemFiniteDiff, params, n::Integer) = + rand(wrapper.model, params, n) # rand() overloads without SEM params Distributions.rand(implied::Union{SemImplied, SemLoss, Sem}, n::Integer) =