""" PHI-Arc Engine PHM — Turbojet Engine Model Reference: J85/GE-15 class military turbojet Certification: MIL-E-5007D, FAR Part 33 """ import numpy as np from typing import Dict, List from .base_engine import BaseEngine GAMMA_C = 1.40 CP_C = 1005.0 GAMMA_H = 1.333 CP_H = 1148.0 P_SL = 101325.0 T_ISA_SL = 288.15 LAPSE = 0.0065 LHV_JP8 = 43.24e6 class TurbojetEngine(BaseEngine): def __init__(self): super().__init__( name="J85-GE Class Turbojet", engine_type="turbojet", ata_chapters={ "72-00": "Engine General", "72-30": "Compressor", "72-50": "Turbine", "73-10": "Fuel System", "75-30": "Bleed Air", "77-10": "Engine Controls" } ) self._build_fault_library() self.cert_standards = [ "MIL-E-5007D — General Specification for Aircraft Turbojet Engines", "FAR Part 33 — Airworthiness Standards", "MIL-STD-810 — Environmental Engineering Considerations", "JSSG-2007B — Engine Subsystem Specification" ] self.OPR = 8.0 self.eta_c = 0.82 self.eta_t = 0.87 self.eta_b = 0.995 self.eta_mech = 0.98 self.mdot = 20.0 self.EGT_REDLINE = 1050.0 self.EGT_WARN = 1000.0 def _build_fault_library(self): self.fault_library = [ { "id": "F1", "name": "F1 — Compressor Surge / Stall", "ata": "ATA 72-30", "mechanism": "Axial compressor blade stall causes flow separation and pressure drop. EGT spikes, N1 fluctuates. Potential for engine flameout.", "leading": "N1 fluctuation + EGT spike + P3 drop (surge signature)", "color": "#e74c3c", "levels": [[0.98,1.00,1.00,0.00],[0.94,1.00,1.00,0.00],[0.90,1.00,1.00,0.00]], "actions": [ "Compressor borescope + blade inspection (AMM 72-30-00-200-801)", "Bleed valve / surge recovery system test (AMM 75-30-00-040-801)", "Compressor replacement (AMM 72-30-00-720-801)" ], "cert_requirements": [ "MIL-E-5007D 3.2.1.1 — Compressor stall margin ≥ 10%", "FAR 33.65 — Surge and stall characteristics", "JSSG-2007B 4.1.2 — Stall recovery within 5 seconds" ] }, { "id": "F2", "name": "F2 — Turbine Blade Overtemp / Creep", "ata": "ATA 72-50", "mechanism": "Prolonged high EGT operation causes turbine blade creep and coating degradation. Blade tip clearance increases, efficiency drops.", "leading": "EGT↑↑ with N1 stable (turbine degradation)", "color": "#f39c12", "levels": [[1.00,0.99,1.00,0.00],[1.00,0.96,1.00,0.00],[1.00,0.93,1.00,0.00]], "actions": [ "Hot section borescope (AMM 72-50-00-200-801)", "Turbine blade coating repair (AMM 72-50-00-400-801)", "Turbine module replacement (AMM 72-50-00-720-801)" ], "cert_requirements": [ "MIL-E-5007D 3.2.2.1 — Turbine blade life ≥ declared cycles", "FAR 33.27 — Turbine operating temperature limits", "JSSG-2007B 4.2.1 — Creep life tracking required" ] }, { "id": "F3", "name": "F3 — Afterburner Fuel System Fault", "ata": "ATA 73-10 / 75-40", "mechanism": "Afterburner fuel manifold leak or nozzle blockage causes asymmetric combustion. Thrust loss, EGT spread, potential for burner can damage.", "leading": "Thrust↓ + EGT spread↑ + AB fuel pressure↓", "color": "#9b59b6", "levels": [[0.99,1.00,1.00,0.00],[0.97,1.00,1.00,0.00],[0.95,1.00,1.00,0.00]], "actions": [ "Afterburner fuel nozzle flow test (AMM 73-10-00-200-801)", "AB fuel manifold pressure test + repair (AMM 73-10-00-400-801)", "Afterburner module replacement (AMM 75-40-00-720-801)" ], "cert_requirements": [ "MIL-E-5007D 3.2.3.1 — Afterburner fuel distribution tolerance", "FAR 33.33 — Fuel system integrity", "JSSG-2007B 4.3.1 — Afterburner pattern factor ≤ 0.30" ] }, { "id": "F4", "name": "F4 — Nozzle Area Degradation", "ata": "ATA 75-40", "mechanism": "Thermal erosion of convergent-divergent nozzle throat increases area. Back pressure drops, N1 increases to maintain thrust, EGT may drop.", "leading": "N1↑ + EGT↓ + thrust↓ (nozzle area↑)", "color": "#3498db", "levels": [[1.00,1.00,0.99,0.00],[1.00,1.00,0.97,0.00],[1.00,1.00,0.94,0.00]], "actions": [ "Nozzle throat dimension inspection (AMM 75-40-00-200-801)", "Nozzle segment replacement (AMM 75-40-00-400-801)", "Full nozzle assembly replacement (AMM 75-40-00-720-801)" ], "cert_requirements": [ "MIL-E-5007D 3.2.4.1 — Nozzle area tolerance ±2%", "FAR 33.5 — Engine ratings and operating limitations", "JSSG-2007B 4.4.1 — Nozzle throat erosion limits" ] }, { "id": "F5", "name": "F5 — Bearing / Lubrication System", "ata": "ATA 79-20", "mechanism": "Main shaft bearing wear increases vibration and oil temperature. Metal particles in oil. Potential for catastrophic failure if undetected.", "leading": "Oil temp↑ + vibration↑ + metal particles↑ (SOAP)", "color": "#1abc9c", "levels": [[1.00,1.00,1.00,0.020],[1.00,1.00,1.00,0.040],[1.00,1.00,1.00,0.065]], "actions": [ "Oil SOAP + vibration analysis (AMM 79-20-00-200-801)", "Bearing replacement (AMM 72-00-00-400-801)", "Engine removal + overhaul (AMM 72-00-00-720-801)" ], "cert_requirements": [ "MIL-E-5007D 3.2.5.1 — Bearing life ≥ declared cycles", "FAR 33.62 — Oil system integrity", "MIL-PRF-23699 — Oil contamination limits" ] } ] def isa_conditions(self, alt_m): T = T_ISA_SL - LAPSE * alt_m P = P_SL * (T / T_ISA_SL) ** 5.2561 return T, P def compute_healthy_baseline(self, flight_conditions: Dict) -> Dict[str, float]: alt_m = flight_conditions.get("altitude_m", 0) M = flight_conditions.get("mach", 0.0) thrust = flight_conditions.get("thrust_n", 15000) T, P = self.isa_conditions(alt_m) T_t2 = T * (1 + 0.2 * M * M) P_t2 = P * (T_t2 / T) ** 3.5 T_t3 = T_t2 * (1 + (self.OPR**0.2857 - 1) / self.eta_c) P_t3 = P_t2 * self.OPR mdot = self.mdot * (P_t2 / P_SL) * (T_ISA_SL / T_t2)**0.5 W_comp = mdot * CP_C * (T_t3 - T_t2) W_req = W_comp + thrust / self.eta_mech TIT = T_t3 + W_req / (mdot * CP_H * 0.35) T_t5 = TIT * (1 - self.eta_t * 0.35) EGT = T_t5 - 273.15 FF = mdot * CP_H * (TIT - T_t3) / (self.eta_b * LHV_JP8) N1 = (mdot / self.mdot)**0.5 * (self.OPR/8)**0.25 * 100 P3 = P_t3 / 1000 return {"EGT": EGT, "FF": FF, "N1": N1, "P3": P3, "TIT": TIT, "T_amb": T, "P_amb": P} def compute_fault_signatures(self, baseline: Dict, flight_conditions: Dict) -> List[List[Dict]]: results = [] for f in self.fault_library: lvl_vals = [] for lv in range(3): m = f["levels"][lv] alt_m = flight_conditions.get("altitude_m", 0) M = flight_conditions.get("mach", 0.0) thrust = flight_conditions.get("thrust_n", 15000) T, P = self.isa_conditions(alt_m) T_t2 = T * (1 + 0.2 * M * M) P_t2 = P * (T_t2 / T) ** 3.5 eta_c = self.eta_c * m[0] eta_t = self.eta_t * m[1] eta_b = self.eta_b * m[2] mech_loss = m[3] T_t3 = T_t2 * (1 + (self.OPR**0.2857 - 1) / eta_c) P_t3 = P_t2 * self.OPR mdot = self.mdot * (P_t2 / P_SL) * (T_ISA_SL / T_t2)**0.5 W_comp = mdot * CP_C * (T_t3 - T_t2) W_req = W_comp + thrust / (self.eta_mech - mech_loss) TIT = T_t3 + W_req / (mdot * CP_H * 0.35) T_t5 = TIT * (1 - eta_t * 0.35) EGT = T_t5 - 273.15 FF = mdot * CP_H * (TIT - T_t3) / (eta_b * LHV_JP8) N1 = (mdot / self.mdot)**0.5 * (self.OPR/8)**0.25 * 100 P3 = P_t3 / 1000 lvl_vals.append({"EGT": EGT, "FF": FF, "N1": N1, "P3": P3}) results.append(lvl_vals) return results def classify_fault(self, measured: Dict, baseline: Dict, fault_sigs: List[List[Dict]]) -> Dict: params = ["EGT", "FF", "N1", "P3"] user_sig = np.zeros(4) for i, p in enumerate(params): if p in measured and p in baseline: denom = baseline[p] * 0.05 if p == "N1" else (baseline[p] * 0.10 if p == "FF" else (self.EGT_REDLINE - baseline[p] if p == "EGT" else baseline[p] * 0.10)) user_sig[i] = (measured[p] - baseline[p]) / denom if denom != 0 else 0 scores = [] for fi, f in enumerate(self.fault_library): best_score = -1e9 best_lvl = 0 for lv in range(3): fv = fault_sigs[fi][lv] sig = np.zeros(4) for i, p in enumerate(params): denom = baseline[p] * 0.05 if p == "N1" else (baseline[p] * 0.10 if p == "FF" else (self.EGT_REDLINE - baseline[p] if p == "EGT" else baseline[p] * 0.10)) sig[i] = (fv[p] - baseline[p]) / denom if denom != 0 else 0 denom = (np.linalg.norm(sig) * np.linalg.norm(user_sig)) + 1e-9 score = np.dot(sig, user_sig) / denom if score > best_score: best_score = score best_lvl = lv sev = max(0.0, min(1.0, best_score)) scores.append({"fi": fi, "f": f, "sev": sev, "lvl": best_lvl, "sig": user_sig}) scores.sort(key=lambda x: x["sev"], reverse=True) magnitude = np.linalg.norm(user_sig) status = "HEALTHY" if magnitude < 0.12 else ("INDETERMINATE" if scores[0]["sev"] < 0.30 else "FAULT_DETECTED") return { "base": baseline, "faults": fault_sigs, "scores": scores, "user_sig": user_sig, "status": status, "thresholds": { "EGT_warn": self.EGT_WARN, "EGT_maint": self.EGT_REDLINE, "FF_warn": baseline["FF"] * 1.03, "FF_maint": baseline["FF"] * 1.06, "N1_warn": baseline["N1"] * 0.985, "N1_maint": baseline["N1"] * 0.970 }, "deviations": {p: user_sig[i] for i, p in enumerate(params)} } def get_parameter_labels(self) -> List[str]: return ["EGT", "Fuel Flow", "N1", "P3"] def get_parameter_units(self) -> Dict[str, str]: return {"EGT": "°C", "FF": "kg/s", "N1": "%", "P3": "kPa"} def get_input_fields(self) -> List[Dict]: return [ {"name": "altitude", "label": "Altitude", "default": 0, "unit": "ft", "type": "number"}, {"name": "mach", "label": "Mach Number", "default": 0.0, "unit": "Mach", "type": "number"}, {"name": "thrust", "label": "Thrust Setting", "default": 15, "unit": "kN", "type": "number"}, {"name": "EGT", "label": "EGT", "default": 650.0, "unit": "°C", "type": "number"}, {"name": "FF", "label": "Fuel Flow", "default": 1200.0, "unit": "kg/hr", "type": "number"}, {"name": "N1", "label": "N1 Speed", "default": 100.0, "unit": "%", "type": "number"}, {"name": "P3", "label": "Compressor Discharge Pressure", "default": 450.0, "unit": "kPa", "type": "number"} ]