Skip to content

Conversation

@alyst
Copy link
Contributor

@alyst alyst commented Feb 4, 2025

These are rather trivial commits cherry-picked from #193 that don't change anything essential, but once they are in, it should be easier to manage the rest.

The changes are:

  • code formatter whitespace fixes for the .md files
  • move some using directives from the individual unit test .jl files to the parent .jl file
  • in RAMSymbolic rename xxx_function to xxx_eval! as that better follows Julia naming conventions and should make the code a bit more readable (and shorter)
  • cleanups to the fitmeasures that simplify some method signatures (remove unused type parameters).
    This is a bit less trivial set of changes than the rest, but in the end it does not really change how the functions are calculated.
    Except for switching p-values calculation to use ccdf(x) instead of 1 - cdf(x) -- this could potentially give more precise results for p-values close to 0.

@codecov
Copy link

codecov bot commented Feb 4, 2025

Codecov Report

❌ Patch coverage is 82.75862% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.24%. Comparing base (8b0f880) to head (acd1748).
⚠️ Report is 57 commits behind head on devel.

Files with missing lines Patch % Lines
src/optimizer/abstract.jl 10.00% 9 Missing ⚠️
src/additional_functions/helper.jl 0.00% 3 Missing ⚠️
ext/SEMNLOptExt/NLopt.jl 85.71% 2 Missing ⚠️
src/additional_functions/params_array.jl 33.33% 2 Missing ⚠️
src/frontend/fit/fitmeasures/chi2.jl 92.85% 2 Missing ⚠️
src/implied/RAM/symbolic.jl 93.10% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##            devel     #245      +/-   ##
==========================================
- Coverage   72.94%   72.24%   -0.71%     
==========================================
  Files          50       51       +1     
  Lines        2218     2212       -6     
==========================================
- Hits         1618     1598      -20     
- Misses        600      614      +14     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@alyst
Copy link
Contributor Author

alyst commented Mar 22, 2025

I've rebased the PR after the #253 got merged.

@Maximilian-Stefan-Ernst I have also noticed that you have implemented #246.
I am not sure I understand what was exactly the reason for these changes -- I guess it was related to running some examples/tests, but if that's the case we can fix it differently.
To me this change defeats the purpose of using extension mechanism for supporting different optimization engines as it moves the SemOptimizer subtypes back into the main package.
There is no strong reason to make engine-specific SemOptimizer subtypes public and recommend the user constructing them explicitly, especially since these constructors take all their input as keyword arguments.
Yes, before #246 the SemOptimizerEngine type was only available within the extension and was not exposed upon extension activation,
but SemOptimizer(engine = ...) => SemOptimizer{engine}(...) => SemOptimizerEngine(...) mechanism can handle SemOptimizerEngine construction just fine.
It is a standard pattern for implementing Julia extensions.
Also, I think for the user it is easier to memorize the SemOptimizer(engine = ...) construct, and it also makes it easier to switch the engines.

So I have added commits that revert #246 in this PR, but also updated the optimization documentation to be in sync with the actual opt. engine API:

  • SemOptimizationOptim(), SemOptimizationNLopt() etc in the docs are replaced with SemOptimizer(engine = <engine_name>, args...).
  • I have updated the fallback SemOptimizer(engine = ...) constructor to inform the user, which packages should be loaded for NLopt and ProximalAlgorithms, if that was one of the reasons for Fix extensions #246.
  • For NLopt I have replaced NLoptConstraint(f = func, tol = tol) with func => tol.
    That simplifies the syntax and avoids defining NLoptContraint type, which is only really used for SemOptimizerNLopt construction.
    I have also allowed specifying just the function without the tolerance -- in that case the tolerance will be taken from the constraint_tol parameter.
  • In the documentation I have changed the term "backend" into "engine" to match the SemOptimizer keyword, but if you have strong preference for "backend",
    I can revert it and rename the "engine" keyword into "backend".
  • I have also simplified the examples: since the optimizer is now separate from the model, we don't need to duplicate the model if we want to fit it with another optimizer.

Please let me know if you have any concerns regarding these changes, I will be happy to address them.

@alyst alyst closed this Apr 26, 2025
@alyst alyst reopened this Apr 26, 2025
@alyst
Copy link
Contributor Author

alyst commented Nov 24, 2025

@Maximilian-Stefan-Ernst @brandmaier @aaronpeikert In the recent months the merging of some of the changes from my staging branch (PR #193) into the main branch was dormant. But I would be still interested in it, if you think it will be beneficial for the SEM.jl. I mention it, because I see there are new issues/PRs, which could at least partially be addressed in PR #193. Thank you!

@alyst alyst closed this Jan 21, 2026
@alyst alyst reopened this Jan 21, 2026
@alyst
Copy link
Contributor Author

alyst commented Jan 21, 2026

The FormatCheck action updates were extracted to PR #296.
It probably has to be merged first.

@Maximilian-Stefan-Ernst
Copy link
Collaborator

Thanks a lot! I merged #296 - is this PR ready for review, or is there anything else you would like to add before?

@alyst alyst closed this Jan 22, 2026
@alyst alyst reopened this Jan 22, 2026
@alyst alyst closed this Jan 22, 2026
@alyst alyst reopened this Jan 22, 2026
@alyst
Copy link
Contributor Author

alyst commented Jan 22, 2026

@Maximilian-Stefan-Ernst Thank you! I've just rebased this branch to the latest devel, but somehow GitHub does not pick up the FormatCheck.yml changes (still uses v1).
Anyway, this PR should be ready for the review.

@Maximilian-Stefan-Ernst
Copy link
Collaborator

Alright, I'll review - I will also try to remind myself why I made the changes related to package extensions and write something about it.

W.r.t. the FormatCheck action - I believe changes to workflows only take effect once they are merged into main - I added the changes to main now, so it should pick up.

@Maximilian-Stefan-Ernst
Copy link
Collaborator

Maximilian-Stefan-Ernst commented Jan 26, 2026

Okay, w.r.t. the package extensions:

I believe the problem was that there was no way to access types defined in extensions at the time. This seems to be fixed now, but it is still cumbersome (https://discourse.julialang.org/t/having-trouble-using-types-defined-in-extension-modules/97260):

using StructuralEquationModels, NLopt

?SemOptimizerNLopt

Couldn't find SemOptimizerNLopt
Perhaps you meant SemOptimizerEmpty
  No documentation found.

  Binding SemOptimizerNLopt does not exist.

m = Base.get_extension(StructuralEquationModels, :SEMNLOptExt)
?m.SemOptimizerNLopt

So out of the box, this leads to some problems - without explicitely using get_extension, users can not access documentation on the extensions, and they can not define methods on types from extensions.

So I thought the main objective of having extensions is to avoid unnecessary dependencies - therefore I moved the types that are defined to the main package, so they are accessible, and their definition does not depend on the dependencies. Definitions of new methods, however, remained in the extension because they actually depend on additonal packages.

However, I really like your changes to the documentation, and the improved syntax for defining constraints, and I would definitely keep those. I am also happy with reverting the changes and moving the types back to the extension, the only thing we would need to fix is the accessibility of the documentation.

I also pushed these changes to another branch because I did not manage to manually trigger the documentation build workflow on this PR, but it is now here (https://github.com/StructuralEquationModels/StructuralEquationModels.jl/actions/runs/21357427498/job/61468060053) and it also seems to have the issue of accessing the documentation of types defined in extensions.

So the only thing I can think of atm would be to move the documentation of all extensions to SemOptimizer - but these docs are pretty long, and that might be quite overwhelming.

@alyst
Copy link
Contributor Author

alyst commented Jan 26, 2026

Okay, w.r.t. the package extensions:
I believe the problem was that there was no way to access types defined in extensions at the time. This seems to be fixed now, but it is still cumbersome (https://discourse.julialang.org/t/having-trouble-using-types-defined-in-extension-modules/97260):

using StructuralEquationModels, NLopt

?SemOptimizerNLopt

Couldn't find SemOptimizerNLopt

Yes, initially, I had the same confusion about Julia extensions.
It looks like they don't allow defining types -- either because of the implementation constraints or by design to limit their scope.
But in the case of the optimizer engines we actually don't need to expose the types like SemOptimizerNLOpt -- this is an implementation detail.
The user have to use the SemOptimizer(engine = ...) call, which also makes the API more flexible.

So out of the box, this leads to some problems - without explicitely using get_extension, users can not access documentation on the extensions, and they can not define methods on types from extensions.

I totally agree, the extensions need to provide the docstrings for the specific engine parameters, but currently in this PR it does not happen as it documents
these internal types/ctors. This definitely needs to be fixed. I have tried to fix it, but I am running into some DocStringExtensions.jl issues, so I have to research a bit more.

Which kind of use case you have in mind for allowing the users to define methods on the types of extensions?
Within the extensions it is possible to define the methods with engine-specific types as parameters.
It could be used for engine-specific convergence statistics etc.

However, I really like your changes to the documentation, and the improved syntax for defining constraints, and I would definitely keep those. I am also happy with reverting the changes and moving the types back to the extension, the only thing we would need to fix is the accessibility of the documentation.

+1 for fixing the docs.

So the only thing I can think of atm would be to move the documentation of all extensions to SemOptimizer - but these docs are pretty long, and that might be quite overwhelming.

Yes, I agree -- it might be too long and unspecific. I'll take a look how this is handled in other packages.
For example, the SemOptimizer{:engine}() docstrings could be very short, providing just a link to the proper docs.
It also solves the problem that the user has to discover the backend-specific types (SemOptimizerNLOpt) first to get their documentation.

To improve the discovery, I can also try adding the function optimizer_engines(), which would return the vector of symbols for the engines that are currently available.

@alyst alyst closed this Jan 26, 2026
@alyst alyst reopened this Jan 26, 2026
inequality_constraints = nothing,
constraint_tol::Number = 0.0,
kwargs...)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
!isa(inequality_constraints, NamedTuple) ||


# computes A*S*B -> C, where ind gives the entries of S that are 1
function sparse_outer_mul!(C, A, B, ind)
function sparse_outer_mul!(C, A, B, ind)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
function sparse_outer_mul!(C, A, B, ind)
function sparse_outer_mul!(C, A, B, ind)


# computes A*∇m, where ∇m ind gives the entries of ∇m that are 1
function sparse_outer_mul!(C, A, ind)
function sparse_outer_mul!(C, A, ind)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
function sparse_outer_mul!(C, A, ind)
function sparse_outer_mul!(C, A, ind)


# computes A*S*B -> C, where ind gives the entries of S that are 1
function sparse_outer_mul!(C, A, B::Vector, ind)
function sparse_outer_mul!(C, A, B::Vector, ind)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
function sparse_outer_mul!(C, A, B::Vector, ind)
function sparse_outer_mul!(C, A, B::Vector, ind)

ArgumentError(
"StructuralEquationModels does not support the `CoefTable` interface; see [`ParameterTable`](@ref) instead.",
),
)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
)
coeftable(model::AbstractSem; level::Real = 0.95) = throw(
ArgumentError(
"StructuralEquationModels does not support the `CoefTable` interface; see [`ParameterTable`](@ref) instead.",
),
)

StructuralEquationModels.minimum(fit_prox) - StructuralEquationModels.minimum(sem_fit),
StructuralEquationModels.minimum(fit_prox) -
StructuralEquationModels.minimum(sem_fit),
) < 1.0

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
) < 1.0
StructuralEquationModels.minimum(fit_prox) -
StructuralEquationModels.minimum(sem_fit),

Comment on lines +8 to 11
model = Sem(specification = partable, data = data)
model_fit = fit(model)

@testset "params" begin

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
model = Sem(specification = partable, data = data)
model_fit = fit(model)
@testset "params" begin
model = Sem(specification = partable, data = data)

end


ram_matrices =

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
ram_matrices =

"specification" => "SemSpecification",
"model" => "Sem model",
"StatsAPI" => "StatsAPI"
"StatsAPI" => "StatsAPI",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
"StatsAPI" => "StatsAPI",
"StatsAPI" => "StatsAPI",

@warn "Error initializing Test Env" exception=(e, catch_backtrace())
end
include("unit_tests.jl")
include("unit_tests.jl")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
include("unit_tests.jl")
include("unit_tests.jl")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[JuliaFormatter] reported by reviewdog 🐶

function sparse_materialize(::Type{T}, arr::ParamsMatrix, params::AbstractVector) where {T}
nparams(arr) == length(params) || throw(
DimensionMismatch(
"Number of values ($(length(params))) does not match the number of parameter ($(nparams(arr)))",
),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[JuliaFormatter] reported by reviewdog 🐶

for i in first_i:last_i
if isempty(param_occurences_range(arr, i))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[JuliaFormatter] reported by reviewdog 🐶

function params!(out::AbstractVector, partable::ParameterTable, col::Symbol = :estimate)
(length(out) == nparams(partable)) || throw(
DimensionMismatch(
"The length of parameter values vector ($(length(out))) does not match the number of parameters ($(nparams(partable)))",
),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[JuliaFormatter] reported by reviewdog 🐶

expected =
StructuralEquationModels.lavaan_params(partable_lav, partable, lav_col, lav_group)
@test !any(isnan, actual)
@test !any(isnan, expected)
if skip # workaround skip=false not supported in earlier versions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants