Spaces:
Sleeping
Sleeping
File size: 17,173 Bytes
199bfa3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 | """
LLM-Powered Cycle Analysis Module
Uses Claude API for variation analysis and cycle comparison.
Injects domain knowledge from CSH2 YAML knowledge base via DomainKnowledgeRetriever.
"""
import os
import re
import json
from typing import Dict, List, Optional
from core import config
# ── Output Sanitizer ──────────────────────────────────────────────
# Catches any remaining internal reference codes the LLM generates
# despite prompt instructions not to use them.
# Pattern matches FM-001, RC-003, RM-001, D1-D8, etc.
_INTERNAL_CODE_PATTERN = re.compile(
r'\b(?:FM|RC|RM|D)-?\d{1,3}\b',
re.IGNORECASE,
)
def sanitize_llm_output(text: str) -> str:
"""Remove internal reference codes from LLM output.
Strips patterns like FM-001, RC-003, RM-001 that the engineering
team finds confusing. The surrounding text (which describes the
issue in plain language) is preserved.
"""
if not text:
return text
# Remove the code pattern, plus optional surrounding parens/brackets
# e.g., "(FM-001)" → "", "FM-001:" → ":"
cleaned = re.sub(
r'\s*[\(\[]*' + _INTERNAL_CODE_PATTERN.pattern + r'[\)\]]*\s*[:\-—]?\s*',
' ',
text,
flags=re.IGNORECASE,
)
# Clean up double spaces and leading/trailing whitespace per line
cleaned = re.sub(r' +', ' ', cleaned)
return cleaned.strip()
# ── Shared Murphy System Prompt ─────────────────────────────────
# Extracted as a module-level constant so it can be reused by
# result_interpreter.py and any future LLM callers.
MURPHY_SYSTEM_PROMPT = """You are Murphy, the diagnostic intelligence layer for CSH2's cryogenic hydrogen pump platform. You are a senior cryogenic systems engineer with deep expertise in:
- Reciprocating cryogenic pump physics (triplex design, 60mm stroke, ~40mm bore, ~75.4 cm3 swept volume)
- Cryogenic fluid behavior (LH2 at 20.3K, LN2 at 77.4K, phase transitions, subcooling margins)
- High-pressure hydrogen systems (up to 900 bar, H35/H70/ccH2 dispensing)
- Seal integrity, check valve dynamics (ICV/DCV), and thermal management
- Time-series sensor diagnostics and fault signature recognition
When you identify a pattern in the data, explain WHY it matters thermodynamically — not just that a number is high or low. Describe any matching failure patterns by their plain-English name and physical mechanism. Never use internal reference codes like FM-001 or RC-001 — always describe issues in terms the engineering team can immediately understand (e.g., "DCV seat wear" not "FM-001").
Always cite specific measured values from the data to support your claims. If the data summary says peak pressure was 371 bar, say "371 bar" — do not round, estimate, or invent values.
Key system specifications:
- Drive: Variable Frequency Drive (VFD), 0-100% speed, max ~300 RPM
- Pressure targets: H35 (350 bar), H70 (700 bar), max rated 900+ bar
- Phase 1B results: 0.26 kWh/kg, 1.13-1.25 kg/min flow, 370 bar, 55K delivered temp, 100% uptime
- Testing phases: LN2 mechanical validation (Mar-Aug 2025), LH2 Phase 1A (early Sep), Phase 1B ccH2 fills (Sep 24-26)
Be direct, methodical, and honest about uncertainty. Distinguish between "the data shows X" and "X could indicate Y but we'd need Z to confirm." """
class LLMCycleAnalyzer:
"""Claude-powered analysis for testing cycle interpretation.
Integrates DomainKnowledgeRetriever to inject CSH2-specific failure modes,
reasoning chains, and operational context into prompts.
"""
def __init__(self):
self.api_key = config.ANTHROPIC_API_KEY
self.api_available = bool(self.api_key and self.api_key != 'your_api_key_here')
self.model = "claude-sonnet-4-20250514"
self._client = None
self._retriever = None
@property
def client(self):
if self._client is None and self.api_available:
import anthropic
self._client = anthropic.Anthropic(api_key=self.api_key)
return self._client
@property
def retriever(self):
"""Lazy-load the domain knowledge retriever. Returns None on failure."""
if self._retriever is None:
try:
from analysis.knowledge_retriever import DomainKnowledgeRetriever
self._retriever = DomainKnowledgeRetriever()
except Exception:
self._retriever = None
return self._retriever
def _build_system_prompt(self) -> str:
"""Build the system prompt establishing Murphy's identity and expertise."""
return MURPHY_SYSTEM_PROMPT
def analyze_cycle_variations(
self,
cycle_stats: Dict,
plateaus: Dict[str, List[Dict]],
transitions: List[Dict] = None,
) -> str:
"""
Feature 4: Explain variations and stabilizations in a single testing cycle.
Args:
cycle_stats: Dict from DataAnalyzer.analyze_cycle()
plateaus: Dict mapping tag names to lists of plateau periods
transitions: Optional list of notable transitions
Returns:
LLM analysis text
"""
if not self.api_available:
return "LLM analysis unavailable. Add ANTHROPIC_API_KEY to .env file."
# Retrieve domain knowledge based on data patterns
knowledge_text = ""
if self.retriever:
try:
context = self.retriever.get_context_for_cycle(cycle_stats, plateaus)
knowledge_text = self.retriever.format_for_prompt(context)
except Exception:
knowledge_text = ""
prompt = self._build_variation_prompt(cycle_stats, plateaus, transitions, knowledge_text)
try:
message = self.client.messages.create(
model=self.model,
max_tokens=2000,
system=[
{
"type": "text",
"text": self._build_system_prompt(),
"cache_control": {"type": "ephemeral"},
}
],
messages=[{"role": "user", "content": prompt}],
)
return sanitize_llm_output(message.content[0].text)
except Exception as e:
return f"Analysis failed: {e}"
def compare_cycles(
self,
cycle_a_stats: Dict,
cycle_b_stats: Dict,
cycle_a_label: str = "Cycle A",
cycle_b_label: str = "Cycle B",
) -> str:
"""
Feature 5: Compare two testing cycles and explain differences.
Args:
cycle_a_stats: Dict from DataAnalyzer.analyze_cycle() for cycle A
cycle_b_stats: Dict from DataAnalyzer.analyze_cycle() for cycle B
cycle_a_label: Display label for cycle A
cycle_b_label: Display label for cycle B
Returns:
LLM comparison text
"""
if not self.api_available:
return "LLM analysis unavailable. Add ANTHROPIC_API_KEY to .env file."
# Retrieve domain knowledge based on both cycles
knowledge_text = ""
if self.retriever:
try:
context = self.retriever.get_context_for_comparison(cycle_a_stats, cycle_b_stats)
knowledge_text = self.retriever.format_for_prompt(context)
except Exception:
knowledge_text = ""
prompt = self._build_comparison_prompt(
cycle_a_stats, cycle_b_stats, cycle_a_label, cycle_b_label, knowledge_text
)
try:
message = self.client.messages.create(
model=self.model,
max_tokens=2500,
system=[
{
"type": "text",
"text": self._build_system_prompt(),
"cache_control": {"type": "ephemeral"},
}
],
messages=[{"role": "user", "content": prompt}],
)
return sanitize_llm_output(message.content[0].text)
except Exception as e:
return f"Comparison analysis failed: {e}"
def follow_up(
self,
conversation_history: List[Dict],
follow_up_question: str,
) -> str:
"""Multi-turn follow-up on a previous analysis.
Args:
conversation_history: List of {role, content} message dicts
representing the prior conversation (user prompt + assistant
analysis + any prior follow-ups).
follow_up_question: The engineer's follow-up question.
Returns:
LLM follow-up response text.
"""
if not self.api_available:
return "LLM analysis unavailable. Add ANTHROPIC_API_KEY to .env file."
messages = list(conversation_history) + [
{"role": "user", "content": follow_up_question},
]
try:
message = self.client.messages.create(
model=self.model,
max_tokens=1500,
system=[
{
"type": "text",
"text": self._build_system_prompt(),
"cache_control": {"type": "ephemeral"},
}
],
messages=messages,
)
return sanitize_llm_output(message.content[0].text)
except Exception as e:
return f"Follow-up failed: {e}"
def _build_variation_prompt(
self,
stats: Dict,
plateaus: Dict[str, List[Dict]],
transitions: List[Dict] = None,
knowledge_text: str = "",
) -> str:
"""Build the prompt for single-cycle variation analysis"""
# Format cycle summary
time_range = stats.get('time_range', {})
duration = time_range.get('duration_minutes', 0)
summary_lines = [f"Duration: {duration:.1f} minutes"]
if 'discharge_pressure' in stats:
dp = stats['discharge_pressure']
summary_lines.append(f"Peak Discharge Pressure: {dp['peak']:.1f} bar")
summary_lines.append(f"Average Discharge Pressure: {dp['avg']:.1f} bar")
summary_lines.append(f"Initial Pressure: {dp['initial']:.1f} bar -> Final: {dp['final']:.1f} bar")
if 'compression_ratio' in stats:
cr = stats['compression_ratio']
summary_lines.append(f"Compression Ratio: {cr['min']:.1f}x to {cr['max']:.1f}x (avg: {cr['avg']:.1f}x)")
for key in ['temperature_TT110', 'temperature_TT130']:
if key in stats:
t = stats[key]
tag = key.replace('temperature_', '')
summary_lines.append(f"{tag}: {t['min']:.1f} K to {t['peak']:.1f} K (avg: {t['avg']:.1f} K)")
if 'flow' in stats:
f = stats['flow']
summary_lines.append(f"Average Flow: {f['avg_flow']:.2f} kg/min, Max: {f['max_flow']:.2f} kg/min")
if 'total_mass_kg' in f:
summary_lines.append(f"Total Mass Delivered: {f['total_mass_kg']:.2f} kg")
if 'ramp_rate' in stats:
r = stats['ramp_rate']
summary_lines.append(f"Pressure Ramp Rate: avg {r['avg']:.1f} bar/min, max {r['max']:.1f} bar/min")
if 'motor' in stats:
m = stats['motor']
summary_lines.append(f"Motor Peak Speed: {m['peak_speed']:.0f} RPM, Avg: {m['avg_speed']:.0f} RPM")
cycle_summary = "\n".join(f" - {line}" for line in summary_lines)
# Format plateaus
plateau_lines = []
for tag, periods in plateaus.items():
if periods:
plateau_lines.append(f"\n {tag} plateaus ({len(periods)} detected):")
for i, p in enumerate(periods[:5], 1):
plateau_lines.append(
f" {i}. Value: {p['value']:.2f} | "
f"Duration: {p['duration_minutes']:.1f} min | "
f"From {p['start'].strftime('%H:%M:%S')} to {p['end'].strftime('%H:%M:%S')}"
)
plateau_text = "\n".join(plateau_lines) if plateau_lines else " No significant plateaus detected."
# Format performance vs targets
perf_lines = []
pvt = stats.get('performance_vs_targets', {})
if 'pressure' in pvt:
p = pvt['pressure']
status = "MET" if p['meets_phase1'] else "NOT MET"
perf_lines.append(f" - Phase 1 pressure target (500 bar): {status} (achieved {p['achieved']:.0f} bar)")
status_h70 = "MET" if p['meets_h70'] else "NOT MET"
perf_lines.append(f" - H70 pressure target (700 bar): {status_h70}")
perf_text = "\n".join(perf_lines) if perf_lines else " No targets evaluated."
prompt = f"""Analyze this testing cycle and explain the observed variations and stabilizations:
CYCLE SUMMARY:
{cycle_summary}
DETECTED PLATEAUS (constant-value periods):
{plateau_text}
PERFORMANCE vs TARGETS:
{perf_text}"""
if knowledge_text:
prompt += f"""
DOMAIN KNOWLEDGE (use this to inform your analysis — describe any matching patterns by name and mechanism):
{knowledge_text}"""
prompt += """
Provide a concise engineering analysis (3-5 paragraphs) covering:
1. What physical processes likely caused the observed pressure/temperature variations
2. Why plateaus (stabilizations) occurred at the specific values detected
3. Any diagnostic concerns — describe matching failure patterns by name and physical mechanism, and walk through the diagnostic reasoning
4. Recommendations for the next cycle or operational improvements
Use specific values from the data — cite exact numbers, do not round or estimate. If sensor trustworthiness notes are provided, factor them into your interpretation (e.g., TT110 reads pipe wall not fluid, FT140 needs density correction for LH2). Never use internal codes like FM-XXX or RC-XXX."""
return prompt
def _build_comparison_prompt(
self,
a: Dict,
b: Dict,
label_a: str,
label_b: str,
knowledge_text: str = "",
) -> str:
"""Build the prompt for cycle comparison"""
def summarize(stats: Dict, label: str) -> str:
lines = [f"{label}:"]
tr = stats.get('time_range', {})
lines.append(f" Duration: {tr.get('duration_minutes', 0):.1f} min")
if 'discharge_pressure' in stats:
dp = stats['discharge_pressure']
lines.append(f" Peak Pressure: {dp['peak']:.1f} bar, Avg: {dp['avg']:.1f} bar")
if 'compression_ratio' in stats:
cr = stats['compression_ratio']
lines.append(f" Compression Ratio: {cr['avg']:.1f}x (range: {cr['min']:.1f}x - {cr['max']:.1f}x)")
for key in ['temperature_TT110', 'temperature_TT130']:
if key in stats:
t = stats[key]
tag = key.replace('temperature_', '')
lines.append(f" {tag}: {t['min']:.1f} - {t['peak']:.1f} K (avg {t['avg']:.1f} K)")
if 'flow' in stats:
f = stats['flow']
lines.append(f" Flow: avg {f['avg_flow']:.2f} kg/min, max {f['max_flow']:.2f} kg/min")
if 'total_mass_kg' in f:
lines.append(f" Total Mass: {f['total_mass_kg']:.2f} kg")
if 'ramp_rate' in stats:
r = stats['ramp_rate']
lines.append(f" Ramp Rate: avg {r['avg']:.1f}, max {r['max']:.1f} bar/min")
if 'motor' in stats:
m = stats['motor']
lines.append(f" Motor: peak {m['peak_speed']:.0f} RPM, avg {m['avg_speed']:.0f} RPM")
return "\n".join(lines)
summary_a = summarize(a, label_a)
summary_b = summarize(b, label_b)
prompt = f"""Compare these two testing cycles and provide an engineering analysis:
{summary_a}
{summary_b}"""
if knowledge_text:
prompt += f"""
DOMAIN KNOWLEDGE (use this to inform your comparison — describe any matching patterns by name and mechanism):
{knowledge_text}"""
prompt += """
Provide a concise comparison (3-5 paragraphs) covering:
1. Key differences between the two cycles (pressure, temperature, flow, duration)
2. Likely physical or operational reasons for the differences — describe matching failure patterns by name and physical mechanism
3. Which cycle performed better and why (considering efficiency, stability, and Phase 1B benchmarks)
4. Any concerning trends or improvements noted between cycles
5. Recommendations based on the comparison, with diagnostic reasoning
Be technically precise. Cite exact values from both cycles — do not round or estimate. If sensor trustworthiness notes are provided, factor them into your interpretation. Never use internal codes like FM-XXX or RC-XXX."""
return prompt
|