chore(derivation): remove Inc3 formatting churn

This commit is contained in:
Shay 2026-06-17 09:53:27 -07:00
parent 64eaf43cd8
commit 5ad6c6390b
2 changed files with 128 additions and 378 deletions

View file

@ -248,7 +248,9 @@ def inject_discrete_count_statement(
anchor, sentence anchor, sentence
) )
elif anchor_kind == "acquisition": elif anchor_kind == "acquisition":
cand = _build_operation_from_discrete_count_acquisition(anchor, sentence) cand = _build_operation_from_discrete_count_acquisition(
anchor, sentence
)
else: else:
# Unknown anchor_kind — under-admit. Future widenings (e.g. # Unknown anchor_kind — under-admit. Future widenings (e.g.
# "depletion" verbs as CandidateOperation(subtract)) extend # "depletion" verbs as CandidateOperation(subtract)) extend
@ -294,13 +296,10 @@ def _build_initial_from_discrete_count(
counted_noun = anchor.get("counted_noun") counted_noun = anchor.get("counted_noun")
if ( if (
not isinstance(subject_role, str) not isinstance(subject_role, str) or not subject_role
or not subject_role or not isinstance(count_token, str) or not count_token
or not isinstance(count_token, str)
or not count_token
or not isinstance(count_kind, str) or not isinstance(count_kind, str)
or not isinstance(counted_noun, str) or not isinstance(counted_noun, str) or not counted_noun
or not counted_noun
): ):
return None return None
@ -391,15 +390,11 @@ def _build_operation_from_discrete_count_acquisition(
verb_token = anchor.get("verb_token") verb_token = anchor.get("verb_token")
if ( if (
not isinstance(subject_role, str) not isinstance(subject_role, str) or not subject_role
or not subject_role or not isinstance(count_token, str) or not count_token
or not isinstance(count_token, str)
or not count_token
or not isinstance(count_kind, str) or not isinstance(count_kind, str)
or not isinstance(counted_noun, str) or not isinstance(counted_noun, str) or not counted_noun
or not counted_noun or not isinstance(verb_token, str) or not verb_token
or not isinstance(verb_token, str)
or not verb_token
): ):
return None return None
@ -466,7 +461,10 @@ def _count_token_followed_by_times(sentence: str, count_token: str) -> bool:
admitting path. admitting path.
""" """
target = count_token.lower() target = count_token.lower()
tokens = [raw.strip(".,;:!?\"'()[]{}").lower() for raw in sentence.split()] tokens = [
raw.strip(".,;:!?\"'()[]{}").lower()
for raw in sentence.split()
]
for i, tok in enumerate(tokens[:-1]): for i, tok in enumerate(tokens[:-1]):
if tok == target and tokens[i + 1] == "times": if tok == target and tokens[i + 1] == "times":
return True return True
@ -522,11 +520,9 @@ def _locate_possession_verb(sentence: str) -> str | None:
# registers its injector. No global state, no side effects. # registers its injector. No global state, no side effects.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
_WAVE_A_INJECTABLE_ANCHOR_KINDS: frozenset[str] = frozenset( _WAVE_A_INJECTABLE_ANCHOR_KINDS: frozenset[str] = frozenset({
{ "multiplicative_aggregate_each_weighing",
"multiplicative_aggregate_each_weighing", })
}
)
def inject_multiplicative_aggregation( def inject_multiplicative_aggregation(
@ -675,12 +671,7 @@ def inject_rate_with_currency(
# can still pick an unrelated earlier "a". # can still pick an unrelated earlier "a".
rate_anchor_token = anchor.get("rate_anchor_token") rate_anchor_token = anchor.get("rate_anchor_token")
if not rate_anchor_token or rate_anchor_token not in ( if not rate_anchor_token or rate_anchor_token not in (
"per", "per", "each", "every", "a", "an", "one",
"each",
"every",
"a",
"an",
"one",
): ):
# Missing or invalid connector for this rate surface (e.g. absent # Missing or invalid connector for this rate surface (e.g. absent
# token). "one" (from "for one cup") is now supported (Inc 3). # token). "one" (from "for one cup") is now supported (Inc 3).

View file

@ -39,51 +39,18 @@ from generate.recognizer_registry import RatifiedRecognizer
# multipliers ("dozen"). Mirrors the Phase A categorizer's # multipliers ("dozen"). Mirrors the Phase A categorizer's
# _NUMBER_WORDS so the matcher's "has any quantity marker" predicate # _NUMBER_WORDS so the matcher's "has any quantity marker" predicate
# is the same shape as Phase A's "has no quantity marker" predicate. # is the same shape as Phase A's "has no quantity marker" predicate.
_NUMBER_WORDS: Final[frozenset[str]] = frozenset( _NUMBER_WORDS: Final[frozenset[str]] = frozenset({
{ "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
"one", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen",
"two", "seventeen", "eighteen", "nineteen", "twenty", "thirty", "forty", "fifty",
"three", "sixty", "seventy", "eighty", "ninety",
"four", "hundred", "thousand", "million", "billion",
"five", "dozen", "dozens",
"six", })
"seven",
"eight",
"nine",
"ten",
"eleven",
"twelve",
"thirteen",
"fourteen",
"fifteen",
"sixteen",
"seventeen",
"eighteen",
"nineteen",
"twenty",
"thirty",
"forty",
"fifty",
"sixty",
"seventy",
"eighty",
"ninety",
"hundred",
"thousand",
"million",
"billion",
"dozen",
"dozens",
}
)
_DIGIT_RE: Final[re.Pattern[str]] = re.compile(r"\d") _DIGIT_RE: Final[re.Pattern[str]] = re.compile(r"\d")
_INDEFINITE_TOKENS: Final[tuple[str, ...]] = ( _INDEFINITE_TOKENS: Final[tuple[str, ...]] = (
" some ", " some ", " several ", " a few ", " many ", " any ",
" several ",
" a few ",
" many ",
" any ",
) )
@ -160,13 +127,7 @@ _TEMPORAL_PATTERNS: Final[tuple[tuple[re.Pattern[str], str], ...]] = (
# Day-of-week enumeration: at least two distinct day names with at # Day-of-week enumeration: at least two distinct day names with at
# least one numeric count. Matches "20 ... Monday, 36 ... Tuesday". # least one numeric count. Matches "20 ... Monday, 36 ... Tuesday".
_DAY_NAMES: Final[tuple[str, ...]] = ( _DAY_NAMES: Final[tuple[str, ...]] = (
"monday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
) )
_DAY_HIT_RE: Final[re.Pattern[str]] = re.compile( _DAY_HIT_RE: Final[re.Pattern[str]] = re.compile(
r"""(?ix) r"""(?ix)
@ -267,13 +228,12 @@ def _match_temporal_aggregation(
return None return None
anchors: list[Mapping[str, Any]] = [] anchors: list[Mapping[str, Any]] = []
padded = " " + statement.lower() + " "
# Pass 1 — day-of-week enumeration. At least two distinct day # Pass 1 — day-of-week enumeration. At least two distinct day
# names + a count per day yields multi-anchor day-windowed # names + a count per day yields multi-anchor day-windowed
# aggregation. # aggregation.
if "day" in observed_units and ( if "day" in observed_units and ("each" in observed_quantifiers or "every" in observed_quantifiers):
"each" in observed_quantifiers or "every" in observed_quantifiers
):
day_hits: list[tuple[str, str]] = [] day_hits: list[tuple[str, str]] = []
for m in _DAY_HIT_RE.finditer(statement): for m in _DAY_HIT_RE.finditer(statement):
day_hits.append((m.group(1), m.group(2).lower())) day_hits.append((m.group(1), m.group(2).lower()))
@ -282,14 +242,12 @@ def _match_temporal_aggregation(
if len(distinct_days) >= 2: if len(distinct_days) >= 2:
quant = "each" if "each" in observed_quantifiers else "every" quant = "each" if "each" in observed_quantifiers else "every"
for count_token, _day in day_hits: for count_token, _day in day_hits:
anchors.append( anchors.append({
{ "kind": "event_count_per_window",
"kind": "event_count_per_window", "count_token": count_token,
"count_token": count_token, "window_unit": "day",
"window_unit": "day", "window_quantifier": quant,
"window_quantifier": quant, })
}
)
if anchors: if anchors:
return (tuple(anchors), "aggregate") return (tuple(anchors), "aggregate")
@ -297,11 +255,7 @@ def _match_temporal_aggregation(
for pat, kind in _TEMPORAL_PATTERNS: for pat, kind in _TEMPORAL_PATTERNS:
for m in pat.finditer(statement): for m in pat.finditer(statement):
if kind == "explicit_quantifier": if kind == "explicit_quantifier":
count_token, quantifier, unit = ( count_token, quantifier, unit = m.group(1), m.group(2).lower(), m.group(3).lower()
m.group(1),
m.group(2).lower(),
m.group(3).lower(),
)
elif kind == "in_window": elif kind == "in_window":
count_token, quantifier, unit = m.group(1), "per", m.group(2).lower() count_token, quantifier, unit = m.group(1), "per", m.group(2).lower()
else: # adverbial else: # adverbial
@ -309,11 +263,8 @@ def _match_temporal_aggregation(
adverb = m.group(2).lower() adverb = m.group(2).lower()
# Map adverb → unit. # Map adverb → unit.
unit_map = { unit_map = {
"daily": "day", "daily": "day", "weekly": "week", "monthly": "month",
"weekly": "week", "yearly": "year", "hourly": "hour",
"monthly": "month",
"yearly": "year",
"hourly": "hour",
} }
unit = unit_map[adverb] unit = unit_map[adverb]
quantifier = "per" quantifier = "per"
@ -321,14 +272,12 @@ def _match_temporal_aggregation(
continue continue
if quantifier not in observed_quantifiers: if quantifier not in observed_quantifiers:
continue continue
anchors.append( anchors.append({
{ "kind": "event_count_per_window",
"kind": "event_count_per_window", "count_token": count_token,
"count_token": count_token, "window_unit": unit,
"window_unit": unit, "window_quantifier": quantifier,
"window_quantifier": quantifier, })
}
)
if not anchors: if not anchors:
return None return None
@ -410,16 +359,14 @@ def _match_rate_with_currency(
else: else:
amount_kind = "integer" amount_kind = "integer"
anchors.append( anchors.append({
{ "kind": "currency_per_unit_rate",
"kind": "currency_per_unit_rate", "currency_symbol": symbol,
"currency_symbol": symbol, "amount": amount_token,
"amount": amount_token, "amount_kind": amount_kind,
"amount_kind": amount_kind, "per_unit": per_unit_lc,
"per_unit": per_unit_lc, "rate_anchor_token": connector.lower() if connector else None,
"rate_anchor_token": connector.lower() if connector else None, })
}
)
if not anchors: if not anchors:
return None return None
@ -559,11 +506,7 @@ def _try_extract_currency_per_unit_composition_anchor(
if composed_value_f != composed_value_f: # NaN guard if composed_value_f != composed_value_f: # NaN guard
return None return None
composed_value: int | float composed_value: int | float
if ( if composed_value_f.is_integer() and "." not in count_token and "." not in amount_token:
composed_value_f.is_integer()
and "." not in count_token
and "." not in amount_token
):
composed_value = int(composed_value_f) composed_value = int(composed_value_f)
else: else:
composed_value = composed_value_f composed_value = composed_value_f
@ -781,44 +724,23 @@ def try_extract_cross_sentence_composition_anchor(
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
_PER_UNIT_TOKENS: Final[tuple[str, ...]] = ( _PER_UNIT_TOKENS: Final[tuple[str, ...]] = (
" per ", " per ", "/", " an hour", " a hour", " a day", " a week", " a month",
"/", " a year", " for one ", " for each ", " for every ",
" an hour",
" a hour",
" a day",
" a week",
" a month",
" a year",
" for one ",
" for each ",
" for every ",
# RAT-1 — standalone per-item quantifiers. "$400 each" is per-unit # RAT-1 — standalone per-item quantifiers. "$400 each" is per-unit
# framing semantically equivalent to "$400 per item". The detection- # framing semantically equivalent to "$400 per item". The detection-
# only currency_amount matcher must refuse this so the per-unit # only currency_amount matcher must refuse this so the per-unit
# composition path (ME-1 / ME-2 currency_per_unit_composition) gets # composition path (ME-1 / ME-2 currency_per_unit_composition) gets
# a turn at the same statement. # a turn at the same statement.
" each ", " each ", " each.", " apiece ", " apiece.",
" each.",
" apiece ",
" apiece.",
) )
_TEMPORAL_QUANTIFIER_TOKENS: Final[tuple[str, ...]] = ( _TEMPORAL_QUANTIFIER_TOKENS: Final[tuple[str, ...]] = (
" per ", " per ", " each ", " every ", " daily", " weekly", " monthly",
" each ", " yearly", " hourly",
" every ",
" daily",
" weekly",
" monthly",
" yearly",
" hourly",
) )
_MULTIPLICATIVE_CONNECTIVES: Final[tuple[str, ...]] = ( _MULTIPLICATIVE_CONNECTIVES: Final[tuple[str, ...]] = (
" with ", " with ", " each ", " in each ", " per each ",
" each ",
" in each ",
" per each ",
) )
@ -921,13 +843,9 @@ def _match_discrete_count_statement(
# CandidateInitial post-init whitelist. Widening to owns/holds/contains # CandidateInitial post-init whitelist. Widening to owns/holds/contains
# requires a coordinated CandidateInitial change and lands in a follow-up # requires a coordinated CandidateInitial change and lands in a follow-up
# PR after the framework's empirical lift is operator-reviewed. # PR after the framework's empirical lift is operator-reviewed.
_POSSESSION_VERBS: Final[frozenset[str]] = frozenset( _POSSESSION_VERBS: Final[frozenset[str]] = frozenset({
{ "has", "have", "had",
"has", })
"have",
"had",
}
)
# ADR-0170 W2 — acquisition verbs: surface verbs that grammatically place # ADR-0170 W2 — acquisition verbs: surface verbs that grammatically place
# the actor as the *gainer* of the operand quantity, NOT as having the # the actor as the *gainer* of the operand quantity, NOT as having the
@ -948,60 +866,28 @@ _POSSESSION_VERBS: Final[frozenset[str]] = frozenset(
# #
# Widening this set is operator-reviewable per the wrong=0 hazard # Widening this set is operator-reviewable per the wrong=0 hazard
# documented in feedback-wrong-zero-hazard-case-0050. # documented in feedback-wrong-zero-hazard-case-0050.
_ACQUISITION_VERBS: Final[frozenset[str]] = frozenset( _ACQUISITION_VERBS: Final[frozenset[str]] = frozenset({
{ "collected", "collects", "collect",
"collected", "received", "receives", "receive",
"collects", "bought", "buys", "buy",
"collect", "got", "gets", "get",
"received", })
"receives",
"receive",
"bought",
"buys",
"buy",
"got",
"gets",
"get",
}
)
# Pronoun subjects refused at extraction (ambiguous referent). The # Pronoun subjects refused at extraction (ambiguous referent). The
# extractor requires a concrete proper-noun subject the source span can # extractor requires a concrete proper-noun subject the source span can
# ground. # ground.
_REFUSED_SUBJECT_TOKENS: Final[frozenset[str]] = frozenset( _REFUSED_SUBJECT_TOKENS: Final[frozenset[str]] = frozenset({
{ "he", "she", "they", "it", "we", "you", "i",
"he", "him", "her", "them", "us",
"she", })
"they",
"it",
"we",
"you",
"i",
"him",
"her",
"them",
"us",
}
)
# Clause-splitting / enumeration markers. Their presence indicates a # Clause-splitting / enumeration markers. Their presence indicates a
# second clause that may carry operations or additional anchors, so # second clause that may carry operations or additional anchors, so
# v1 refuses extraction (skip-only fallback preserves wrong=0). # v1 refuses extraction (skip-only fallback preserves wrong=0).
_CLAUSE_SPLIT_TOKENS: Final[tuple[str, ...]] = ( _CLAUSE_SPLIT_TOKENS: Final[tuple[str, ...]] = (
" but ", " but ", " then ", " however ", " before ", " after ",
" then ", " and ", " or ", " while ", " until ", " unless ",
" however ", ", and ", ", but ", ", or ", ", then ",
" before ",
" after ",
" and ",
" or ",
" while ",
" until ",
" unless ",
", and ",
", but ",
", or ",
", then ",
) )
# Hyphenated compound cardinal: 'twenty-five', 'ninety-nine'. These # Hyphenated compound cardinal: 'twenty-five', 'ninety-nine'. These
@ -1023,11 +909,11 @@ def _extract_discrete_count_re_for(counted_nouns: list[str]) -> re.Pattern[str]:
noun_alt = "|".join(re.escape(n) for n in options) noun_alt = "|".join(re.escape(n) for n in options)
return re.compile( return re.compile(
r"^\s*" r"^\s*"
r"(?P<subject>(?-i:[A-Z][a-z]+))" # case-sensitive proper noun r"(?P<subject>(?-i:[A-Z][a-z]+))" # case-sensitive proper noun
r"\s+(?P<verb>[A-Za-z]+)" # any word; verified against whitelist r"\s+(?P<verb>[A-Za-z]+)" # any word; verified against whitelist
r"\s+(?P<count>\d+|[A-Za-z\-]+)" # integer or word/hyphenated cardinal r"\s+(?P<count>\d+|[A-Za-z\-]+)" # integer or word/hyphenated cardinal
r"\s+(?P<noun>" + noun_alt + r")" r"\s+(?P<noun>" + noun_alt + r")"
r"(?:\b.*)?$", # optional trailing content r"(?:\b.*)?$", # optional trailing content
flags=re.IGNORECASE, flags=re.IGNORECASE,
) )
@ -1065,8 +951,7 @@ def _extract_discrete_count_re_open(counted_nouns: list[str]) -> re.Pattern[str]
open_tok = rf"(?-i:(?!(?:{_OPEN_NOUN_STOP})\b)[a-z]+)" open_tok = rf"(?-i:(?!(?:{_OPEN_NOUN_STOP})\b)[a-z]+)"
open_noun = rf"{open_tok}(?:\s+{open_tok}){{0,2}}" open_noun = rf"{open_tok}(?:\s+{open_tok}){{0,2}}"
noun_group = ( noun_group = (
rf"(?P<noun>{closed_alt}|{open_noun})" rf"(?P<noun>{closed_alt}|{open_noun})" if closed_alt
if closed_alt
else rf"(?P<noun>{open_noun})" else rf"(?P<noun>{open_noun})"
) )
return re.compile( return re.compile(
@ -1074,7 +959,8 @@ def _extract_discrete_count_re_open(counted_nouns: list[str]) -> re.Pattern[str]
r"(?P<subject>(?-i:[A-Z][a-z]+))" r"(?P<subject>(?-i:[A-Z][a-z]+))"
r"\s+(?P<verb>[A-Za-z]+)" r"\s+(?P<verb>[A-Za-z]+)"
r"\s+(?P<count>\d+|[A-Za-z\-]+)" r"\s+(?P<count>\d+|[A-Za-z\-]+)"
r"\s+" + noun_group + r"(?:\b.*)?$", r"\s+" + noun_group +
r"(?:\b.*)?$",
flags=re.IGNORECASE, flags=re.IGNORECASE,
) )
@ -1224,25 +1110,12 @@ def _try_extract_discrete_count_anchor(
# appears in the sentence we refuse the compound extraction; the case # appears in the sentence we refuse the compound extraction; the case
# routes to a future phase that handles those shapes. # routes to a future phase that handles those shapes.
_COMPOUND_REFUSE_SUBSTRINGS: Final[tuple[str, ...]] = ( _COMPOUND_REFUSE_SUBSTRINGS: Final[tuple[str, ...]] = (
" times ", " times ", " times.", " times,",
" times.", " as long", " as many", " as much", " as old",
" times,", " greater than", " less than", " more than", " fewer than",
" as long", " half as ", " twice as ", " thrice ",
" as many", "%", " percent",
" as much", " half of ", " quarter of ", " third of ",
" as old",
" greater than",
" less than",
" more than",
" fewer than",
" half as ",
" twice as ",
" thrice ",
"%",
" percent",
" half of ",
" quarter of ",
" third of ",
) )
# Fraction literal pattern (matched against raw statement, not padded). # Fraction literal pattern (matched against raw statement, not padded).
@ -1292,7 +1165,10 @@ def _try_extract_compound_discrete_count_anchors(
return None return None
# Must have a conjunctive separator — otherwise this isn't compound # Must have a conjunctive separator — otherwise this isn't compound
has_conjunctive = any(tok in padded_lower for tok in (", and ", " and ", ", ")) has_conjunctive = any(
tok in padded_lower
for tok in (", and ", " and ", ", ")
)
if not has_conjunctive: if not has_conjunctive:
return None return None
@ -1404,7 +1280,6 @@ def _try_extract_compound_discrete_count_anchors(
# HYPOTHESIS_CAP enforcement — refusal-preferring rather than truncate # HYPOTHESIS_CAP enforcement — refusal-preferring rather than truncate
from generate.comprehension.state import HYPOTHESIS_CAP from generate.comprehension.state import HYPOTHESIS_CAP
if len(anchors) > HYPOTHESIS_CAP: if len(anchors) > HYPOTHESIS_CAP:
return None return None
@ -1455,8 +1330,7 @@ def _match_multiplicative_aggregation(
# two needed to admit a multiplicative shape. # two needed to admit a multiplicative shape.
digit_hits = len(_DIGIT_RE.findall(statement)) digit_hits = len(_DIGIT_RE.findall(statement))
word_hits = sum( word_hits = sum(
1 1 for token in padded.split()
for token in padded.split()
if token.strip(".,;:!?\"'()[]{}").lower() in _NUMBER_WORDS if token.strip(".,;:!?\"'()[]{}").lower() in _NUMBER_WORDS
) )
if (digit_hits + word_hits) < 2: if (digit_hits + word_hits) < 2:
@ -1571,24 +1445,9 @@ def _try_extract_each_weighing_anchor(
# matched_anchor must be in CandidateInitial post-init whitelist. # matched_anchor must be in CandidateInitial post-init whitelist.
outer_verb = m.group("outer_verb").lower() outer_verb = m.group("outer_verb").lower()
matched_anchor = ( matched_anchor = outer_verb if outer_verb in {
outer_verb "has", "had", "made", "makes", "buys", "bought", "paid", "earned", "saved", "got", "received"
if outer_verb } else "had"
in {
"has",
"had",
"made",
"makes",
"buys",
"bought",
"paid",
"earned",
"saved",
"got",
"received",
}
else "had"
)
composed_initial = CandidateInitial( composed_initial = CandidateInitial(
initial=InitialPossession( initial=InitialPossession(
@ -1716,10 +1575,7 @@ def _try_extract_additive_composition_anchor(
if unit_a.rstrip("s") != unit_b.rstrip("s"): if unit_a.rstrip("s") != unit_b.rstrip("s"):
return None return None
canonical_unit = unit_a canonical_unit = unit_a
if ( if canonical_unit not in observed_units and canonical_unit.rstrip("s") not in observed_units:
canonical_unit not in observed_units
and canonical_unit.rstrip("s") not in observed_units
):
return None return None
count_a_token = m.group("count_a") count_a_token = m.group("count_a")
@ -1751,11 +1607,9 @@ def _try_extract_additive_composition_anchor(
# Verb whitelist maps to a CandidateInitial.matched_anchor value # Verb whitelist maps to a CandidateInitial.matched_anchor value
# the post-init guard accepts (existing whitelist includes # the post-init guard accepts (existing whitelist includes
# has/have/had/saved/earned/got/received/bought/made/paid). # has/have/had/saved/earned/got/received/bought/made/paid).
matched_anchor = ( matched_anchor = verb if verb in {
verb "saved", "earned", "got", "received", "bought", "made", "paid"
if verb in {"saved", "earned", "got", "received", "bought", "made", "paid"} } else "had"
else "had"
)
composed_initial = CandidateInitial( composed_initial = CandidateInitial(
initial=InitialPossession( initial=InitialPossession(
@ -1787,22 +1641,10 @@ def _try_extract_additive_composition_anchor(
return ((anchor,), "aggregate") return ((anchor,), "aggregate")
_ADDITIVE_COMPOSITION_VERBS: Final[frozenset[str]] = frozenset( _ADDITIVE_COMPOSITION_VERBS: Final[frozenset[str]] = frozenset({
{ "lost", "gained", "earned", "saved", "made", "paid", "spent",
"lost", "bought", "sold", "added", "removed", "received",
"gained", })
"earned",
"saved",
"made",
"paid",
"spent",
"bought",
"sold",
"added",
"removed",
"received",
}
)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -1844,34 +1686,15 @@ _SUBTRACTIVE_TWO_QUANTITY_RE: Final[re.Pattern[str]] = re.compile(
_SUBTRACTIVE_COMPOSITION_SHAPE: Final[str] = "bound(initial) bound(removed)" _SUBTRACTIVE_COMPOSITION_SHAPE: Final[str] = "bound(initial) bound(removed)"
_SUBTRACTIVE_INITIAL_VERBS: Final[frozenset[str]] = frozenset( _SUBTRACTIVE_INITIAL_VERBS: Final[frozenset[str]] = frozenset({
{ "had", "has", "got", "owns", "owned", "earned", "saved",
"had", "made", "received", "bought",
"has", })
"got",
"owns",
"owned",
"earned",
"saved",
"made",
"received",
"bought",
}
)
_SUBTRACTIVE_REMOVAL_VERBS: Final[frozenset[str]] = frozenset( _SUBTRACTIVE_REMOVAL_VERBS: Final[frozenset[str]] = frozenset({
{ "lost", "spent", "gave", "donated", "paid", "removed",
"lost", "sold", "used", "consumed",
"spent", })
"gave",
"donated",
"paid",
"removed",
"sold",
"used",
"consumed",
}
)
def _try_extract_subtractive_composition_anchor( def _try_extract_subtractive_composition_anchor(
@ -1944,22 +1767,9 @@ def _try_extract_subtractive_composition_anchor(
from generate.math_candidate_parser import CandidateInitial from generate.math_candidate_parser import CandidateInitial
from generate.math_problem_graph import InitialPossession, Quantity from generate.math_problem_graph import InitialPossession, Quantity
matched_anchor = ( matched_anchor = verb_a if verb_a in {
verb_a "has", "had", "saved", "earned", "got", "received", "bought", "made", "paid",
if verb_a } else "had"
in {
"has",
"had",
"saved",
"earned",
"got",
"received",
"bought",
"made",
"paid",
}
else "had"
)
composed_initial = CandidateInitial( composed_initial = CandidateInitial(
initial=InitialPossession( initial=InitialPossession(
@ -2111,76 +1921,25 @@ def match(
# Cross-sentence subject resolution helper (ME-2). # Cross-sentence subject resolution helper (ME-2).
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
_PROPER_NOUN_SUBJECT_RE: Final[re.Pattern[str]] = re.compile(r"^\s*([A-Z][a-zA-Z]+)\b") _PROPER_NOUN_SUBJECT_RE: Final[re.Pattern[str]] = re.compile(
r"^\s*([A-Z][a-zA-Z]+)\b"
)
_COMMON_DETERMINERS_AT_HEAD: Final[frozenset[str]] = frozenset( _COMMON_DETERMINERS_AT_HEAD: Final[frozenset[str]] = frozenset(
{ {
# Articles + demonstratives # Articles + demonstratives
"the", "the", "a", "an", "this", "that", "these", "those",
"a",
"an",
"this",
"that",
"these",
"those",
# Possessives # Possessives
"his", "his", "her", "their", "its", "my", "your", "our",
"her",
"their",
"its",
"my",
"your",
"our",
# Sentence-initial connectors / prepositions that get capitalized # Sentence-initial connectors / prepositions that get capitalized
"after", "after", "before", "when", "while", "if", "then", "so", "but",
"before", "and", "or", "during", "since", "until", "though", "although",
"when", "however", "moreover", "additionally", "first", "next", "later",
"while", "finally", "now", "soon", "today", "tomorrow", "yesterday",
"if", "every", "all", "some", "many", "each", "another", "other",
"then", "in", "on", "at", "by", "for", "from", "with", "without",
"so", "how", "why", "what", "where", "who", "when",
"but",
"and",
"or",
"during",
"since",
"until",
"though",
"although",
"however",
"moreover",
"additionally",
"first",
"next",
"later",
"finally",
"now",
"soon",
"today",
"tomorrow",
"yesterday",
"every",
"all",
"some",
"many",
"each",
"another",
"other",
"in",
"on",
"at",
"by",
"for",
"from",
"with",
"without",
"how",
"why",
"what",
"where",
"who",
"when",
} }
) )