API Reference
The user-facing API is built around four types: SCEBasis, SCEDataset, SCEFit, and SCEModel. Names listed below are stable: removals and signature-incompatible changes follow a deprecation cycle. For lower-level building blocks used internally by the SCE pipeline (and not covered by this stability guarantee), see the Internal API.
Main types
Magesty.SCEBasis — Type
SCEBasisMaterial + basis: crystal structure, symmetry, and SALC basis. The heavy part is the SALC construction; an SCEBasis can be persisted and reused. Cluster is a construction step, not a stored field — it is computed inside the constructor to build salcbasis, then discarded.
Fields
structure::Structure: Crystal structure information.symmetry::Symmetry: Symmetry operations.salcbasis::SALCBasis: SALC basis functions.isotropy::Bool: Whether the basis was built with the isotropic restriction (onlyLf = 0terms). Provenance metadata: it is not derivable fromsalcbasisalone without inspecting every basis function.salc_fingerprint::UInt64: Structural fingerprint ofsalcbasiscomputed viaSALCBases.salc_fingerprint(salcbasis). Used by the basis-identity check between anSCEModel/SCEFitand anSCEDatasetso that a basis reloaded from disk still matches the in-memory original. Stable acrossMagesty.save/Magesty.loadbecause the recipe hashes only integer structural identifiers; seeSALCBases.salc_fingerprintfor the included / excluded fields.
Examples
# Build from a TOML input file (most common path):
basis = SCEBasis("input.toml")
# The SALC construction is the expensive step; persist for reuse:
Magesty.save(basis, "basis.xml")
basis2 = Magesty.load(SCEBasis, "basis.xml")Magesty.SCEDataset — Type
SCEDatasetA SCEBasis paired with training data: the spin configurations and the unweighted design matrices and observation vectors derived from them.
X_E and X_T are stored unweighted. The torque weight is applied later at fit time, so a weight sweep reuses one SCEDataset without rebuilding design matrices.
Fields
basis::SCEBasis: The SCE basis the design matrices were built from.spinconfigs::Vector{SpinConfig}: Training spin configurations.X_E::Matrix{Float64}: Energy design matrix, one row per configuration, one column per SALC. No bias column — the reference energyj0is recovered analytically atfittime and is not a column ofX_E. Unweighted.X_T::Matrix{Float64}: Torque design matrix, no bias column, a3 * num_atomsblock of rows per configuration. Unweighted.y_E::Vector{Float64}: Observed energies, lengthn_configs.y_T::Vector{Float64}: Observed torques, flattened, length3 * num_atoms * n_configs.
Examples
# Pair a basis with training data read from an EMBSET file:
dataset = SCEDataset(basis, "EMBSET")
# Indexing yields a new SCEDataset — handy for train/test splits:
train = dataset[1:80]
test = dataset[81:end]
# Concatenate datasets that share the same basis (e.g., for cross-validation):
combined = vcat(train, test)Magesty.SCEFit — Type
SCEFitA fitted SCE regression. Holds the dataset it was fit on, the fitted coefficients, the estimator and torque weight used, and the residuals of the augmented (weighted) least-squares system.
SCEFit <: StatsAPI.RegressionModel. The response-block-independent verbs coef, intercept, nobs, dof are defined for it.
Fields
dataset::SCEDataset: The training dataset.j0::Float64: Fitted reference energy (bias term).jphi::Vector{Float64}: Fitted SCE coefficients.estimator::AbstractEstimator: The estimator used (OLS/Ridge).torque_weight::Float64: Torque weight in[0, 1]used at fit time.residuals::Vector{Float64}: Residuals of the augmented weighted least-squares system (energy rows stacked above flattened torque rows). These carry thetorque_weightscaling and are not in physical units; usermse_energy(f)/rmse_torque(f)/r2_energy(f)/r2_torque(f)for interpretable in-sample errors.
Examples
# Fit (see `fit(SCEFit, ...)` for full options):
f = fit(SCEFit, dataset, Ridge(lambda = 1e-4))
# Inspect:
coef(f) # SCE coefficient vector
intercept(f) # fitted reference energy j0 (eV)
rmse_torque(f) # in-sample torque RMSE
r2_torque(f) # in-sample torque R²
# Convert to the persistable predictor:
model = SCEModel(f)Magesty.SCEModel — Type
SCEModelA fitted SCE model: a SCEBasis together with the fitted reference energy j0 and SCE coefficients jphi. This is the lightweight, persistable predictor — predict_energy / predict_torque and the evaluation verbs accept it. Build one from a SCEFit via SCEModel(f).
Fields
basis::SCEBasis: Structure, symmetry, and SALC basis the fit used.j0::Float64: Reference energy (bias term) in eV.jphi::Vector{Float64}: SCE coefficients.
The constructor checks that length(jphi) matches the number of SALCs in basis — there is one coefficient per SALC.
Examples
# From a fit:
model = SCEModel(f)
# Predict on any dataset (e.g., the held-out split):
ŷ_E = predict_energy(model, test)
ŷ_T = predict_torque(model, test)
# Persist and reload (round-trip is byte-stable):
Magesty.save(model, "model.xml")
model2 = Magesty.load(SCEModel, "model.xml")Fitting
StatsAPI.fit — Function
fit(::Type{SCEFit}, dataset::SCEDataset, estimator::AbstractEstimator;
torque_weight::Real = 1.0, verbosity::Bool = true) -> SCEFitFit SCE coefficients on dataset with estimator, returning a SCEFit.
torque_weight in [0, 1] sets the convex combination of the per-sample energy and torque mean squared errors that the augmented least-squares problem minimizes:
loss(jphi, j0) = (1 - torque_weight) * MSE_energy + torque_weight * MSE_torquewith
MSE_energy = (1 / n_E) * Σ_{i=1..n_E} (y_E[i] - ŷ_E[i])^2
MSE_torque = (1 / n_T) * Σ_{k=1..n_T} (y_T[k] - ŷ_T[k])^2where n_E is the number of configurations, n_T = 3 * num_atoms * n_E is the total number of torque components, y_E / y_T are the observed energies and flattened torques, and ŷ_E / ŷ_T are the predictions of the model parameterised by (j0, jphi). Dividing each block by its sample count makes the two terms commensurate, so the convex combination is meaningful regardless of how many torque components a system has.
Limiting cases:
torque_weight | Behavior |
|---|---|
0 | Energy-only fit; torques are ignored. |
1 | Torque-only fit (default); energies enter through j0 only. |
0 < w < 1 | Joint fit. 0.5 weighs both per-sample MSEs equally. |
The default 1.0 is chosen on physical grounds: the SCE coefficients jphi are best determined by torque residuals, which carry the per-atom directional information that drives the response of the spin model. Energies enter the fit through the closed-form reference-energy recovery j0 = mean(y_E - X_E * jphi), so a torque-only fit still yields a usable j0. Set torque_weight < 1 only when the energies carry information that the torques do not — typically because the dataset is energy-rich and torque-poor.
Both MSEs are computed on the raw (unscaled) energy and torque residuals — the energy unit of the DFT input (typically eV) for the energy term, and eV per unit spin direction for the torque term. The two terms therefore live on the same physical scale (energy squared), so the default and the limiting cases are meaningful out of the box.
The design matrices stored in dataset are unweighted, so a torque_weight sweep reuses one SCEDataset without rebuilding them.
This is the StatsAPI fit verb; using Magesty re-exports it.
Arguments
::Type{SCEFit}: Target type — dispatch tag, written literally.dataset::SCEDataset: Training dataset (basis + design matrices + observations).estimator::AbstractEstimator: Regression estimator (OLS()orRidge(lambda=...)).torque_weight::Real = 1.0: Convex weight described above.verbosity::Bool = true: Whether to print a summary of the fit (estimator, sizes,j0, in-sample RSS / RMSE / R² on energy and torque blocks, elapsed time) to stdout after solving.
Returns
SCEFit: The fitted model. Inspect withcoef,intercept,r2_energy,rmse_torque, …; persist withMagesty.save(SCEModel(f), path).
Examples
# Default: torque-only fit with Ridge regularization.
f = fit(SCEFit, dataset, Ridge(lambda = 1e-4))
# Energy-only fit, ignoring torques entirely.
f0 = fit(SCEFit, dataset, OLS(); torque_weight = 0.0)
# Reuse one dataset across a `torque_weight` sweep without rebuilding
# the design matrices.
for w in (0.0, 0.25, 0.5, 0.75, 1.0)
println(w, " ", rmse_torque(fit(SCEFit, dataset, OLS();
torque_weight = w)))
endMagesty.refit — Function
refit(fit::SCEFit, estimator::AbstractEstimator = OLS();
threshold::Real = 0.0, verbosity::Bool = true) -> SCEFitPost-selection refit on the basis support of fit.
A basis index j enters the support iff abs(coef(fit)[j]) * norm(X[:, j]) > threshold, where X is the weighted, energy-centered design matrix used by fit (reassembled from fit.dataset with the input fit's torque_weight). The criterion is scale-equivariant: multiplying column j by any positive constant leaves the product invariant, because the OLS coefficient scales inversely while the column norm scales linearly. This cancels both the per-cluster (4π)^(N/2) factor and any other column-scale convention, so a single criterion serves both selector families: L1 estimators (Lasso, AdaptiveLasso) have exact-zero coefficients that fall out at the default threshold = 0.0, while AdaptiveRidge users pass a positive threshold to drop near-negligible bases.
The support columns are resolved on the same weighted problem with estimator; dropped bases stay at 0.0, so jphi keeps its full length and SALC ordering — a refit SCEFit round-trips through SCEModel and Magesty.save like any other fit. j0 is recovered from the uncentered energy data via extract_j0_jphi, identical to fit.
A PrecomputedPilot-backed estimator (including an AdaptiveLasso whose pilot is a PrecomputedPilot, such as AdaptiveLasso(::SCEFit)) is rejected upfront: the fixed pilot vector has the original column count, not the refit support length. A precomputed pilot is also meaningless once a support has been chosen.
Arguments
fit::SCEFit: The fitted model whose coefficient support is reused.fit.datasetandfit.torque_weightare reused verbatim, so the refit minimizes the same weighted objective as the originalfit.estimator::AbstractEstimator = OLS(): Estimator for the support resolve. Defaults toOLS()— classic post-selection debiasing. PassRidge(lambda = small)when the selected support is still rank-deficient or near-collinear.threshold::Real = 0.0: Scaled-magnitude cutoff. Bases withabs(coef(fit)[j]) * norm(X[:, j]) <= thresholdare dropped. Must be>= 0. The cutoff is in the scale of the row-whitened augmented residual block (energy rows scaled bysqrt((1-torque_weight)/n_E), torque rows bysqrt(torque_weight/n_T)) rather than raw eV, so pick it relative to the largest scaled magnitude in the input fit, e.g.maximum(abs.(coef(fit)) .* [norm(@view X[:, j]) for j in axes(X, 2)]).verbosity::Bool = true: Whether to print the standard fit summary, exactly asfitdoes.
Returns
SCEFit: A fresh fit; dropped bases carry coefficient0.0. The returnedestimatorfield is the refit estimator; the selection estimator is not recorded.
Examples
Both snippets assume dataset::SCEDataset is already in scope (built from an SCEBasis plus spin configurations).
# L1 selection + OLS debiasing. The default threshold = 0.0 keeps the
# Lasso's exact-zero support verbatim.
fit_lasso = fit(SCEFit, dataset, Lasso(lambda = 1e-3))
fit_db = refit(fit_lasso, OLS())
# AdaptiveRidge fit: pass a positive threshold to drop near-zero bases,
# then refit with Ridge against residual collinearity.
fit_ar = fit(SCEFit, dataset, AdaptiveRidge(lambda = 1e-2))
fit_db = refit(fit_ar, Ridge(lambda = 1e-6); threshold = 1e-4)StatsAPI.coef — Function
coef(f::SCEFit) -> Vector{Float64}
coef(m::SCEModel) -> Vector{Float64}The fitted SCE coefficients jphi — a single set shared by the energy and torque models.
Magesty.intercept — Function
intercept(f::SCEFit) -> Float64
intercept(m::SCEModel) -> Float64The fitted reference energy j0 (bias term), in the energy unit of the DFT input (typically eV). The returned value is exactly the j0 field of the underlying SCEFit / SCEModel, kept separate from the SCE coefficients jphi; XML persistence stores it as the j0 attribute of the <JPhi> block. intercept is a Magesty-native verb adopted because StatsAPI has no intercept concept; the two names refer to the same quantity.
Returns
Float64: the reference energyj0(the bias term), in the energy unit of the DFT input.
StatsAPI.nobs — Function
nobs(f::SCEFit) -> IntNumber of observations (spin configurations) the fit was trained on, i.e. the energy-block observation count n_configs.
Returns
Int: the number of training spin configurations.
StatsAPI.dof — Function
dof(f::SCEFit) -> IntDegrees of freedom consumed by the fit: length(coef(f)) + 1 — the SCE coefficients plus the intercept.
Returns
Int: the number of SCE coefficients plus one for the intercept.
Prediction
Magesty.predict_energy — Function
predict_energy(model::SCEModel, spin_directions::AbstractMatrix{<:Real}) -> Float64
predict_energy(model::SCEModel, sc::SpinConfig) -> Float64
predict_energy(model::SCEModel, sd_list::AbstractVector{<:AbstractMatrix{<:Real}}) -> Vector{Float64}
predict_energy(model::SCEModel, configs::AbstractVector{SpinConfig}) -> Vector{Float64}
predict_energy(model::SCEModel, dataset::SCEDataset) -> Vector{Float64}f::SCEFit may be passed in place of model; the SCEFit overloads delegate through SCEModel(f).
Predict SCE energies for one or more spin configurations.
Arguments
model::SCEModelorf::SCEFit: Trained predictor.SCEFitinputs delegate throughSCEModel(f).spin_directions::AbstractMatrix{<:Real}: Spin direction matrix of size3 × num_atoms(rows = x, y, z). Columns must be unit vectors (‖·‖ = 1within1e-6); a non-unit or non-finite column throws anArgumentError.sc::SpinConfig: Single spin configuration; equivalent to passingsc.spin_directions.sd_list::AbstractVector{<:AbstractMatrix{<:Real}}: Sequence of spin direction matrices. The number of atoms must match across entries and the predictor'sSCEBasis; the matrices themselves can be views or freshly allocated.configs::AbstractVector{SpinConfig}: Sequence of spin configurations (e.g. the output ofread_embset); onlyspin_directionsis read, the other fields are ignored.dataset::SCEDataset: Batch evaluation reusing the dataset's stored energy design matrix. Must share the predictor'sSCEBasis(same(l, m, site)column ordering as the fitted coefficients).
Returns
Return type depends on the second positional argument:
| Input form | Return |
|---|---|
spin_directions::AbstractMatrix | Float64 — energy in the unit of the training data (typically eV). |
sc::SpinConfig | Float64 — same as above, evaluated at sc.spin_directions. |
sd_list::AbstractVector{<:AbstractMatrix} | Vector{Float64} of length length(sd_list), in input order. |
configs::AbstractVector{SpinConfig} | Vector{Float64} of length length(configs), in input order. |
dataset::SCEDataset | Vector{Float64} of length length(dataset), in dataset order. |
Magesty.predict_torque — Function
predict_torque(model::SCEModel, spin_directions::AbstractMatrix{<:Real}) -> Matrix{Float64}
predict_torque(model::SCEModel, sc::SpinConfig) -> Matrix{Float64}
predict_torque(model::SCEModel, sd_list::AbstractVector{<:AbstractMatrix{<:Real}}) -> Vector{Matrix{Float64}}
predict_torque(model::SCEModel, configs::AbstractVector{SpinConfig}) -> Vector{Matrix{Float64}}
predict_torque(model::SCEModel, dataset::SCEDataset) -> Vector{Matrix{Float64}}f::SCEFit may be passed in place of model; the SCEFit overloads delegate through SCEModel(f).
Predict per-atom SCE torques for one or more spin configurations.
Arguments
model::SCEModelorf::SCEFit: Trained predictor.SCEFitinputs delegate throughSCEModel(f).spin_directions::AbstractMatrix{<:Real}: Spin direction matrix of size3 × num_atoms(rows = x, y, z). Columns must be unit vectors (‖·‖ = 1within1e-6); a non-unit or non-finite column throws anArgumentError.sc::SpinConfig: Single spin configuration; equivalent to passingsc.spin_directions.sd_list::AbstractVector{<:AbstractMatrix{<:Real}}: Sequence of spin direction matrices. The number of atoms must match across entries and the predictor'sSCEBasis.configs::AbstractVector{SpinConfig}: Sequence of spin configurations (e.g. the output ofread_embset); onlyspin_directionsis read.dataset::SCEDataset: Batch evaluation reusing the dataset's stored torque design matrix. Must share the predictor'sSCEBasis(same(l, m, site)column ordering as the fitted coefficients).
Returns
Return type depends on the second positional argument:
| Input form | Return |
|---|---|
spin_directions::AbstractMatrix | Matrix{Float64} of size 3 × num_atoms (rows = x, y, z). |
sc::SpinConfig | Matrix{Float64} of size 3 × num_atoms, evaluated at sc.spin_directions. |
sd_list::AbstractVector{<:AbstractMatrix} | Vector{Matrix{Float64}} of length length(sd_list), in input order; each element is 3 × num_atoms. |
configs::AbstractVector{SpinConfig} | Vector{Matrix{Float64}} of length length(configs), in input order; each element is 3 × num_atoms. |
dataset::SCEDataset | Vector{Matrix{Float64}} of length length(dataset), in dataset order; each element is 3 × num_atoms. |
Evaluation
Magesty.r2_energy — Function
r2_energy(predictor, data) -> Float64
r2_energy(f::SCEFit) -> Float64Coefficient of determination (R²) of the SCE energy predictions.
Call as r2_energy(f) to evaluate in-sample on the training dataset embedded in f; call as r2_energy(predictor, data) to evaluate on a different dataset — typically a held-out test set.
Arguments
predictor::Union{SCEModel, SCEFit}: Trained predictor.data: Evaluation data —SCEDataset,AbstractVector{SpinConfig}, or an EMBSET file path (AbstractString).f::SCEFit(single-argument form): Evaluatesfin-sample on its own training dataset.
Returns
Float64: R² over the observed and predicted energies (eV).
Magesty.r2_torque — Function
r2_torque(predictor, data) -> Float64
r2_torque(f::SCEFit) -> Float64Coefficient of determination (R²) of the SCE torque predictions, computed over the flattened torque components.
Call as r2_torque(f) to evaluate in-sample on the training dataset embedded in f; call as r2_torque(predictor, data) to evaluate on a different dataset — typically a held-out test set.
Arguments
predictor::Union{SCEModel, SCEFit}: Trained predictor.data: Evaluation data —SCEDataset,AbstractVector{SpinConfig}, or an EMBSET file path (AbstractString).f::SCEFit(single-argument form): Evaluatesfin-sample on its own training dataset.
Returns
Float64: R² over the flattened observed and predicted torques (eV), length3 * num_atoms * n_configs.
Magesty.rmse_energy — Function
rmse_energy(predictor, data) -> Float64
rmse_energy(f::SCEFit) -> Float64Root mean squared error of the SCE energy predictions (eV).
Call as rmse_energy(f) to evaluate in-sample on the training dataset embedded in f; call as rmse_energy(predictor, data) to evaluate on a different dataset — typically a held-out test set.
Arguments
predictor::Union{SCEModel, SCEFit}: Trained predictor.data: Evaluation data —SCEDataset,AbstractVector{SpinConfig}, or an EMBSET file path (AbstractString).f::SCEFit(single-argument form): Evaluatesfin-sample on its own training dataset.
Returns
Float64:sqrt(mean((observed - predicted).^2))over the energies (eV).
Magesty.rmse_torque — Function
rmse_torque(predictor, data) -> Float64
rmse_torque(f::SCEFit) -> Float64Root mean squared error of the SCE torque predictions (eV), over the flattened torque components.
Call as rmse_torque(f) to evaluate in-sample on the training dataset embedded in f; call as rmse_torque(predictor, data) to evaluate on a different dataset — typically a held-out test set.
Arguments
predictor::Union{SCEModel, SCEFit}: Trained predictor.data: Evaluation data —SCEDataset,AbstractVector{SpinConfig}, or an EMBSET file path (AbstractString).f::SCEFit(single-argument form): Evaluatesfin-sample on its own training dataset.
Returns
Float64:sqrt(mean((observed - predicted).^2))over the flattened torques (eV), length3 * num_atoms * n_configs.
Magesty.rss_energy — Function
rss_energy(predictor, data) -> Float64
rss_energy(f::SCEFit) -> Float64Residual sum of squares of the SCE energy predictions.
Call as rss_energy(f) to evaluate in-sample on the training dataset embedded in f; call as rss_energy(predictor, data) to evaluate on a different dataset — typically a held-out test set.
Arguments
predictor::Union{SCEModel, SCEFit}: Trained predictor.data: Evaluation data —SCEDataset,AbstractVector{SpinConfig}, or an EMBSET file path (AbstractString).f::SCEFit(single-argument form): Evaluatesfin-sample on its own training dataset.
Returns
Float64:sum((observed - predicted).^2)over the energies (eV²).
Magesty.rss_torque — Function
rss_torque(predictor, data) -> Float64
rss_torque(f::SCEFit) -> Float64Residual sum of squares of the SCE torque predictions, over the flattened torque components.
Call as rss_torque(f) to evaluate in-sample on the training dataset embedded in f; call as rss_torque(predictor, data) to evaluate on a different dataset — typically a held-out test set.
Arguments
predictor::Union{SCEModel, SCEFit}: Trained predictor.data: Evaluation data —SCEDataset,AbstractVector{SpinConfig}, or an EMBSET file path (AbstractString).f::SCEFit(single-argument form): Evaluatesfin-sample on its own training dataset.
Returns
Float64:sum((observed - predicted).^2)over the flattened torques (eV²), length3 * num_atoms * n_configs.
Magesty.residuals_energy — Function
residuals_energy(predictor, data) -> Vector{Float64}
residuals_energy(f::SCEFit) -> Vector{Float64}Per-configuration energy residuals observed - predicted (eV).
Call as residuals_energy(f) to evaluate in-sample on the training dataset embedded in f; call as residuals_energy(predictor, data) to evaluate on a different dataset — typically a held-out test set.
Arguments
predictor::Union{SCEModel, SCEFit}: Trained predictor.data: Evaluation data —SCEDataset,AbstractVector{SpinConfig}, or an EMBSET file path (AbstractString).f::SCEFit(single-argument form): Evaluatesfin-sample on its own training dataset.
Returns
Vector{Float64}of lengthn_configs, in input order.
Magesty.residuals_torque — Function
residuals_torque(predictor, data) -> Vector{Float64}
residuals_torque(f::SCEFit) -> Vector{Float64}Flattened torque residuals observed - predicted (eV).
Call as residuals_torque(f) to evaluate in-sample on the training dataset embedded in f; call as residuals_torque(predictor, data) to evaluate on a different dataset — typically a held-out test set.
Arguments
predictor::Union{SCEModel, SCEFit}: Trained predictor.data: Evaluation data —SCEDataset,AbstractVector{SpinConfig}, or an EMBSET file path (AbstractString).f::SCEFit(single-argument form): Evaluatesfin-sample on its own training dataset.
Returns
Vector{Float64}of length3 * num_atoms * n_configs, flattened over (component, atom, configuration).
Fit-quality output
write_energies and write_torques dump observed (DFT) versus predicted (SCE) values to whitespace-separated text files. The files are consumed by the FitCheck_energy.py / FitCheck_torque.py visualization scripts under tools/.
Magesty.write_energies — Function
write_energies(f::SCEFit, filename::AbstractString = "energy_list.txt")
write_energies(predictor::Union{SCEModel, SCEFit},
data::Union{SCEDataset, AbstractVector{SpinConfig}, AbstractString},
filename::AbstractString)Write observed (DFT) versus predicted (SCE) energies to a text file for fit-quality inspection. The output is consumed by the FitCheck_energy.py visualization script under tools/.
The two-argument form evaluates f against its own training dataset; the lone string argument is always the output path. The three-argument form evaluates any predictor against an explicit dataset, e.g. a held-out validation or test set, and requires an explicit output path so the data and output arguments cannot be confused.
Arguments
f::SCEFit: A fitted model; evaluated againstf.dataset.predictor::Union{SCEModel, SCEFit}: The predictor producing SCE energies.data::Union{SCEDataset, AbstractVector{SpinConfig}, AbstractString}: The configurations supplying observed energies. AnAbstractStringis read as an EMBSET file path.filename::AbstractString: Output path. Defaults to"energy_list.txt"only in the two-argumentSCEFitform.
Returns
nothing. The file atfilenameis created (overwritten if it exists).
The file has a two-line comment header followed by one row per configuration: data_index DFT_Energy SCE_Energy. Energies are in the unit of the training data (typically eV).
Examples
f = fit(SCEFit, dataset, Ridge(lambda = 1e-4))
write_energies(f) # -> energy_list.txt
write_energies(f, "train_E.txt") # training set
write_energies(f, test_dataset, "test_E.txt") # held-out set
write_energies(SCEModel(f), "EMBSET", "E.txt") # model + EMBSET pathMagesty.write_torques — Function
write_torques(f::SCEFit, filename::AbstractString = "torque_list.txt")
write_torques(predictor::Union{SCEModel, SCEFit},
data::Union{SCEDataset, AbstractVector{SpinConfig}, AbstractString},
filename::AbstractString)Write observed (DFT) versus predicted (SCE) per-atom torques to a text file for fit-quality inspection. The output is consumed by the FitCheck_torque.py visualization script under tools/.
The two-argument form evaluates f against its own training dataset; the lone string argument is always the output path. The three-argument form evaluates any predictor against an explicit dataset, e.g. a held-out validation or test set, and requires an explicit output path so the data and output arguments cannot be confused.
Arguments
f::SCEFit: A fitted model; evaluated againstf.dataset.predictor::Union{SCEModel, SCEFit}: The predictor producing SCE torques.data::Union{SCEDataset, AbstractVector{SpinConfig}, AbstractString}: The configurations supplying observed torques. AnAbstractStringis read as an EMBSET file path.filename::AbstractString: Output path. Defaults to"torque_list.txt"only in the two-argumentSCEFitform.
Returns
nothing. The file atfilenameis created (overwritten if it exists).
The file has a two-line comment header followed by a # data index: N block per configuration. Each block holds one row per atom: atom_index element DFT_torque_{x,y,z} SCE_torque_{x,y,z}. Torque components are in the unit of the training data (typically eV).
Examples
f = fit(SCEFit, dataset, Ridge(lambda = 1e-4))
write_torques(f) # -> torque_list.txt
write_torques(f, "train_T.txt") # training set
write_torques(f, test_dataset, "test_T.txt") # held-out set
write_torques(SCEModel(f), "EMBSET", "T.txt") # model + EMBSET pathCross-validation diagnostics
Generalized cross-validation (GCV) estimates out-of-sample prediction error from a single fit, on the same combined energy+torque weighted objective that fit minimizes. gcv returns the score for a fit and gcv_r2 returns the companion predictive R² (1 − GCV/MSY), which reads on a fixed scale (1 perfect, 0 matches the null model) where the raw GCV score does not; gcv_lambda sweeps the ridge penalty (one SVD serves the whole path) and reports the GCV minimizer; gcv_learning_curve sweeps the training-set size with random subsets to check data sufficiency. GCV is defined only for the linear estimators OLS, Ridge, and AdaptiveRidge. write_gcv_lambda / write_gcv_learning_curve write the sweep results to text for the FitCheck_gcv_lambda.py / FitCheck_gcv_learning_curve.py scripts under tools/. See Cross-validation diagnostics in the theory notes for the formula and conventions.
Magesty.gcv — Function
gcv(f::SCEFit) -> Float64Combined energy+torque generalized cross-validation score for the fitted model f, evaluated on its training dataset and the weighted objective f was fit with (same torque_weight and estimator).
GCV estimates the out-of-sample prediction error from a single fit via the hat matrix H (ŷ = H y):
GCV = (‖r‖² / N) / (1 − tr(H)/N)²,where r is the augmented weighted residual, tr(H) the effective degrees of freedom, and N the number of live rows — energy plus torque, minus any block zeroed by the weighting (torque_weight = 1 drops the energy block, torque_weight = 0 drops the torque block). The eliminated reference energy j0 counts one degree of freedom only when the energy block is live (torque_weight < 1). The score is in the weighted-objective unit, not eV²; compare scores (e.g. across penalties or data sizes), not the absolute magnitude.
Defined only for linear estimators (OLS, Ridge, AdaptiveRidge).
Arguments
f::SCEFit: A fitted model whose estimator is linear.
Returns
Float64: The GCV score, orNaNif the model is numerically saturated (tr(H) ≥ N).
Throws
ArgumentErroriff.estimatoris non-linear (ElasticNet/Lasso/AdaptiveLasso).
Examples
f = fit(SCEFit, dataset, Ridge(lambda = 1e-4))
gcv(f)Magesty.gcv_r2 — Function
gcv_r2(f::SCEFit) -> Float64GCV-based predictive R² for the fitted model f: the gcv score normalized against the null-model mean square msy = ‖y‖² / N, namely
R²_gcv = 1 − GCV / msy.The null model is β = 0 on the weighted, energy-centered augmented system (energy predicted at its mean, torque predicted as zero), so R²_gcv measures the cross-validated variance explained on a fixed scale: 1 is a perfect fit, 0 matches the null model, and a negative value means the fit predicts worse than the null (over-parameterized / too little data). Unlike the raw gcv score — which is in the weighted-objective unit and only meaningful in relative comparison — this value can be read in isolation.
Defined only for linear estimators (OLS, Ridge, AdaptiveRidge).
Arguments
f::SCEFit: A fitted model whose estimator is linear.
Returns
Float64: The predictive R², orNaNif the model is numerically saturated (tr(H) ≥ N).
Throws
ArgumentErroriff.estimatoris non-linear (ElasticNet/Lasso/AdaptiveLasso).
Examples
f = fit(SCEFit, dataset, Ridge(lambda = 1e-4))
gcv_r2(f) # ~1 good, ~0 no better than the mean / zero-torque modelMagesty.gcv_lambda — Function
gcv_lambda(dataset::SCEDataset, lambdas::AbstractVector{<:Real};
torque_weight::Real = 1.0) -> GCVLambdaPathRidge GCV penalty sweep: compute the combined energy+torque GCV score for every lambda and report the minimizer. A single SVD of the weighted, energy-centered design matrix serves the whole path, so passing a fine lambdas grid is cheap.
Arguments
dataset::SCEDataset: The training data (design matrices built once).lambdas::AbstractVector{<:Real}: Non-negative ridge penalties to evaluate.torque_weight::Real = 1.0: Convex energy/torque weight in[0, 1], as infit.
Returns
GCVLambdaPath: Per-lambdaGCV, predictive R² (gcv_r2), and effective dof, pluslambda_best.
Throws
ArgumentErroriflambdasis empty, contains a negative value,torque_weightis outside[0, 1], or every penalty gives a non-finite GCV.
Examples
path = gcv_lambda(dataset, 10.0 .^ (-6:0.5:0))
f = fit(SCEFit, dataset, Ridge(lambda = path.lambda_best))Magesty.gcv_learning_curve — Function
gcv_learning_curve(dataset::SCEDataset, estimator::AbstractEstimator = OLS();
sizes::AbstractVector{<:Integer} = <auto grid>,
repeats::Integer = 5, seed::Integer = 0,
torque_weight::Real = 1.0) -> GCVSizeCurveData-sufficiency GCV learning curve. At each training-set size, draw repeats random config subsets, fit estimator to each, and average their combined GCV scores; a curve that flattens with size indicates enough training data.
Subsets are drawn from the full dataset with a seeded RNG (reproducible). Each draw reuses the prebuilt design matrices via row slicing — the heavy design-matrix construction is not repeated. A draw that yields a rank-deficient OLS solve or a saturated model (tr(H) ≥ N) contributes NaN and is dropped from that size's statistics (with a warning); if every draw at a size fails, the size reports NaN.
Arguments
dataset::SCEDataset: The full training data.estimator::AbstractEstimator = OLS(): Linear estimator fit on each subset.sizes::AbstractVector{<:Integer}: Training-set sizes. Defaults to six points spanningmax(p + 2, 10)tolength(dataset)(p= number of SALCs).repeats::Integer = 5: Random draws averaged per size.seed::Integer = 0: RNG seed.torque_weight::Real = 1.0: Convex energy/torque weight in[0, 1], as infit.
Returns
GCVSizeCurve:sizes,gcv_mean,gcv_std, the predictive-R² summary (gcv_r2_mean,gcv_r2_std), and the sweep settings.
Throws
ArgumentErrorifestimatoris non-linear,repeats < 1,torque_weightis outside[0, 1], or any size is outside1:length(dataset).
Examples
curve = gcv_learning_curve(dataset, Ridge(lambda = 1e-4); repeats = 8)
curve.sizes # training-set sizes
curve.gcv_mean # mean GCV at each size — look for a plateau
curve.gcv_r2_mean # mean predictive R² — ~1 good, plateau ⇒ enough data
curve.gcv_std # spread across draws — large spread suggests more data neededMagesty.GCVLambdaPath — Type
GCVLambdaPathResult of a ridge GCV penalty sweep (gcv_lambda). The combined energy+torque GCV score is evaluated at each penalty lambda from a single SVD of the weighted design matrix.
Fields
lambdas::Vector{Float64}: The penalty values swept, in input order.gcv_scores::Vector{Float64}: GCV score at eachlambda(NaNwhere the model is numerically saturated).gcv_r2::Vector{Float64}: GCV-based predictive R² at eachlambda,1 - gcv / msy(1perfect,0matches the null model,< 0worse than null). Interpretable on a fixed scale, unlike the rawgcv_scores.dof::Vector{Float64}: Effective degrees of freedomtr(H)at eachlambda.lambda_best::Float64: Thelambdaminimizing the GCV score.torque_weight::Float64: The torque weight the sweep used.
Magesty.GCVSizeCurve — Type
GCVSizeCurveResult of a data-sufficiency GCV sweep (gcv_learning_curve). At each training-set size, repeats random config subsets are fit and scored; the mean and standard deviation across draws are reported, so a flattening curve signals that enough data is present.
Fields
sizes::Vector{Int}: Training-set sizes, ascending.gcv_mean::Vector{Float64}: Mean GCV over the random draws at each size.gcv_std::Vector{Float64}: Standard deviation over the draws at each size.gcv_r2_mean::Vector{Float64}: Mean GCV-based predictive R² over the draws at each size (1 - gcv / msy;1perfect,0matches the null model). Read on a fixed scale, unlike the rawgcv_mean.gcv_r2_std::Vector{Float64}: Standard deviation of the predictive R² over the draws at each size.repeats::Int: Random draws per size.seed::Int: RNG seed used for reproducibility.estimator::AbstractEstimator: The estimator fit on each subset.torque_weight::Float64: The torque weight the sweep used.
Magesty.write_gcv_lambda — Function
write_gcv_lambda(path::GCVLambdaPath,
filename::AbstractString = "gcv_lambda.txt")Write a ridge GCV penalty sweep (gcv_lambda) to a text file for inspection or plotting. The output is consumed by the FitCheck_gcv_lambda.py script under tools/.
The file has a comment header followed by one row per penalty: lambda gcv gcv_r2 dof. The GCV score is in the weighted-objective unit (not eV²), gcv_r2 is the GCV-based predictive R² (1 - gcv / msy; 1 perfect, 0 matches the null model) and reads on a fixed scale, and dof is the effective degrees of freedom tr(H). The selected lambda_best and the torque weight are recorded in the header.
Arguments
path::GCVLambdaPath: The sweep result.filename::AbstractString: Output path. Defaults to"gcv_lambda.txt".
Returns
nothing. The file atfilenameis created (overwritten if it exists). Any filesystem error is logged and re-thrown.
Examples
path = gcv_lambda(dataset, 10.0 .^ (-6:0.5:0))
write_gcv_lambda(path, "gcv_lambda.txt")Magesty.write_gcv_learning_curve — Function
write_gcv_learning_curve(curve::GCVSizeCurve,
filename::AbstractString = "gcv_learning_curve.txt")Write a data-sufficiency GCV learning curve (gcv_learning_curve) to a text file for inspection or plotting. The output is consumed by the FitCheck_gcv_learning_curve.py script under tools/.
The file has a comment header followed by one row per training-set size: size gcv_mean gcv_std gcv_r2_mean gcv_r2_std, where the means and standard deviations are taken over the random subset draws at that size. The GCV score is in the weighted-objective unit (not eV²); gcv_r2 is the GCV-based predictive R² (1 - gcv / msy), which reads on a fixed scale (1 perfect, 0 null). The estimator, torque weight, repeats, and seed are recorded in the header.
Arguments
curve::GCVSizeCurve: The sweep result.filename::AbstractString: Output path. Defaults to"gcv_learning_curve.txt".
Returns
nothing. The file atfilenameis created (overwritten if it exists). Any filesystem error is logged and re-thrown.
Examples
curve = gcv_learning_curve(dataset, Ridge(lambda = 1e-4); repeats = 8)
write_gcv_learning_curve(curve, "gcv_learning_curve.txt")VASP conversion
The vasp_to_extxyz, poscar_to_toml, and oszicar_to_embset functions convert VASP output to, respectively, extended XYZ, a Magesty input TOML configuration, and the EMBSET training-data format. Each is also available from the command line under magesty vasp (see Installation and Tools).
Magesty.vasp_to_extxyz — Function
vasp_to_extxyz(vasprun; oszicar=nothing, output=nothing) -> StringConvert a VASP run to extended XYZ (extxyz) format and return the extxyz text.
When only vasprun is given, the extxyz carries structure, forces, stress, and energies. Passing oszicar additionally writes per-atom magnetic moments (magmom_smoothed, magmom_raw) and the constraint field (constr_field).
The header also records soc (spin-orbit coupling flag) and, when the electronic-convergence status can be determined, converged (T/F for the electronic SCF loop). A non-converged run is still written, with converged=F and a warning. The converged key is omitted when the status is indeterminate (no NELM, single-shot NELM = 1, EDIFF <= 0, or no SCF steps).
Arguments
vasprun::AbstractString: path tovasprun.xml.
Keyword arguments
oszicar::Union{AbstractString, Nothing} = nothing: path toOSZICAR; when given, the magnetic-moment and constraint-field columns are added.output::Union{AbstractString, Nothing} = nothing: when given, the extxyz text is also written to this file (.extxyzis appended if the name does not already end with it).
Returns
String: the full extxyz text.
Throws
VaspParseError: ifvasprun(oroszicar) cannot be parsed. The error message names the offending file path.
Examples
text = vasp_to_extxyz("vasprun.xml")
vasp_to_extxyz("vasprun.xml"; oszicar = "OSZICAR", output = "frame.extxyz")Magesty.poscar_to_toml — Function
poscar_to_toml(poscar; output=nothing) -> StringConvert a VASP POSCAR structure file to a Magesty input TOML configuration and return the TOML text.
The generated configuration is a starting point for an SCE input file: it fills [general], [symmetry], [interaction], and [structure] from the POSCAR, with placeholder interaction settings (lmax = 0, cutoff = -1) meant to be edited before use.
Arguments
poscar::AbstractString: path to a POSCAR structure file.
Keyword arguments
output::Union{AbstractString, Nothing} = nothing: when given, the TOML text is also written to this file (.tomlis appended if the name does not already end with it).
Returns
String: the full TOML text.
Examples
text = poscar_to_toml("POSCAR")
poscar_to_toml("POSCAR"; output = "input.toml")Magesty.oszicar_to_embset — Function
oszicar_to_embset(oszicars; saxis=[0.0, 0.0, 1.0], energy_kind="f", mint=false, output=nothing) -> StringConvert one or more VASP OSZICAR files to the EMBSET training-data format and return the EMBSET text.
Each OSZICAR contributes one configuration block: the final-step energy, the per-atom magnetic moments, and the per-atom constraining field. The magnetic moments and fields are rotated by the saxis quantization-axis rotation Rz(alpha) * Ry(beta).
Arguments
oszicars::AbstractVector{<:AbstractString}: paths to the OSZICAR files; each becomes one configuration, numbered in the given order.
Keyword arguments
saxis::AbstractVector{<:Real} = [0.0, 0.0, 1.0]: quantization axis.energy_kind::AbstractString = "f":"f"for the free energy,"e0"for energy(sigma->0).mint::Bool = false: whentrue, read the magnetic moment from theM_intcolumns; otherwise fromMW_int.output::Union{AbstractString, Nothing} = nothing: when given, the EMBSET text is also written to this file.
Returns
String: the full EMBSET text.
Examples
text = oszicar_to_embset(["run1/OSZICAR", "run2/OSZICAR"])
oszicar_to_embset(["OSZICAR"]; saxis = [1.0, 0.0, 0.0], output = "EMBSET")Spin sampling
sample_mfa_incar draws thermally conditioned spin configurations from a VASP INCAR with the Mean-Field Approximation (von Mises-Fisher direction sampling) and writes one INCAR per configuration. It is also available from the command line as magesty vasp mfa (see Tools).
Magesty.sample_mfa_incar — Function
sample_mfa_incar(incar_path; variable, start, stop, num_points,
num_samples=1, randomize=false, fix="", uniform_atoms="",
outdir=".", prefix="sample") -> Vector{String}Sample thermally conditioned spin configurations from a VASP INCAR and write each one to its own INCAR file.
The initial spin matrix is read from MAGMOM (or M_CONSTR if MAGMOM is absent). For every value in an evenly spaced sweep of the control variable, num_samples configurations are drawn with the Mean-Field Approximation sampler (per-atom directions from a von Mises-Fisher distribution, magnitudes preserved). Each output file copies all keys from the input INCAR and sets both MAGMOM and M_CONSTR to the sampled configuration.
Arguments
incar_path::AbstractString: path to the inputINCAR.
Keyword arguments
variable::AbstractString(required): control variable,"tau"(scaled temperatureT/Tc, expected in(0, 1]) or"m"(magnetization, expected in[0, 1)). Values outside the range are clamped to the corresponding ordered/disordered limit.start::Real,stop::Real,num_points::Integer(required): the sweep values arerange(start, stop; length = num_points).num_samples::Integer = 1: configurations drawn per sweep value.randomize::Bool = false: apply a Haar-uniform random global rotation (quantization-axis randomization) to each drawn configuration. Uniform over all ofSO(3), so the sampled orientations are isotropic regardless of the direction the referenceMAGMOMhappens to be written along.fix::AbstractString = "": 1-based atom indices kept at their input directions (rotated by the same global rotation whenrandomize), e.g."1-10,12,20-22".uniform_atoms::AbstractString = "": 1-based atom indices whose direction is redrawn uniformly on the sphere instead of from the vMF distribution (same index syntax). These carry no mean-field alignment — fully isotropic for every sweep value, independent ofvariable(the disorderedκ → 0limit), unlike a default atom (partially aligned) or afixatom (frozen). Magnitudes are preserved; if an index is also infix,fixtakes precedence.outdir::AbstractString = ".": directory for the output files (created if needed).prefix::AbstractString = "sample": output file-name prefix. Themagesty vasp mfacommand does not expose this; it always uses"sample".
Returns
Vector{String}: the written file paths, in sweep order. Files are namedjoinpath(outdir, "<prefix>-NN.INCAR")whereNNruns from1tonum_points * num_samplesin(point, sample)order, zero-padded to the width of that count.
Examples
# 3 temperatures from 0.1 to 0.3, two samples each -> 6 INCAR files.
sample_mfa_incar("INCAR"; variable="tau", start=0.1, stop=0.3,
num_points=3, num_samples=2, outdir="samples")Sunny.jl export
sce_to_sunny turns a fitted SCEModel into a runnable Sunny.jl script that computes a linear spin-wave-theory magnon dispersion. It is also available from the command line as magesty sunny script (see Tools). Magesty itself gains no Sunny dependency — the function only emits text.
Magesty.sce_to_sunny — Function
sce_to_sunny(model::SCEModel; spin, g=2, mode=:auto, scaling=:auto, output=nothing, placement=:auto) -> StringExport a fitted spin-cluster-expansion model to a runnable Sunny.jl script that computes a linear spin-wave-theory (LSWT) magnon dispersion, and return the script text.
The lowest-order SALCs are converted to a conventional spin Hamiltonian: two-site l₁ = l₂ = 1 terms become 3×3 bilinear exchange matrices (Heisenberg, Dzyaloshinskii–Moriya, and anisotropic symmetric parts together), and single-site l = 2 terms become single-ion anisotropy. Higher-order SALCs (higher-l pairs, three-body and beyond) cannot be represented in Sunny and are skipped, with a warning listing what was dropped. The reference energy j0 and spin-independent terms are dropped (Sunny carries no constant energy term); the dispersion is unaffected. Energies are in the unit of the fit (typically eV).
Physical spin
The SCE couplings are fit with unit spin directions, so they absorb the spin magnitude (J_SCE = J_phys·S²). The classical energy is therefore independent of the spin length, but the magnon dispersion scales as ħω ∝ 1/S for a fixed energy landscape. You must pass the physical effective spin S_eff = m/(g μ_B) (the local-moment magnitude); using s = 1 would inflate the dispersion by a factor ~S (for MnTe, S = 5/2 ⇒ ~2.5× too high).
scaling selects how S_eff is encoded, since Sunny's Moment only accepts spin lengths that are exact multiples of 1/2:
:moment— putS_effdirectly intoMoment. Each bilinear bond is rescaled by1/(s_i s_j)and each single-ion term by a mode-dependent factor, soenergy(sys)still reproducespredict_energy(model, …) - j0and the dispersion is physical. RequiresS_effto be a half-integer.:coupling— keepMomentat a fixed half-integer placeholder (s₀ = 1) and let the couplings carryS_eff(J = M/(s₀·√(S_i S_j)), single-ion1/(s₀ S_i)). Accepts any positive realS_eff(itinerant / non-half-integer moments). The dispersion is invariant under an overall spin scale, so this still yields the physical magnon dispersion, but the represented energy landscape is rescaled —energy(sys)is then not the SCE energy. Exact for a uniformS_eff; for a non-uniformS_effthe off-diagonal exchange stays exact while the on-site (Larmor) term is approximate (a warning is emitted).
:auto (default) uses :moment when every magnetic S_eff is a half-integer and :coupling otherwise, so half-integer moments keep the exact (energy-preserving) behavior while itinerant moments still produce a physical dispersion.
Arguments
model::SCEModel: a fitted model (e.g. fromSCEModel(fit)orMagesty.load(SCEModel, path)).
Keyword arguments
spin::Union{Real, AbstractDict}(required): the effective spin lengthS_eff = m/(g μ_B). A scalar applies to every magnetic species; aDict(species => S_eff)sets it per species (every magnetic species must be present). Must be positive. Withscaling = :momentit must also be a half-integer;scaling = :coupling(or:auto) accepts any positive real. Omitting it is an error.g::Union{Real, AbstractDict} = 2: theg-factor passed toMoment; scalar or per-speciesDict. It does not affect the bare dispersion (only an external field or neutron intensities would use it).mode::Symbol = :auto: Sunny system mode.:autoselects:dipolewhen every magnetic spin is a half-integer, otherwise:dipole_uncorrected(the classical limit, appropriate for non-half-integer / itinerant moments).:dipoleapplies the quantum single-ion renormalizations(2s-1)/2(undefined fors ≤ 1/2);:dipole_uncorrecteduses the classicals². Note:mode = :dipolecombined withscaling = :couplingand single-ion anisotropy is rejected — the quantum renormalization cannot ride the placeholder Moment (use:dipole_uncorrected).scaling::Symbol = :auto: how the physicalS_effis encoded (see Physical spin).:momentputsS_effinMoment(energy-preserving, half-integer only);:couplingkeepsMomentat a placeholders₀ = 1and rescales the couplings (dispersion-only, any positive realS_eff);:autopicks:momentfor all-half-integer magnetic spins and:couplingotherwise.output::Union{AbstractString, Nothing} = nothing: when given, the script is also written to this file (.jlis appended if absent).placement::Symbol = :auto::primitivemaps interactions onto the chemical primitive cell for an unfolded dispersion;:explicitkeeps the training supercell (the dispersion is folded into the supercell Brillouin zone) but is exact for any model.:autochooses:primitivewhen the model is cleanly unfoldable (interaction range below half the supercell) and:explicitotherwise.
Returns
String: the full Sunny.jl script.
Throws
ArgumentError: ifspinis omitted; if aspin/gDictis missing a magnetic species; if any magneticspinis not positive; ifscaling = :moment(or:autoresolving to it) is used with a non-half-integer spin; ifmode,scaling, orplacementis invalid; if a model with single-ion anisotropy usesmode = :dipolewiths ≤ 1/2; or ifscaling = :couplingis combined withmode = :dipoleand single-ion anisotropy.
Examples
model = Magesty.load(SCEModel, "model.xml")
# MnTe: Mn²⁺ has S = 5/2 (half-integer ⇒ :moment route, energy-preserving).
script = sce_to_sunny(model; spin = 5//2, output = "lswt.jl")
# Itinerant moment, e.g. Fe 2.2 μB ⇒ S_eff = 1.1 (:auto picks the :coupling route).
script = sce_to_sunny(model; spin = 1.1)
# Per-species:
script = sce_to_sunny(model; spin = Dict("Mn" => 5//2, "Fe" => 1.1))Persistence
save and load are not exported — call them as Magesty.save / Magesty.load to avoid clashing with the generic save / load exported by JLD2, FileIO, CSV.jl, and others.
Magesty.save — Function
save(obj::SCEBasis, path::AbstractString)
save(obj::SCEModel, path::AbstractString)
save(f::SCEFit, path::AbstractString)Write obj to an XML file at path. An SCEBasis is written as structure, symmetry parameters, and SALC basis; an SCEModel adds the fitted reference energy and SCE coefficients in a <JPhi> block. An SCEFit is serialized as the corresponding SCEModel(f) — the fit-time dataset and estimator are not persisted.
The path must end in .xml; any other extension is an error. Use load to read the file back.
Arguments
obj::Union{SCEBasis, SCEModel}: The object to serialize.f::SCEFit: Trained fit; saved asSCEModel(f).path::AbstractString: Output file path; must end in.xml.
Returns
nothing. The XML file is written as a side effect.
Throws
ArgumentErrorifpathdoes not end in.xml.
Examples
Magesty.save(basis, "basis.xml")
Magesty.save(model, "model.xml")Magesty.load — Function
load(::Type{SCEBasis}, path::AbstractString) -> SCEBasis
load(::Type{SCEModel}, path::AbstractString) -> SCEModelRead an SCEBasis or SCEModel from the XML file at path. structure is parsed from the file, symmetry is recomputed from structure and the stored tolerance_sym, and the SALC basis is reconstructed from the stored SALC data (the expensive SALC computation is skipped).
load(SCEModel, path) additionally reads the <JPhi> block; load(SCEBasis, path) accepts an SCEModel XML as well and simply ignores the <JPhi> block.
The path must end in .xml; any other extension is an error.
Arguments
T::Type:SCEBasisorSCEModel.path::AbstractString: Input file path; must end in.xml.
Returns
SCEBasisorSCEModel: the deserialized object, matching the requested typeT.
Throws
ArgumentErrorifpathdoes not end in.xml, if a required schema attribute is missing, or (forSCEModel) if the<JPhi>block is absent.
Examples
basis = Magesty.load(SCEBasis, "basis.xml")
model = Magesty.load(SCEModel, "model.xml")Estimators
Magesty.Fitting.AbstractEstimator — Type
AbstractEstimatorAbstract type for SCE coefficient estimation methods. Concrete subtypes are passed to fit(SCEFit, dataset, estimator; torque_weight) and carry the estimator's hyperparameters in their fields; estimators without hyperparameters are zero-field singleton types (OLS), and those with hyperparameters expose them through keyword constructors (Ridge).
Magesty.Fitting.OLS — Type
OLS()Ordinary least-squares estimator (no regularization). OLS is a zero-field singleton because the OLS solver has no hyperparameter to tune — there is no lambda-like knob, by design. Construct as OLS(); contrast with Ridge(lambda = ...).
The solve goes through Cholesky on the normal equations: cholesky(Symmetric(X'X)) \ (X'y). Cholesky is the fastest stable factorization for symmetric positive-definite systems and keeps memory to the num_salcs × num_salcs Gram matrix.
If the design matrix is rank-deficient or numerically near-collinear, OLS throws ArgumentError whose message explains the cause and recommends Ridge(lambda = ε). An unregularized fit on such data is physically meaningless (the SCE coefficients are not identifiable), so the right answer is an explicit error rather than a silent fallback.
Examples
est = OLS()
f = fit(SCEFit, dataset, OLS(); torque_weight = 0.3)Magesty.Fitting.Ridge — Type
Ridge(; lambda::Real = 0.0)L2-regularized least-squares (ridge) estimator. The penalty applies uniformly to every SCE coefficient; the bias term j0 does not need to be excluded explicitly because it is eliminated analytically before the solve (see assemble_weighted_problem / extract_j0_jphi).
The solve is cholesky(Symmetric(X'X + lambda * I)) \ (X'y) — the same Cholesky-on-normal-equations route as OLS, with the lambda * I shift guaranteeing strict positive-definiteness for lambda > 0 (so Cholesky cannot fail here). When lambda ≈ 0, the call delegates to the OLS solver and inherits its PosDefException → ArgumentError behavior.
Unlike OLS, Ridge carries one hyperparameter, lambda, and is therefore a regular struct rather than a singleton. Constructing Ridge(lambda = 0.0) is exactly equivalent to OLS numerically — the extra type exists so that estimator sweeps can iterate over OLS() and Ridge(lambda = λ) without special-casing.
Fields
lambda::Float64: Regularization strengthλ ∈ [0, ∞).λ = 0reduces to OLS; larger values shrinkjphitoward zero.
Examples
# Default keyword form (lambda = 0.0 -> equivalent to OLS).
est = Ridge()
# Typical regularized fit.
est = Ridge(lambda = 1e-4)Magesty.Fitting.ElasticNet — Type
ElasticNet(; alpha::Real, lambda::Real, standardize::Bool = true)Elastic-Net estimator backed by GLMNet.jl. Covers Lasso (alpha = 1), GLMNet-style L2 (alpha = 0), and honest Elastic Net (mixed norm). standardize = true divides each column of X by its empirical standard deviation before the solve. The per-cluster (4π)^(N/2) basis normalization already puts the columns on an equal footing in the sphere-averaged (population) sense — that is what makes the fitted coefficients map onto conventional spin-model parameters — so standardization corrects only the residual per-column scale that finite, non-uniform sampling leaves behind. L1 and mixed-norm selection are sensitive to that residual scale (it shifts which clusters enter the active set), so the default is true.
The bias term j0 is eliminated analytically inside assemble_weighted_problem (the energy block of X is mean-centered before the solve) and re-fit afterward by extract_j0_jphi from the un-scaled energy residual. This estimator therefore calls GLMNet with intercept = false; adding GLMNet's own intercept on top of the already-centered system would re-introduce a uniform offset across both energy and torque rows and bias jphi.
Fields
alpha::Float64: Mixing parameter,0 ≤ alpha ≤ 1.alpha = 1is pure Lasso,alpha = 0is pure L2 (GLMNet's coordinate-descent variant), in between is Elastic Net.lambda::Float64: Penalty strength,λ ≥ 0.λ = 0reduces to OLS up to GLMNet's coordinate-descent precision.standardize::Bool: Forwarded to GLMNet, which divides each column by its empirical standard deviation. Defaulttrue: it equalizes the residual per-column scale left by finite sampling, to which L1 / mixed-norm selection is sensitive. (The(4π)^(N/2)basis factor already handles the population-level normalization.)
Examples
est = ElasticNet(alpha = 0.5, lambda = 1e-3)
# Lasso(λ = ...) is a convenience function returning ElasticNet(alpha = 1, ...).
est = Lasso(lambda = 1e-3)Magesty.Fitting.Lasso — Function
Lasso(; lambda::Real, standardize::Bool = true) -> ElasticNetConvenience function (not a type) returning ElasticNet(alpha = 1.0, lambda = lambda, standardize = standardize).
There is no separate Lasso struct: Lasso(lambda = ...) isa ElasticNet is true, and code that needs to detect the Lasso case should test e isa ElasticNet && e.alpha == 1.0. A function (rather than a const alias) prevents Lasso(alpha = 0.5, ...) from parsing, which would contradict the intended meaning "α = 1 only".
Examples
est = Lasso(lambda = 1e-3)
est isa ElasticNet # true
est.alpha # 1.0Magesty.Fitting.AdaptiveLasso — Type
AdaptiveLasso(; pilot::AbstractEstimator = OLS(),
lambda::Real,
gamma::Real = 1.0,
epsilon::Real = eps(Float64),
standardize::Bool = true)One-shot Adaptive Lasso (Zou 2006). Runs pilot on (X, y) to obtain beta_pilot, then solves the weighted-L1 Lasso
min_b ||y - X * b||^2 / (2 n) + lambda * sum_j w_j * |b_j|with w_j = 1 / max(|beta_pilot[j]|, epsilon)^gamma. Backed by GLMNet.jl with intercept = false; the energy block of X is already mean-centered upstream by assemble_weighted_problem and j0 is recovered downstream by extract_j0_jphi, the same post-processing OLS, Ridge, and ElasticNet use.
Defaults match the ALAMODE adaptive-LASSO recipe (OLS pilot, gamma = 1). gamma = 0 reduces to plain Lasso, which the test suite exploits as a correctness anchor.
Note: GLMNet internally rescales penalty_factor so the supplied weights sum to nvars. The user-supplied lambda therefore interacts with the rescaled weights, and matched-lambda comparison against plain Lasso is not apples-to-apples once gamma > 0. Per-side lambda tuning is the recommended pattern.
Fields
pilot::AbstractEstimator: First-stage estimator producingbeta_pilot. DefaultOLS()(Zou 2006 verbatim; matches ALAMODE). For SCE designs that are rank-deficient or near-collinear (e.g.num_salcs >= num_spinconfigsattorque_weight = 0), usepilot = Ridge(lambda = small)instead – the OLS minimum-norm solution populates null-space directions with ~1e-10 noise that is not clipped byeps(Float64)and miscalibrates the adaptive weights.lambda::Float64: Final L1 penalty strength,lambda >= 0.gamma::Float64: Weight exponent,gamma >= 0. Default1.0.gamma = 0reduces to plain Lasso.epsilon::Float64: Floor on|beta_pilot[j]|before reciprocation,epsilon > 0. Defaulteps(Float64). Preventspenalty_factor = Infwhen a pilot coefficient is numerically zero (e.g. whenpilotis itself a Lasso).standardize::Bool: Forwarded to GLMNet. Defaulttruefor consistency withElasticNet/Lasso; on SCE designs this is partially redundant with the adaptive reweighting but does not hurt.
Examples
# Default: OLS pilot, gamma = 1.0 (ALAMODE-style).
est = AdaptiveLasso(lambda = 1e-3)
# Recommended for rank-deficient designs.
est = AdaptiveLasso(pilot = Ridge(lambda = 1e-4), lambda = 1e-3)
# Sanity check: gamma = 0 reduces to plain Lasso.
est = AdaptiveLasso(lambda = 1e-3, gamma = 0.0)
# Reuse a previously fitted SCEFit / SCEModel as the pilot, skipping
# the pilot regression. See `PrecomputedPilot` for the underlying
# adapter and `AdaptiveLasso(::SCEFit; ...)` /
# `AdaptiveLasso(::SCEModel; ...)` for the convenience constructors.
est = AdaptiveLasso(pilot = PrecomputedPilot(coef(prior_fit)), lambda = 1e-3)Magesty.Fitting.PrecomputedPilot — Type
PrecomputedPilot(beta::AbstractVector{<:Real})Estimator adapter that returns a fixed coefficient vector from solve_coefficients, ignoring the supplied (X, y) except for a length check against size(X, 2). Designed as an AdaptiveLasso.pilot choice: lets the adaptive call reuse coefficients from a previous fit instead of running a fresh pilot regression.
The input vector is copied at construction (enforced by the inner constructor), so later mutation of the caller's storage does not leak into PrecomputedPilot.beta.
Fields
beta::Vector{Float64}: Pilot coefficient vector. Namedbetarather thancoefto avoid visual collision with theStatsAPI.coeffunction that Magesty extends.
Examples
# Reuse an existing fit's coefficients as the AdaptiveLasso pilot.
est = AdaptiveLasso(pilot = PrecomputedPilot(coef(fit)), lambda = 1e-3)Magesty.Fitting.AdaptiveRidge — Type
AdaptiveRidge(; lambda::Real,
epsilon::Real = 1e-8,
max_iter::Integer = 50,
tol::Real = 1e-6)Iterative Adaptive Ridge estimator (Frommlet & Nuel 2016). Approximates an L0-penalized fit by repeatedly refitting a per-coefficient weighted ridge problem
min_b ||y - X b||^2 + lambda * sum_j w_j * b_j^2and updating the weights w_j = 1 / (b_j^2 + epsilon) between iterations. Iteration zero is a plain ridge solve (uniform weights); each subsequent step rebuilds the weights from the current coefficients. Large coefficients receive a light penalty and small ones a heavy penalty, so iterating drives the small coefficients toward zero – an L0 approximation.
Each weighted ridge subproblem is solved analytically via the closed form b = (X'X + lambda * Diagonal(w)) \ (X'y), the same analytic family as Ridge. Unlike ElasticNet / AdaptiveLasso no GLMNet call is involved, so there is no standardize keyword: the penalty acts directly on the coefficients, where the per-cluster (4π)^(N/2) basis normalization already places them on the conventional spin-model scale, so a single epsilon is a roughly uniform magnitude floor across clusters. Because the reweighting approximates L0 (it selects), residual per-column scale from finite, non-uniform sampling can still influence which coefficients survive; rescale the design upstream if that matters under strongly non-uniform sampling.
The iteration stops when the relative infinity-norm change in the coefficient vector drops below tol, or after max_iter reweighting steps, whichever comes first.
Fields
lambda::Float64: Ridge penalty strength,lambda >= 0.lambda = 0reduces to OLS – the penalty term vanishes and the iteration is a no-op.epsilon::Float64: Floor added tob_j^2before reciprocation,epsilon > 0. Default1e-8. Keeps the weights finite when a coefficient is numerically zero, and sets the scale below which a coefficient is treated as negligible.max_iter::Int: Maximum number of reweighting iterations,max_iter >= 1. Default50.tol::Float64: Convergence threshold on the relative infinity-norm coefficient change,tol > 0. Default1e-6.
Examples
# Default iterative Adaptive Ridge.
est = AdaptiveRidge(lambda = 1e-3)
# Tighter convergence, more iterations.
est = AdaptiveRidge(lambda = 1e-3, tol = 1e-8, max_iter = 200)Data loading
Magesty.SpinConfigs.read_embset — Function
read_embset(filepath::AbstractString) -> Vector{SpinConfig}Read spin configurations from an EMBSET file.
Arguments
filepath::AbstractString: Path to the EMBSET file
Returns
Vector{SpinConfig}: Array of spin configurations
Throws
ErrorExceptionif the file format is invalidArgumentErrorif the file does not exist
Magesty.SpinConfigs.SpinConfig — Type
SpinConfigA single noncollinear spin configuration plus the DFT observables that go with it. Typically produced by read_embset, then handed to SCEDataset, predict_energy, predict_torque, or the evaluation verbs (r2_energy, rmse_torque, …).
Fields
energy::Float64: DFT total energy of the configuration [eV].magmom_size::Vector{Float64}: Magnitude of the magnetic moment on each atom [μB]. Length is `numatoms`.spin_directions::Matrix{Float64}: Unit-vector spin directions, laid out as3 × num_atoms(rows = x, y, z; each column has unit norm).local_magfield::Matrix{Float64}: Local constraining magnetic field at each atom [eV / μB], same `3 × numatomslayout asspindirections. The values come from VASP'slambda*MWperpblock, which has dimensions of energy per magnetic moment so thatE = −m · B` is in eV — not the Tesla a conventional magnetic field would carry.local_magfield_vertical::Matrix{Float64}: Component oflocal_magfieldperpendicular to the spin direction at each atom [eV / μ_B]. Computed at construction; the parallel component is dropped because classical-spin torque depends only on the perpendicular field.torques::Matrix{Float64}: Per-atom torque vectorsτᵢ = −mᵢ (eᵢ × Bᵢ)(wheremᵢis the moment magnitude,eᵢthe unit-vector spin direction,Bᵢthe local field), laid out as3 × num_atoms. These are the observables compared againstpredict_torqueduring fitting and evaluation.
Constructors
SpinConfig(energy, magmom_size, spin_directions, local_magfield; atol_unit_norm = 1e-6)—local_magfield_verticalandtorquesare computed from the other four fields.atol_unit_normis the absolute tolerance applied to‖spin_directions[:, i]‖ - 1for every atom column.
Throws
ArgumentErrorifspin_directionsandlocal_magfielddo not have matching3 × num_atomsshapes.ArgumentErrorif any entry ofmagmom_sizeis negative.ArgumentErrorif any column ofspin_directionsdeviates from unit norm by more thanatol_unit_norm(NaN columns are also rejected).
Examples
# Two atoms: one along +z, one in-plane along +x.
energy = -12.34
magmom_size = [1.5, 1.5]
spin_directions = [0.0 1.0; 0.0 0.0; 1.0 0.0] # 3 × 2 (columns = atoms)
local_magfield = [0.0 0.1; 0.2 0.0; 0.0 0.0] # 3 × 2, eV / μ_B
sc = SpinConfig(energy, magmom_size, spin_directions, local_magfield)
sc.torques # per-atom τᵢ = −mᵢ (eᵢ × Bᵢ), 3 × 2Version information
Use the standard Julia idioms:
pkgversion(Magesty)returns the package version as aVersionNumber.Base.versioninfo()dumps the active Julia / platform / threading context.
Magesty does not provide its own VERSION constant or versioninfo function — the standard library already covers both needs.