Base Phylogeny

PortfolioOptimisers.factoryMethod
factory(
    pl::Union{AbstractPhylogenyEstimator, AbstractPhylogenyResult},
    args...;
    kwargs...
) -> NetworkEstimator{<:CovarianceEstimator, <:AbstractDistanceEstimator, KruskalTree{Tuple{}, @NamedTuple{}}, <:AbstractSeparationAlgorithm}

Return the phylogeny estimator or result pl unchanged.

Identity pass-through used when a phylogeny estimator or pre-computed result is provided in a context that calls factory.

Related

source
PortfolioOptimisers.AbstractSeparationAlgorithmType
abstract type AbstractSeparationAlgorithm <: AbstractAlgorithm

Abstract supertype for all separation algorithms.

A separation algorithm is the rule saying how far apart two assets sit in a network, and how far is too far. It answers two questions with one object, through three kernels: separation_graph builds the structure the member measures over, separation_matrix reads the dense assets × assets separations off that structure, and separation_budget resolves the budget beyond which a pair counts as unrelated. The family is open: a new member is a struct and one method of each.

The two questions travel together because they share a unit. A hop count is budgeted in hops and a weighted path length in the distance estimator's units, so a budget stated apart from the rule that measures it would be a number nobody could interpret — which is why the budget lives on the member rather than on NetworkEstimator.

Building the structure is a separate kernel from measuring it

The measuring kernel takes the structure, not the estimator that produces one: separation_matrix(sep, g) is the interface, and separation_matrix(sep, nte, X) is a wrapper that calls separation_graph first. The split is calc_weighted_adjacency_graph's two-entry-point shape, for the same reason — the structure is expensive and a caller often holds one already. Under VariationInfoDistance building it is 98% of clusterise's runtime, so a consumer that resolves a budget rule and measures the separations must build once and pass the graph, not call two estimator-taking kernels.

It is also the seam a test or an extension enters through. Every structure a shipped estimator can build is connected — a spanning tree or a PMFG — so a disconnected graph, and with it the unreachable sentinel below, is reachable only by handing one in.

Two more kernels, and why neither is a third question

resolve_separation turns a member whose budget is a rule into one whose budget is a value, and is called by every consumer before the other two kernels. It is not a third question about the network: a member whose budget is already a number is returned unchanged by the fallback on this type, so an extension inherits the kernel and never writes one.

is_related applies the budget to one entry of the separation matrix, over is_reachable's sentinel test. Both are single generic methods on this type rather than per-member ones, because the rule — not the sentinel, and no further than the budget — is the same rule whatever the unit; a member whose underlying routine reports an exotic sentinel overrides is_reachable alone. Every consumer applying a budget calls them instead of open-coding the comparison, which is what keeps the ordering obligation of separation_matrix's "the unreachable sentinel" inside an interface.

The two shipped members widen their budget field to admit a rule — HopCountValue and PathLengthValue — so a caller who cannot state the budget in advance states what would produce it instead. The resolution happens where the data is in hand, which is the only place a rule can be answered, and it is why separation_budget refuses an unresolved member rather than returning a function.

Separation is not decay

AbstractSeparationDecayAlgorithm turns a separation into a score; this family produces the separation and says where it runs out. The seam is that sep decides which pairs are related — every consumer of a network needs that — while decay decides how strongly, as a number, which only the feature producer wants. That is why sep sits on NetworkEstimator and decay sits on Proximity.

The family is unqualified on purpose

The name says nothing about graphs. A taxonomy depth is a separation too, so the room is left for a member that measures one, rather than being closed off by an AbstractGraphSeparationAlgorithm.

Related

source
PortfolioOptimisers.HopCountAlgorithmType
abstract type HopCountAlgorithm <: AbstractAlgorithm

Abstract supertype for all rules computing a hop count from the network and the data.

A HopCount budget is usually a number the caller states. It does not have to be. A subtype of HopCountAlgorithm is a callable struct standing in the n field, and resolve_separation calls it with the network estimator and the data matrix in hand. That is what lets a budget follow a universe whose size the caller cannot know in advance — a cross-validation fold, or a subproblem of a meta optimiser such as NestedClustered.

The extension contract

A subtype defines one method, the functor:

(rule::MySubtype)(nte::AbstractNetworkEstimator, X::MatNum, g::Graphs.AbstractGraph;                  dims::Int = 1, kwargs...) -> Integer

g is the structure separation_graph built for the separation the rule stands in, so a rule reads what it needs off a graph it did not pay to build — through separation_matrix, which takes g directly. It still pays for the all-pairs traversal; see resolve_separation.

nte owns the separation and X is the data g was built from. Both are inert for the shipped rule, and are the channel through which an extension reaches what the graph does not carry — the distance estimator, the observation count, a covariance.

The return value must be an Integer, and this is checked rather than bounded. A functor's return type is not part of its signature, so the family cannot state the requirement in the type system. resolve_separation checks it instead, and the check is not a formality: three readers use 0:n as a matrix-power count, where 0:1.5 silently drops a power rather than failing.

A bare Function is admitted in the same field and carries the same obligation, unchecked at construction. Subtype this instead when the rule has parameters — the struct holds them, prints them, and is comparable.

Related

source
PortfolioOptimisers.PathLengthAlgorithmType
abstract type PathLengthAlgorithm <: AbstractAlgorithm

Abstract supertype for all rules computing a path-length budget from the network and the data.

The PathLength counterpart of HopCountAlgorithm: a callable struct standing in the dmax field, called by resolve_separation with the network estimator, the data matrix, and the structure already built from them in hand.

The two families are separate because their return obligations differ, and the split is what lets one of them be checked. A hop count must be an Integer; a path-length budget is stated in the distance estimator's units, so it is any Number — or nothing, which resolves to the observed diameter exactly as a stated nothing does.

The extension contract

A subtype defines one method, the functor:

(rule::MySubtype)(nte::AbstractNetworkEstimator, X::MatNum, g::Graphs.AbstractGraph;                  dims::Int = 1, kwargs...) -> Number

g is separation_graph's structure, weighted by distance on both branches under a PathLength. A bare Function is admitted in the same field and carries the same obligation, unchecked at construction.

A rule must return a Number, and nothing is not one. nothing in the dmax field means the observed diameter, which is a statement the caller makes instead of stating a rule. A rule that meant to ask for the diameter is asking for something the field already spells, and a rule that returned nothing by accident would silently get the maximal ball. So PathLengthValue covers the rules and the numbers, and the field is an Option of it.

Related

source
PortfolioOptimisers.PathLengthValueType
const PathLengthValue = Union{<:Number, <:PathLengthAlgorithm, <:Function}

Alias for everything PathLength's dmax field accepts, apart from nothing.

The PathLength counterpart of HopCountValue, and the field is Option{PathLengthValue} rather than this alias alone. The asymmetry is deliberate: nothing in that field means the observed diameter, which is one of the stated budgets and not something a rule may answer with. Keeping it outside the alias is what makes resolve_separation's check a plain isa(dmax, Number).

Related

source
PortfolioOptimisers.HopCountType
struct HopCount{__T_n} <: AbstractSeparationAlgorithm

Separation measured as the number of graph edges between two assets.

The separation between two assets is the length of the shortest path between them counted in edges, ignoring the weights those edges carry, and the budget is n of them. It is the separation the network family has always used: phylogeny_matrix's sum(A^i for i in 0:n) and both clusterise methods' power sums are hop budgets, and this member is where that n now lives.

The budget is a field rather than an argument because it is stated in hops, a unit only this member uses. PathLength measures the same structure in the distance estimator's units and carries its own budget in those, so no caller has to know which unit is in play.

The power sum is the source's range connection matrix

phylogeny_matrix's $\mathbf{P} = \mathbb{1}_{x \geq 1}\left(\sum_{i=0}^{n} \mathbf{A}^{i}\right) - \mathbf{I}$ is the range connection matrix $\mathbf{B}_{1,n}$ of the source, spelled with one indicator instead of n of them. The source builds a per-length connection matrix $\mathbf{B}_{k} = \mathbb{1}_{x \geq 1}(\mathbf{A}^{k} + \mathbf{I}) - \mathbf{I}$ and then indicates their sum; the library adds the powers first, and the $\mathbf{A}^{0} = \mathbf{I}$ term the sum picks up is the term the trailing $- \mathbf{I}$ removes again. The two agree entry for entry: on the source's own six-node example the matrices are identical for every n from 1 to 5.

The budget may be a rule instead of a number

n also takes a HopCountAlgorithm or a bare Function, which resolve_separation calls as n(nte, X, g; dims = dims, kwargs...) at the point of use, g being the structure the consumer already built. A caller who cannot state the budget in advance — because the universe is a cross-validation fold or a subproblem of a meta optimiser — states the rule that produces it instead of a number that was right for one universe. HopCountQuantile is the shipped rule.

n is still an Integer once resolved, and never a Real. Three readers use 0:(nte.sep.n) as a matrix-power count, where 0:1.5 silently drops a power instead of failing, so resolve_separation checks the rule's return value rather than trusting it.

Fields

  • n: Number of steps to take in the network for deciding adjacency. An Integer is used as it stands. A HopCountAlgorithm or a Function is a rule, called as n(nte, X, g; dims = dims, kwargs...) by resolve_separation at the point of use, g being the structure the consumer already built, and must return an Integer.

Constructors

HopCount(;    n::HopCountValue = 1) -> HopCount

Keywords correspond to the struct's fields.

Validation

  • If n is an Integer, 1 <= n <= RESOURCE_LIMITS[].max_hop_count (three readers sum A^i over i in 0:n, so the compute cost is linear in n; see RESOURCE_LIMITS). A rule is checked when it is resolved, not when it is stored.

Examples

julia> HopCount()HopCount  n ┴ Int64: 1julia> HopCount(; n = HopCountQuantile())HopCount  n ┼ HopCountQuantile    │   q ┴ Float64: 0.25

Related

References

  • [4] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 13.1.2, Equations 13.1-13.2.
source
PortfolioOptimisers.PathLengthType
struct PathLength{__T_dmax} <: AbstractSeparationAlgorithm

Separation measured as the length of the shortest weighted path between two assets.

The separation between two assets is the sum of the distances along the shortest path joining them in the network, and the budget is dmax of the same units. It is the graded counterpart of HopCount: both measure how far apart two assets sit in the same structure, but one counts the edges and the other adds up how long they are.

It is a library generalisation and rests on no published source. HopCount has one — it is the range connection matrix of a walk length — but that literature counts edges throughout, and states no budget in the units a distance estimator emits.

The path runs over distances on both branches

The path is taken over the distance matrix restricted to the structure's edge setcalc_distance_weighted_graph — whichever branch built the structure. On the tree branch that is the graph's own weights; on the PMFG branch the structure is selected by similarity and then re-weighted by the distance that the similarity is a strictly decreasing function of.

Pathing over the PMFG's similarities instead is not a second convention, it is backwards: a shortest path over similarities minimises total similarity, so it prefers the route through the weakest links. It fails quietly — measured over the four similarity algorithms, the backwards answer correlates 0.95 to 0.97 with the right one, which is far too close to catch by looking.

It is not comparable with a hop count, only interchangeable with one

PathLength and HopCount satisfy the same contract, so any consumer reading a separation through that contract takes either — Proximity and phylogeny_matrix both do. Their outputs are not comparable as values: the budgets are in different units, the supports differ, and under LinearDecay the scales differ.

Both clusterise methods are the exception, and refuse PathLength at dispatch. They do not read the separation through the contract at all: they index a matrix power by sep.n, and a radius has no analogue of one.

On a real universe the two agree far more than that suggests — measured over twenty assets, rho = 0.99 on a minimum spanning tree and 0.95 to 0.98 on a PMFG, with 0.16% of pairs of pairs strictly inverted on the tree and none at all on the PMFG — because both structures are selected by distance to begin with. That agreement is empirical, not guaranteed: it is a fact about the graphs an AbstractDistanceEstimator tends to produce, not a property of either separation.

The budget

dmax = nothing is the default and means the whole connected component, implemented as the observed diameter: the largest finite entry of the separation matrix. It is the default because nobody has an intuition for a summed path in the units an AbstractDistanceEstimator emits — dmax = 0.37 is not a number a caller can reason about, whereas "look at the whole component and let the decay do the falling off" is. Choosing a number is how a caller buys fold-stability.

separation_budget clamps a chosen dmax to the observed diameter. The clamp cuts nothing — no pair sits beyond the diameter — so it is a scale-top correction and bites only LinearDecay, the one decay that reads the budget: without it, a dmax far above the diameter would flatten Z towards a constant while forbidding no pair at all.

The default reads very differently through phylogeny_matrix, which selects on the budget instead of shaping a fall-off inside it. "The whole connected component" there means every reachable pair is related, so NetworkEstimator(; sep = PathLength()) yields a matrix of ones off the diagonal — the opposite end of the dial from HopCount's default n = 1. State a numeric dmax to select anything narrower.

The budget may be a rule instead of a number

dmax also takes a PathLengthAlgorithm or a bare Function, which resolve_separation calls as dmax(nte, X, g; dims = dims, kwargs...) at the point of use, g being the structure the consumer already built. This is the answer to the paragraph above for a caller who cannot state a number: PathLengthQuantile asks for the budget that relates a stated fraction of the reachable pairs, which is a quantity a caller does have an intuition for, and which means the same thing on every fold of a cross-validation and in every subproblem of a meta optimiser.

A fixed dmax and a rule buy different things, and the difference is the whole point. A fixed dmax holds the radius still and lets the related-pair count move with the graph. A quantile rule holds the count still and lets the radius move. Neither is fold-stable in both senses at once, because the graph is refitted either way.

Fields

  • dmax: Separation budget, in the units the separation is measured in. nothing means the observed diameter of the structure. A PathLengthAlgorithm or a Function is a rule, called as dmax(nte, X, g; dims = dims, kwargs...) by resolve_separation at the point of use, g being the structure the consumer already built, and must return a Number.

Constructors

PathLength(;    dmax::Option{PathLengthValue} = nothing) -> PathLength

Keywords correspond to the struct's fields.

Validation

  • If dmax is a Number, dmax > 0. A rule is checked when it is resolved, not when it is stored.

Examples

julia> PathLength()PathLength  dmax ┴ nothingjulia> PathLength(; dmax = 0.5)PathLength  dmax ┴ Float64: 0.5julia> PathLength(; dmax = PathLengthQuantile(; q = 0.3))PathLength  dmax ┼ PathLengthQuantile       │   q ┴ Float64: 0.3

Related

source
PortfolioOptimisers.is_reachableFunction
is_reachable(sep::AbstractSeparationAlgorithm, d::Number)

Is d a separation at all, or the sentinel an unreachable pair carries?

separation_matrix passes the underlying routine's sentinel through unrepaired, so this is the test that tells a measured separation from a missing one. It is one generic method on AbstractSeparationAlgorithm rather than one per member, because the two shipped sentinels are both covered by the same expression; a member whose routine reports something else overrides this method, and inherits is_related unchanged.

The test is not isfinite alone, and not typemax alone

Both clauses carry a sentinel of their own.

  • isfinite is true for every Integer, so on its own it admits HopCount's typemax(Int) — which ReciprocalDecay then overflows.
  • typemax of a Float64 is Inf, so the comparison covers PathLength's sentinel as well; isfinite stays to reject a NaN, which no shipped path produces and which would compare false against every budget anyway.

Arguments

  • sep: Separation algorithm. Inert for the shipped members, and the dispatch channel for an extension whose routine reports a different sentinel.
  • d: One entry of a separation matrix from separation_matrix.

Returns

  • reachable::Bool: true when d is a measured separation.

Related

source
PortfolioOptimisers.is_relatedFunction
is_related(sep::AbstractSeparationAlgorithm, d::Number, dmax::Number)

Does a separation of d count as related under a budget of dmax?

The one place the budget is applied to an entry of a separation matrix: reachable, and no further than dmax. Every consumer that selects on a budget calls this instead of writing the comparison out, so the rule has one spelling — phylogeny_matrix and phylogeny_features had two, and one of them was a budget test with no sentinel test behind it.

The reachability test comes first

d <= dmax is not sufficient on its own. It happens to reject both shipped sentinels, because separation_budget clamps a PathLength budget to the observed finite diameter and a HopCount budget is capped far below typemax(Int) — but that is a property of the two shipped budgets, not of the comparison. is_reachable makes the rejection the predicate's own, so a budget that reached its unit's ceiling would still exclude an unreachable pair.

It does not remove the caller's obligation to short-circuit

A consumer that scores the separation must still keep the evaluation of the score inside a short-circuiting branch — is_related(...) ? separation_decay(...) : zero(...), never ifelse — because an ifelse evaluates both arms and ReciprocalDecay overflows 1 + d at typemax(Int), which a fractional power turns into a DomainError. The predicate owns the rule; the call site owns the laziness.

Arguments

  • sep: Separation algorithm, forwarded to is_reachable.
  • d: One entry of a separation matrix from separation_matrix.
  • dmax: Separation budget in scope, from separation_budget. In the units sep measures in, which is why the two arrive together.

Returns

  • related::Bool: true when the pair is reachable and inside the budget.

Related

source
PortfolioOptimisers.AbstractSeparationDecayAlgorithmType
abstract type AbstractSeparationDecayAlgorithm <: AbstractAlgorithm

Abstract supertype for all separation decay algorithms.

A separation decay turns a separation d >= 0 — how far apart two assets sit in whatever structure the caller is reading — into a score, and is applied by separation_decay. The family is open: a caller wanting a different fall-off defines a member and a separation_decay method for it, exactly as AbstractSimilarityMatrixAlgorithm is extended through distance_to_similarity.

d is a real separation rather than an integer hop count, so one family serves an unweighted graph — where hop counts enter as integer-valued reals — and any structure whose separation is continuous.

The contract

  • Defined for every d >= 0.
  • f(0) > 0 and maximal. Self-inclusion is load-bearing rather than cosmetic: a decay that does not put an asset at the top of its own scale silently produces a structural equivalence matrix instead of a proximity one — see PhylogenyFeatures's "Why the diagonal includes self".
  • Monotone non-increasing in d.
  • Never assumed to reach zero. Truncation is a separate knob: the consumer applies its own budget — separation_budget of the AbstractSeparationAlgorithm in scope — and the decay only shapes the fall-off inside it. An exponential never reaches zero, so budget and fall-off cannot be the same dial.
  • f(d) >= 0 for 0 <= d <= dmax. 0 is the unreachable sentinel, so a negative score inside the budget would place a reachable pair strictly below an unreachable one — an ordering inversion within the producer's own scale. It is not a claim that a signed score is wrong in general: the feature matrix is signed-tolerant by decision, and assert_metric_domain checks non-negativity per metric at the consumer rather than blanket. This clause is producer-local, and it is non-negativity rather than strict positivity because a decay that bottoms out at zero says no relatedness, which is the same claim an unreachable pair makes.

The clause is scoped to the budget because the sign outside it is unobservable — the consumer's h[u] <= n test short-circuits before the decay is ever evaluated there — and because the family's own default violates the wider statement: LinearDecay crosses zero at d = dmax + 1 and is negative above it. Binding the clause on all d >= 0 would need the max(0, ⋅) floor the budget knob exists to avoid, a second truncation biting before n does.

A zero in the resulting feature matrix therefore means functionally unreachable: either the graph is disconnected there, or the decay has fallen to nothing — the same claim about the pair, and nothing downstream can act on the difference. No shipped member emits zero anywhere inside the budget, so for what ships a zero is disconnection and nothing else.

The budget is an argument, not a field

separation_decay takes the budget in scope as its third argument, dmax, and members may ignore it — only LinearDecay reads it, to set f(0). Keeping it off the member is what makes the two knobs impossible to desync: the AbstractSeparationAlgorithm stays the single source of truth for the budget, rather than mirroring it on an algorithm that cannot see it at construction. ExponentialDecay provides the self-versus-neighbour contrast a free top-of-scale would have bought, without the hazard of a second truncation hiding inside the decay.

Enforcement

The contract is enforced rather than merely documented, by a probing assert_separation_decay fallback on this type. The shipped members satisfy it by construction and override that fallback to a no-op, so the check is opt-out: an extension that says nothing about itself gets probed.

Related

source
PortfolioOptimisers.LinearDecayType
struct LinearDecay <: AbstractSeparationDecayAlgorithm

Separation decay falling off linearly to the edge of the budget.

Mathematical definition

\[\begin{align} f(d) &= d_{\mathrm{max}} + 1 - d\,, \end{align}\]

Where:

  • $d$: Separation between two assets.
  • $d_{\mathrm{max}}$: Separation budget in scope.

The default, and the only member that reads the budget. It is the fall-off the graded neighbourhood hardcoded before the family existed, so it reproduces those values exactly: a direct neighbour scores $d_{\mathrm{max}}$, the asset itself $d_{\mathrm{max}} + 1$.

Because truncation lives with the budget rather than in the decay, no max(0, ⋅) floor is needed — on the kept range 0 <= d <= dmax the expression is strictly positive, bottoming out at 1.

Examples

julia> separation_decay.(Ref(LinearDecay()), 0:3, 3)4-element Vector{Int64}: 4 3 2 1

Related

source
PortfolioOptimisers.ExponentialDecayType
struct ExponentialDecay{__T_rate} <: AbstractSeparationDecayAlgorithm

Separation decay falling off exponentially.

Mathematical definition

\[\begin{align} f(d) &= e^{-\lambda d}\,, \end{align}\]

Where:

  • $d$: Separation between two assets.
  • $\lambda$: Rate of the fall-off, rate.

Pins f(0) = 1 and lets rate set the self-versus-neighbour contrast independently of the budget, which is what a caller wanting relatedness to drop sharply needs — the budget only says how far to look.

Parameterised by rate rather than by per-step retention. $\rho^d$ and $e^{-\lambda d}$ are the same function ($\lambda = -\log\rho$), but "retention per step" is a statement about integers, and the family's argument is a real separation. The rate form also matches ExponentialSimilarity and ExpGerberIQDecay, and needs only a one-sided bound to stay monotone.

Fields

  • rate: Rate of the exponential fall-off, exp(-rate * d). Larger values decay faster. The per-step retention form, ratio^d, is rate = -log(ratio).

Constructors

ExponentialDecay(;    rate::Number = 1.0) -> ExponentialDecay

Keywords correspond to the struct's fields.

Validation

  • rate > 0.

Examples

julia> ExponentialDecay()ExponentialDecay  rate ┴ Float64: 1.0julia> separation_decay.(Ref(ExponentialDecay()), 0:3, 3)4-element Vector{Float64}: 1.0 0.36787944117144233 0.1353352832366127 0.049787068367863944

Related

source
PortfolioOptimisers.ReciprocalDecayType
struct ReciprocalDecay{__T_power} <: AbstractSeparationDecayAlgorithm

Separation decay falling off as a power of the separation.

Mathematical definition

\[\begin{align} f(d) &= \left(1 + d\right)^{-p}\,, \end{align}\]

Where:

  • $d$: Separation between two assets.
  • $p$: Exponent of the fall-off, power.

The middle ground between LinearDecay and ExponentialDecay: heavier-tailed than the exponential, so distant assets keep a small but non-negligible score.

The 1 + is what makes classical inverse-distance weighting finite at $d = 0$; it also pins f(0) = 1, matching ExponentialDecay's scale for free. The alternative spelling $(1 + d^p)^{-1}$ is not used: it pins $f(1) = 1/2$ for every p and is non-monotone in p at fixed d, so raising the exponent would score near neighbours lower and far ones higher.

Fields

  • power: Exponent of the reciprocal fall-off, inv((1 + d)^power). Larger values decay faster.

Constructors

ReciprocalDecay(;    power::Number = 1.0) -> ReciprocalDecay

Keywords correspond to the struct's fields.

Validation

  • power > 0.

Examples

julia> ReciprocalDecay()ReciprocalDecay  power ┴ Float64: 1.0julia> separation_decay.(Ref(ReciprocalDecay()), 0:3, 3)4-element Vector{Float64}: 1.0 0.5 0.3333333333333333 0.25

Related

source
PortfolioOptimisers.NoDecayType
struct NoDecay <: AbstractSeparationDecayAlgorithm

Separation decay that does not fall off at all.

Mathematical definition

\[\begin{align} f(d) &= 1\,, \end{align}\]

Where:

  • $d$: Separation between two assets.

No decay is not no truncation

The name is about the fall-off and nothing else. The budget still cuts: a pair outside it scores 0, because truncation was never the decay's job — see AbstractSeparationDecayAlgorithm's "The budget is an argument, not a field". What comes out is therefore an indicator of the neighbourhood the budget selects, not a matrix of ones.

That is exactly what makes it useful. Under HopCount it turns Proximity into the n-hop neighbourhood indicator, which is what the retired BinaryNeighbourhood produced; under PathLength it is an ε-ball, and neither needs a type of its own once the fall-off is a knob.

It is the flat end of the family, so it is the only member that is not strictly decreasing. The contract asks for monotone non-increasing, which a constant satisfies.

Examples

julia> separation_decay.(Ref(NoDecay()), 0:3, 3)4-element Vector{Int64}: 1 1 1 1

Related

source
PortfolioOptimisers.separation_decayFunction
separation_decay(dk::LinearDecay, d::Number, dmax::Number)
separation_decay(dk::ExponentialDecay, d::Number, dmax::Number)
separation_decay(dk::ReciprocalDecay, d::Number, dmax::Number)
separation_decay(dk::NoDecay, d::Number, dmax::Number)

Score a separation under a decay algorithm.

The whole extension contract of AbstractSeparationDecayAlgorithm: a new member is a struct and one method of this function.

Arguments

  • dk: Separation decay algorithm.
  • d: Separation between two assets, d >= 0. Real rather than integral, so a weighted path length is as admissible as a hop count.
  • dmax: Separation budget in scope. Inert for members that do not need it — only LinearDecay reads it. Inert arguments have precedent here: phylogeny_features ignores its alg entirely for a partition source.

Validation

The contract is not checked here — this runs inside an assets × assets loop. Callers probe once up front with assert_separation_decay instead.

Returns

  • f::Number: Score for the separation. Non-negative for 0 <= d <= dmax; above the budget the sign is unconstrained and LinearDecay does go negative, which is harmless because the consumer's budget test short-circuits before the call.

Examples

julia> separation_decay(LinearDecay(), 2, 3)2julia> separation_decay(ExponentialDecay(; rate = 2.0), 2, 3)0.01831563888873418julia> separation_decay(ReciprocalDecay(; power = 2.0), 2, 3)0.1111111111111111julia> separation_decay(NoDecay(), 2, 3)1

Related

source
PortfolioOptimisers.assert_separation_decayFunction
assert_separation_decay(dk::AbstractSeparationDecayAlgorithm, ds, dmax::Number)
assert_separation_decay(dk::Union{<:LinearDecay, <:ExponentialDecay, <:ReciprocalDecay,
                                  <:NoDecay}, ds, dmax::Number)

Check that a separation decay honours its contract over the separations it will be asked about.

The fallback probes: it evaluates separation_decay over ds and checks the result is finite, that f(0) is strictly positive and maximal, that the values are monotone non-increasing, and that none of them is negative. The four shipped members satisfy the contract by construction and override this to a no-op, so the probe costs nothing for what ships and is fail-safe for extensions.

Probing is cheap where it is used because ds is small and the loop it guards is not: Proximity passes 0:dmax, which under HopCount is exhaustive — every separation the assets × assets loop can ever ask about, in dmax + 1 evaluations. Under a separation whose budget is not an integer the same range is a unit-spaced sample, which is all a continuum admits and all the clauses below need.

Non-negativity gets one extra evaluation at d = dmax, whether or not dmax appears in ds, mirroring the out-of-loop evaluation of f(0). That endpoint is what closes the clause over a continuum: monotonicity is already promised, so f(dmax) >= 0 implies f(d) >= 0 for every d in [0, dmax], and a ds that can only ever be a sample — as it must be once separations are weighted path lengths — costs this clause nothing. Monotonicity itself gains nothing from the endpoint and remains genuinely sampled.

Arguments

  • dk: Separation decay algorithm.
  • ds: Separations to probe. Need not be sorted. Precondition: ds ⊆ [0, dmax]ds is what the guarded loop will ask about, and the loop never asks outside the budget.
  • dmax: Separation budget in scope, forwarded to separation_decay and probed as an endpoint in its own right.

Validation

  • Every probed value is finite.
  • f(0) > 0.
  • f(0) >= f(d) for every probed d.
  • The probed values are monotone non-increasing in d.
  • f(d) >= 0 for every probed d, and at d = dmax whether or not it was probed.

Returns

  • nothing.

Related

source

References

[4]
D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025).