File size: 6,698 Bytes
3da2703
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
Node 2: Parameter Anomaly Detector β€” Sub-env 1.

Pure rule-based heuristic: no LLM calls, no I/O, no side effects.
Receives the user's proposed generation config alongside diagnostic signals
forwarded from Node 1, and returns a structured ParamAnomalyAction.
"""

from src.schemas.subenv1 import (
    DirectionalFix,
    ParameterAnomaly,
    ParamAnomalyAction,
    ParamAnomalyObservation,
)


def detect_param_anomalies(obs: ParamAnomalyObservation) -> ParamAnomalyAction:
    """Detect parameter anomalies from a proposed generation config.

    Only keys present in ``obs.proposed_config`` are evaluated.  All decisions
    are deterministic and rule-based β€” no model inference, no external calls,
    no mutable state.

    Args:
        obs: Proposed config plus diagnostic signals from Node 1.

    Returns:
        A fully populated ParamAnomalyAction.
    """

    cfg = obs.proposed_config
    anomalies: list[ParameterAnomaly] = []

    # ------------------------------------------------------------------
    # Anomaly detection β€” check only keys present in proposed_config
    # ------------------------------------------------------------------

    # --- denoise_alt ---
    if "denoise_alt" in cfg:
        val = cfg["denoise_alt"]
        if obs.regime == "non_frontal" and val < 0.45:
            anomalies.append(
                ParameterAnomaly(
                    parameter="denoise_alt",
                    issue="too low for non-frontal regime β€” reference token loses lateral coverage",
                    severity="severe",
                    linked_failure_mode="reference_token_dropout",
                )
            )
        elif obs.regime == "complex_background" and val > 0.65:
            anomalies.append(
                ParameterAnomaly(
                    parameter="denoise_alt",
                    issue="too high for complex background β€” risks washing out identity",
                    severity="moderate",
                    linked_failure_mode="identity_collapse",
                )
            )

    # --- cfg ---
    if "cfg" in cfg:
        val = cfg["cfg"]
        if obs.background_complexity_score > 0.6 and val > 7.0:
            anomalies.append(
                ParameterAnomaly(
                    parameter="cfg",
                    issue="elevated CFG with complex background β€” attention bleeds into non-face regions",
                    severity="moderate",
                    linked_failure_mode="background_bleed",
                )
            )

    # --- eta ---
    if "eta" in cfg:
        val = cfg["eta"]
        if val > 0.12 and obs.image_usability_score < 0.5:
            anomalies.append(
                ParameterAnomaly(
                    parameter="eta",
                    issue="high stochasticity with weak reference β€” identity drifts across frames",
                    severity="moderate",
                    linked_failure_mode="identity_collapse",
                )
            )

    # ------------------------------------------------------------------
    # Config risk level
    # ------------------------------------------------------------------
    severities = [a.severity for a in anomalies]
    moderate_count = severities.count("moderate")

    if "severe" in severities:
        config_risk_level = "dangerous"
    elif moderate_count >= 2:
        config_risk_level = "risky"
    elif moderate_count == 1:
        config_risk_level = "marginal"
    else:
        config_risk_level = "safe"

    # ------------------------------------------------------------------
    # Predicted failure modes β€” unique, in order of first appearance
    # ------------------------------------------------------------------
    seen: set[str] = set()
    predicted_failure_modes: list[str] = []
    for a in anomalies:
        if a.linked_failure_mode not in seen:
            seen.add(a.linked_failure_mode)
            predicted_failure_modes.append(a.linked_failure_mode)

    # ------------------------------------------------------------------
    # Directional fixes β€” one per anomaly, keyed on failure mode + param
    # ------------------------------------------------------------------
    directional_fixes: list[DirectionalFix] = []
    for a in anomalies:
        if a.linked_failure_mode == "reference_token_dropout":
            directional_fixes.append(
                DirectionalFix(
                    target="reference_token_strength",
                    direction="increase",
                    rationale=(
                        "compensates for lateral pose β€” keeps identity anchored "
                        "in side-facing frames"
                    ),
                    priority="critical",
                )
            )
        elif a.linked_failure_mode == "identity_collapse" and a.parameter == "eta":
            directional_fixes.append(
                DirectionalFix(
                    target="stochasticity (eta)",
                    direction="decrease",
                    rationale="reduces frame-to-frame identity variance",
                    priority="critical",
                )
            )
        elif a.linked_failure_mode == "identity_collapse" and a.parameter == "denoise_alt":
            directional_fixes.append(
                DirectionalFix(
                    target="denoise_alt",
                    direction="decrease",
                    rationale="prevents identity washout in complex background",
                    priority="recommended",
                )
            )
        elif a.linked_failure_mode == "background_bleed":
            directional_fixes.append(
                DirectionalFix(
                    target="guidance_scale",
                    direction="decrease",
                    rationale="prevents attention competition with background elements",
                    priority="recommended",
                )
            )

    # ------------------------------------------------------------------
    # Summary
    # ------------------------------------------------------------------
    n = len(anomalies)
    summary = f"Config risk: {config_risk_level}. {n} anomaly/anomalies detected."
    if anomalies:
        # Top severity: prefer severe, then moderate, then minor
        top = max(anomalies, key=lambda a: {"severe": 2, "moderate": 1, "minor": 0}[a.severity])
        summary += f" Top issue ({top.severity}): {top.issue}"

    return ParamAnomalyAction(
        config_risk_level=config_risk_level,
        anomalies=anomalies,
        predicted_failure_modes=predicted_failure_modes,
        directional_fixes=directional_fixes,
        summary=summary,
    )