pythonise-exercice / app /knowledge /pythonisation_rules.md
wilenPxs
Deploiement app pythonisation (cache FAISS reconstruit au boot)
77a0a72
|
Raw
History Blame Contribute Delete
42.5 kB

Règles de pythonisation PyxiScience — Base de connaissances

Document destiné à être injecté dans un prompt système Claude Code modifiant le générateur d'exercices pythonisés. Chaque règle est dérivée de bugs réels rencontrés en production sur les annales Bac (Métropole J2 18/06/2025, exos 2 à 4) et de leur correction validée par l'équipe contenu.


0. Objectif

Le générateur prend en entrée :

  • un énoncé APMEP (PDF/LaTeX/image)
  • une version PyxiScience non pythonisée déjà validée par Chabane

Et produit en sortie une version pythonisée MyST (.md) prête pour soumission à la plateforme.

Les règles ci-dessous codifient les invariants à respecter. Tout livrable qui les enfreint sera rejeté par l'audit.


1. Conventions PyxiScience (rappel)

Ces conventions sont préalables et non négociables. Le générateur les respecte par défaut.

  • Variables Python : camelCase uniquement. Les underscores (p_a, coef_t) provoquent des erreurs de subscript LaTeX dans certains contextes.
  • Bloc Python : ouvert avec 4 backticks (vérifié sur 222/222 exemples livrés — constante PYTHON_FENCE_BACKTICKS dans app/config.py), terminé par globals() pour exposer les variables au moteur de rendu MyST (accès via {{var}}). Enveloppe {exercise} = 5 backticks.
  • Bilingue : rôles inline {fr}`...`{en}`...` UNIQUEMENT (aucun bloc :::{fr}/:::{en} dans les 222 exemples livrés — ne pas en produire).
  • Math display : \begin{equation*}...\end{equation*} avec & pour alignement. Ne JAMAIS utiliser \[ \] ni $$ $$.
  • Espacement : \phantom{-}\\ pour saut vertical en math, % avant la première équation d'un bloc.
  • Interdits : \textbf{} (utiliser **...**), \begin{itemize} (utiliser $\bullet$ ou tirets MyST), \bbRac (n'existe pas), variable nommée total (réservée), if pxs_variation_number (vaut toujours 1).
  • Niveaux de difficulté : Advanced = avancé, Intermediate = intermédiaire.
  • pxsl_format_number ne fait PAS d'arrondi — c'est purement un formattage (virgule française). Toujours arrondir AVANT.

RÈGLE 1.1 — $ collé à un chiffre : préfixer par ${} (casse silencieuse)

Ajoutée 2026-06-12 (cas réel : 1413 occurrences dans les lots livrés ; motif interdit du harnais).

❌ FAUTIF :

Calculer $4\bigl(g(\sqrt{x})\bigr)^2$. Le prix est ${{prixAff}}$ €.

✅ CORRIGÉ :

Calculer ${}4\bigl(g(\sqrt{x})\bigr)^2$. Le prix est ${}{{prixAff}}$ €.

POURQUOI : un $ immédiatement suivi d'un chiffre est lu par le moteur comme un MONTANT en devise, pas comme une ouverture de math inline ; le $…$ se désynchronise et toute la directive part en texte brut. Le groupe vide {} est invisible au rendu. Préfixer par ${} TOUTE injection inline ${{…}} susceptible de rendre un nombre. (L'app applique un auto-correctif déterministe dans app/pipeline/postprocess.py::fix_dollar_digit.)


2. Métadonnées du fichier

RÈGLE 2.1 — :id: en première ligne, vide

`````{exercise}
:id:
:title:...
:modules: ...
...

❌ FAUTIF

`````{exercise}
:title:...
:id: 7dd83aee-26e1-11f1-828c-0ed8d3b012a9

✅ CORRIGÉ

`````{exercise}
:id:
:title:...

POURQUOI : la plateforme génère un nouvel ID à la soumission. Tout ID hardcodé sera réutilisé et provoquera un conflit.

RÈGLE 2.2 — Toutes les autres métadonnées sont conservées strictement à l'identique

Aucun ajout, aucune modification de :title:, :modules:, :level:, :involvedConcepts:, :originalSource:, etc.


3. Architecture du bloc Python — UN SEUL BLOC PRINCIPAL

RÈGLE 3.1 — Tous les paramètres aléatoires et calculs sont dans UN bloc Python en début d'exercice

❌ FAUTIF (cas vu dans la version pythonisée fournie de l'exo 4)

# Bloc 1 (début exo)
v0 = rd.randint(10, 15)
k = round(rd.uniform(0.4, 0.8), 1)
...

# Bloc 2 (avant Q3)
k_val = round(rd.uniform(0.4, 0.8), 1)  # REDÉFINIT k !
t_A = round(rd.uniform(4.0, 6.0), 1)
...

# Bloc 3 (avant Q5)
v0 = rd.randint(10, 15)  # REDÉFINIT v0 !
k_val = round(rd.uniform(0.4, 0.8), 1)
...

✅ CORRIGÉ

# UN SEUL bloc en début d'exercice
import random as rd
import math
import numpy as np
import matplotlib.pyplot as plt
from sympy import Rational, latex
from pyxiscience.Mes_fctions_generalistes_bis import pxs_config, pxsl_format_number

config_std = pxs_config()

# Tirage initial
v0 = rd.choice([11, 12, 13])
k_num = rd.choice([5, 6])
k_coef = Rational(k_num, 10)

# Toutes les variables dérivées calculées ici
inv_k = Rational(10, k_num)
asymptote = v0 * inv_k + inv_k**2
alpha = ...
# etc.

globals()

POURQUOI : random.seed n'est pas réinitialisé entre blocs Python. Redéfinir v0, k_coef, etc. dans des blocs ultérieurs fait que les valeurs diffèrent entre questions : Q4 dit "y' + 0,6y = e^(-0,6t)" puis Q7 dit "v(t) = (12+t)e^(-0,4t)" → exercice incohérent.

RÈGLE 3.2 — Imports uniquement dans le premier bloc

Tous les import numpy as np, import matplotlib.pyplot as plt, etc. sont factorisés. Les blocs ultérieurs (typiquement le bloc graphique) réutilisent les imports via globals().

RÈGLE 3.3 — Le bloc graphique réutilise les variables du bloc principal

Si un bloc Python séparé est nécessaire pour le graphique (placement après l'intro de la partie A par exemple), il doit utiliser les variables déjà calculées, pas les re-tirer.

# Bloc graphique (après l'énoncé de la partie A)
fig, ax = plt.subplots(figsize=(7, 5))
t_graph = np.linspace(0, 16.5, 2000)
y_graph = np.array([d_num(x) for x in t_graph])  # d_num défini dans bloc principal
ax.plot(t_graph, y_graph, color="blue")
# ...
plt.show()

4. Tirage des paramètres — Cohérence mathématique

RÈGLE 4.1 — Tirages en Rational exact, pas en flottant arrondi

❌ FAUTIF

k = round(rd.uniform(0.4, 0.8), 1)
k_coef = Rational(int(k * 10), 10)  # int(7.0) peut donner 6 à cause des flottants

✅ CORRIGÉ

k_num = rd.choice([5, 6, 7, 8])
k_coef = Rational(k_num, 10)

POURQUOI : round(rd.uniform(0.4, 0.8), 1) peut produire 0.6999999999999..., et int(0.6999... * 10) = 6 au lieu de 7. Bug d'arrondi flottant classique. Tirer directement les entiers numérateurs/dénominateurs.

TOLÉRANCE : round(rd.uniform(...), N) est ACCEPTABLE pour une probabilité d'affichage dont la précision n'est pas critique (par ex. p = round(rd.uniform(0.05, 0.25), 3) pour un coefficient binomial affiché à 0.001 près). À ne PAS utiliser pour des valeurs entrant dans des calculs sympy exacts, des bornes d'intervalles, ou des paramètres de récurrence.

RÈGLE 4.2 — Les paramètres dérivés sont COHÉRENTS avec les paramètres tirés

Tout paramètre qui apparaît dans l'énoncé ET qui dépend mathématiquement d'autres paramètres ne doit pas être tiré indépendamment.

❌ FAUTIF (vu dans exo 4)

v0 = rd.randint(10, 15)
k = round(rd.uniform(0.4, 0.8), 1)
L_min = rd.randint(20, 25)  # tirage indépendant
d_15 = rd.randint(12, 18)   # tirage indépendant

Conséquences :

  • L_min peut être inférieur à l'asymptote v0/k + 1/k² → la zone de freinage est trop petite, l'énoncé est faux.
  • d_15 n'a aucun lien avec la valeur de d(2) à lire sur le graphique → la "lecture graphique" donne un résultat incohérent.

✅ CORRIGÉ

v0 = rd.choice([11, 12, 13])
k_num = rd.choice([5, 6])
k_coef = Rational(k_num, 10)

# Paramètres DÉRIVÉS
asymptote = v0 * Rational(10, k_num) + Rational(10, k_num)**2
L_min = math.ceil(float(asymptote))           # garanti > asymptote
d_lecture = round(d_num(2))                   # cohérent avec graphique

POURQUOI : un paramètre qui doit satisfaire une contrainte mathématique se calcule, il ne se tire pas.

RÈGLE 4.3 — Les paramètres aléatoires PRÉSERVENT la propriété démontrée

Si l'exercice démontre une propriété (ex: w_n ≥ n), les paramètres tirés doivent la rendre vraie pour TOUS les seeds.

❌ FAUTIF (vu dans exo 3)

w0 = rd.randint(-2, 3)
alpha = rd.randint(2, 4)
beta = rd.randint(-3, -1)
gamma = rd.randint(1, 5)

Sur 1000 seeds testés :

  • 36% ont w0 < 0 → l'initialisation w_0 ≥ 0 est fausse.
  • 34% ont α + β ≤ 0 → la récurrence w_n ≥ n est mathématiquement fausse (testé : α=2, β=-3, γ=1, w_0=1 donne w_4 = -2 < 4).

✅ CORRIGÉ

# Contrainte : w_0 >= 0, alpha + beta = 1, gamma >= 1
w0 = rd.randint(0, 3)
ab_pairs = [(2, -1), (3, -2), (4, -3)]   # alpha + beta = 1 garanti
alpha, beta = rd.choice(ab_pairs)
gamma = rd.randint(1, 5)

POURQUOI : pythoniser ne signifie pas randomiser sans contrainte. La propriété démontrée doit rester vraie sur 100% des variantes. Si nécessaire, restreindre la plage ou utiliser une boucle while pour rejeter les tirages invalides.

EXTENSION — calculs intermédiaires dans detailedSolution : si une variable aléatoire change (ex. p ∈ {0.7, 0.8, 0.9}), TOUS les calculs intermédiaires affichés dans les detailedSolution doivent être recalculés à partir de cette variable. Aucune valeur dérivée ne peut être hardcodée en littéral.

❌ FAUTIF : `= 0{,}81 + 0{,}01 = 0{,}82` (vrai uniquement pour p=0.9)
✅ CORRIGÉ : `= {{p_fidele_carre_str}} + {{p_contraire_carre_str}} = {{p_V3_str}}`

L'audit doit lister TOUS les nombres décimaux apparaissant dans les detailedSolution et vérifier qu'ils proviennent d'une variable Python, pas d'une chaîne littérale.

RÈGLE 4.5 — Détection des invariants Vrai/Faux dépendants des paramètres

Pour les exercices "Vrai ou Faux", la réponse V/F dépend souvent de valeurs numériques précises. Avant de tirer aléatoirement un paramètre, vérifier algébriquement pour quelles valeurs la réponse V/F annoncée par la solution reste correcte.

Exemples typiques :

  • ∫₀^a x² dx = a² (aire d'un carré) ⟺ a = 3
  • f(x) = e^x + e^(cx) solution de y' = cy - e^xc = 2
  • A(n, k) = C(m, p) ⟺ couples (n, k, m, p) précis

Deux stratégies acceptables :

  1. Fixer les paramètres sur la valeur source (pas de variation aléatoire)
  2. Adapter dynamiquement la conclusion V/F selon le tirage (cf. règle 8.4)

RÈGLE 4.6 — Arrondi nul à supprimer

Quand une probabilité ou un résultat est exprimé en notation scientifique (valeur < 10⁻³), ne pas afficher l'arrondi ≈ 0,000 qui n'apporte aucune information.

✅ PATTERN

if round(float(proba), 3) == 0:
    proba_final = proba_exact_str           # ex: "8,1 × 10⁻⁵"
else:
    proba_final = f"{proba_exact_str} \\approx {proba_str}"

RÈGLE 4.7 — Cohérence des formules développées avec les paramètres

Si une solution développe une formule pas à pas (ex. n × (n-1) × (n-2) pour un arrangement à k=3), le paramètre k DOIT être fixé OU le développement DOIT être généré dynamiquement.

✅ PATTERN — helpers à inclure dans le bloc {python}

def develop_chain(start, length):
    """start × (start-1) × ... (length facteurs)"""
    return " \\times ".join(str(start - i) for i in range(length))

def develop_chain_to(start, end_inclusive):
    """start × (start-1) × ... × end_inclusive"""
    return " \\times ".join(str(i) for i in range(start, end_inclusive - 1, -1))

RÈGLE 4.8 — Pas d'assertion sur les distances numériques

Quand on tire une variable "presque égale" à une valeur cible (typiquement pour créer un piège dans un Vrai/Faux), NE PAS imposer un seuil de distance strict — l'inégalité mathématique structurelle (ex: rationnel vs irrationnel) suffit.

❌ FAUTIF : assert abs(float(frac) - J_approx) > 1e-4
            # plante pour 7/11 (différence ≈ 7×10⁻⁵) qui est pourtant
            # le piège le plus crédible

✅ CORRIGÉ : pas d'assertion sur la distance — la distinction structurelle
            (rationnel vs irrationnel) garantit l'inégalité.

RÈGLE 4.4 — Identifier les contraintes mathématiques avant de pythoniser

Avant de transformer une constante en variable aléatoire, vérifier qu'elle peut varier sans casser :

  • l'initialisation d'une récurrence
  • les conditions d'une inégalité
  • les hypothèses d'un théorème invoqué dans la solution
  • le sens d'une asymptote / limite

Si une contrainte existe, soit borner le tirage, soit ne pas pythoniser ce paramètre.


5. Calculs — Rational exact, pas de double approximation

RÈGLE 5.1 — Tous les calculs intermédiaires en Rational/Fraction exacte

❌ FAUTIF

p_A = round(rd.uniform(0.4, 0.8) / 0.05) * 0.05   # produit 0.35000000000000003

✅ CORRIGÉ

num_pA = rd.randint(8, 16)         # 0,40 à 0,80 par pas de 0,05
p_A = Fraction(num_pA, 20)

RÈGLE 5.2 — Conversion float() UNIQUEMENT au moment de l'affichage

# Calculs : tout en Rational
asymptote = v0 * inv_k + inv_k**2

# Affichage : convertir avec arrondi explicite
asymptote_disp = pxsl_format_number(round(float(asymptote), 2), **config_std)

RÈGLE 5.3 — Pas de fractions reconstruites par double limit_denominator

❌ FAUTIF

frac = Fraction(p_float).limit_denominator(1000) / Fraction(q_float).limit_denominator(1000)
frac = frac.limit_denominator(100)

✅ CORRIGÉ

# Calculer la fraction exacte directement à partir des Rational initiaux
frac_exact = p_inter_q / p_total

RÈGLE 5.4 — Centième inférieur via math.floor, pas via formule arithmétique

❌ FAUTIF

seuil_arrondi = round(seuil * 100 - 5) / 100   # donne 0,73 au lieu de 0,77

✅ CORRIGÉ

seuil_arrondi = math.floor(seuil * 100) / 100

RÈGLE 5.5 — Vérifier les opérations sur les coefficients

Si la solution simplifie αn + βn, le coefficient résultant est α + β, pas α - β.

❌ FAUTIF (vu dans exo 3)

# Dans la solution :
{{alpha}} w_n {{beta}} n + {{gamma}} &\geqslant {{alpha - beta}} n + {{gamma}}

Avec α=3, β=-2, le code calcule α - β = 5 au lieu de α + β = 1. Bug critique de simplification.

✅ CORRIGÉ

{{alpha}} w_n {{beta_n_str}} + {{gamma}} &\geqslant n + {{gamma}}    # car alpha + beta = 1

6. Interpolation MyST {{...}} — Pièges critiques

RÈGLE 6.1 — INTERDIT : appels de fonction avec **kwargs dans {{...}}

❌ FAUTIF

La suite converge vers ${{latex(limite_proposee, **config_standard)}}$.

Le rendu produit latex(limite_proposee, **config_standard) en clair, pas la valeur. Cause : MyST parse **config_standard comme du gras markdown, ce qui casse l'expression.

✅ CORRIGÉ

# Dans le bloc Python
limite_proposee_tex = latex(limite_proposee, **config_standard)
La suite converge vers ${{limite_proposee_tex}}$.

POURQUOI : tout appel f(x, **kwargs) dans {{...}} est interprété par MyST comme markdown avant interpolation Python. Toujours stocker le résultat dans une variable string en amont.

RÈGLE 6.2 — pxsl_format_number(...) dans {{...}} : créer la string en amont

❌ FAUTIF

Le coefficient vaut ${{pxsl_format_number(round(float(coef), 2), **config_standard)}}$.

✅ CORRIGÉ

coef_disp = pxsl_format_number(round(float(coef), 2), **config_std)
Le coefficient vaut ${{coef_disp}}$.

RÈGLE 6.3 — Pré-calculer toutes les variables d'affichage dans le bloc Python

À la fin du bloc Python principal, créer un bloc dédié au formattage. Seul sympy.latex(...) accepte **config_standard — les helpers PyxiScience (pxsl_format_number, pxsl_res_num, etc.) ont leurs propres kwargs spécifiques (cf. règle 6.4).

# === Variables d'affichage prêtes ===
k_disp        = pxsl_format_number(float(k_coef))                    # PAS de **config_std
coef_const_disp = pxsl_format_number(float(coef_const))
inv_k_tex     = latex(inv_k, **config_standard)                      # OK ici
asymptote_tex = latex(asymptote, **config_standard)                  # OK ici
alpha_disp    = pxsl_format_number(alpha)
# etc.

globals()

Le markdown ne contient alors plus que {{var}} simples, sans appel de fonction.

RÈGLE 6.4 — **config_standard réservé à sympy.latex

Le résultat de pxs_config() contient des clés (ln_notation, mat_str, etc.) destinées uniquement à sympy.latex(). Les helpers PyxiScience (pxsl_format_number, pxsl_res_num, pxsl_matrix, etc.) ont leurs propres kwargs et plantent si on leur passe **config_standard.

❌ FAUTIF : pxsl_format_number(aire_carre, **config_standard)
            → TypeError: got an unexpected keyword argument 'ln_notation'

✅ CORRECT : pxsl_format_number(aire_carre)                    ← pas de splat
✅ CORRECT : pxsl_res_num(proba, dec=4, egal=False)            ← kwargs explicites
✅ CORRECT : latex(p_fidele, **config_standard)                ← seul sympy.latex

Liste explicite : NE PAS splat **config_standard sur pxsl_format_number, pxsl_res_num, pxsl_matrix, pxsl_pow, pxsl_latex_coefficient (alias lc), pxsl_par, pxsl_mult, pxsl_choose_udv, pxs_Interval(...).print(). TOUJOURS splat sur sympy.latex(...).

RÈGLE 6.5 — {{ }} ne contient QU'UN nom de variable nu (camelCase, suffixe Aff)

Ajoutée 2026-06-12 (convention vérifiée sur 222/222 exemples livrés : 0 appel de fonction, 0 underscore, 2563 injections …Aff ; contrôle STATIQUE du harnais).

❌ FAUTIF :

$T(x) = {{latex(T)}}$, coefficient {{lc(a, sign=True)}}, sur {{dom.print()}}, p = {{p_str}}

✅ CORRIGÉ :

# Dans le bloc Python :
tTex = latex(T, **config_standard)
coefAAff = lc(a, sign=True)
domAff = dom.print()
pStr = str(p)
$T(x) = {{tTex}}$, coefficient {{coefAAff}}, sur {{domAff}}, p = {{pStr}}

POURQUOI : le runtime de la plateforme ne substitue de façon fiable que des identifiants nus ; un appel/calcul dans {{ }} peut fuir tel quel dans le rendu, et un underscore crée des indices LaTeX parasites. Tout s'évalue dans le bloc Python, dans des variables d'affichage camelCase suffixées Aff. (Auto-lift déterministe : app/pipeline/postprocess.py::auto_lift_injections + rename_underscore_injections.)


7. Affichage des coefficients signés — Anti-concaténation

RÈGLE 7.1 — NE JAMAIS concaténer un signe à une valeur dans le markdown

❌ FAUTIF

w_{n+1} = {{alpha}} w_n {{beta}} n + {{gamma}}

Avec β = -1, le rendu donne 2 w_n -1 n + 3 (moche : -1n au lieu de -n).

✅ CORRIGÉ

# Dans le bloc Python
if beta == -1:
    beta_n_str = "- n"
elif beta < 0:
    beta_n_str = f"- {-beta}n"
else:
    beta_n_str = f"+ {beta}n"
w_{n+1} = {{alpha}} w_n {{beta_n_str}} + {{gamma}}

RÈGLE 7.2 — Pour tout coefficient pouvant être ±, générer une string complète avec signe intégré

# Pour v'(t) = (coef_const + coef_t * t) e^(-kt)
# coef_const < 0 toujours → latex(coef_const) commence par "-"
# coef_t < 0 toujours → on construit explicitement
coef_t_signed = f"- {pxsl_format_number(float(-coef_t), **config_std)}"
# Affichage : ({{coef_const_disp}} {{coef_t_signed}} t)
# → "(-6,2 - 0,6 t)"

RÈGLE 7.3 — Cas typiques de coefficients à traiter explicitement

  • Coefficient devant t, n, x (terme variable d'un polynôme)
  • Constante d'intégration ajoutée
  • Reste d'une factorisation
  • Termes dans une somme avec signes alternés

RÈGLE 7.4 — Format des décimales à 3 chiffres fixes

Pour les arrondis à 10⁻³ près (consigne fréquente en Bac), utiliser un format à 3 décimales fixes plutôt que str(round(...)) qui tronque les zéros.

❌ FAUTIF : str(round(0.6561, 3))  →  '0.656'  (OK)
            str(round(0.6, 3))     →  '0.6'    (manque "00" final attendu)

✅ CORRECT : f"{x:.3f}".replace('.', '{,}')
            # 0.6561 → '0{,}656'
            # 0.6    → '0{,}600'

8. Solutions validées — Directive Chabane

RÈGLE 8.1 — INTERDICTION ABSOLUE de modifier les solutions validées

Le générateur ne doit pas :

  • ajouter des listes à puces qui n'existaient pas
  • ajouter des préambules redondants en début de solution
  • ajouter des étapes intermédiaires de calcul non validées
  • reformuler des phrases (changer "factoriser" en "diviser", etc.)
  • ajouter de la pédagogie ("Les quatre conditions de la loi binomiale sont vérifiées...")

❌ FAUTIF (vu dans exo 3)

{fr}`On divise le numérateur et le dénominateur par {{b1}}^n :`

Alors que la version validée disait :

{fr}`On factorise le numérateur par 5^n et le dénominateur par 3^n :`

(La manipulation est en réalité une factorisation séparée au numérateur et au dénominateur, pas une division commune.)

✅ CORRIGÉ

{fr}`On factorise le numérateur par {{b1}}^n et le dénominateur par {{d1}}^n :`

RÈGLE 8.2 — En cas de doute, copier-coller la solution validée

Le générateur ne doit jamais "améliorer" ou "compléter" une solution validée. Si la pythonisation impose une adaptation (variable au lieu de constante), substituer mécaniquement et rien d'autre.

RÈGLE 8.3 — Si la pythonisation a touché à une solution validée, signaler avant correction

L'audit doit demander confirmation explicite avant de restaurer. Le générateur ne doit pas le faire silencieusement.

RÈGLE 8.4 — Conclusions Vrai/Faux adaptatives

Pour les exos "Vrai ou Faux" où on veut faire varier des paramètres qui peuvent changer la réponse, préparer la conclusion en Python comme variable conditionnelle :

if condition_vraie:
    conclusion_fr = "... L'affirmation est vraie."
    conclusion_en = "... Statement is true."
else:
    conclusion_fr = f"... (détail spécifique). L'affirmation est fausse."
    conclusion_en = f"... (specific detail). Statement is false."

Puis injecter via {{conclusion_fr}} / {{conclusion_en}} dans la detailedSolution. La structure du raisonnement reste identique, seul le verdict final s'adapte au tirage.


9. Structure MyST — Paragraphes contextuels

RÈGLE 9.1 — Les paragraphes contextuels (intro de partie, intro de question groupée) sont HORS questionStatement

❌ FAUTIF

:::::{question}
:questionType: STQ

::::{questionStatement}
$\underline{\textbf{Partie A}}$

{fr}`Le centre propose aux personnes...`{en}`...`

{fr}`Question 1...`{en}`...`
::::

✅ CORRIGÉ

$\underline{\textbf{Partie A}}$

{fr}`Le centre propose aux personnes...`{en}`...`

(exo1-q1)=

:::::{question}
:questionType: STQ

::::{questionStatement}
{fr}`Question 1...`{en}`...`
::::

POURQUOI : questionStatement doit contenir UNIQUEMENT la consigne de la question. Tout contexte commun à plusieurs questions est un paragraphe libre entre les blocs :::::{question}.

RÈGLE 9.2 — Les paragraphes contextuels qui utilisent des variables {{var}} viennent APRÈS le bloc Python qui les définit

```{python}
v0 = ...
k_coef = ...
globals()

{fr}On considère la fonction $v$ avec $v(0) = {{v0}}$.{en}...


### RÈGLE 9.3 — Phrase "Dans cette question, on étudie..." (regroupement APMEP)

L'APMEP utilise parfois "Dans cette question, on étudie X" pour introduire un groupe de sous-questions (2.a, 2.b, 2.c, 2.d). En PyxiScience, les sous-questions sont éclatées en questions distinctes.

**RÈGLE** : reformuler en précisant le périmètre exact pour ne pas inclure les questions hors groupe.

{fr}Dans les quatre questions qui suivent, on étudie la fonction $v$ sur $[0~;~+\infty[$.


Le **nombre est explicite** (`les quatre questions`) pour éviter d'englober la question suivante (numérotée séparément en APMEP).

### RÈGLE 9.4 — Pas de doublons d'énoncés

L'énoncé de chaque question apparaît UNE SEULE FOIS, dans son `questionStatement`. Ne pas le répéter en paragraphe libre avant le bloc question.

**❌ FAUTIF** (vu dans exo 4)

{fr}Que vaut $d'(t_A)$ ? Interpréter ce résultat.

:::::{question} ::::{questionStatement} {fr}Que vaut $d'(t_A)$ ? Interpréter ce résultat. # DOUBLON ::::


### RÈGLE 9.5 — Variables Python dans les blocs matplotlib

Les substitutions `{{...}}` ne fonctionnent que dans le **texte MyST**, **PAS** à l'intérieur des blocs ```` ```{python} ```` . Dans un bloc Python, utiliser les vraies variables Python (f-string ou concaténation).

❌ FAUTIF : probs = r"${{p_fidele_str}}$" # matplotlib reçoit littéralement "${{p_fidele_str}}$" # mathtext rend "p_fidele_str" en italique math (incompréhensible)

✅ CORRECT : probs = f"${p_fidele_str}$" # Python interpole d'abord → matplotlib reçoit "$0{,}9$"


Pareil pour les `ax.set_title(...)`, `ax.text(...)`, `ax.legend(...)` : utiliser des f-strings Python, jamais `{{...}}`.

---

## 10. Bilinguisme FR/EN — Symétrie

### RÈGLE 10.1 — FR et EN doivent être équivalents en niveau de détail

**❌ FAUTIF** (vu dans exo 2 Q6, exo 4 Q9)
- FR détaille le changement de variable `X = 0,6t` puis applique les croissances comparées (5 lignes)
- EN saute directement à "by comparison of growth rates" puis le boxed (2 lignes)

**✅ CORRIGÉ** : les deux versions ont le même squelette de raisonnement, mêmes étapes, mêmes formules.

### RÈGLE 10.2 — Notation math identique en FR et EN

**❌ FAUTIF**

{fr}...l'intervalle $[0\,;\,1]$.{en}...the interval [0\,;\,1]. ^^^^^^^^^^^ pas en mode math


**✅ CORRIGÉ**

{fr}...l'intervalle $[0\,;\,1]$.{en}...the interval $[0\,;\,1]$.


### RÈGLE 10.3 — Si l'utilisateur a modifié l'énoncé, effacer la version EN

Si la version pythonisée modifie le contenu de l'énoncé (pas seulement réagencement structurel), le générateur efface tous les blocs `{en}` pour permettre passage en retraduction par l'app dédiée. Le réagencement (séparation a/b/c en questions distinctes) n'est PAS une modification.

### RÈGLE 10.4 — Format auto fraction / décimal selon la nature

Pour les valeurs qui peuvent être rationnelles non-décimales (ex. `0,5 / 0,6 = 5/6`), créer un helper qui choisit automatiquement entre forme décimale finie et fraction LaTeX :

```python
def _frac_to_latex(f):
    """Fraction Python → décimal LaTeX si fini, sinon \\frac{a}{b}."""
    if f.denominator == 1:
        return str(f.numerator)
    d = f.denominator
    while d % 2 == 0: d //= 2
    while d % 5 == 0: d //= 5
    if d == 1:
        return str(float(f)).replace('.', '{,}')
    return f"\\frac{{{f.numerator}}}{{{f.denominator}}}"

Exemples :

  • _frac_to_latex(Fraction(5, 8))'0{,}625' (décimal fini)
  • _frac_to_latex(Fraction(5, 6))'\\frac{5}{6}' (décimal infini)
  • _frac_to_latex(Fraction(5, 4))'1{,}25' (décimal fini)

RÈGLE 10.5 — Notation scientifique française pour valeurs très petites

Pour les valeurs < 10⁻⁴, basculer en notation scientifique LaTeX plutôt qu'afficher 0{,}000081.

import math

def _frac_to_str_smart(f, sci_threshold=1e-4):
    """Décimal pour valeurs raisonnables, sinon notation scientifique."""
    fl = float(f)
    if 0 < abs(fl) < sci_threshold:
        exp = int(math.floor(math.log10(abs(fl))))
        mant = fl / (10 ** exp)
        mant_str = f"{mant:.2f}".rstrip('0').rstrip('.').replace('.', '{,}') or "1"
        return f"{mant_str} \\times 10^{{{exp}}}"
    return _frac_to_latex(f)

11. Rendu graphique matplotlib

RÈGLE 11.1 — Toute variable utilisée dans l'énoncé DOIT être utilisée dans le tracé

❌ FAUTIF (vu dans exo 3)

x_tangente_horiz = rd.randint(1, 3)   # tirée

# Énoncé : "tangente horizontale en x = {{x_tangente_horiz}}"

# Mais le code matplotlib trace une fonction f(x) = K * (x^c - 1) * ln(x)
# qui a SA tangente horizontale FIGÉE en x=1 (par construction f(1)=0, f'(1)=0).

Le graphique montre la tangente horizontale en x=1 alors que l'énoncé annonce x=2 ou x=3. Incohérent dans 66% des seeds.

✅ CORRIGÉ

  • Soit fixer x_tangente_horiz = 1 (constante), soit modifier la fonction tracée pour que sa tangente horizontale soit effectivement en x_tangente_horiz.

RÈGLE 11.2 — La tangente tracée DOIT être la vraie tangente analytique

❌ FAUTIF

slope = f(x_A) / (x_A + 0.3)   # heuristique arbitraire

✅ CORRIGÉ

def f_prime(x):
    c = coeff_puissance
    return facteur * (c * x**(c-1) * np.log(x) + (x**c - 1)/x)

slope = f_prime(x_A)
b_intercept = f(x_A) - slope * x_A

RÈGLE 11.3 — Les labels (Cf, T, Δ, A) doivent rester DANS la fenêtre du graphique

Symptôme : si un label est positionné à (x, y) avec y > ylim_max, matplotlib étend automatiquement la zone d'affichage pour l'inclure, ce qui compresse le graphique.

❌ FAUTIF

ax.set_ylim(-1.2, 8)
ax.text(11, f(11) - 0.4, r"$\mathcal{C}_f$", ...)   # f(11) peut être > 8
ax.text(11, slope*11 + b_intercept + 0.15, "T", ...)  # idem

✅ CORRIGÉ

# Borner les paramètres pour que f(x_max_visible) reste sous le plafond
y_label_target = 6.5
denom_y_max = (x_max**coeff_puissance - 1) * np.log(x_max)
facteur_max = min(70, int(y_label_target / denom_y_max))
facteur_echelle = rd.randint(30, max(30, facteur_max))

# Placer les labels dans une zone garantie sûre
x_label = max(x_A + 1, x_max - 1.5)
x_label = min(x_label, x_max)
y_lab_cf = f(x_label) - 0.3
y_lab_t  = min(slope * x_label + b_intercept + 0.5, ylim_max - 0.5)

RÈGLE 11.4 — Pas de mélange Rational sympy + numpy array

❌ FAUTIF

d_vals = Rational(5, 3) * t_graph + asymptote   # Rational * np.array casse

✅ CORRIGÉ

d_vals = float(Rational(5, 3)) * t_graph + float(asymptote)
# ou
inv_k_float = 1.0 / float(k_coef)
d_vals = -(v0 + t_graph) * inv_k_float * np.exp(-float(k_coef) * t_graph) + ...

RÈGLE 11.5 — plt.show(), jamais plt.savefig

# ❌ Le chemin local n'est pas résolu côté plateforme
plt.savefig('arbre.png')
# puis dans le markdown : ![](arbre.png)

# ✅
plt.show()

RÈGLE 11.6 — Pas d'éléments parasites dans le graphique

  • Pas de ax.text(8.5, 22.2, "Fig. 2", ...) à l'intérieur du graphique. La référence "Fig. 2" est une légende externe APMEP, pas un élément du tracé.
  • Pas de doubles labels "1" sur les axes si les ticks par défaut sont déjà annotés.
  • Pas de titre auto matplotlib.

12. Fidélité APMEP

RÈGLE 12.1 — L'énoncé pythonisé est strictement conforme à l'APMEP, modulo paramètres

  • Toutes les phrases-clés de l'énoncé original sont présentes textuellement.
  • Toutes les valeurs numériques sont identiques (modulo pythonisation).
  • L'ordre des questions est respecté.

RÈGLE 12.2 — Sous-questions APMEP a/b/c → questions PyxiScience distinctes

Convention PyxiScience : chaque sous-question APMEP devient une question distincte (q1, q2, ...) avec son propre questionStatement / questionHint / detailedSolution. Ce réagencement n'est PAS une modification d'énoncé au sens de la directive 2.

RÈGLE 12.3 — Géométrie : pythonisation autorisée à condition de préserver le niveau

La pythonisation des exercices de géométrie dans l'espace (vecteurs, droites, plans, projections, orthocentres) est autorisée (cf. exos validés Amérique du Sud 13 novembre 2025, Amérique du Nord 22 mai 2025). Convention observée :

  • Représenter les points/vecteurs en sympy.Matrix([x, y, z]) pour calculs symboliques.
  • Vérifier l'orthogonalité via .dot() = 0 au lieu de calculer par projection.
  • Pour une projection orthogonale, paramétrer la droite (K = A + lam*(B-A)), construire CK.dot(AB) = 0, résoudre via solve(...).
  • Distances via sqrt(vec.dot(vec)) (norme).

⚠️ NE PAS changer le niveau pédagogique : si l'APMEP demande une démonstration géométrique (collinéarité, coplanarité, équation cartésienne d'un plan), la version pythonisée doit conserver cette difficulté — on randomise les coefficients, pas la nature du raisonnement.

❌ FAUTIF (mauvaise pythonisation)

# Donner les coordonnées du point projeté directement
A = Matrix([1, 0, 0]); B = Matrix([0, 1, 0]); C = Matrix([0, 0, 1])
K = Matrix([0.5, 0.5, 0])  # hardcodé — élève n'a plus à calculer

✅ CORRIGÉ

# Le tirage des sommets randomise les valeurs, mais l'élève DOIT calculer la projection
alpha, beta, gamma = rd.sample(range(1, 4), 3)
A = Matrix([alpha, 0, 0]); B = Matrix([0, beta, 0]); C = Matrix([0, 0, gamma])
lam = Symbol('lam', real=True)
K = A + lam*(B - A)
sol = solve((K - C).dot(B - A), lam)[0]
# K_proj = A + sol*(B - A)  # ← l'élève le démontre dans sa solution

RÈGLE 12.4 — Coquilles APMEP corrigées silencieusement

Si l'APMEP contient une coquille (ex: "au activités" sans "x"), la version PyxiScience peut la corriger silencieusement. Ce n'est pas une modification d'énoncé.


13. Pièges spécifiques par type d'exercice

13.1 — Probabilités

  • Tirages de probabilités : rd.randint(a, b) / 20 + Fraction, jamais round(uniform/0.05)*0.05.
  • Variables conditionnelles : ne pas écraser p_non_B_sachant_non_A (P_{Ā}(B̄)) par p_non_A_sachant_non_B (P_{B̄}(Ā)). Ce sont des conditionnelles inversées (Bayes), pas la même quantité.
  • Loi binomiale : scipy.stats.binom.cdf(k, n, p) pour P(X ≤ k).
  • Bienaymé-Tchebychev : math.floor(seuil*100)/100 pour le centième inférieur.

13.2 — Suites

  • Variation \case1/\case2 : à mettre INSIDE \right{}/\wrong{}, jamais autour.
  • pxs_variation_number toujours = 1 dans le code Python — calculer toutes les variations en parallèle.
  • Récurrence : voir RÈGLE 4.3 (préserver la propriété par contraintes sur les paramètres).

13.3 — Analyse / Intégration

  • Constante d'intégration : ajouter \py{C_new} ou demander explicitement à l'utilisateur.
  • Pas de concaténation \py{sg(x)} : créer une variable complète signe_coeff_x = "+ val" ou "- val" (RÈGLE 7.1).

13.4 — Tracés matplotlib

  • Voir section 11 complète.

16bis. Patterns récurrents observés en production

(Section issue de l'analyse des exos validés de mai/novembre 2025. À ne pas confondre avec la section 16 (Anti-patterns), qui liste les BUGS — ici on liste des CONVENTIONS positives à reproduire.)

RÈGLE 16.1 — Tirage de paramètres avec contraintes : préférer la list comprehension

Quand plusieurs paramètres doivent satisfaire une contrainte couplée (ex: p_vv > p_nv et denominator(1/(1-(p_vv-p_nv))) <= 6), ne PAS faire de for _ in range(100): ... if cond: break qui peut échouer silencieusement. Préférer une list comprehension qui énumère toutes les combinaisons valides, puis rd.choice().

❌ FAUTIF

for _ in range(100):
    p_vv = rd.choice(valeurs)
    p_nv = rd.choice(valeurs)
    if p_vv > p_nv and Rational(1, 1-(p_vv-p_nv)).denominator <= 6:
        break  # peut sortir sans break, valeurs invalides en silence

✅ CORRIGÉ

couples_valides = [
    (p_vv, p_nv) for p_vv in valeurs for p_nv in valeurs
    if p_vv > p_nv and Rational(1, 1 - (p_vv - p_nv)).denominator <= 6
]
p_vv, p_nv = rd.choice(couples_valides)  # garanti valide

POURQUOI : la list comprehension échoue de manière visible (IndexError si liste vide), au lieu de laisser passer des paramètres invalides. Pattern observé dans les exos validés Amérique du Sud 13 novembre 2025.

RÈGLE 16.2 — Loi binomiale : utiliser scipy.stats.binom (sf / cdf / pmf)

Pour les calculs P(X ≥ k), P(X ≤ k), P(X = k) d'une binomiale, toujours utiliser scipy.stats.binom.

❌ FAUTIF (calcul à la main, lent et fragile)

prob_inf = sum(binomial(n, i) * p**i * (1-p)**(n-i) for i in range(k+1))

✅ CORRIGÉ

from scipy.stats import binom
prob_inf       = binom.cdf(k, n, p)      # P(X ≤ k)
prob_sup_strict = binom.sf(k - 1, n, p)  # P(X ≥ k)   (sf = 1 - cdf(k-1))
prob_egal      = binom.pmf(k, n, p)      # P(X = k)

Astuce conditionnelle : si (X ≥ nb) ⊂ (X ≥ ne) (donc nb ≥ ne), alors P(X ≥ nb | X ≥ ne) = binom.sf(nb-1, n, p) / binom.sf(ne-1, n, p). Garantir ne < nb au tirage pour que le quotient soit ≤ 1.

POURQUOI : binom.sf (survival function) évite les sommes manuelles de coefficients binomiaux qui sont coûteuses en sympy et peuvent dériver numériquement. Pattern dans Exo 2 Amérique du Sud, exo binomiale randomisée.

RÈGLE 16.3 — Matplotlib : axes "zero spines" pour la convention math française

Pour les graphes de fonction (style scolaire FR), positionner les axes sur l'origine et masquer les spines droite/haute. C'est la convention attendue par les enseignants.

✅ PATTERN STANDARD

fig, ax = plt.subplots(figsize=(6, 5))
# ... ax.plot(...) ...
ax.spines['left'].set_position('zero')
ax.spines['bottom'].set_position('zero')
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
ax.xaxis.set_label_coords(1.02, -0.05)  # label "x" en bout d'axe
ax.yaxis.set_label_coords(-0.05, 1.02)  # label "y" en haut d'axe
plt.show()

À utiliser pour toute étude de fonction (Bac, terminale). Ne PAS utiliser pour les graphes de distribution / histogrammes (cadre classique avec spines complètes mieux adapté). Pattern observé dans Exos 3 + 4 Amérique du Sud.


14. Checklist pre-flight avant livraison

Le générateur n'envoie pas le fichier tant que les points suivants ne sont pas cochés :

Métadonnées

  • :id: est en première ligne et vide
  • Toutes les autres métadonnées sont identiques à la version non pythonisée

Architecture Python

  • UN SEUL bloc Python principal en début d'exercice
  • Imports uniquement dans le premier bloc
  • Tous les blocs Python se terminent par globals()
  • Aucune redéfinition de variable entre blocs
  • Bloc graphique (s'il existe) réutilise les variables du bloc principal

Tirages aléatoires

  • Calculs en Rational exact, conversion float() uniquement à l'affichage
  • Pas de int(x * 10) sur des flottants
  • Tous les paramètres dérivés sont calculés, pas tirés indépendamment
  • La propriété démontrée par l'exercice est vraie sur 100% des seeds (testée sur ≥ 200 seeds)
  • Pas de valeurs négatives quand l'énoncé exige des entiers naturels (ex: w_0 ≥ 0)

Affichage / interpolation

  • Aucun {{f(x, **kwargs)}} dans le markdown — toutes les strings d'affichage pré-calculées dans le bloc Python
  • Coefficients signés générés explicitement (pas de {{a}}{{b}}t avec b négatif)
  • pxsl_format_number reçoit toujours des valeurs déjà arrondies (round(float(x), n) en amont)

Solutions

  • Toutes les solutions validées Chabane sont conformes textuellement (modulo paramètres)
  • Aucun ajout pédagogique non sollicité (listes à puces, préambules, étapes intermédiaires)
  • Pas de reformulation ("factoriser" reste "factoriser", pas "diviser")

Structure MyST

  • Paragraphes contextuels HORS questionStatement
  • Pas de doublons d'énoncés (chaque énoncé apparaît une seule fois)
  • Phrases "Dans cette question, on étudie..." reformulées avec périmètre explicite
  • Ancres (exoN-qK)= présentes avant chaque bloc question

Bilinguisme

  • FR et EN équivalents en niveau de détail
  • Notation math identique ($[0\,;\,1]$ dans les deux versions)
  • Si l'énoncé est modifié → blocs {en} effacés (sinon conservés intacts)

Rendu graphique

  • Toutes les variables aléatoires utilisées dans l'énoncé sont effectivement utilisées dans le tracé
  • Tangentes calculées analytiquement (f'(x_A)), pas par heuristique
  • Tous les labels (Cf, T, Δ, A) restent dans la fenêtre [xlim, ylim]
  • Paramètres bornés pour que les courbes ne sortent pas du cadre
  • plt.show() (pas plt.savefig)
  • Pas de Rational sympy mélangé à des array numpy

Tests automatiques

  • Simulation sur ≥ 100 seeds : aucune erreur de rendu
  • Simulation sur ≥ 100 seeds : aucun artefact flottant (0.35000000000000003, 0.44999999999999996)
  • Simulation sur ≥ 100 seeds : toutes les contraintes mathématiques de l'exercice sont respectées
  • Aucune valeur affichée hors plage attendue (probabilité > 1, distance négative, etc.)

15. Convention de nommage des fichiers

  • Version non pythonisée corrigée : exoN_corrige.md
  • Version pythonisée corrigée : exoN_pythonise_corrige.md
  • Version FR-only (après modif d'énoncé) : *_FR_only.md
  • Toujours dans /mnt/user-data/outputs/
  • Pas de longues chaînes descriptives dans le nom

16. Anti-patterns récurrents — résumé

Les 10 erreurs les plus fréquentes vues sur les pythonisations livrées :

  1. {{latex(x, **config_standard)}} dans le markdown → casse à cause des **. Stocker en variable.
  2. Variables redéfinies entre blocs Python → valeurs incohérentes entre questions.
  3. Tirages indépendants de paramètres mathématiquement liés → énoncé faux (ex: L_min < asymptote).
  4. Paramètres aléatoires qui invalident la propriété démontrée → solution dit "vrai" alors que c'est faux.
  5. int(x * 10) sur flottant → bugs d'arrondi (ex: int(4.7 * 10) = 46).
  6. Heuristique de tangente au lieu de f'(x_A) analytique.
  7. x_paramètre aléatoire mais ignoré dans le tracé matplotlib → graphique incohérent avec l'énoncé.
  8. Labels matplotlib qui sortent de la fenêtre → graphique compressé.
  9. Solutions validées modifiées (listes à puces ajoutées, reformulations, étapes ajoutées) → directive Chabane violée.
  10. Doublons d'énoncés (même phrase en paragraphe libre + dans questionStatement).