Distribution interface

library(dist.structure)
library(algebraic.dist)

dist_structure IS-A dist

dist_structure is a virtual S3 class that inherits from univariate_dist and dist. Every concrete constructor returns an object that participates in the full algebraic.dist distribution algebra via inheritance:

sys <- series_dist(list(
  exponential(0.5),
  exponential(0.3),
  exponential(0.2)
))
class(sys)
#> [1] "series_dist"     "coherent_dist"   "dist_structure"  "univariate_dist"
#> [5] "continuous_dist" "dist"
algebraic.dist::is_dist(sys)
#> [1] TRUE

Default dist methods

When you call surv, cdf, sampler on a dist_structure object, dispatch goes through three paths in order of specificity:

  1. Specialized closed-form methods (e.g., surv.exp_series, surv.wei_homogeneous_series). Fastest; used when available.

  2. Topology shortcut methods (e.g., surv.series_dist). Rare; most specializations land on the specific parametric class.

  3. The dist_structure default, which composes component distributions through the structure function via the reliability polynomial identity:

    S_sys(t) = R(S_1(t), S_2(t), ..., S_m(t))

    where R is the multilinear extension of phi. This is how coherent_dist(min_paths = ...) gets a surv function for any topology, even one you specify by hand.

Samplers follow the analogous composition: sample each component independently, then apply system_lifetime to reduce to a scalar.

Choosing specializations vs general constructors

dist.structure provides both general and specialized constructors for common cases. You should prefer the specialization when available:

# General: any components, arbitrary topology
sys_general <- series_dist(replicate(3, exponential(1), simplify = FALSE))

# Specialization: exploits Exp(sum(rates)) closed form
sys_special <- exp_series(c(1, 1, 1))

Both return identical distributions, but the specialization’s surv is a one-liner (exp(-total_rate * t)) while the general version computes prod(exp(-rate_j * t)) per component per t. Same math, different cost.

Verify:

for (ti in c(0.5, 1, 2)) {
  stopifnot(isTRUE(all.equal(
    algebraic.dist::surv(sys_general)(ti),
    algebraic.dist::surv(sys_special)(ti),
    tolerance = 1e-10
  )))
}

Closed-form specializations in dist.structure

Constructor Closed form
exp_series(rates) Exp(sum(rates)) exactly
wei_series(shapes, scales) exp(-sum((t/scale)^shape)) for survival
wei_homogeneous_series(shape, scales) single Weibull with aggregate scale
gamma_series(shapes, rates) product of Gamma upper tails
lognormal_series(meanlogs, sdlogs) product of Lognormal upper tails
exp_parallel(rates) 1 - prod(1 - exp(-rate*t))
exp_kofn(k, rates) subset enumeration (O(2^m))
wei_kofn(k, shapes, scales) same for Weibull components

Each has closed-form surv, cdf, sampler, and (where feasible) density. The surv / cdf / density methods registered on these subclasses dispatch before the dist_structure default.

min, max, and order statistics as dist_structure

The base algebraic.dist has min and max operators on dists that return plain dist objects. dist.structure provides structure-aware counterparts:

d <- exponential(1)

# Plain min: just a dist, no topology
d_min <- min(d, d, d)
class(d_min)
#> [1] "exponential"     "univariate_dist" "continuous_dist" "dist"
# Structure-aware: a series_dist, topology preserved
sys_min <- min_iid(d, m = 3)
class(sys_min)
#> [1] "series_dist"     "coherent_dist"   "dist_structure"  "univariate_dist"
#> [5] "continuous_dist" "dist"
dist.structure::min_paths(sys_min)
#> [[1]]
#> [1] 1 2 3

Same survival function, but the structured version lets you ask phi, min_paths, structural_importance, and so on.

Similarly:

# Parallel: max of iid
sys_max <- max_iid(d, m = 3)
class(sys_max)
#> [1] "parallel_dist"   "coherent_dist"   "dist_structure"  "univariate_dist"
#> [5] "continuous_dist" "dist"

# k-th order statistic
sys_k <- order_statistic(d, k = 2, m = 5)
class(sys_k)
#> [1] "kofn_dist"       "coherent_dist"   "dist_structure"  "univariate_dist"
#> [5] "continuous_dist" "dist"

Density and hazard

density and hazard have specialized implementations only where efficient formulas exist:

# Density of exp_series: dexp at the aggregate rate.
f <- density(exp_series(c(0.5, 0.3, 0.2)))
f(1)
#> [1] 0.3678794
dexp(1, rate = 1.0)
#> [1] 0.3678794

For general coherent_dist objects, density is typically computed via -d/dt surv(t) numerically. If you need density on a system where it matters for performance, register a specialized method or use a specialization that ships one.

The dist.structure + algebraic.dist stack

algebraic.dist                    dist, generics, arithmetic on RVs
    |
dist.structure                    dist with internal component structure
    |
specialized + general impls       exp_series, kofn_dist, coherent_dist, ...
    |
downstream packages               serieshaz, kofn, maskedcauses

Downstream packages (e.g., serieshaz’s dfr_dist_series) inherit from dist_structure and get topology + importance + composition for free. Users never need to know whether a given generic is provided by the specialized class, the dist_structure default, or the algebraic.dist base; S3 dispatch finds the most specific method and that’s the one that runs.