"""
alarm_id_extractor.py
─────────────────────────────────────────────────────────────────────────────
Extract alarm and annunciator IDs from nuclear plant text (ALM, ANN, process
tags, SCRAM/SI/AFAS setpoint conditions, etc.).
Plant-specific flexibility
──────────────────────────
Like ``doc_ref_extractor.py``, this module is driven by the same JSON *plant
profile* (see ``ner/data/plant_profiles/default_plant_profile.json``). Alarm
patterns live in the ``alarm_patterns`` array; each entry has a ``name``,
``pattern`` (regex string), and ``score`` (0–1 confidence).
False-positive filtering uses the ``false_positive_filters.excluded_prefixes_from_alarm``
list from the same profile, so document-reference prefixes (CR, WO, …) are
never returned as alarm IDs.
Output
──────
Each extracted alarm reference is an ``AlarmRef`` namedtuple:
pattern_name - stable pattern identifier (e.g. "short_alarm_id")
raw - original matched text (before normalization)
norm - normalized ID (uppercase, collapsed separators)
score - pattern confidence score (0–1) from the plant profile
"""
from __future__ import annotations
import re
from pathlib import Path
from typing import Dict, List, NamedTuple, Optional
from .doc_ref_extractor import load_doc_ref_profile
# ---------------------------------------------------------------------------
# Output type
# ---------------------------------------------------------------------------
[docs]
class AlarmRef(NamedTuple):
"""A single extracted alarm or annunciator reference."""
[docs]
pattern_name: str # stable pattern identifier, e.g. "short_alarm_id"
[docs]
raw: str # verbatim matched text
[docs]
norm: str # normalised ID (uppercase, canonical separator)
[docs]
score: float # pattern confidence score from plant profile [0, 1]
# ---------------------------------------------------------------------------
# Profile compilation
# ---------------------------------------------------------------------------
[docs]
def _compile_alarm_profile(profile: Dict) -> List[tuple]:
"""Return list of (pattern_name, score, compiled_pattern) from profile.
Patterns are compiled case-insensitively.
"""
result = []
for entry in profile.get("alarm_patterns", []):
name = entry.get("name", "unknown")
score = float(entry.get("score", 0.5))
raw_pattern = entry.get("pattern", "")
if raw_pattern:
try:
compiled = re.compile(raw_pattern, re.IGNORECASE)
result.append((name, score, compiled))
except re.error:
pass # silently skip malformed patterns
return result
# ---------------------------------------------------------------------------
# False-positive filtering
# ---------------------------------------------------------------------------
[docs]
def _make_alarm_fp_checker(profile: Dict):
"""Return a callable that returns True when a norm is a false positive
alarm ID and should be dropped."""
fp = profile.get("false_positive_filters", {})
excluded = {p.upper() for p in fp.get("excluded_prefixes_from_alarm", [])}
def is_fp(norm: str) -> bool:
prefix = norm.split("-")[0] if "-" in norm else norm[:3]
return prefix.upper() in excluded
return is_fp
# ---------------------------------------------------------------------------
# Normalization
# ---------------------------------------------------------------------------
[docs]
def _normalize_alarm_ref(raw: str) -> str:
"""Return a canonical alarm reference string.
- Uppercase
- Collapse any internal whitespace to a single hyphen
- Collapse multiple consecutive hyphens
- Strip leading/trailing punctuation
"""
norm = raw.strip().upper()
norm = re.sub(r"\s+", "-", norm)
norm = re.sub(r"-{2,}", "-", norm)
norm = norm.strip("-")
return norm
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------