Lane 3: Geometric evaluation for fraction_decrease
This commit is contained in:
parent
2a17d5b6f3
commit
8383b852d0
5 changed files with 177 additions and 67 deletions
|
|
@ -251,3 +251,99 @@ def clear_resolver_cache() -> None:
|
||||||
"""
|
"""
|
||||||
_pack_lexicon_for.cache_clear()
|
_pack_lexicon_for.cache_clear()
|
||||||
_pack_glosses_for.cache_clear()
|
_pack_glosses_for.cache_clear()
|
||||||
|
_pack_lemma_to_entry_id.cache_clear()
|
||||||
|
_pack_senses_for.cache_clear()
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=16)
|
||||||
|
def _pack_lemma_to_entry_id(pack_id: str) -> dict[str, str]:
|
||||||
|
"""Return ``{lemma_lower: entry_id}`` from the pack's lexicon.jsonl."""
|
||||||
|
lexicon_path = _PACK_ROOT / pack_id / "lexicon.jsonl"
|
||||||
|
if not lexicon_path.exists():
|
||||||
|
return {}
|
||||||
|
out: dict[str, str] = {}
|
||||||
|
for line in lexicon_path.read_text(encoding="utf-8").splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
entry = json.loads(line)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
continue
|
||||||
|
lemma = entry.get("lemma") or entry.get("surface")
|
||||||
|
if not lemma or not isinstance(lemma, str):
|
||||||
|
continue
|
||||||
|
entry_id = entry.get("entry_id")
|
||||||
|
if isinstance(entry_id, str) and entry_id.strip():
|
||||||
|
out[lemma.lower()] = entry_id.strip()
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=16)
|
||||||
|
def _pack_senses_for(pack_id: str) -> dict[str, dict]:
|
||||||
|
"""Return ``{lemma_id: geometric_signature}`` from the pack's optional
|
||||||
|
``senses.jsonl`` file. Also maps ``lemma`` as a fallback key.
|
||||||
|
"""
|
||||||
|
path = _PACK_ROOT / pack_id / "senses.jsonl"
|
||||||
|
if not path.exists():
|
||||||
|
return {}
|
||||||
|
out: dict[str, dict] = {}
|
||||||
|
for line in path.read_text(encoding="utf-8").splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
entry = json.loads(line)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
geometric_signature = entry.get("geometric_signature")
|
||||||
|
if not isinstance(geometric_signature, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
lemma_id = entry.get("lemma_id")
|
||||||
|
if isinstance(lemma_id, str) and lemma_id.strip():
|
||||||
|
out[lemma_id.strip()] = geometric_signature
|
||||||
|
|
||||||
|
# Optional fallback for direct lemma matching if lemma is present
|
||||||
|
lemma = entry.get("lemma")
|
||||||
|
if isinstance(lemma, str) and lemma.strip():
|
||||||
|
out[lemma.strip().lower()] = geometric_signature
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_geometric_signature(
|
||||||
|
lemma: str,
|
||||||
|
pack_ids: tuple[str, ...] = DEFAULT_RESOLVABLE_PACK_IDS,
|
||||||
|
) -> tuple[str, dict] | None:
|
||||||
|
"""Return ``(pack_id, geometric_signature)`` for the first pack in
|
||||||
|
*pack_ids* that BOTH (a) ratifies *lemma* in its ``lexicon.jsonl``
|
||||||
|
AND (b) carries a geometric_signature for it in ``senses.jsonl``.
|
||||||
|
"""
|
||||||
|
if not lemma or not isinstance(lemma, str):
|
||||||
|
return None
|
||||||
|
key = lemma.strip().lower()
|
||||||
|
if not key:
|
||||||
|
return None
|
||||||
|
for pack_id in pack_ids:
|
||||||
|
lex = _pack_lexicon_for(pack_id)
|
||||||
|
if key not in lex:
|
||||||
|
continue
|
||||||
|
|
||||||
|
lemma_ids = _pack_lemma_to_entry_id(pack_id)
|
||||||
|
entry_id = lemma_ids.get(key)
|
||||||
|
|
||||||
|
senses = _pack_senses_for(pack_id)
|
||||||
|
|
||||||
|
if entry_id and entry_id in senses:
|
||||||
|
return (pack_id, senses[entry_id])
|
||||||
|
|
||||||
|
if key in senses:
|
||||||
|
return (pack_id, senses[key])
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ the **decrease by** delta (not the final value).
|
||||||
Narrow organ — not a generic affine equation parser, not ``N/M more than`` affine
|
Narrow organ — not a generic affine equation parser, not ``N/M more than`` affine
|
||||||
surfaces (0010-class), not percent-decrease. Promotion requires:
|
surfaces (0010-class), not percent-decrease. Promotion requires:
|
||||||
|
|
||||||
- exactly one ``decrease to N/M of`` forecast (no ``more than`` fraction affine);
|
- exactly one ``decrease to N/M of forecast`` (no ``more than`` fraction affine);
|
||||||
- question binds ``decrease`` + ``by`` (delta target, not final-value question);
|
- question binds ``decrease`` + ``by`` (delta target, not final-value question);
|
||||||
- exactly one explicit current/base quantity with a unit;
|
- exactly one explicit current/base quantity with a unit;
|
||||||
- hazard refusal (percent, goal language, multiple decrease-to fractions).
|
- hazard refusal (percent, goal language, multiple decrease-to fractions).
|
||||||
|
|
@ -20,21 +20,20 @@ Deterministic; sealed module (no ``chat/`` import).
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from algebra.cga import embed_point, read_scalar_e1
|
||||||
|
from algebra.versor import versor_apply
|
||||||
from generate.derivation.clauses import segment_clauses
|
from generate.derivation.clauses import segment_clauses
|
||||||
from generate.derivation.extract import extract_quantities
|
from generate.derivation.extract import extract_quantities
|
||||||
from generate.derivation.model import GroundedDerivation, Quantity, Step
|
from generate.derivation.model import GroundedDerivation, Quantity, Step
|
||||||
from generate.derivation.target import _question_clause
|
from generate.derivation.target import _question_clause
|
||||||
from generate.derivation.verify import Resolution, SelfVerification, select_self_verified
|
from generate.derivation.verify import Resolution, SelfVerification, select_self_verified
|
||||||
|
from generate.problem_frame_contracts import ContractAssessment
|
||||||
from generate.math_roundtrip import _tokens
|
from generate.math_roundtrip import _tokens
|
||||||
|
|
||||||
_DECREASE_TO_FRACTION_RE: Final[re.Pattern[str]] = re.compile(
|
|
||||||
r"decrease\s+to\s+(\d+)\s*/\s*(\d+)\s+of",
|
|
||||||
re.IGNORECASE,
|
|
||||||
)
|
|
||||||
_EXTRA_FRACTION_RE: Final[re.Pattern[str]] = re.compile(r"\d+\s*/\s*\d+")
|
|
||||||
_GOAL_INTENT: Final[frozenset[str]] = frozenset(
|
_GOAL_INTENT: Final[frozenset[str]] = frozenset(
|
||||||
{"want", "wants", "wanted", "need", "needs", "hoping", "hopes", "plans", "aims", "goal"}
|
{"want", "wants", "wanted", "need", "needs", "hoping", "hopes", "plans", "aims", "goal"}
|
||||||
)
|
)
|
||||||
|
|
@ -55,21 +54,6 @@ def _asks_final_value(question_clause: str) -> bool:
|
||||||
return bool(_FINAL_VALUE_CUES & tokens) or "temperature" in tokens
|
return bool(_FINAL_VALUE_CUES & tokens) or "temperature" in tokens
|
||||||
|
|
||||||
|
|
||||||
def _target_fraction(problem_text: str) -> tuple[int, int, str] | None:
|
|
||||||
matches = list(_DECREASE_TO_FRACTION_RE.finditer(problem_text))
|
|
||||||
if len(matches) != 1:
|
|
||||||
return None
|
|
||||||
num_s, den_s = matches[0].group(1), matches[0].group(2)
|
|
||||||
try:
|
|
||||||
num = int(num_s)
|
|
||||||
den = int(den_s)
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
if num <= 0 or den <= 0 or num >= den:
|
|
||||||
return None
|
|
||||||
return num, den, f"{num_s}/{den_s}"
|
|
||||||
|
|
||||||
|
|
||||||
def _has_hazard_surface(problem_text: str, question_clause: str) -> bool:
|
def _has_hazard_surface(problem_text: str, question_clause: str) -> bool:
|
||||||
text_tokens = _tokens(problem_text)
|
text_tokens = _tokens(problem_text)
|
||||||
question_tokens = _tokens(question_clause)
|
question_tokens = _tokens(question_clause)
|
||||||
|
|
@ -81,18 +65,15 @@ def _has_hazard_surface(problem_text: str, question_clause: str) -> bool:
|
||||||
return True
|
return True
|
||||||
if _asks_final_value(question_clause):
|
if _asks_final_value(question_clause):
|
||||||
return True
|
return True
|
||||||
fractions = _EXTRA_FRACTION_RE.findall(problem_text)
|
return False
|
||||||
target = _target_fraction(problem_text)
|
|
||||||
if target is None:
|
|
||||||
return True
|
|
||||||
_, _, token = target
|
|
||||||
extra = [f for f in fractions if f.replace(" ", "") != token.replace(" ", "")]
|
|
||||||
return len(extra) > 0
|
|
||||||
|
|
||||||
|
|
||||||
def _current_base_quantity(problem_text: str, question_clause: str) -> Quantity | None:
|
def _current_base_quantity(
|
||||||
|
problem_text: str, question_clause: str, contract: ContractAssessment
|
||||||
|
) -> Quantity | None:
|
||||||
|
source_text = contract.bindings[0].source_span.text
|
||||||
for clause in segment_clauses(problem_text):
|
for clause in segment_clauses(problem_text):
|
||||||
if _DECREASE_TO_FRACTION_RE.search(clause):
|
if source_text in clause:
|
||||||
continue
|
continue
|
||||||
tokens = _tokens(clause)
|
tokens = _tokens(clause)
|
||||||
if "current" not in tokens and "now" not in tokens:
|
if "current" not in tokens and "now" not in tokens:
|
||||||
|
|
@ -103,7 +84,7 @@ def _current_base_quantity(problem_text: str, question_clause: str) -> Quantity
|
||||||
for clause in segment_clauses(problem_text):
|
for clause in segment_clauses(problem_text):
|
||||||
if clause == question_clause:
|
if clause == question_clause:
|
||||||
continue
|
continue
|
||||||
if _DECREASE_TO_FRACTION_RE.search(clause):
|
if source_text in clause:
|
||||||
continue
|
continue
|
||||||
quantities = [q for q in extract_quantities(clause) if q.unit]
|
quantities = [q for q in extract_quantities(clause) if q.unit]
|
||||||
if len(quantities) == 1:
|
if len(quantities) == 1:
|
||||||
|
|
@ -111,33 +92,38 @@ def _current_base_quantity(problem_text: str, question_clause: str) -> Quantity
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def build_fraction_decrease(problem_text: str) -> GroundedDerivation | None:
|
def build_fraction_decrease(
|
||||||
"""Construct ``base × (1 − N/M)`` decrease delta, or ``None``."""
|
problem_text: str, contract: ContractAssessment
|
||||||
|
) -> GroundedDerivation | None:
|
||||||
|
"""Construct mathematical decrease delta, or ``None``."""
|
||||||
|
if not contract.is_runnable or not contract.bindings:
|
||||||
|
return None
|
||||||
|
|
||||||
question_clause = _question_clause(problem_text)
|
question_clause = _question_clause(problem_text)
|
||||||
if not _asks_decrease_delta(question_clause):
|
if not _asks_decrease_delta(question_clause):
|
||||||
return None
|
return None
|
||||||
if _has_hazard_surface(problem_text, question_clause):
|
if _has_hazard_surface(problem_text, question_clause):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
fraction = _target_fraction(problem_text)
|
base = _current_base_quantity(problem_text, question_clause, contract)
|
||||||
base = _current_base_quantity(problem_text, question_clause)
|
if base is None:
|
||||||
if fraction is None or base is None:
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
num, den, source_token = fraction
|
D = contract.bindings[0].geometric_payload
|
||||||
delta_factor = 1.0 - (num / den)
|
v = base.value
|
||||||
if delta_factor <= 0:
|
Q = embed_point([v, 0, 0], dtype=np.float64)
|
||||||
return None
|
Q_new = versor_apply(D, Q)
|
||||||
|
new_value = read_scalar_e1(Q_new)
|
||||||
|
|
||||||
return GroundedDerivation(
|
return GroundedDerivation(
|
||||||
start=base,
|
start=base,
|
||||||
steps=(
|
steps=(
|
||||||
Step(
|
Step(
|
||||||
op="multiply",
|
op="subtract",
|
||||||
operand=Quantity(
|
operand=Quantity(
|
||||||
value=delta_factor,
|
value=new_value,
|
||||||
unit="",
|
unit=base.unit,
|
||||||
source_token=source_token,
|
source_token=contract.bindings[0].source_span.text,
|
||||||
),
|
),
|
||||||
cue="decrease",
|
cue="decrease",
|
||||||
),
|
),
|
||||||
|
|
@ -146,34 +132,35 @@ def build_fraction_decrease(problem_text: str) -> GroundedDerivation | None:
|
||||||
|
|
||||||
|
|
||||||
def _self_verifies_fraction_decrease(
|
def _self_verifies_fraction_decrease(
|
||||||
derivation: GroundedDerivation, problem_text: str
|
derivation: GroundedDerivation, problem_text: str, contract: ContractAssessment
|
||||||
) -> SelfVerification:
|
) -> SelfVerification:
|
||||||
from generate.derivation.verify import _base_reasons
|
from generate.derivation.verify import _base_reasons
|
||||||
|
|
||||||
tokens = _tokens(problem_text)
|
tokens = _tokens(problem_text)
|
||||||
reasons = list(_base_reasons(derivation, tokens))
|
reasons = list(_base_reasons(derivation, tokens))
|
||||||
|
|
||||||
fraction = _target_fraction(problem_text)
|
if not contract.bindings:
|
||||||
if fraction is None:
|
|
||||||
reasons.append("missing decrease-to fraction forecast")
|
reasons.append("missing decrease-to fraction forecast")
|
||||||
else:
|
else:
|
||||||
_, _, token = fraction
|
token = contract.bindings[0].source_span.text
|
||||||
if token not in problem_text.replace(" ", ""):
|
if token not in problem_text:
|
||||||
reasons.append(f"fraction token {token!r} not grounded in text")
|
reasons.append(f"fraction token {token!r} not grounded in text")
|
||||||
|
|
||||||
base = _current_base_quantity(problem_text, _question_clause(problem_text))
|
base = _current_base_quantity(problem_text, _question_clause(problem_text), contract)
|
||||||
if base is None:
|
if base is None:
|
||||||
reasons.append("missing explicit current/base quantity")
|
reasons.append("missing explicit current/base quantity")
|
||||||
|
|
||||||
return SelfVerification(verified=not reasons, reasons=tuple(reasons))
|
return SelfVerification(verified=not reasons, reasons=tuple(reasons))
|
||||||
|
|
||||||
|
|
||||||
def compose_fraction_decrease(problem_text: str) -> Resolution | None:
|
def compose_fraction_decrease(
|
||||||
|
problem_text: str, contract: ContractAssessment
|
||||||
|
) -> Resolution | None:
|
||||||
"""Gate the typed fraction-decrease chain through self-verification."""
|
"""Gate the typed fraction-decrease chain through self-verification."""
|
||||||
derivation = build_fraction_decrease(problem_text)
|
derivation = build_fraction_decrease(problem_text, contract)
|
||||||
if derivation is None:
|
if derivation is None:
|
||||||
return None
|
return None
|
||||||
if not _self_verifies_fraction_decrease(derivation, problem_text).verified:
|
if not _self_verifies_fraction_decrease(derivation, problem_text, contract).verified:
|
||||||
return None
|
return None
|
||||||
return Resolution(
|
return Resolution(
|
||||||
answer=derivation.answer,
|
answer=derivation.answer,
|
||||||
|
|
@ -182,6 +169,8 @@ def compose_fraction_decrease(problem_text: str) -> Resolution | None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def resolve_promotable_fraction_decrease(problem_text: str) -> Resolution | None:
|
def resolve_promotable_fraction_decrease(
|
||||||
|
problem_text: str, contract: ContractAssessment
|
||||||
|
) -> Resolution | None:
|
||||||
"""Serving promotion bridge (Gate A2k)."""
|
"""Serving promotion bridge (Gate A2k)."""
|
||||||
return compose_fraction_decrease(problem_text)
|
return compose_fraction_decrease(problem_text, contract)
|
||||||
|
|
|
||||||
|
|
@ -793,16 +793,23 @@ def parse_and_solve(text: str, *, sealed: bool = False) -> CandidateGraphResult:
|
||||||
from generate.derivation.fraction_decrease import (
|
from generate.derivation.fraction_decrease import (
|
||||||
resolve_promotable_fraction_decrease,
|
resolve_promotable_fraction_decrease,
|
||||||
)
|
)
|
||||||
|
from generate.problem_frame_builder import build_problem_frame
|
||||||
|
from generate.problem_frame_contracts import assess_geometric_proposals
|
||||||
|
|
||||||
fraction_decrease_resolution = resolve_promotable_fraction_decrease(text)
|
frame = build_problem_frame(text)
|
||||||
if fraction_decrease_resolution is not None:
|
contracts = assess_geometric_proposals(frame)
|
||||||
return CandidateGraphResult(
|
contract = next((c for c in contracts if c.runnable and c.candidate_organ == "fraction_decrease"), None)
|
||||||
answer=fraction_decrease_resolution.answer,
|
|
||||||
selected_graph=None,
|
if contract is not None:
|
||||||
refusal_reason=None,
|
fraction_decrease_resolution = resolve_promotable_fraction_decrease(text, contract)
|
||||||
branches_enumerated=1,
|
if fraction_decrease_resolution is not None:
|
||||||
branches_admissible=1,
|
return CandidateGraphResult(
|
||||||
)
|
answer=fraction_decrease_resolution.answer,
|
||||||
|
selected_graph=None,
|
||||||
|
refusal_reason=None,
|
||||||
|
branches_enumerated=1,
|
||||||
|
branches_admissible=1,
|
||||||
|
)
|
||||||
|
|
||||||
# Gate A2l — equal half-split percent partition (Sprint 8 R6 lift).
|
# Gate A2l — equal half-split percent partition (Sprint 8 R6 lift).
|
||||||
from generate.derivation.percent_partition import (
|
from generate.derivation.percent_partition import (
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ from generate.construction_affordances import (
|
||||||
)
|
)
|
||||||
from generate.kernel_facts import BoundRelation, GroundedMention, SourceSpan
|
from generate.kernel_facts import BoundRelation, GroundedMention, SourceSpan
|
||||||
from generate.problem_frame import ProblemFrame
|
from generate.problem_frame import ProblemFrame
|
||||||
|
from chat.pack_resolver import resolve_geometric_signature
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -888,7 +889,7 @@ def assess_geometric_proposals(frame: ProblemFrame) -> list[ContractAssessment]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
bindings = []
|
bindings = []
|
||||||
for span in prop.spans:
|
for span in prop.evidence_spans:
|
||||||
signature = resolve_geometric_signature(span.text)
|
signature = resolve_geometric_signature(span.text)
|
||||||
if signature:
|
if signature:
|
||||||
_, geom = signature
|
_, geom = signature
|
||||||
|
|
@ -914,7 +915,7 @@ def assess_geometric_proposals(frame: ProblemFrame) -> list[ContractAssessment]:
|
||||||
candidate_organ=candidate_organ,
|
candidate_organ=candidate_organ,
|
||||||
runnable=runnable,
|
runnable=runnable,
|
||||||
explanation="Assessed via geometric algebra.",
|
explanation="Assessed via geometric algebra.",
|
||||||
evidence_spans=tuple(prop.spans)
|
evidence_spans=prop.evidence_spans
|
||||||
)
|
)
|
||||||
assessments.append(assessment)
|
assessments.append(assessment)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,23 @@
|
||||||
"rank": {
|
"rank": {
|
||||||
"type": ["number", "integer"],
|
"type": ["number", "integer"],
|
||||||
"description": "Priority rank among senses for the same lemma. 1 is most primary."
|
"description": "Priority rank among senses for the same lemma. 1 is most primary."
|
||||||
|
},
|
||||||
|
"geometric_signature": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"operator_type": {
|
||||||
|
"enum": ["static_blade", "parameterized_dilation", "parameterized_translation"]
|
||||||
|
},
|
||||||
|
"blade_values": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "number" }
|
||||||
|
},
|
||||||
|
"generator_binding": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["operator_type", "blade_values"],
|
||||||
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue