# Suite graduee : (enonce, reponse_entiere_attendue).
# NIVEAU FACILE : arithmetique 1 etape (GSM8K niveau CP).
# NIVEAU MOYEN : GSM8K standard, 2-3 etapes.
# NIVEAU DIFFICILE : comptage / enumeration exacte. Ce bucket est calibre PAR
# LA MESURE, pas par l'intuition : un pilote de discrimination a evalue 25+
# candidats sur le modele servi (N=6, temp 0.7). Profil mesure du modele : il
# repond JUSTE ou ne repond pas (25/25 reponses justes, 0 fausses ; les
# non-reponses sont des contenus vides, budget de raisonnement epuise). Le
# bucket garde donc UN probleme d'echec structurel (triplets somme 20 : vide a
# tous les budgets testes, 4096 comme 8192) et UN probleme a succes PARTIEL
# (deux fois le chiffre 7 : repondu juste 1 fois sur 6 au pilote) -- seul
# regime ou BoN ait quelque chose a corriger. Les MATH-lite du premier jet
# (equation lineaire, regles d'exposants, aire 3-4-5) etaient resolus a tous
# les coups.
PROBLEMES = {
'facile': [
("J'ai 7 billes. On m'en donne 5 de plus. Combien ai-je ? Reponds par le nombre seul.", 12),
("Combien font 15 + 28 ? Reponds par le nombre seul.", 43),
("Un paquet de 6 bonbons. J'achete 4 paquets. Combien de bonbons ? Reponds par le nombre seul.", 24),
],
'moyen': [
("Marie a 3 fois l'age de Luc. Dans 5 ans, Marie aura 32 ans. Quel est l'age actuel de Luc ? Reponds par le nombre seul.", 9),
("Une voiture parcourt 60 km en 1h, puis 30 km en 30 min. Quelle distance totale en km ? Reponds par le nombre seul.", 90),
("Un jardinier a 48 fleurs. Il en plante 1/3 dans le jardin A, et la moitie du reste dans le jardin B. Combien reste-t-il de fleurs non plantees ? Reponds par le nombre seul.", 16),
],
'difficile': [
("Un comite de 3 personnes est choisi parmi 8, dont un trio inseparable (si l'un est choisi, les deux autres doivent l'etre aussi). Combien de comites VALIDES differents ? Reponds par le nombre seul.", 11),
("Combien de triplets (x, y, z) d'entiers avec 0 <= x < y < z <= 12 et x + y + z = 20 ? Reponds par le nombre seul.", 17),
("Combien d'entiers entre 1 et 1000 contiennent exactement deux fois le chiffre 7 ? Reponds par le nombre seul.", 27),
("Combien de mots de 5 lettres distinctes prises parmi A, B, C, D, E (dans n'importe quel ordre) ne contiennent pas A et E adjacents ? Reponds par le nombre seul.", 72),
],
}
# ---------------------------------------------------------------------------
# Verite terrain du banc -- recalcul EXACT a chaque execution.
#
# Lecon fondatrice (mesure 2026-09-15). Un attendu brute-force ne vaut QUE si
# l'enumeration calcule la quantite que la QUESTION demande. Le premier jet du
# bucket `difficile` annotait 40 le probleme du trio inseparable, en comptant
# « les comites avec AU PLUS 1 membre du trio » -- alors que l'enonce dit
# « si l'un est choisi, les deux autres doivent l'etre aussi », donc valides =
# « aucun membre du trio, ou les trois » = C(5,3) + 1 = 11. Le modele repondait
# 11 six fois sur six : il avait raison, la verite terrain avait tort.
#
# Ce que ce bloc garantit : chaque attendu declare est recalcule, et une
# divergence leve une AssertionError. Ce qu'il ne garantit PAS : que la
# quantite recalculee soit celle de l'enonce -- la force brute attrape une
# erreur d'arithmetique, jamais une erreur de specification. D'ou les deux
# disciplines ci-dessous, qui se lisent a l'oeil :
# 1. la docstring de chaque controle enonce la quantite EN TOUTES LETTRES ;
# 2. tout comptage non trivial est croise par son complementaire
# (valides + invalides = total), ce qui attrape un terme oublie.
# ---------------------------------------------------------------------------
from itertools import combinations, permutations
def _c_billes():
"""7 billes plus 5 billes."""
return 7 + 5
def _c_somme():
"""Somme de 15 et 28."""
return 15 + 28
def _c_bonbons():
"""4 paquets de 6 bonbons."""
return 4 * 6
def _c_age_luc():
"""Age actuel de Luc : Marie = 3 x Luc, et Marie + 5 = 32."""
marie = 32 - 5
assert marie % 3 == 0
return marie // 3
def _c_distance():
"""Distance totale : 60 km puis 30 km."""
return 60 + 30
def _c_fleurs():
"""Fleurs non plantees : 48, moins 1/3 dans A, moins la moitie du reste dans B."""
reste_apres_a = 48 - 48 // 3
return reste_apres_a - reste_apres_a // 2
def _c_comites_trio():
"""Comites de 3 parmi 8 ou le trio {0, 1, 2} est INSECABLE : un comite est
valide s'il ne contient AUCUN membre du trio, ou s'il contient LES TROIS
(un comite a 3 sieges : « contient les trois » <=> c'est le trio lui-meme)."""
tous = list(combinations(range(8), 3))
valides = [c for c in tous if len({0, 1, 2} & set(c)) in (0, 3)]
invalides = [c for c in tous if len({0, 1, 2} & set(c)) in (1, 2)]
assert len(valides) + len(invalides) == len(tous), 'complementaire incoherent'
return len(valides)
def _c_triplets_somme_20():
"""Triplets d'entiers 0 <= x < y < z <= 12 dont la somme vaut exactement 20."""
return sum(1 for x in range(13) for y in range(x + 1, 13)
for z in range(y + 1, 13) if x + y + z == 20)
def _c_deux_sept():
"""Entiers de 1 a 1000 dont l'ecriture decimale contient EXACTEMENT deux
fois le chiffre 7 (77 compte ; 777 contient trois 7, il ne compte pas)."""
valides = [n for n in range(1, 1001) if str(n).count('7') == 2]
invalides = [n for n in range(1, 1001) if str(n).count('7') != 2]
assert len(valides) + len(invalides) == 1000, 'complementaire incoherent'
return len(valides)
def _c_permutations_sans_adjacence():
"""Permutations de A, B, C, D, E ou A et E ne sont jamais adjacents."""
toutes = list(permutations('ABCDE'))
sans = [p for p in toutes
if not any({p[i], p[i + 1]} == {'A', 'E'} for i in range(4))]
assert len(sans) <= len(toutes)
return len(sans)
_controles = {
"J'ai 7 billes. On m'en donne 5 de plus. Combien ai-je ? Reponds par le nombre seul.": _c_billes,
"Combien font 15 + 28 ? Reponds par le nombre seul.": _c_somme,
"Un paquet de 6 bonbons. J'achete 4 paquets. Combien de bonbons ? Reponds par le nombre seul.": _c_bonbons,
"Marie a 3 fois l'age de Luc. Dans 5 ans, Marie aura 32 ans. Quel est l'age actuel de Luc ? Reponds par le nombre seul.": _c_age_luc,
"Une voiture parcourt 60 km en 1h, puis 30 km en 30 min. Quelle distance totale en km ? Reponds par le nombre seul.": _c_distance,
"Un jardinier a 48 fleurs. Il en plante 1/3 dans le jardin A, et la moitie du reste dans le jardin B. Combien reste-t-il de fleurs non plantees ? Reponds par le nombre seul.": _c_fleurs,
"Un comite de 3 personnes est choisi parmi 8, dont un trio inseparable (si l'un est choisi, les deux autres doivent l'etre aussi). Combien de comites VALIDES differents ? Reponds par le nombre seul.": _c_comites_trio,
"Combien de triplets (x, y, z) d'entiers avec 0 <= x < y < z <= 12 et x + y + z = 20 ? Reponds par le nombre seul.": _c_triplets_somme_20,
"Combien d'entiers entre 1 et 1000 contiennent exactement deux fois le chiffre 7 ? Reponds par le nombre seul.": _c_deux_sept,
"Combien de mots de 5 lettres distinctes prises parmi A, B, C, D, E (dans n'importe quel ordre) ne contiennent pas A et E adjacents ? Reponds par le nombre seul.": _c_permutations_sans_adjacence,
}
# Chaque enonce du banc porte exactement un controle, et l'attendu declare doit
# etre celui que le controle recalcule (appariement par enonce, pas par valeur :
# deux problemes peuvent partager un attendu).
_declares = {enonce: attendu for _, probs in PROBLEMES.items() for enonce, attendu in probs}
assert set(_declares) == set(_controles), (
'banc et controles ne portent pas les memes enonces : '
f'sans controle={sorted(set(_declares) - set(_controles))}, '
f'orphelins={sorted(set(_controles) - set(_declares))}')
for _enonce, _fn in _controles.items():
_recalc = _fn()
assert _recalc == _declares[_enonce], (
f'attendu declare {_declares[_enonce]} != recalcul {_recalc} '
f'pour : {_enonce[:70]}...')
print(f' verite terrain : {len(_controles)} enonces recalcules, tous concordants')
def extraire_nombre(texte):
"""Dernier entier du texte : le modele conclut par sa reponse.
Prendre le PREMIER entier (premier jet) lisait un nombre du raisonnement et
transformait une bonne reponse en echec des que le modele detaille son
calcul -- un modele qui ecrit « ... 45 - 5 = 40 » avant de conclure etait
note 45. Le prompt demande explicitement de repondre par le nombre seul, le
dernier entier est donc la reponse.
"""
m = re.findall(r'-?\d+', texte or '')
return int(m[-1]) if m else None
def est_correct(reponse_attendue):
def _verif(texte):
n = extraire_nombre(texte)
return n is not None and n == reponse_attendue
return _verif
def pass_at_k(n, c, k):
"""Estimateur non-biaise pass@k (HumanEval / Snell 2024)."""
if n - c < k:
return 1.0
return 1.0 - math.comb(n - c, k) / math.comb(n, k)
# Sanity check de l'estimateur
print('pass@k (n=8, c=5):', {k: round(pass_at_k(8, 5, k), 3) for k in (1, 2, 4, 6)})
print('Nb problemes par bucket :', {b: len(p) for b, p in PROBLEMES.items()})