|
|
|
|
|
import pandas as pd
|
|
|
import plotly.graph_objects as go
|
|
|
from plotly.subplots import make_subplots
|
|
|
import base64
|
|
|
import json
|
|
|
from langchain_core.messages import HumanMessage
|
|
|
import streamlit as st
|
|
|
|
|
|
def get_dcrm_prompt(data_str):
|
|
|
return f"""
|
|
|
I have extracted data from a DCRM (Dynamic Contact Resistance Measurement) graph.
|
|
|
Data (Sampled): {data_str}
|
|
|
|
|
|
The columns are:
|
|
|
- 'time': Time in milliseconds.
|
|
|
- 'curr': Current signal amplitude (Blue curve) - represents the test current flowing through the contacts.
|
|
|
- 'res': Dynamic Resistance amplitude (Green curve) - represents the contact resistance in micro-ohms (µΩ).
|
|
|
- 'travel': Travel signal amplitude (Red curve) - represents the mechanical position/displacement of the moving contact.
|
|
|
|
|
|
IMPORTANT: Higher values mean the signal is HIGHER on the graph.
|
|
|
|
|
|
I have also provided the image of the graph. Use the visual information from the image to cross-reference with the data.
|
|
|
|
|
|
=== HEALTHY DCRM SIGNATURE REFERENCE ===
|
|
|
|
|
|
**Resistance (Green) - Healthy Characteristics:**
|
|
|
- Pre-contact: Infinite/Very High (off-scale or flat at top)
|
|
|
- Arcing engagement: Drops sharply with moderate spikes (arcing activity), typically 100-500 µΩ
|
|
|
- Main conduction: LOW and STABLE (30-80 µΩ for healthy contacts), minimal oscillation (<10 µΩ variance)
|
|
|
- Parting: Sharp rise with spikes (arcing during separation)
|
|
|
- Final open: Returns to infinite/very high (off-scale)
|
|
|
|
|
|
**Current (Blue) - Healthy Characteristics:**
|
|
|
- Pre-contact: Near zero baseline
|
|
|
- Arcing engagement: Begins rising as circuit closes
|
|
|
- Main conduction: Stable at test current level (plateau)
|
|
|
- Parting: Maintained until final separation
|
|
|
- Final open: Drops to zero
|
|
|
|
|
|
**Travel (Red) - Healthy Characteristics:**
|
|
|
- Pre-contact: Increasing linearly (contacts approaching)
|
|
|
- Arcing engagement: Continues increasing
|
|
|
- Main conduction: Reaches MAXIMUM and plateaus (fully closed position)
|
|
|
- Parting: Decreases linearly (contacts separating)
|
|
|
- Final open: Stabilizes at minimum (fully open position)
|
|
|
|
|
|
=== TASK: SEGMENT INTO 5 KINEMATIC ZONES ===
|
|
|
|
|
|
Use ALL THREE curves together for accurate boundary detection. Each zone represents a distinct physical state of the circuit breaker.
|
|
|
|
|
|
**Zone 1: Pre-Contact Travel (Initial Closing Motion)**
|
|
|
* **Physical Meaning**: The moving contact is traveling toward the stationary contact but has NOT yet made electrical contact. This is pure mechanical motion with no current flow.
|
|
|
* **Start**: time = 0 ms
|
|
|
* **End Boundary**: Detect when CURRENT (blue) FIRST starts rising significantly from baseline.
|
|
|
* Cross-reference: Resistance (green) should still be very high/infinite
|
|
|
* Cross-reference: Travel (red) should be steadily increasing
|
|
|
* **Typical Duration**: 80-120 ms
|
|
|
* **Detection Logic**: Find the point where 'curr' rises above baseline noise (e.g., >5% of max current)
|
|
|
|
|
|
**Zone 2: Arcing Contact Engagement (Initial Electrical Contact)**
|
|
|
* **Physical Meaning**: The arcing contacts (W-Cu tips) make first contact and establish an electrical path. Current begins flowing through a small contact area, causing arcing and resistance fluctuations. This is the "make" transition.
|
|
|
* **Start**: End of Zone 1
|
|
|
* **End Boundary**: Detect when resistance SETTLES after initial spike activity.
|
|
|
* Primary indicator: Resistance (green) drops from high values, exhibits spikes, then STABILIZES to low plateau
|
|
|
* Cross-reference: Current (blue) should be rising/stabilizing
|
|
|
* Cross-reference: Travel (red) continues increasing toward maximum
|
|
|
* **Typical Duration**: 20-40 ms (Zone 2 typically ends around 110-150 ms total time)
|
|
|
* **Detection Logic**: Find where 'res' completes its descent and spike activity, settling into a stable low range
|
|
|
|
|
|
**Zone 3: Main Contact Conduction (Fully Closed State)**
|
|
|
* **Physical Meaning**: The main contacts (Ag-plated) are fully engaged, providing a large, stable contact area. This is the "healthy contact" signature zone - resistance should be at its MINIMUM and STABLE. The breaker is in its fully closed, current-carrying state.
|
|
|
* **Start**: End of Zone 2
|
|
|
* **End Boundary**: Detect when the breaker begins OPENING (travel reverses direction).
|
|
|
* Primary indicator: Travel (red) reaches MAXIMUM and starts to DESCEND
|
|
|
* Cross-reference: Resistance (green) should remain low and stable throughout this zone
|
|
|
* Cross-reference: Current (blue) should be stable at test level
|
|
|
* **Typical Duration**: 100-200 ms (this is the longest zone, representing the dwell time)
|
|
|
* **Detection Logic**: Find the peak of 'travel' curve and the point where it starts decreasing
|
|
|
|
|
|
**Zone 4: Main Contact Parting (Breaking/Opening Transition)**
|
|
|
* **Physical Meaning**: The main contacts are separating. As the contact area decreases, resistance rises sharply. Arcing occurs during the final separation of the arcing contacts. This is the "break" transition - the most critical phase for fault detection.
|
|
|
* **Start**: End of Zone 3
|
|
|
* **End Boundary**: Detect when resistance STABILIZES at high value after parting spikes.
|
|
|
* Primary indicator: Resistance (green) shoots UP, exhibits parting spikes, then STABILIZES at high/infinite value
|
|
|
* Cross-reference: Travel (red) should be decreasing (opening motion)
|
|
|
* Cross-reference: Current (blue) may drop or fluctuate during final arc extinction
|
|
|
* **Typical Duration**: 40-80 ms (Zone 4 typically ends around 280-340 ms total time)
|
|
|
* **Detection Logic**: Find where 'res' completes its rise and spike activity, becoming constant at high value
|
|
|
* **CRITICAL**: Do NOT extend this zone too long - end AS SOON AS resistance stabilizes
|
|
|
|
|
|
**Zone 5: Final Open State (Fully Open)**
|
|
|
* **Physical Meaning**: The contacts are fully separated with an air gap. No current flows, resistance is infinite. The breaker is in its fully open, non-conducting state.
|
|
|
* **Start**: End of Zone 4
|
|
|
* **End**: The last time point in the dataset
|
|
|
* **Characteristics**:
|
|
|
* Resistance (green): Very high/infinite (flat line at top)
|
|
|
* Current (blue): Zero or near-zero
|
|
|
* Travel (red): Stable at minimum (fully open position)
|
|
|
|
|
|
**MULTI-CURVE ANALYSIS STRATEGY:**
|
|
|
1. Use Current (blue) to identify Zone 1 → Zone 2 transition (first current rise)
|
|
|
2. Use Resistance (green) to identify Zone 2 → Zone 3 transition (resistance settles to low plateau)
|
|
|
3. Use Travel (red) to identify Zone 3 → Zone 4 transition (travel peak and reversal)
|
|
|
4. Use Resistance (green) to identify Zone 4 → Zone 5 transition (resistance stabilizes at high value)
|
|
|
5. Always cross-validate boundaries using all three curves for consistency
|
|
|
|
|
|
**OUTPUT FORMAT (Strict JSON)**
|
|
|
Return ONLY this JSON object:
|
|
|
{{
|
|
|
"zones": {{
|
|
|
"zone_1_pre_contact": {{ "start_ms": float, "end_ms": float, "justification": "string (explain which curve indicators were used)" }},
|
|
|
"zone_2_arcing_engagement": {{ "start_ms": float, "end_ms": float, "justification": "string (explain which curve indicators were used)" }},
|
|
|
"zone_3_main_conduction": {{ "start_ms": float, "end_ms": float, "justification": "string (explain which curve indicators were used)" }},
|
|
|
"zone_4_parting": {{ "start_ms": float, "end_ms": float, "justification": "string (explain which curve indicators were used)" }},
|
|
|
"zone_5_final_open": {{ "start_ms": float, "end_ms": float, "justification": "string (explain which curve indicators were used)" }}
|
|
|
}},
|
|
|
"report_card": {{
|
|
|
"opening_speed": {{ "status": "Pass"|"Warning"|"Fail", "comment": "Assessment of travel curve steepness" }},
|
|
|
"contact_wear": {{ "status": "Pass"|"Warning"|"Fail", "comment": "Based on resistance fluctuations in Zone 2/4" }},
|
|
|
"timing_consistency": {{ "status": "Pass"|"Warning"|"Fail", "comment": "Are phases within expected ranges?" }},
|
|
|
"overall_health": {{ "status": "Healthy"|"Needs Review"|"Critical", "comment": "Overall summary" }}
|
|
|
}},
|
|
|
"detailed_analysis": "Provide a comprehensive technical analysis (in Markdown)..."
|
|
|
}}
|
|
|
"""
|
|
|
|
|
|
def create_dcrm_plot(df, zones):
|
|
|
|
|
|
fig = make_subplots(specs=[[{"secondary_y": True}]])
|
|
|
|
|
|
|
|
|
fig.add_trace(go.Scatter(x=df['Time_ms'], y=df['Current'], name="Current (A)", line=dict(color='#2980b9', width=2)), secondary_y=False)
|
|
|
fig.add_trace(go.Scatter(x=df['Time_ms'], y=df['Resistance'], name="Resistance (uOhm)", line=dict(color='#27ae60', width=2)), secondary_y=False)
|
|
|
fig.add_trace(go.Scatter(x=df['Time_ms'], y=df['Travel'], name="Travel (mm)", line=dict(color='#c0392b', width=2)), secondary_y=True)
|
|
|
|
|
|
|
|
|
zone_colors = {
|
|
|
"zone_1_pre_contact": "rgba(52, 152, 219, 0.1)",
|
|
|
"zone_2_arcing_engagement": "rgba(231, 76, 60, 0.1)",
|
|
|
"zone_3_main_conduction": "rgba(46, 204, 113, 0.1)",
|
|
|
"zone_4_parting": "rgba(155, 89, 182, 0.1)",
|
|
|
"zone_5_final_open": "rgba(149, 165, 166, 0.1)"
|
|
|
}
|
|
|
|
|
|
|
|
|
for zone_name, details in zones.items():
|
|
|
start = details.get("start_ms")
|
|
|
end = details.get("end_ms")
|
|
|
color = zone_colors.get(zone_name, "rgba(0,0,0,0)")
|
|
|
|
|
|
if start is not None and end is not None:
|
|
|
fig.add_vrect(
|
|
|
x0=start, x1=end,
|
|
|
fillcolor=color, opacity=1,
|
|
|
layer="below", line_width=0,
|
|
|
annotation_text=zone_name.split('_')[1].upper(),
|
|
|
annotation_position="top left",
|
|
|
annotation_font_color="#7f8c8d"
|
|
|
)
|
|
|
|
|
|
fig.update_layout(
|
|
|
title_text="<b>Main Signals & Zones</b>",
|
|
|
height=500,
|
|
|
hovermode="x unified",
|
|
|
plot_bgcolor="white",
|
|
|
paper_bgcolor="white",
|
|
|
font=dict(family="Segoe UI, sans-serif"),
|
|
|
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
|
|
|
margin=dict(l=20, r=20, t=60, b=20)
|
|
|
)
|
|
|
fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='#f0f0f0')
|
|
|
fig.update_yaxes(title_text="Current / Resistance", secondary_y=False, showgrid=True, gridwidth=1, gridcolor='#f0f0f0')
|
|
|
fig.update_yaxes(title_text="Travel", secondary_y=True, showgrid=False)
|
|
|
|
|
|
return fig
|
|
|
|
|
|
def create_velocity_plot(df):
|
|
|
|
|
|
|
|
|
|
|
|
df['Velocity'] = df['Travel'].diff() / df['Time_ms'].diff()
|
|
|
|
|
|
fig = go.Figure()
|
|
|
fig.add_trace(go.Scatter(x=df['Time_ms'], y=df['Velocity'], name="Velocity (m/s)", line=dict(color='#e67e22', width=2), fill='tozeroy'))
|
|
|
|
|
|
fig.update_layout(
|
|
|
title_text="<b>Contact Velocity Profile</b>",
|
|
|
height=300,
|
|
|
hovermode="x unified",
|
|
|
plot_bgcolor="white",
|
|
|
paper_bgcolor="white",
|
|
|
font=dict(family="Segoe UI, sans-serif"),
|
|
|
margin=dict(l=20, r=20, t=40, b=20)
|
|
|
)
|
|
|
fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='#f0f0f0')
|
|
|
fig.update_yaxes(title_text="Velocity (m/s)", showgrid=True, gridwidth=1, gridcolor='#f0f0f0')
|
|
|
return fig
|
|
|
|
|
|
def create_resistance_zoom_plot(df):
|
|
|
fig = go.Figure()
|
|
|
fig.add_trace(go.Scatter(x=df['Time_ms'], y=df['Resistance'], name="Resistance", line=dict(color='#27ae60', width=2)))
|
|
|
|
|
|
fig.update_layout(
|
|
|
title_text="<b>Detailed Resistance (Log Scale)</b>",
|
|
|
height=300,
|
|
|
hovermode="x unified",
|
|
|
plot_bgcolor="white",
|
|
|
paper_bgcolor="white",
|
|
|
font=dict(family="Segoe UI, sans-serif"),
|
|
|
yaxis_type="log",
|
|
|
margin=dict(l=20, r=20, t=40, b=20)
|
|
|
)
|
|
|
fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='#f0f0f0')
|
|
|
fig.update_yaxes(title_text="Resistance (uOhm)", showgrid=True, gridwidth=1, gridcolor='#f0f0f0')
|
|
|
return fig
|
|
|
|
|
|
def enrich_data_with_zones(df, llm):
|
|
|
"""
|
|
|
Uses the LLM to identify zones and adds a 'Zone' column to the DataFrame.
|
|
|
"""
|
|
|
try:
|
|
|
|
|
|
data_str = df.to_string(index=False)
|
|
|
prompt_text = get_dcrm_prompt(data_str)
|
|
|
|
|
|
|
|
|
|
|
|
fig = make_subplots(specs=[[{"secondary_y": True}]])
|
|
|
fig.add_trace(go.Scatter(x=df['Time_ms'], y=df['Current'], name="Current", line=dict(color='blue')), secondary_y=False)
|
|
|
fig.add_trace(go.Scatter(x=df['Time_ms'], y=df['Resistance'], name="Resistance", line=dict(color='green')), secondary_y=False)
|
|
|
fig.add_trace(go.Scatter(x=df['Time_ms'], y=df['Travel'], name="Travel", line=dict(color='red')), secondary_y=True)
|
|
|
fig.update_layout(title="DCRM Graph", showlegend=True)
|
|
|
|
|
|
|
|
|
img_bytes = fig.to_image(format="png", width=1024, height=600)
|
|
|
base64_image = base64.b64encode(img_bytes).decode('utf-8')
|
|
|
|
|
|
|
|
|
message = HumanMessage(
|
|
|
content=[
|
|
|
{"type": "text", "text": prompt_text},
|
|
|
{
|
|
|
"type": "image_url",
|
|
|
"image_url": {"url": f"data:image/png;base64,{base64_image}"}
|
|
|
}
|
|
|
]
|
|
|
)
|
|
|
|
|
|
|
|
|
response = llm.invoke([message])
|
|
|
content = response.content.replace("```json", "").replace("```", "").strip()
|
|
|
result = json.loads(content)
|
|
|
zones = result.get("zones", {})
|
|
|
|
|
|
|
|
|
df['Zone'] = "Unknown"
|
|
|
|
|
|
for zone_name, details in zones.items():
|
|
|
start = details.get("start_ms")
|
|
|
end = details.get("end_ms")
|
|
|
if start is not None and end is not None:
|
|
|
|
|
|
short_name = zone_name.split('_')[1]
|
|
|
mask = (df['Time_ms'] >= start) & (df['Time_ms'] <= end)
|
|
|
df.loc[mask, 'Zone'] = f"Zone {short_name}"
|
|
|
|
|
|
return df, zones
|
|
|
except Exception as e:
|
|
|
st.error(f"Enrichment failed: {str(e)}")
|
|
|
return df, {}
|
|
|
|