Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
6aa78cb
FIML: SemFIMLPattern
alyst Feb 4, 2025
6294cd9
EM: optimizations
Mar 20, 2024
53c7da8
EM MVN: decouple from SemObsMissing
alyst Dec 22, 2024
507b34d
ObsMissing: docstring update
Feb 9, 2026
e6f881f
EM MVN: further optimizations
Apr 17, 2024
6171e87
trunc_eigvals(): new helper func
Feb 9, 2026
77a07fa
em_mvn(): min_eigval to enforce pos-def
Feb 9, 2026
a39ef68
add ProgressMeter dep
Feb 9, 2026
87b5a07
em_mvn(): verbose arg and progress bar
Feb 9, 2026
4fc4470
test/fiml: set EM MVN rtol=1e-10
alyst Apr 14, 2024
5f637cd
SemObsMissing: fix obs_mean() test
alyst Aug 11, 2024
6a40d0a
cleanup docstring
alyst Feb 12, 2026
5f88b29
em_mvn(): cleanup docstring
Feb 13, 2026
068076e
em_step!(): enforce symmetry
Feb 13, 2026
37cab4b
trunc_eval(): check that mtx is sym, fix reconstruct
Feb 13, 2026
17ce0bb
FIML: fixup ws and msg indent
alyst Feb 15, 2026
4855d2e
tests/model: fix ws
Mar 17, 2024
16f7be9
check_meanstruct_spec(): fix ws
Mar 17, 2024
1cda451
SemML&WLS: warn if SemObsMissing
alyst Feb 16, 2026
5dc2fa0
SemObservedData: fix ws and msg indent
Mar 17, 2024
373ca37
fix RMSEA
Mar 19, 2024
8174b59
declare cov matrices symmetric
alyst Jun 13, 2024
9e80ba3
start_simple(SemEnsemble): simplify
Mar 20, 2024
029ee54
RAM: reuse sigma array
Mar 23, 2024
1628a35
RAM: optional sparse Sigma matrix
Apr 1, 2024
4ca93c7
ML: refactor to minimize allocs
alyst Feb 4, 2025
6ba42f9
add PackageExtensionCompat
Mar 12, 2024
c51ac5b
variance_params(SEMSpec)
Mar 26, 2024
3094723
predict_latent_vars()
alyst Dec 23, 2024
428d964
fixup docstring
Feb 6, 2026
3bd4de9
lavaan_model()
Apr 1, 2024
82f988a
test_grad/hess(): check that alt calls give same results
May 29, 2024
b4d682b
start_simple(): code cleanup
alyst Aug 11, 2024
432e594
start_simple(): start vals for lat and obs means
Jul 9, 2024
4dae7e9
observed_vars(RAMMatrices; order): rows/cols order
alyst Dec 22, 2024
f6fc013
observed_var_indices(::RAMMatrices; order=:columns)
Sep 22, 2024
6eb2065
move sparse mtx utils to new file
alyst Dec 22, 2024
638743e
reorder_observed_vars!(spec) method
alyst Feb 2, 2025
05a15b5
vech() and vechinds() functions
alyst Dec 24, 2024
21d70af
SemImplied/SemLossFun: drop meanstructure kwarg
alyst Feb 4, 2025
d818237
RAMMatrices(): ctor to replace params
May 27, 2024
a79d412
use `@printf` to limit signif digits printed
alyst Dec 24, 2024
eeda75b
ML/FIML: workaround generic_matmul issue
alyst Dec 24, 2024
51199ea
refactor Sem, SemEnsemble, SemLoss
alyst Feb 3, 2025
55c9694
remove multigroup2 tests
alyst Dec 24, 2024
f922d79
BlackBoxOptim.jl backend support
alyst Dec 23, 2024
3ce9089
fit_measures(): formatting tweaks
May 27, 2024
3556233
improve docstrings for fit measures
Aug 31, 2024
cb5760c
non_posdef_objective()
Aug 31, 2024
fbbf0fc
prepare_start_params(): tighten type check
Jan 16, 2025
35e9c09
WIP SemImpliedState
Jan 16, 2025
93c02d5
MeanStruct(ram)
alyst Feb 8, 2026
2b5bcaa
SemObserved: fix mean_and_cov() call
Feb 21, 2025
0abd063
filter_used_params()
Jan 16, 2025
67fa787
param_indices(spec) method
May 20, 2024
97406bd
SemNorm: generalize SemRidge
alyst Feb 15, 2026
2e495a3
tests: regularization/SemNorm
alyst Feb 15, 2026
001bc35
add SemHinge
May 20, 2024
463cae5
add SemSquaredHinge
May 20, 2024
b5d3e6c
generalized hinge
alyst Feb 15, 2026
736504d
tests: regularization/SemHinge
alyst Feb 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ LineSearches = "d3d80556-e9d4-5f37-9878-2ab0fcc64255"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
NLSolversBase = "d41bc354-129a-5804-8e4c-c37616107c6c"
Optim = "429524aa-4258-5aef-a3af-852621145aeb"
PackageExtensionCompat = "65ce6f38-6b18-4e1d-a461-8949797d7930"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
Expand Down Expand Up @@ -50,9 +53,12 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
test = ["Test"]

[weakdeps]
BlackBoxOptim = "a134a8b2-14d6-55f6-9291-3336d3ab0209"
NLopt = "76087f3c-5699-56af-9a33-bf431cd00edd"
Optimisers = "3bd65402-5787-11e9-1adc-39752487f4e2"
ProximalAlgorithms = "140ffc9f-1907-541a-a177-7475e0a401e9"

[extensions]
SEMNLOptExt = "NLopt"
SEMProximalOptExt = "ProximalAlgorithms"
SEMBlackBoxOptimExt = ["BlackBoxOptim", "Optimisers"]
49 changes: 49 additions & 0 deletions ext/SEMBlackBoxOptimExt/AdamMutation.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# mutate by moving in the gradient direction
mutable struct AdamMutation{M <: AbstractSem, O, S} <: MutationOperator
model::M
optim::O
opt_state::S
params_fraction::Float64

function AdamMutation(model::AbstractSem, params::AbstractDict)
optim = RAdam(params[:AdamMutation_eta], params[:AdamMutation_beta])
params_fraction = params[:AdamMutation_params_fraction]
opt_state = Optimisers.init(optim, Vector{Float64}(undef, nparams(model)))

new{typeof(model), typeof(optim), typeof(opt_state)}(
model,
optim,
opt_state,
params_fraction,
)
end
end

Base.show(io::IO, op::AdamMutation) =
print(io, "AdamMutation(", op.optim, " state[3]=", op.opt_state[3], ")")

"""
Default parameters for `AdamMutation`.
"""
const AdamMutation_DefaultOptions = ParamsDict(
:AdamMutation_eta => 1E-1,
:AdamMutation_beta => (0.99, 0.999),
:AdamMutation_params_fraction => 0.25,
)

function BlackBoxOptim.apply!(m::AdamMutation, v::AbstractVector{<:Real}, target_index::Int)
grad = similar(v)
obj = SEM.evaluate!(0.0, grad, nothing, m.model, v)
@inbounds for i in eachindex(grad)
(rand() > m.params_fraction) && (grad[i] = 0.0)
end

m.opt_state, dv = Optimisers.apply!(m.optim, m.opt_state, v, grad)
if (m.opt_state[3][1] <= 1E-20) || !isfinite(obj) || any(!isfinite, dv)
m.opt_state = Optimisers.init(m.optim, v)
else
v .-= dv
end

return v
end
89 changes: 89 additions & 0 deletions ext/SEMBlackBoxOptimExt/BlackBoxOptim.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
############################################################################################
### connect to BlackBoxOptim.jl as backend
############################################################################################

"""
"""
struct SemOptimizerBlackBoxOptim <: SemOptimizer{:BlackBoxOptim}
lower_bound::Float64 # default lower bound
variance_lower_bound::Float64 # default variance lower bound
lower_bounds::Union{Dict{Symbol, Float64}, Nothing}

upper_bound::Float64 # default upper bound
upper_bounds::Union{Dict{Symbol, Float64}, Nothing}
end

function SemOptimizerBlackBoxOptim(;
lower_bound::Float64 = -1000.0,
lower_bounds::Union{AbstractDict{Symbol, Float64}, Nothing} = nothing,
variance_lower_bound::Float64 = 0.001,
upper_bound::Float64 = 1000.0,
upper_bounds::Union{AbstractDict{Symbol, Float64}, Nothing} = nothing,
kwargs...,
)
if variance_lower_bound < 0.0
throw(ArgumentError("variance_lower_bound must be non-negative"))
end
return SemOptimizerBlackBoxOptim(
lower_bound,
variance_lower_bound,
lower_bounds,
upper_bound,
upper_bounds,
)
end

SEM.SemOptimizer{:BlackBoxOptim}(args...; kwargs...) =
SemOptimizerBlackBoxOptim(args...; kwargs...)

SEM.algorithm(optimizer::SemOptimizerBlackBoxOptim) = optimizer.algorithm
SEM.options(optimizer::SemOptimizerBlackBoxOptim) = optimizer.options

struct SemModelBlackBoxOptimProblem{M <: AbstractSem} <:
OptimizationProblem{ScalarFitnessScheme{true}}
model::M
fitness_scheme::ScalarFitnessScheme{true}
search_space::ContinuousRectSearchSpace
end

function BlackBoxOptim.search_space(model::AbstractSem)
optim = model.optimizer::SemOptimizerBlackBoxOptim
varparams = Set(SEM.variance_params(model.implied.ram_matrices))
return ContinuousRectSearchSpace(
[
begin
def = in(p, varparams) ? optim.variance_lower_bound : optim.lower_bound
isnothing(optim.lower_bounds) ? def : get(optim.lower_bounds, p, def)
end for p in SEM.params(model)
],
[
begin
def = optim.upper_bound
isnothing(optim.upper_bounds) ? def : get(optim.upper_bounds, p, def)
end for p in SEM.params(model)
],
)
end

function SemModelBlackBoxOptimProblem(
model::AbstractSem,
optimizer::SemOptimizerBlackBoxOptim,
)
SemModelBlackBoxOptimProblem(model, ScalarFitnessScheme{true}(), search_space(model))
end

BlackBoxOptim.fitness(params::AbstractVector, wrapper::SemModelBlackBoxOptimProblem) =
return SEM.evaluate!(0.0, nothing, nothing, wrapper.model, params)

# sem_fit method
function SEM.sem_fit(
optimizer::SemOptimizerBlackBoxOptim,
model::AbstractSem,
start_params::AbstractVector;
MaxSteps::Integer = 50000,
kwargs...,
)
problem = SemModelBlackBoxOptimProblem(model, optimizer)
res = bboptimize(problem; MaxSteps, kwargs...)
return SemFit(best_fitness(res), best_candidate(res), nothing, model, res)
end
196 changes: 196 additions & 0 deletions ext/SEMBlackBoxOptimExt/DiffEvoFactory.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
"""
Base class for factories of optimizers for a specific problem.
"""
abstract type OptimizerFactory{P <: OptimizationProblem} end

problem(factory::OptimizerFactory) = factory.problem

const OptController_DefaultParameters = ParamsDict(
:MaxTime => 60.0,
:MaxSteps => 10^8,
:TraceMode => :compact,
:TraceInterval => 5.0,
:RecoverResults => false,
:SaveTrace => false,
)

function generate_opt_controller(alg::Optimizer, optim_factory::OptimizerFactory, params)
return BlackBoxOptim.OptController(
alg,
problem(optim_factory),
BlackBoxOptim.chain(
BlackBoxOptim.DefaultParameters,
OptController_DefaultParameters,
params,
),
)
end

function check_population(
factory::OptimizerFactory,
popmatrix::BlackBoxOptim.PopulationMatrix,
)
ssp = factory |> problem |> search_space
for i in 1:popsize(popmatrix)
@assert popmatrix[:, i] ∈ ssp "Individual $i is out of space: $(popmatrix[:,i])" # fitness: $(fitness(population, i))"
end
end

initial_search_space(factory::OptimizerFactory, id::Int) = search_space(factory.problem)

function initial_population_matrix(factory::OptimizerFactory, id::Int)
#@info "Standard initial_population_matrix()"
ini_ss = initial_search_space(factory, id)
if !isempty(factory.initial_population)
numdims(factory.initial_population) == numdims(factory.problem) || throw(
DimensionMismatch(
"Dimensions of :Population ($(numdims(factory.initial_population))) " *
"are different from the problem dimensions ($(numdims(factory.problem)))",
),
)
res = factory.initial_population[
:,
StatsBase.sample(
1:popsize(factory.initial_population),
factory.population_size,
),
]
else
res = rand_individuals(ini_ss, factory.population_size, method = :latin_hypercube)
end
prj = RandomBound(ini_ss)
if size(res, 2) > 1
apply!(prj, view(res, :, 1), SEM.start_fabin3(factory.problem.model))
end
if size(res, 2) > 2
apply!(prj, view(res, :, 2), SEM.start_simple(factory.problem.model))
end
return res
end

# convert individuals in the archive into population matrix
population_matrix(archive::Any) = population_matrix!(
Matrix{Float64}(undef, length(BlackBoxOptim.params(first(archive))), length(archive)),
archive,
)

function population_matrix!(pop::AbstractMatrix{<:Real}, archive::Any)
npars = length(BlackBoxOptim.params(first(archive)))
size(pop, 1) == npars || throw(
DimensionMismatch(
"Matrix rows count ($(size(pop, 1))) doesn't match the number of problem dimensions ($(npars))",
),
)
@inbounds for (i, indi) in enumerate(archive)
(i <= size(pop, 2)) || break
pop[:, i] .= BlackBoxOptim.params(indi)
end
if size(pop, 2) > length(archive)
@warn "Matrix columns count ($(size(pop, 2))) is bigger than population size ($(length(archive))), last columns not set"
end
return pop
end

generate_embedder(factory::OptimizerFactory, id::Int, problem::OptimizationProblem) =
RandomBound(search_space(problem))

abstract type DiffEvoFactory{P <: OptimizationProblem} <: OptimizerFactory{P} end

generate_selector(
factory::DiffEvoFactory,
id::Int,
problem::OptimizationProblem,
population,
) = RadiusLimitedSelector(get(factory.params, :selector_radius, popsize(population) ÷ 5))

function generate_modifier(factory::DiffEvoFactory, id::Int, problem::OptimizationProblem)
ops = GeneticOperator[
MutationClock(UniformMutation(search_space(problem)), 1 / numdims(problem)),
BlackBoxOptim.AdaptiveDiffEvoRandBin1(
BlackBoxOptim.AdaptiveDiffEvoParameters(
factory.params[:fdistr],
factory.params[:crdistr],
),
),
SimplexCrossover{3}(1.05),
SimplexCrossover{2}(1.1),
#SimulatedBinaryCrossover(0.05, 16.0),
#SimulatedBinaryCrossover(0.05, 3.0),
#SimulatedBinaryCrossover(0.1, 5.0),
#SimulatedBinaryCrossover(0.2, 16.0),
UnimodalNormalDistributionCrossover{2}(
chain(BlackBoxOptim.UNDX_DefaultOptions, factory.params),
),
UnimodalNormalDistributionCrossover{3}(
chain(BlackBoxOptim.UNDX_DefaultOptions, factory.params),
),
ParentCentricCrossover{2}(chain(BlackBoxOptim.PCX_DefaultOptions, factory.params)),
ParentCentricCrossover{3}(chain(BlackBoxOptim.PCX_DefaultOptions, factory.params)),
]
if problem isa SemModelBlackBoxOptimProblem
push!(
ops,
AdamMutation(problem.model, chain(AdamMutation_DefaultOptions, factory.params)),
)
end
FAGeneticOperatorsMixture(ops)
end

function generate_optimizer(
factory::DiffEvoFactory,
id::Int,
problem::OptimizationProblem,
popmatrix,
)
population = FitPopulation(popmatrix, nafitness(fitness_scheme(problem)))
BlackBoxOptim.DiffEvoOpt(
"AdaptiveDE/rand/1/bin/gradient",
population,
generate_selector(factory, id, problem, population),
generate_modifier(factory, id, problem),
generate_embedder(factory, id, problem),
)
end

const Population_DefaultParameters = ParamsDict(
:Population => BlackBoxOptim.PopulationMatrix(undef, 0, 0),
:PopulationSize => 100,
)

const DE_DefaultParameters = chain(
ParamsDict(
:SelectorRadius => 0,
:fdistr =>
BlackBoxOptim.BimodalCauchy(0.65, 0.1, 1.0, 0.1, clampBelow0 = false),
:crdistr =>
BlackBoxOptim.BimodalCauchy(0.1, 0.1, 0.95, 0.1, clampBelow0 = false),
),
Population_DefaultParameters,
)

struct DefaultDiffEvoFactory{P <: OptimizationProblem} <: DiffEvoFactory{P}
problem::P
initial_population::BlackBoxOptim.PopulationMatrix
population_size::Int
params::ParamsDictChain
end

DefaultDiffEvoFactory(problem::OptimizationProblem; kwargs...) =
DefaultDiffEvoFactory(problem, BlackBoxOptim.kwargs2dict(kwargs))

function DefaultDiffEvoFactory(problem::OptimizationProblem, params::AbstractDict)
params = chain(DE_DefaultParameters, params)
DefaultDiffEvoFactory{typeof(problem)}(
problem,
params[:Population],
params[:PopulationSize],
params,
)
end

function BlackBoxOptim.bbsetup(factory::OptimizerFactory; kwargs...)
popmatrix = initial_population_matrix(factory, 1)
check_population(factory, popmatrix)
alg = generate_optimizer(factory, 1, problem(factory), popmatrix)
return generate_opt_controller(alg, factory, BlackBoxOptim.kwargs2dict(kwargs))
end
13 changes: 13 additions & 0 deletions ext/SEMBlackBoxOptimExt/SEMBlackBoxOptimExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module SEMBlackBoxOptimExt

using StructuralEquationModels, BlackBoxOptim, Optimisers

SEM = StructuralEquationModels

export SemOptimizerBlackBoxOptim

include("AdamMutation.jl")
include("DiffEvoFactory.jl")
include("SemOptimizerBlackBoxOptim.jl")

end
Loading
Loading