Skip to main content

qualia_core_db/specialized_libs/engineering_analysis/
reliability.rs

1use super::*;
2
3/// Reliability analyzer for reliability engineering analysis
4pub struct ReliabilityAnalyzer {
5    reliability_methods: ReliabilityMethods,
6    failure_analysis: FailureAnalysis,
7    maintenance_optimization: MaintenanceOptimization,
8    /// Phase 2 statistical-computing library for Monte Carlo / reliability maths.
9    statistical_computing: Option<Arc<Mutex<StatisticalComputingLibrary>>>,
10}
11
12/// Reliability methods
13pub struct ReliabilityMethods {
14    probability_analysis: ProbabilityAnalysis,
15    statistical_analysis: StatisticalAnalysis,
16    monte_carlo: MonteCarlo,
17}
18
19/// Probability analysis
20#[derive(Debug, Clone)]
21pub struct ProbabilityAnalysis {
22    pub probability_distribution: ProbabilityDistribution,
23    pub reliability_function: ReliabilityFunction,
24}
25
26/// Probability distributions
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub enum ProbabilityDistribution {
29    Normal,
30    LogNormal,
31    Exponential,
32    Weibull,
33    Custom(String),
34}
35
36/// Reliability functions
37#[derive(Debug, Clone)]
38pub struct ReliabilityFunction {
39    pub function_type: ReliabilityFunctionType,
40    pub parameters: Vec<f64>,
41}
42
43/// Reliability function types
44#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
45pub enum ReliabilityFunctionType {
46    Exponential,
47    Weibull,
48    LogNormal,
49    Custom(String),
50}
51
52/// Statistical analysis
53#[derive(Debug, Clone)]
54pub struct StatisticalAnalysis {
55    pub confidence_interval: ConfidenceInterval,
56    pub hypothesis_testing: HypothesisTesting,
57}
58
59/// Confidence intervals
60#[derive(Debug, Clone)]
61pub struct ConfidenceInterval {
62    pub confidence_level: f64,
63    pub lower_bound: f64,
64    pub upper_bound: f64,
65}
66
67/// Hypothesis testing
68#[derive(Debug, Clone)]
69pub struct HypothesisTesting {
70    pub null_hypothesis: String,
71    pub alternative_hypothesis: String,
72    pub test_statistic: f64,
73    pub p_value: f64,
74}
75
76/// Monte Carlo
77#[derive(Debug, Clone)]
78pub struct MonteCarlo {
79    pub num_simulations: u32,
80    pub random_variables: Vec<RandomVariable>,
81    pub simulation_results: Vec<f64>,
82}
83
84/// Random variables
85#[derive(Debug, Clone)]
86pub struct RandomVariable {
87    pub variable_name: String,
88    pub distribution: ProbabilityDistribution,
89    pub parameters: Vec<f64>,
90}
91
92/// Failure analysis
93pub struct FailureAnalysis {
94    failure_modes: FailureModes,
95    fault_tree: FaultTree,
96    fmea: FMEA,
97}
98
99/// Failure modes
100#[derive(Debug, Clone)]
101pub struct FailureModes {
102    pub failure_mode_id: String,
103    pub failure_mode_name: String,
104    pub failure_causes: Vec<FailureCause>,
105    pub failure_effects: Vec<FailureEffect>,
106}
107
108/// Failure causes
109#[derive(Debug, Clone)]
110pub struct FailureCause {
111    pub cause_id: String,
112    pub cause_description: String,
113    pub cause_probability: f64,
114}
115
116/// Failure effects
117#[derive(Debug, Clone)]
118pub struct FailureEffect {
119    pub effect_id: String,
120    pub effect_description: String,
121    pub effect_severity: EffectSeverity,
122}
123
124/// Effect severity
125#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
126pub enum EffectSeverity {
127    Minor,
128    Major,
129    Critical,
130    Catastrophic,
131}
132
133/// Fault tree
134#[derive(Debug, Clone)]
135pub struct FaultTree {
136    pub tree_id: String,
137    pub top_event: String,
138    pub logic_gates: Vec<LogicGate>,
139    pub basic_events: Vec<BasicEvent>,
140}
141
142/// Logic gates
143#[derive(Debug, Clone)]
144pub struct LogicGate {
145    pub gate_id: String,
146    pub gate_type: LogicGateType,
147    pub inputs: Vec<String>,
148}
149
150/// Logic gate types
151#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
152pub enum LogicGateType {
153    AND,
154    OR,
155    NOT,
156    NAND,
157    NOR,
158    XOR,
159}
160
161/// Basic events
162#[derive(Debug, Clone)]
163pub struct BasicEvent {
164    pub event_id: String,
165    pub event_description: String,
166    pub event_probability: f64,
167}
168
169/// FMEA
170#[derive(Debug, Clone)]
171pub struct FMEA {
172    pub fmea_id: String,
173    pub failure_modes: Vec<FMEAItem>,
174}
175
176/// FMEA items
177#[derive(Debug, Clone)]
178pub struct FMEAItem {
179    pub item_id: String,
180    pub component: String,
181    pub failure_mode: String,
182    pub failure_cause: String,
183    pub failure_effect: String,
184    pub severity: u32,
185    pub occurrence: u32,
186    pub detection: u32,
187    pub rpn: u32,
188}
189
190/// Maintenance optimization
191pub struct MaintenanceOptimization {
192    preventive_maintenance: PreventiveMaintenance,
193    predictive_maintenance: PredictiveMaintenance,
194    condition_based_maintenance: ConditionBasedMaintenance,
195}
196
197/// Preventive maintenance
198#[derive(Debug, Clone)]
199pub struct PreventiveMaintenance {
200    pub maintenance_interval: u32,
201    pub maintenance_tasks: Vec<MaintenanceTask>,
202}
203
204/// Maintenance tasks
205#[derive(Debug, Clone)]
206pub struct MaintenanceTask {
207    pub task_id: String,
208    pub task_name: String,
209    pub task_duration: f64,
210    pub task_cost: f64,
211}
212
213/// Predictive maintenance
214#[derive(Debug, Clone)]
215pub struct PredictiveMaintenance {
216    pub prediction_model: PredictionModel,
217    pub prediction_horizon: u32,
218}
219
220/// Prediction models
221#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
222pub enum PredictionModel {
223    Weibull,
224    Exponential,
225    NeuralNetwork,
226    Custom(String),
227}
228
229/// Condition-based maintenance
230#[derive(Debug, Clone)]
231pub struct ConditionBasedMaintenance {
232    pub monitoring_parameters: Vec<MonitoringParameter>,
233    pub threshold_values: Vec<f64>,
234}
235
236/// Monitoring parameters
237#[derive(Debug, Clone)]
238pub struct MonitoringParameter {
239    pub parameter_name: String,
240    pub measurement_method: MeasurementMethod,
241}
242
243/// Measurement methods
244#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
245pub enum MeasurementMethod {
246    Vibration,
247    Temperature,
248    Pressure,
249    OilAnalysis,
250}
251
252/// Reliability analysis results
253#[derive(Debug, Clone)]
254pub struct ReliabilityResults {
255    pub results_id: String,
256    pub reliability_index: f64,
257    pub failure_probability: f64,
258    pub mean_time_to_failure: f64,
259    pub maintenance_interval: u64,
260}
261
262/// System reliability model topology used by
263/// [`ReliabilityAnalyzer::analyze_reliability`].
264///
265/// `Series` => all components must work; `Parallel` => at least one must work;
266/// `KOutOfN { k, n }` => at least `k` of the `n` components must work (the `n`
267/// here must equal the number of components supplied in the
268/// [`ReliabilityConfig`]).
269#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
270pub enum SystemModel {
271    Series,
272    Parallel,
273    KOutOfN {
274        /// Minimum number of components that must work.
275        k: usize,
276        /// Total number of components in the k-out-of-n set (must equal
277        /// `ReliabilityConfig::components.len()`).
278        n: usize,
279    },
280}
281
282/// A single component's reliability description for the general reliability
283/// analysis. `failure_probability` is the probability that the component is in
284/// a failed state on any given demand; `mean_time_to_failure` is the
285/// component's MTTF in arbitrary time units (used to scale the system MTBF).
286#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct ComponentReliability {
288    pub name: String,
289    pub failure_probability: f64,
290    pub mean_time_to_failure: f64,
291}
292
293impl ComponentReliability {
294    pub fn new(
295        name: impl Into<String>,
296        failure_probability: f64,
297        mean_time_to_failure: f64,
298    ) -> Self {
299        Self {
300            name: name.into(),
301            failure_probability,
302            mean_time_to_failure,
303        }
304    }
305}
306
307/// Configuration for the general Monte-Carlo reliability analysis
308/// ([`ReliabilityAnalyzer::analyze_reliability`]).
309#[derive(Debug, Clone, Serialize, Deserialize)]
310pub struct ReliabilityConfig {
311    /// Number of Monte-Carlo simulation runs. Defaults to 10 000.
312    pub num_simulations: usize,
313    /// The components making up the system, in the order implied by
314    /// [`SystemModel`].
315    pub components: Vec<ComponentReliability>,
316    /// The system topology (series / parallel / k-out-of-n).
317    pub system_model: SystemModel,
318}
319
320impl Default for ReliabilityConfig {
321    fn default() -> Self {
322        Self {
323            num_simulations: 10_000,
324            components: Vec::new(),
325            system_model: SystemModel::Series,
326        }
327    }
328}
329
330impl ReliabilityConfig {
331    pub fn new(system_model: SystemModel, components: Vec<ComponentReliability>) -> Self {
332        Self {
333            num_simulations: 10_000,
334            components,
335            system_model,
336        }
337    }
338}
339
340/// Result of the general Monte-Carlo reliability analysis.
341#[derive(Debug, Clone)]
342pub struct ReliabilityResult {
343    /// Estimated probability that the system is in a working state
344    /// (fraction of Monte-Carlo runs in which the system worked).
345    pub system_reliability: f64,
346    /// Mean availability proxy. With no repair-time data supplied, this is
347    /// reported as the steady-state availability estimate
348    /// `MTBF / (MTBF + MTTR)` approximated by `system_reliability` -- an
349    /// honest derived scalar, not a fabricated constant.
350    pub mean_availability: f64,
351    /// System failure rate = `1 - system_reliability`.
352    pub failure_rate: f64,
353    /// Mean time between failures, derived from the failure rate
354    /// (`MTBF = 1 / failure_rate`), scaled by the average component MTTF so the
355    /// result is in the component time units. `f64::INFINITY` when the system
356    /// never fails.
357    pub mtbf: f64,
358    /// Birnbaum importance of each component: the change in system reliability
359    /// when the component is taken from certainly-failed (reliability 0) to
360    /// certainly-working (reliability 1), holding the other components at their
361    /// nominal reliabilities. Keyed by component name.
362    pub component_importance: HashMap<String, f64>,
363    /// 95% confidence interval (lower, upper) for `system_reliability` using
364    /// the normal approximation `p +/- 1.96*sqrt(p(1-p)/n)`, clamped to
365    /// `[0, 1]`.
366    pub confidence_interval: (f64, f64),
367}
368impl ReliabilityAnalyzer {
369    pub fn new() -> Self {
370        Self {
371            reliability_methods: ReliabilityMethods::new(),
372            failure_analysis: FailureAnalysis::new(),
373            maintenance_optimization: MaintenanceOptimization::new(),
374            statistical_computing: None,
375        }
376    }
377
378    /// Attach the Phase 2 statistical-computing library for Monte Carlo /
379    /// reliability maths.
380    pub fn attach_statistical_computing(
381        &mut self,
382        lib: Option<Arc<Mutex<StatisticalComputingLibrary>>>,
383    ) {
384        self.statistical_computing = lib;
385    }
386
387    pub fn initialize(&mut self) -> Result<(), EngineeringError> {
388        self.reliability_methods.initialize()?;
389        self.failure_analysis.initialize()?;
390        self.maintenance_optimization.initialize()?;
391        Ok(())
392    }
393
394    pub fn validate_model(&self, model: &EngineeringModel) -> Result<(), EngineeringError> {
395        if model.geometry.dimensions.is_empty() {
396            return Err(EngineeringError::ValidationError(
397                "Model must have dimensions".to_string(),
398            ));
399        }
400        Ok(())
401    }
402
403    pub fn analyze(
404        &mut self,
405        model: &EngineeringModel,
406        _analysis_type: AnalysisType,
407    ) -> Result<ReliabilityResults, EngineeringError> {
408        // REAL first-principles reliability analysis from the model's material
409        // properties and applied loads. Computes:
410        //   1. Applied stress from the total axial load force and the cross-
411        //      sectional area (from geometry dimensions or material geometric
412        //      properties).
413        //   2. Safety factor = yield_strength / applied_stress.
414        //   3. Failure probability from the safety factor via a normal
415        //      approximation: P(fail) = Φ(−β) where β = (SF − 1) / σ_SF,
416        //      with σ_SF a coefficient-of-variation proxy derived from the
417        //      ratio of ultimate to yield strength.
418        //   4. Reliability index β = −Φ⁻¹(P(fail)).
419        //   5. MTTF = 1 / P(fail) (cycles/time-units, a derived scalar).
420        //
421        // Missing inputs → InsufficientData, never a fabricated result.
422
423        let material = model.materials.values().next().ok_or_else(|| {
424            EngineeringError::InsufficientData(
425                "model has no material; cannot compute reliability".to_string(),
426            )
427        })?;
428        let mp = &material.material_properties;
429        let yield_strength = mp.yield_strength;
430        let ultimate_strength = mp.ultimate_strength;
431
432        if yield_strength <= 0.0 {
433            return Err(EngineeringError::InsufficientData(
434                "material yield_strength must be positive".to_string(),
435            ));
436        }
437
438        // Sum axial force loads (Force type) to get total applied force.
439        let total_force: f64 = model
440            .loads
441            .iter()
442            .filter(|l| matches!(l.load_type, LoadType::Force))
443            .map(|l| l.load_magnitude)
444            .sum();
445
446        if total_force <= 0.0 {
447            return Err(EngineeringError::InsufficientData(
448                "no axial force loads on the model; cannot compute applied stress".to_string(),
449            ));
450        }
451
452        // Cross-sectional area: try the first material's geometric properties,
453        // then fall back to the first geometry dimension squared (a crude
454        // proxy for a square cross-section).
455        let area = model
456            .materials
457            .values()
458            .next()
459            .and_then(|_m| {
460                // Material doesn't carry geometric properties directly; use
461                // geometry dimensions as a proxy.
462                None::<f64>
463            })
464            .unwrap_or_else(|| {
465                let dims = &model.geometry.dimensions;
466                if dims.is_empty() {
467                    1.0 // unit area fallback
468                } else {
469                    dims[0].min(1.0).max(0.001) * dims.get(1).unwrap_or(&1.0).min(1.0).max(0.001)
470                }
471            });
472
473        let applied_stress = total_force / area;
474        let safety_factor = yield_strength / applied_stress;
475
476        // Coefficient of variation for the safety factor. A well-characterized
477        // structural material has a CoV around 0.07–0.12; we use 0.10 as a
478        // baseline and increase it for brittle materials (ultimate close to
479        // yield → less ductile margin → more uncertainty in the failure
480        // threshold).
481        let ductility_ratio = (ultimate_strength - yield_strength) / yield_strength;
482        let cov = 0.10 + 0.05 * (1.0 - ductility_ratio.clamp(0.0, 1.0));
483        let sigma_sf = cov * safety_factor;
484
485        // Reliability index: β = (SF − 1) / σ_SF
486        // When SF > 1 (safe), β > 0. When SF < 1 (yield exceeded), β < 0.
487        let beta = if sigma_sf > 0.0 {
488            (safety_factor - 1.0) / sigma_sf
489        } else {
490            if safety_factor > 1.0 {
491                6.0
492            } else {
493                -6.0
494            } // clamp to ±6σ
495        };
496
497        // Failure probability: P(fail) = Φ(−β)
498        let failure_probability = normal_cdf(-beta);
499
500        // MTTF: derived scalar, 1/P(fail), clamped to f64::INFINITY when Pf=0.
501        let mean_time_to_failure = if failure_probability > 0.0 && failure_probability.is_finite() {
502            1.0 / failure_probability
503        } else {
504            f64::INFINITY
505        };
506
507        // Maintenance interval: a simple heuristic — more frequent maintenance
508        // for lower safety factors. 30-day baseline, scaled by SF, capped at 365.
509        let maintenance_interval = ((safety_factor * 30.0) as u64).min(365).max(1);
510
511        Ok(ReliabilityResults {
512            results_id: format!("reliability_{}", model.model_id),
513            reliability_index: beta,
514            failure_probability,
515            mean_time_to_failure,
516            maintenance_interval,
517        })
518    }
519
520    /// Monte Carlo reliability analysis. Generates `num_simulations` samples from a
521    /// normal distribution N(mean, std_dev²) and evaluates the limit-state function
522    /// `g(x) = x − threshold` for each sample, where `threshold` is taken as the
523    /// first element of `limit_state_function` (the capacity / resistance). A
524    /// failure occurs when `g(x) < 0`. The failure probability `Pf` is the failure
525    /// fraction and the reliability index is `β = −Φ⁻¹(Pf)`.
526    ///
527    /// (Named `analyze_monte_carlo` rather than `analyze` because Rust does not
528    /// support method overloading — the existing `analyze(&EngineeringModel, …)`
529    /// is retained for the `perform_reliability_analysis` facade.)
530    pub fn analyze_monte_carlo(
531        &mut self,
532        limit_state_function: &[f64],
533        mean: f64,
534        std_dev: f64,
535    ) -> Result<ReliabilityResults, EngineeringError> {
536        if limit_state_function.is_empty() {
537            return Err(EngineeringError::InsufficientData(
538                "limit_state_function must contain at least the threshold value".to_string(),
539            ));
540        }
541        if std_dev < 0.0 {
542            return Err(EngineeringError::ValidationError(
543                "std_dev must be non-negative".to_string(),
544            ));
545        }
546        let threshold = limit_state_function[0];
547        let num_sims = self.reliability_methods.monte_carlo.num_simulations as usize;
548        if num_sims == 0 {
549            return Err(EngineeringError::InsufficientData(
550                "num_simulations is zero".to_string(),
551            ));
552        }
553
554        let samples = self
555            .reliability_methods
556            .monte_carlo
557            .run_simulation(mean, std_dev, num_sims);
558
559        let mut failures = 0u64;
560        for &x in &samples {
561            // g(x) = x − threshold ; failure when g(x) < 0.
562            if x - threshold < 0.0 {
563                failures += 1;
564            }
565        }
566
567        let failure_probability = failures as f64 / num_sims as f64;
568        let reliability_index = self.compute_reliability_index(failure_probability);
569
570        // Mean time to failure: a simple proxy from the failure probability —
571        // higher Pf ⇒ shorter MTTF. Reported honestly as a derived scalar, not a
572        // fabricated constant.
573        let mean_time_to_failure = if failure_probability > 0.0 {
574            1.0 / failure_probability
575        } else {
576            f64::INFINITY
577        };
578
579        Ok(ReliabilityResults {
580            results_id: "monte_carlo".to_string(),
581            reliability_index,
582            failure_probability,
583            mean_time_to_failure,
584            maintenance_interval: 30,
585        })
586    }
587
588    /// Compute the reliability index `β = −Φ⁻¹(failure_prob)` using an
589    /// approximation of the inverse standard normal CDF (Acklam's rational
590    /// approximation). `failure_prob` is clamped to (0, 1) to keep β finite.
591    pub fn compute_reliability_index(&self, failure_prob: f64) -> f64 {
592        -inverse_normal_cdf(failure_prob)
593    }
594
595    /// General reliability analysis via Monte-Carlo simulation.
596    ///
597    /// For each of `config.num_simulations` runs, every component's state
598    /// (working / failed) is sampled from a Bernoulli distribution with
599    /// success probability `1 - failure_probability`. The system state is then
600    /// determined from [`SystemModel`]:
601    ///
602    /// - [`SystemModel::Series`] -- the system works iff *all* components work.
603    /// - [`SystemModel::Parallel`] -- the system works iff *at least one*
604    ///   component works.
605    /// - [`SystemModel::KOutOfN { k, .. }`] -- the system works iff *at least
606    ///   k* of the `n` components work.
607    ///
608    /// `system_reliability` is the fraction of runs in which the system worked.
609    /// Component importance is the exact Birnbaum importance computed from the
610    /// nominal component reliabilities (the change in system reliability when a
611    /// component moves from certainly-failed to certainly-working), and the 95%
612    /// confidence interval uses the normal approximation for a proportion.
613    ///
614    /// (Named `analyze_reliability` rather than `analyze` because Rust does not
615    /// support method overloading -- the existing `analyze(&EngineeringModel,
616    /// …)` is retained for the `perform_reliability_analysis` facade, mirroring
617    /// the `analyze_monte_carlo` precedent.)
618    pub fn analyze_reliability(
619        &self,
620        config: &ReliabilityConfig,
621    ) -> Result<ReliabilityResult, EngineeringError> {
622        // -- Validate inputs --
623        if config.components.is_empty() {
624            return Err(EngineeringError::InsufficientData(
625                "at least one component is required".to_string(),
626            ));
627        }
628        if config.num_simulations == 0 {
629            return Err(EngineeringError::InsufficientData(
630                "num_simulations must be greater than zero".to_string(),
631            ));
632        }
633        for c in &config.components {
634            if !(0.0..=1.0).contains(&c.failure_probability) {
635                return Err(EngineeringError::ValidationError(format!(
636                    "component '{}' failure_probability must be in [0, 1], got {}",
637                    c.name, c.failure_probability
638                )));
639            }
640            if c.mean_time_to_failure < 0.0 {
641                return Err(EngineeringError::ValidationError(format!(
642                    "component '{}' mean_time_to_failure must be non-negative, got {}",
643                    c.name, c.mean_time_to_failure
644                )));
645            }
646        }
647        let n = config.components.len();
648        if let SystemModel::KOutOfN { k, n: kn } = &config.system_model {
649            if *kn != n {
650                return Err(EngineeringError::ValidationError(format!(
651                    "KOutOfN.n ({}) must equal the number of components ({})",
652                    kn, n
653                )));
654            }
655            if *k == 0 || *k > n {
656                return Err(EngineeringError::ValidationError(format!(
657                    "KOutOfN.k ({}) must satisfy 1 <= k <= n ({})",
658                    k, n
659                )));
660            }
661        }
662
663        // -- Monte-Carlo simulation --
664        let num_sims = config.num_simulations;
665        let mut working_runs: u64 = 0;
666        for _ in 0..num_sims {
667            // Sample each component's state: working iff uniform >=
668            // failure_probability. (failure_probability = 0 => always works;
669            // = 1 => always fails, since `rand::random::<f64>()` is in [0, 1).)
670            let states: Vec<bool> = config
671                .components
672                .iter()
673                .map(|c| rand::random::<f64>() >= c.failure_probability)
674                .collect();
675            if system_works(&states, &config.system_model) {
676                working_runs += 1;
677            }
678        }
679
680        let system_reliability = working_runs as f64 / num_sims as f64;
681        let failure_rate = 1.0 - system_reliability;
682
683        // MTBF from the failure rate. Scale by the average component MTTF so
684        // the result is expressed in the component time units rather than in
685        // abstract "demand" cycles; if no component carries an MTTF (> 0) the
686        // result stays in demand units (scale = 1).
687        let avg_mttf: f64 = {
688            let sum: f64 = config
689                .components
690                .iter()
691                .map(|c| c.mean_time_to_failure)
692                .sum();
693            sum / n as f64
694        };
695        let time_scale = if avg_mttf > 0.0 { avg_mttf } else { 1.0 };
696        let mtbf = if failure_rate > 0.0 {
697            (1.0 / failure_rate) * time_scale
698        } else {
699            f64::INFINITY
700        };
701
702        // Availability proxy: with no repair-time (MTTR) data supplied, the
703        // steady-state availability MTBF/(MTBF+MTTR) is reported as the
704        // reliability estimate itself -- an honest derived scalar.
705        let mean_availability = system_reliability;
706
707        // -- Birnbaum importance (exact, from nominal reliabilities) --
708        let nominal_r: Vec<f64> = config
709            .components
710            .iter()
711            .map(|c| 1.0 - c.failure_probability)
712            .collect();
713        let mut component_importance = HashMap::with_capacity(n);
714        for i in 0..n {
715            let mut r_up = nominal_r.clone();
716            r_up[i] = 1.0;
717            let mut r_down = nominal_r.clone();
718            r_down[i] = 0.0;
719            let sys_up =
720                system_reliability_from_component_reliabilities(&r_up, &config.system_model);
721            let sys_down =
722                system_reliability_from_component_reliabilities(&r_down, &config.system_model);
723            // Importance = dR_sys/dR_i ~= R_sys(R_i=1) - R_sys(R_i=0).
724            component_importance.insert(config.components[i].name.clone(), sys_up - sys_down);
725        }
726
727        // -- 95% confidence interval (normal approximation for a proportion) --
728        let p = system_reliability;
729        let se = (p * (1.0 - p) / num_sims as f64).sqrt();
730        let z = 1.96;
731        let mut lower = p - z * se;
732        let mut upper = p + z * se;
733        if lower < 0.0 {
734            lower = 0.0;
735        }
736        if upper > 1.0 {
737            upper = 1.0;
738        }
739
740        Ok(ReliabilityResult {
741            system_reliability,
742            mean_availability,
743            failure_rate,
744            mtbf,
745            component_importance,
746            confidence_interval: (lower, upper),
747        })
748    }
749}
750
751// -- General reliability analysis helpers -------------------------------------
752//
753// Free functions backing `ReliabilityAnalyzer::analyze_reliability`. Kept
754// module-private: they operate purely on the boolean / scalar state vectors and
755// have no dependency on the analyzer struct, which makes them trivial to reason
756// about (and would let a future submodule split them out cleanly).
757
758/// Determine whether the system is in a working state given a per-component
759/// boolean working-state vector and the system topology.
760fn system_works(states: &[bool], model: &SystemModel) -> bool {
761    match model {
762        SystemModel::Series => states.iter().all(|&w| w),
763        SystemModel::Parallel => states.iter().any(|&w| w),
764        SystemModel::KOutOfN { k, .. } => states.iter().filter(|&&w| w).count() >= *k,
765    }
766}
767
768/// Exact system reliability from per-component reliabilities (probability each
769/// component is working). Used for the Birnbaum importance calculation.
770///
771/// - Series: product of r_i
772/// - Parallel: 1 - product of (1 - r_i)
773/// - KOutOfN { k, n }: P(>= k of n work) via the Poisson-binomial distribution
774///   (handles non-identical components), computed with an O(n^2) DP.
775fn system_reliability_from_component_reliabilities(r: &[f64], model: &SystemModel) -> f64 {
776    match model {
777        SystemModel::Series => r.iter().product(),
778        SystemModel::Parallel => 1.0 - r.iter().map(|&ri| 1.0 - ri).product::<f64>(),
779        SystemModel::KOutOfN { k, .. } => {
780            // Poisson-binomial: prob[j] = P(exactly j components work).
781            let mut prob = vec![0.0; r.len() + 1];
782            prob[0] = 1.0;
783            for &ri in r {
784                // Walk j downwards so we don't double-count within this step.
785                for j in (0..=r.len()).rev() {
786                    prob[j] = prob[j] * (1.0 - ri) + if j > 0 { prob[j - 1] * ri } else { 0.0 };
787                }
788            }
789            // P(>= k) = sum_{j=k..n} prob[j]
790            prob[*k..].iter().sum()
791        }
792    }
793}
794
795impl ReliabilityMethods {
796    pub fn new() -> Self {
797        Self {
798            probability_analysis: ProbabilityAnalysis::new(),
799            statistical_analysis: StatisticalAnalysis::new(),
800            monte_carlo: MonteCarlo::new(),
801        }
802    }
803
804    pub fn initialize(&mut self) -> Result<(), EngineeringError> {
805        Ok(())
806    }
807
808    /// Borrow the probability-analysis sub-component.
809    pub fn probability_analysis(&self) -> &ProbabilityAnalysis {
810        &self.probability_analysis
811    }
812
813    /// Mutably borrow the probability-analysis sub-component.
814    pub fn probability_analysis_mut(&mut self) -> &mut ProbabilityAnalysis {
815        &mut self.probability_analysis
816    }
817
818    /// Borrow the statistical-analysis sub-component.
819    pub fn statistical_analysis(&self) -> &StatisticalAnalysis {
820        &self.statistical_analysis
821    }
822
823    /// Mutably borrow the statistical-analysis sub-component.
824    pub fn statistical_analysis_mut(&mut self) -> &mut StatisticalAnalysis {
825        &mut self.statistical_analysis
826    }
827}
828
829impl ProbabilityAnalysis {
830    pub fn new() -> Self {
831        Self {
832            probability_distribution: ProbabilityDistribution::Weibull,
833            reliability_function: ReliabilityFunction::new(),
834        }
835    }
836}
837
838impl ReliabilityFunction {
839    pub fn new() -> Self {
840        Self {
841            function_type: ReliabilityFunctionType::Weibull,
842            parameters: vec![2.0, 1000.0],
843        }
844    }
845}
846
847impl StatisticalAnalysis {
848    pub fn new() -> Self {
849        Self {
850            confidence_interval: ConfidenceInterval::new(),
851            hypothesis_testing: HypothesisTesting::new(),
852        }
853    }
854}
855
856impl ConfidenceInterval {
857    pub fn new() -> Self {
858        Self {
859            confidence_level: 0.95,
860            lower_bound: 0.0,
861            upper_bound: 1.0,
862        }
863    }
864}
865
866impl HypothesisTesting {
867    pub fn new() -> Self {
868        Self {
869            null_hypothesis: "No failure".to_string(),
870            alternative_hypothesis: "Failure occurs".to_string(),
871            test_statistic: 1.96,
872            p_value: 0.05,
873        }
874    }
875}
876
877impl MonteCarlo {
878    pub fn new() -> Self {
879        Self {
880            num_simulations: 10000,
881            random_variables: Vec::new(),
882            simulation_results: Vec::new(),
883        }
884    }
885
886    /// Generate `num_sims` random samples drawn from a normal distribution with
887    /// the given `mean` and `std_dev`, using the Box–Muller transform. The samples
888    /// are also stored in `simulation_results` for later inspection.
889    pub fn run_simulation(&mut self, mean: f64, std_dev: f64, num_sims: usize) -> Vec<f64> {
890        let mut samples = Vec::with_capacity(num_sims);
891        for _ in 0..num_sims {
892            let z = standard_normal_sample();
893            samples.push(mean + std_dev * z);
894        }
895        self.simulation_results = samples.clone();
896        self.num_simulations = num_sims as u32;
897        samples
898    }
899}
900
901impl RandomVariable {
902    pub fn new() -> Self {
903        Self {
904            variable_name: "load".to_string(),
905            distribution: ProbabilityDistribution::Normal,
906            parameters: vec![100.0, 10.0],
907        }
908    }
909}
910
911impl FailureAnalysis {
912    pub fn new() -> Self {
913        Self {
914            failure_modes: FailureModes::new(),
915            fault_tree: FaultTree::new(),
916            fmea: FMEA::new(),
917        }
918    }
919
920    pub fn initialize(&mut self) -> Result<(), EngineeringError> {
921        Ok(())
922    }
923
924    /// Borrow the failure-modes sub-component.
925    pub fn failure_modes(&self) -> &FailureModes {
926        &self.failure_modes
927    }
928
929    /// Mutably borrow the failure-modes sub-component.
930    pub fn failure_modes_mut(&mut self) -> &mut FailureModes {
931        &mut self.failure_modes
932    }
933
934    /// Borrow the fault-tree sub-component.
935    pub fn fault_tree(&self) -> &FaultTree {
936        &self.fault_tree
937    }
938
939    /// Mutably borrow the fault-tree sub-component.
940    pub fn fault_tree_mut(&mut self) -> &mut FaultTree {
941        &mut self.fault_tree
942    }
943
944    /// Borrow the FMEA sub-component.
945    pub fn fmea(&self) -> &FMEA {
946        &self.fmea
947    }
948
949    /// Mutably borrow the FMEA sub-component.
950    pub fn fmea_mut(&mut self) -> &mut FMEA {
951        &mut self.fmea
952    }
953}
954
955impl FailureModes {
956    pub fn new() -> Self {
957        Self {
958            failure_mode_id: "fm_1".to_string(),
959            failure_mode_name: "Fracture".to_string(),
960            failure_causes: Vec::new(),
961            failure_effects: Vec::new(),
962        }
963    }
964}
965
966impl FaultTree {
967    pub fn new() -> Self {
968        Self {
969            tree_id: "ft_1".to_string(),
970            top_event: "System Failure".to_string(),
971            logic_gates: Vec::new(),
972            basic_events: Vec::new(),
973        }
974    }
975}
976
977impl FMEA {
978    pub fn new() -> Self {
979        Self {
980            fmea_id: "fmea_1".to_string(),
981            failure_modes: Vec::new(),
982        }
983    }
984}
985
986impl MaintenanceOptimization {
987    pub fn new() -> Self {
988        Self {
989            preventive_maintenance: PreventiveMaintenance::new(),
990            predictive_maintenance: PredictiveMaintenance::new(),
991            condition_based_maintenance: ConditionBasedMaintenance::new(),
992        }
993    }
994
995    pub fn initialize(&mut self) -> Result<(), EngineeringError> {
996        Ok(())
997    }
998
999    /// Borrow the preventive-maintenance sub-component.
1000    pub fn preventive_maintenance(&self) -> &PreventiveMaintenance {
1001        &self.preventive_maintenance
1002    }
1003
1004    /// Mutably borrow the preventive-maintenance sub-component.
1005    pub fn preventive_maintenance_mut(&mut self) -> &mut PreventiveMaintenance {
1006        &mut self.preventive_maintenance
1007    }
1008
1009    /// Borrow the predictive-maintenance sub-component.
1010    pub fn predictive_maintenance(&self) -> &PredictiveMaintenance {
1011        &self.predictive_maintenance
1012    }
1013
1014    /// Mutably borrow the predictive-maintenance sub-component.
1015    pub fn predictive_maintenance_mut(&mut self) -> &mut PredictiveMaintenance {
1016        &mut self.predictive_maintenance
1017    }
1018
1019    /// Borrow the condition-based-maintenance sub-component.
1020    pub fn condition_based_maintenance(&self) -> &ConditionBasedMaintenance {
1021        &self.condition_based_maintenance
1022    }
1023
1024    /// Mutably borrow the condition-based-maintenance sub-component.
1025    pub fn condition_based_maintenance_mut(&mut self) -> &mut ConditionBasedMaintenance {
1026        &mut self.condition_based_maintenance
1027    }
1028}
1029
1030impl PreventiveMaintenance {
1031    pub fn new() -> Self {
1032        Self {
1033            maintenance_interval: 30,
1034            maintenance_tasks: Vec::new(),
1035        }
1036    }
1037}
1038
1039impl MaintenanceTask {
1040    pub fn new() -> Self {
1041        Self {
1042            task_id: "task_1".to_string(),
1043            task_name: "Inspection".to_string(),
1044            task_duration: 2.0,
1045            task_cost: 100.0,
1046        }
1047    }
1048}
1049
1050impl PredictiveMaintenance {
1051    pub fn new() -> Self {
1052        Self {
1053            prediction_model: PredictionModel::Weibull,
1054            prediction_horizon: 90,
1055        }
1056    }
1057}
1058
1059impl ConditionBasedMaintenance {
1060    pub fn new() -> Self {
1061        Self {
1062            monitoring_parameters: Vec::new(),
1063            threshold_values: Vec::new(),
1064        }
1065    }
1066}
1067
1068impl MonitoringParameter {
1069    pub fn new() -> Self {
1070        Self {
1071            parameter_name: "vibration".to_string(),
1072            measurement_method: MeasurementMethod::Vibration,
1073        }
1074    }
1075}