Lifecycle workspace

Organize the life data before fitting the model

Reliability core: fit β and η from failure and censored life data, then read mission reliability, percentile life, and failure pattern from the Weibull curves.

Recommended data structure

Use one row per unique observation pattern. Quantity lets one row stand in for repeated identical units. Interval rows let you approximate failure discovered between two inspections by fitting at the midpoint. Segment tags can represent product family, supplier lot, design revision, or reliability-growth phase.

Scenario management

Save, reload, or move full reliability models

Comparative scenario workspace

Compare saved reliability stories side by side

Select the current draft and any saved scenarios you want to compare. The first selected scenario becomes the delta baseline.

Select at least one valid scenario to compare reliability curves, B10 life, mission reliability, and warranty exposure.

Scenario Segment β η B10 Mission reliability Expected claims Warranty status Δ mission
Save or select scenarios to activate side-by-side comparison.

Life data

Failure and censored observations

# Start life End life Status Quantity Segment Unit / lot note Actions

Analysis plots

Weibull probability plot

Failed data Censored data Fitted Weibull

Decision planning

Warranty and maintenance planning

Planning summary

Run the analysis to estimate preventive-replacement timing, warranty exposure, and the life required to hit your target reliability.

Life at target reliability - Time required to hit the target survival probability.
Suggested replacement interval - Uses an early-action rule based on pattern and B-life timing.
Expected warranty claims - Estimated failures inside the warranty window for the entered fleet size.
Warranty plan status - Compares expected claims against the allowed-claims threshold.
Warranty-window survival - Estimated unit survival through the planned warranty window.
Recommended spare pool - Central and upper-bound replacement demand translated into spare coverage.
Inspection checkpoint - Recommended review point before the risk curve steepens materially.
Candidate interval Reliability Expected claims Claims range Hazard rate Status
Run the analysis to compare candidate warranty and replacement intervals.
The comparison table will show how reliability and expected claims change as you move the interval earlier or later.
Checkpoint Life Reliability Expected failures Suggested spares
Run the analysis to build a service-checkpoint plan.

Model validation

Fit and model comparison

Recommended model

Run the analysis to compare Weibull, lognormal, and exponential fits before relying on the life estimates.

Best fit model -
Probability plot quality -
Data quality warning level -
  • Warnings and model-quality notes will appear here after the analysis runs.

Interpretation guidance

Failure pattern framing

These guidance cards translate the fitted beta shape and mission reliability into a practical lifecycle story.

Infant mortality

Use this frame when the fitted beta is below 1. This usually points to screening, workmanship, startup defects, or early-life corrective actions.

Random failure zone

Use this frame when beta is near 1. This usually suggests exposure-driven failures, steady hazard, and operational spare-parts planning.

Wear-out pattern

Use this frame when beta is above 1. This usually points to replacement timing, service intervals, and design-life commitments.

Reliability growth tracking

Segment and phase progression

Tag rows with segments such as prototype, pilot, rev B, supplier lot, or launch phase to track whether reliability is improving or regressing.

Baseline segment - First valid segment with enough failures to fit a curve.
Latest segment - Most recent valid segment in the data-entry order.
Mission reliability delta - Difference between latest and baseline segment mission survival.
Segment Units Failures β η Mission reliability Δ vs baseline Data quality
Add segment labels to life-data rows to activate reliability growth tracking.

How this tool will work

Planned workflow

1

Enter the life data

Load failure and censored units, confirm time units, and establish the mission-time question.

2

Fit the Weibull model

Estimate shape and scale, then check the probability plot and fitted curves.

3

Read B-life and reliability

Review B10, characteristic life, mean life, and mission-time reliability.

4

Translate to action

Use the interpretation layer for warranty, maintenance, redesign, or screening decisions.

SixSigmaKaizen.com · Product Lifecycle Reliability Analysis · Exported ${escapeHtml(reportDate)}

${escapeHtml(scenarioName)}

This stakeholder report summarizes the current 2-parameter Weibull life-data model for ${escapeHtml(datasetName)}, including reliability at the selected mission time, life-percentile outputs, confidence ranges, and the interpreted failure pattern.

Executive takeaway

${escapeHtml(reportNarrative)}

Key outputs

Shape (β)${escapeHtml(formatNumber(analysis.beta, 3))}
Scale (η)${escapeHtml(formatLife(analysis.eta))}
Mission reliability${escapeHtml(formatPercent(analysis.missionReliability, 1))}
Failure pattern${escapeHtml(analysis.pattern.title)}
B10 life${escapeHtml(formatLife(analysis.b10))}
B50 life${escapeHtml(formatLife(analysis.b50))}
B90 life${escapeHtml(formatLife(analysis.b90))}
Mean life${escapeHtml(formatLife(analysis.meanLife))}

Study setup

Data set${escapeHtml(datasetName)}
Time unit${escapeHtml(dom.timeUnit.value)}
Mission time${escapeHtml(formatNumber(analysis.missionTime, 0))} ${escapeHtml(unitLabel)}
Confidence level${escapeHtml(dom.confidenceLevel.value)}
Total observations${escapeHtml(formatNumber(totalUnits, 0))}
Failures${escapeHtml(formatNumber(failedUnits, 0))}
Censored${escapeHtml(formatNumber(censoredUnits, 0))}
Scenario refresh${escapeHtml(selectedScenario ? formatDateTime(selectedScenario.savedAt) : 'Current browser draft')}

Confidence framing

  • β interval: ${escapeHtml(formatNumber(confidence.beta.lower, 3))} to ${escapeHtml(formatNumber(confidence.beta.upper, 3))}
  • η interval: ${escapeHtml(formatLife(confidence.eta.lower))} to ${escapeHtml(formatLife(confidence.eta.upper))}
  • Mission reliability interval: ${escapeHtml(formatPercent(confidence.missionReliability.lower, 1))} to ${escapeHtml(formatPercent(confidence.missionReliability.upper, 1))}
  • B10 interval: ${escapeHtml(formatLife(confidence.b10.lower))} to ${escapeHtml(formatLife(confidence.b10.upper))}
  • B50 interval: ${escapeHtml(formatLife(confidence.b50.lower))} to ${escapeHtml(formatLife(confidence.b50.upper))}
  • B90 interval: ${escapeHtml(formatLife(confidence.b90.lower))} to ${escapeHtml(formatLife(confidence.b90.upper))}

Bootstrap resamples accepted: ${escapeHtml(formatNumber(confidence.resamples, 0))}. Intervals use the selected ${escapeHtml(formatNumber(confidence.confidence * 100, 0))}% confidence level.

Model validation

Recommended model${escapeHtml(bestModel.name)}
Selection strength${escapeHtml(analysis.fitDiagnostics.selectionStrength)}
Probability-plot quality${escapeHtml(analysis.fitDiagnostics.fitQuality)}
Data quality status${escapeHtml(analysis.fitDiagnostics.dataQuality)}

${escapeHtml(analysis.fitDiagnostics.recommendation)}

${modelRows}
Model AIC ΔAIC BIC Log likelihood

Warranty and maintenance planning

Target reliability${escapeHtml(formatPercent(planning.targetReliability, 1))}
Life at target${escapeHtml(formatLife(planning.targetLife))}${planning.targetLifeRange ? `
${escapeHtml(formatLife(planning.targetLifeRange.low))} to ${escapeHtml(formatLife(planning.targetLifeRange.high))}` : ''}
Replacement interval${escapeHtml(formatLife(planning.replacementInterval))}${planning.replacementIntervalRange ? `
${escapeHtml(formatLife(planning.replacementIntervalRange.low))} to ${escapeHtml(formatLife(planning.replacementIntervalRange.high))}` : ''}
Warranty plan status${escapeHtml(planning.warrantyStatus)}
Fleet size${escapeHtml(formatNumber(planning.fleetSize, 0))}
Warranty window${escapeHtml(formatNumber(planning.warrantyWindow, 0))} ${escapeHtml(unitLabel)}
Expected claims${escapeHtml(formatNumber(planning.expectedClaims, 0))}${planning.expectedClaimsRange ? `
${escapeHtml(formatNumber(planning.expectedClaimsRange.low, 0))} to ${escapeHtml(formatNumber(planning.expectedClaimsRange.high, 0))}` : ''}
Expected claim rate${escapeHtml(formatPercent(planning.expectedClaimRate, 1))}
Warranty survival${escapeHtml(formatPercent(planning.warrantyReliability, 1))}
Recommended spare pool${escapeHtml(formatNumber(planning.recommendedSparePool, 0))}
Inspection checkpoint${escapeHtml(formatLifeWithUnit(planning.inspectionCheckpoint, analysis.timeUnit))}

${escapeHtml(planning.summary)}

${escapeHtml(planning.replacementSummary)}

${planningRows}
Candidate interval Reliability Expected claims Claims range Hazard rate Status
${serviceRows}
Checkpoint Life Reliability Expected failures Suggested spares

Reliability growth tracking

${escapeHtml(analysis.growthTracking.summary)}

${growthRows || ''}
Segment Units Failures β η Mission reliability Data quality
No segment-tagged growth table available.

${escapeHtml(chartTitle)}

${dom.chartSvg.innerHTML}

${escapeHtml(dom.chartFootnote.textContent || CHART_TABS[state.activeChartTab].copy)}

Interpretation and recommended action

${escapeHtml(analysis.pattern.title)}

${escapeHtml(analysis.pattern.summary)}

${escapeHtml(analysis.pattern.guidance)}

Current decision summary

${escapeHtml(dom.decisionSummary.textContent)}

Source life data

${rowsMarkup}
# Time Status Quantity Note
`; } function exportStakeholderReport() { try { const reportHtml = buildStakeholderReportHtml(); const blob = new Blob([reportHtml], { type: 'text/html;charset=utf-8;' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); const scenarioName = String(dom.scenarioNameInput.value || '').trim() || 'reliability_report'; link.href = url; link.download = `${sanitizeIdentifier(scenarioName)}_stakeholder_report.html`; document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); clearError(); } catch (error) { showError(error.message || 'The stakeholder report could not be exported.'); } } function buildEngineeringCsv() { const analysis = state.lastAnalysis; if (!analysis) { throw new Error('Run the analysis before exporting engineering CSV.'); } const lines = [ 'section,metric,value', `summary,scenario,${JSON.stringify(analysis.scenarioName)}`, `summary,dataset,${JSON.stringify(analysis.datasetName)}`, `summary,time_unit,${JSON.stringify(analysis.timeUnit)}`, `summary,beta,${analysis.beta}`, `summary,eta,${analysis.eta}`, `summary,b10,${analysis.b10}`, `summary,b50,${analysis.b50}`, `summary,b90,${analysis.b90}`, `summary,mission_reliability,${analysis.missionReliability}`, `summary,warranty_reliability,${analysis.decisionPlanning.warrantyReliability}`, `summary,expected_claims,${analysis.decisionPlanning.expectedClaims}`, `summary,recommended_spare_pool,${analysis.decisionPlanning.recommendedSparePool}`, '', 'candidate_interval,life,reliability,expected_claims,claims_low,claims_high,hazard_rate,status,recommended' ]; analysis.decisionPlanning.candidateIntervals.forEach(candidate => { lines.push([ 'candidate_interval', candidate.interval, candidate.reliability, candidate.expectedClaims, candidate.claimsRange ? candidate.claimsRange.low : '', candidate.claimsRange ? candidate.claimsRange.high : '', candidate.hazardRate, JSON.stringify(candidate.status), candidate.isRecommended ? 'yes' : 'no' ].join(',')); }); lines.push('', 'service_checkpoint,label,life,reliability,expected_failures,suggested_spares'); analysis.decisionPlanning.serviceCheckpoints.forEach(checkpoint => { lines.push([ 'service_checkpoint', JSON.stringify(checkpoint.label), checkpoint.life, checkpoint.reliability, checkpoint.expectedFailures, checkpoint.suggestedSpares ].join(',')); }); lines.push('', 'segment_growth,segment,units,failures,beta,eta,mission_reliability,data_quality'); analysis.growthTracking.segments.forEach(segment => { lines.push([ 'segment_growth', JSON.stringify(segment.name), segment.observations, segment.failures, segment.beta ?? '', segment.eta ?? '', segment.missionReliability ?? '', JSON.stringify(segment.fitDiagnostics?.dataQuality || segment.status) ].join(',')); }); return lines.join('\n'); } function exportEngineeringCsv() { const csv = buildEngineeringCsv(); const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `${sanitizeIdentifier(String(dom.scenarioNameInput.value || 'reliability_engineering'))}_engineering.csv`; document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); } function buildComparisonReportHtml() { const records = collectScenarioComparisonAnalyses(); const validRecords = records.filter(record => record.analysis); if (!validRecords.length) { throw new Error('Select at least one valid scenario before exporting a comparison report.'); } const rows = records.map((record, index) => { if (!record.analysis) { return `${escapeHtml(record.label)}${escapeHtml(record.error || 'Scenario is not ready for comparison.')}`; } const baseline = validRecords[0].analysis; const delta = record.analysis.missionReliability - baseline.missionReliability; return ` ${escapeHtml(record.label)} ${escapeHtml(record.analysis.selectedSegment === 'all' ? 'All segments' : record.analysis.selectedSegment)} ${escapeHtml(formatNumber(record.analysis.beta, 3))} ${escapeHtml(formatLifeWithUnit(record.analysis.eta, record.analysis.timeUnit))} ${escapeHtml(formatLifeWithUnit(record.analysis.b10, record.analysis.timeUnit))} ${escapeHtml(formatPercent(record.analysis.missionReliability, 1))} ${escapeHtml(formatNumber(record.analysis.decisionPlanning.expectedClaims, 0))} ${escapeHtml(record.analysis.decisionPlanning.warrantyStatus)} ${escapeHtml(delta >= 0 ? '+' : '')}${escapeHtml(formatPercent(delta, 1))} `; }).join(''); return ` Reliability comparison report

Reliability comparison report

Baseline scenario: ${escapeHtml(validRecords[0].label)}. Generated ${escapeHtml(new Date().toLocaleString())}.

${rows}
Scenario Segment β η B10 Mission reliability Expected claims Warranty status Δ mission
`; } function exportComparisonReport() { const reportHtml = buildComparisonReportHtml(); const blob = new Blob([reportHtml], { type: 'text/html;charset=utf-8;' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `${sanitizeIdentifier(String(dom.scenarioNameInput.value || 'reliability_comparison'))}_comparison_report.html`; document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); } function renderRows() { dom.body.innerHTML = state.rows.map((row, index) => ` ${index + 1}
`).join(''); updateSegmentFilterOptions(); updatePrepSummary(); } function escapeHtml(value) { return String(value ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function updateSegmentFilterOptions() { const currentValue = dom.segmentFilter.value || 'all'; const segments = Array.from(new Set(state.rows.map(row => String(row.segment || '').trim()).filter(Boolean))); const options = [''] .concat(segments.map(segment => ``)); dom.segmentFilter.innerHTML = options.join(''); dom.segmentFilter.value = segments.includes(currentValue) || currentValue === 'all' ? currentValue : 'all'; } function updatePrepSummary() { const missionTime = Number(dom.missionTime.value) || 0; const failures = state.rows.reduce((sum, row) => sum + (row.status === 'failed' ? Number(row.quantity || 0) : 0), 0); const interval = state.rows.reduce((sum, row) => sum + (row.status === 'interval' ? Number(row.quantity || 0) : 0), 0); const censored = state.rows.reduce((sum, row) => sum + (row.status === 'censored' ? Number(row.quantity || 0) : 0), 0); dom.totalRowsPill.textContent = `Rows: ${state.rows.length}`; dom.failedPill.textContent = `Failed qty: ${failures}`; dom.censoredPill.textContent = `Censored / interval qty: ${censored + interval}`; dom.missionNote.textContent = `What is the estimated reliability of the product at ${missionTime.toLocaleString()} ${dom.timeUnit.value.toLowerCase()}, and what does that imply for warranty or preventive replacement timing?`; } function formatNumber(value, decimals = 0) { if (!Number.isFinite(value)) { return '-'; } return value.toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }); } function formatLife(value) { return formatLifeWithUnit(value, dom.timeUnit.value); } function formatLifeWithUnit(value, timeUnit) { if (!Number.isFinite(value)) { return '-'; } const absolute = Math.abs(value); let decimals = 0; if (absolute > 0 && absolute < 10) { decimals = 3; } else if (absolute < 1000) { decimals = 2; } return `${formatNumber(value, decimals)} ${String(timeUnit || 'cycles').toLowerCase()}`; } function formatPercent(value, decimals = 1) { if (!Number.isFinite(value)) { return '-'; } return `${(value * 100).toFixed(decimals)}%`; } function formatDateTime(value) { if (!value) return '-'; const date = new Date(value); return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString(); } function quantile(sortedValues, probability) { if (!sortedValues.length) { return NaN; } if (probability <= 0) return sortedValues[0]; if (probability >= 1) return sortedValues[sortedValues.length - 1]; const position = (sortedValues.length - 1) * probability; const lower = Math.floor(position); const upper = Math.ceil(position); if (lower === upper) { return sortedValues[lower]; } const weight = position - lower; return sortedValues[lower] * (1 - weight) + sortedValues[upper] * weight; } function deriveSeedFromText(text) { let hash = 2166136261; for (let index = 0; index < text.length; index += 1) { hash ^= text.charCodeAt(index); hash = Math.imul(hash, 16777619); } return (hash >>> 0) || 123456789; } function seededRandom(seed) { let stateSeed = seed >>> 0; return function () { stateSeed = (1664525 * stateSeed + 1013904223) >>> 0; return stateSeed / 4294967296; }; } function parseLifeRowsFromRows(rows) { const parsed = []; rows.forEach((row, index) => { const startTime = Number(row.time); const endTime = row.endTime === '' || row.endTime == null ? NaN : Number(row.endTime); const quantity = Math.max(1, Math.round(Number(row.quantity) || 1)); const status = row.status === 'censored' ? 'censored' : row.status === 'interval' ? 'interval' : 'failed'; if (!Number.isFinite(startTime) || startTime <= 0) { return; } if (status === 'interval') { if (!Number.isFinite(endTime) || endTime <= startTime) { throw new Error(`Row ${index + 1} is marked interval-censored but the end life is missing or not greater than the start life.`); } } parsed.push({ index, time: startTime, startTime, endTime: Number.isFinite(endTime) ? endTime : null, analysisTime: status === 'interval' ? (startTime + endTime) / 2 : startTime, status, quantity, segment: String(row.segment || '').trim() || 'Unspecified', note: String(row.note || '') }); }); return parsed; } function parseLifeRows() { return parseLifeRowsFromRows(state.rows); } function collectExpandedObservations(parsedRows) { const observations = []; parsedRows.forEach(row => { for (let count = 0; count < row.quantity; count += 1) { observations.push({ time: row.analysisTime, failed: row.status !== 'censored', status: row.status, segment: row.segment, note: row.note, startTime: row.startTime, endTime: row.endTime }); } }); return observations.sort((a, b) => a.time - b.time || (a.failed === b.failed ? 0 : a.failed ? -1 : 1)); } function getConfidenceProbabilityFromValue(value) { return Number(String(value).replace('%', '')) / 100; } function buildPlanningOptions(modelState) { return { timeUnit: modelState.timeUnit || 'Cycles', targetReliability: Number(modelState.targetReliability), fleetSize: Number(modelState.fleetSize), warrantyWindow: Number(modelState.warrantyWindow), allowedClaims: Number(modelState.allowedClaims) }; } function gammaApprox(z) { const coefficients = [ 676.5203681218851, -1259.1392167224028, 771.3234287776531, -176.6150291621406, 12.507343278686905, -0.13857109526572012, 9.984369578019572e-6, 1.5056327351493116e-7 ]; if (z < 0.5) { return Math.PI / (Math.sin(Math.PI * z) * gammaApprox(1 - z)); } let x = 0.9999999999998099; const g = 7; z -= 1; coefficients.forEach((coefficient, index) => { x += coefficient / (z + index + 1); }); const t = z + g + 0.5; return Math.sqrt(2 * Math.PI) * Math.pow(t, z + 0.5) * Math.exp(-t) * x; } function medianRank(index, total) { return (index - 0.3) / (total + 0.4); } function erfApprox(x) { const sign = x < 0 ? -1 : 1; const absolute = Math.abs(x); const a1 = 0.254829592; const a2 = -0.284496736; const a3 = 1.421413741; const a4 = -1.453152027; const a5 = 1.061405429; const p = 0.3275911; const t = 1 / (1 + p * absolute); const y = 1 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-absolute * absolute); return sign * y; } function normalCdf(value) { return 0.5 * (1 + erfApprox(value / Math.SQRT2)); } function correlation(valuesX, valuesY) { if (!Array.isArray(valuesX) || !Array.isArray(valuesY) || valuesX.length !== valuesY.length || valuesX.length < 2) { return NaN; } const meanX = valuesX.reduce((sum, value) => sum + value, 0) / valuesX.length; const meanY = valuesY.reduce((sum, value) => sum + value, 0) / valuesY.length; let numerator = 0; let denominatorX = 0; let denominatorY = 0; for (let index = 0; index < valuesX.length; index += 1) { const dx = valuesX[index] - meanX; const dy = valuesY[index] - meanY; numerator += dx * dy; denominatorX += dx * dx; denominatorY += dy * dy; } const denominator = Math.sqrt(denominatorX * denominatorY); return denominator ? numerator / denominator : NaN; } function calculateWeibullLogLikelihood(observations, beta, eta) { return observations.reduce((sum, item) => { const scaled = Math.pow(item.time / eta, beta); return sum + (item.failed ? Math.log(beta / eta) + (beta - 1) * Math.log(item.time / eta) - scaled : -scaled); }, 0); } function fitExponentialMLE(observations) { const failures = observations.filter(item => item.failed).length; if (failures < 2) { throw new Error('Enter at least two failure observations to estimate an exponential curve.'); } const totalTime = observations.reduce((sum, item) => sum + item.time, 0); const lambda = failures / totalTime; const meanLife = 1 / lambda; const logLikelihood = (failures * Math.log(lambda)) - (lambda * totalTime); return { lambda, meanLife, logLikelihood }; } function fitLognormalMLE(observations) { const failures = observations.filter(item => item.failed); if (failures.length < 2) { throw new Error('Enter at least two failure observations to estimate a lognormal curve.'); } const failureLogs = failures.map(item => Math.log(item.time)); const initialMu = failureLogs.reduce((sum, value) => sum + value, 0) / failureLogs.length; const variance = failureLogs.reduce((sum, value) => sum + Math.pow(value - initialMu, 2), 0) / Math.max(1, failureLogs.length - 1); let bestMu = initialMu; let bestLogSigma = Math.log(Math.max(Math.sqrt(Math.max(variance, 1e-6)), 0.15)); function logLikelihood(mu, logSigma) { const sigma = Math.max(Math.exp(logSigma), 1e-6); let total = 0; for (const item of observations) { const z = (Math.log(item.time) - mu) / sigma; if (item.failed) { total += -Math.log(item.time * sigma * Math.sqrt(2 * Math.PI)) - 0.5 * z * z; } else { const survival = Math.max(1 - normalCdf(z), 1e-12); total += Math.log(survival); } } return total; } let bestValue = logLikelihood(bestMu, bestLogSigma); let stepMu = Math.max(Math.sqrt(Math.max(variance, 0.04)), 0.25); let stepLogSigma = 0.35; for (let iteration = 0; iteration < 80; iteration += 1) { const candidates = [ [bestMu, bestLogSigma], [bestMu + stepMu, bestLogSigma], [bestMu - stepMu, bestLogSigma], [bestMu, bestLogSigma + stepLogSigma], [bestMu, bestLogSigma - stepLogSigma], [bestMu + stepMu, bestLogSigma + stepLogSigma], [bestMu + stepMu, bestLogSigma - stepLogSigma], [bestMu - stepMu, bestLogSigma + stepLogSigma], [bestMu - stepMu, bestLogSigma - stepLogSigma] ]; let improved = false; for (const [candidateMu, candidateLogSigma] of candidates) { const candidateValue = logLikelihood(candidateMu, candidateLogSigma); if (candidateValue > bestValue) { bestValue = candidateValue; bestMu = candidateMu; bestLogSigma = candidateLogSigma; improved = true; } } if (!improved) { stepMu *= 0.5; stepLogSigma *= 0.5; if (stepMu < 1e-4 && stepLogSigma < 1e-4) { break; } } } const sigma = Math.exp(bestLogSigma); return { mu: bestMu, sigma, medianLife: Math.exp(bestMu), meanLife: Math.exp(bestMu + (sigma * sigma) / 2), logLikelihood: bestValue }; } function buildModelComparison(observations, weibullFit) { const weibullLogLikelihood = calculateWeibullLogLikelihood(observations, weibullFit.beta, weibullFit.eta); const exponentialFit = fitExponentialMLE(observations); const lognormalFit = fitLognormalMLE(observations); const sampleSize = observations.length; const models = [ { id: 'weibull', name: 'Weibull', parameterSummary: `β ${formatNumber(weibullFit.beta, 3)} · η ${formatLife(weibullFit.eta)}`, logLikelihood: weibullLogLikelihood, parameterCount: 2 }, { id: 'lognormal', name: 'Lognormal', parameterSummary: `μ ${formatNumber(lognormalFit.mu, 3)} · σ ${formatNumber(lognormalFit.sigma, 3)}`, logLikelihood: lognormalFit.logLikelihood, parameterCount: 2 }, { id: 'exponential', name: 'Exponential', parameterSummary: `λ ${formatNumber(exponentialFit.lambda, 5)} · mean ${formatLife(exponentialFit.meanLife)}`, logLikelihood: exponentialFit.logLikelihood, parameterCount: 1 } ].map(model => ({ ...model, aic: (2 * model.parameterCount) - (2 * model.logLikelihood), bic: (Math.log(sampleSize) * model.parameterCount) - (2 * model.logLikelihood) })).sort((a, b) => a.aic - b.aic); const best = models[0]; return { models: models.map(model => ({ ...model, deltaAic: model.aic - best.aic, deltaBic: model.bic - best.bic })), bestModelId: best.id }; } function buildFitDiagnostics(observations, probabilityPoints, weibullFit, modelComparison) { const failurePoints = probabilityPoints.slice(); const xValues = failurePoints.map(point => Math.log(point.time)); const yValues = failurePoints.map(point => probabilityTransform(point.unreliability)); const probabilityCorrelation = correlation(xValues, yValues); const medianRankMae = failurePoints.length ? failurePoints.reduce((sum, point) => { const modeled = 1 - Math.exp(-Math.pow(point.time / weibullFit.eta, weibullFit.beta)); return sum + Math.abs(modeled - point.unreliability); }, 0) / failurePoints.length : NaN; const failureCount = observations.filter(item => item.failed).length; const censoredCount = observations.length - failureCount; const censoringShare = observations.length ? censoredCount / observations.length : 0; const bestModel = modelComparison.models.find(model => model.id === modelComparison.bestModelId) || modelComparison.models[0]; const runnerUp = modelComparison.models[1]; const warnings = []; if (observations.length < 8) { warnings.push('Small sample size: fewer than 8 total observations means model ranking can move materially as more life data arrives.'); } if (failureCount < 4) { warnings.push('Low failure count: fewer than 4 failures means the shape and model-comparison results should be treated as directional rather than final.'); } if (censoringShare > 0.6) { warnings.push('Heavy censoring: more than 60% of observations are censored, so tail estimates and B-life outputs are less stable.'); } if (Number.isFinite(probabilityCorrelation) && probabilityCorrelation < 0.97) { warnings.push('Probability-plot fit is loose: the failure points do not align tightly to the Weibull line, so compare alternate models before using the result for commitments.'); } if (Number.isFinite(medianRankMae) && medianRankMae > 0.08) { warnings.push('Median-rank error is elevated: observed failure ordering differs meaningfully from the fitted Weibull CDF.'); } let fitQuality = 'Strong'; if (warnings.length >= 3 || (Number.isFinite(probabilityCorrelation) && probabilityCorrelation < 0.95)) { fitQuality = 'Weak'; } else if (warnings.length >= 1 || (Number.isFinite(probabilityCorrelation) && probabilityCorrelation < 0.985)) { fitQuality = 'Moderate'; } let selectionStrength = 'close'; if (runnerUp) { if (runnerUp.deltaAic >= 6) selectionStrength = 'strong'; else if (runnerUp.deltaAic >= 2) selectionStrength = 'moderate'; } const recommendation = bestModel.id === 'weibull' ? selectionStrength === 'strong' ? 'Weibull is the recommended planning model and is clearly better than the alternatives on AIC.' : 'Weibull is the current recommended planning model, but the alternate distributions remain close enough that you should avoid over-claiming precision.' : `${bestModel.name} currently fits better than Weibull on AIC, so treat the Weibull outputs as a planning approximation rather than the strongest statistical choice.`; const dataQuality = warnings.length >= 3 ? 'High caution' : warnings.length >= 1 ? 'Review needed' : 'Decision-grade'; return { probabilityCorrelation, medianRankMae, censoringShare, warnings, fitQuality, dataQuality, recommendation, selectionStrength }; } function solveLifeAtReliability(beta, eta, targetReliability) { const safeTarget = Math.min(0.99999, Math.max(0.00001, targetReliability)); return eta * Math.pow(-Math.log(safeTarget), 1 / beta); } function classifyWarrantyStatus(expectedClaims, allowedClaims) { return expectedClaims <= allowedClaims ? 'Within target' : expectedClaims <= allowedClaims * 1.25 ? 'Borderline' : 'Above target'; } function buildDecisionPlanning(beta, eta, missionTime, missionReliability, missionUnreliability, pattern, fitDiagnostics, confidence = null, options = buildPlanningOptions(collectModelState())) { const targetReliabilityInput = Number(options.targetReliability) / 100; const targetReliability = Number.isFinite(targetReliabilityInput) && targetReliabilityInput > 0 && targetReliabilityInput < 1 ? targetReliabilityInput : 0.9; const fleetSize = Math.max(1, Math.round(Number(options.fleetSize) || 1)); const warrantyWindow = Number(options.warrantyWindow); const allowedClaims = Math.max(0, Math.round(Number(options.allowedClaims) || 0)); const safeWarrantyWindow = Number.isFinite(warrantyWindow) && warrantyWindow > 0 ? warrantyWindow : missionTime; const targetLife = solveLifeAtReliability(beta, eta, targetReliability); const warrantyReliability = Math.exp(-Math.pow(safeWarrantyWindow / eta, beta)); const expectedClaims = fleetSize * (1 - warrantyReliability); const expectedClaimRate = expectedClaims / fleetSize; const replacementRuleFactor = pattern.id === 'wearout' ? 0.85 : pattern.id === 'random' ? 0.95 : 0.7; const baseReplacementInterval = Math.min(targetLife, solveLifeAtReliability(beta, eta, Math.min(targetReliability + 0.03, 0.97))); const replacementInterval = baseReplacementInterval * replacementRuleFactor; const inspectionCheckpoint = Math.min(replacementInterval, safeWarrantyWindow) * (pattern.id === 'wearout' ? 0.8 : pattern.id === 'random' ? 0.6 : 0.45); const warrantyStatus = classifyWarrantyStatus(expectedClaims, allowedClaims); const replacementSummary = pattern.id === 'wearout' ? 'Wear-out behavior supports a planned replacement interval before the steep hazard rise.' : pattern.id === 'random' ? 'Random-failure behavior supports a lighter preventive interval and more focus on spares and protection.' : 'Infant-mortality behavior points more toward screening and burn-in than routine replacement.' ; const confidenceFlag = fitDiagnostics.dataQuality === 'High caution' ? 'Use this planning output as directional until you add more failures or reduce censoring.' : fitDiagnostics.dataQuality === 'Review needed' ? 'Use this planning output with review of the warning notes and lower confidence bounds.' : 'The current planning output is suitable for first-pass warranty and maintenance decisions.' ; const confidenceScenarios = confidence?.beta?.count ? [ { label: 'low', beta: confidence.beta.lower, eta: confidence.eta.lower }, { label: 'high', beta: confidence.beta.upper, eta: confidence.eta.upper } ] : []; const rangeMetrics = confidenceScenarios.map(scenario => { const targetLifeScenario = solveLifeAtReliability(scenario.beta, scenario.eta, targetReliability); const warrantyReliabilityScenario = Math.exp(-Math.pow(safeWarrantyWindow / scenario.eta, scenario.beta)); const expectedClaimsScenario = fleetSize * (1 - warrantyReliabilityScenario); const baseReplacementScenario = Math.min( targetLifeScenario, solveLifeAtReliability(scenario.beta, scenario.eta, Math.min(targetReliability + 0.03, 0.97)) ); return { label: scenario.label, targetLife: targetLifeScenario, replacementInterval: baseReplacementScenario * replacementRuleFactor, expectedClaims: expectedClaimsScenario, warrantyReliability: warrantyReliabilityScenario, status: classifyWarrantyStatus(expectedClaimsScenario, allowedClaims) }; }); const targetLifeRange = rangeMetrics.length ? { low: Math.min(...rangeMetrics.map(item => item.targetLife)), high: Math.max(...rangeMetrics.map(item => item.targetLife)) } : null; const replacementIntervalRange = rangeMetrics.length ? { low: Math.min(...rangeMetrics.map(item => item.replacementInterval)), high: Math.max(...rangeMetrics.map(item => item.replacementInterval)) } : null; const expectedClaimsRange = rangeMetrics.length ? { low: Math.min(...rangeMetrics.map(item => item.expectedClaims)), high: Math.max(...rangeMetrics.map(item => item.expectedClaims)) } : null; const recommendedSparePool = Math.ceil((expectedClaimsRange ? expectedClaimsRange.high : expectedClaims) * 1.1); const statusRange = rangeMetrics.length ? Array.from(new Set(rangeMetrics.map(item => item.status))) : [warrantyStatus]; const candidateFactors = [0.5, 0.75, 1, 1.25, 1.5]; const candidateIntervals = candidateFactors.map(factor => { const interval = Math.max(1e-6, replacementInterval * factor); const reliability = Math.exp(-Math.pow(interval / eta, beta)); const expectedClaimsAtInterval = fleetSize * (1 - reliability); const hazardRate = (beta / eta) * Math.pow(interval / eta, beta - 1); const status = classifyWarrantyStatus(expectedClaimsAtInterval, allowedClaims); const scenarioClaims = confidenceScenarios.map(scenario => { const scenarioReliability = Math.exp(-Math.pow(interval / scenario.eta, scenario.beta)); return fleetSize * (1 - scenarioReliability); }); const claimsRange = scenarioClaims.length ? { low: Math.min(...scenarioClaims), high: Math.max(...scenarioClaims) } : null; return { factor, interval, reliability, expectedClaims: expectedClaimsAtInterval, claimsRange, hazardRate, status, isRecommended: Math.abs(factor - 1) < 1e-9 }; }); const serviceCheckpoints = [0.25, 0.5, 0.75, 1, 1.25].map(factor => { const checkpointLife = Math.max(1e-6, safeWarrantyWindow * factor); const checkpointReliability = Math.exp(-Math.pow(checkpointLife / eta, beta)); const checkpointFailures = fleetSize * (1 - checkpointReliability); const checkpointScenarioClaims = confidenceScenarios.map(scenario => { const scenarioReliability = Math.exp(-Math.pow(checkpointLife / scenario.eta, scenario.beta)); return fleetSize * (1 - scenarioReliability); }); const checkpointUpper = checkpointScenarioClaims.length ? Math.max(...checkpointScenarioClaims) : checkpointFailures; return { factor, label: factor === 1 ? 'Warranty window' : `${formatNumber(factor * 100, 0)}% of warranty`, life: checkpointLife, reliability: checkpointReliability, expectedFailures: checkpointFailures, suggestedSpares: Math.ceil(checkpointUpper * 1.1) }; }); return { targetReliability, fleetSize, warrantyWindow: safeWarrantyWindow, allowedClaims, targetLife, replacementInterval, inspectionCheckpoint, warrantyReliability, expectedClaims, expectedClaimRate, recommendedSparePool, warrantyStatus, targetLifeRange, replacementIntervalRange, expectedClaimsRange, statusRange, candidateIntervals, serviceCheckpoints, summary: `To hold ${formatPercent(targetReliability, 1)} reliability, the fitted life target is ${formatLifeWithUnit(targetLife, options.timeUnit)}${targetLifeRange ? ` with a confidence-aware range of ${formatLifeWithUnit(targetLifeRange.low, options.timeUnit)} to ${formatLifeWithUnit(targetLifeRange.high, options.timeUnit)}` : ''}. At a warranty window of ${formatNumber(safeWarrantyWindow, 0)} ${String(options.timeUnit).toLowerCase()}, the model expects about ${formatNumber(expectedClaims, 0)} claims from ${formatNumber(fleetSize, 0)} units${expectedClaimsRange ? `, with a likely range of ${formatNumber(expectedClaimsRange.low, 0)} to ${formatNumber(expectedClaimsRange.high, 0)}` : ''}. Suggested spare coverage is ${formatNumber(recommendedSparePool, 0)} units. ${confidenceFlag}`, replacementSummary, timeUnit: options.timeUnit }; } function fitWeibullMLE(observations) { const failureCount = observations.filter(item => item.failed).length; if (failureCount < 2) { throw new Error('Enter at least two failure observations to estimate a Weibull curve.'); } const failureLogs = observations.filter(item => item.failed).map(item => Math.log(item.time)); const averageFailureLog = failureLogs.reduce((sum, value) => sum + value, 0) / failureLogs.length; function score(beta) { let a = 0; let c = 0; let b = 0; observations.forEach(item => { const ln = Math.log(item.time); const weighted = Math.exp(beta * ln); a += weighted; c += weighted * ln; b += weighted * ln * ln; }); const weightedAverageLog = c / a; const g = (1 / beta) + averageFailureLog - weightedAverageLog; const varianceTerm = (b / a) - weightedAverageLog * weightedAverageLog; const derivative = (-1 / (beta * beta)) - varianceTerm; return { g, derivative, a, c }; } let beta = 1.5; for (let iteration = 0; iteration < 40; iteration += 1) { const { g, derivative } = score(beta); const step = g / derivative; beta -= step; if (!Number.isFinite(beta) || beta <= 0) { beta = 1.5 + iteration * 0.2; } if (Math.abs(step) < 1e-8) { break; } } if (!Number.isFinite(beta) || beta <= 0) { throw new Error('The Weibull shape estimate did not converge. Recheck the life data.'); } const { a } = score(beta); const eta = Math.pow(a / failureCount, 1 / beta); if (!Number.isFinite(eta) || eta <= 0) { throw new Error('The Weibull scale estimate could not be calculated from the provided data.'); } return { beta, eta }; } function getConfidenceProbability() { return getConfidenceProbabilityFromValue(dom.confidenceLevel.value); } function bootstrapIntervals(observations, missionTime, sampleKey, confidenceValue = dom.confidenceLevel.value) { const confidence = getConfidenceProbabilityFromValue(confidenceValue); const alpha = (1 - confidence) / 2; const metrics = { beta: [], eta: [], b10: [], b50: [], b90: [], meanLife: [], missionReliability: [], hazardAtMission: [] }; const maxIterations = 120; const random = seededRandom(deriveSeedFromText(sampleKey)); let accepted = 0; for (let iteration = 0; iteration < maxIterations; iteration += 1) { const sample = Array.from({ length: observations.length }, () => observations[Math.floor(random() * observations.length)]); const failures = sample.filter(item => item.failed).length; if (failures < 2) { continue; } try { const fit = fitWeibullMLE(sample); const beta = fit.beta; const eta = fit.eta; metrics.beta.push(beta); metrics.eta.push(eta); metrics.b10.push(eta * Math.pow(-Math.log(0.9), 1 / beta)); metrics.b50.push(eta * Math.pow(-Math.log(0.5), 1 / beta)); metrics.b90.push(eta * Math.pow(-Math.log(0.1), 1 / beta)); metrics.meanLife.push(eta * gammaApprox(1 + (1 / beta))); metrics.missionReliability.push(Math.exp(-Math.pow(missionTime / eta, beta))); metrics.hazardAtMission.push((beta / eta) * Math.pow(missionTime / eta, beta - 1)); accepted += 1; } catch (error) { // skip failed resample } } function interval(values) { const sorted = values.slice().sort((a, b) => a - b); return { lower: quantile(sorted, alpha), upper: quantile(sorted, 1 - alpha), count: sorted.length }; } return { confidence, resamples: accepted, beta: interval(metrics.beta), eta: interval(metrics.eta), b10: interval(metrics.b10), b50: interval(metrics.b50), b90: interval(metrics.b90), meanLife: interval(metrics.meanLife), missionReliability: interval(metrics.missionReliability), hazardAtMission: interval(metrics.hazardAtMission) }; } function calculateProbabilityPlotPoints(observations) { const failures = observations.filter(item => item.failed); const total = observations.length; return failures.map((item, index) => { const unreliability = Math.min(0.999, Math.max(0.001, medianRank(index + 1, total))); return { time: item.time, unreliability }; }); } function classifyPattern(beta) { if (beta < 0.95) { return { id: 'infant', title: 'Infant mortality', summary: 'Early-life failures dominate and the failure rate decreases with time.', guidance: 'Strengthen screening, workmanship controls, startup checks, and burn-in discipline.' }; } if (beta <= 1.05) { return { id: 'random', title: 'Random failure zone', summary: 'Failure rate is approximately flat, suggesting exposure-driven or chance failures.', guidance: 'Focus on operating environment, protection margins, and spare-parts planning.' }; } return { id: 'wearout', title: 'Wear-out pattern', summary: 'Failure rate rises with age, pointing to degradation and end-of-life behavior.', guidance: 'Use preventive replacement, maintenance intervals, and design-life decisions.' }; } function buildSegmentAnalyses(parsedRows, missionTime, confidenceValue, planningOptions) { const groups = new Map(); parsedRows.forEach((row, index) => { const key = row.segment || 'Unspecified'; if (!groups.has(key)) { groups.set(key, { key, rows: [], order: index }); } groups.get(key).rows.push(row); }); const segments = Array.from(groups.values()) .sort((a, b) => a.order - b.order) .map(group => { const observations = collectExpandedObservations(group.rows); const failures = observations.filter(item => item.failed); if (observations.length < 3 || failures.length < 2) { return { name: group.key, observations: observations.length, failures: failures.length, status: 'Insufficient data' }; } const fit = fitWeibullMLE(observations); const missionReliability = Math.exp(-Math.pow(missionTime / fit.eta, fit.beta)); const probabilityPoints = calculateProbabilityPlotPoints(observations); const modelComparison = buildModelComparison(observations, fit); const fitDiagnostics = buildFitDiagnostics(observations, probabilityPoints, fit, modelComparison); return { name: group.key, observations: observations.length, failures: failures.length, beta: fit.beta, eta: fit.eta, missionReliability, modelComparison, fitDiagnostics, planning: buildDecisionPlanning(fit.beta, fit.eta, missionTime, missionReliability, 1 - missionReliability, classifyPattern(fit.beta), fitDiagnostics, null, planningOptions), status: 'Valid' }; }); const validSegments = segments.filter(segment => segment.status === 'Valid'); const baseline = validSegments[0] || null; const latest = validSegments[validSegments.length - 1] || null; const missionDelta = baseline && latest ? latest.missionReliability - baseline.missionReliability : NaN; const summary = baseline && latest ? `${latest.name} is ${missionDelta >= 0 ? 'above' : 'below'} ${baseline.name} by ${formatPercent(Math.abs(missionDelta), 1)} at the selected mission time.` : 'Add at least two segments with enough failures to fit separate curves and read reliability growth.'; return { segments, validSegments, baseline, latest, missionDelta, summary }; } function analyzeModelData(modelState) { const parsedRows = parseLifeRowsFromRows(Array.isArray(modelState.rows) ? modelState.rows : []); const selectedSegment = modelState.segmentFilter && modelState.segmentFilter !== 'all' ? modelState.segmentFilter : 'all'; const activeParsedRows = selectedSegment === 'all' ? parsedRows : parsedRows.filter(row => row.segment === selectedSegment); const observations = collectExpandedObservations(activeParsedRows); if (observations.length < 3) { throw new Error('Enter at least three total observations to run the analysis.'); } const failures = observations.filter(item => item.failed); const missionTime = Number(modelState.missionTime); if (!Number.isFinite(missionTime) || missionTime <= 0) { throw new Error('Enter a valid mission time greater than zero.'); } const fit = fitWeibullMLE(observations); const beta = fit.beta; const eta = fit.eta; const b10 = eta * Math.pow(-Math.log(0.9), 1 / beta); const b50 = eta * Math.pow(-Math.log(0.5), 1 / beta); const b90 = eta * Math.pow(-Math.log(0.1), 1 / beta); const meanLife = eta * gammaApprox(1 + (1 / beta)); const missionReliability = Math.exp(-Math.pow(missionTime / eta, beta)); const missionUnreliability = 1 - missionReliability; const hazardAtMission = (beta / eta) * Math.pow(missionTime / eta, beta - 1); const probabilityPoints = calculateProbabilityPlotPoints(observations); const pattern = classifyPattern(beta); const modelComparison = buildModelComparison(observations, fit); const fitDiagnostics = buildFitDiagnostics(observations, probabilityPoints, fit, modelComparison); const planningOptions = buildPlanningOptions(modelState); const confidence = bootstrapIntervals( observations, missionTime, JSON.stringify({ rows: activeParsedRows, missionTime, confidence: modelState.confidenceLevel, unit: modelState.timeUnit, selectedSegment }), modelState.confidenceLevel ); const decisionPlanning = buildDecisionPlanning(beta, eta, missionTime, missionReliability, missionUnreliability, pattern, fitDiagnostics, confidence, planningOptions); const growthTracking = buildSegmentAnalyses(parsedRows, missionTime, modelState.confidenceLevel, planningOptions); return { parsedRows: activeParsedRows, allParsedRows: parsedRows, observations, failures, beta, eta, b10, b50, b90, meanLife, missionTime, missionReliability, missionUnreliability, hazardAtMission, pattern, selectedSegment, probabilityPoints, modelComparison, fitDiagnostics, decisionPlanning, confidence, growthTracking, timeUnit: modelState.timeUnit || 'Cycles', scenarioName: modelState.scenarioName || 'Reliability model', datasetName: modelState.datasetName || 'Reliability data set' }; } function analyzeLifeData() { return analyzeModelData(collectModelState()); } function makeTicks(minValue, maxValue, count = 5) { if (!Number.isFinite(minValue) || !Number.isFinite(maxValue)) { return []; } if (maxValue <= minValue) { return [minValue]; } return Array.from({ length: count }, (_, index) => minValue + ((maxValue - minValue) * index) / (count - 1)); } function probabilityTransform(probability) { return Math.log(-Math.log(1 - probability)); } function renderProbabilityPlot(analysis) { const svg = dom.chartSvg; const width = 940; const height = 420; const margin = { top: 28, right: 24, bottom: 58, left: 72 }; const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom; const xValues = analysis.probabilityPoints.map(point => Math.log(point.time)); const yValues = analysis.probabilityPoints.map(point => probabilityTransform(point.unreliability)); const xMin = Math.min(...xValues); const xMax = Math.max(...xValues); const yMin = probabilityTransform(0.01); const yMax = probabilityTransform(0.99); const x = value => margin.left + ((value - xMin) / (xMax - xMin || 1)) * innerWidth; const y = value => margin.top + innerHeight - ((value - yMin) / (yMax - yMin || 1)) * innerHeight; const xTicks = makeTicks(xMin, xMax, 6); const probabilityTicks = [0.01, 0.05, 0.1, 0.5, 0.9, 0.99]; const fitLine = `M ${x(xMin)} ${y(betaLineY(analysis.beta, analysis.eta, Math.exp(xMin)))} L ${x(xMax)} ${y(betaLineY(analysis.beta, analysis.eta, Math.exp(xMax)))}`; const lowerLine = analysis.confidence?.beta.count ? `M ${x(xMin)} ${y(betaLineY(analysis.confidence.beta.lower, analysis.confidence.eta.lower, Math.exp(xMin)))} L ${x(xMax)} ${y(betaLineY(analysis.confidence.beta.lower, analysis.confidence.eta.lower, Math.exp(xMax)))}` : ''; const upperLine = analysis.confidence?.beta.count ? `M ${x(xMin)} ${y(betaLineY(analysis.confidence.beta.upper, analysis.confidence.eta.upper, Math.exp(xMin)))} L ${x(xMax)} ${y(betaLineY(analysis.confidence.beta.upper, analysis.confidence.eta.upper, Math.exp(xMax)))}` : ''; function betaLineY(beta, eta, time) { return beta * Math.log(time) - beta * Math.log(eta); } svg.innerHTML = ` ${xTicks.map(tick => ` ${formatNumber(Math.exp(tick), 0)} `).join('')} ${probabilityTicks.map(probability => ` ${formatPercent(probability, 0)} `).join('')} ${lowerLine ? `` : ''} ${upperLine ? `` : ''} ${analysis.probabilityPoints.map(point => ` `).join('')} ${analysis.observations.filter(item => !item.failed).map(item => ` `).join('')} Life time (${escapeHtml(dom.timeUnit.value)}) Unreliability F(t) `; } function renderFunctionCurve(analysis, curveType) { const svg = dom.chartSvg; const width = 940; const height = 420; const margin = { top: 28, right: 24, bottom: 58, left: 72 }; const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom; const maxObserved = Math.max(...analysis.observations.map(item => item.time), analysis.missionTime); const xMin = Math.max(maxObserved * 0.01, 1e-6); const xMax = maxObserved * 1.2; const points = Array.from({ length: 80 }, (_, index) => { const time = xMin + (xMax - xMin) * (index / 79); const safeTime = Math.max(time, 1e-6); let value = 0; if (curveType === 'reliability') { value = Math.exp(-Math.pow(safeTime / analysis.eta, analysis.beta)); } else if (curveType === 'cdf') { value = 1 - Math.exp(-Math.pow(safeTime / analysis.eta, analysis.beta)); } else { value = (analysis.beta / analysis.eta) * Math.pow(safeTime / analysis.eta, analysis.beta - 1); } return { time, value }; }); const maxY = curveType === 'hazard' ? Math.max(...points.map(point => point.value)) * 1.1 : 1; const x = value => margin.left + ((value - xMin) / (xMax - xMin || 1)) * innerWidth; const y = value => margin.top + innerHeight - ((value - 0) / (maxY || 1)) * innerHeight; const yTicks = makeTicks(0, maxY, 5); const xTicks = makeTicks(xMin, xMax, 6); const path = points.map((point, index) => `${index === 0 ? 'M' : 'L'} ${x(point.time)} ${y(point.value)}`).join(' '); const lowerCurve = analysis.confidence?.beta.count ? points.map((point, index) => { const beta = analysis.confidence.beta.lower; const eta = analysis.confidence.eta.lower; let value = 0; if (curveType === 'reliability') value = Math.exp(-Math.pow(Math.max(point.time,1e-6) / eta, beta)); else if (curveType === 'cdf') value = 1 - Math.exp(-Math.pow(Math.max(point.time,1e-6) / eta, beta)); else value = (beta / eta) * Math.pow(Math.max(point.time,1e-6) / eta, beta - 1); return `${index === 0 ? 'M' : 'L'} ${x(point.time)} ${y(value)}`; }).join(' ') : ''; const upperCurve = analysis.confidence?.beta.count ? points.map((point, index) => { const beta = analysis.confidence.beta.upper; const eta = analysis.confidence.eta.upper; let value = 0; if (curveType === 'reliability') value = Math.exp(-Math.pow(Math.max(point.time,1e-6) / eta, beta)); else if (curveType === 'cdf') value = 1 - Math.exp(-Math.pow(Math.max(point.time,1e-6) / eta, beta)); else value = (beta / eta) * Math.pow(Math.max(point.time,1e-6) / eta, beta - 1); return `${index === 0 ? 'M' : 'L'} ${x(point.time)} ${y(value)}`; }).join(' ') : ''; const missionValue = curveType === 'reliability' ? analysis.missionReliability : curveType === 'cdf' ? analysis.missionUnreliability : analysis.hazardAtMission; svg.innerHTML = ` ${xTicks.map(tick => ` ${formatNumber(tick, 0)} `).join('')} ${yTicks.map(tick => ` ${curveType === 'hazard' ? formatNumber(tick, 4) : formatPercent(tick, 0)} `).join('')} ${lowerCurve ? `` : ''} ${upperCurve ? `` : ''} Mission Life time (${escapeHtml(dom.timeUnit.value)}) ${curveType === 'reliability' ? 'Reliability R(t)' : curveType === 'cdf' ? 'Unreliability F(t)' : 'Hazard rate h(t)'} `; } function renderChart(analysis) { const tab = state.activeChartTab; dom.chartHeading.textContent = CHART_TABS[tab].title; dom.chartCopy.textContent = CHART_TABS[tab].copy; if (tab === 'probability') { renderProbabilityPlot(analysis); } else { renderFunctionCurve(analysis, tab); } const detailItems = [ `Failure observations: ${analysis.failures.length}`, `Total units represented: ${analysis.observations.length}`, `Beta (β): ${formatNumber(analysis.beta, 3)}`, `Eta (η): ${formatLife(analysis.eta)}`, `Mission reliability: ${formatPercent(analysis.missionReliability, 1)}`, `Best AIC model: ${(analysis.modelComparison.models.find(model => model.id === analysis.modelComparison.bestModelId) || {}).name || 'Weibull'}`, `Probability-plot quality: ${analysis.fitDiagnostics.fitQuality}`, `Selected confidence level: ${dom.confidenceLevel.value}`, `Bootstrap resamples accepted: ${analysis.confidence.resamples}` ]; dom.chartDetailList.innerHTML = detailItems.map(item => `
  • ${escapeHtml(item)}
  • `).join(''); dom.chartFootnote.textContent = tab === 'hazard' ? `At mission time, the modeled hazard rate is ${formatNumber(analysis.hazardAtMission, 5)} per ${dom.timeUnit.value.toLowerCase()} with dashed bootstrap confidence bounds.` : `The ${CHART_TABS[tab].title.toLowerCase()} is based on the current 2-parameter Weibull fit with dashed bootstrap confidence bounds.`; } function renderModelValidation(analysis) { const bestModel = analysis.modelComparison.models.find(model => model.id === analysis.modelComparison.bestModelId) || analysis.modelComparison.models[0]; dom.bestFitModelOutput.textContent = bestModel ? bestModel.name : '-'; dom.fitQualityOutput.textContent = analysis.fitDiagnostics.fitQuality; dom.dataQualityOutput.textContent = analysis.fitDiagnostics.dataQuality; dom.modelRecommendationSummary.textContent = `${analysis.fitDiagnostics.recommendation} Probability-plot quality is ${analysis.fitDiagnostics.fitQuality.toLowerCase()} and the current data-quality status is ${analysis.fitDiagnostics.dataQuality.toLowerCase()}.`; const warnings = analysis.fitDiagnostics.warnings.length ? analysis.fitDiagnostics.warnings : ['No major fit warnings were triggered from sample size, censoring share, or Weibull probability-plot alignment.']; dom.modelWarningList.innerHTML = warnings.map(item => `
  • ${escapeHtml(item)}
  • `).join(''); dom.modelComparisonGrid.innerHTML = analysis.modelComparison.models.map(model => `
    ${model.id === analysis.modelComparison.bestModelId ? 'Recommended' : model.deltaAic <= 2 ? 'Close alternative' : 'Comparison model'}

    ${escapeHtml(model.name)}

    ${escapeHtml(model.parameterSummary)}

    AIC ${escapeHtml(formatNumber(model.aic, 2))}
    ΔAIC ${escapeHtml(formatNumber(model.deltaAic, 2))}
    BIC ${escapeHtml(formatNumber(model.bic, 2))}
    Log likelihood ${escapeHtml(formatNumber(model.logLikelihood, 2))}
    `).join(''); } function renderDecisionPlanning(analysis) { const plan = analysis.decisionPlanning; dom.planningSummary.textContent = plan.summary; dom.targetLifeOutput.textContent = formatLife(plan.targetLife); dom.targetLifeNote.textContent = plan.targetLifeRange ? `${formatPercent(plan.targetReliability, 1)} target reliability corresponds to this fitted life estimate, with a likely range of ${formatLife(plan.targetLifeRange.low)} to ${formatLife(plan.targetLifeRange.high)}.` : `${formatPercent(plan.targetReliability, 1)} target reliability corresponds to this fitted life estimate.`; dom.replacementIntervalOutput.textContent = formatLife(plan.replacementInterval); dom.replacementIntervalNote.textContent = `${plan.replacementSummary}${plan.replacementIntervalRange ? ` Confidence-aware interval range: ${formatLife(plan.replacementIntervalRange.low)} to ${formatLife(plan.replacementIntervalRange.high)}.` : ''}`; dom.expectedClaimsOutput.textContent = formatNumber(plan.expectedClaims, 0); dom.expectedClaimsNote.textContent = `${formatPercent(plan.expectedClaimRate, 1)} expected claims inside ${formatNumber(plan.warrantyWindow, 0)} ${dom.timeUnit.value.toLowerCase()} from ${formatNumber(plan.fleetSize, 0)} units.${plan.expectedClaimsRange ? ` Confidence-aware claim range: ${formatNumber(plan.expectedClaimsRange.low, 0)} to ${formatNumber(plan.expectedClaimsRange.high, 0)}.` : ''}`; dom.warrantyStatusOutput.textContent = plan.warrantyStatus; dom.warrantyReliabilityOutput.textContent = formatPercent(plan.warrantyReliability, 1); dom.warrantyReliabilityNote.textContent = `Modeled survival through ${formatNumber(plan.warrantyWindow, 0)} ${plan.timeUnit.toLowerCase()}.`; dom.sparePoolOutput.textContent = formatNumber(plan.recommendedSparePool, 0); dom.sparePoolNote.textContent = plan.expectedClaimsRange ? `Built from the upper planning claim range of ${formatNumber(plan.expectedClaimsRange.high, 0)} units with 10% buffer.` : 'Built from central expected claims with a 10% service buffer.'; dom.inspectionCheckpointOutput.textContent = formatLifeWithUnit(plan.inspectionCheckpoint, plan.timeUnit); dom.inspectionCheckpointNote.textContent = `Review incoming field behavior before ${formatLifeWithUnit(plan.inspectionCheckpoint, plan.timeUnit)} to catch drift before the recommended replacement point.`; const baseStatusNote = plan.warrantyStatus === 'Within target' ? `Expected claims are within the allowed threshold of ${formatNumber(plan.allowedClaims, 0)} units.` : plan.warrantyStatus === 'Borderline' ? `Expected claims are close to the allowed threshold of ${formatNumber(plan.allowedClaims, 0)} units.` : `Expected claims exceed the allowed threshold of ${formatNumber(plan.allowedClaims, 0)} units.`; dom.warrantyStatusNote.textContent = `${baseStatusNote}${plan.statusRange.length > 1 ? ` Confidence-aware statuses range from ${plan.statusRange.join(' to ')} depending on the fitted bounds.` : ''}`; dom.decisionRangeBody.innerHTML = plan.candidateIntervals.map(candidate => ` ${formatLife(candidate.interval)}${candidate.isRecommended ? ' (recommended)' : ''} ${escapeHtml(formatPercent(candidate.reliability, 1))} ${escapeHtml(formatNumber(candidate.expectedClaims, 0))} ${candidate.claimsRange ? `${escapeHtml(formatNumber(candidate.claimsRange.low, 0))} to ${escapeHtml(formatNumber(candidate.claimsRange.high, 0))}` : '-'} ${escapeHtml(formatNumber(candidate.hazardRate, 5))} ${escapeHtml(candidate.status)} `).join(''); dom.decisionRangeNote.textContent = `These candidate intervals are centered on the suggested replacement timing and show the reliability, central expected claims, confidence-aware claim range, and hazard tradeoff if you move earlier or later.`; dom.serviceCheckpointBody.innerHTML = plan.serviceCheckpoints.map(checkpoint => ` ${escapeHtml(checkpoint.label)} ${escapeHtml(formatLifeWithUnit(checkpoint.life, plan.timeUnit))} ${escapeHtml(formatPercent(checkpoint.reliability, 1))} ${escapeHtml(formatNumber(checkpoint.expectedFailures, 0))} ${escapeHtml(formatNumber(checkpoint.suggestedSpares, 0))} `).join(''); } function collectScenarioComparisonAnalyses() { const selectedIds = state.compareScenarioIds.length ? state.compareScenarioIds : ['__current__']; return selectedIds.map(id => { if (id === '__current__') { try { return { id, label: `${String(dom.scenarioNameInput.value || '').trim() || 'Current draft'} (current)`, savedAt: 'Live draft', analysis: analyzeModelData(collectModelState()) }; } catch (error) { return { id, label: `${String(dom.scenarioNameInput.value || '').trim() || 'Current draft'} (current)`, error: error.message || 'Current draft is not ready for comparison.' }; } } const scenario = state.savedScenarios.find(entry => entry.id === id); if (!scenario) { return null; } try { return { id, label: scenario.name, savedAt: scenario.savedAt, analysis: analyzeModelData(scenario.model) }; } catch (error) { return { id, label: scenario.name, savedAt: scenario.savedAt, error: error.message || 'Scenario could not be analyzed.' }; } }).filter(Boolean); } function renderComparisonChart(records) { const svg = dom.comparisonChart; const validRecords = records.filter(record => record.analysis); if (!validRecords.length) { svg.innerHTML = ''; dom.comparisonLegend.innerHTML = ''; return; } const width = 940; const height = 420; const margin = { top: 28, right: 24, bottom: 58, left: 72 }; const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom; const maxObserved = Math.max(...validRecords.map(record => Math.max(...record.analysis.observations.map(item => item.time), record.analysis.missionTime))); const xMin = Math.max(maxObserved * 0.01, 1e-6); const xMax = maxObserved * 1.2; const x = value => margin.left + ((value - xMin) / (xMax - xMin || 1)) * innerWidth; const y = value => margin.top + innerHeight - value * innerHeight; const xTicks = makeTicks(xMin, xMax, 6); const yTicks = makeTicks(0, 1, 5); svg.innerHTML = ` ${xTicks.map(tick => ` ${formatNumber(tick, 0)} `).join('')} ${yTicks.map(tick => ` ${formatPercent(tick, 0)} `).join('')} ${validRecords.map((record, index) => { const color = COMPARISON_COLORS[index % COMPARISON_COLORS.length]; const points = Array.from({ length: 80 }, (_, pointIndex) => { const time = xMin + (xMax - xMin) * (pointIndex / 79); const value = Math.exp(-Math.pow(Math.max(time, 1e-6) / record.analysis.eta, record.analysis.beta)); return `${pointIndex === 0 ? 'M' : 'L'} ${x(time)} ${y(value)}`; }).join(' '); return ``; }).join('')} Life time (${escapeHtml(validRecords[0].analysis.timeUnit)}) Reliability R(t) `; dom.comparisonLegend.innerHTML = validRecords.map((record, index) => ` ${escapeHtml(record.label)} `).join(''); } function renderScenarioComparison() { const records = collectScenarioComparisonAnalyses(); const validRecords = records.filter(record => record.analysis); const baseline = validRecords[0]?.analysis || null; renderComparisonChart(records); if (!records.length) { dom.comparisonSummary.textContent = 'Save or select scenarios to activate side-by-side comparison.'; dom.comparisonTableBody.innerHTML = 'Save or select scenarios to activate side-by-side comparison.'; return; } dom.comparisonSummary.textContent = validRecords.length ? `Comparing ${validRecords.length} valid scenario${validRecords.length === 1 ? '' : 's'}. Baseline deltas are measured against ${validRecords[0].label}.` : 'Selected scenarios are not yet valid enough to compare.'; dom.comparisonTableBody.innerHTML = records.map(record => { if (!record.analysis) { return `${escapeHtml(record.label)}${escapeHtml(record.error || 'Scenario is not ready for comparison.')}`; } const analysis = record.analysis; const deltaMission = baseline ? analysis.missionReliability - baseline.missionReliability : 0; return ` ${escapeHtml(record.label)} ${escapeHtml(analysis.selectedSegment === 'all' ? 'All segments' : analysis.selectedSegment)} ${escapeHtml(formatNumber(analysis.beta, 3))} ${escapeHtml(formatLifeWithUnit(analysis.eta, analysis.timeUnit))} ${escapeHtml(formatLifeWithUnit(analysis.b10, analysis.timeUnit))} ${escapeHtml(formatPercent(analysis.missionReliability, 1))} ${escapeHtml(formatNumber(analysis.decisionPlanning.expectedClaims, 0))} ${escapeHtml(analysis.decisionPlanning.warrantyStatus)} ${baseline ? `${deltaMission >= 0 ? '+' : ''}${formatPercent(deltaMission, 1)}` : '-'} `; }).join(''); } function renderCompareScenarioList() { const options = [ { id: '__current__', title: `${String(dom.scenarioNameInput.value || '').trim() || 'Current draft'} (current)`, note: `${dom.datasetName.value || 'Current draft data set'} · ${dom.timeUnit.value}` }, ...state.savedScenarios.map(scenario => ({ id: scenario.id, title: scenario.name, note: `Saved ${formatDateTime(scenario.savedAt)}` })) ]; dom.compareScenarioList.innerHTML = options.map(option => `
    `).join(''); renderScenarioComparison(); } function renderGrowthTracking(analysis) { const growth = analysis.growthTracking; dom.growthSummary.textContent = growth.summary; dom.growthBaselineOutput.textContent = growth.baseline ? growth.baseline.name : '-'; dom.growthBaselineNote.textContent = growth.baseline ? `${formatPercent(growth.baseline.missionReliability, 1)} mission reliability with β ${formatNumber(growth.baseline.beta, 3)}.` : 'First valid segment with enough failures to fit a curve.'; dom.growthLatestOutput.textContent = growth.latest ? growth.latest.name : '-'; dom.growthLatestNote.textContent = growth.latest ? `${formatPercent(growth.latest.missionReliability, 1)} mission reliability with β ${formatNumber(growth.latest.beta, 3)}.` : 'Most recent valid segment in the data-entry order.'; dom.growthDeltaOutput.textContent = Number.isFinite(growth.missionDelta) ? `${growth.missionDelta >= 0 ? '+' : ''}${formatPercent(growth.missionDelta, 1)}` : '-'; dom.growthDeltaNote.textContent = Number.isFinite(growth.missionDelta) ? `${growth.missionDelta >= 0 ? 'Improvement' : 'Regression'} between latest and baseline segment mission survival.` : 'Difference between latest and baseline segment mission survival.'; dom.growthTableBody.innerHTML = growth.segments.length ? growth.segments.map(segment => { if (segment.status !== 'Valid') { return ` ${escapeHtml(segment.name)} ${escapeHtml(formatNumber(segment.observations, 0))} ${escapeHtml(formatNumber(segment.failures, 0))} Need at least 3 units and 2 failures to fit this segment. ${escapeHtml(segment.status)} `; } const delta = growth.baseline ? segment.missionReliability - growth.baseline.missionReliability : NaN; return ` ${escapeHtml(segment.name)} ${escapeHtml(formatNumber(segment.observations, 0))} ${escapeHtml(formatNumber(segment.failures, 0))} ${escapeHtml(formatNumber(segment.beta, 3))} ${escapeHtml(formatLifeWithUnit(segment.eta, analysis.timeUnit))} ${escapeHtml(formatPercent(segment.missionReliability, 1))} ${Number.isFinite(delta) ? `${delta >= 0 ? '+' : ''}${escapeHtml(formatPercent(delta, 1))}` : '-'} ${escapeHtml(segment.fitDiagnostics.dataQuality)} `; }).join('') : 'Add segment labels to life-data rows to activate reliability growth tracking.'; } function resetOutputs() { state.lastAnalysis = null; dom.shapeOutput.textContent = '-'; dom.scaleOutput.textContent = '-'; dom.b10Output.textContent = '-'; dom.meanLifeOutput.textContent = '-'; dom.missionReliabilityOutput.textContent = '-'; dom.patternOutput.textContent = '-'; dom.shapeNote.textContent = 'Shape of the failure pattern.'; dom.scaleNote.textContent = '63.2% cumulative-failure point.'; dom.b10Note.textContent = '10% cumulative-failure point.'; dom.meanLifeNote.textContent = 'Expected life across the fitted curve.'; dom.missionReliabilityNote.textContent = 'Survival probability at the selected mission time.'; dom.patternNote.textContent = 'Interpreted from the fitted beta shape.'; dom.interpretSummary.textContent = 'These guidance cards translate the fitted beta shape and mission reliability into a practical lifecycle story.'; dom.decisionSummary.textContent = 'Run the analysis to generate percentile-life decisions, warranty risk framing, and confidence ranges for the key outputs.'; dom.b50Pill.textContent = 'B50: -'; dom.b90Pill.textContent = 'B90: -'; dom.warrantyPill.textContent = 'Warranty risk: -'; dom.chartDetailList.innerHTML = ''; dom.chartSvg.innerHTML = ''; dom.chartFootnote.textContent = 'Run the analysis after editing the life data to refresh the chart and detail summary.'; dom.bestFitModelOutput.textContent = '-'; dom.fitQualityOutput.textContent = '-'; dom.dataQualityOutput.textContent = '-'; dom.modelRecommendationSummary.textContent = 'Run the analysis to compare Weibull, lognormal, and exponential fits before relying on the life estimates.'; dom.modelWarningList.innerHTML = '
  • Warnings and model-quality notes will appear here after the analysis runs.
  • '; dom.modelComparisonGrid.innerHTML = ''; dom.planningSummary.textContent = 'Run the analysis to estimate preventive-replacement timing, warranty exposure, and the life required to hit your target reliability.'; dom.targetLifeOutput.textContent = '-'; dom.targetLifeNote.textContent = 'Time required to hit the target survival probability.'; dom.replacementIntervalOutput.textContent = '-'; dom.replacementIntervalNote.textContent = 'Uses an early-action rule based on pattern and B-life timing.'; dom.expectedClaimsOutput.textContent = '-'; dom.expectedClaimsNote.textContent = 'Estimated failures inside the warranty window for the entered fleet size.'; dom.warrantyStatusOutput.textContent = '-'; dom.warrantyStatusNote.textContent = 'Compares expected claims against the allowed-claims threshold.'; dom.warrantyReliabilityOutput.textContent = '-'; dom.warrantyReliabilityNote.textContent = 'Estimated unit survival through the planned warranty window.'; dom.sparePoolOutput.textContent = '-'; dom.sparePoolNote.textContent = 'Central and upper-bound replacement demand translated into spare coverage.'; dom.inspectionCheckpointOutput.textContent = '-'; dom.inspectionCheckpointNote.textContent = 'Recommended review point before the risk curve steepens materially.'; dom.decisionRangeBody.innerHTML = 'Run the analysis to compare candidate warranty and replacement intervals.'; dom.decisionRangeNote.textContent = 'The comparison table will show how reliability and expected claims change as you move the interval earlier or later.'; dom.serviceCheckpointBody.innerHTML = 'Run the analysis to build a service-checkpoint plan.'; dom.growthSummary.textContent = 'Tag rows with segments such as prototype, pilot, rev B, supplier lot, or launch phase to track whether reliability is improving or regressing.'; dom.growthBaselineOutput.textContent = '-'; dom.growthBaselineNote.textContent = 'First valid segment with enough failures to fit a curve.'; dom.growthLatestOutput.textContent = '-'; dom.growthLatestNote.textContent = 'Most recent valid segment in the data-entry order.'; dom.growthDeltaOutput.textContent = '-'; dom.growthDeltaNote.textContent = 'Difference between latest and baseline segment mission survival.'; dom.growthTableBody.innerHTML = 'Add segment labels to life-data rows to activate reliability growth tracking.'; document.querySelectorAll('.rel-interpret-card').forEach(card => { card.style.borderColor = ''; card.style.background = ''; }); renderScenarioComparison(); persistDraft(); } function updateInterpretation(pattern, beta, missionReliability) { const activeId = pattern.id === 'infant' ? 'pattern-card-infant' : pattern.id === 'random' ? 'pattern-card-random' : 'pattern-card-wearout'; document.querySelectorAll('.rel-interpret-card').forEach(card => { card.style.borderColor = ''; card.style.background = ''; }); const activeCard = document.getElementById(activeId); if (activeCard) { activeCard.style.borderColor = 'rgba(106, 149, 255, 0.55)'; activeCard.style.background = 'rgba(106, 149, 255, 0.12)'; } dom.interpretSummary.textContent = `${pattern.title}: β = ${formatNumber(beta, 3)} and mission reliability = ${formatPercent(missionReliability, 1)}. ${pattern.guidance}`; } function updateOutputs(analysis) { const parsedRows = analysis.parsedRows; const failedRows = parsedRows.filter(row => row.status === 'failed'); const censoredRows = parsedRows.filter(row => row.status === 'censored'); const intervalRows = parsedRows.filter(row => row.status === 'interval'); const totalQty = parsedRows.reduce((sum, row) => sum + row.quantity, 0); const lastObserved = parsedRows.reduce((max, row) => Math.max(max, row.time), 0); dom.dataReadinessText.textContent = `${dom.datasetName.value || 'This data set'}${analysis.selectedSegment !== 'all' ? ` (${analysis.selectedSegment})` : ''} contains ${failedRows.length} failure rows, ${censoredRows.length} censored rows, ${intervalRows.length} interval rows, ${totalQty} total units represented, and a latest observed life of ${formatNumber(lastObserved, 0)} ${dom.timeUnit.value.toLowerCase()}.`; dom.shapeOutput.textContent = formatNumber(analysis.beta, 3); dom.scaleOutput.textContent = formatLife(analysis.eta); dom.b10Output.textContent = formatLife(analysis.b10); dom.meanLifeOutput.textContent = formatLife(analysis.meanLife); dom.missionReliabilityOutput.textContent = formatPercent(analysis.missionReliability, 1); dom.patternOutput.textContent = analysis.pattern.title; dom.b50Pill.textContent = `B50: ${formatLife(analysis.b50)}`; dom.b90Pill.textContent = `B90: ${formatLife(analysis.b90)}`; dom.warrantyPill.textContent = `Warranty risk: ${formatPercent(analysis.missionUnreliability, 1)}`; dom.shapeNote.textContent = analysis.beta < 1 ? 'Below 1 means early-life failures dominate.' : analysis.beta <= 1.05 ? 'Near 1 means approximately constant hazard.' : 'Above 1 means wear-out risk is increasing.'; dom.scaleNote.textContent = `${formatNumber(analysis.confidence.confidence * 100, 0)}% bootstrap interval: ${formatLife(analysis.confidence.eta.lower)} to ${formatLife(analysis.confidence.eta.upper)}.`; dom.b10Note.textContent = `${formatNumber(analysis.confidence.confidence * 100, 0)}% bootstrap interval: ${formatLife(analysis.confidence.b10.lower)} to ${formatLife(analysis.confidence.b10.upper)}.`; dom.meanLifeNote.textContent = `${formatNumber(analysis.confidence.confidence * 100, 0)}% bootstrap interval: ${formatLife(analysis.confidence.meanLife.lower)} to ${formatLife(analysis.confidence.meanLife.upper)}.`; dom.missionReliabilityNote.textContent = `${formatPercent(analysis.missionReliability, 1)} chance of surviving ${formatNumber(analysis.missionTime, 0)} ${dom.timeUnit.value.toLowerCase()} (${formatNumber(analysis.confidence.confidence * 100, 0)}% interval ${formatPercent(analysis.confidence.missionReliability.lower, 1)} to ${formatPercent(analysis.confidence.missionReliability.upper, 1)}).`; dom.patternNote.textContent = analysis.pattern.summary; dom.decisionSummary.textContent = analysis.missionReliability >= 0.9 ? `Mission reliability is strong at ${formatPercent(analysis.missionReliability, 1)}. The central planning question is whether the lower confidence bound of ${formatPercent(analysis.confidence.missionReliability.lower, 1)} is still acceptable for warranty or field commitments.` : analysis.missionReliability >= 0.7 ? `Mission reliability is moderate at ${formatPercent(analysis.missionReliability, 1)}. Review the lower confidence bound of ${formatPercent(analysis.confidence.missionReliability.lower, 1)} and compare B10 ${formatLife(analysis.b10)} against the intended warranty or preventive-replacement window.` : `Mission reliability is weak at ${formatPercent(analysis.missionReliability, 1)} with warranty-risk exposure of ${formatPercent(analysis.missionUnreliability, 1)} by mission time. The next engineering question is whether to redesign, screen early-life issues, or shorten the committed life window.`; updateInterpretation(analysis.pattern, analysis.beta, analysis.missionReliability); renderDecisionPlanning(analysis); renderModelValidation(analysis); renderGrowthTracking(analysis); renderChart(analysis); renderScenarioComparison(); persistDraft(); } function prepareAnalysis() { clearError(); try { const analysis = analyzeLifeData(); state.lastAnalysis = analysis; updateOutputs(analysis); persistDraft(); } catch (error) { state.lastAnalysis = null; resetOutputs(); showError(error.message || 'The reliability analysis could not be completed.'); } } function setChartTab(tabKey) { state.activeChartTab = tabKey; const tab = CHART_TABS[tabKey]; dom.chartHeading.textContent = tab.title; dom.chartCopy.textContent = tab.copy; document.querySelectorAll('[data-chart-tab]').forEach(button => { button.classList.toggle('is-active', button.dataset.chartTab === tabKey); }); if (state.lastAnalysis) { renderChart(state.lastAnalysis); } else { dom.chartDetailList.innerHTML = ''; dom.chartSvg.innerHTML = ''; dom.chartFootnote.textContent = 'Run the analysis after editing the life data to refresh the chart and detail summary.'; } } function parseBulkRows(input) { return String(input || '') .split(/\r?\n/) .map(line => line.trim()) .filter(Boolean) .map(line => { const parts = line.split(',').map(part => part.trim()); const normalizedThird = String(parts[2] || '').toLowerCase(); const looksLikeStatusInSecond = /^(failed|censored|suspended|surv|interval)/i.test(parts[1] || ''); if (looksLikeStatusInSecond) { const status = /^censored|suspended|surv/i.test(parts[1] || '') ? 'censored' : /^interval/i.test(parts[1] || '') ? 'interval' : 'failed'; return { time: parts[0] || '', endTime: '', status, quantity: Number(parts[2] || 1) || 1, segment: parts[3] || '', note: parts.slice(4).join(', ') }; } return { time: parts[0] || '', endTime: normalizedThird === 'interval' ? (parts[1] || '') : '', status: normalizedThird === 'interval' ? 'interval' : /^censored|suspended|surv/i.test(parts[2] || '') ? 'censored' : 'failed', quantity: normalizedThird === 'interval' ? (Number(parts[3] || 1) || 1) : (Number(parts[3] || 1) || 1), segment: normalizedThird === 'interval' ? (parts[4] || '') : (parts[4] || ''), note: normalizedThird === 'interval' ? parts.slice(5).join(', ') : parts.slice(5).join(', ') }; }); } document.getElementById('add-row-btn').addEventListener('click', () => { state.rows.push(createEmptyRow()); renderRows(); persistDraft(); }); document.getElementById('load-example-btn').addEventListener('click', () => { state.rows = cloneRows(EXAMPLE_ROWS); renderRows(); prepareAnalysis(); }); document.getElementById('clear-rows-btn').addEventListener('click', () => { state.rows = [createEmptyRow()]; dom.bulkPaste.value = ''; renderRows(); state.lastAnalysis = null; resetOutputs(); }); document.getElementById('paste-rows-btn').addEventListener('click', () => { const parsed = parseBulkRows(dom.bulkPaste.value); if (parsed.length) { state.rows = parsed; renderRows(); prepareAnalysis(); } }); document.getElementById('run-prep-btn').textContent = 'Run analysis'; document.getElementById('run-prep-btn').addEventListener('click', prepareAnalysis); document.getElementById('mission-time').addEventListener('input', () => { updatePrepSummary(); if (state.lastAnalysis) { prepareAnalysis(); } }); document.getElementById('time-unit').addEventListener('change', () => { updatePrepSummary(); if (state.lastAnalysis) { prepareAnalysis(); } renderCompareScenarioList(); }); document.getElementById('confidence-level').addEventListener('change', () => { if (state.lastAnalysis) { prepareAnalysis(); } }); document.getElementById('dataset-name').addEventListener('input', () => { if (state.lastAnalysis) { updateOutputs(state.lastAnalysis); } renderCompareScenarioList(); }); document.getElementById('life-data-body').addEventListener('input', event => { const index = Number(event.target.dataset.index); const field = event.target.dataset.field; if (!Number.isInteger(index) || !field || !state.rows[index]) { return; } state.rows[index][field] = field === 'quantity' || field === 'time' ? event.target.value : event.target.value; updatePrepSummary(); persistDraft(); }); document.getElementById('life-data-body').addEventListener('change', event => { const index = Number(event.target.dataset.index); const field = event.target.dataset.field; if (!Number.isInteger(index) || !field || !state.rows[index]) { return; } state.rows[index][field] = event.target.value; updatePrepSummary(); persistDraft(); }); document.getElementById('life-data-body').addEventListener('click', event => { const button = event.target.closest('[data-action]'); if (!button) return; const index = Number(button.dataset.index); if (!Number.isInteger(index) || !state.rows[index]) return; if (button.dataset.action === 'duplicate') { state.rows.splice(index + 1, 0, { ...state.rows[index] }); } if (button.dataset.action === 'delete' && state.rows.length > 1) { state.rows.splice(index, 1); } renderRows(); prepareAnalysis(); }); dom.scenarioNameInput.addEventListener('input', persistDraft); dom.bulkPaste.addEventListener('input', persistDraft); dom.missionTime.addEventListener('change', persistDraft); dom.timeUnit.addEventListener('change', persistDraft); dom.confidenceLevel.addEventListener('change', persistDraft); dom.datasetName.addEventListener('input', persistDraft); dom.targetReliability.addEventListener('input', () => { persistDraft(); if (state.lastAnalysis) prepareAnalysis(); }); dom.fleetSize.addEventListener('input', () => { persistDraft(); if (state.lastAnalysis) prepareAnalysis(); }); dom.warrantyWindow.addEventListener('input', () => { persistDraft(); if (state.lastAnalysis) prepareAnalysis(); }); dom.allowedClaims.addEventListener('input', () => { persistDraft(); if (state.lastAnalysis) prepareAnalysis(); }); dom.segmentFilter.addEventListener('change', () => { persistDraft(); prepareAnalysis(); }); document.getElementById('save-scenario-btn').addEventListener('click', saveScenario); document.getElementById('update-scenario-btn').addEventListener('click', updateSelectedScenario); document.getElementById('load-scenario-btn').addEventListener('click', loadSelectedScenario); document.getElementById('delete-scenario-btn').addEventListener('click', deleteSelectedScenario); document.getElementById('export-model-btn').addEventListener('click', exportModelJson); document.getElementById('import-model-btn').addEventListener('click', () => dom.importModelFile.click()); document.getElementById('export-report-btn').addEventListener('click', exportStakeholderReport); document.getElementById('export-engineering-btn').addEventListener('click', () => { try { exportEngineeringCsv(); clearError(); } catch (error) { showError(error.message || 'Engineering CSV could not be exported.'); } }); document.getElementById('export-comparison-btn').addEventListener('click', () => { try { exportComparisonReport(); clearError(); } catch (error) { showError(error.message || 'Comparison report could not be exported.'); } }); dom.importModelFile.addEventListener('change', event => importModelJson(event.target.files?.[0])); dom.savedScenariosSelect.addEventListener('change', event => { state.selectedScenarioId = event.target.value || ''; clearError(); }); dom.compareScenarioList.addEventListener('change', event => { const target = event.target.closest('[data-compare-id]'); if (!target) return; const id = target.getAttribute('data-compare-id'); if (!id) return; if (target.checked) { if (!state.compareScenarioIds.includes(id)) { state.compareScenarioIds.push(id); } } else { state.compareScenarioIds = state.compareScenarioIds.filter(item => item !== id); } if (!state.compareScenarioIds.length) { state.compareScenarioIds = ['__current__']; } renderCompareScenarioList(); }); document.querySelectorAll('[data-chart-tab]').forEach(button => { button.addEventListener('click', () => setChartTab(button.dataset.chartTab)); }); loadSavedScenarios(); renderSavedScenarios(); setChartTab('probability'); restoreDraft();