diff --git a/Project.toml b/Project.toml
index eabc5b36f..14e394cd3 100644
--- a/Project.toml
+++ b/Project.toml
@@ -27,22 +27,23 @@ Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7"
SymbolicUtils = "d1185830-fcd6-423d-90d6-eec64667417b"
[compat]
-julia = "1.9, 1.10, 1.11"
+julia = "1.10, 1.11, 1.12"
StenoGraphs = "0.5"
DataFrames = "1"
Distributions = "0.25"
FiniteDiff = "2"
-InteractiveUtils = "1.11.0"
LineSearches = "7"
-NLSolversBase = "7"
+NLSolversBase = "8"
NLopt = "0.6, 1"
-Optim = "1"
+Optim = "2"
PrettyTables = "3"
ProximalAlgorithms = "0.7"
StatsBase = "0.33, 0.34"
Symbolics = "4, 5, 6, 7"
SymbolicUtils = "1.4 - 1.5, 1.7, 2, 3, 4"
StatsAPI = "1"
+DelimitedFiles = "1"
+Statistics = "1"
[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
diff --git a/docs/make.jl b/docs/make.jl
index f3824dd79..2990f8dbb 100644
--- a/docs/make.jl
+++ b/docs/make.jl
@@ -52,11 +52,6 @@ makedocs(
"Starting values" => "performance/starting_values.md",
"Parametric Types" => "performance/parametric.md",
],
- "Internals and design" => [
- "Internals and design" => "internals/internals.md",
- "files" => "internals/files.md",
- "types" => "internals/types.md",
- ],
],
format = Documenter.HTML(
prettyurls = get(ENV, "CI", nothing) == "true",
diff --git a/docs/src/assets/concept.svg b/docs/src/assets/concept.svg
index f82e47943..fe6fd2081 100644
--- a/docs/src/assets/concept.svg
+++ b/docs/src/assets/concept.svg
@@ -1 +1,154 @@
-
\ No newline at end of file
+
+
+
+
diff --git a/docs/src/assets/concept_typed.svg b/docs/src/assets/concept_typed.svg
index e046819ae..4592fd87c 100644
--- a/docs/src/assets/concept_typed.svg
+++ b/docs/src/assets/concept_typed.svg
@@ -1 +1,152 @@
-
\ No newline at end of file
+
+
+
+
diff --git a/docs/src/developer/extending.md b/docs/src/developer/extending.md
index 5c3183da4..20f558def 100644
--- a/docs/src/developer/extending.md
+++ b/docs/src/developer/extending.md
@@ -1,10 +1,12 @@
# Extending the package
-As discussed in the section on [Model Construction](@ref), every Structural Equation Model (`Sem`) consists of three (four with the optimizer) parts:
+As discussed in [Our Concept of a Structural Equation Model](@ref), a `Sem` is built from one or more loss
+terms, and each SEM loss function bundles an *observed* and an *implied* part:

-On the following pages, we will explain how you can define your own custom parts and "plug them in". There are certain things you **have to do** to define custom parts and some things you **can do** to have a more pleasent experience. In general, these requirements fall into the categories
+On the following pages, we will explain how you can define your own custom parts (a loss function, an observed
+type, an implied type, or an optimizer) and "plug them in". There are certain things you **have to do** to define custom parts and some things you **can do** to have a more pleasent experience. In general, these requirements fall into the categories
- minimal (to use your custom part and fit a `Sem` with it)
- use the outer constructor to build a model in a more convenient way
- use additional functionality like standard errors, fit measures, etc.
\ No newline at end of file
diff --git a/docs/src/developer/implied.md b/docs/src/developer/implied.md
index 6321decbc..f695e3e82 100644
--- a/docs/src/developer/implied.md
+++ b/docs/src/developer/implied.md
@@ -43,8 +43,7 @@ We implement an `ImpliedEmpty` type in our package that does nothing but serving
### Types
############################################################################################
"""
-Empty placeholder for models that don't need an implied part.
-(For example, models that only regularize parameters.)
+Empty placeholder for loss functions that don't need an implied part.
# Constructor
@@ -55,7 +54,7 @@ Empty placeholder for models that don't need an implied part.
# Examples
A multigroup model with ridge regularization could be specified as a `Sem` with one
-model per group and an additional model with `ImpliedEmpty` and `SemRidge` for the regularization part.
+SEM term (`SemLoss`) per group and an additional `SemRidge` regularization term.
# Extended help
@@ -73,13 +72,13 @@ end
### Constructors
############################################################################################
-function ImpliedEmpty(
- spec::SemSpecification;
- hessianeval::HessianApprox = ExactHessian(),
+function ImpliedEmpty(;
+ specification,
+ meanstruct = NoMeanStruct(),
+ hessianeval = ExactHessian(),
kwargs...,
)
- ram_matrices = convert(RAMMatrices, spec)
- return ImpliedEmpty(hessianeval, MeanStruct(ram_matrices), ram_matrices)
+ return ImpliedEmpty(hessianeval, meanstruct, convert(RAMMatrices, specification))
end
############################################################################################
diff --git a/docs/src/developer/loss.md b/docs/src/developer/loss.md
index 8cdf2150f..e763364b2 100644
--- a/docs/src/developer/loss.md
+++ b/docs/src/developer/loss.md
@@ -71,7 +71,7 @@ partable = ParameterTable(
)
parameter_indices = getindex.([param_indices(partable)], [:a, :b, :c])
-myridge = Ridge(0.01, parameter_indices)
+myridge = MyRidge(0.01, parameter_indices)
model = SemFiniteDiff(
specification = partable,
@@ -82,16 +82,16 @@ model = SemFiniteDiff(
model_fit = fit(model)
```
-This is one way of specifying the model - we now have **one model** with **multiple loss functions**. Because we did not provide a gradient for `Ridge`, we have to specify a `SemFiniteDiff` model that computes numerical gradients with finite difference approximation.
+This is one way of specifying the model - we now have **one model** with **multiple loss functions**. Because we did not provide a gradient for `MyRidge`, we have to specify a `SemFiniteDiff` model that computes numerical gradients with finite difference approximation.
-Note that the last argument to the `objective!` method is the whole model. Therefore, we can access everything that is stored inside our model everytime we compute the objective value for our loss function. Since ridge regularization is a very easy case, we do not need to do this. But maximum likelihood estimation for example depends on both the observed and the model implied covariance matrix. See [Second example - maximum likelihood](@ref) for information on how to do that.
+Ridge regularization only depends on the parameters, so the `evaluate!` method above does not need anything else. Other loss functions, however, depend on the observed data and on what the model implies about it. Loss functions that compare the implied and the observed structure are subtypes of [`SemLoss`](@ref) and store their own `observed` and `implied` parts, which can be accessed inside `evaluate!` via `observed(loss)` and `implied(loss)`. See [Second example - maximum likelihood](@ref) for information on how to do that.
### Improve performance
By far the biggest improvements in performance will result from specifying analytical gradients. We can do this for our example:
```@example loss
-function evaluate!(objective, gradient, hessian::Nothing, ridge::Ridge, model::AbstractSem, par)
+function evaluate!(objective, gradient, hessian::Nothing, ridge::MyRidge, par)
# compute gradient
if !isnothing(gradient)
fill!(gradient, 0)
@@ -136,28 +136,34 @@ Additionally, you may provide analytic hessians by writing a respective method f
## Convenient
-To be able to build the loss term, it needs a constructor.
-Every `SemLoss` subtype should provide a constructor with 3 positional arguments:
- * `observed::SemObserved`: the observed part of the model
- * `implied::SemImplied`: the implied part of the model
- * `refloss::Union{MyLoss, Nothing} = nothing`: optional loss term of the same type
- to use as a reference for any loss-specific configuration.
+In the minimal example above we built `myridge` ourselves and passed the ready-made instance to
+the model via `loss = (SemML, myridge)`. Alternatively, you can let the outer [`Sem`](@ref)
+constructor build the loss term for you: pass the loss *type* instead of an instance and provide
+a keyword constructor.
-Any additional loss configuration details should be passed as optional keyword arguments.
-If both `refloss` and the keyword arguments are provided, the keyword arguments take
-precedence. This constructor is used internally by the functions like [`replace_observed`](@ref)
-to rebuild the loss term with new observed data while preserving the implied state.
+```julia
+MyRidge(; α_ridge, which_ridge, kwargs...) = MyRidge(α_ridge, which_ridge)
+```
+
+Any keyword arguments passed to `Sem(...)` are forwarded to this constructor (along with some that
+the model supplies automatically, such as `nparams`), so the loss can be configured directly from
+the model call:
```julia
-function MyLoss(
- observed::SemObserved, implied::SemImplied, refloss::Union{MyLoss, Nothing} = nothing;
- kwarg1 = ..., kwarg2 = ..., kwargs...
+model = SemFiniteDiff(
+ specification = partable,
+ data = example_data("political_democracy"),
+ loss = (SemML, MyRidge),
+ α_ridge = 0.01,
+ which_ridge = parameter_indices,
)
- ...
- return MyLoss(...) # internal MyLoss constructor
-end
```
+Note that, being a plain `AbstractLoss`, `MyRidge` neither stores nor receives an `observed` or
+`implied` part — it depends only on the parameters.
+SEM-specific loss functions are constructed differently; see
+[Second example - maximum likelihood](@ref).
+
## Additional functionality
### Access additional information
@@ -165,8 +171,8 @@ end
If you want to provide a way to query information about loss functions of your type, you can provide functions for that:
```julia
-hyperparameter(ridge::Ridge) = ridge.α
-regularization_indices(ridge::Ridge) = ridge.I
+hyperparameter(ridge::MyRidge) = ridge.α
+regularization_indices(ridge::MyRidge) = ridge.I
```
# Second example - maximum likelihood
@@ -178,7 +184,9 @@ To keep it simple, we only cover models without a meanstructure. The maximum lik
F_{ML} = \log \det \Sigma_i + \mathrm{tr}\left(\Sigma_{i}^{-1} \Sigma_o \right)
```
-where ``\Sigma_i`` is the model implied covariance matrix and ``\Sigma_o`` is the observed covariance matrix. We can query the model implied covariance matrix from the `implied` par of our model, and the observed covariance matrix from the `observed` path of our model.
+where ``\Sigma_i`` is the model implied covariance matrix and ``\Sigma_o`` is the observed covariance matrix. We can query the model implied covariance matrix from the `implied` part of our loss term, and the observed covariance matrix from the `observed` part of our loss term.
+
+Since this loss function compares the implied and the observed structure, it is a subtype of [`SemLoss`](@ref) rather than a plain `AbstractLoss`. Every `SemLoss` stores its own `observed` and `implied` parts, which can be accessed inside `evaluate!` via `observed(loss)` and `implied(loss)`.
To get information on what we can access from a certain `implied` or `observed` type, we can check it`s documentation an the pages [API - model parts](@ref) or via the help mode of the REPL:
@@ -190,20 +198,33 @@ help?> RAM
help?> SemObservedData
```
-We see that the model implied covariance matrix can be assessed as `Σ(implied)` and the observed covariance matrix as `obs_cov(observed)`.
+We see that the model implied covariance matrix can be assessed as `implied(loss).Σ` and the observed covariance matrix as `obs_cov(observed(loss))`.
+
+Unlike a plain `AbstractLoss`, a `SemLoss` subtype stores its `observed` and `implied` parts (in its first two fields), and the [`Sem`](@ref) constructor builds it for you. To support this, every `SemLoss` subtype should provide a constructor with three positional arguments:
+ * `observed::SemObserved`: the observed part of the loss term
+ * `implied::SemImplied`: the implied part of the loss term
+ * `refloss::Union{MaximumLikelihood, Nothing} = nothing`: an optional existing loss term of the
+ same type, used as a reference for any loss-specific configuration.
-With this information, we write can implement maximum likelihood optimization as
+Any additional configuration is passed as optional keyword arguments; if both `refloss` and keyword arguments are given, the keyword arguments take precedence. This constructor is also used by [`replace_observed`](@ref) to rebuild the loss term with new observed data while sharing the implied state. With this, we can implement maximum likelihood optimization as
```@example loss
-struct MaximumLikelihood <: SemLossFunction end
+struct MaximumLikelihood{O <: SemObserved, I <: SemImplied} <: SemLoss{O, I}
+ observed::O
+ implied::I
+end
+
+# constructor used by the `Sem` constructor to build the loss term
+MaximumLikelihood(observed::SemObserved, implied::SemImplied, refloss = nothing; kwargs...) =
+ MaximumLikelihood{typeof(observed), typeof(implied)}(observed, implied)
using LinearAlgebra
-import StructuralEquationModels: obs_cov, evaluate!
+import StructuralEquationModels: evaluate!
-function evaluate!(objective::Number, gradient::Nothing, hessian::Nothing, semml::MaximumLikelihood, model::AbstractSem, par)
+function evaluate!(objective::Number, gradient::Nothing, hessian::Nothing, semml::MaximumLikelihood, par)
# access the model implied and observed covariance matrices
- Σᵢ = implied(model).Σ
- Σₒ = obs_cov(observed(model))
+ Σᵢ = implied(semml).Σ
+ Σₒ = obs_cov(observed(semml))
# compute the objective
if isposdef(Symmetric(Σᵢ)) # is the model implied covariance matrix positive definite?
return logdet(Σᵢ) + tr(inv(Σᵢ)*Σₒ)
@@ -221,7 +242,7 @@ Let's specify and fit a model:
model_ml = SemFiniteDiff(
specification = partable,
data = example_data("political_democracy"),
- loss = MaximumLikelihood()
+ loss = MaximumLikelihood
)
model_fit = fit(model_ml)
diff --git a/docs/src/internals/files.md b/docs/src/internals/files.md
deleted file mode 100644
index 4c2338393..000000000
--- a/docs/src/internals/files.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# Files
-
-We briefly describe the file and folder structure of the package.
-
-## Source code
-
-Source code is in the `"src"` folder:
-
-`"src"`
-- `"StructuralEquationModels.jl"` defines the module and the exported objects
-- `"types.jl"` defines all abstract types and the basic type hierarchy
-- `"objective_gradient_hessian.jl"` contains methods for computing objective, gradient and hessian values for different model types as well as generic fallback methods
-- The folders `"observed"`, `"implied"`, and `"loss"` contain implementations of specific subtypes (for example, the `"loss"` folder contains a file `"ML.jl"` that implements the `SemML` loss function).
-- `"optimizer"` contains connections to different optimization backends (aka methods for `fit`)
- - `"optim.jl"`: connection to the `Optim.jl` package
-- `"frontend"` contains user-facing functions
- - `"specification"` contains functionality for model specification
- - `"fit"` contains functionality for model assessment, like fit measures and standard errors
-- `"additional_functions"` contains helper functions for simulations, loading artifacts (example data) and various other things
-
-Code for the package extentions can be found in the `"ext"` folder:
-- `"SEMNLOptExt"` for connection to `NLopt.jl`.
-- `"SEMProximalOptExt"` for connection to `ProximalAlgorithms.jl`.
-
-## Tests and Documentation
-
-Tests are in the `"test"` folder, documentation in the `"docs"` folder.
\ No newline at end of file
diff --git a/docs/src/internals/internals.md b/docs/src/internals/internals.md
deleted file mode 100644
index 18f82dbca..000000000
--- a/docs/src/internals/internals.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Internals and Design
-
-On the following pages, we document some technical information about the package. Those informations are no prerequisite for extending the package (as decribed in the developer documentation)!, but they may be useful.
\ No newline at end of file
diff --git a/docs/src/internals/types.md b/docs/src/internals/types.md
deleted file mode 100644
index 4b4cd4faa..000000000
--- a/docs/src/internals/types.md
+++ /dev/null
@@ -1,17 +0,0 @@
-# Type hierarchy
-
-The type hierarchy is implemented in `"src/types.jl"`.
-
-[`AbstractLoss`](@ref): is the base abstract type for all loss functions:
-- `SemLoss{O <: SemObserved, I <: SemImplied}`: is the subtype of `AbstractLoss`, which is the
- base for all SEM-specific loss functions ([`SemML`](@ref), [`SemWLS`](@ref) etc) that
- evaluate how closely the implied covariation structure (represented by the object of type `I`)
- matches the observed one (contained in the object of type `O`);
-- regularizing terms (e.g. [`SemRidge`](@ref)) are implemented as subtypes of `AbstractLoss`.
-
-[`AbstractSem`](@ref) is the base abstract type for all SEM models. It has two concrete subtypes:
-- `Sem{L <: Tuple} <: AbstractSem`: the main SEM model type that implements a list of weighted
-loss terms (using [`LossTerm`](@ref) wrapper around `AbstractLoss`) and allows modeling both single
-and multi-group SEMs and combining them with regularization terms.
-- `SemFiniteDiff{S <: AbstractSem} <: AbstractSem`: a wrapper around any `AbstractSem` that
- substitutes dedicated gradient/hessian evaluation with finite difference approximation.
diff --git a/docs/src/performance/parametric.md b/docs/src/performance/parametric.md
index 9c6be382b..ce1781ab6 100644
--- a/docs/src/performance/parametric.md
+++ b/docs/src/performance/parametric.md
@@ -25,7 +25,7 @@ giving each field a type and adding them as parameters to our type declaration.
Recall our example from [Custom loss functions](@ref):
```julia
-struct Ridge <: SemLossFunction
+struct Ridge <: AbstractLoss
α
I
end
@@ -34,7 +34,7 @@ end
We could also declare it as a parametric type:
```julia
-struct ParametricRidge{X, Y} <: SemLossFunction
+struct ParametricRidge{X, Y} <: AbstractLoss
α::X
I::Y
end
diff --git a/docs/src/tutorials/collection/collection.md b/docs/src/tutorials/collection/collection.md
index 2a8ea92c2..423ac5bc7 100644
--- a/docs/src/tutorials/collection/collection.md
+++ b/docs/src/tutorials/collection/collection.md
@@ -3,7 +3,7 @@
With *StructuralEquationModels.jl*, you can fit weighted sums of structural equation models.
The most common use case for this are [Multigroup models](@ref).
Another use case may be optimizing the sum of loss functions for some of which you do know the analytic gradient, but not for others.
-In this case, [`FiniteDiffWrapper`](@ref) can generate a wrapper around the specific `SemLoss` term. The wrapper loss term will
+In this case, [`FiniteDiffWrapper`](@ref StructuralEquationModels.FiniteDiffWrapper) can generate a wrapper around the specific `SemLoss` term. The wrapper loss term will
only use the objective of the original term to calculate its gradient using finite difference approximation.
```julia
@@ -26,16 +26,22 @@ It is also possible to use finite difference for the entire `Sem` model:
model_findiff2 = FiniteDiffWrapper(model)
```
-The weighting scheme of the SEM loss terms is specified using `default_set_weights` argument of the `Sem` constructor.
-The `:nsamples` scheme (the default) weights SEM terms by ``N_{term}/N_{total}``, i.e. each term is weighted by the number
-of observations in its data (which matches the formula for multigroup models).
-The weights for the loss terms (both SEM and regularization) can be explicitly specified the pair syntax `loss => weight`:
+The weighting scheme of the SEM loss terms is specified using the `default_sem_weights` argument of the `Sem` constructor.
+The available schemes are:
+- `:nsamples_corrected` (the default): like `:nsamples`, but applies a loss-type-specific correction
+ to the sample counts (e.g. ``N_{term} - 1`` for maximum likelihood and weighted least squares). For FIML the correction is zero, so it coincides with `:nsamples`,
+- `:nsamples`: weights each SEM term by ``N_{term}/N_{total}``, i.e. by the (uncorrected) number of
+ observations in its data,
+- `:uniform`: weights each of the ``k`` SEM terms by ``1/k``,
+- `:one`: leaves all SEM terms unweighted.
+
+The weights for the loss terms (both SEM and regularization) can also be explicitly specified using the pair syntax `loss => weight`:
```julia
model_weighted = Sem(loss_1 => 0.5, loss_2 => 1.0)
```
-`Sem` support assigning unique identifier to each loss term, which is essential for complex multi-term model.
+`Sem` supports assigning a unique identifier to each loss term, which is useful for complex multi-term models.
The syntax is `id => loss`, or `id => loss => weight`:
```julia
@@ -47,6 +53,8 @@ model2_weighted = Sem(:main => loss_1 => 0.5, :alt => loss_2 => 1.0)
```@docs
Sem
-LossTerm
-FiniteDiffWrapper
+SemFiniteDiff
+AbstractSem
+StructuralEquationModels.LossTerm
+StructuralEquationModels.FiniteDiffWrapper
```
\ No newline at end of file
diff --git a/docs/src/tutorials/collection/multigroup.md b/docs/src/tutorials/collection/multigroup.md
index 04f1893d7..ccd3bdd65 100644
--- a/docs/src/tutorials/collection/multigroup.md
+++ b/docs/src/tutorials/collection/multigroup.md
@@ -52,7 +52,7 @@ graph = @StenoGraph begin
end
```
-You can pass multiple arguments to `fix()` and `label()` for each group. Parameters with the same label (within and across groups) are constrained to be equal. To fix a parameter in one group, but estimate it freely in the other, you may write `fix(NaN, 4.3)`.
+You can pass multiple arguments to `fixed()` and `label()` for each group. Parameters with the same label (within and across groups) are constrained to be equal. To fix a parameter in one group, but estimate it freely in the other, you may write `fixed(NaN, 4.3)`.
You can then use the resulting graph to specify an `EnsembleParameterTable`
@@ -72,8 +72,7 @@ The parameter table can be used to create a multigroup `Sem` model:
model_ml_multigroup = Sem(
specification = partable,
data = dat,
- semterm_column = :school,
- groups = groups)
+ semterm_column = :school)
```
!!! note "A different way to specify"
diff --git a/docs/src/tutorials/concept.md b/docs/src/tutorials/concept.md
index 49f0d404f..39f189d2e 100644
--- a/docs/src/tutorials/concept.md
+++ b/docs/src/tutorials/concept.md
@@ -1,27 +1,48 @@
# Our Concept of a Structural Equation Model
-In our package, every Structural Equation Model (`Sem`) consists of three parts (four, if you count the optimizer):
+In our package, a structural equation model (a [`Sem`](@ref)) is built from one or more **loss terms**.
+Fitting the model means finding the parameters that minimize the (weighted) sum of all of its loss terms.
+This simple idea is remarkably general: within the same structure it covers a single SEM fit by maximum
+likelihood, a regularized SEM (e.g. maximum likelihood plus a ridge penalty), and multigroup models
+(one SEM term per group).

-Those parts are interchangable building blocks (like 'Legos'), i.e. there are different pieces available you can choose as the `observed` slot of the model, and stick them together with other pieces that can serve as the `implied` part.
+A loss term is anything of type [`AbstractLoss`](@ref) — a function that maps the model parameters to a
+number that should be minimized. There are two kinds of loss terms:
-The `observed` part is for observed data, the `implied` part is what the model implies about your data (e.g. the model implied covariance matrix), and the loss part compares the observed data and implied properties (e.g. weighted least squares difference between the observed and implied covariance matrix).
-The optimizer part is not part of the model itself, but it is needed to fit the model as it connects to the optimization backend (e.g. the type of optimization algorithm used).
+- **SEM loss functions** ([`SemLoss`](@ref)), such as [`SemML`](@ref), [`SemWLS`](@ref) and [`SemFIML`](@ref),
+ measure how well the model explains the data. To do so, each `SemLoss` *bundles its own observed part
+ (the data) and implied part (what the model implies about the data)*. They are the heart of a SEM.
+- **Other loss functions**, such as the regularization terms [`SemRidge`](@ref) and [`SemConstant`](@ref),
+ depend only on the parameters and therefore need neither an observed nor an implied part.
-For example, to build a model for maximum likelihood estimation with the NLopt optimization suite as a backend you would choose `SemML` as a loss function and `SemOptimizerNLopt` as the optimizer.
+Because a model is just a (weighted) sum of loss terms, you can freely combine them.
+For example, ridge-regularized full information maximum likelihood estimation is a model with two loss terms,
+a [`SemFIML`](@ref) term and a [`SemRidge`](@ref) term. A two-group model is a model with two [`SemML`](@ref)
+terms, one per group, weighted by the respective sample sizes.
-As you can see, a model can have as many loss functions as you want it to have. We always optimize over their (weighted) sum. So to build a model for ridge regularized full information maximum likelihood estimation, you would choose two loss functions, `SemFIML` and `SemRidge`.
+All models are subtypes of [`AbstractSem`](@ref). The default [`Sem`](@ref) computes the weighted sum of its
+loss terms together with their (analytic) gradients. [`SemFiniteDiff`](@ref) is an alternative that
+approximates the gradient with finite differences, which is useful for loss functions that do not provide an
+analytic gradient.
-In julia, everything has a type. To make more precise which objects can be used as the different building blocks, we require them to have a certain type:
+## The parts of a SEM loss
+
+Each SEM loss function ([`SemLoss`](@ref)) is itself composed of interchangeable building blocks (like 'Legos'):
+an *observed* part and an *implied* part. To make precise which objects can play each role, we require them to
+have a certain type:

-So everything that can be used as the 'observed' part has to be of type `SemObserved`.
+So everything that can serve as the *observed* part has to be of type [`SemObserved`](@ref), everything that can
+serve as the *implied* part has to be of type [`SemImplied`](@ref), and the loss function that combines them is a
+[`SemLoss`](@ref). To fit the model, you additionally choose a [`SemOptimizer`](@ref); it connects to the
+numerical optimization backend but is not itself part of the model.
Here is an overview on the available building blocks:
-|[`SemObserved`](@ref) | [`SemImplied`](@ref) | [`SemLossFunction`](@ref) | [`SemOptimizer`](@ref) |
+|[`SemObserved`](@ref) | [`SemImplied`](@ref) | [`AbstractLoss`](@ref) | [`SemOptimizer`](@ref) |
|---------------------------------|-----------------------|---------------------------|----------------------------|
| [`SemObservedData`](@ref) | [`RAM`](@ref) | [`SemML`](@ref) | [:Optim](@ref StructuralEquationModels.SemOptimizerOptim) |
| [`SemObservedCovariance`](@ref) | [`RAMSymbolic`](@ref) | [`SemWLS`](@ref) | [:NLopt](@ref SEMNLOptExt.SemOptimizerNLopt) |
@@ -29,28 +50,44 @@ Here is an overview on the available building blocks:
| | | [`SemRidge`](@ref) | |
| | | [`SemConstant`](@ref) | |
-The rest of this page explains the building blocks for each part. First, we explain every part and give an overview on the different options that are available. After that, the [API - model parts](@ref) section serves as a reference for detailed explanations about the different options.
-(How to stick them together to a final model is explained in the section on [Model Construction](@ref).)
+The rest of this page explains each building block and the available options. After that, the
+[API - model parts](@ref) section serves as a reference for detailed explanations.
+(How to stick the building blocks together into a final model is explained in the section on
+[Model Construction](@ref).)
## The observed part aka [`SemObserved`](@ref)
-The *observed* part contains all necessary information about the observed data. Currently, we have three options: [`SemObservedData`](@ref) for fully observed datasets, [`SemObservedCovariance`](@ref) for observed covariances (and means) and [`SemObservedMissing`](@ref) for data that contains missing values.
+The *observed* part contains all necessary information about the observed data, and pre-computes the statistics
+a loss function needs from it — for example the observed covariance matrix, or the different patterns of
+missingness used for full information maximum likelihood (FIML) estimation.
+Currently, we have three options: [`SemObservedData`](@ref) for fully observed datasets,
+[`SemObservedCovariance`](@ref) for observed covariances (and means) and [`SemObservedMissing`](@ref) for data
+that contains missing values.
## The implied part aka [`SemImplied`](@ref)
-The *implied* part is what your model implies about the data, for example, the model-implied covariance matrix.
-There are two options at the moment: [`RAM`](@ref), which uses the reticular action model to compute the model implied covariance matrix, and [`RAMSymbolic`](@ref) which does the same but symbolically pre-computes part of the model, which increases subsequent performance in model fitting (see [Symbolic precomputation](@ref)). There is also a third option, [`ImpliedEmpty`](@ref) that can serve as a 'placeholder' for models that do not need an implied part.
-
-## The loss part aka `SemLoss`
-The loss part specifies the objective that is optimized to find the parameter estimates.
-If it contains more then one loss function (aka [`SemLossFunction`](@ref))), we find the parameters by minimizing the sum of loss functions (for example in maximum likelihood estimation + ridge regularization).
+The *implied* part defines how the model-implied statistics (for example, the model-implied covariance matrix
+and mean vector) are computed from the parameters.
+There are two options at the moment: [`RAM`](@ref), which uses the reticular action model to compute the model
+implied covariance matrix, and [`RAMSymbolic`](@ref) which does the same but symbolically pre-computes part of
+the model, which increases subsequent performance in model fitting (see [Symbolic precomputation](@ref)). There
+is also a third option, [`ImpliedEmpty`](@ref) that can serve as a 'placeholder' for loss terms that do not need
+an implied part.
+
+## The loss functions aka [`AbstractLoss`](@ref)
+The loss terms specify the objective that is minimized to find the parameter estimates; a model minimizes the
+(weighted) sum of all its loss terms.
+SEM loss functions ([`SemLoss`](@ref)) compare what the model implies to the observed data, while regularization
+terms depend only on the parameters.
Available loss functions are
- [`SemML`](@ref): maximum likelihood estimation
- [`SemWLS`](@ref): weighted least squares estimation
- [`SemFIML`](@ref): full-information maximum likelihood estimation
- [`SemRidge`](@ref): ridge regularization
+- [`SemConstant`](@ref): adds a constant to the objective
-## The optimizer part aka `SemOptimizer`
-The optimizer part of a model connects to the numerical optimization backend used to fit the model.
+## The optimizer aka [`SemOptimizer`](@ref)
+The optimizer connects to the numerical optimization backend used to fit the model. It is not part of the model
+itself, but it is chosen when fitting (see [Model fitting](@ref)).
It can be used to control options like the optimization algorithm, linesearch, stopping criteria, etc.
There are currently three available engines (i.e., backends used to carry out the numerical optimization), [`:Optim`](@ref StructuralEquationModels.SemOptimizerOptim) connecting to the [Optim.jl](https://github.com/JuliaNLSolvers/Optim.jl) backend, [`:NLopt`](@ref SEMNLOptExt.SemOptimizerNLopt) connecting to the [NLopt.jl](https://github.com/JuliaOpt/NLopt.jl) backend and [`:Proximal`](@ref SEMProximalOptExt.SemOptimizerProximal) connecting to [ProximalAlgorithms.jl](https://github.com/JuliaFirstOrder/ProximalAlgorithms.jl).
For more information about the available options see also the tutorials about [Using Optim.jl](@ref) and [Using NLopt.jl](@ref), as well as [Constrained optimization](@ref) and [Regularization](@ref) .
@@ -75,6 +112,7 @@ SemObservedMissing
samples
observed_vars
SemSpecification
+em_mvn
```
## implied
@@ -89,8 +127,8 @@ ImpliedEmpty
## loss functions
```@docs
+AbstractLoss
SemLoss
-SemLossFunction
SemML
SemFIML
SemWLS
diff --git a/docs/src/tutorials/constraints/constraints.md b/docs/src/tutorials/constraints/constraints.md
index 32bb6a529..e50fd2ef5 100644
--- a/docs/src/tutorials/constraints/constraints.md
+++ b/docs/src/tutorials/constraints/constraints.md
@@ -76,7 +76,7 @@ parind[:y3y7] # 29
```
The bound constraint is easy to specify: just give a vector of upper or lower bounds for each parameter.
-In our example, only the parameter labeled `:λₗ` has an upper bound, and the number of total parameters is `n_par(model) = 31`, so
+In our example, only the parameter labeled `:λₗ` has an upper bound, and the number of total parameters is `nparams(model) = 31`, so
```@example constraints
upper_bounds = fill(Inf, 31)
diff --git a/docs/src/tutorials/construction/build_by_parts.md b/docs/src/tutorials/construction/build_by_parts.md
index 52e12f30b..afa51eeb0 100644
--- a/docs/src/tutorials/construction/build_by_parts.md
+++ b/docs/src/tutorials/construction/build_by_parts.md
@@ -51,19 +51,17 @@ Now, we construct the different parts:
observed = SemObservedData(specification = partable, data = data)
# implied ------------------------------------------------------------------------------
-implied_ram = RAM(specification = partable)
+implied_ram = RAM(partable)
# loss ---------------------------------------------------------------------------------
-ml = SemML(observed = observed)
-
-loss_ml = SemLoss(ml)
+ml = SemML(observed, implied_ram)
# optimizer ----------------------------------------------------------------------------
optimizer = SemOptimizer()
# model --------------------------------------------------------------------------------
-model_ml = Sem(observed, implied_ram, loss_ml)
+model_ml = Sem(ml)
fit(optimizer, model_ml)
```
\ No newline at end of file
diff --git a/docs/src/tutorials/construction/outer_constructor.md b/docs/src/tutorials/construction/outer_constructor.md
index e0c69ef3c..cd982ecca 100644
--- a/docs/src/tutorials/construction/outer_constructor.md
+++ b/docs/src/tutorials/construction/outer_constructor.md
@@ -12,13 +12,12 @@ model = Sem(
Structural Equation Model
- Loss Functions
- SemML
-- Fields
- observed: SemObservedData
- implied: RAM
+ > SemML
+ - observed: SemObservedData
+ - implied: RAM
```
-The output of this call tells you exactly what model you just constructed (i.e. what the loss functions, observed, implied and optimizer parts are).
+The output of this call tells you exactly what model you just constructed (i.e. what the loss functions and their observed and implied parts are).
As you can see, by default, we use maximum likelihood estimation abd the RAM implied type.
To choose something different, you can provide it as a keyword argument:
@@ -40,11 +39,12 @@ model = Sem(
specification = partable,
data = data,
implied = RAMSymbolic,
- loss = SemWLS,
- optimizer = SemOptimizer
+ loss = SemWLS
)
```
+The optimizer is not part of the model itself; it is chosen when fitting (see [Model fitting](@ref)).
+
In the section on [Our Concept of a Structural Equation Model](@ref), we go over the different options you have for each part of the model, and in [API - model parts](@ref) we explain each option in detail.
Let's make another example: to use full information maximum likelihood information (FIML), we use
diff --git a/docs/src/tutorials/fitting/fitting.md b/docs/src/tutorials/fitting/fitting.md
index 1af03ce8e..c14d9b993 100644
--- a/docs/src/tutorials/fitting/fitting.md
+++ b/docs/src/tutorials/fitting/fitting.md
@@ -13,10 +13,9 @@ Fitted Structural Equation Model
Structural Equation Model
- Loss Functions
- SemML
-- Fields
- observed: SemObservedData
- implied: RAM
+ > SemML
+ - observed: SemObservedData
+ - implied: RAM
------------- Optimization result -------------
diff --git a/docs/src/tutorials/inspection/inspection.md b/docs/src/tutorials/inspection/inspection.md
index ff572eb58..cc4d92e3d 100644
--- a/docs/src/tutorials/inspection/inspection.md
+++ b/docs/src/tutorials/inspection/inspection.md
@@ -81,7 +81,7 @@ update_estimate!(partable, model_fit)
details(partable)
```
-We can also update the `ParameterTable` object with other information via [`update_partable!`](@ref). For example, if we want to compare hessian-based and bootstrap-based standard errors, we may write
+We can also update the `ParameterTable` object with other information via [`update_partable!`](@ref). For example, we can obtain standard errors from the inverse Hessian with [`se_hessian`](@ref) or by bootstrapping with [`se_bootstrap`](@ref), and add both to the table to compare them:
```@example colored; ansicolor = true
se_bs = se_bootstrap(model_fit; n_boot = 20)
@@ -93,6 +93,22 @@ update_partable!(partable, :se_bootstrap, model_fit, se_bs)
details(partable)
```
+From a vector of standard errors we can also compute *p*-values and confidence intervals for the parameter estimates.
+[`z_test!`](@ref) adds the two-sided *p*-value of the test that each parameter is zero (using `z = estimate / se`), and [`normal_CI!`](@ref) adds the lower and upper bounds of a normal-theory confidence interval (95% by default).
+Both update the `ParameterTable` in place:
+
+```@example colored; ansicolor = true
+z_test!(partable, model_fit, se_he)
+normal_CI!(partable, model_fit, se_he)
+
+details(partable; show_columns = [:to, :estimate, :p_value, :ci_lower, :ci_upper])
+```
+
+The non-mutating variants [`z_test`](@ref) and [`normal_CI`](@ref) return the values instead of writing them to the table.
+
+Beyond standard errors, [`bootstrap`](@ref) draws bootstrap samples of an arbitrary statistic of a fitted model (not only the parameter estimates), while [`se_bootstrap`](@ref) is a convenience wrapper returning bootstrapped standard errors.
+Both support parallel resampling across the available Julia threads via the `parallel = true` keyword.
+
## Export results
You may convert a `ParameterTable` to a `DataFrame` and use the [`DataFrames`](https://github.com/JuliaData/DataFrames.jl) package for further analysis (or to save it to your hard drive).
@@ -100,7 +116,7 @@ You may convert a `ParameterTable` to a `DataFrame` and use the [`DataFrames`](h
```@example colored; ansicolor = true
using DataFrames
-parameters_df = DataFrame(partable)
+parameters_df = DataFrame(partable);
```
# API - model inspection
@@ -135,4 +151,18 @@ dof
minus2ll
p_value
RMSEA
+CFI
+```
+
+## Standard errors and inference
+
+```@docs
+se_hessian
+se_bootstrap
+bootstrap
+StructuralEquationModels.BootstrapResult
+z_test
+z_test!
+normal_CI
+normal_CI!
```
diff --git a/docs/src/tutorials/meanstructure.md b/docs/src/tutorials/meanstructure.md
index 4e6d2a36a..1d8539355 100644
--- a/docs/src/tutorials/meanstructure.md
+++ b/docs/src/tutorials/meanstructure.md
@@ -106,11 +106,11 @@ For our example,
```@example meanstructure
observed = SemObservedData(specification = partable, data = data, meanstructure = true)
-implied_ram = RAM(specification = partable, meanstructure = true)
+implied_ram = RAM(partable, meanstructure = true)
-ml = SemML(observed = observed, meanstructure = true)
+ml = SemML(observed, implied_ram)
-model = Sem(observed, implied_ram, SemLoss(ml))
+model = Sem(ml)
fit(model)
```
\ No newline at end of file
diff --git a/docs/src/tutorials/specification/graph_interface.md b/docs/src/tutorials/specification/graph_interface.md
index 62eeef00b..fe03cf304 100644
--- a/docs/src/tutorials/specification/graph_interface.md
+++ b/docs/src/tutorials/specification/graph_interface.md
@@ -105,4 +105,4 @@ end
The syntax to specify graphs (`@StenoGraph`) may seem a bit strange if you are not familiar with the julia language. It is called a **macro**, but explaining this concept in detail is beyond this documentation (and not necessary to understand to specify models). However, if you want to know more about it, you may have a look at the respective part of the [manual](https://docs.julialang.org/en/v1/manual/metaprogramming/#man-macros).
### The StenoGraphs Package
-Behind the scenes, we are using the [StenoGraphs](https://github.com/aaronpeikert/StenoGraphs.jl) package to specify our graphs. It makes a domain specific language available that allows you to specify graphs with arbitrary information attached to its edges and nodes (for structural equation models, this may be the name or the value of a parameter). Is also allows you to specify your own types to "attach" to the graph, called a `Modifier`. So if you contemplate about writing your own modifier (e.g., to mark a variable as ordinal, an effect as quadratic, ...), please refer to the `StenoGraphs` [documentation](https://aaronpeikert.github.io/StenoGraphs.jl/dev/).
\ No newline at end of file
+Behind the scenes, we are using the [StenoGraphs](https://github.com/aaronpeikert/StenoGraphs.jl) package to specify our graphs. It makes a domain specific language available that allows you to specify graphs with arbitrary information attached to its edges and nodes (for structural equation models, this may be the name or the value of a parameter). Is also allows you to specify your own types to "attach" to the graph, called a `Modifier`. So if you contemplate about writing your own modifier, please refer to the `StenoGraphs` [documentation](https://aaronpeikert.github.io/StenoGraphs.jl/dev/).
\ No newline at end of file
diff --git a/src/frontend/finite_diff.jl b/src/frontend/finite_diff.jl
index 0ecd4865a..ac8fbf627 100644
--- a/src/frontend/finite_diff.jl
+++ b/src/frontend/finite_diff.jl
@@ -24,6 +24,19 @@ replace_observed(
kwargs...,
) = SemLossFiniteDiff(replace_observed(_unwrap(wrapper), data; kwargs...))
+"""
+ FiniteDiffWrapper(model::AbstractSem)
+ FiniteDiffWrapper(loss::AbstractLoss)
+
+Wrap a SEM `model` or an individual `loss` term so that its gradient and Hessian
+are approximated with finite differences of the objective (using the *FiniteDiff.jl*
+package) instead of dedicated analytic evaluation.
+
+Wrapping the whole `model` returns a [`SemFiniteDiff`](@ref). Wrapping a single loss
+term returns a loss wrapper that only uses the objective of the original term to
+compute its gradient/Hessian, which is useful in [Collections](@ref) where analytic
+gradients are available for some terms but not for others.
+"""
FiniteDiffWrapper(model::AbstractSem) = SemFiniteDiff(model)
FiniteDiffWrapper(loss::AbstractLoss) = LossFiniteDiff(loss)
FiniteDiffWrapper(loss::SemLoss) = SemLossFiniteDiff(loss)
diff --git a/src/frontend/fit/fitmeasures/chi2.jl b/src/frontend/fit/fitmeasures/chi2.jl
index c56b9a2a0..444d9678b 100644
--- a/src/frontend/fit/fitmeasures/chi2.jl
+++ b/src/frontend/fit/fitmeasures/chi2.jl
@@ -21,7 +21,9 @@ function χ²(fit::SemFit, model::AbstractSem)
end
# bollen, p. 115, only correct for GLS weight matrix
-χ²(::Type{<:SemWLS}, fit::SemFit, model::AbstractSem) = (nsamples(model) - 1) * fit.minimum
+# N - G (= Σ(Nᵍ-1)) multiplier; reduces to N-1 for a single group
+χ²(::Type{<:SemWLS}, fit::SemFit, model::AbstractSem) =
+ (nsamples(model) - nsem_terms(model)) * fit.minimum
function χ²(::Type{<:SemML}, fit::SemFit, model::AbstractSem)
G = sum(loss_terms(model)) do term
@@ -32,7 +34,8 @@ function χ²(::Type{<:SemML}, fit::SemFit, model::AbstractSem)
return 0.0
end
end
- return (nsamples(model) - 1) * (fit.minimum - G)
+ # N - G (= Σ(Nᵍ-1)) multiplier; reduces to N-1 for a single group
+ return (nsamples(model) - nsem_terms(model)) * (fit.minimum - G)
end
function χ²(::Type{<:SemFIML}, fit::SemFit, model::AbstractSem)
diff --git a/src/frontend/fit/standard_errors/confidence_intervals.jl b/src/frontend/fit/standard_errors/confidence_intervals.jl
index 20bf58a73..59c6bb7dd 100644
--- a/src/frontend/fit/standard_errors/confidence_intervals.jl
+++ b/src/frontend/fit/standard_errors/confidence_intervals.jl
@@ -10,7 +10,7 @@ Return normal-theory confidence intervals for all model parameters.
- `fitted`: a fitted SEM.
- `se`: standard errors for each parameter, e.g. from [`se_hessian`](@ref) or
[`se_bootstrap`](@ref).
-- `partable`: a [`ParameterTable`](@ref) to write confidence intervals to.
+- `partable`: a `ParameterTable` to write confidence intervals to.
- `α`: significance level. Defaults to `0.05` (95% intervals).
- `name_lower`: column name for the lower bound in `partable`. Defaults to `:ci_lower`.
- `name_upper`: column name for the upper bound in `partable`. Defaults to `:ci_upper`.
diff --git a/src/frontend/fit/standard_errors/z_test.jl b/src/frontend/fit/standard_errors/z_test.jl
index 27bebf147..d912fd867 100644
--- a/src/frontend/fit/standard_errors/z_test.jl
+++ b/src/frontend/fit/standard_errors/z_test.jl
@@ -13,7 +13,7 @@ Tests the null hypothesis that each parameter is zero using the test statistic
- `fitted`: a fitted SEM.
- `se`: standard errors for each parameter, e.g. from [`se_hessian`](@ref) or
[`se_bootstrap`](@ref).
-- `partable`: a [`ParameterTable`](@ref) to write p-values to.
+- `partable`: a `ParameterTable` to write p-values to.
- `name`: column name for the p-values in `partable`. Defaults to `:p_value`.
# Returns
diff --git a/src/frontend/specification/Sem.jl b/src/frontend/specification/Sem.jl
index f53d50715..734b63c02 100644
--- a/src/frontend/specification/Sem.jl
+++ b/src/frontend/specification/Sem.jl
@@ -67,7 +67,9 @@ end
multigroup_correction_scale(::Type{<:SemLoss}) = nothing
multigroup_correction_scale(::Type{<:SemFIML}) = 0
-multigroup_correction_scale(::Type{<:SemML}) = 0
+# ML (like WLS) uses the Wishart convention: per-group (Nᵍ-1) weighting and N-G in the
+# χ²/RMSEA.
+multigroup_correction_scale(::Type{<:SemML}) = -1
multigroup_correction_scale(::Type{<:SemWLS}) = -1
multigroup_correction_scale(loss::SemLoss) = multigroup_correction_scale(typeof(loss))
@@ -104,9 +106,9 @@ end
function Sem(
loss_terms...;
params::Union{Vector{Symbol}, Nothing} = nothing,
- default_sem_weights = :nsamples,
+ default_sem_weights = :nsamples_corrected,
)
- default_sem_weights ∈ [:nsamples, :uniform, :one] ||
+ default_sem_weights ∈ [:nsamples_corrected, :nsamples, :uniform, :one] ||
throw(ArgumentError("Unsupported default_sem_weights=:$default_sem_weights"))
# assemble a list of weighted losses and check params equality
terms = Vector{LossTerm}()
@@ -169,7 +171,16 @@ function Sem(
if !has_sem_weights && nsems > 1
# set the weights of SEMs in the ensemble
- if default_sem_weights == :nsamples
+ if default_sem_weights == :nsamples_corrected
+ # weight SEM terms by the number of samples, applying a loss-type-specific
+ # correction (see multigroup_correction_scale); consistent with the
+ # multigroup RMSEA sample-size correction
+ sem_idxs = [i for (i, term) in enumerate(terms) if issemloss(term)]
+ sem_weights = multigroup_weights(terms[sem_idxs]...)
+ for (k, i) in enumerate(sem_idxs)
+ terms[i] = LossTerm(loss(terms[i]), id(terms[i]), sem_weights[k])
+ end
+ elseif default_sem_weights == :nsamples
# weight SEM by the number of samples
nsamples_total = sum(nsamples(term) for term in terms if issemloss(term))
for (i, term) in enumerate(terms)
diff --git a/src/loss/ML/FIML.jl b/src/loss/ML/FIML.jl
index 74d5edfb4..1f02ade18 100644
--- a/src/loss/ML/FIML.jl
+++ b/src/loss/ML/FIML.jl
@@ -68,7 +68,7 @@ function gradient!(JΣ, Jμ, patloss::SemFIMLPattern, pat::SemObservedMissingPat
end
"""
- SemFIML{T, W} <: SemLossFunction
+ SemFIML{O, I, T, W} <: SemLoss{O, I}
Full information maximum likelihood (FIML) estimation.
Can handle observed data with missing values.
diff --git a/src/objective_gradient_hessian.jl b/src/objective_gradient_hessian.jl
index 23cef4e61..4a8ec649e 100644
--- a/src/objective_gradient_hessian.jl
+++ b/src/objective_gradient_hessian.jl
@@ -40,6 +40,8 @@ its computation will be turned off by setting `hessian` to `nothing`.
During the evaluation, the internal state of the loss term or of the model
could be modified.
+Returns the objective value or `nothing`.
+
# Arguments
- `objective`: a Number if the objective should be evaluated, otherwise `nothing`
- `gradient`: a pre-allocated vector the gradient should be written to, otherwise `nothing`
diff --git a/src/observed/EM.jl b/src/observed/EM.jl
index 88af6112a..b495c8861 100644
--- a/src/observed/EM.jl
+++ b/src/observed/EM.jl
@@ -39,8 +39,8 @@ Estimate the covariance and the mean for data with missing values using
the expectation maximization (EM) algorithm.
# Arguments
-- `patterns`: the observed data with missing values, grouped by missingness pattern (see [`
- SemObservedMissingPattern`](@ref))
+- `patterns`: the observed data with missing values, grouped by missingness pattern
+ (each pattern is a `SemObservedMissingPattern`)
- `max_iter_em`: the maximum number of EM iterations
- `rtol_em`: the relative tolerance for convergence of the EM algorithm
- `max_nsamples_em`: the maximum number of samples to use for each pattern in each EM iteration,
diff --git a/src/optimizer/optim.jl b/src/optimizer/optim.jl
index a0aae22ab..e81017ed3 100644
--- a/src/optimizer/optim.jl
+++ b/src/optimizer/optim.jl
@@ -113,7 +113,7 @@ function fit(
)
start_params = clamp.(start_params, lbounds, ubounds)
result = Optim.optimize(
- Optim.only_fgh!((F, G, H, par) -> evaluate!(F, G, H, model, par)),
+ NLSolversBase.only_fgh!((F, G, H, par) -> evaluate!(F, G, H, model, par)),
lbounds,
ubounds,
start_params,
@@ -122,7 +122,7 @@ function fit(
)
else
result = Optim.optimize(
- Optim.only_fgh!((F, G, H, par) -> evaluate!(F, G, H, model, par)),
+ NLSolversBase.only_fgh!((F, G, H, par) -> evaluate!(F, G, H, model, par)),
start_params,
optim.algorithm,
optim.options,
diff --git a/src/types.jl b/src/types.jl
index eb251a3b2..d0218b14f 100644
--- a/src/types.jl
+++ b/src/types.jl
@@ -16,7 +16,7 @@ MeanStruct(::Type{T}) where {T} =
MeanStruct(semobj) = MeanStruct(typeof(semobj))
-"Hessian Evaluation trait for `SemImplied` and `SemLossFunction` subtypes"
+"Hessian Evaluation trait for `SemImplied` and `AbstractLoss` subtypes"
abstract type HessianEval end
struct ApproxHessian <: HessianEval end
struct ExactHessian <: HessianEval end
@@ -41,7 +41,7 @@ abstract type SemOptimizerResult{O <: SemOptimizer} end
Supertype of all objects that can serve as the observed field of a SEM.
Pre-processes data and computes sufficient statistics for example.
-If you have a special kind of data, e.g. ordinal data, you should implement a subtype of SemObserved.
+If you have a special kind of data, you should implement a subtype of SemObserved.
"""
abstract type SemObserved end