Skip to contents

The Problem

Consider a server with three independent failure modes:

  • Disk — mechanical wear follows a Weibull distribution
  • Memory — random bit-flip failures follow an exponential distribution
  • Power supply — aging degradation follows a Gompertz distribution

The server fails when any component fails. This is a series system — like a chain that breaks at its weakest link. The serieshaz package lets you compose these failure modes into a single distribution that captures the full system behavior.

What Is a Series System?

Given mm independent components with hazard functions h1(t),,hm(t)h_1(t), \ldots, h_m(t), the series system hazard is simply the sum:

hsys(t)=j=1mhj(t,θj)h_{sys}(t) = \sum_{j=1}^{m} h_j(t, \theta_j)

Equivalently, the system survival is the product of component survivals:

Ssys(t)=j=1mSj(t,θj)S_{sys}(t) = \prod_{j=1}^{m} S_j(t, \theta_j)

This follows directly from component independence: the probability that the system survives to time tt is the probability that all components survive to time tt.

Quick Start

library(serieshaz)

# Define three failure mode components
disk    <- dfr_weibull(shape = 2, scale = 500)
memory  <- dfr_exponential(0.001)
psu     <- dfr_gompertz(a = 0.0001, b = 0.02)

# Compose into a series system
server <- dfr_dist_series(list(disk, memory, psu))
print(server)
#> Series system distribution with 3 components
#>   Component 1: 2 param(s) [  2, 500]
#>   Component 2: 1 param(s) [0.001]
#>   Component 3: 2 param(s) [1e-04, 2e-02]
#> System hazard: h_sys(t) = sum_j h_j(t, theta_j)
#> Survival: S_sys(t) = exp(-H_sys(t)) = prod_j S_j(t, theta_j)

The system object has 5 parameters total: 2 from Weibull + 1 from exponential + 2 from Gompertz. You can see how they map to components:

param_layout(server)
#> [[1]]
#> [1] 1 2
#> 
#> [[2]]
#> [1] 3
#> 
#> [[3]]
#> [1] 4 5

Everything from flexhaz Works

Because dfr_dist_series inherits from dfr_dist, all standard distribution methods work out of the box — no special code needed.

Hazard and Survival

h <- hazard(server)
S <- surv(server)

# Evaluate at t = 100 hours
cat(sprintf("System hazard at t=100:  %.6f\n", h(100)))
#> System hazard at t=100:  0.002539
cat(sprintf("System survival at t=100: %.4f\n", S(100)))
#> System survival at t=100: 0.8420

The hazard is low (0.0025\approx 0.0025) because the Weibull scale is 500 and the exponential rate is only 0.001. The survival probability of 84%\approx 84\% at t=100t = 100 tells us most servers are still running at 100 hours.

CDF and Density

F_sys <- cdf(server)
f_sys <- density(server)

cat(sprintf("P(T <= 100) = %.4f\n", F_sys(100)))
#> P(T <= 100) = 0.1580
cat(sprintf("f(100)      = %.6f\n", f_sys(100)))
#> f(100)      = 0.002138

The CDF and survival sum to 1: F(100)+S(100)=1F(100) + S(100) = 1. The density f(t)=h(t)S(t)f(t) = h(t) \cdot S(t) gives the instantaneous failure rate weighted by the probability of surviving to that point.

Sampling

samp <- sampler(server)
set.seed(42)
times <- samp(5)
times
#> [1] 294.9884 302.0998 150.3888 274.2224 236.8369

Log-Likelihood

ll <- loglik(server)
df <- data.frame(t = c(100, 200, 150, 300, 250), delta = c(1, 1, 0, 1, 0))
ll(df)
#> [1] -18.97089

Fitting (MLE)

Fitting a 5-parameter model (Weibull + exponential + Gompertz) from system-level data alone is challenging — the components’ hazard contributions overlap, making individual parameters hard to identify. For a cleaner demonstration, we fit a simpler 2-component model:

# Simpler 2-component model for a clean fit demo
fit_model <- dfr_dist_series(list(
    dfr_weibull(shape = 2, scale = 100),
    dfr_exponential(0.01)
))
true_par <- c(2, 100, 0.01)

set.seed(42)
fit_samp <- sampler(fit_model)
train <- data.frame(t = fit_samp(500), delta = rep(1L, 500))

solver <- fit(fit_model)
result <- suppressWarnings(solver(train, par = c(1.5, 80, 0.02)))

cat("True parameters:  ", true_par, "\n")
#> True parameters:   2 100 0.01
cat("Fitted parameters:", round(coef(result), 3), "\n")
#> Fitted parameters: 2.494 112.753 0.013

With 500 observations and a mixed-type model (Weibull + exponential), the MLE recovers the parameters reasonably well. The Weibull’s increasing hazard shape is distinguishable from the exponential’s constant hazard, making this system identifiable. See vignette("series-fitting") for a thorough treatment of identifiability, censoring, and model selection.

System Introspection

The package provides functions specifically for understanding series systems.

Component Count and Extraction

ncomponents(server)  # 3
#> [1] 3

# Extract the Weibull (disk) component
disk_comp <- component(server, 1)
params(disk_comp)
#> [1]   2 500

Component Hazard Decomposition

# Get per-component hazard closures
h1 <- component_hazard(server, 1)  # disk
h2 <- component_hazard(server, 2)  # memory
h3 <- component_hazard(server, 3)  # PSU

# At t = 200, which component contributes most?
t0 <- 200
hazards <- c(Disk = h1(t0), Memory = h2(t0), PSU = h3(t0))
cat(sprintf("Disk:   %.6f (%.1f%%)\n", hazards[1], 100 * hazards[1] / sum(hazards)))
#> Disk:   0.001600 (19.9%)
cat(sprintf("Memory: %.6f (%.1f%%)\n", hazards[2], 100 * hazards[2] / sum(hazards)))
#> Memory: 0.001000 (12.4%)
cat(sprintf("PSU:    %.6f (%.1f%%)\n", hazards[3], 100 * hazards[3] / sum(hazards)))
#> PSU:    0.005460 (67.7%)
cat(sprintf("System: %.6f\n", h(t0)))
#> System: 0.008060
cat(sprintf("Sum:    %.6f (matches system)\n", sum(hazards)))
#> Sum:    0.008060 (matches system)

At t=200t = 200, the PSU’s Gompertz degradation dominates system risk (~68%), with the disk’s Weibull wear-out contributing ~20% and the memory’s constant exponential hazard only ~12%. This decomposition is the key advantage of series system modeling: it reveals which failure mode drives system risk at any given age. Early on, the constant memory hazard is the largest contributor, but the accelerating Gompertz term overtakes it by around t=150t = 150.

Failure Attribution via Sampling

set.seed(42)
mat <- sample_components(server, n = 10000)

# System lifetime = min across components
t_sys <- apply(mat, 1, min)

# Which component caused each failure?
failing <- apply(mat, 1, which.min)
cat("Failure proportions:\n")
#> Failure proportions:
round(table(failing) / length(failing), 3)
#> failing
#>     1     2     3 
#> 0.182 0.200 0.618

The Ecosystem

serieshaz builds on three packages:

  • algebraic.dist — Base distribution generics: hazard, surv, cdf, inv_cdf, sampler, params
  • likelihood.model — Statistical inference: loglik, score, hess_loglik, fit, assumptions
  • flexhaz — DFR distributions: dfr_dist, dfr_exponential, dfr_weibull, dfr_gompertz, dfr_loglogistic

The inheritance chain is: dfr_dist_seriesdfr_distlikelihood_modelunivariate_distdist. Every method defined at any level in this chain works automatically on series systems.

Where to Go Next