Orthogonal Uncertainty Sets
PortfolioOptimisers.AbstractOrthogonalScaling — Type
abstract type AbstractOrthogonalScaling <: AbstractAlgorithmAbstract supertype of the scalings that size each direction inside the Orthogonal Subspace.
A member states the $r \times r$ matrix $\mathbf{\Lambda}$ of the mean set, whose square root maps the ball's coordinates onto the subspace basis. The scaling changes the shape of the set and not its support: every member confines the set to the same subspace, and the radius reads the same rank degrees of freedom.
Interfaces
orthogonal_scaling
orthogonal_scaling(scaling::AbstractOrthogonalScaling, G::MatNum, rr::AbstractLoadingsRegressionResult) -> MatNum: Returns $\mathbf{\Lambda}$,rank × rank, symmetric and positive semi-definite.
Related
PortfolioOptimisers.AbstractOrthogonalityMetric — Type
abstract type AbstractOrthogonalityMetric <: AbstractAlgorithmAbstract supertype of the cross-sectional weightings that name a weight source on a fitted loadings block.
A member of this family selects a weight vector a fitted loadings block already carries, and computes none. It is the marker-family form the library uses for a fixed choice among stored quantities, as class does on NormBallUncertaintySet. Its sibling AbstractCrossSectionalWeightsAlgorithm is the other side of the pair: that family computes the cross-sectional weights inside the prior's own fit, and this one names which of the fit's outputs to read back.
Two readers resolve a member to two different shapes, both over the same field of the block. orthogonality_weights reads the weights of the latest observation, one entry per asset, for an uncertainty set whose geometry serves the next decision. cs_diagnostic_weights reads the whole observation history, one row per observation, for a diagnostic that scores every observation instead of only the latest.
Interfaces
orthogonality_weights
orthogonality_weights(metric::AbstractOrthogonalityMetric, rr::AbstractLoadingsRegressionResult) -> Option{<:VecNum}: Returns the weights of the latest observation, ornothingwhen the member is the unweighted one.
cs_diagnostic_weights
cs_diagnostic_weights(metric::AbstractOrthogonalityMetric, csfm::CrossSectionalFactorModel) -> Option{<:MatNum}: Returns the whole observation history the member names, ornothingwhen the member is the unweighted one.
Related
PortfolioOptimisers.BenchmarkWeightMetric — Type
struct BenchmarkWeightMetric <: AbstractOrthogonalityMetricNames the benchmark weights as the cross-sectional weight source.
A block's bw field carries the benchmark weight of every asset at every observation, and a reader takes its latest row for an uncertainty set or its whole history for a diagnostic. A block fitted per asset over the observations carries no such field and refuses.
The metric needs every investable asset to have entered the latest fit. An asset can be in the Investable Mask and outside the estimation universe of the latest cross-section: its return is finite, so the prior states a moment for it, and it is dropped from the fit, so its benchmark weight is zero there. A zero weight cannot be inverted into a metric, and an uncertainty set built on this metric refuses the block. On a point-in-time Asset Panel whose latest cross-section does not cover the whole Investable Mask, use InverseIdiosyncraticVarianceMetric or IdentityMetric, which read no weight history.
Examples
julia> BenchmarkWeightMetric()BenchmarkWeightMetric()Related
PortfolioOptimisers.RegressionWeightMetric — Type
struct RegressionWeightMetric <: AbstractOrthogonalityMetricNames the regression weights as the cross-sectional weight source.
A block's rw field carries the weight the Cross-Sectional Regression gave every asset at every observation, and a reader takes its latest row for an uncertainty set or its whole history for a diagnostic. A block fitted per asset over the observations carries no such field and refuses.
The metric needs every investable asset to have entered the latest fit. An asset can be in the Investable Mask and outside the estimation universe of the latest cross-section: its return is finite, so the prior states a moment for it, and it is dropped from the fit, so its regression weight is zero there. A zero weight cannot be inverted into a metric, and an uncertainty set built on this metric refuses the block. On a point-in-time Asset Panel whose latest cross-section does not cover the whole Investable Mask, use InverseIdiosyncraticVarianceMetric or IdentityMetric, which read no weight history.
Examples
julia> RegressionWeightMetric()RegressionWeightMetric()Related
PortfolioOptimisers.InverseIdiosyncraticVarianceMetric — Type
struct InverseIdiosyncraticVarianceMetric <: AbstractOrthogonalityMetricNames the inverse of the idiosyncratic variances as the cross-sectional weight source, the default.
An asset whose returns the factors explain well carries a large weight. The variances come from idiosyncratic_variances on the loadings block, which reads esigma, and a non-positive variance refuses.
Examples
julia> InverseIdiosyncraticVarianceMetric()InverseIdiosyncraticVarianceMetric()Related
PortfolioOptimisers.IdentityMetric — Type
struct IdentityMetric <: AbstractOrthogonalityMetricNames no weight source: every asset carries the same weight.
It is the one member that reads nothing off the loadings block, so it serves a block that carries neither a weight history nor an idiosyncratic variance. Both readers answer nothing on this member, which their caller takes as a vector of ones.
Examples
julia> IdentityMetric()IdentityMetric()Related
PortfolioOptimisers.IdentityScaling — Type
struct IdentityScaling <: AbstractOrthogonalScalingGives every direction of the Orthogonal Subspace the same uncertainty, the default.
$\mathbf{\Lambda} = \mathbf{I}_{r}$, so the geometry map is the subspace basis itself and the set is a Euclidean ball inside the subspace.
Examples
julia> IdentityScaling()IdentityScaling()Related
PortfolioOptimisers.IdiosyncraticVarianceScaling — Type
struct IdiosyncraticVarianceScaling <: AbstractOrthogonalScalingSizes each direction of the Orthogonal Subspace by the idiosyncratic covariance projected onto it.
$\mathbf{\Lambda} = \mathbf{G}^{\intercal}\mathbf{D}\mathbf{G}$, with $\mathbf{D}$ the idiosyncratic covariance the loadings block carries. A direction the factors leave noisy is then given more uncertainty than a quiet one, where IdentityScaling gives both the same. The block must carry an idiosyncratic covariance, so an unset esigma refuses.
Examples
julia> IdiosyncraticVarianceScaling()IdiosyncraticVarianceScaling()Related
PortfolioOptimisers.OrthogonalUncertaintySet — Type
struct OrthogonalUncertaintySet{__T_q, __T_method, __T_scaling, __T_kappa, __T_metric} <: AbstractPriorUncertaintySetEstimatorFits both uncertainty sets from the factor model of the optimisation's own prior, confined to the directions the factors do not span.
The estimator reads the loadings block rr of the Prior Result it is handed, and never fits a prior of its own. It is the one member of AbstractPriorUncertaintySetEstimator, and the two JuMP builders pass the reduced prior beside the returns so that the sets and the moments they correct are fitted on one object.
One estimator answers both axes. The weighted loadings, their thin singular value decomposition and the numerical rank are computed once, and ucs returns the pair. The mean axis gets a NormBallUncertaintySet whose geometry map spans the Orthogonal Subspace, and the covariance axis gets a CompactCovarianceUncertaintySet whose basis is the weighted factor span itself. The two are complementary: a portfolio inside the span pays nothing on either axis, and one outside it pays on both.
The point estimates are unchanged. The mean set is centred on pr.mu and the covariance set carries pr.sigma, so nothing is shrunk in the prior. The correction is a portfolio-dependent worst case that grows with the exposure to the subspace, which is what an optimiser that over-allocates to unspanned directions needs.
Mathematical definition
\[\begin{align} \mathbf{B}_{\mathbf{W}} &= \mathbf{W}^{1/2}\mathbf{B}\,, \quad \mathbf{Q} = \operatorname{svd}_{r_{\mathbf{B}}}(\mathbf{B}_{\mathbf{W}})\,, \\ \mathbf{A} &= \mathbf{W}^{-1/2}\left(\mathbf{I} - \mathbf{Q}\mathbf{Q}^{\intercal}\right)\,, \quad \mathbf{G} = \operatorname{qr}(\mathbf{A}\mathbf{V}_{+})\,, \\ \mathbf{L} &= \mathbf{G}\mathbf{\Lambda}^{1/2}\,, \quad \kappa_{\boldsymbol{\mu}} = \sqrt{\chi^{2,\,-1}_{r}(1 - q)}\,, \\ \mathbf{C} &= \mathbf{W}^{-1/2}\,. \end{align}\]
Where:
- $\mathbf{B}$: Effective loadings, $N \times K$, reduced to a full-rank basis when a Factor Family was re-based.
- $\mathbf{W} = \operatorname{diag}(\boldsymbol{w})$: Cross-sectional metric the
AbstractOrthogonalityMetricnames, the identity onIdentityMetric. - $\mathbf{Q}$: Left singular vectors of the weighted loadings kept by the numerical rank $r_{\mathbf{B}}$, an orthonormal basis of the weighted factor span.
- $\mathbf{A}$: Orthogonal projector, mapped back through the metric.
- $\mathbf{V}_{+}$: Eigenvectors of $\mathbf{A}^{\intercal}\mathbf{A}$ that carry its $N - r_{\mathbf{B}}$ largest eigenvalues, which is the rank of the projector.
- $\mathbf{G}$: Orthonormal basis of the Orthogonal Subspace, $N \times r$.
- $\mathbf{\Lambda}$: Scaling the
AbstractOrthogonalScalingnames, $r \times r$. - $\mathbf{L}$: Geometry map of the mean set.
- $\kappa_{\boldsymbol{\mu}}$: Radius of the mean set, at $r$ degrees of freedom.
- $\mathbf{C}$: Diagonal metric square root of the covariance set.
The rank of the mean set is the dimension of the Orthogonal Subspace, not the number of assets, because a flat set is a confidence region of its own subspace and not of the ambient space. A model whose loadings span the whole cross-section leaves $r = 0$, a radius of zero, and no correction at all.
Fields
q: Confidence level that sizes the uncertainty set (0 < q < 1). A smallerqis more demanding and yields a larger, more conservative set (wider box intervals / larger ellipsoid radius); a largerqgives a tighter set closer to the point estimate.
method: Ellipsoidal uncertainty set estimation method.
scaling: Scaling of the mean set inside the Orthogonal Subspace. It changes the shape of the set and not the subspace the set lives in.
kappa: Radius $\kappa \geq 0$ of the covariance set, the multiplier of its quadratic penalty, and0leaves the nominal variance. It is a size the caller states, a rule ofAbstractCompactRadiusAlgorithmthat computes one from the sample and the span, or a value a search picks:kappais a plain field, so"ucs.kappa"is a lens path aGridSearchCrossValidationor aRandomisedSearchCrossValidationgrid ranges over, and a grid may hold rules beside numbers.
metric: Cross-sectional weighting under which the factor span is taken. It fixes the geometry of both sets, because both read one span.
Constructors
OrthogonalUncertaintySet(; q::Number = 0.05, method::Num_UcSK = ChiSqKUncertaintyAlgorithm(), scaling::AbstractOrthogonalScaling = IdentityScaling(), kappa::Num_CptRad = 1.0, metric::AbstractOrthogonalityMetric = InverseIdiosyncraticVarianceMetric()) -> OrthogonalUncertaintySetKeywords correspond to the struct's fields.
Validation
0 < q < 1.- If
kappais a number:isfinite(kappa)andkappa >= 0. A rule is checked where its number lands, by the constructor ofCompactCovarianceUncertaintySet.
Examples
julia> OrthogonalUncertaintySet()OrthogonalUncertaintySet q ┼ Float64: 0.05 method ┼ ChiSqKUncertaintyAlgorithm() scaling ┼ IdentityScaling() kappa ┼ Float64: 1.0 metric ┴ InverseIdiosyncraticVarianceMetric()Related
AbstractPriorUncertaintySetEstimatorAbstractOrthogonalityMetricAbstractOrthogonalScalingNormBallUncertaintySetCompactCovarianceUncertaintySetucsmu_ucssigma_ucsk_norm_ball
References
- [94] D. P. Palomar. Portfolio Optimization: Theory and Application (Cambridge University Press, 2025). Chapter 14.
- [89] D. Goldfarb and G. Iyengar. Robust Portfolio Selection Problems. Mathematics of Operations Research 28, 1–38 (2003). Section 5.
- [88] A. Ben-Tal and A. Nemirovski. Robust Convex Optimization. Mathematics of Operations Research 23, 769–805 (1998). Section 3.
PortfolioOptimisers.orthogonal_scaling — Function
orthogonal_scaling(::IdentityScaling, G::MatNum, ::AbstractLoadingsRegressionResult)
orthogonal_scaling(::IdiosyncraticVarianceScaling, G::MatNum, rr::AbstractLoadingsRegressionResult)Scaling $\\mathbf{\\Lambda}$ of the mean set inside the Orthogonal Subspace.
Mathematical definition
\[\begin{align} \mathbf{\Lambda}_{\mathrm{id}} &= \mathbf{I}_{r}\,, \\ \mathbf{\Lambda}_{\mathrm{idio}} &= \mathbf{G}^{\intercal}\mathbf{D}\mathbf{G}\,. \end{align}\]
Where:
- $\mathbf{G}$: Orthonormal basis of the Orthogonal Subspace, $N \times r$.
- $\mathbf{D}$: Idiosyncratic covariance the loadings block carries, $N \times N$, diagonal when the block stores a variance vector.
- $r$: Dimension of the subspace.
Algorithm
- On
IdentityScaling, return the $r \times r$ identity as a dense matrix, so the caller's square root and the radius read one type on both routes. - On
IdiosyncraticVarianceScaling, readrr.esigma, form $\mathbf{G}^{\intercal}\mathbf{D}\mathbf{G}$ and symmetrise it. A stored variance vector is used as a diagonal without materialising the $N \times N$ matrix.
Arguments
scaling: Orthogonal scaling.G: Orthonormal basis of the Orthogonal Subspace, $N \times r$.rr: Fitted loadings block.
Validation
- On
IdiosyncraticVarianceScaling:idiosyncratic_variancesthrows when the block carries noesigma.
Returns
lambda::MatNum: $r \times r$ scaling, symmetric and positive semi-definite.
Related
PortfolioOptimisers.orthogonality_weights — Function
orthogonality_weights(::IdentityMetric, ::AbstractLoadingsRegressionResult)
orthogonality_weights(::InverseIdiosyncraticVarianceMetric, rr::AbstractLoadingsRegressionResult)
orthogonality_weights(::BenchmarkWeightMetric, rr::AbstractLoadingsRegressionResult)
orthogonality_weights(::RegressionWeightMetric, rr::AbstractLoadingsRegressionResult)
orthogonality_weights(::BenchmarkWeightMetric, rr::CrossSectionalFactorModel)
orthogonality_weights(::RegressionWeightMetric, rr::CrossSectionalFactorModel)Cross-sectional weight vector the AbstractOrthogonalityMetric names, read off a fitted loadings block.
Algorithm
- On
IdentityMetric, returnnothing. The caller reads anothingas a vector of ones and skips both the scaling of the loadings and the division of the projector, so the unweighted route costs no arithmetic. - On
InverseIdiosyncraticVarianceMetric, takeidiosyncratic_variances(rr)and return its element-wise inverse. - On
BenchmarkWeightMetricandRegressionWeightMetricover aCrossSectionalFactorModel, take the last row ofbwor ofrw, the weights of the latest observation. - On the same two metrics over any other loadings block, throw. A block fitted per asset over the observations carries no cross-sectional weight history, so the metric has nothing to read.
Arguments
metric: Orthogonality metric.rr: Fitted loadings block.
Validation
- On
InverseIdiosyncraticVarianceMetric: every idiosyncratic variance is finite and> 0, else aDomainError.idiosyncratic_variancesitself throws when the block carries none. - On a weight history: the history is non-empty, and every weight of its last row is finite and
> 0, else anIsEmptyErroror aDomainError. - On a weight metric over a block that carries no history: an
IsNothingErrornaming the field.
Returns
w::Option{<:VecNum}: Weight vector of length $N$, ornothingonIdentityMetric.
Related
PortfolioOptimisers.cs_diagnostic_weights — Function
cs_diagnostic_weights(weighting::IdentityMetric, csfm::CrossSectionalFactorModel)
cs_diagnostic_weights(weighting::BenchmarkWeightMetric, csfm::CrossSectionalFactorModel)
cs_diagnostic_weights(weighting::RegressionWeightMetric, csfm::CrossSectionalFactorModel)
cs_diagnostic_weights(weighting::InverseIdiosyncraticVarianceMetric, csfm::CrossSectionalFactorModel)Return the cross-sectional weight history an AbstractOrthogonalityMetric names, over the whole observation axis of a factor model block.
orthogonality_weights reads the same family off the same block and answers with the weights of the latest observation, which is what an uncertainty set needs. A diagnostic scores every observation, so it needs the whole history, and this verb is that reading of the family.
Arguments
weighting: The metric that names the history.csfm: A cross-sectional factor model block.
Validation
- The field the metric names is not
nothing, else anIsNothingErrornaming it is raised.
Returns
u::Option{<:MatNum}:observations × assets, andnothingunderIdentityMetric, which the diagnostics read as equal weights.
Related
PortfolioOptimisers.ucs — Method
ucs(
ue::OrthogonalUncertaintySet,
pr::AbstractPriorResult;
rd,
kwargs...
) -> Tuple{Union{NormBallUncertaintySet{var"#s185", <:AbstractMatrix{var"#s137"}, Int64, MuUncertaintySetClass, Nothing} where {var"#s185"<:Number, var"#s137"<:(Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar})}, NormBallUncertaintySet{var"#s185", var"#s1851", Int64, MuUncertaintySetClass, <:AbstractArray{var"#s137", N}} where {var"#s185"<:Number, var"#s137"<:(Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}), var"#s1851"<:AbstractMatrix{var"#s137"}, var"#s137"<:(Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}), N}}, Union{CompactCovarianceUncertaintySet{var"#s185", var"#s1851", <:AbstractMatrix{var"#s137"}, Nothing} where {var"#s185"<:Number, var"#s137"<:(Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}), var"#s1851"<:AbstractVector{var"#s137"}, var"#s137"<:(Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar})}, CompactCovarianceUncertaintySet{var"#s185", var"#s1851", var"#s1852", <:AbstractMatrix{var"#s137"}} where {var"#s185"<:Number, var"#s137"<:(Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}), var"#s1851"<:AbstractVector{var"#s137"}, var"#s137"<:(Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}), var"#s1852"<:AbstractMatrix{var"#s137"}, var"#s137"<:(Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar})}}}
Fits both uncertainty sets of an OrthogonalUncertaintySet from the prior result in one pass.
Algorithm
- Reduce the prior result, and the returns data beside it, to the Investable Mask with
investable_ucs_reduction. Inside an optimiser the result arrives already reduced and the step is a passthrough; standalone, on a prior fitted on a point-in-time Asset Panel, it takes the view the optimiser would have taken. - Take the weighted factor span once with
orthogonal_factor_span. - Build the mean set with
orthogonal_mu_setand the covariance set withorthogonal_sigma_set, both from that span. - Write each set back onto the full universe with
expand_investable_ucs, so a set fitted standalone is over the same assets the prior is, with a zero row on every asset outside the mask, and a view of it at the mask recovers the reduced fit.
A caller that needs one axis alone calls mu_ucs or sigma_ucs, which take the same span and build one set.
Arguments
ue: Orthogonal uncertainty set estimator.pr: Prior result the optimisation is solving on.rd: Returns data the set is being fitted beside, ornothing. Only aVarianceFractionholding an optimiser reads it, and the three-argument forms ofucsfill it in.kwargs...: Additional keyword arguments (ignored).
Returns
mu_ucs::NormBallUncertaintySet: Mean set on the Orthogonal Subspace.sigma_ucs::CompactCovarianceUncertaintySet: Covariance set that spares the factor span.
Related
PortfolioOptimisers.mu_ucs — Method
mu_ucs(
ue::OrthogonalUncertaintySet,
pr::AbstractPriorResult;
kwargs...
) -> Union{NormBallUncertaintySet{var"#s185", <:AbstractMatrix{var"#s137"}, Int64, MuUncertaintySetClass, Nothing} where {var"#s185"<:Number, var"#s137"<:(Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar})}, NormBallUncertaintySet{var"#s185", var"#s1851", Int64, MuUncertaintySetClass, <:AbstractArray{var"#s137", N}} where {var"#s185"<:Number, var"#s137"<:(Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}), var"#s1851"<:AbstractMatrix{var"#s137"}, var"#s137"<:(Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}), N}}
Fits the mean uncertainty set of an OrthogonalUncertaintySet from the prior result.
Algorithm
- Reduce the prior result to the Investable Mask with
investable_ucs_reduction, a passthrough on a result that arrived reduced. - Take the weighted factor span with
orthogonal_factor_span. - Build the mean set with
orthogonal_mu_set. - Write it back onto the full universe with
expand_investable_ucs.
Arguments
ue: Orthogonal uncertainty set estimator.pr: Prior result the optimisation is solving on.kwargs...: Additional keyword arguments (ignored).
Returns
ucs::NormBallUncertaintySet: Mean set on the Orthogonal Subspace.
Related
PortfolioOptimisers.sigma_ucs — Method
sigma_ucs(
ue::OrthogonalUncertaintySet,
pr::AbstractPriorResult;
rd,
kwargs...
) -> Union{CompactCovarianceUncertaintySet{var"#s185", var"#s1851", <:AbstractMatrix{var"#s137"}, Nothing} where {var"#s185"<:Number, var"#s137"<:(Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}), var"#s1851"<:AbstractVector{var"#s137"}, var"#s137"<:(Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar})}, CompactCovarianceUncertaintySet{var"#s185", var"#s1851", var"#s1852", <:AbstractMatrix{var"#s137"}} where {var"#s185"<:Number, var"#s137"<:(Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}), var"#s1851"<:AbstractVector{var"#s137"}, var"#s137"<:(Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}), var"#s1852"<:AbstractMatrix{var"#s137"}, var"#s137"<:(Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar})}}
Fits the covariance uncertainty set of an OrthogonalUncertaintySet from the prior result.
Algorithm
- Reduce the prior result, and the returns data beside it, to the Investable Mask with
investable_ucs_reduction, a passthrough on a result that arrived reduced. - Take the weighted factor span with
orthogonal_factor_span. - Build the covariance set with
orthogonal_sigma_set. - Write it back onto the full universe with
expand_investable_ucs.
Arguments
ue: Orthogonal uncertainty set estimator.pr: Prior result the optimisation is solving on.rd: Returns data the set is being fitted beside, ornothing. Only aVarianceFractionholding an optimiser reads it, and the three-argument forms ofsigma_ucsfill it in.kwargs...: Additional keyword arguments (ignored).
Returns
ucs::CompactCovarianceUncertaintySet: Covariance set that spares the factor span.
Related
References
- [88]
- A. Ben-Tal and A. Nemirovski. Robust Convex Optimization. Mathematics of Operations Research 23, 769–805 (1998).
- [89]
- D. Goldfarb and G. Iyengar. Robust Portfolio Selection Problems. Mathematics of Operations Research 28, 1–38 (2003).
- [94]
- D. P. Palomar. Portfolio Optimization: Theory and Application (Cambridge University Press, 2025).