Add policy_evaluation/src/policy_evaluation/core.py
This commit is contained in:
parent
78610b1f7a
commit
9c890b9304
1 changed files with 116 additions and 0 deletions
116
policy_evaluation/src/policy_evaluation/core.py
Normal file
116
policy_evaluation/src/policy_evaluation/core.py
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
import json
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from dataclasses import dataclass, asdict
|
||||||
|
from typing import List, Dict, Any
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format='[%(levelname)s] %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LogEntry:
|
||||||
|
t_publish: float
|
||||||
|
t_gate_read: float
|
||||||
|
t_index_visible: float
|
||||||
|
pinned_flag: bool
|
||||||
|
timeouts: int
|
||||||
|
drift_signature: str
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate(cls, entry: Dict[str, Any]) -> 'LogEntry':
|
||||||
|
required = {
|
||||||
|
't_publish': float,
|
||||||
|
't_gate_read': float,
|
||||||
|
't_index_visible': float,
|
||||||
|
'pinned_flag': bool,
|
||||||
|
'timeouts': int,
|
||||||
|
'drift_signature': str,
|
||||||
|
}
|
||||||
|
for k, typ in required.items():
|
||||||
|
if k not in entry:
|
||||||
|
raise ValueError(f"Missing field '{k}' in log entry")
|
||||||
|
if not isinstance(entry[k], typ):
|
||||||
|
# allow np types for numeric
|
||||||
|
if typ in (float, int) and isinstance(entry[k], (np.floating, np.integer)):
|
||||||
|
continue
|
||||||
|
raise TypeError(f"Field '{k}' must be of type {typ.__name__}, got {type(entry[k]).__name__}")
|
||||||
|
return cls(**{k: entry[k] for k in required})
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PolicyResults:
|
||||||
|
p99_coverage: float
|
||||||
|
remaining_missing_cases: int
|
||||||
|
conversion_rates: float
|
||||||
|
max_wait_time: float
|
||||||
|
|
||||||
|
def to_json(self) -> Dict[str, Any]:
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_policies(log_data: List[Dict[str, Any]]) -> PolicyResults:
|
||||||
|
if not isinstance(log_data, list):
|
||||||
|
raise TypeError("log_data must be a list of dictionaries")
|
||||||
|
validated_entries = [LogEntry.validate(entry) for entry in log_data]
|
||||||
|
if not validated_entries:
|
||||||
|
return PolicyResults(0.0, 0, 0.0, 0.0)
|
||||||
|
|
||||||
|
df = pd.DataFrame([asdict(e) for e in validated_entries])
|
||||||
|
df['wait_time'] = df['t_index_visible'] - df['t_publish']
|
||||||
|
|
||||||
|
try:
|
||||||
|
p99 = np.percentile(df['wait_time'], 99)
|
||||||
|
except IndexError:
|
||||||
|
p99 = 0.0
|
||||||
|
|
||||||
|
missing_cases = int((df['drift_signature'] != 'OK').sum())
|
||||||
|
|
||||||
|
# Conversion: approximate via success ratio (no timeouts)
|
||||||
|
conversions = len(df[df['timeouts'] == 0]) / max(len(df), 1)
|
||||||
|
|
||||||
|
max_wait = float(df['wait_time'].max()) if not df.empty else 0.0
|
||||||
|
|
||||||
|
result = PolicyResults(
|
||||||
|
p99_coverage=float(p99),
|
||||||
|
remaining_missing_cases=missing_cases,
|
||||||
|
conversion_rates=float(conversions),
|
||||||
|
max_wait_time=max_wait,
|
||||||
|
)
|
||||||
|
logger.info("Policy evaluation complete: %s", result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _load_jsonl(path: Path) -> List[Dict[str, Any]]:
|
||||||
|
with path.open('r', encoding='utf-8') as f:
|
||||||
|
return [json.loads(line.strip()) for line in f if line.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Evaluate storage policies from log data.")
|
||||||
|
parser.add_argument('--input', required=True, help='Pfad zur JSONL-Logdatei mit Messdaten')
|
||||||
|
parser.add_argument('--output', required=True, help='Pfad zur Ausgabedatei mit Evaluierungsergebnissen')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
input_path = Path(args.input)
|
||||||
|
output_path = Path(args.output)
|
||||||
|
|
||||||
|
if not input_path.exists():
|
||||||
|
raise FileNotFoundError(f"Input file not found: {input_path}")
|
||||||
|
|
||||||
|
log_data = _load_jsonl(input_path)
|
||||||
|
result = evaluate_policies(log_data)
|
||||||
|
|
||||||
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with output_path.open('w', encoding='utf-8') as f:
|
||||||
|
json.dump(result.to_json(), f, indent=2)
|
||||||
|
|
||||||
|
logger.info("Results written to %s", output_path)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Loading…
Reference in a new issue