Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ dependencies = [
]

[project.optional-dependencies]
dev = ["pytest>=8.0", "ruff>=0.5", "mypy>=1.10"]
dev = ["pytest>=8.0", "ruff>=0.5,<0.16", "mypy>=1.10"]

[build-system]
requires = ["hatchling"]
Expand Down
34 changes: 22 additions & 12 deletions src/trinity/adapters/drop.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,12 @@ def _maybe_unbox(segment: str) -> str:
return boxed if boxed is not None else ""


#: Surrounding punctuation stripped from a token, EXCLUDING the signs ``+``/``-`` — a
#: leading sign is part of a number's value, not wrapping noise.
_STRIP_EDGE = "".join(c for c in string.punctuation if c not in "+-")
#: Surrounding punctuation stripped from a token, EXCLUDING the signs ``+``/``-`` and
#: the decimal point ``.`` — a leading sign or decimal point is part of a number's
#: value, not wrapping noise. A genuinely-trailing ``.`` (sentence period) is handled
#: by the dedicated rstrip retry in :func:`_normalize_token`, which can tell it apart
#: from a value-bearing leading point; a blanket edge-strip cannot.
_STRIP_EDGE = "".join(c for c in string.punctuation if c not in "+-.")


def _normalize_token(raw: str) -> str:
Expand All @@ -147,20 +150,27 @@ def _normalize_token(raw: str) -> str:
dropped the ``-`` and left commas to break ``float()``) did not deliver.

A token that is ALREADY a number is recognised before any punctuation is
stripped: the edge-strip set includes ``.``, so a leading-decimal token like
``".5"`` would otherwise lose its point and normalize to ``"5.0"`` — equal to
a gold ``"5"`` (false positive) and unequal to the value-identical gold
``"0.5"`` (false negative). The official DROP ``_remove_punc`` tests
``_is_number`` first and leaves numbers untouched for exactly this reason."""
stripped: a leading-decimal token like ``".5"`` must not lose its point and
normalize to ``"5.0"`` — equal to a gold ``"5"`` (false positive) and unequal
to the value-identical gold ``"0.5"`` (false negative). The official DROP
``_remove_punc`` tests ``_is_number`` first and leaves numbers untouched for
exactly this reason (issue #423). The float-first path alone only covers the
*bare* token: with ``.`` in the edge-strip set, wrapped forms like ``"$.5"``
and ``".5."`` still lost the leading point on the second-chance path. So the
edge strip excludes ``.`` entirely, and a genuinely-trailing period (``".5."``,
``"16.."``) is retried with an explicit ``rstrip(".")`` — right-side dots are
sentence punctuation, left-side dots are value."""
try:
return str(float(raw.replace(",", "")))
except ValueError:
pass
core = raw.strip(_STRIP_EDGE)
try:
return str(float(core.replace(",", "")))
except ValueError:
return _PUNCT.sub("", raw)
for cand in (core, core.rstrip(".")):
try:
return str(float(cand.replace(",", "")))
except ValueError:
continue
return _PUNCT.sub("", raw)


def _split_internal_hyphens(token: str) -> list[str]:
Expand Down
18 changes: 17 additions & 1 deletion src/trinity/orchestration/reward.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,16 @@ def _iter_latex_frac_sqrt_spans(text: str) -> list[tuple[int, int, str]]:
j = cmd_end
while j < len(text) and text[j] in " \t":
j += 1
# Optional root index ``[n]`` (``\sqrt[3]{8}``); skip so the radicand
# is taken with the whole term instead of as a lone digit (#483).
if j < len(text) and text[j] == "[":
close_br = text.find("]", j + 1)
if close_br == -1:
i = cmd_end
continue
j = close_br + 1
while j < len(text) and text[j] in " \t":
j += 1
if j < len(text) and text[j] == "{":
close = _scan_balanced_braces(text, j)
if close is not None:
Expand Down Expand Up @@ -510,7 +520,8 @@ def extract_last_number(text: str) -> str | None:
brace = r"\{(?:[^{}]|\{[^{}]*\})*\}"
pattern = re.compile(
rf"-?\\[dt]?frac(?![a-zA-Z])\s*(?:{brace}\s*{brace}|\d\s*\d)"
rf"|-?\d*\s*\\sqrt(?![a-zA-Z])\s*(?:{brace}|[0-9a-zA-Z])"
# Optional ``[n]`` index so ``\sqrt[3]{8}`` is one term, not digit ``8``.
rf"|-?\d*\s*\\sqrt(?![a-zA-Z])\s*(?:\[[^\]]*\]\s*)?(?:{brace}|[0-9a-zA-Z])"
r"|-?\d+\s*/\s*-?\d+"
# Scientific notation BEFORE bare decimals so "1e3" is not read as "3".
r"|-?(?:\d+(?:\.\d+)?|\.\d+)[eE][+-]?\d+"
Expand Down Expand Up @@ -820,6 +831,11 @@ def normalize_math_answer(ans: str | None) -> str:
# The [dt]? family — \frac, \dfrac and \tfrac — all render the same value.
s = _unwrap_latex_fractions(s)
s = re.sub(r"\\[dt]?frac\s*(\d)\s*(\d)", r"\1/\2", s)
# Indexed roots BEFORE plain ``\sqrt``: ``\sqrt[3]{8}`` -> ``(8)^(1/(3))`` so
# sympy can equate the value to ``2``. Plain ``\sqrt{...}`` folding would leave
# the ``[n]`` index behind and still miss the gold (#483).
s = re.sub(r"\\sqrt\s*\[([^\]]+)\]\s*\{([^{}]*)\}", r"(\2)^(1/(\1))", s)
s = re.sub(r"\\sqrt\s*\[([^\]]+)\]\s*([0-9a-zA-Z])", r"(\2)^(1/(\1))", s)
# \sqrt{x} -> sqrt(x), and the unbraced single-token \sqrt2 -> sqrt(2). Like the
# \frac handling just above, the braced and bare forms render the SAME value, so
# they must normalize identically (else \sqrt{2} is a false negative against a
Expand Down
23 changes: 23 additions & 0 deletions tests/test_reward_nth_root_sqrt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Indexed roots ``\\sqrt[n]{x}`` must not grade as the radicand digit (issue #483)."""

from trinity.orchestration.reward import extract_last_number, normalize_math_answer, score_text


def test_extract_takes_whole_indexed_sqrt():
assert extract_last_number(r"\sqrt[3]{8}") == r"\sqrt[3]{8}"
assert extract_last_number(r"the cube root is $\sqrt[3]{8}$") == r"\sqrt[3]{8}"


def test_normalize_indexed_sqrt_to_power():
assert "sqrt" not in normalize_math_answer(r"\sqrt[3]{8}")
assert "\\sqrt" not in normalize_math_answer(r"\sqrt[3]{8}")


def test_unboxed_nth_root_grades_value_not_operand():
assert score_text("math500", r"the answer is $\sqrt[3]{8}$", "2") == 1.0
assert score_text("math500", r"the answer is $\sqrt[3]{8}$", "8") == 0.0


def test_boxed_nth_root_equals_simplified_value():
assert score_text("math500", r"\boxed{\sqrt[3]{8}}", "2") == 1.0
assert score_text("math500", r"\boxed{\sqrt[4]{16}}", "2") == 1.0
Loading