Spaces:
Paused
Paused
File size: 5,800 Bytes
0127c21 | 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 | """
CONFLICTING evidence state.
Phase 12. Named in the master brief Β§4B as one of four Evidence
Sufficiency Engine outcomes (SUFFICIENT / INSUFFICIENT / CONFLICTING /
UNSUPPORTED) but never implemented through Phase 11 β PHASE11.md Β§5
flagged it explicitly as deferred, not forgotten.
SCOPE β what this deliberately is and is not:
This is NOT a new model, and it does NOT invent a second opinion where
only one signal exists. It cross-checks two signals SatQuery ALREADY
computes independently for the same request:
1. change_vqa: the deterministic pixel-diff change_map (magnitude:
high/medium/low) vs. the free-text answer from the composite-image
VQA bridge (models/change.py's _ChangeVQABridge). These are already
shown side-by-side in the API response (see PHASE7 notes) but were
never checked against each other for agreement.
2. optical_sar: the fused agreement_pct vs. how much of the target
each modality flags independently (optical_only_pct / sar_only_pct).
If the two modalities are pointing at substantially different areas
rather than overlapping, that is a real cross-modal disagreement,
not a fusion result.
Both checks are plain arithmetic on numbers/strings SatQuery already
has β no external verification model, no fabricated second confidence
score. If a judge asks "what verifies the verification", the honest
answer is: these are two views of the same deterministic pipeline
outputs, not two independent models. Real multi-model verification
(master brief Β§13B) is a larger, separate feature β not built here.
"""
from __future__ import annotations
from dataclasses import dataclass
# Words that, if present in a change-VQA free-text answer, assert that
# something DID change. Deliberately small and literal β this is a
# keyword heuristic, not an NLU classifier; it can be wrong on unusual
# phrasing, which is fine because CONFLICTING is advisory, not blocking.
_AFFIRMS_CHANGE = (
"increased", "decreased", "expanded", "reduced", "grew", "shrank",
"new construction", "new built-up", "damage", "damaged", "flooded",
"flooding", "cleared", "deforest", "significant change", "yes,",
)
_DENIES_CHANGE = (
"no change", "no significant change", "unchanged", "remained the same",
"no evidence of change", "not changed", "no visible change", "stable",
)
# Pixel-diff change_level below which we call it "the heuristic saw
# almost nothing" for conflict purposes. Matches models/change.py's own
# _MEDIUM_CHANGE_PCT boundary so this stays consistent with the change
# map's own high/medium/low labeling rather than inventing a new cutoff.
_LOW_LEVEL = "low"
_HIGH_LEVEL = "high"
@dataclass
class ConflictResult:
conflicting: bool
reason: str | None # human-readable, goes straight into the trace/note
def check_change_conflict(change_level: str, changed_pixel_pct: float, vqa_answer: str) -> ConflictResult:
"""Compare the deterministic pixel-diff change magnitude against the
change-VQA free-text answer's implied direction. Only ever flags a
conflict on a clear mismatch β an ambiguous/unmatched answer is left
alone rather than guessed at."""
answer_l = vqa_answer.lower()
affirms = any(k in answer_l for k in _AFFIRMS_CHANGE)
denies = any(k in answer_l for k in _DENIES_CHANGE)
if affirms and denies:
# Answer hedges both ways ("no major change, but some new
# structures") β not a clean signal either way, don't flag.
return ConflictResult(False, None)
if change_level == _LOW_LEVEL and affirms:
return ConflictResult(
True,
f"pixel-diff heuristic measured only {changed_pixel_pct}% of pixels changed (LOW), "
"but the composite-VQA answer describes a specific change having occurred β "
"the two signals disagree on whether meaningful change happened.",
)
if change_level == _HIGH_LEVEL and denies:
return ConflictResult(
True,
f"pixel-diff heuristic measured {changed_pixel_pct}% of pixels changed (HIGH), "
"but the composite-VQA answer states no meaningful change occurred β "
"the two signals disagree on whether meaningful change happened.",
)
return ConflictResult(False, None)
def check_optical_sar_conflict(
agreement_pct: float, optical_only_pct: float, sar_only_pct: float
) -> ConflictResult:
"""Flag cross-modal disagreement when optical and SAR are each
flagging a non-trivial, largely NON-overlapping area for the same
target β i.e. the two sensors are pointing at different things, not
confirming/complementing each other. A small disagreement is normal
and expected (see models/optical_sar.py's own disclosure); this only
fires when the disagreement dominates over the agreement."""
disagreement = optical_only_pct + sar_only_pct
if disagreement < 1.0:
# Too little signal from either modality to call this a real
# conflict β more likely just an empty/near-empty scene.
return ConflictResult(False, None)
# Conflict when each modality's independent signal is at least ~3x
# the overlap β i.e. agreement is a minority of what either sensor
# flagged on its own. Prototype policy threshold, not validated.
if agreement_pct * 3 < optical_only_pct and agreement_pct * 3 < sar_only_pct:
return ConflictResult(
True,
f"optical flags {optical_only_pct}% and SAR flags {sar_only_pct}% independently, "
f"but they only overlap on {agreement_pct}% β the two modalities are pointing at "
"substantially different areas rather than confirming each other for this target.",
)
return ConflictResult(False, None)
|