The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.
dist.structure is a protocol package: the closed-form
constructors (exp_series, exp_kofn, …) and
topology shortcuts (series_dist, kofn_dist,
bridge_dist, …) are reference implementations of an S3
contract that anyone can extend. Downstream packages do exactly that:
serieshaz adds a dfr_dist_series class for
arbitrary dynamic-failure-rate series systems; kofn builds
inference machinery on the closed-form k-out-of-n classes; future
packages will add their own.
This vignette walks through the protocol from the implementor’s side: what to provide, what you get for free, when to override defaults for performance, and how to validate your subclass.
Every dist_structure subclass provides:
"dist_structure", "univariate_dist", and
"dist". Almost always also a sub-chain like
"coherent_dist" if the system is coherent.ncomponents(x): number of components
m.component(x, j, ...): the j-th
component returned as an algebraic.dist::dist object with
parameters baked in. The returned dist must be independently evaluable:
surv(component(x, j))(t),
sampler(component(x, j))(n), etc., must all work.phi(x, state): the structure function, returning 0 or 1
for any binary state vector of length m.min_paths(x): a list of integer vectors enumerating
minimal functioning component subsets.phi and min_paths are dual: each has a default
that derives from the other. If you provide both, they must be
consistent (phi(x, state) == 1 iff state
covers some path in min_paths(x)). Inconsistent
implementations produce silently inconsistent results, because
reliability / critical_states /
is_coherent route through phi while
min_cuts / system_signature route through
min_paths.Everything else has a default that derives from those four
primitives. Defaults exist for min_cuts,
system_signature, critical_states,
reliability, is_coherent,
structural_importance, system_lifetime,
system_censoring, dual, surv,
cdf, and sampler.
Consider a fire alarm with m redundant smoke sensors and
one master controller. The system functions if (i) at
least k of the m sensors function AND (ii) the
master controller functions. This is not exactly a k-out-of-n system
(the master is special) and not exactly a
series-of-(parallel-of-sensors-with-master) (the master gates
everything). We can capture it directly as a custom
dist_structure.
Components are indexed 1..m for the sensors and
m+1 for the master.
alarm_dist <- function(k, sensor_components, master_component) {
m_sensors <- length(sensor_components)
stopifnot(k >= 1L, k <= m_sensors,
inherits(master_component, "dist"),
all(vapply(sensor_components,
function(d) inherits(d, "dist"),
logical(1L))))
structure(
list(
k = as.integer(k),
m_sensors = as.integer(m_sensors),
m = as.integer(m_sensors + 1L),
components = c(sensor_components, list(master_component))
),
class = c("alarm_dist", "dist_structure",
"univariate_dist", "continuous_dist", "dist")
)
}The class chain declares membership in dist_structure
and the algebraic.dist ancestors. We bundle the master into
the components list so the j-th component is uniformly
accessible.
ncomponents.alarm_dist <- function(x) x$m
component.alarm_dist <- function(x, j, ...) {
stopifnot(j >= 1L, j <= x$m)
x$components[[j]]
}
phi.alarm_dist <- function(x, state) {
stopifnot(length(state) == x$m)
sensor_state <- state[seq_len(x$m_sensors)]
master_state <- state[x$m]
as.integer(sum(sensor_state) >= x$k && master_state == 1L)
}That’s all that’s strictly required by the protocol.
sensors <- replicate(4, algebraic.dist::exponential(0.5), simplify = FALSE)
master <- algebraic.dist::exponential(0.1) # master is most reliable
alarm <- alarm_dist(k = 2L, sensors, master)
validate_dist_structure(alarm)If we had forgotten phi.alarm_dist,
validate_dist_structure() would have stopped with a clear
error pointing at the missing primitive instead of letting the failure
surface much later in some default method’s dispatch.
Every default below is now available on alarm:
# Number of components
ncomponents(alarm)
#> [1] 5
# Structure function evaluation
phi(alarm, c(1, 1, 0, 0, 1)) # 2 sensors + master alive: alarms
#> [1] 1
phi(alarm, c(1, 1, 1, 1, 0)) # all sensors but no master: silent
#> [1] 0
# Minimal cut sets (derived from phi via the Berge transversal)
min_cuts(alarm)
#> [[1]]
#> [1] 1 2 3
#>
#> [[2]]
#> [1] 1 2 4
#>
#> [[3]]
#> [1] 1 3 4
#>
#> [[4]]
#> [1] 2 3 4
#>
#> [[5]]
#> [1] 5
# Critical states for component j
critical_states(alarm, 5L) # the master: critical in every state where >= k sensors are alive
#> [,1] [,2] [,3] [,4]
#> [1,] 1 1 0 0
#> [2,] 1 0 1 0
#> [3,] 0 1 1 0
#> [4,] 1 1 1 0
#> [5,] 1 0 0 1
#> [6,] 0 1 0 1
#> [7,] 1 1 0 1
#> [8,] 0 0 1 1
#> [9,] 1 0 1 1
#> [10,] 0 1 1 1
#> [11,] 1 1 1 1
# Reliability at uniform component reliability p
reliability(alarm, 0.9)
#> [1] 0.89667
# Distribution algebra: system survival via component composition
S <- algebraic.dist::surv(alarm)
S(c(1, 5, 10))
#> [1] 0.7494236430 0.0219195370 0.0000993122
# Sampling: m independent component lifetimes, combined via system_lifetime
withr::with_seed(1, {
x <- algebraic.dist::sampler(alarm)(1000)
mean(x)
})
#> [1] 1.878051Notice that min_cuts, critical_states,
reliability, surv, and sampler
all “just work” without being implemented for alarm_dist.
They composed from ncomponents, component,
phi, and the component-level distribution algebra.
Defaults are designed for correctness across all topologies. They are not always the fastest path. The two most common reasons to override:
1. Closed-form distributional shortcuts.
The default surv.dist_structure enumerates over
component states via the reliability polynomial, which is
O(2^m). If your subclass has an analytical formula for
S_sys(t), override surv directly:
surv.alarm_dist <- function(x, ...) {
k <- x$k
m_sens <- x$m_sensors
sensor_surv <- lapply(seq_len(m_sens),
function(j) algebraic.dist::surv(component(x, j)))
master_surv <- algebraic.dist::surv(component(x, x$m))
function(t, ...) {
vapply(t, function(ti) {
# P(>= k sensors alive at ti) * P(master alive at ti).
ps <- vapply(sensor_surv, function(S) S(ti), numeric(1L))
sensor_p <- sum(vapply(seq.int(k, m_sens), function(sz) {
sum(vapply(utils::combn(m_sens, sz, simplify = FALSE),
function(A) {
prod(ps[A]) * prod(1 - ps[setdiff(seq_len(m_sens), A)])
}, numeric(1L)))
}, numeric(1L)))
sensor_p * master_surv(ti)
}, numeric(1L))
}
}2. Topology-specific shortcuts.
Closed-form min_paths saves the Berge transversal from
rederiving them. For our alarm system, every minimal path is a
(k-of-m-sensors) subset unioned with the master:
min_paths.alarm_dist <- function(x) {
k <- x$k
m_sens <- x$m_sensors
master_idx <- x$m
lapply(utils::combn(m_sens, k, simplify = FALSE),
function(P) sort(c(as.integer(P), master_idx)))
}With min_paths.alarm_dist defined,
min_cuts(alarm) becomes the default Berge transversal but
starts from a small explicit list rather than re-deriving paths from
phi (which it would do via the default chain through
binary_grid).
The general rule: provide what you know in closed form; rely on defaults for the rest. Do not pre-emptively override a default unless you have a reason; correctness across all dispatch chains is much easier when only the primitives are subclass-specific.
coherent_distIf your subclass is a coherent system, you can inherit from
coherent_dist and skip step 3 entirely:
parallel_of_master_and_sensors <- function(sensor_components, master_component) {
components <- c(sensor_components, list(master_component))
m <- length(components)
obj <- coherent_dist(
min_paths = list(seq_len(m - 1L), m), # all sensors as one path; master as another
components = components,
m = m
)
class(obj) <- c("master_or_sensors_dist", class(obj))
obj
}coherent_dist already implements
ncomponents, component, and
min_paths for you; you supply only the topology-specific
data (min_paths and components). The class chain automatically includes
"coherent_dist", so all the usual dispatch (including
dual.coherent_dist, which produces a real dual
coherent_dist instead of a lazy wrapper) takes over.
dual doesdual(x) produces the dual structure
phi_dual(state) = 1 - phi(x, 1 - state). When
x inherits from coherent_dist, the
dual.coherent_dist method swaps minimal paths and cuts to
produce a fresh coherent_dist. When x is a
generic dist_structure (your own subclass), the default
returns a lazy wrapper of class "dual_of_system". The lazy
wrapper implements ncomponents, component, and
phi by deferring to the original; everything else
(signatures, importance, surv) flows through default methods exactly as
for any other dist_structure.
If you want dual() on your subclass to return something
more specific, override:
exp_series.R, wei_kofn.R, etc.) are concrete
examples of subclasses that override surv,
cdf, sampler, density, and
hazard for speed while inheriting the topology
defaults.serieshaz::dfr_dist_series is an out-of-package
implementor that adds a fully general dynamic-failure-rate component
family while participating in the protocol.kofn::kofn is an inference layer built on top of
exp_kofn / wei_kofn; it does not subclass
dist_structure itself but uses the protocol heavily.For more detail on any specific generic, see the ?phi,
?min_paths, ?dual, ?reliability,
and ?dist_structure help pages.
These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.