///|
/// A reliability demonstration test plan.
pub struct DemonstrationTestPlan {
target_reliability : Double
confidence : Double
units : Int
duration : Double
allowed_failures : Int
expected_failures : Double
total_test_hours : Double
test_cost : Double
pass_probability : Double
name : String
}
///|
pub fn demonstration_test_plan(
target_reliability : Double,
confidence : Double,
units : Int,
duration : Double,
allowed_failures : Int,
test_cost : Double,
name : String,
) -> DemonstrationTestPlan {
if target_reliability <= 0.0 ||
target_reliability >= 1.0 ||
confidence <= 0.0 ||
confidence >= 1.0 ||
units < 1 ||
duration <= 0.0 ||
allowed_failures < 0 ||
allowed_failures >= units ||
test_cost < 0.0 {
abort("invalid demonstration test plan")
}
let expected = units.to_double() * (1.0 - target_reliability)
let hours = units.to_double() * duration
let pass_probability = demonstration_pass_probability(
target_reliability, units, allowed_failures,
)
{
target_reliability,
confidence,
units,
duration,
allowed_failures,
expected_failures: expected,
total_test_hours: hours,
test_cost: hours * test_cost,
pass_probability,
name,
}
}
///|
pub fn demonstration_pass_probability(
reliability : Double,
units : Int,
allowed_failures : Int,
) -> Double {
if reliability < 0.0 ||
reliability > 1.0 ||
units < 0 ||
allowed_failures < 0 ||
allowed_failures > units {
abort("invalid binomial probability inputs")
}
let failure_probability = 1.0 - reliability
let mut total = 0.0
for failures in 0..<=allowed_failures {
total += @math.exp(
log_binomial_probability(
units, failures, failure_probability, reliability,
),
)
}
total.min(1.0).max(0.0)
}
///|
fn log_binomial_probability(
n : Int,
k : Int,
probability : Double,
complement : Double,
) -> Double {
if k == 0 {
n.to_double() * safe_log_probability(complement)
} else if k == n {
n.to_double() * safe_log_probability(probability)
} else {
log_factorial(n) -
log_factorial(k) -
log_factorial(n - k) +
k.to_double() * safe_log_probability(probability) +
(n - k).to_double() * safe_log_probability(complement)
}
}
///|
pub fn demonstration_sample_size(
target_reliability : Double,
confidence : Double,
allowed_failures : Int,
) -> Int {
if target_reliability <= 0.0 ||
target_reliability >= 1.0 ||
confidence <= 0.0 ||
confidence >= 1.0 ||
allowed_failures < 0 {
abort("invalid sample size inputs")
}
let mut units = allowed_failures + 1
while units < 100000 {
if demonstration_pass_probability(
target_reliability, units, allowed_failures,
) <=
1.0 - confidence {
return units
}
units += 1
}
100000
}
///|
pub fn zero_failure_sample_size(
target_reliability : Double,
confidence : Double,
) -> Int {
demonstration_sample_size(target_reliability, confidence, 0)
}
///|
pub fn zero_failure_test_time(
target_reliability : Double,
confidence : Double,
failure_rate : Double,
) -> Double {
if failure_rate <= 0.0 {
abort("failure rate must be positive")
}
let units = zero_failure_sample_size(target_reliability, confidence)
-@math.ln(1.0 - confidence) / failure_rate / units.to_double()
}
///|
pub fn demonstration_duration_for_units(
target_reliability : Double,
confidence : Double,
units : Int,
failure_rate : Double,
) -> Double {
if target_reliability <= 0.0 ||
target_reliability >= 1.0 ||
confidence <= 0.0 ||
confidence >= 1.0 ||
units < 1 ||
failure_rate <= 0.0 {
abort("invalid duration inputs")
}
let probability = (1.0 - confidence).max(1.0e-300)
-@math.ln(probability) / failure_rate / units.to_double()
}
///|
pub fn demonstration_plan_cost(
plan : DemonstrationTestPlan,
cost_per_hour : Double,
) -> Double {
if cost_per_hour < 0.0 {
abort("cost per hour must be non-negative")
}
plan.total_test_hours * cost_per_hour
}
///|
pub fn demonstration_plan_margin(
plan : DemonstrationTestPlan,
observed_failures : Int,
) -> Int {
plan.allowed_failures - observed_failures
}
///|
pub fn demonstration_plan_passes(
plan : DemonstrationTestPlan,
observed_failures : Int,
) -> Bool {
observed_failures >= 0 && observed_failures <= plan.allowed_failures
}
///|
pub fn demonstration_plan_evidence_fraction(
plan : DemonstrationTestPlan,
observed_hours : Double,
) -> Double {
if observed_hours < 0.0 {
abort("observed hours must be non-negative")
}
(observed_hours / plan.total_test_hours).min(1.0)
}
///|
pub fn demonstration_plan_progress(
plan : DemonstrationTestPlan,
observed_hours : Double,
observed_failures : Int,
) -> Double {
let evidence = demonstration_plan_evidence_fraction(plan, observed_hours)
let failure_margin = if plan.allowed_failures == 0 {
if observed_failures == 0 {
1.0
} else {
0.0
}
} else {
(plan.allowed_failures - observed_failures).to_double() /
plan.allowed_failures.to_double()
}
(0.7 * evidence + 0.3 * failure_margin.max(0.0).min(1.0)).min(1.0)
}
///|
pub fn demonstration_plan_status(
plan : DemonstrationTestPlan,
observed_hours : Double,
observed_failures : Int,
) -> String {
if observed_failures > plan.allowed_failures {
"failed"
} else if observed_hours >= plan.total_test_hours {
"complete"
} else {
"running"
}
}
///|
pub fn demonstration_plan_checksum(plan : DemonstrationTestPlan) -> Double {
plan.target_reliability +
plan.confidence +
plan.units.to_double() +
plan.duration +
plan.allowed_failures.to_double() +
plan.expected_failures +
plan.total_test_hours +
plan.test_cost +
plan.pass_probability
}
///|
pub struct PoissonTestPlan {
target_rate : Double
confidence : Double
exposure : Double
allowed_events : Int
event_probability : Double
cost : Double
name : String
}
///|
pub fn poisson_test_plan(
target_rate : Double,
confidence : Double,
exposure : Double,
allowed_events : Int,
cost_per_unit : Double,
name : String,
) -> PoissonTestPlan {
if target_rate <= 0.0 ||
confidence <= 0.0 ||
confidence >= 1.0 ||
exposure <= 0.0 ||
allowed_events < 0 ||
cost_per_unit < 0.0 {
abort("invalid Poisson test plan")
}
let mean_events = target_rate * exposure
{
target_rate,
confidence,
exposure,
allowed_events,
event_probability: poisson_cdf(allowed_events, mean_events),
cost: exposure * cost_per_unit,
name,
}
}
///|
pub fn poisson_probability(events : Int, mean_events : Double) -> Double {
if events < 0 || mean_events < 0.0 {
abort("invalid Poisson probability inputs")
}
@math.exp(
events.to_double() * safe_log_probability(mean_events) -
mean_events -
log_factorial(events),
)
}
///|
pub fn poisson_cdf(events : Int, mean_events : Double) -> Double {
if events < 0 || mean_events < 0.0 {
abort("invalid Poisson CDF inputs")
}
let mut total = 0.0
for k in 0..<=events {
total += poisson_probability(k, mean_events)
}
total.min(1.0)
}
///|
pub fn poisson_survival(events : Int, mean_events : Double) -> Double {
(1.0 - poisson_cdf(events, mean_events)).max(0.0)
}
///|
pub fn poisson_expected_events(plan : PoissonTestPlan) -> Double {
plan.target_rate * plan.exposure
}
///|
pub fn poisson_plan_passes(
plan : PoissonTestPlan,
observed_events : Int,
) -> Bool {
observed_events >= 0 && observed_events <= plan.allowed_events
}
///|
pub fn poisson_plan_margin(
plan : PoissonTestPlan,
observed_events : Int,
) -> Int {
plan.allowed_events - observed_events
}
///|
pub fn poisson_upper_rate_bound(
events : Int,
exposure : Double,
confidence : Double,
) -> Double {
if events < 0 || exposure <= 0.0 || confidence <= 0.0 || confidence >= 1.0 {
abort("invalid Poisson rate bound inputs")
}
let mut low = 0.0
let mut high = (events + 10).to_double() / exposure * 10.0
for _ in 0..<80 {
let middle = (low + high) / 2.0
let probability = poisson_cdf(events, middle * exposure)
if probability > 1.0 - confidence {
low = middle
} else {
high = middle
}
}
high
}
///|
pub fn poisson_lower_rate_bound(
events : Int,
exposure : Double,
confidence : Double,
) -> Double {
if events <= 0 || exposure <= 0.0 || confidence <= 0.0 || confidence >= 1.0 {
return 0.0
}
let mut low = 0.0
let mut high = events.to_double() / exposure
for _ in 0..<80 {
let middle = (low + high) / 2.0
let probability = poisson_survival(events - 1, middle * exposure)
if probability > 1.0 - confidence {
high = middle
} else {
low = middle
}
}
low
}
///|
pub struct TestCell {
cell_id : Int
stress : Double
units : Int
duration : Double
cost_per_unit_hour : Double
expected_rate : Double
failures : Int
}
///|
pub fn test_cell(
cell_id : Int,
stress : Double,
units : Int,
duration : Double,
cost_per_unit_hour : Double,
expected_rate : Double,
failures : Int,
) -> TestCell {
if cell_id < 0 ||
stress < 0.0 ||
units < 1 ||
duration <= 0.0 ||
cost_per_unit_hour < 0.0 ||
expected_rate < 0.0 ||
failures < 0 {
abort("invalid test cell")
}
{
cell_id,
stress,
units,
duration,
cost_per_unit_hour,
expected_rate,
failures,
}
}
///|
pub fn test_cell_exposure(cell : TestCell) -> Double {
cell.units.to_double() * cell.duration
}
///|
pub fn test_cell_expected_failures(cell : TestCell) -> Double {
test_cell_exposure(cell) * cell.expected_rate
}
///|
pub fn test_cell_cost(cell : TestCell) -> Double {
test_cell_exposure(cell) * cell.cost_per_unit_hour
}
///|
pub fn test_cell_failure_rate(cell : TestCell) -> Double {
if test_cell_exposure(cell) == 0.0 {
0.0
} else {
cell.failures.to_double() / test_cell_exposure(cell)
}
}
///|
pub fn test_cell_failure_residual(cell : TestCell) -> Double {
cell.failures.to_double() - test_cell_expected_failures(cell)
}
///|
pub fn test_cell_passes(cell : TestCell, maximum_failures : Int) -> Bool {
cell.failures >= 0 && cell.failures <= maximum_failures
}
///|
pub fn test_cell_stress_factor(
cell : TestCell,
baseline_stress : Double,
) -> Double {
if baseline_stress <= 0.0 {
abort("baseline stress must be positive")
}
cell.stress / baseline_stress
}
///|
pub fn test_cell_acceleration(
cell : TestCell,
baseline_stress : Double,
exponent : Double,
) -> Double {
if exponent < 0.0 {
abort("acceleration exponent must be non-negative")
}
@math.pow(test_cell_stress_factor(cell, baseline_stress), exponent)
}
///|
pub struct TestMatrix {
cells : Array[TestCell]
name : String
confidence : Double
target_rate : Double
}
///|
pub fn test_matrix(
cells : Array[TestCell],
name : String,
confidence : Double,
target_rate : Double,
) -> TestMatrix {
if cells.is_empty() ||
confidence <= 0.0 ||
confidence >= 1.0 ||
target_rate < 0.0 {
abort("invalid test matrix")
}
{ cells, name, confidence, target_rate }
}
///|
pub fn test_matrix_cell_count(matrix : TestMatrix) -> Int {
matrix.cells.length()
}
///|
pub fn test_matrix_total_units(matrix : TestMatrix) -> Int {
matrix.cells.fold(init=0, (total, cell) => total + cell.units)
}
///|
pub fn test_matrix_total_exposure(matrix : TestMatrix) -> Double {
matrix.cells.fold(init=0.0, (total, cell) => total + test_cell_exposure(cell))
}
///|
pub fn test_matrix_total_cost(matrix : TestMatrix) -> Double {
matrix.cells.fold(init=0.0, (total, cell) => total + test_cell_cost(cell))
}
///|
pub fn test_matrix_total_failures(matrix : TestMatrix) -> Int {
matrix.cells.fold(init=0, (total, cell) => total + cell.failures)
}
///|
pub fn test_matrix_expected_failures(matrix : TestMatrix) -> Double {
matrix.cells.fold(init=0.0, (total, cell) => {
total + test_cell_expected_failures(cell)
})
}
///|
pub fn test_matrix_failure_rate(matrix : TestMatrix) -> Double {
let exposure = test_matrix_total_exposure(matrix)
if exposure == 0.0 {
0.0
} else {
test_matrix_total_failures(matrix).to_double() / exposure
}
}
///|
pub fn test_matrix_rate_interval(matrix : TestMatrix) -> MetricEstimate {
let lower = poisson_lower_rate_bound(
test_matrix_total_failures(matrix),
test_matrix_total_exposure(matrix),
matrix.confidence,
)
let upper = poisson_upper_rate_bound(
test_matrix_total_failures(matrix),
test_matrix_total_exposure(matrix),
matrix.confidence,
)
metric_estimate(
estimate=test_matrix_failure_rate(matrix),
lower~,
upper~,
confidence_level=matrix.confidence,
)
}
///|
pub fn test_matrix_stress_values(matrix : TestMatrix) -> Array[Double] {
matrix.cells.map(cell => cell.stress)
}
///|
pub fn test_matrix_exposure_by_cell(matrix : TestMatrix) -> Array[Double] {
matrix.cells.map(cell => test_cell_exposure(cell))
}
///|
pub fn test_matrix_failure_rates(matrix : TestMatrix) -> Array[Double] {
matrix.cells.map(cell => test_cell_failure_rate(cell))
}
///|
pub fn test_matrix_cost_share(matrix : TestMatrix) -> Array[Double] {
let total = test_matrix_total_cost(matrix)
if total == 0.0 {
matrix.cells.map(_ => 0.0)
} else {
matrix.cells.map(cell => test_cell_cost(cell) / total)
}
}
///|
pub fn test_matrix_failure_share(matrix : TestMatrix) -> Array[Double] {
let total = test_matrix_total_failures(matrix)
if total == 0 {
matrix.cells.map(_ => 0.0)
} else {
matrix.cells.map(cell => cell.failures.to_double() / total.to_double())
}
}
///|
pub fn test_matrix_best_cell(matrix : TestMatrix) -> TestCell {
let mut best = matrix.cells[0]
for cell in matrix.cells[1:] {
if test_cell_cost(cell) < test_cell_cost(best) {
best = cell
}
}
best
}
///|
pub fn test_matrix_worst_cell(matrix : TestMatrix) -> TestCell {
let mut worst = matrix.cells[0]
for cell in matrix.cells[1:] {
if test_cell_failure_rate(cell) > test_cell_failure_rate(worst) {
worst = cell
}
}
worst
}
///|
pub fn test_matrix_compliance(
matrix : TestMatrix,
maximum_rate : Double,
) -> Bool {
test_matrix_failure_rate(matrix) <= maximum_rate
}
///|
pub fn test_matrix_progress(
matrix : TestMatrix,
planned_exposure : Double,
) -> Double {
if planned_exposure <= 0.0 {
abort("planned exposure must be positive")
}
(test_matrix_total_exposure(matrix) / planned_exposure).min(1.0)
}
///|
pub fn test_matrix_checksum(matrix : TestMatrix) -> Double {
matrix.cells.fold(init=matrix.confidence + matrix.target_rate, (total, cell) => {
total +
cell.cell_id.to_double() +
cell.stress +
cell.units.to_double() +
cell.duration +
cell.failures.to_double()
})
}
///|
pub struct SequentialTestBoundary {
accept_failures : Int
reject_failures : Int
minimum_exposure : Double
maximum_exposure : Double
target_rate : Double
adverse_rate : Double
}
///|
pub fn sequential_test_boundary(
target_rate : Double,
adverse_rate : Double,
alpha : Double,
beta : Double,
minimum_exposure : Double,
maximum_exposure : Double,
) -> SequentialTestBoundary {
if target_rate <= 0.0 ||
adverse_rate <= target_rate ||
alpha <= 0.0 ||
alpha >= 1.0 ||
beta <= 0.0 ||
beta >= 1.0 ||
minimum_exposure <= 0.0 ||
maximum_exposure <= minimum_exposure {
abort("invalid sequential test boundary")
}
{
accept_failures: @math.ln((beta / alpha).abs()).ceil().to_int().max(0),
reject_failures: @math.ln((1.0 - beta) / alpha).ceil().to_int().max(1),
minimum_exposure,
maximum_exposure,
target_rate,
adverse_rate,
}
}
///|
pub fn sequential_accepts(
boundary : SequentialTestBoundary,
failures : Int,
exposure : Double,
) -> Bool {
exposure >= boundary.minimum_exposure && failures <= boundary.accept_failures
}
///|
pub fn sequential_rejects(
boundary : SequentialTestBoundary,
failures : Int,
exposure : Double,
) -> Bool {
exposure >= boundary.minimum_exposure && failures >= boundary.reject_failures
}
///|
pub fn sequential_test_status(
boundary : SequentialTestBoundary,
failures : Int,
exposure : Double,
) -> String {
if sequential_rejects(boundary, failures, exposure) {
"reject"
} else if sequential_accepts(boundary, failures, exposure) {
"accept"
} else if exposure >= boundary.maximum_exposure {
"inconclusive"
} else {
"continue"
}
}
///|
pub fn sequential_failure_rate(
boundary : SequentialTestBoundary,
failures : Int,
exposure : Double,
) -> Double {
if boundary.target_rate < 0.0 || exposure <= 0.0 {
abort("exposure must be positive")
}
failures.to_double() / exposure
}
///|
pub fn sequential_rate_margin(
boundary : SequentialTestBoundary,
failures : Int,
exposure : Double,
) -> Double {
boundary.target_rate - sequential_failure_rate(boundary, failures, exposure)
}
///|
pub fn sequential_operating_characteristic(
boundary : SequentialTestBoundary,
true_rate : Double,
exposure : Double,
) -> Double {
if true_rate < 0.0 || exposure < 0.0 {
abort("invalid operating characteristic inputs")
}
let mean_events = true_rate * exposure
poisson_cdf(boundary.accept_failures, mean_events)
}
///|
pub struct ReliabilityTestEvidence {
planned_exposure : Double
observed_exposure : Double
planned_failures : Int
observed_failures : Int
target_rate : Double
confidence : Double
lower_rate : Double
upper_rate : Double
evidence_fraction : Double
conclusion : String
}
///|
pub fn reliability_test_evidence(
planned_exposure : Double,
observed_exposure : Double,
planned_failures : Int,
observed_failures : Int,
target_rate : Double,
confidence : Double,
) -> ReliabilityTestEvidence {
if planned_exposure <= 0.0 ||
observed_exposure < 0.0 ||
planned_failures < 0 ||
observed_failures < 0 ||
target_rate < 0.0 ||
confidence <= 0.0 ||
confidence >= 1.0 {
abort("invalid test evidence")
}
let lower = poisson_lower_rate_bound(
observed_failures,
observed_exposure.max(1.0e-12),
confidence,
)
let upper = poisson_upper_rate_bound(
observed_failures,
observed_exposure.max(1.0e-12),
confidence,
)
let conclusion = if observed_failures > planned_failures {
"failed"
} else if observed_exposure >= planned_exposure && upper <= target_rate {
"demonstrated"
} else {
"incomplete"
}
{
planned_exposure,
observed_exposure,
planned_failures,
observed_failures,
target_rate,
confidence,
lower_rate: lower,
upper_rate: upper,
evidence_fraction: (observed_exposure / planned_exposure).min(1.0),
conclusion,
}
}
///|
pub fn evidence_is_pass(evidence : ReliabilityTestEvidence) -> Bool {
evidence.conclusion == "demonstrated"
}
///|
pub fn evidence_is_complete(evidence : ReliabilityTestEvidence) -> Bool {
evidence.conclusion != "incomplete"
}
///|
pub fn evidence_remaining_exposure(
evidence : ReliabilityTestEvidence,
) -> Double {
(evidence.planned_exposure - evidence.observed_exposure).max(0.0)
}
///|
pub fn evidence_remaining_failures(evidence : ReliabilityTestEvidence) -> Int {
(evidence.planned_failures - evidence.observed_failures).max(0)
}
///|
pub fn evidence_margin(evidence : ReliabilityTestEvidence) -> Double {
evidence.target_rate - evidence.upper_rate
}
///|
pub fn evidence_quality_score(evidence : ReliabilityTestEvidence) -> Double {
let exposure_score = evidence.evidence_fraction
let rate_score = (evidence.target_rate / evidence.upper_rate.max(1.0e-300)).min(
1.0,
)
(0.5 * exposure_score + 0.5 * rate_score).min(1.0)
}
///|
pub fn evidence_checksum(evidence : ReliabilityTestEvidence) -> Double {
evidence.planned_exposure +
evidence.observed_exposure +
evidence.planned_failures.to_double() +
evidence.observed_failures.to_double() +
evidence.target_rate +
evidence.confidence +
evidence.lower_rate +
evidence.upper_rate
}
///|
pub fn confidence_from_zero_failures(
target_reliability : Double,
units : Int,
) -> Double {
if target_reliability <= 0.0 || target_reliability >= 1.0 || units < 1 {
abort("invalid zero-failure confidence inputs")
}
1.0 - @math.pow(target_reliability, units.to_double())
}
///|
pub fn reliability_lower_bound_zero_failures(
units : Int,
confidence : Double,
) -> Double {
if units < 1 || confidence <= 0.0 || confidence >= 1.0 {
abort("invalid lower bound inputs")
}
@math.pow(1.0 - confidence, 1.0 / units.to_double())
}
///|
pub fn reliability_lower_bound_failures(
units : Int,
failures : Int,
confidence : Double,
) -> Double {
if units < 1 ||
failures < 0 ||
failures > units ||
confidence <= 0.0 ||
confidence >= 1.0 {
abort("invalid reliability bound inputs")
}
let observed = (units - failures).to_double() / units.to_double()
(observed -
standard_normal_inv(0.5 + confidence / 2.0) *
(observed * (1.0 - observed) / units.to_double()).sqrt()).max(0.0)
}
///|
pub fn reliability_upper_bound_failures(
units : Int,
failures : Int,
confidence : Double,
) -> Double {
if units < 1 ||
failures < 0 ||
failures > units ||
confidence <= 0.0 ||
confidence >= 1.0 {
abort("invalid reliability bound inputs")
}
let observed = (units - failures).to_double() / units.to_double()
(observed +
standard_normal_inv(0.5 + confidence / 2.0) *
(observed * (1.0 - observed) / units.to_double()).sqrt()).min(1.0)
}
///|
pub fn test_planning_information_gain(
old_confidence : Double,
new_confidence : Double,
) -> Double {
if old_confidence <= 0.0 ||
old_confidence >= 1.0 ||
new_confidence <= 0.0 ||
new_confidence >= 1.0 {
abort("confidence values must be in (0, 1)")
}
new_confidence - old_confidence
}
///|
pub fn test_planning_cost_effectiveness(
information_gain : Double,
cost : Double,
) -> Double {
if cost < 0.0 {
abort("cost must be non-negative")
}
if cost == 0.0 {
information_gain
} else {
information_gain / cost
}
}