Add artifact.timing_analysis/src/artifact_timing_analysis/core.py
This commit is contained in:
parent
cac7e4d126
commit
934712d4d8
1 changed files with 104 additions and 0 deletions
104
artifact.timing_analysis/src/artifact_timing_analysis/core.py
Normal file
104
artifact.timing_analysis/src/artifact_timing_analysis/core.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
from __future__ import annotations
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any
|
||||
import pandas as pd
|
||||
import statistics
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TimingData:
|
||||
timestamp: datetime
|
||||
t_gate_read: float
|
||||
t_index_visible: float
|
||||
offset: float
|
||||
|
||||
@staticmethod
|
||||
def from_dict(d: Dict[str, Any]) -> 'TimingData':
|
||||
try:
|
||||
ts = d.get('timestamp')
|
||||
if isinstance(ts, str):
|
||||
ts = datetime.fromisoformat(ts)
|
||||
elif not isinstance(ts, datetime):
|
||||
raise ValueError('Invalid timestamp format')
|
||||
|
||||
return TimingData(
|
||||
timestamp=ts,
|
||||
t_gate_read=float(d.get('t_gate_read', 0.0)),
|
||||
t_index_visible=float(d.get('t_index_visible', 0.0)),
|
||||
offset=float(d.get('offset', d.get('t_index_visible', 0.0) - d.get('t_gate_read', 0.0)))
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Invalid TimingData input: {e}")
|
||||
raise
|
||||
|
||||
|
||||
_anomaly_cache: List[Dict[str, Any]] = []
|
||||
|
||||
def analyze_timing_offsets(timing_data: List[TimingData]) -> Dict[str, Any]:
|
||||
if not timing_data:
|
||||
raise ValueError("timing_data list is empty")
|
||||
|
||||
df = pd.DataFrame([vars(td) for td in timing_data])
|
||||
if 'offset' not in df.columns:
|
||||
raise ValueError('Missing offset field in timing data')
|
||||
|
||||
valid_offsets = df['offset'].dropna()
|
||||
if len(valid_offsets) == 0:
|
||||
raise ValueError('No valid offset values found')
|
||||
|
||||
mean = statistics.fmean(valid_offsets)
|
||||
p95 = valid_offsets.quantile(0.95)
|
||||
stddev = statistics.pstdev(valid_offsets)
|
||||
|
||||
anomaly_threshold = mean + 2 * stddev
|
||||
anomalies = df[df['offset'] > anomaly_threshold]
|
||||
|
||||
global _anomaly_cache
|
||||
_anomaly_cache = anomalies.to_dict(orient='records')
|
||||
|
||||
result = {
|
||||
'count': len(df),
|
||||
'mean_offset': mean,
|
||||
'p95_offset': float(p95),
|
||||
'stddev_offset': stddev,
|
||||
'anomaly_threshold': anomaly_threshold,
|
||||
'anomaly_count': len(anomalies)
|
||||
}
|
||||
|
||||
# Assertions for CI readiness
|
||||
assert 'mean_offset' in result
|
||||
assert isinstance(result['count'], int)
|
||||
|
||||
logger.info(f"Analyzed {len(df)} records, found {len(anomalies)} anomalies")
|
||||
return result
|
||||
|
||||
|
||||
def report_timing_anomalies() -> str:
|
||||
global _anomaly_cache
|
||||
if not _anomaly_cache:
|
||||
return 'No anomalies detected.'
|
||||
|
||||
lines = [
|
||||
'Timing Anomalies Report',
|
||||
'=======================',
|
||||
f'Total anomalies: {len(_anomaly_cache)}',
|
||||
''
|
||||
]
|
||||
|
||||
for i, anom in enumerate(_anomaly_cache, 1):
|
||||
ts = anom.get('timestamp')
|
||||
if isinstance(ts, datetime):
|
||||
ts_str = ts.isoformat()
|
||||
else:
|
||||
ts_str = str(ts)
|
||||
lines.append(f"{i}. Timestamp: {ts_str}, Offset: {anom.get('offset'):.4f}")
|
||||
|
||||
report = '\n'.join(lines)
|
||||
logger.info("Generated anomaly report")
|
||||
return report
|
||||
Loading…
Reference in a new issue