wilenPxs commited on
Commit
87b004b
·
1 Parent(s): cce0786

Mode Declinaisons QCM/QAT: spec MCQ/FGQ (calibree sur 33 exemples valides), harnais etendu (collisions options multi-graines, arite FGQ, un seul bloc python), filets deterministes (mcqOption, None dernier, fusion blocs), UI mode+types, analyse partagee QCM/QAT, zero regression pythonisation (smoke 45/45)

Browse files
Files changed (47) hide show
  1. README_APP.md +116 -0
  2. app/config.py +6 -0
  3. app/knowledge/fewshots/declinaisons/{en}`Domain of`{fr}`Domaine de définition de` $_frac{a + bx}{x^2 - c}$.md +84 -0
  4. app/knowledge/fewshots/declinaisons/{en}`Domain of`{fr}`Domaine de définition de` $_sqrt{a - bx}$.md +73 -0
  5. app/knowledge/fewshots/declinaisons/{en}`Solution sets for homogeneous and nonhomogeneous systems in $_mathbb{R}^3$`{fr}`Ensembles des solutions pour des systèmes homogènes et inhomogènes de $_mathbb{R}^3$` - MCQ.md +373 -0
  6. app/knowledge/fewshots/declinaisons/{en}`The set of solutions of a homogeneous system`{fr}`Ensemble des solutions d'un système homogène` - MCQ.md +133 -0
  7. app/knowledge/fewshots/declinaisons/{en}`The set of solutions of an inhomogeneous system`{fr}`Ensemble des solutions d'un système inhomogène` - MCQ.md +131 -0
  8. app/knowledge/fewshots/declinaisons/{fr}`Amortissement dégressif d'un équipement`{en}`Declining Balance Depreciation of Equipment`.md +189 -0
  9. app/knowledge/fewshots/declinaisons/{fr}`Capitalisation annuelle contre capitalisation continue`{en}`Annual Compounding versus Continuous Compounding`.md +193 -0
  10. app/knowledge/fewshots/declinaisons/{fr}`Contrainte de budget publicitaire`{en}`Advertising budget constraint`.md +110 -0
  11. app/knowledge/fewshots/declinaisons/{fr}`Discontinuité d'une fraction rationnelle`{en}`Discontinuity of a Rational Fraction`.md +81 -0
  12. app/knowledge/fewshots/declinaisons/{fr}`Formule générale du seuil de rentabilité`{en}`General Break-Even Point Formula`.md +261 -0
  13. app/knowledge/fewshots/declinaisons/{fr}`Intérêts composés _ calcul d'un capital futur`{en}`Compound Interest_ Future Capital Calculation`.md +196 -0
  14. app/knowledge/fewshots/declinaisons/{fr}`Inéquation linéaire en deux étapes`{en}`Linear Inequality in Two Steps`.md +94 -0
  15. app/knowledge/fewshots/declinaisons/{fr}`Inéquation linéaire en une étape et notation intervalle`{en}`One-step linear inequality and interval notation`.md +150 -0
  16. app/knowledge/fewshots/declinaisons/{fr}`Lecture graphique du sens de variation et des extremums`{en}`Graphical reading of monotonicity and extrema`.md +218 -0
  17. app/knowledge/fewshots/declinaisons/{fr}`Monotonie selon la base et comparaison de valeurs`{en}`Monotonicity according to base and comparison of values`.md +157 -0
  18. app/knowledge/fewshots/declinaisons/{fr}`Objectif de revenu`{en}`Income Target`.md +163 -0
  19. app/knowledge/fewshots/declinaisons/{fr}`Plafond budgétaire`{en}`Budget cap`.md +154 -0
  20. app/knowledge/fewshots/declinaisons/{fr}`Prix admissible entre marge minimale et plafond réglementaire`{en}`Admissible price between minimum margin and regulatory ceiling`.md +213 -0
  21. app/knowledge/fewshots/declinaisons/{fr}`Prix plafond et demande décroissante`{en}`Price ceiling and decreasing demand`.md +127 -0
  22. app/knowledge/fewshots/declinaisons/{fr}`Règle des exposants _ simplification et calcul`{en}`Exponent Rules_ Simplification and Calculation`.md +162 -0
  23. app/knowledge/fewshots/declinaisons/{fr}`Résolution de`{en}`Resolution of` $_ax+b_ = k$ {fr}`avec`{en}`with` $k _ 0$.md +151 -0
  24. app/knowledge/fewshots/declinaisons/{fr}`Seuil de rentabilité d'une boutique`{en}`Break-Even Point of a Store`.md +198 -0
  25. app/knowledge/fewshots/declinaisons/{fr}`Simplifier des expressions avec exponentielle et logarithme`{en}`Simplify expressions with exponential and logarithm`.md +119 -0
  26. app/knowledge/fewshots/declinaisons/{fr}`Symétries d'une courbe de demande et restriction économique`{en}`Symmetries of a demand curve and economic restriction`.md +192 -0
  27. app/knowledge/fewshots/declinaisons/{fr}`Unités minimales pour couvrir les coûts fixes`{en}`Minimum units to cover fixed costs`.md +242 -0
  28. app/knowledge/fewshots/declinaisons/{fr}`Valeurs absolues élémentaires 2`{en}`Elementary Absolute Values 2`.md +215 -0
  29. app/knowledge/fewshots/declinaisons/{fr}`Valeurs absolues élémentaires 3`{en}`Elementary Absolute Values 3`.md +163 -0
  30. app/knowledge/fewshots/declinaisons/{fr}`Valeurs absolues élémentaires`{en}`Elementary Absolute Values`.md +283 -0
  31. app/knowledge/fewshots/declinaisons/{fr}`Zone de rentabilité symétrique autour de l'optimum`{en}`Symmetric profitability zone around the optimum`.md +236 -0
  32. app/knowledge/fewshots/declinaisons/{fr}`Équations exponentielles et logarithmiques`{en}`Exponential and Logarithmic Equations`.md +302 -0
  33. app/knowledge/fewshots/declinaisons/{fr}`Équilibre de marché _ l'offre rencontre la demande`{en}`Market Equilibrium_ Supply Meets Demand`.md +202 -0
  34. app/knowledge/fewshots/declinaisons/{fr}`Équilibre offre-demande sur un marché`{en}`Supply-demand equilibrium in a market`.md +237 -0
  35. app/knowledge/fewshots/declinaisons/{fr}`Évaluer un logarithme en reconnaissant une puissance de la base`{en}`Evaluate a logarithm by recognizing a power of the base`.md +131 -0
  36. app/knowledge/fewshots/qat.md +62 -0
  37. app/knowledge/fewshots/qcm.md +85 -0
  38. app/pipeline/fewshots.py +13 -0
  39. app/pipeline/generate.py +53 -15
  40. app/pipeline/orchestrator.py +106 -10
  41. app/pipeline/postprocess.py +110 -1
  42. app/pipeline/prompts.py +285 -0
  43. app/server.py +63 -21
  44. app/validation/harness.py +152 -1
  45. app/validation/sandbox.py +57 -18
  46. app/web/templates/index.html +45 -1
  47. tests/smoke.py +363 -0
README_APP.md ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Pythonise Exercice — v2
2
+
3
+ App Flask à deux modes :
4
+ - **Pythonisation** d'exercices PyxiScience MyST (valeurs fixes → randomisées) ;
5
+ - **Déclinaisons** (2026-07) : génération de versions **QCM** (MCQ) et/ou
6
+ **QAT** (FGQ, champs libres ordonnés) d'un exercice, randomisées elles aussi.
7
+
8
+ Pipeline async : analyse LLM + notions + RAG (parallèles, **partagés entre QCM
9
+ et QAT**) → génération par paires → audit LLM → filets déterministes → **porte
10
+ harnais** (100 graines + contrôles étendus déclinaisons : unicité des options
11
+ MCQ sur toutes les graines, 1 seule bonne réponse, « None » en dernier, arité
12
+ FGQ, `:solution:` JSON) ; verdict exposé dans l'UI.
13
+
14
+ ## Architecture (refonte 2026-06)
15
+
16
+ ```
17
+ run.py Lancement : python run.py (ou python -m app)
18
+ app/
19
+ ├── __init__.py create_app() + logging + .env
20
+ ├── config.py TOUTE la config : modèles, PYTHON_FENCE_BACKTICKS=4,
21
+ │ ANALYSIS_MODEL_IDX, RAG_TOP_K, USE_REASONING,
22
+ │ DEFAULT_LANG, JOB_TTL, chemins, prix
23
+ ├── server.py routes Flask (/, /health, /api/models, /api/jobs)
24
+ ├── keys.py chargement des clés API (.env)
25
+ ├── pipeline/
26
+ │ ├── orchestrator.py chef d'orchestre (1 exercice)
27
+ │ ├── analyze.py analyse + notions + RAG en PARALLÈLE
28
+ │ ├── generate.py découpage + génération par paires
29
+ │ ├── audit.py audit LLM ≤2 itérations + filet de sécurité patches
30
+ │ ├── postprocess.py filets déterministes (auto-lift {{}}, ${}, fences 4,
31
+ │ │ renommage camelCase, décimales/langue, dédoublonnage)
32
+ │ ├── solutions.py substitution des solutions validées (règle 8.1)
33
+ │ ├── translate.py langue cible fr/en/both (prose seule, Python masqué)
34
+ │ ├── prompts.py tous les prompts (v1 archivée dans knowledge/)
35
+ │ └── fewshots.py sélection d'un exemple canonique par type détecté
36
+ ├── rag/ functions.py (FAISS) · formatter.py · notions.py ·
37
+ │ catalogue.py (catalogue curé injecté par domaine)
38
+ ├── llm/ client.py (OpenRouter + reasoning opt.) · cost.py
39
+ ├── validation/ harness.py (porte qualité) · sandbox.py (exec sandboxée)
40
+ ├── knowledge/ pythonisation_rules.md · rules_digest.py ·
41
+ │ fewshots/ (exemples VERTS) · prompts_v1_archive.md ·
42
+ │ functions_catalogue.md (catalogue curé des helpers)
43
+ ├── corpus/ 5 fichiers de fonctions PyxiScience (RAG)
44
+ └── web/templates/index.html UI (batch, langue, verdict harnais, coût)
45
+ data/ notions.xlsx · faiss_cache/
46
+ tests/smoke.py smoke tests hors-ligne (LLM mocké)
47
+ ```
48
+
49
+ ## Installation (WSL Ubuntu)
50
+
51
+ ```bash
52
+ python3 -m venv .venv && source .venv/bin/activate
53
+ pip install -r requirements.txt # faiss-cpu + sentence-transformers ≈ 1.5 Go
54
+ cp .env.example .env # renseigner OPENROUTER_API_KEY / OPENAI_API_KEY
55
+ python run.py # http://127.0.0.1:5000
56
+ ```
57
+
58
+ Smoke tests (sans réseau) : `.venv/bin/python tests/smoke.py`
59
+ Validation d'une sortie : `python ../.claude/skills/validation-harness/harness.py <fichier>.md --seeds 300`
60
+
61
+ ### Endpoints
62
+
63
+ | Méthode | URL | Description |
64
+ |---------|------------------|--------------------------------------------------------|
65
+ | GET | `/` | UI web |
66
+ | GET | `/health` | sanity check |
67
+ | GET | `/api/models` | roster des modèles + défauts |
68
+ | POST | `/api/jobs` | démarre un job (1 fichier OU batch) → `{job_id}` |
69
+ | GET | `/api/jobs/<id>` | suivi + résultats par fichier (harnais, coût, langue) |
70
+ | GET | `/api/jobs/<id>/download` | ZIP de toutes les sorties `.md` + `_recapitulatif.md` |
71
+
72
+ POST body :
73
+ ```json
74
+ {
75
+ "files": [{"filename": "exo.md", "content": "<MyST>"}, ...],
76
+ "lang": "fr | en | both",
77
+ "level": "",
78
+ "model_idx": 1,
79
+ "mode": "pythonise | declinaisons",
80
+ "types": {"qcm": true, "qat": true}
81
+ }
82
+ ```
83
+ (`mode` absent ⇒ `pythonise`, rétro-compatible ; `types` requis en mode
84
+ `declinaisons`, au moins un `true` — les deux cochés ⇒ 2 fichiers par source,
85
+ nommés `<source>_QCM.md` / `<source>_QAT.md`, avec UNE seule analyse partagée.
86
+ Rétro-compat : `{"content": "...", "filename": "..."}` accepté pour 1 fichier.)
87
+
88
+ Résultat par fichier : `exercise`, `analysis`, `notions`, `audit_patches`,
89
+ `warnings`, **`harness` {ok, seeds, summary}**, **`lang` {source, target, action}**,
90
+ **`cost` {usd, eur, requests}**, `duration_s`.
91
+
92
+ **Téléchargement** (UI, panneau Résultat) : bouton **« Télécharger .md »**
93
+ (sortie pythonisée du fichier affiché, suffixe `_pythonise.md` pour ne pas
94
+ écraser la source) ; en **batch**, bouton **« Tout (.zip) »** → ZIP de toutes
95
+ les sorties + un `_recapitulatif.md` (verdict harnais / warnings / coût par fichier).
96
+
97
+ ## Sécurité
98
+
99
+ 🚨 **Les clés API dans `.env` étaient en clair dans le zip d'origine** —
100
+ considère-les comme exposées et **rotate-les** :
101
+ - OpenRouter : https://openrouter.ai/keys
102
+ - OpenAI : https://platform.openai.com/api-keys
103
+
104
+ La sandbox (`app/validation/sandbox.py`) exécute le code généré par le LLM
105
+ avec builtins restreints, imports whitelistés et timeout (mono-poste : le
106
+ modèle de menace est l'accident LLM, pas un adversaire).
107
+
108
+ ## Notes
109
+
110
+ - Premier lancement : reconstruit le cache FAISS si absent (≈ 30 s).
111
+ - Modèles (IDs vérifiés sur OpenRouter 2026-06-12) : Opus 4.8, **Sonnet 4.6
112
+ (défaut)**, Fable 5, Haiku 4.5, Gemini 2.5 Pro, GPT-5.2.
113
+ - L'étape d'analyse suit le modèle choisi par l'utilisateur
114
+ (`ANALYSIS_MODEL_IDX=None` dans config.py pour ce comportement).
115
+ - Mode batch : fichiers traités séquentiellement ; un échec n'arrête pas les
116
+ autres ; récapitulatif VERT/ROUGE/erreurs + coût total.
app/config.py CHANGED
@@ -70,6 +70,12 @@ HARNESS_REPAIR_MAX = 2 # boucles de réparation LLM si la porte est ro
70
  # ── Langue cible ─────────────────────────────────────────────────────────────
71
  DEFAULT_LANG = "fr" # "fr" | "en" | "both"
72
 
 
 
 
 
 
 
73
  # ── Serveur / jobs ───────────────────────────────────────────────────────────
74
  JOB_TTL = 1800 # s avant purge d'un job terminé
75
  # HOST/PORT pilotables par l'environnement (déploiement). En local : 127.0.0.1.
 
70
  # ── Langue cible ─────────────────────────────────────────────────────────────
71
  DEFAULT_LANG = "fr" # "fr" | "en" | "both"
72
 
73
+ # ── Modes (pythonisation / déclinaisons QCM-QAT) ─────────────────────────────
74
+ DEFAULT_MODE = "pythonise" # "pythonise" | "declinaisons"
75
+ DECLINAISON_TYPES = ("qcm", "qat")
76
+ MCQ_NUM_OPTIONS = 5 # 1 correcte + 3 distracteurs + « None » en dernier
77
+ QAT_FALLBACK_TO_MCQ = True # question non auto-corrigeable en champ libre → MCQ
78
+
79
  # ── Serveur / jobs ───────────────────────────────────────────────────────────
80
  JOB_TTL = 1800 # s avant purge d'un job terminé
81
  # HOST/PORT pilotables par l'environnement (déploiement). En local : 127.0.0.1.
app/knowledge/fewshots/declinaisons/{en}`Domain of`{fr}`Domaine de définition de` $_frac{a + bx}{x^2 - c}$.md ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :originalExerciseId: 22d960b8-d57e-46d2-8ebd-09d8bed0a1fb
3
+ :title: {en}`Domain of`{fr}`Domaine de définition de` $\frac{a + bx}{x^2 - c}$
4
+ :modules: Analyse_I_EFREI, Calc_1_Pyx
5
+ :recommendedExecutionTime: 0
6
+ :level: Elementary
7
+ :chap: chap_realFunctions_Graphs_functionsFundamentalDefinitions_ESCP, Second_chap_VIII, chap_I_fonction_reel_EFREI, Calculus_I_Functions_and_their_Representations_1_1
8
+ :involvedConcepts:
9
+ :originalSource: Selin
10
+ :visibility: All
11
+ :variations:
12
+ :comment:
13
+ :id: 91465036-709e-11f1-a8a1-0ed8d3b012a9
14
+
15
+ ```{python}
16
+
17
+ import random as rd
18
+ from sympy import *
19
+ from pyxiscience.Mes_fctions_d_analyse import pxs_config
20
+
21
+ from pyxiscience.Classes_Extensions import pxs_Interval
22
+
23
+ config_standard = pxs_config()
24
+
25
+ import random as rd
26
+
27
+ x = symbols('x')
28
+ a, b, c = rd.sample(range(1, 10), 3)
29
+
30
+
31
+ f_num = (a*x+b)
32
+ f_den =(x**2 - c)
33
+ f = f_num/f_den
34
+ domaine1 = pxs_Interval(-oo, -sqrt(c), True, True)
35
+ domaine2 = pxs_Interval(sqrt(c), +oo, True, True)
36
+
37
+ # === Ajouts conversion FGQ ===
38
+ b_inf = latex(-sqrt(c))
39
+ b_sup = latex(sqrt(c))
40
+ ```
41
+
42
+ :::::{question}
43
+ :questionType: FGQ
44
+ :solution: [["ord","${{ b_inf }}$","${{ b_sup }}$"],["0.000001","0.000001"]]
45
+
46
+ ::::{questionStatement}
47
+ {en}`Find the domain of the function`{fr}`Donner le domaine de la fonction`
48
+ \begin{equation*}
49
+ \ds f(x) = {{ latex(f, **config_standard) }}.
50
+ \end{equation*}
51
+ {en}`The function is defined for every real number except the two values that cancel the denominator. Give these two excluded values`{fr}`La fonction est définie pour tout réel sauf aux deux valeurs qui annulent le dénominateur. Donner ces deux valeurs exclues` $x_1 < x_2$ :
52
+
53
+ $x_1 =$ {input}`||120` {en}`and`{fr}`et` $x_2 =$ {input}`||120`
54
+ ::::
55
+
56
+ ::::{questionHint}
57
+ {en}`A rational function is defined whenever its denominator is not zero. Start by solving`{fr}`Une fonction rationnelle est définie lorsque son dénominateur n’est pas nul. Commencez par résoudre` $x^2-c=0$.
58
+ ::::
59
+
60
+ ::::{displayedSolution}
61
+ $x_1 = {{ b_inf }}$ {en}`and`{fr}`et` $x_2 = {{ b_sup }}$
62
+ ::::
63
+
64
+ ::::{detailedSolution}
65
+
66
+ {en}`The denominator cannot be zero, therefore:`{fr}`Le dénominateur doit être différent de zéro :`
67
+ \begin{equation*}
68
+ {{ latex(f_den, **config_standard) }}\neq 0 \implies x \neq \pm {{ latex(sqrt(c)) }}.
69
+ \end{equation*}
70
+ {en}`Thus we have`{fr}`On a donc`
71
+ \begin{equation*}
72
+ \boxed{
73
+ \text{{en}`Domain:`{fr}`Domaine de définition :` }{{ domaine1.print() }} \cup {{ domaine2.print() }}}.
74
+ \end{equation*}
75
+ ::::
76
+
77
+ ::::{weightDistribution}
78
+ :reasoning: 20
79
+ :logic: 35
80
+ :abstraction: 30
81
+ :calculation: 15
82
+ ::::
83
+ :::::
84
+ `````
app/knowledge/fewshots/declinaisons/{en}`Domain of`{fr}`Domaine de définition de` $_sqrt{a - bx}$.md ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :originalExerciseId: e8559359-32c0-4394-94be-daf9a701cb6a
3
+ :title: {en}`Domain of`{fr}`Domaine de définition de` $\sqrt{a - bx}$
4
+ :modules: Analyse_I_EFREI, Calc_1_Pyx
5
+ :recommendedExecutionTime: 0
6
+ :level: Elementary
7
+ :chap: chap_realFunctions_Graphs_functionsFundamentalDefinitions_ESCP, Second_chap_VIII, chap_I_fonction_reel_EFREI, Calculus_I_Functions_and_their_Representations_1_1
8
+ :involvedConcepts:
9
+ :originalSource: Selin
10
+ :visibility: All
11
+ :variations:
12
+ :comment:
13
+ :id: 8f0a1b0c-709e-11f1-a8a1-0ed8d3b012a9
14
+
15
+ ```{python}
16
+
17
+ import random as rd
18
+ from sympy import *
19
+ from pyxiscience.Mes_fctions_d_analyse import pxs_config
20
+ from pyxiscience.Classes_Extensions import pxs_Interval
21
+
22
+ config_standard = pxs_config()
23
+
24
+ import random as rd
25
+
26
+ x = symbols('x')
27
+ a, b = rd.sample(range(1, 10), 2)
28
+
29
+ arg = a - b*x
30
+ f = sqrt(arg)
31
+
32
+ domaine = pxs_Interval(-oo, Rational(a,b), True, False)
33
+
34
+ # === Ajouts conversion FGQ ===
35
+ borne = latex(Rational(a, b))
36
+ ```
37
+
38
+ :::::{question}
39
+ :questionType: FGQ
40
+ :solution: [["ord","${{ borne }}$"],["0.000001"]]
41
+
42
+ ::::{questionStatement}
43
+ {en}`Find the domain of the function`{fr}`Trouver le domaine de définition de la fonction` $f(x) = {{ latex(f) }}$. {en}`Its domain is an interval of the form`{fr}`Son domaine de définition est un intervalle de la forme` $(-\infty,\ \alpha]$. {en}`Give the value of`{fr}`Donner la valeur de` $\alpha$ :
44
+
45
+ $\alpha =$ {input}`||120`
46
+ ::::
47
+
48
+ ::::{questionHint}
49
+ {en}`A square root is defined only when its radicand is nonnegative. Start by solving`{fr}`Une racine carrée est définie seulement lorsque son radical est positif ou nul. Commencez par résoudre` ${{ latex(arg) }} \ge 0$.
50
+ ::::
51
+
52
+ ::::{displayedSolution}
53
+ $\alpha = {{ borne }}$
54
+ ::::
55
+
56
+ ::::{detailedSolution}
57
+
58
+ {en}`For the square root to be defined,`{fr}`Pour que la racine carrée soit définie, il faut que` ${{ latex(arg) }}\geq 0$. {en}`Solving for $x$ gives`{fr}`En résolvant l'inéquation, nous obtenons` ${{ a }} \geq {{ latex(b*x) }}$, {en}`or`{fr}`ou` $x \leq {{ latex(Rational(a,b)) }}$.
59
+ \begin{equation*}
60
+ \boxed{
61
+ \text{{en}`Domain:`{fr}`Domaine :` }{{ domaine.print() }}}.
62
+ \end{equation*}
63
+
64
+ ::::
65
+
66
+ ::::{weightDistribution}
67
+ :reasoning: 30
68
+ :logic: 25
69
+ :abstraction: 30
70
+ :calculation: 15
71
+ ::::
72
+ :::::
73
+ `````
app/knowledge/fewshots/declinaisons/{en}`Solution sets for homogeneous and nonhomogeneous systems in $_mathbb{R}^3$`{fr}`Ensembles des solutions pour des systèmes homogènes et inhomogènes de $_mathbb{R}^3$` - MCQ.md ADDED
@@ -0,0 +1,373 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :id: 27512f8b-6fb2-11f1-a8a1-0ed8d3b012a9
3
+ :originalExerciseId: 9cad7853-dc83-4e8f-b06c-dcfc516b66fb
4
+ :title: {en}`Solution sets for homogeneous and nonhomogeneous systems in $\mathbb{R}^3$`{fr}`Ensembles des solutions pour des systèmes homogènes et inhomogènes de $\mathbb{R}^3$` - MCQ
5
+ :modules: Linear_Algebra_Pyx, Linear_Algebra_Trush
6
+ :recommendedExecutionTime: 30
7
+ :level: Elementary
8
+ :chap: section_2_4_linalg_Trush
9
+ :involvedConcepts:
10
+ :originalSource: 7.4 Ronan
11
+ :visibility: All
12
+ :variations:
13
+ :comment:
14
+
15
+ ```{python}
16
+
17
+ # Code Python : Ecrivez ci-dessous votre code Python
18
+ from __future__ import division
19
+ from pyxiscience.Mes_fctions_d_alg_lineaire_bis import *
20
+ from sympy import *
21
+ import random as rd
22
+
23
+
24
+
25
+
26
+ #### Question 1
27
+
28
+
29
+ n = 3
30
+ p = 3
31
+
32
+ lam=rd.randint(-9,9)
33
+ nu=rd.randint(-9,9)
34
+ bnn=rd.randint(-9,9)
35
+ while lam==0 or nu==0 or bnn==0:
36
+ lam=rd.randint(-9,9)
37
+ nu=rd.randint(-9,9)
38
+ bnn=rd.randint(-9,9)
39
+
40
+ A= randmatrixrect(n,p,-9,9)
41
+ B= randmatrixrect(n,1,-9,9)
42
+
43
+ A[0,0]=1
44
+ A[0,1]=lam
45
+ A[0,2]=nu
46
+
47
+ for i in range(1,n):
48
+ for j in range(0,p):
49
+ A[i,j]=0
50
+ B[i]=0
51
+
52
+ B[0]=0
53
+ x='x'
54
+ vect_x=Matrix([Symbol(x+'_1')])
55
+ for i in range(p-1):
56
+ vect_x=vect_x.row_join(Matrix([Symbol(x+'_'+str(i+2))]))
57
+ expr=myst(r""" """,globals(),locals())
58
+
59
+ if A[0, :].is_zero_matrix:
60
+ expr += "0"
61
+ # Gère l'affichage du premier terme non nul sans le '+' devant
62
+ if A[0, 0] != 0:
63
+ expr+=pxsl_ax(A[0,0],vect_x[0], frac = frac)
64
+ sign="+"
65
+ else:
66
+ sign=""
67
+ for j in range(1,p):
68
+ if A[0,j]!=0:
69
+ expr+=pxsl_ax(A[0,j],vect_x[j],sign, frac = frac)
70
+ sign="+"
71
+
72
+ rhs = myst(r"""{{ B[0].p }}/{{ B[0].q }}""", globals(), locals()) if (isinstance(B[i], Rational) and B[0].q != 1 and not frac) else latex(B[0])
73
+
74
+ expr+=myst(r""" ={{ rhs }}""",globals(),locals())
75
+
76
+ sys=expr
77
+
78
+
79
+ Alat=pxsl_matrix(A)
80
+ Blat=pxsl_matrix(B)
81
+ augmentmat1=pxsl_double_matrix(A,B,opt='ext')
82
+
83
+
84
+
85
+
86
+ soluce1=pxs_gauss_jordan(A.copy(), B.copy(), method = "mat", view = "ext")
87
+
88
+ Ared1=pxsl_double_matrix(soluce1["A"],soluce1["B"],opt='ext')
89
+
90
+
91
+
92
+ #### Question 2
93
+
94
+ B[0]=bnn
95
+
96
+ x='x'
97
+ vect_x=Matrix([Symbol(x+'_1')])
98
+ for i in range(p-1):
99
+ vect_x=vect_x.row_join(Matrix([Symbol(x+'_'+str(i+2))]))
100
+ expr=myst(r""" """,globals(),locals())
101
+
102
+ if A[0, :].is_zero_matrix:
103
+ expr += "0"
104
+ # Gère l'affichage du premier terme non nul sans le '+' devant
105
+ if A[0, 0] != 0:
106
+ expr+=pxsl_ax(A[0,0],vect_x[0], frac = frac)
107
+ sign="+"
108
+ else:
109
+ sign=""
110
+ for j in range(1,p):
111
+ if A[0,j]!=0:
112
+ expr+=pxsl_ax(A[0,j],vect_x[j],sign, frac = frac)
113
+ sign="+"
114
+
115
+ rhs = myst(r"""{{ B[0].p }}/{{ B[0].q }}""", globals(), locals()) if (isinstance(B[i], Rational) and B[0].q != 1 and not frac) else latex(B[0])
116
+
117
+ expr+=myst(r""" ={{ rhs }}""",globals(),locals())
118
+
119
+ sys2=expr
120
+
121
+
122
+ Alat=pxsl_matrix(A)
123
+ Blat=pxsl_matrix(B)
124
+ augmentmat2=pxsl_double_matrix(A,B,opt='ext')
125
+
126
+
127
+
128
+
129
+ soluce2=pxs_gauss_jordan(A.copy(), B.copy(), method = "mat", view = "ext")
130
+
131
+ Ared2=pxsl_double_matrix(soluce2["A"],soluce2["B"],opt='ext')
132
+
133
+
134
+
135
+ ```
136
+
137
+
138
+
139
+
140
+
141
+ :::::{question}
142
+ :questionType: MCQ
143
+ :questionId: 1
144
+ :questionIndex: 1
145
+
146
+ ::::{questionStatement}
147
+ {en}`Which of the following best describes the solution set of the equation`{fr}`Laquelle des propositions suivantes décrit le mieux l'ensemble des solutions de l'équation`
148
+
149
+ \begin{equation}\label{hom}
150
+ {{ sys }}.
151
+ \end{equation}
152
+ ::::
153
+
154
+ ::::{questionHint}
155
+
156
+ ::::
157
+
158
+ ::::{mcqAnswer}
159
+ :isRightAnswer: true
160
+ {en}`A plane through the origin`{fr}`Un plan passant par l'origine`
161
+ ::::
162
+
163
+ ::::{mcqAnswer}
164
+ :isRightAnswer: false
165
+ {en}`A line through the origin`{fr}`Une droite passant par l'origine`
166
+ ::::
167
+
168
+ ::::{mcqAnswer}
169
+ :isRightAnswer: false
170
+ {en}`The whole space $\mathbb{R}^3$`{fr}`L'espace $\mathbb{R}^3$ tout entier`
171
+ ::::
172
+
173
+ ::::{mcqAnswer}
174
+ :isRightAnswer: false
175
+ {en}`Only the origin $\{0\}$`{fr}`Uniquement l'origine $\{0\}$`
176
+ ::::
177
+
178
+ ::::{mcqAnswer}
179
+ :isRightAnswer: false
180
+ {fr}`Aucune de ces réponses n'est correcte` {en}`None of these answers are correct`
181
+ ::::
182
+
183
+ ::::{detailedSolution}
184
+
185
+ {en}`The equation`{fr}`L'équation` ${{ sys }}$ {en}`may be written with an augmented matrix as`{fr}`peut s'écrire sous la forme de matrice augmentée comme`
186
+ \begin{equation*}
187
+ {{ Ared1 }},
188
+ \end{equation*}
189
+
190
+
191
+ {en}`which is equivalent to the system`{fr}`ce qui est équivalent au système`
192
+ \begin{equation*}
193
+ {{ soluce1["sys"] }}.
194
+ \end{equation*}
195
+
196
+ {en}`The set of solutions of the equation`{fr}`L'ensemble des solution de l'équation` ${{ sys }}$ {en}`is then`{fr}`est donc`
197
+ \begin{equation*}
198
+ \boxed{
199
+ {{ soluce1["span"] }}.
200
+ }
201
+ \end{equation*}
202
+ {en}`Geometrically, the solution set forms a plane in`{fr}`Géométriquement, l'ensemble des solutions forme un plan de` $\mathbb{R}^3.$
203
+ ::::
204
+
205
+ ::::{weightDistribution}
206
+ :
207
+ ::::
208
+ :::::
209
+
210
+
211
+
212
+ :::::{question}
213
+ :questionType: MCQ
214
+ :questionId: 2
215
+ :questionIndex: 2
216
+
217
+ ::::{questionStatement}
218
+ {en}`Which of the following best describes the solution set of the equation`{fr}`Laquelle des propositions suivantes décrit le mieux l'ensemble des solutions de l'équation`
219
+ \begin{equation}\label{inhom}
220
+ {{ sys2 }}.
221
+ \end{equation}
222
+ ::::
223
+
224
+ ::::{questionHint}
225
+
226
+ ::::
227
+
228
+ ::::{mcqAnswer}
229
+ :isRightAnswer: true
230
+ {en}`A plane that does not pass through the origin`{fr}`Un plan ne passant pas par l'origine`
231
+ ::::
232
+
233
+ ::::{mcqAnswer}
234
+ :isRightAnswer: false
235
+ {en}`A plane through the origin`{fr}`Un plan passant par l'origine`
236
+ ::::
237
+
238
+ ::::{mcqAnswer}
239
+ :isRightAnswer: false
240
+ {en}`A single point`{fr}`Un unique point`
241
+ ::::
242
+
243
+ ::::{mcqAnswer}
244
+ :isRightAnswer: false
245
+ {en}`The empty set (the equation has no solution)`{fr}`L'ensemble vide (l'équation n'a aucune solution)`
246
+ ::::
247
+
248
+ ::::{mcqAnswer}
249
+ :isRightAnswer: false
250
+ {fr}`Aucune de ces réponses n'est correcte` {en}`None of these answers are correct`
251
+ ::::
252
+
253
+ ::::{detailedSolution}
254
+
255
+ {en}`The equation`{fr}`L'équation` ${{ sys2 }}$ {en}`may be written with an augmented matrix as`{fr}`peut s'écrire sous la forme de matrice augmentée comme`
256
+ \begin{equation*}
257
+ {{ Ared2 }},
258
+ \end{equation*}
259
+
260
+
261
+ {en}`which is equivalent to the system`{fr}`ce qui est équivalent au système`
262
+ \begin{equation*}
263
+ {{ soluce2["sys"] }}.
264
+ \end{equation*}
265
+
266
+ {en}`The set of solutions of the equation`{fr}`L'ensemble des solution de l'équation` ${{ sys2 }}$ {en}`is then`{fr}`est donc`
267
+ \begin{equation*}
268
+ \boxed{
269
+ {{ soluce2["span"] }}.
270
+ }
271
+ \end{equation*}
272
+ {en}`Geometrically, the solution set forms a plane in`{fr}`Géométriquement, l'ensemble des solutions forme un plan de` $\mathbb{R}^3.$
273
+ ::::
274
+
275
+ ::::{weightDistribution}
276
+ :
277
+ ::::
278
+ :::::
279
+
280
+
281
+
282
+ :::::{question}
283
+ :questionType: MCQ
284
+ :questionId: 3
285
+ :questionIndex: 3
286
+
287
+ ::::{questionStatement}
288
+ {en}`How do the two solution sets compare geometrically?`{fr}`Comment peut-on comparer géométriquement les deux ensembles de solutions ?`
289
+ ::::
290
+
291
+ ::::{questionHint}
292
+
293
+ ::::
294
+
295
+ ::::{mcqAnswer}
296
+ :isRightAnswer: true
297
+ {en}`They are parallel planes: the nonhomogeneous one is a translate of the homogeneous one`{fr}`Ce sont des plans parallèles : celui du système inhomogène est un translaté de celui du système homogène`
298
+ ::::
299
+
300
+ ::::{mcqAnswer}
301
+ :isRightAnswer: false
302
+ {en}`They are the same plane`{fr}`Ce sont le même plan`
303
+ ::::
304
+
305
+ ::::{mcqAnswer}
306
+ :isRightAnswer: false
307
+ {en}`They are two perpendicular planes`{fr}`Ce sont deux plans perpendiculaires`
308
+ ::::
309
+
310
+ ::::{mcqAnswer}
311
+ :isRightAnswer: false
312
+ {en}`They intersect along a line`{fr}`Ils se coupent selon une droite`
313
+ ::::
314
+
315
+ ::::{mcqAnswer}
316
+ :isRightAnswer: false
317
+ {fr}`Aucune de ces réponses n'est correcte` {en}`None of these answers are correct`
318
+ ::::
319
+
320
+ ::::{detailedSolution}
321
+
322
+ {en}`Geometrically, the solution set of the nonhomogeneous equation` [](#inhom) {en}`is a translation by the vector` ${{ Blat }}$ {en}`of the solution set of the homogeneous equation` [](#hom).
323
+
324
+
325
+ ```{python}
326
+
327
+ # Code Python : Ecrivez ci-dessous votre code Python
328
+
329
+
330
+ import numpy as np
331
+ import matplotlib.pyplot as plt
332
+ from mpl_toolkits.mplot3d import Axes3D
333
+
334
+ # Création de la figure et de l'axe 3D
335
+ fig = plt.figure(figsize=(10, 8))
336
+ ax = fig.add_subplot(111, projection='3d')
337
+
338
+ # Définition des limites pour x et y
339
+ x = np.linspace(-4, 4, 10)
340
+ y = np.linspace(-4, 4, 10)
341
+ X, Y = np.meshgrid(x, y)
342
+
343
+ # Équation du premier plan : z = a*x + b*y + c
344
+ a, b, c = -1/nu, -lam/nu, 0
345
+ Z1 = a*X + b*Y + c
346
+
347
+ # Équation du deuxième plan parallèle : z = a*x + b*y + d (d ≠ c)
348
+ d = -bnn/nu
349
+ Z2 = a*X + b*Y + d
350
+
351
+ # Tracé des deux plans
352
+ ax.plot_surface(X, Y, Z1, alpha=0.5, color='blue', label='Plane (1)')
353
+ ax.plot_surface(X, Y, Z2, alpha=0.5, color='red', label='Plane (2)')
354
+
355
+ # Ajout des labels et titre
356
+ ax.set_xlabel('$x_1$')
357
+ ax.set_ylabel('$x_2$')
358
+ ax.set_zlabel('$x_3$')
359
+ ax.set_title('The two planes')
360
+
361
+ # Affichage de la légende
362
+ ax.legend()
363
+
364
+ plt.show()
365
+
366
+ ```
367
+ ::::
368
+
369
+ ::::{weightDistribution}
370
+ :
371
+ ::::
372
+ :::::
373
+ `````
app/knowledge/fewshots/declinaisons/{en}`The set of solutions of a homogeneous system`{fr}`Ensemble des solutions d'un système homogène` - MCQ.md ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :id: e2c3b203-6fb1-11f1-a8a1-0ed8d3b012a9
3
+ :originalExerciseId: e5717816-752a-4abf-9afc-55df5774e4e0
4
+ :title: {en}`The set of solutions of a homogeneous system`{fr}`Ensemble des solutions d'un système homogène` - MCQ
5
+ :modules:
6
+ :recommendedExecutionTime: 30
7
+ :level: Elementary
8
+ :chap: section_2_4_linalg_Trush
9
+ :involvedConcepts:
10
+ :originalSource: 7.1 Ronan
11
+ :visibility: All
12
+ :variations:
13
+ :comment:
14
+
15
+ ````{python}
16
+ # Code Python : Ecrivez ci-dessous votre code Python
17
+ from __future__ import division
18
+ from pyxiscience.Mes_fctions_d_alg_lineaire_bis import *
19
+ from sympy import *
20
+ import random as rd
21
+
22
+
23
+
24
+
25
+ systems = []
26
+
27
+
28
+ n = rd.randint(2,3)
29
+ p = rd.randint(n+1,n+2)
30
+
31
+ nb_pivots = rd.randint(2, min(n, p))
32
+ pivots = tuple(rd.sample([i for i in range(1, min(n, p) + 1)], nb_pivots))
33
+ A, B = pxs_repeat_generate_sys(n = n, p = p, M = tuple(pivots), N = Matrix([rd.randint(1, 5) for _ in range(n-1)] + [0]), backup = zeros(3, 3))
34
+ if A == zeros(3, 3):
35
+ A = Matrix([[1, -5, -1, 0], [-2, 10, 1, -4], [0, 0, -1, -4]])
36
+ B = Matrix([[0, -3, -3]]).T
37
+
38
+ A, B = pxs_break_all_colinear_rows(A, B, max_iter=5)
39
+ systems.append((A, B))
40
+
41
+ B=0*B
42
+ Blat=pxsl_matrix(B)
43
+ Alat=pxsl_matrix(A)
44
+
45
+ augmentmat=pxsl_double_matrix(A,B,opt='ext')
46
+
47
+ sys = pxsl_system_lin(A, B)
48
+
49
+ soluce=pxs_gauss_jordan(A.copy(), B.copy(), method = "mat", view = "ext")
50
+
51
+ Ared=pxsl_double_matrix(soluce["A"],soluce["B"],opt='ext')
52
+
53
+
54
+ ````
55
+
56
+ :::::{question}
57
+ :questionType: MCQ
58
+ :questionId: 1
59
+ :questionIndex: 1
60
+
61
+ ::::{questionStatement}
62
+ {en}`Which of the following best describes the solution set of the equation`{fr}`Laquelle des propositions suivantes décrit le mieux l'ensemble des solutions de l'équation` $Ax=0$ {en}`where`{fr}`où`
63
+ \begin{equation*}
64
+ A={{Alat}}?
65
+ \end{equation*}
66
+ ::::
67
+
68
+ ::::{questionHint}
69
+
70
+ ::::
71
+
72
+ ::::{mcqAnswer}
73
+ :isRightAnswer: true
74
+ {en}`An infinite set of vectors forming a subspace through the origin (the system has infinitely many solutions)`{fr}`Un ensemble infini de vecteurs formant un sous-espace vectoriel passant par l'origine (le système a une infinité de solutions)`
75
+ ::::
76
+
77
+ ::::{mcqAnswer}
78
+ :isRightAnswer: false
79
+ {en}`Only the trivial solution $x=0$`{fr}`Uniquement la solution triviale $x=0$`
80
+ ::::
81
+
82
+ ::::{mcqAnswer}
83
+ :isRightAnswer: false
84
+ {en}`The empty set (the system has no solution)`{fr}`L'ensemble vide (le système n'a aucune solution)`
85
+ ::::
86
+
87
+ ::::{mcqAnswer}
88
+ :isRightAnswer: false
89
+ {en}`A single nonzero vector`{fr}`Un unique vecteur non nul`
90
+ ::::
91
+
92
+ ::::{mcqAnswer}
93
+ :isRightAnswer: false
94
+ {fr}`Aucune de ces réponses n'est correcte` {en}`None of these answers are correct`
95
+ ::::
96
+
97
+ ::::{detailedSolution}
98
+ {en}`The equation `{fr}`L'équation ` $Ax=0$ {en}`may be written with an augmented matrix as `{fr}`peut s'écrire sous la forme de matrice augmentée comme `
99
+ \begin{equation*}
100
+ {{augmentmat}}
101
+ \end{equation*}
102
+ {en}`The Row Reduction Echelon Form of this matrix is`{fr}`La forme échelonnée réduite de cette matrice est `
103
+ \begin{equation*}
104
+ {{Ared}}
105
+ \end{equation*}
106
+
107
+ :::{dropdown} {en}`See more details`{fr}`Voir plus de détails`
108
+ {en}`We may apply the Row Reduction Algorithm to reduce the augmented matrix: `{fr}`Nous pouvons appliquer l'algorithme de Gauss pour réduire la matrice augmentée :`
109
+ {{soluce["resol"]}}
110
+ :::
111
+
112
+ {en}`which is equivalent to the system `{fr}`ce qui est équivalent au système`
113
+ \begin{equation*}
114
+ {{soluce["sys"]}}.
115
+ \end{equation*}
116
+
117
+ {en}`The set of solutions of the equation `{fr}`L'ensemble des solution de l'équation ` $Ax=0$ {en}` is then `{fr}` est donc `
118
+ \begin{equation*}
119
+ \boxed{
120
+ {{soluce["span"]}}.
121
+
122
+ }
123
+ \end{equation*}
124
+ ::::
125
+
126
+ ::::{weightDistribution}
127
+ :reasoning: 25
128
+ :logic: 25
129
+ :abstraction: 25
130
+ :calculation: 25
131
+ ::::
132
+ :::::
133
+ `````
app/knowledge/fewshots/declinaisons/{en}`The set of solutions of an inhomogeneous system`{fr}`Ensemble des solutions d'un système inhomogène` - MCQ.md ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :id: f48b19b6-6fb1-11f1-a8a1-0ed8d3b012a9
3
+ :originalExerciseId: d8e6061b-52be-424b-8dc6-45548d0c00ce
4
+ :title: {en}`The set of solutions of an inhomogeneous system`{fr}`Ensemble des solutions d'un système inhomogène` - MCQ
5
+ :modules:
6
+ :recommendedExecutionTime: 30
7
+ :level: Elementary
8
+ :chap: section_2_4_linalg_Trush
9
+ :involvedConcepts:
10
+ :originalSource: 7.1 Ronan
11
+ :visibility: All
12
+ :variations:
13
+ :comment:
14
+
15
+ ````{python}
16
+ # Code Python : Ecrivez ci-dessous votre code Python
17
+ from __future__ import division
18
+ from pyxiscience.Mes_fctions_d_alg_lineaire_bis import *
19
+ from sympy import *
20
+ import random as rd
21
+
22
+
23
+
24
+
25
+ systems = []
26
+
27
+
28
+ n = rd.randint(2,3)
29
+ p = rd.randint(n+1,n+2)
30
+
31
+ nb_pivots = rd.randint(2, min(n, p))
32
+ pivots = tuple(rd.sample([i for i in range(1, min(n, p) + 1)], nb_pivots))
33
+ A, B = pxs_repeat_generate_sys(n = n, p = p, M = tuple(pivots), N = Matrix([rd.randint(1, 5) for _ in range(n-1)] + [0]), backup = zeros(3, 3))
34
+ if A == zeros(3, 3):
35
+ A = Matrix([[1, -5, -1, 0], [-2, 10, 1, -4], [0, 0, -1, -4]])
36
+ B = Matrix([[0, -3, -3]]).T
37
+
38
+ A, B = pxs_break_all_colinear_rows(A, B, max_iter=5)
39
+ systems.append((A, B))
40
+
41
+ Blat=pxsl_matrix(B)
42
+ Alat=pxsl_matrix(A)
43
+
44
+ augmentmat=pxsl_double_matrix(A,B,opt='ext')
45
+
46
+ sys = pxsl_system_lin(A, B)
47
+
48
+ soluce=pxs_gauss_jordan(A.copy(), B.copy(), method = "mat", view = "ext")
49
+
50
+ Ared=pxsl_double_matrix(soluce["A"],soluce["B"],opt='ext')
51
+
52
+
53
+ ````
54
+
55
+ :::::{question}
56
+ :questionType: MCQ
57
+ :questionId: 1
58
+ :questionIndex: 1
59
+
60
+ ::::{questionStatement}
61
+ {en}`Which of the following best describes the solution set of the equation`{fr}`Laquelle des propositions suivantes décrit le mieux l'ensemble des solutions de l'équation` $Ax=b$ {en}`where`{fr}`où`
62
+ \begin{equation*}
63
+ A={{Alat}} \quad \textrm{{en}` and `{fr}` et `} \quad b={{Blat}}?
64
+ \end{equation*}
65
+ ::::
66
+
67
+ ::::{questionHint}
68
+
69
+ ::::
70
+
71
+ ::::{mcqAnswer}
72
+ :isRightAnswer: true
73
+ {en}`An infinite affine set: a particular solution plus all the solutions of $Ax=0$ (in general it does not pass through the origin)`{fr}`Un ensemble affine infini : une solution particulière à laquelle on ajoute toutes les solutions de $Ax=0$ (en général il ne passe pas par l'origine)`
74
+ ::::
75
+
76
+ ::::{mcqAnswer}
77
+ :isRightAnswer: false
78
+ {en}`A subspace through the origin`{fr}`Un sous-espace vectoriel passant par l'origine`
79
+ ::::
80
+
81
+ ::::{mcqAnswer}
82
+ :isRightAnswer: false
83
+ {en}`A single unique solution`{fr}`Une unique solution`
84
+ ::::
85
+
86
+ ::::{mcqAnswer}
87
+ :isRightAnswer: false
88
+ {en}`The empty set (the system has no solution)`{fr}`L'ensemble vide (le système n'a aucune solution)`
89
+ ::::
90
+
91
+ ::::{mcqAnswer}
92
+ :isRightAnswer: false
93
+ {fr}`Aucune de ces réponses n'est correcte` {en}`None of these answers are correct`
94
+ ::::
95
+
96
+ ::::{detailedSolution}
97
+ {en}`The equation `{fr}`L'équation ` $Ax=b$ {en}`may be written with an augmented matrix as `{fr}`peut s'écrire sous la forme de matrice augmentée comme `
98
+ \begin{equation*}
99
+ {{augmentmat}}
100
+ \end{equation*}
101
+ {en}`The Row Reduction Echelon Form of this matrix is`{fr}`La forme échelonnée réduite de cette matrice est `
102
+ \begin{equation*}
103
+ {{Ared}}
104
+ \end{equation*}
105
+
106
+ :::{dropdown} {en}`See more details`{fr}`Voir plus de détails`
107
+ {en}`We may apply the Row Reduction Algorithm to reduce the augmented matrix: `{fr}`Nous pouvons appliquer l'algorithme de Gauss pour réduire la matrice augmentée :`
108
+ {{soluce["resol"]}}
109
+ :::
110
+
111
+ {en}`which is equivalent to the system `{fr}`ce qui est équivalent au système`
112
+ \begin{equation*}
113
+ {{soluce["sys"]}}.
114
+ \end{equation*}
115
+
116
+ {en}`The set of solutions of the equation `{fr}`L'ensemble des solution de l'équation ` $Ax=b$ {en}` is then `{fr}` est donc `
117
+ \begin{equation*}
118
+ \boxed{
119
+ {{soluce["span"]}}.
120
+ }
121
+ \end{equation*}
122
+ ::::
123
+
124
+ ::::{weightDistribution}
125
+ :reasoning: 25
126
+ :logic: 25
127
+ :abstraction: 25
128
+ :calculation: 25
129
+ ::::
130
+ :::::
131
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Amortissement dégressif d'un équipement`{en}`Declining Balance Depreciation of Equipment`.md ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :title: {fr}`Amortissement dégressif d'un équipement`{en}`Declining Balance Depreciation of Equipment`
3
+ :modules:
4
+ :recommendedExecutionTime: 10
5
+ :level: Elementary
6
+ :chap: chap_expLogFunctions_exponentialFunctions_ESCP
7
+ :involvedConcepts:
8
+ :originalSource:
9
+ :visibility: All
10
+ :variations:
11
+ :comment: Appliqué (économie & gestion) : valeur résiduelle V(n)=V0·(1−d)^n d'un actif, décroissance exponentielle (base 0,8∈(0,1)) et interprétation.
12
+ :id: c70b202b-74d9-11f1-a8a1-0ed8d3b012a9
13
+ :originalExerciseId: 773a7c51-6597-11f1-a8a1-0ed8d3b012a9
14
+
15
+ ````{python}
16
+ import random as rd
17
+ from fractions import Fraction
18
+ from decimal import Decimal, ROUND_HALF_UP
19
+
20
+ def _grp(ip):
21
+ neg = ip.startswith('-'); ip = ip.lstrip('-'); g = []
22
+ while len(ip) > 3:
23
+ g.insert(0, ip[-3:]); ip = ip[:-3]
24
+ g.insert(0, ip)
25
+ return ('-' if neg else '') + '\\,'.join(g)
26
+
27
+ def money_fr(v, dec=2):
28
+ f = Fraction(v)
29
+ d = (Decimal(f.numerator) / Decimal(f.denominator)).quantize(Decimal(1).scaleb(-dec), rounding=ROUND_HALF_UP)
30
+ s = format(abs(d), 'f'); ip, _, fp = s.partition('.')
31
+ return ('-' if d < 0 else '') + _grp(ip) + (('{,}' + fp) if dec > 0 else '')
32
+
33
+ def num_fr(v, maxdec=5):
34
+ f = Fraction(v)
35
+ d = (Decimal(f.numerator) / Decimal(f.denominator)).quantize(Decimal(1).scaleb(-maxdec), rounding=ROUND_HALF_UP)
36
+ s = format(abs(d), 'f'); ip, _, fp = s.partition('.'); fp = fp.rstrip('0')
37
+ return ('-' if d < 0 else '') + _grp(ip) + (('{,}' + fp) if fp else '')
38
+
39
+ # Construction déterministe, arithmétique exacte
40
+ V0 = rd.choice([5000, 8000, 10000, 12000, 15000, 18000, 20000])
41
+ taux = rd.choice([Fraction(7, 10), Fraction(3, 4), Fraction(4, 5), Fraction(17, 20), Fraction(9, 10)])
42
+
43
+ V1val = V0 * taux
44
+ V2val = V0 * taux**2
45
+ V3val = V0 * taux**3
46
+ n5 = rd.randint(4, 7)
47
+ V5val = V0 * taux**n5
48
+ pourcentageRestant = taux**n5 * 100
49
+ pourcentagePerdu = 100 - pourcentageRestant
50
+
51
+ # Rendus
52
+ V0Aff = money_fr(V0, 0)
53
+ V1Aff = money_fr(V1val, 2)
54
+ V2Aff = money_fr(V2val, 2)
55
+ V3Aff = money_fr(V3val, 2)
56
+ V5valAff = money_fr(V5val, 2)
57
+ tauxAff = num_fr(taux, 2)
58
+ taux2Aff = num_fr(taux**2, 4)
59
+ taux3Aff = num_fr(taux**3, 5)
60
+ n5Aff = str(n5)
61
+ tauxPuissanceN5Aff = num_fr(taux**n5, 5)
62
+ pourcentageRestantAffQ3 = num_fr(pourcentageRestant, 2)
63
+ pourcentagePerduAffQ3 = num_fr(pourcentagePerdu, 2)
64
+
65
+ # === Ajouts conversion FGQ ===
66
+ def _solnum(s):
67
+ return s.replace("\\,", "").replace("{,}", ".")
68
+ V0Sol = _solnum(V0Aff)
69
+ V1Sol = _solnum(V1Aff)
70
+ V2Sol = _solnum(V2Aff)
71
+ V3Sol = _solnum(V3Aff)
72
+ V5Sol = _solnum(V5valAff)
73
+ globals()
74
+ ````
75
+
76
+ :::::{question}
77
+ :questionType: FGQ
78
+ :questionId: 0
79
+ :questionIndex: 0
80
+ :solution: [["ord","${{ V0Sol }}$","${{ V1Sol }}$","${{ V2Sol }}$","${{ V3Sol }}$"],["0","0","0","0"]]
81
+
82
+ ::::{questionStatement}
83
+ {fr}`Une entreprise acquiert un serveur pour`{en}`A company acquires a server for` ${{ V0Aff }}$ €. {fr}`Sa valeur résiduelle après`{en}`Its residual value after` $n$ {fr}`années est`{en}`years is` $V(n)={{ V0Aff }}\times({{ tauxAff }})^{n}$. {fr}`Calculer`{en}`Calculate` $V(0)$, $V(1)$, $V(2)$ {fr}`et`{en}`and` $V(3)$ {fr}`(valeurs exactes).`{en}`(exact values).`
84
+
85
+ $V(0) =$ {input}`||80` $\quad V(1) =$ {input}`||80` $\quad V(2) =$ {input}`||80` $\quad V(3) =$ {input}`||80`
86
+ ::::
87
+
88
+ ::::{questionHint}
89
+ {fr}`Appliquer la formule ; on a`{en}`Apply the formula; we have` $({{ tauxAff }})^{2}={{ taux2Aff }}$ {fr}`et`{en}`and` $({{ tauxAff }})^{3}={{ taux3Aff }}$.
90
+ ::::
91
+
92
+ ::::{displayedSolution}
93
+ $V(0) = {{ V0Aff }}$ € $\quad V(1) = {{ V1Aff }}$ € $\quad V(2) = {{ V2Aff }}$ € $\quad V(3) = {{ V3Aff }}$ €
94
+ ::::
95
+
96
+ ::::{detailedSolution}
97
+ {fr}`On applique la formule en augmentant`{en}`We apply the formula by increasing` $n$. {fr}`La base élevée à la puissance`{en}`The base raised to the power` $0$ {fr}`vaut`{en}`equals` $1$, {fr}`donc`{en}`so` $V(0)={{ V0Aff }}$. {fr}`Ensuite :`{en}`Then:`
98
+
99
+ $V(1)={{ V0Aff }}\times{{ tauxAff }}={{ V1Aff }}$, $\quad V(2)={{ V0Aff }}\times{{ taux2Aff }}={{ V2Aff }}$, $\quad V(3)={{ V0Aff }}\times{{ taux3Aff }}={{ V3Aff }}$.
100
+ ::::
101
+
102
+ ::::{weightDistribution}
103
+ :logic: 10
104
+ :abstraction: 20
105
+ :reasoning: 25
106
+ :calculation: 45
107
+ ::::
108
+ :::::
109
+
110
+ :::::{question}
111
+ :questionType: MCQ
112
+ :questionId: 1
113
+ :questionIndex: 1
114
+
115
+ ::::{questionStatement}
116
+ {fr}`Déterminer si`{en}`Determine whether` $V$ {fr}`est croissante ou décroissante, et justifier à partir de la base.`{en}`is increasing or decreasing, and justify from the base.`
117
+ ::::
118
+
119
+ ::::{questionHint}
120
+ {fr}`La base est`{en}`The base is` ${{ tauxAff }}\in(0,1)$.
121
+ ::::
122
+
123
+ ::::{mcqAnswer}
124
+ :isRightAnswer: true
125
+ {fr}`Strictement décroissante (base dans $(0,1)$)`{en}`Strictly decreasing (base in $(0,1)$)`
126
+ ::::
127
+
128
+ ::::{mcqAnswer}
129
+ :isRightAnswer: false
130
+ {fr}`Strictement croissante (base $> 1$)`{en}`Strictly increasing (base $> 1$)`
131
+ ::::
132
+
133
+ ::::{mcqAnswer}
134
+ :isRightAnswer: false
135
+ {fr}`Constante`{en}`Constant`
136
+ ::::
137
+
138
+ ::::{detailedSolution}
139
+ {fr}`La base est`{en}`The base is` $b={{ tauxAff }}\in(0,1)$, {fr}`donc`{en}`so` $V$ {fr}`est strictement décroissante : la valeur du serveur diminue chaque année. On vérifie :`{en}`is strictly decreasing: the server's value decreases each year. We verify:` ${{ V0Aff }}>{{ V1Aff }}>{{ V2Aff }}>{{ V3Aff }}$.
140
+ ::::
141
+
142
+ ::::{weightDistribution}
143
+ :logic: 25
144
+ :abstraction: 35
145
+ :reasoning: 30
146
+ :calculation: 10
147
+ ::::
148
+ :::::
149
+
150
+ :::::{question}
151
+ :questionType: FGQ
152
+ :questionId: 2
153
+ :questionIndex: 2
154
+ :solution: [["ord","${{ V5Sol }}$"],["0"]]
155
+
156
+ ::::{questionStatement}
157
+ {fr}`Calculer`{en}`Calculate` $V({{ n5Aff }})$ {fr}`(on donne`{en}`(given` $({{ tauxAff }})^{ {{ n5Aff }} }={{ tauxPuissanceN5Aff }}$), {fr}`puis interpréter le résultat dans le contexte de l'entreprise.`{en}`then interpret the result in the company's context.`
158
+
159
+ $V({{ n5Aff }}) =$ {input}`||80`
160
+ ::::
161
+
162
+ ::::{questionHint}
163
+ {fr}`Multiplier`{en}`Multiply` ${{ V0Aff }}$ {fr}`par la valeur fournie, puis comparer à la valeur d'achat.`{en}`by the given value, then compare to the purchase value.`
164
+ ::::
165
+
166
+ ::::{displayedSolution}
167
+ $V({{ n5Aff }}) = {{ V5valAff }}$ €
168
+ ::::
169
+
170
+ ::::{detailedSolution}
171
+ {fr}`On évalue`{en}`We evaluate` $V$ {fr}`en`{en}`at` $n={{ n5Aff }}$ :
172
+
173
+ \begin{equation*}
174
+ V({{ n5Aff }}) &= {{ V0Aff }} \times {{ tauxPuissanceN5Aff }} \\
175
+ &= {{ V5valAff }}.
176
+ \end{equation*}
177
+
178
+ {fr}`Après`{en}`After` ${{ n5Aff }}$ {fr}`ans, le serveur ne vaut plus que`{en}`years, the server is only worth` ${{ V5valAff }}$ €, {fr}`soit environ`{en}`which is approximately` ${{ pourcentageRestantAffQ3 }}\,\%$ {fr}`de sa valeur d'achat : l'entreprise a perdu environ`{en}`of its purchase value: the company has lost approximately` ${{ pourcentagePerduAffQ3 }}\,\%$ {fr}`de la valeur de cet actif en`{en}`of the value of this asset in` {{ n5Aff }} {fr}`ans.`{en}`years.`
179
+ ::::
180
+
181
+ ::::{weightDistribution}
182
+ :logic: 20
183
+ :abstraction: 25
184
+ :reasoning: 35
185
+ :calculation: 20
186
+ ::::
187
+ :::::
188
+
189
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Capitalisation annuelle contre capitalisation continue`{en}`Annual Compounding versus Continuous Compounding`.md ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :title: {fr}`Capitalisation annuelle contre capitalisation continue`{en}`Annual Compounding versus Continuous Compounding`
3
+ :modules:
4
+ :recommendedExecutionTime: 12
5
+ :level: Elementary
6
+ :chap: chap_expLogFunctions_exponentialFunctions_ESCP
7
+ :involvedConcepts:
8
+ :originalSource:
9
+ :visibility: All
10
+ :variations:
11
+ :comment: Appliqué (économie & gestion) : comparaison d'un placement en capitalisation annuelle S=P(1+r)^n et en capitalisation continue S=P·e^(rn), évaluations numériques (e fourni) et interprétation de l'écart.
12
+ :id: daf335a0-74d9-11f1-a8a1-0ed8d3b012a9
13
+ :originalExerciseId: 5c4c10be-6598-11f1-a8a1-0ed8d3b012a9
14
+
15
+ ````{python}
16
+ import random as rd
17
+ import math
18
+ from fractions import Fraction
19
+ from decimal import Decimal, ROUND_HALF_UP
20
+
21
+ def _grp(ip):
22
+ neg = ip.startswith('-'); ip = ip.lstrip('-'); g = []
23
+ while len(ip) > 3:
24
+ g.insert(0, ip[-3:]); ip = ip[:-3]
25
+ g.insert(0, ip)
26
+ return ('-' if neg else '') + '\\,'.join(g)
27
+
28
+ def money_fr(v, dec=2):
29
+ f = Fraction(v)
30
+ d = (Decimal(f.numerator) / Decimal(f.denominator)).quantize(Decimal(1).scaleb(-dec), rounding=ROUND_HALF_UP)
31
+ s = format(abs(d), 'f'); ip, _, fp = s.partition('.')
32
+ return ('-' if d < 0 else '') + _grp(ip) + (('{,}' + fp) if dec > 0 else '')
33
+
34
+ def num_fr(v, maxdec=4):
35
+ f = Fraction(v)
36
+ d = (Decimal(f.numerator) / Decimal(f.denominator)).quantize(Decimal(1).scaleb(-maxdec), rounding=ROUND_HALF_UP)
37
+ s = format(abs(d), 'f'); ip, _, fp = s.partition('.'); fp = fp.rstrip('0')
38
+ return ('-' if d < 0 else '') + _grp(ip) + (('{,}' + fp) if fp else '')
39
+
40
+ # Paramètres financiers (déterministe). Annuel exact ; continu approché (e).
41
+ P = rd.choice([5000, 10000, 15000, 20000, 25000, 30000, 40000, 50000])
42
+ n = rd.randint(3, 10)
43
+ rPct = rd.choice([Fraction(2), Fraction(5, 2), Fraction(3), Fraction(7, 2), Fraction(4),
44
+ Fraction(9, 2), Fraction(5), Fraction(11, 2), Fraction(6), Fraction(13, 2),
45
+ Fraction(7), Fraction(15, 2), Fraction(8)])
46
+ r = rPct / 100
47
+
48
+ # Placement A : capitalisation annuelle (exact)
49
+ baseComp = 1 + r
50
+ puissance = baseComp ** n
51
+ sA = P * puissance
52
+
53
+ # Placement B : capitalisation continue (e^(r n), approché)
54
+ exposant = r * n
55
+ expVal = Fraction(math.exp(float(exposant)))
56
+ sB = P * expVal
57
+ ecart = sB - sA
58
+
59
+ # Rendus
60
+ pAff = money_fr(P, 0)
61
+ nAff = str(n)
62
+ rPctStr = num_fr(rPct, 1)
63
+ baseCompStr = num_fr(baseComp, 4)
64
+ puissanceApproxStr = num_fr(puissance, 4)
65
+ saAff = money_fr(sA, 2)
66
+ exposantContStr = num_fr(exposant, 2)
67
+ expApproxStr = num_fr(expVal, 4)
68
+ sbAff = money_fr(sB, 2)
69
+ ecartAff = money_fr(ecart, 2)
70
+
71
+ # === Ajouts conversion FGQ ===
72
+ def _solnum(s):
73
+ return s.replace("\\,", "").replace("{,}", ".")
74
+ saSol = _solnum(saAff)
75
+ sbSol = _solnum(sbAff)
76
+ ecartSol = _solnum(ecartAff)
77
+ globals()
78
+ ````
79
+
80
+ :::::{question}
81
+ :questionType: FGQ
82
+ :questionId: 0
83
+ :questionIndex: 0
84
+ :solution: [["ord","${{ saSol }}$"],["0"]]
85
+
86
+ ::::{questionStatement}
87
+ {fr}`Un investisseur place`{en}`An investor invests` ${{ pAff }}$ € {fr}`sur`{en}`over` ${{ nAff }}$ {fr}`ans au taux de`{en}`years at a rate of` ${{ rPctStr }}\,\%$ {fr}`par an. Placement A (capitalisation annuelle) :`{en}`per year. Investment A (annual compounding):` $S_{A}={{ pAff }}\times({{ baseCompStr }})^{ {{ nAff }} }$. {fr}`Calculer`{en}`Calculate` $S_{A}$ {fr}`(arrondir au centime). On donne`{en}`(round to the nearest cent). We are given` $({{ baseCompStr }})^{ {{ nAff }} }\approx {{ puissanceApproxStr }}$.
88
+
89
+ $S_{A} =$ {input}`||80`
90
+ ::::
91
+
92
+ ::::{questionHint}
93
+ {fr}`Multiplier`{en}`Multiply` ${{ pAff }}$ {fr}`par la valeur fournie.`{en}`by the given value.`
94
+ ::::
95
+
96
+ ::::{displayedSolution}
97
+ $S_{A} = {{ saAff }}$ €
98
+ ::::
99
+
100
+ ::::{detailedSolution}
101
+ {fr}`On applique la formule de capitalisation annuelle :`{en}`We apply the annual compounding formula:`
102
+
103
+ \begin{equation*}
104
+ S_{A} &= {{ pAff }} \times ({{ baseCompStr }})^{ {{ nAff }} } \approx {{ pAff }} \times {{ puissanceApproxStr }} \\
105
+ &= {{ saAff }}.
106
+ \end{equation*}
107
+ ::::
108
+
109
+ ::::{weightDistribution}
110
+ :logic: 10
111
+ :abstraction: 15
112
+ :reasoning: 25
113
+ :calculation: 50
114
+ ::::
115
+ :::::
116
+
117
+ :::::{question}
118
+ :questionType: FGQ
119
+ :questionId: 1
120
+ :questionIndex: 1
121
+ :solution: [["ord","${{ sbSol }}$"],["0"]]
122
+
123
+ ::::{questionStatement}
124
+ {fr}`Placement B (capitalisation continue) :`{en}`Investment B (continuous compounding):` $S_{B}={{ pAff }}\times e^{r\times {{ nAff }}}$ {fr}`où`{en}`where` $r=\dfrac{ {{ rPctStr }} }{100}$, {fr}`avec`{en}`with` $e\approx 2{,}718$. {fr}`Calculer`{en}`Calculate` $S_{B}$ {fr}`(arrondir au centime). On donne`{en}`(round to the nearest cent). We are given` $e^{ {{ exposantContStr }} }\approx {{ expApproxStr }}$.
125
+
126
+ $S_{B} =$ {input}`||80`
127
+ ::::
128
+
129
+ ::::{questionHint}
130
+ {fr}`L'exposant vaut`{en}`The exponent equals` $r\times {{ nAff }}=\dfrac{ {{ rPctStr }} }{100}\times {{ nAff }}={{ exposantContStr }}$ ; {fr}`utiliser`{en}`use` $e^{ {{ exposantContStr }} }\approx {{ expApproxStr }}$.
131
+ ::::
132
+
133
+ ::::{displayedSolution}
134
+ $S_{B} = {{ sbAff }}$ €
135
+ ::::
136
+
137
+ ::::{detailedSolution}
138
+ {fr}`L'exposant vaut`{en}`The exponent equals` $r\times {{ nAff }}=\dfrac{ {{ rPctStr }} }{100}\times {{ nAff }}={{ exposantContStr }}$, {fr}`donc :`{en}`so:`
139
+
140
+ \begin{equation*}
141
+ S_{B} &= {{ pAff }} \times e^{ {{ exposantContStr }} } \approx {{ pAff }} \times {{ expApproxStr }} \\
142
+ &= {{ sbAff }}.
143
+ \end{equation*}
144
+ ::::
145
+
146
+ ::::{weightDistribution}
147
+ :logic: 10
148
+ :abstraction: 20
149
+ :reasoning: 25
150
+ :calculation: 45
151
+ ::::
152
+ :::::
153
+
154
+ :::::{question}
155
+ :questionType: FGQ
156
+ :questionId: 2
157
+ :questionIndex: 2
158
+ :solution: [["ord","${{ ecartSol }}$"],["0"]]
159
+
160
+ ::::{questionStatement}
161
+ {fr}`Comparer`{en}`Compare` $S_{A}$ {fr}`et`{en}`and` $S_{B}$, {fr}`et interpréter l'écart dans le contexte financier.`{en}`and interpret the difference in the financial context.`
162
+
163
+ $S_{B} - S_{A} =$ {input}`||80`
164
+ ::::
165
+
166
+ ::::{questionHint}
167
+ {fr}`Calculer`{en}`Calculate` $S_{B}-S_{A}$.
168
+ ::::
169
+
170
+ ::::{displayedSolution}
171
+ $S_{B} - S_{A} = {{ ecartAff }}$ €
172
+ ::::
173
+
174
+ ::::{detailedSolution}
175
+ {fr}`On compare les deux montants :`{en}`We compare the two amounts:`
176
+
177
+ \begin{equation*}
178
+ S_{B} - S_{A} &\approx {{ sbAff }} - {{ saAff }} \\
179
+ &= {{ ecartAff }}.
180
+ \end{equation*}
181
+
182
+ {fr}`À taux affiché identique (`{en}`At the same stated rate (` ${{ rPctStr }}\,\%$ ), {fr}`la capitalisation continue`{en}`continuous compounding` $S_{B}\approx {{ sbAff }}$ € {fr}`rapporte un peu plus que la capitalisation annuelle`{en}`yields slightly more than annual compounding` $S_{A}\approx {{ saAff }}$ €, {fr}`l'écart étant d'environ`{en}`the difference being approximately` ${{ ecartAff }}$ €. {fr}`La capitalisation continue réinvestit les intérêts à chaque instant, alors que la capitalisation annuelle ne les réinvestit qu'une fois par an.`{en}`Continuous compounding reinvests interest at every instant, whereas annual compounding reinvests it only once per year.`
183
+ ::::
184
+
185
+ ::::{weightDistribution}
186
+ :logic: 20
187
+ :abstraction: 25
188
+ :reasoning: 35
189
+ :calculation: 20
190
+ ::::
191
+ :::::
192
+
193
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Contrainte de budget publicitaire`{en}`Advertising budget constraint`.md ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :originalExerciseId: df9c98f5-633f-11f1-a8a1-0ed8d3b012a9
3
+ :title: {fr}`Contrainte de budget publicitaire`{en}`Advertising budget constraint`
4
+ :modules:
5
+ :recommendedExecutionTime: 10
6
+ :level: Elementary
7
+ :chap:
8
+ :involvedConcepts: Solving_inequalities, Sets
9
+ :originalSource:
10
+ :visibility: All
11
+ :variations:
12
+ :comment: Version QCM — contrainte de budget, nombre maximal d'insertions et ensemble admissible.
13
+ :id: 0f2a4c67-6e48-11f1-a8a1-0ed8d3b012a9
14
+
15
+ ````{python}
16
+ import random as rd
17
+ from pyxiscience.Mes_fctions_generalistes_bis import pxs_config, pxsl_format_number
18
+ from pyxiscience.Classes_Extensions import pxs_Interval
19
+
20
+ config_standard = pxs_config()
21
+
22
+ # Contrainte : budget multiple du coût d'insertion, nMax entier >= 5
23
+ for _ in range(2000):
24
+ cout_insertion = rd.choice([40, 50, 60, 80, 100, 120, 150])
25
+ budget = cout_insertion * rd.randint(10, 50)
26
+ nMax = budget // cout_insertion
27
+ if nMax >= 5 and 800 <= budget <= 3000:
28
+ break
29
+
30
+ verif_max = cout_insertion * nMax
31
+ verif_exclu = cout_insertion * (nMax + 1)
32
+
33
+ # Rendus
34
+ budgetAff = pxsl_format_number(budget)
35
+ coutInsertion = cout_insertion
36
+ nMaxPlusUn = nMax + 1
37
+ verifMaxAff = pxsl_format_number(verif_max)
38
+ verifExcluAff = pxsl_format_number(verif_exclu)
39
+
40
+ # === Distracteurs MCQ (ajouts ; le code ci-dessus est inchangé) ===
41
+ interCorrect = pxs_Interval(0, nMax, False, False).print()
42
+ interD1 = pxs_Interval(0, budget, True, False).print()
43
+ interD2 = pxs_Interval(0, nMax, True, False).print()
44
+ interD3 = pxs_Interval(0, budget, False, False).print()
45
+
46
+ globals()
47
+ ````
48
+
49
+ :::::{question}
50
+ :questionType: MCQ
51
+
52
+ ::::{questionStatement}
53
+ {fr}`Une start-up dispose d'un budget de communication de`{en}`A start-up has a communication budget of` ${{ budgetAff }}$ €. {fr}`Chaque insertion publicitaire coûte`{en}`Each advertising insertion costs` ${{ coutInsertion }}$ €. {fr}`On note`{en}`Let` $n$ {fr}`le nombre d'insertions. Quel est l'ensemble des valeurs admissibles de`{en}`be the number of insertions. What is the set of admissible values of` $n$ {fr}`en notation intervalle ?`{en}`in interval notation?`
54
+ ::::
55
+
56
+ ::::{questionHint}
57
+ {fr}`Traduire la contrainte de budget par`{en}`Translate the budget constraint as` ${{ coutInsertion }}n\leq {{ budgetAff }}$, {fr}`puis diviser par`{en}`then divide by` ${{ coutInsertion }}>0$ {fr}`(sens conservé). Ne pas oublier`{en}`(inequality sign preserved). Do not forget` $n\geq 0$.
58
+ ::::
59
+
60
+ ::::{mcqAnswer}
61
+ :isRightAnswer: true
62
+ $S = {{ interCorrect }}$
63
+ ::::
64
+
65
+ ::::{mcqAnswer}
66
+ :isRightAnswer: false
67
+ $S = {{ interD1 }}$
68
+ ::::
69
+
70
+ ::::{mcqAnswer}
71
+ :isRightAnswer: false
72
+ $S = {{ interD2 }}$
73
+ ::::
74
+
75
+ ::::{mcqAnswer}
76
+ :isRightAnswer: false
77
+ $S = {{ interD3 }}$
78
+ ::::
79
+
80
+ ::::{mcqAnswer}
81
+ :isRightAnswer: false
82
+ {fr}`Aucune de ces réponses n'est correcte`{en}`None of these answers are correct`
83
+ ::::
84
+
85
+ ::::{detailedSolution}
86
+ {fr}`La dépense totale`{en}`The total expenditure` ${{ coutInsertion }}n$ {fr}`ne doit pas dépasser le budget, d'où une inéquation résolue par division par un réel positif.`{en}`must not exceed the budget, hence an inequality solved by division by a positive real number.`
87
+
88
+ \begin{equation*}
89
+ {{ coutInsertion }}n \leq {{ budgetAff }}&\implies \frac{ {{ coutInsertion }}n }{ {{ coutInsertion }} } \leq \frac{ {{ budgetAff }} }{ {{ coutInsertion }} } \\[10pt]
90
+ &\implies n \leq {{ nMax }}.
91
+ \end{equation*}
92
+
93
+ {fr}`Comme`{en}`Since` $n$ {fr}`est un nombre d'insertions,`{en}`is a number of insertions,` $n\geq 0$. {fr}`L'ensemble admissible est donc :`{en}`The admissible set is therefore:`
94
+
95
+ \begin{equation*}
96
+ S = {{pxs_Interval(0, nMax, False, False).print()}}.
97
+ \end{equation*}
98
+
99
+ {fr}`La start-up peut financer au maximum`{en}`The start-up can finance at most` ${{ nMax }}$ {fr}`insertions.`{en}`insertions.`
100
+ ::::
101
+
102
+ ::::{weightDistribution}
103
+ :logic: 15
104
+ :abstraction: 20
105
+ :reasoning: 30
106
+ :calculation: 35
107
+ ::::
108
+ :::::
109
+
110
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Discontinuité d'une fraction rationnelle`{en}`Discontinuity of a Rational Fraction`.md ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :id: ff0cf9ac-7067-11f1-a8a1-0ed8d3b012a9
3
+ :originalExerciseId: dd834157-3f2f-11f1-a8a1-0ed8d3b012a9
4
+ :title: {fr}`Discontinuité d'une fraction rationnelle`{en}`Discontinuity of a Rational Fraction`
5
+ :modules: CalcI, Calc_1_Pyx
6
+ :recommendedExecutionTime: 5
7
+ :level: Intermediate
8
+ :chap: chap_limitsContinuity_continuity_ESCP, Calculus_I_Continuity_1_5
9
+ :involvedConcepts: Continuity_at_a_point, Domain_of_function, Rational_Function
10
+ :originalSource: Selin
11
+ :visibility: All
12
+ :variations:
13
+ :comment:
14
+
15
+ ```{python}
16
+ import random as rd
17
+ from sympy import *
18
+ from pyxiscience.Mes_fctions_generalistes_bis import pxs_config
19
+ from pyxiscience.Classes_Extensions import pxs_Interval
20
+ from pyxiscience.Mes_fctions_d_analyse import pxsl_pow
21
+ from pyxiscience.Mes_fctions_generalistes_bis import pxsl_latex_avec_formatage
22
+ from pyxiscience.Mes_fctions_probabilistes_bis import pxsl_res_num
23
+
24
+ from pyxiscience.Mes_fctions_generalistes_bis import pxs_config
25
+
26
+ config_standard = pxs_config()
27
+
28
+ x = symbols('x')
29
+ a,b,c,d = rd.sample(range(1, 11), 4)
30
+ p = rd.randint(2, 6)
31
+ q = Rational(1,rd.randint(2, 6))
32
+ f = 1/(a*x+b)
33
+
34
+ x0 = Rational(-b,a)
35
+
36
+ l = f.subs(x,x0)
37
+ ```
38
+
39
+ :::::{question}
40
+ :questionType: MCQ
41
+
42
+ ::::{questionStatement}
43
+ {fr}`Pour quelle raison la fonction`{en}`Why is the function` $\ds f(x) = {{latex(f)}}$ {fr}`est-elle discontinue en`{en}`discontinuous at` $\ds a = {{latex(x0)}}$ ?
44
+ ::::
45
+
46
+ ::::{questionHint}
47
+ {fr}`Évaluer le dénominateur de la fonction au point indiqué.`{en}`Evaluate the denominator of the function at the given point.`
48
+ ::::
49
+
50
+ ::::{mcqAnswer}
51
+ :isRightAnswer: true
52
+ {fr}`Car $\ds f\left({{latex(x0)}}\right)$ n'est pas définie : le dénominateur s'annule en $\ds {{latex(x0)}}$.`{en}`Because $\ds f\left({{latex(x0)}}\right)$ is undefined: the denominator vanishes at $\ds {{latex(x0)}}$.`
53
+ ::::
54
+
55
+ ::::{mcqAnswer}
56
+ :isRightAnswer: false
57
+ {fr}`Car le numérateur s'annule en $\ds {{latex(x0)}}$.`{en}`Because the numerator vanishes at $\ds {{latex(x0)}}$.`
58
+ ::::
59
+
60
+ ::::{mcqAnswer}
61
+ :isRightAnswer: false
62
+ {fr}`Car $\ds \lim_{x \to {{latex(x0)}}} f(x) = 0$.`{en}`Because $\ds \lim_{x \to {{latex(x0)}}} f(x) = 0$.`
63
+ ::::
64
+
65
+ ::::{mcqAnswer}
66
+ :isRightAnswer: false
67
+ {fr}`Car $f$ n'est pas dérivable en $\ds {{latex(x0)}}$, bien qu'elle y soit continue.`{en}`Because $f$ is not differentiable at $\ds {{latex(x0)}}$, although it is continuous there.`
68
+ ::::
69
+
70
+ ::::{detailedSolution}
71
+ {fr}`La fonction est discontinue en`{en}`The function is discontinuous at` $\ds a = {{latex(x0)}}$ {fr}`car`{en}`because` $\ds f\left({{latex(x0)}}\right)$ {fr}`n'est pas définie (le dénominateur devient zéro).`{en}`is undefined (the denominator becomes zero).`
72
+ ::::
73
+
74
+ ::::{weightDistribution}
75
+ :reasoning: 35
76
+ :logic: 25
77
+ :abstraction: 20
78
+ :calculation: 20
79
+ ::::
80
+ :::::
81
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Formule générale du seuil de rentabilité`{en}`General Break-Even Point Formula`.md ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :title: {fr}`Formule générale du seuil de rentabilité`{en}`General Break-Even Point Formula`
3
+ :modules:
4
+ :recommendedExecutionTime: 15
5
+ :level: Elementary
6
+ :chap: chap_lines_Systems_economicEquilibriumBreakeven_ESCP
7
+ :involvedConcepts:
8
+ :originalSource: Session 4 — Erwan Lamy, ESCP Business School
9
+ :visibility: All
10
+ :variations:
11
+ :comment: Synthèse (gestion) : établissement de la formule q* = yFC/(p - c), condition p > c, application numérique et analyse de sensibilité aux coûts fixes.
12
+ :id: 60088df7-74c0-11f1-a8a1-0ed8d3b012a9
13
+ :originalExerciseId: 27f935cf-64e3-11f1-a8a1-0ed8d3b012a9
14
+
15
+ ````{python}
16
+ import random as rd
17
+ # --- Helpers PyxiScience (definis inline, autonomes : aucun import externe) ---
18
+ def pxs_config():
19
+ return {}
20
+ def pxsl_format_number(n):
21
+ f = float(n)
22
+ if abs(f - round(f)) < 1e-9:
23
+ return "%d" % round(f)
24
+ s = ("%.10f" % f).rstrip("0").rstrip(".")
25
+ return s.replace(".", "{,}")
26
+ def pxsl_latex_coefficient(coef, ones=False, sign=False):
27
+ coef = int(coef)
28
+ if coef == 0:
29
+ return "+0" if sign else "0"
30
+ if not ones:
31
+ if coef == 1:
32
+ return "+" if sign else ""
33
+ if coef == -1:
34
+ return "-"
35
+ if sign:
36
+ return ("+%d" % coef) if coef > 0 else ("%d" % coef)
37
+ return "%d" % coef
38
+ lc = pxsl_latex_coefficient
39
+ config_standard = pxs_config()
40
+
41
+ # Contraintes : p > c (marge strictement positive, et p - c >= 2 pour eviter un
42
+ # denominateur d'affichage egal a 1) ; yFc et yFcNouv multiples de (p - c) pour
43
+ # des seuils entiers ; yFcNouv > yFc.
44
+ for _ in range(2000):
45
+ p = rd.randint(15, 25)
46
+ c = rd.randint(4, 10)
47
+ diff = p - c
48
+ if diff < 2:
49
+ continue
50
+ multFc = [k * diff for k in range(1, 200) if 240 <= k * diff <= 480]
51
+ multFcNouv = [k * diff for k in range(1, 200) if 480 <= k * diff <= 720]
52
+ if not (multFc and multFcNouv):
53
+ continue
54
+ yFc = rd.choice(multFc)
55
+ candNouv = [x for x in multFcNouv if x > yFc]
56
+ if not candNouv:
57
+ continue
58
+ yFcNouv = rd.choice(candNouv)
59
+ break
60
+
61
+ qStarNum = yFc // diff
62
+ qStarNouv = yFcNouv // diff
63
+ augmentation = qStarNouv - qStarNum
64
+
65
+ # --- Affichage precalcule ---
66
+ qStarAff = r"\dfrac{y_{FC}}{p - c}"
67
+ yFcAff = pxsl_format_number(yFc)
68
+ yFcNouvAff = pxsl_format_number(yFcNouv)
69
+ pMinusCAff = str(diff)
70
+ qStarNumAff = str(qStarNum)
71
+ qStarNouvAff = str(qStarNouv)
72
+ augmentationAff = str(augmentation)
73
+
74
+ globals()
75
+ ````
76
+
77
+ :::::{question}
78
+ :questionType: MCQ
79
+
80
+ ::::{questionStatement}
81
+ {fr}`Une entreprise vend un produit au prix unitaire`{en}`A company sells a product at unit price` $p${fr}`, avec un coût variable unitaire`{en}`, with a unit variable cost` $c$ {fr}`(avec`{en}`(with` $c < p${fr}`) et des coûts fixes`{en}`) and fixed costs` $y_{FC}${fr}`. Écrire le revenu total`{en}`. Write the total revenue` $y_{TR}(q)$ {fr}`et le coût total`{en}`and the total cost` $y_{TC}(q)${fr}`.`{en}`.`
82
+ ::::
83
+
84
+ ::::{questionHint}
85
+ {fr}`Reprendre les définitions générales du revenu et du coût total.`{en}`Use the general definitions of revenue and total cost.`
86
+ ::::
87
+
88
+ ::::{mcqAnswer}
89
+ :isRightAnswer: true
90
+ $y_{TR}(q) = p\,q$ {fr}`et`{en}`and` $y_{TC}(q) = c\,q + y_{FC}$
91
+ ::::
92
+
93
+ ::::{mcqAnswer}
94
+ :isRightAnswer: false
95
+ $y_{TR}(q) = p\,q + y_{FC}$ {fr}`et`{en}`and` $y_{TC}(q) = c\,q$
96
+ ::::
97
+
98
+ ::::{mcqAnswer}
99
+ :isRightAnswer: false
100
+ $y_{TR}(q) = (p-c)\,q$ {fr}`et`{en}`and` $y_{TC}(q) = c\,q + y_{FC}$
101
+ ::::
102
+
103
+ ::::{mcqAnswer}
104
+ :isRightAnswer: false
105
+ $y_{TR}(q) = p\,q$ {fr}`et`{en}`and` $y_{TC}(q) = c\,q$
106
+ ::::
107
+
108
+ ::::{mcqAnswer}
109
+ :isRightAnswer: false
110
+ {fr}`Aucune de ces réponses n'est correcte`{en}`None of these answers are correct`
111
+ ::::
112
+
113
+ ::::{detailedSolution}
114
+ {fr}`Par définition :`{en}`By definition:`
115
+
116
+ \begin{equation*}
117
+ y_{TR}(q) = p\,q, \qquad y_{TC}(q) = c\,q + y_{FC}.
118
+ \end{equation*}
119
+ ::::
120
+
121
+ ::::{weightDistribution}
122
+ :logic: 20
123
+ :abstraction: 30
124
+ :reasoning: 30
125
+ :calculation: 20
126
+ ::::
127
+ :::::
128
+
129
+ :::::{question}
130
+ :questionType: MCQ
131
+
132
+ ::::{questionStatement}
133
+ {fr}`En résolvant`{en}`By solving` $y_{TR}(q) = y_{TC}(q)${fr}`, établir la formule générale du seuil`{en}`, establish the general break-even formula` $q^{*} = \dfrac{y_{FC}}{p - c}${fr}`. Justifier pourquoi la condition`{en}`. Justify why the condition` $p > c$ {fr}`est nécessaire.`{en}`is necessary.`
134
+ ::::
135
+
136
+ ::::{questionHint}
137
+ {fr}`Regrouper les termes en`{en}`Group the terms in` $q${fr}`, factoriser par`{en}`, factor by` $q${fr}`, puis diviser par`{en}`, then divide by` $p - c${fr}`.`{en}`.`
138
+ ::::
139
+
140
+ ::::{mcqAnswer}
141
+ :isRightAnswer: true
142
+ $q^{*} = \dfrac{y_{FC}}{p-c}$, {fr}`valable si`{en}`valid if` $p > c$
143
+ ::::
144
+
145
+ ::::{mcqAnswer}
146
+ :isRightAnswer: false
147
+ $q^{*} = \dfrac{y_{FC}}{p+c}$, {fr}`valable si`{en}`valid if` $p > c$
148
+ ::::
149
+
150
+ ::::{mcqAnswer}
151
+ :isRightAnswer: false
152
+ $q^{*} = \dfrac{y_{FC}}{c-p}$, {fr}`valable si`{en}`valid if` $c > p$
153
+ ::::
154
+
155
+ ::::{mcqAnswer}
156
+ :isRightAnswer: false
157
+ $q^{*} = \dfrac{p-c}{y_{FC}}$, {fr}`valable si`{en}`valid if` $p > c$
158
+ ::::
159
+
160
+ ::::{mcqAnswer}
161
+ :isRightAnswer: false
162
+ {fr}`Aucune de ces réponses n'est correcte`{en}`None of these answers are correct`
163
+ ::::
164
+
165
+ ::::{detailedSolution}
166
+ {fr}`On résout`{en}`We solve` $y_{TR}(q) = y_{TC}(q)$ {fr}`:`{en}`:`
167
+
168
+ \begin{equation*}
169
+ p\,q = c\,q + y_{FC} \implies (p - c)\,q = y_{FC}.
170
+ \end{equation*}
171
+
172
+ {fr}`Comme`{en}`Since` $p > c${fr}`, on a`{en}`, we have` $p - c > 0${fr}`, et la division est licite :`{en}`, and the division is valid:`
173
+
174
+ \begin{equation*}
175
+ q^{*} = {{qStarAff}}.
176
+ \end{equation*}
177
+
178
+ {fr}`La condition`{en}`The condition` $p > c$ {fr}`est nécessaire : si`{en}`is necessary: if` $p \leq c${fr}`, alors`{en}`, then` $p - c \leq 0$ {fr}`et l'équation`{en}`and the equation` $(p - c)\,q = y_{FC}$ {fr}`n'a pas de solution positive (car`{en}`has no positive solution (since` $y_{FC} > 0${fr}`). L'entreprise ne pourrait jamais couvrir ses coûts fixes.`{en}`). The company could never cover its fixed costs.`
179
+ ::::
180
+
181
+ ::::{weightDistribution}
182
+ :logic: 25
183
+ :abstraction: 30
184
+ :reasoning: 35
185
+ :calculation: 10
186
+ ::::
187
+ :::::
188
+
189
+ :::::{question}
190
+ :questionType: FGQ
191
+ :solution: [["ord","${{ qStarNumAff }}$"],["0"]]
192
+
193
+ ::::{questionStatement}
194
+ {fr}`Une boutique d'accessoires vend des coques de téléphone à`{en}`An accessories shop sells phone cases at` $p = {{p}}$ {fr}`€, avec`{en}`€, with` $c = {{c}}$ {fr}`€ et`{en}`€ and` $y_{FC} = {{yFcAff}}$ {fr}`€/mois. Appliquer la formule pour trouver`{en}`€/month. Apply the formula to find` $q^{*}${fr}`.`{en}`.`\
195
+ \
196
+ $q^{*}$ = {input}`||80`
197
+ ::::
198
+
199
+ ::::{questionHint}
200
+ {fr}`Remplacer directement dans`{en}`Substitute directly into` $q^{*} = \dfrac{y_{FC}}{p - c}${fr}`.`{en}`.`
201
+ ::::
202
+
203
+ ::::{displayedSolution}
204
+ $q^{*} = {{ qStarNumAff }}$
205
+ ::::
206
+
207
+ ::::{detailedSolution}
208
+ {fr}`On applique la formule :`{en}`We apply the formula:`
209
+
210
+ \begin{equation*}
211
+ q^{*} = \dfrac{ {{yFcAff}} }{ {{p}} - {{c}} } = \dfrac{ {{yFcAff}} }{ {{pMinusCAff}} } = {{qStarNumAff}} \text{ {fr}`unités/mois`{en}`units/month`}.
212
+ \end{equation*}
213
+ ::::
214
+
215
+ ::::{weightDistribution}
216
+ :logic: 15
217
+ :abstraction: 20
218
+ :reasoning: 25
219
+ :calculation: 40
220
+ ::::
221
+ :::::
222
+
223
+ :::::{question}
224
+ :questionType: FGQ
225
+ :questionId: 3
226
+ :questionIndex: 3
227
+ :solution: [["ord","${{ augmentationAff }}$"],["0"]]
228
+
229
+ ::::{questionStatement}
230
+ {fr}`Si les coûts fixes passent à`{en}`If the fixed costs increase to` $y_{FC} = {{yFcNouvAff}}$ {fr}`€ (nouveau bail), de combien d'unités le seuil augmente-t-il ? Interpréter le résultat.`{en}`€ (new lease), by how many units does the break-even point increase? Interpret the result.`\
231
+ \
232
+ {fr}`Augmentation du seuil (unités/mois) :`{en}`Increase in break-even (units/month):` {input}`||80`.
233
+ ::::
234
+
235
+ ::::{questionHint}
236
+ {fr}`Recalculer`{en}`Recalculate` $q^{*}$ {fr}`avec le nouveau`{en}`with the new` $y_{FC}${fr}`, puis faire la différence.`{en}`, then compute the difference.`
237
+ ::::
238
+
239
+ ::::{displayedSolution}
240
+ {fr}`Augmentation :`{en}`Increase:` ${{ augmentationAff }}$
241
+ ::::
242
+
243
+ ::::{detailedSolution}
244
+ {fr}`Avec`{en}`With` $y_{FC} = {{yFcNouvAff}}$ {fr}`€ :`{en}`€:`
245
+
246
+ \begin{equation*}
247
+ q^{*} = \dfrac{ {{yFcNouvAff}} }{ {{pMinusCAff}} } = {{qStarNouvAff}} \text{ {fr}`unités/mois`{en}`units/month`}.
248
+ \end{equation*}
249
+
250
+ {fr}`Le seuil augmente de`{en}`The break-even point increases by` ${{qStarNouvAff}} - {{qStarNumAff}} = {{augmentationAff}}$ {fr}`unités : la boutique doit vendre`{en}`units: the shop must sell` {{augmentationAff}} {fr}`coques de plus chaque mois uniquement pour absorber le loyer plus élevé, sans gagner de profit supplémentaire.`{en}`more cases each month just to absorb the higher rent, without earning any additional profit.`
251
+ ::::
252
+
253
+ ::::{weightDistribution}
254
+ :logic: 20
255
+ :abstraction: 25
256
+ :reasoning: 30
257
+ :calculation: 25
258
+ ::::
259
+ :::::
260
+
261
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Intérêts composés _ calcul d'un capital futur`{en}`Compound Interest_ Future Capital Calculation`.md ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :title: {fr}`Intérêts composés : calcul d'un capital futur`{en}`Compound Interest: Future Capital Calculation`
3
+ :modules:
4
+ :recommendedExecutionTime: 10
5
+ :level: Elementary
6
+ :chap: chap_expLogFunctions_exponentialFunctions_ESCP
7
+ :involvedConcepts:
8
+ :originalSource:
9
+ :visibility: All
10
+ :variations:
11
+ :comment: Appliqué (économie & gestion) : capitalisation annuelle S(n)=P(1+r)^n, évaluation à plusieurs dates et monotonie (base 1,03>1).
12
+ :id: 9378268f-74d9-11f1-a8a1-0ed8d3b012a9
13
+ :originalExerciseId: 7265a5b6-6597-11f1-a8a1-0ed8d3b012a9
14
+
15
+ ````{python}
16
+ import random as rd
17
+ from fractions import Fraction
18
+ from decimal import Decimal, ROUND_HALF_UP
19
+
20
+ def _grp(ip):
21
+ neg = ip.startswith('-'); ip = ip.lstrip('-'); g = []
22
+ while len(ip) > 3:
23
+ g.insert(0, ip[-3:]); ip = ip[:-3]
24
+ g.insert(0, ip)
25
+ return ('-' if neg else '') + '\\,'.join(g)
26
+
27
+ def money_fr(v, dec=2):
28
+ f = Fraction(v)
29
+ d = (Decimal(f.numerator) / Decimal(f.denominator)).quantize(Decimal(1).scaleb(-dec), rounding=ROUND_HALF_UP)
30
+ s = format(abs(d), 'f'); ip, _, fp = s.partition('.')
31
+ return ('-' if d < 0 else '') + _grp(ip) + (('{,}' + fp) if dec > 0 else '')
32
+
33
+ def num_fr(v, maxdec=4):
34
+ f = Fraction(v)
35
+ d = (Decimal(f.numerator) / Decimal(f.denominator)).quantize(Decimal(1).scaleb(-maxdec), rounding=ROUND_HALF_UP)
36
+ s = format(abs(d), 'f'); ip, _, fp = s.partition('.'); fp = fp.rstrip('0')
37
+ return ('-' if d < 0 else '') + _grp(ip) + (('{,}' + fp) if fp else '')
38
+
39
+ # Construction déterministe, arithmétique exacte (Fraction)
40
+ P = rd.choice([3000, 4000, 5000, 6000, 7000, 8000, 10000])
41
+ rPourcent = rd.choice([Fraction(2), Fraction(5, 2), Fraction(3), Fraction(7, 2),
42
+ Fraction(4), Fraction(9, 2), Fraction(5)])
43
+ base = 1 + rPourcent / 100
44
+ nLong = rd.randint(8, 15)
45
+
46
+ S0 = Fraction(P)
47
+ S1 = P * base
48
+ S2 = P * base**2
49
+ SNLong = P * base**nLong
50
+
51
+ # Rendus
52
+ PAff = money_fr(P, 0)
53
+ rPourcentAff = num_fr(rPourcent, 2)
54
+ baseAff = num_fr(base, 4)
55
+ baseMonoAff = num_fr(base, 2)
56
+ nLongAff = str(nLong)
57
+ baseCarreAff = num_fr(base**2, 4)
58
+ basePuissNLongAff = num_fr(base**nLong, 4)
59
+ s0Aff = money_fr(S0, 2)
60
+ s1Aff = money_fr(S1, 2)
61
+ s2Aff = money_fr(S2, 2)
62
+ sNLongAff = money_fr(SNLong, 2)
63
+
64
+ # === Ajouts conversion FGQ ===
65
+ def _solnum(s):
66
+ return s.replace("\\,", "").replace("{,}", ".")
67
+ PSol = _solnum(PAff)
68
+ s1Sol = _solnum(s1Aff)
69
+ s2Sol = _solnum(s2Aff)
70
+ sNLongSol = _solnum(sNLongAff)
71
+ globals()
72
+ ````
73
+
74
+ :::::{question}
75
+ :questionType: FGQ
76
+ :questionId: 0
77
+ :questionIndex: 0
78
+ :solution: [["ord","${{ PSol }}$","${{ s1Sol }}$","${{ s2Sol }}$"],["0","0","0"]]
79
+
80
+ ::::{questionStatement}
81
+ {fr}`Un épargnant place`{en}`A saver invests` ${{ PAff }}$ € {fr}`à un taux annuel de`{en}`at an annual rate of` ${{ rPourcentAff }}\,\%$, {fr}`capitalisation annuelle :`{en}`annual compounding:` $S(n)={{ PAff }}\times({{ baseAff }})^{n}$. {fr}`Calculer`{en}`Calculate` $S(0)$, $S(1)$ {fr}`et`{en}`and` $S(2)$, {fr}`puis interpréter`{en}`then interpret` $S(0)$.
82
+
83
+ $S(0) =$ {input}`||80` $\qquad S(1) =$ {input}`||80` $\qquad S(2) =$ {input}`||80`
84
+ ::::
85
+
86
+ ::::{questionHint}
87
+ {fr}`Appliquer la formule avec`{en}`Apply the formula with` $n=0,1,2$ ; {fr}`rappeler que toute base élevée à la puissance`{en}`recall that any base raised to the power` $0$ {fr}`vaut`{en}`equals` $1$.
88
+ ::::
89
+
90
+ ::::{displayedSolution}
91
+ $S(0) = {{ PAff }}$ € $\qquad S(1) = {{ s1Aff }}$ € $\qquad S(2) = {{ s2Aff }}$ €
92
+ ::::
93
+
94
+ ::::{detailedSolution}
95
+ {fr}`On applique la formule. Pour`{en}`We apply the formula. For` $n=0$, {fr}`la base élevée à la puissance`{en}`the base raised to the power` $0$ {fr}`vaut`{en}`equals` $1$, {fr}`donc`{en}`so` $S(0)={{ PAff }}$.
96
+
97
+ \begin{equation*}
98
+ S(1) &= {{ PAff }} \times {{ baseAff }} \\
99
+ &= {{ s1Aff }}.
100
+ \end{equation*}
101
+
102
+ \begin{equation*}
103
+ S(2) &= {{ PAff }} \times ({{ baseAff }})^{2} \\
104
+ &= {{ PAff }} \times {{ baseCarreAff }} \\
105
+ &= {{ s2Aff }}.
106
+ \end{equation*}
107
+
108
+ $S(0)={{ PAff }}$ € {fr}`est le capital initial : aucun intérêt n'a encore été versé.`{en}`is the initial capital: no interest has been paid yet.`
109
+ ::::
110
+
111
+ ::::{weightDistribution}
112
+ :logic: 10
113
+ :abstraction: 20
114
+ :reasoning: 25
115
+ :calculation: 45
116
+ ::::
117
+ :::::
118
+
119
+ :::::{question}
120
+ :questionType: FGQ
121
+ :questionId: 1
122
+ :questionIndex: 1
123
+ :solution: [["ord","${{ sNLongSol }}$"],["0"]]
124
+
125
+ ::::{questionStatement}
126
+ {fr}`Calculer`{en}`Calculate` $S({{ nLongAff }})$. {fr}`On donne`{en}`We are given` $({{ baseAff }})^{ {{ nLongAff }} }\approx {{ basePuissNLongAff }}$.
127
+
128
+ $S({{ nLongAff }}) =$ {input}`||80`
129
+ ::::
130
+
131
+ ::::{questionHint}
132
+ {fr}`Multiplier`{en}`Multiply` ${{ PAff }}$ {fr}`par la valeur fournie.`{en}`by the provided value.`
133
+ ::::
134
+
135
+ ::::{displayedSolution}
136
+ $S({{ nLongAff }}) = {{ sNLongAff }}$ €
137
+ ::::
138
+
139
+ ::::{detailedSolution}
140
+ {fr}`En utilisant la valeur fournie :`{en}`Using the provided value:`
141
+
142
+ \begin{equation*}
143
+ S({{ nLongAff }}) &= {{ PAff }} \times ({{ baseAff }})^{ {{ nLongAff }} } \approx {{ PAff }} \times {{ basePuissNLongAff }} \\
144
+ &= {{ sNLongAff }}.
145
+ \end{equation*}
146
+ ::::
147
+
148
+ ::::{weightDistribution}
149
+ :logic: 10
150
+ :abstraction: 15
151
+ :reasoning: 25
152
+ :calculation: 50
153
+ ::::
154
+ :::::
155
+
156
+ :::::{question}
157
+ :questionType: MCQ
158
+ :questionId: 2
159
+ :questionIndex: 2
160
+
161
+ ::::{questionStatement}
162
+ {fr}`Déterminer si`{en}`Determine whether` $S$ {fr}`est croissante ou décroissante, et justifier à partir de la base.`{en}`is increasing or decreasing, and justify from the base.`
163
+ ::::
164
+
165
+ ::::{questionHint}
166
+ {fr}`La base est`{en}`The base is` ${{ baseMonoAff }}>1$.
167
+ ::::
168
+
169
+ ::::{mcqAnswer}
170
+ :isRightAnswer: true
171
+ {fr}`Strictement croissante (base $> 1$)`{en}`Strictly increasing (base $> 1$)`
172
+ ::::
173
+
174
+ ::::{mcqAnswer}
175
+ :isRightAnswer: false
176
+ {fr}`Strictement décroissante (base $< 1$)`{en}`Strictly decreasing (base $< 1$)`
177
+ ::::
178
+
179
+ ::::{mcqAnswer}
180
+ :isRightAnswer: false
181
+ {fr}`Constante`{en}`Constant`
182
+ ::::
183
+
184
+ ::::{detailedSolution}
185
+ {fr}`La base est`{en}`The base is` $b={{ baseMonoAff }}>1$, {fr}`donc`{en}`so` $S$ {fr}`est strictement croissante : le capital augmente chaque année. C'est cohérent avec`{en}`is strictly increasing: the capital increases each year. This is consistent with` ${{ s0Aff }}<{{ s1Aff }}<{{ s2Aff }}<\dots<{{ sNLongAff }}$.
186
+ ::::
187
+
188
+ ::::{weightDistribution}
189
+ :logic: 25
190
+ :abstraction: 35
191
+ :reasoning: 30
192
+ :calculation: 10
193
+ ::::
194
+ :::::
195
+
196
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Inéquation linéaire en deux étapes`{en}`Linear Inequality in Two Steps`.md ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :title: {fr}`Inéquation linéaire en deux étapes`{en}`Linear Inequality in Two Steps`
3
+ :modules:
4
+ :recommendedExecutionTime: 6
5
+ :level: Elementary
6
+ :chap: chap_equations_Inequalities_linearInequalitiesIntervals_ESCP
7
+ :involvedConcepts:
8
+ :originalSource:
9
+ :visibility: All
10
+ :variations:
11
+ :comment: Échauffement : résolution en deux étapes (addition puis division par un réel positif), ensemble solution en intervalle, borne exclue.
12
+ :originalExerciseId: d40ec55f-633f-11f1-a8a1-0ed8d3b012a9
13
+ :id: e8a23848-7098-11f1-a8a1-0ed8d3b012a9
14
+
15
+ ````{python}
16
+ import random as rd
17
+ from sympy import oo
18
+ from pyxiscience.Mes_fctions_generalistes_bis import pxs_config, pxsl_latex_coefficient as lc
19
+ from pyxiscience.Classes_Extensions import pxs_Interval
20
+
21
+ config_standard = pxs_config()
22
+
23
+ # Contrainte : (c - b) divisible par a pour une solution entière
24
+ a = rd.randint(2, 9)
25
+ k = rd.randint(2, 9)
26
+ somme = k * a
27
+ b = rd.randint(-15, -1)
28
+ c = somme + b
29
+
30
+ x_sol = somme // a
31
+ x_test_exclu = x_sol
32
+ x_test_inclus = x_sol + 1
33
+ verif_exclu = a * x_test_exclu + b
34
+ verif_inclus = a * x_test_inclus + b
35
+ intervalAff = pxs_Interval(x_sol, oo, True, True).print()
36
+
37
+ # Rendus
38
+ aAff = lc(a)
39
+ bSignAff = lc(b, ones=True, sign=True)
40
+ negBAff = str(-b)
41
+ xSol = x_sol
42
+ xTestExclu = x_test_exclu
43
+ xTestInclus = x_test_inclus
44
+ verifExclu = verif_exclu
45
+ verifInclus = verif_inclus
46
+
47
+ globals()
48
+ ````
49
+
50
+ :::::{question}
51
+ :questionType: FGQ
52
+ :solution: [["ord","${{ xSol }}$","$+\infty$"],["0","0"]]
53
+
54
+ ::::{questionStatement}
55
+ {fr}`Résoudre l'inéquation`{en}`Solve the inequality` ${{ aAff }}x {{ bSignAff }} > {{ c }}$ {fr}`et soit`{en}`and let` $S$ {fr}`son ensemble solution. Écrire`{en}`be its solution set. Write` $S$ {fr}`sous forme d'intervalle (choisir le crochet adapté au caractère strict ou large de l'inégalité) :`{en}`as an interval (choose the bracket matching the strict or non-strict nature of the inequality):`
56
+
57
+ $S =$ {select}`[|]||]||45` {input}`||120` $;$ {input}`||120` {select}`]|[||[||45`
58
+ ::::
59
+
60
+ ::::{questionHint}
61
+ {fr}`Isoler`{en}`Isolate` $x$ {fr}`en deux étapes : ajouter`{en}`in two steps: add` ${{ negBAff }}${fr}`, puis diviser par`{en}`, then divide by` ${{ a }} > 0$ {fr}`(le sens est conservé).`{en}`(the inequality sign is preserved).`
62
+ ::::
63
+
64
+ ::::{displayedSolution}
65
+ $S = \left] {{ xSol }} ;\ +\infty \right[$
66
+ ::::
67
+
68
+ ::::{detailedSolution}
69
+ {fr}`L'addition puis la division par un réel strictement positif conserve le sens de l'inégalité.`{en}`Addition followed by division by a strictly positive real number preserves the inequality sign.`
70
+
71
+ \begin{equation*}
72
+ {{ aAff }}x {{ bSignAff }} + {{ negBAff }} > {{ c }} + {{ negBAff }} &\implies {{ aAff }}x > {{ somme }}\\[10pt]
73
+ &\implies \frac{ {{ aAff }}x }{ {{ a }} } > \frac{ {{ somme }} }{ {{ a }} } \\[10pt]
74
+ &\implies x > {{ xSol }}.
75
+ \end{equation*}
76
+
77
+ {fr}`L'ensemble des solutions est donc`{en}`The solution set is therefore`
78
+
79
+ \begin{equation*}
80
+ S = {{ intervalAff }}.
81
+ \end{equation*}
82
+
83
+ {fr}`La borne`{en}`The bound` ${{ xSol }}$ {fr}`est exclue (inégalité stricte`{en}`is excluded (strict inequality` $>${fr}`), d'où la parenthèse ouverte.`{en}`), hence the open parenthesis.`
84
+ ::::
85
+
86
+ ::::{weightDistribution}
87
+ :logic: 15
88
+ :abstraction: 20
89
+ :reasoning: 30
90
+ :calculation: 35
91
+ ::::
92
+ :::::
93
+
94
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Inéquation linéaire en une étape et notation intervalle`{en}`One-step linear inequality and interval notation`.md ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :title: {fr}`Inéquation linéaire en une étape et notation intervalle`{en}`One-step linear inequality and interval notation`
3
+ :modules:
4
+ :recommendedExecutionTime: 5
5
+ :level: Elementary
6
+ :chap:
7
+ :involvedConcepts: Interval_Notation, Solving_inequalities
8
+ :originalSource:
9
+ :visibility: All
10
+ :variations:
11
+ :comment: Échauffement : résolution d'une inéquation en une étape (règle d'addition), écriture de l'ensemble solution en notation intervalle et statut de la borne.
12
+ :originalExerciseId: cdbe79e9-633f-11f1-a8a1-0ed8d3b012a9
13
+ :id: 6fef6add-6e40-11f1-a8a1-0ed8d3b012a9
14
+
15
+ ````{python}
16
+ import random as rd
17
+ from sympy import oo
18
+ from pyxiscience.Mes_fctions_generalistes_bis import pxs_config, pxsl_latex_coefficient as lc
19
+ from pyxiscience.Classes_Extensions import pxs_Interval
20
+
21
+ config_standard = pxs_config()
22
+
23
+ # Tirage des paramètres
24
+ a = rd.choice([i for i in range(-10, 11) if i != 0])
25
+ b = rd.randint(-20, 20)
26
+ signe = rd.choice(['\\leq', '\\geq', '<', '>'])
27
+
28
+ # Solution de x + a (signe) b -> x (signe) b - a
29
+ sol = b - a
30
+ stricte = signe in ['<', '>']
31
+
32
+ if signe == '\\leq':
33
+ intervalAff = pxs_Interval(-oo, sol, True, False).print()
34
+ valVraie, valFausse = sol, sol + 1
35
+ elif signe == '\\geq':
36
+ intervalAff = pxs_Interval(sol, oo, False, True).print()
37
+ valVraie, valFausse = sol, sol - 1
38
+ elif signe == '<':
39
+ intervalAff = pxs_Interval(-oo, sol, True, True).print()
40
+ valVraie, valFausse = sol - 1, sol
41
+ else: # '>'
42
+ intervalAff = pxs_Interval(sol, oo, True, True).print()
43
+ valVraie, valFausse = sol + 1, sol
44
+
45
+
46
+ if pxs_lang == "fr":
47
+ borneStatutAff = "exclue" if stricte else "incluse"
48
+ inegaliteTypeAff = "stricte" if stricte else "large"
49
+ crochetAff = "ouvert" if stricte else "fermé"
50
+ else:
51
+ borneStatutAff = "excluded" if stricte else "included"
52
+ inegaliteTypeAff = "strict" if stricte else "non-strict"
53
+ crochetAff = "open" if stricte else "closed"
54
+
55
+ infiniAff = "-\\infty" if signe in ['\\leq', '<'] else "+\\infty"
56
+
57
+ # Rendus
58
+ aSignAff = lc(a, ones=True, sign=True)
59
+ negASignAff = lc(-a, ones=True, sign=True)
60
+ verifVraieGauche = valVraie + a
61
+ verifFausseGauche = valFausse + a
62
+ solLineAff = f"x {aSignAff} {negASignAff} {signe} {b} {negASignAff} \\implies x {signe} {sol}"
63
+
64
+ globals()
65
+
66
+
67
+ # ═══════════════ Ajouts conversion QCM (distracteurs) ═══════════════
68
+ from sympy import oo as _oo
69
+ _sol2 = b + a # erreur arithmétique : b + a au lieu de b - a
70
+ if signe == '\\leq':
71
+ distInt1 = pxs_Interval(-_oo, sol, True, True).print()
72
+ distInt2 = pxs_Interval(sol, _oo, False, True).print()
73
+ distInt3 = pxs_Interval(-_oo, _sol2, True, False).print()
74
+ elif signe == '\\geq':
75
+ distInt1 = pxs_Interval(sol, _oo, True, True).print()
76
+ distInt2 = pxs_Interval(-_oo, sol, True, False).print()
77
+ distInt3 = pxs_Interval(_sol2, _oo, False, True).print()
78
+ elif signe == '<':
79
+ distInt1 = pxs_Interval(-_oo, sol, True, False).print()
80
+ distInt2 = pxs_Interval(sol, _oo, True, True).print()
81
+ distInt3 = pxs_Interval(-_oo, _sol2, True, True).print()
82
+ else:
83
+ distInt1 = pxs_Interval(sol, _oo, False, True).print()
84
+ distInt2 = pxs_Interval(-_oo, sol, True, True).print()
85
+ distInt3 = pxs_Interval(_sol2, _oo, True, True).print()
86
+ ````
87
+
88
+ :::::{question}
89
+ :questionType: MCQ
90
+ :questionId: 0
91
+ :questionIndex: 0
92
+
93
+ ::::{questionStatement}
94
+ {fr}`Résoudre l'inéquation`{en}`Solve the inequality` $x{{ aSignAff }} {{ signe }} {{ b }}$, {fr}`écrire l'ensemble solution en notation intervalle et préciser si la borne obtenue est incluse ou exclue.`{en}`write the solution set in interval notation and specify whether the obtained bound is included or excluded.`
95
+ ::::
96
+
97
+ ::::{questionHint}
98
+ {fr}`Soustraire`{en}`Subtract` ${{ a }}$ {fr}`des deux membres : ajouter ou soustraire un même réel conserve le sens de l'inégalité (règle d'addition).`{en}`from both sides: adding or subtracting the same real number preserves the direction of the inequality (addition rule).`
99
+ ::::
100
+
101
+ ::::{mcqAnswer}
102
+ :isRightAnswer: true
103
+ $S = {{ intervalAff }}$
104
+ ::::
105
+
106
+ ::::{mcqAnswer}
107
+ :isRightAnswer: false
108
+ $S = {{ distInt1 }}$
109
+ ::::
110
+
111
+ ::::{mcqAnswer}
112
+ :isRightAnswer: false
113
+ $S = {{ distInt2 }}$
114
+ ::::
115
+
116
+ ::::{mcqAnswer}
117
+ :isRightAnswer: false
118
+ $S = {{ distInt3 }}$
119
+ ::::
120
+
121
+ ::::{mcqAnswer}
122
+ :isRightAnswer: false
123
+ {fr}`Aucune de ces réponses n'est correcte`{en}`None of these answers are correct`
124
+ ::::
125
+
126
+ ::::{detailedSolution}
127
+ {fr}`Additionner ou soustraire ne change pas le sens de l'inégalité.`{en}`Adding or subtracting does not change the direction of the inequality.`
128
+
129
+ \begin{equation*}
130
+ {{ solLineAff }}.
131
+ \end{equation*}
132
+
133
+ {fr}`L'ensemble des solutions est`{en}`The solution set is`
134
+
135
+ \begin{equation*}
136
+ S = {{ intervalAff }}.
137
+ \end{equation*}
138
+
139
+ {fr}`La borne`{en}`The bound` ${{ sol }}$ {fr}`est`{en}`is` {{ borneStatutAff }} ({fr}`inégalité`{en}`inequality` {{ inegaliteTypeAff }} ${{ signe }}$), {fr}`d'où le crochet`{en}`hence the bracket is` {{ crochetAff }}. {fr}`Le symbole`{en}`The symbol` ${{ infiniAff }}$ {fr}`n'est pas un réel donc la parenthèse ouverte est obligatoire.`{en}`is not a real number so the open parenthesis is mandatory.`
140
+ ::::
141
+
142
+ ::::{weightDistribution}
143
+ :logic: 15
144
+ :abstraction: 20
145
+ :reasoning: 30
146
+ :calculation: 35
147
+ ::::
148
+ :::::
149
+
150
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Lecture graphique du sens de variation et des extremums`{en}`Graphical reading of monotonicity and extrema`.md ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :originalExerciseId: a6a3c85d-689a-11f1-a8a1-0ed8d3b012a9
3
+ :title: {fr}`Lecture graphique du sens de variation et des extremums`{en}`Graphical reading of monotonicity and extrema`
4
+ :modules: fund_of_math_I_ESCP
5
+ :recommendedExecutionTime: 10
6
+ :level: Elementary
7
+ :chap: chap_extremaCurveSketching_monotonicityRelativeExtrema_ESCP
8
+ :involvedConcepts:
9
+ :originalSource:
10
+ :visibility: All
11
+ :variations:
12
+ :comment: Échauffement (thème pur) : lecture sur une courbe des intervalles de croissance/décroissance, du minimum et du maximum, et comparaison de deux images par la monotonie. Aucune dérivée.
13
+ :id: e2835bb5-74b2-11f1-a8a1-0ed8d3b012a9
14
+
15
+ ````{python}
16
+ import random as rd
17
+ # ──────────────────────────────────────────────────────────────────────────
18
+ # MÉTHODE CONSTRUCTIVE — partir de la réponse (les extremums et la structure)
19
+ # ──────────────────────────────────────────────────────────────────────────
20
+ # On veut une fonction continue sur [xMin, xMax] avec :
21
+ # - un minimum global en (x2, y2)
22
+ # - un maximum global en (x3, y3)
23
+ # - décroissante sur [xMin, x2], croissante sur [x2, x3], décroissante sur [x3, xMax]
24
+ #
25
+ # On tire d'abord les extremums (la réponse), puis on place les points de départ
26
+ # et d'arrivée de manière cohérente (y1 et y4 entre le min et le max).
27
+
28
+ xMin = rd.randint(-6, -3)
29
+ xMax = rd.randint(4, 7)
30
+
31
+ # Abscisses des extremums : x2 < x3
32
+ x2 = rd.randint(xMin + 2, xMin + 4)
33
+ x3 = rd.randint(x2 + 2, xMax - 2) if xMax - 2 >= x2 + 2 else x2 + 2
34
+
35
+ # Valeurs des extremums (la réponse)
36
+ y2 = rd.randint(-3, -1) # minimum
37
+ y3 = rd.randint(3, 5) # maximum
38
+
39
+ minVal = y2
40
+ maxVal = y3
41
+
42
+ # Points de départ et d'arrivée : ordonnées entre min et max
43
+ y1 = rd.randint(2, 4) # au-dessus du min, en dessous du max
44
+ y4 = rd.randint(0, 2) # idem
45
+
46
+ # Points remarquables pour le tracé
47
+ x1 = xMin
48
+ x4 = xMax
49
+
50
+ # Question 3 : comparaison par monotonie sur l'intervalle de croissance [x2, x3]
51
+ # On tire xComp1 < xComp2 dans cet intervalle -> f(xComp1) < f(xComp2)
52
+ xComp1 = rd.randint(x2 + 1, x3 - 2) if x3 - x2 > 2 else x2 + 1
53
+ xComp2 = x3
54
+ # ──────────────────────────────────────────────────────────────────────────
55
+ # MÉTHODE CONSTRUCTIVE — partir de la réponse (la comparaison par monotonie)
56
+ # ──────────────────────────────────────────────────────────────────────────
57
+ # La réponse attendue est : f(xComp1) < f(xComp2) car f est croissante sur [x2, x3]
58
+ # et xComp1 < xComp2 appartiennent à cet intervalle.
59
+ #
60
+ # Les variables xMin, xMax, x2, x3, y2, y3, minVal, maxVal, xComp1, xComp2
61
+ # sont déjà définies dans le bloc précédent. On les réutilise pour construire
62
+ # l'énoncé de cette question.
63
+
64
+ # === Ajouts conversion FGQ ===
65
+ minValSolAff = str(minVal)
66
+ x2SolAff = str(x2)
67
+ maxValSolAff = str(maxVal)
68
+ x3SolAff = str(x3)
69
+ globals()
70
+ ````
71
+ :::::{question}
72
+ :questionType: MCQ
73
+
74
+ ::::{questionStatement}
75
+ {fr}`La figure ci-dessous représente la courbe d'une fonction`{en}`The figure below represents the curve of a function` $f$ {fr}`définie sur`{en}`defined on` $[{{ xMin }}\,;\,{{ xMax }}]$. {fr}`Lire les intervalles sur lesquels`{en}`Read the intervals on which` $f$ {fr}`est croissante, puis ceux sur lesquels`{en}`is increasing, then those on which` $f$ {fr}`est décroissante.`{en}`is decreasing.`
76
+
77
+ ```{python}
78
+ import matplotlib.pyplot as plt
79
+ import numpy as np
80
+
81
+ def bez(p0, p1, p2, p3, t):
82
+ return (1-t)**3*p0 + 3*(1-t)**2*t*p1 + 3*(1-t)*t**2*p2 + t**3*p3
83
+
84
+ t = np.linspace(0, 1, 60)
85
+ fig, ax = plt.subplots(figsize=(6, 4.5))
86
+ segments = [((x1, y1), (x1+1, (y1+y2)/2), (x2-1, y2+0.5), (x2, y2)),
87
+ ((x2, y2), ((x2+x3)/2, (y2+y3)/2), (x3-1, y3-0.5), (x3, y3)),
88
+ ((x3, y3), (x3+1, (y3+y4)/2), (x4-1, y4+0.5), (x4, y4))]
89
+ for P0, C1, C2, P1 in segments:
90
+ ax.plot(bez(P0[0], C1[0], C2[0], P1[0], t), bez(P0[1], C1[1], C2[1], P1[1], t),
91
+ color="#1f77b4", linewidth=2)
92
+ pts = [(x1, y1, "right", "bottom"), (x2, y2, "left", "top"),
93
+ (x3, y3, "left", "bottom"), (x4, y4, "left", "top")]
94
+ for px, py, ha, va in pts:
95
+ ax.plot(px, py, "o", color="red", markersize=5)
96
+ ax.annotate(f"$({px}\\,;\\,{py})$", (px, py), textcoords="offset points",
97
+ xytext=(6 if ha == "left" else -6, 6 if va == "bottom" else -10),
98
+ ha=ha, fontsize=9)
99
+ ax.axhline(0, color="k", linewidth=0.8)
100
+ ax.axvline(0, color="k", linewidth=0.8)
101
+ ax.set_xticks(range(xMin, xMax+1))
102
+ ax.set_yticks(range(y2, y3+1))
103
+ ax.set_xlabel("$x$")
104
+ ax.set_ylabel("$y$")
105
+ ax.grid(True, alpha=0.3)
106
+ plt.show()
107
+ ```
108
+ ::::
109
+
110
+ ::::{questionHint}
111
+ {fr}`Lire les intervalles de croissance (courbe montante) et de décroissance (courbe descendante) directement sur la courbe.`{en}`Read the intervals of increase (rising curve) and decrease (falling curve) directly from the curve.`
112
+ ::::
113
+
114
+ ::::{mcqAnswer}
115
+ :isRightAnswer: true
116
+ {fr}`Croissante sur`{en}`Increasing on` $[{{ x2 }}\,;\,{{ x3 }}]$ {fr}`; décroissante sur`{en}`; decreasing on` $[{{ xMin }}\,;\,{{ x2 }}]$ {fr}`et`{en}`and` $[{{ x3 }}\,;\,{{ xMax }}]$
117
+ ::::
118
+
119
+ ::::{mcqAnswer}
120
+ :isRightAnswer: false
121
+ {fr}`Décroissante sur`{en}`Decreasing on` $[{{ x2 }}\,;\,{{ x3 }}]$ {fr}`; croissante sur`{en}`; increasing on` $[{{ xMin }}\,;\,{{ x2 }}]$ {fr}`et`{en}`and` $[{{ x3 }}\,;\,{{ xMax }}]$
122
+ ::::
123
+
124
+ ::::{mcqAnswer}
125
+ :isRightAnswer: false
126
+ {fr}`Croissante sur tout`{en}`Increasing on the whole` $[{{ xMin }}\,;\,{{ xMax }}]$
127
+ ::::
128
+
129
+ ::::{mcqAnswer}
130
+ :isRightAnswer: false
131
+ {fr}`Aucune de ces réponses n'est correcte`{en}`None of these answers are correct`
132
+ ::::
133
+
134
+ ::::{detailedSolution}
135
+ {fr}`La courbe descend de`{en}`The curve falls from` $x={{ xMin }}$ {fr}`à`{en}`to` $x={{ x2 }}$ : $f$ {fr}`est décroissante sur`{en}`is decreasing on` $[{{ xMin }}\,;\,{{ x2 }}]$. {fr}`Elle monte de`{en}`It rises from` $x={{ x2 }}$ {fr}`à`{en}`to` $x={{ x3 }}$ : $f$ {fr}`est croissante sur`{en}`is increasing on` $[{{ x2 }}\,;\,{{ x3 }}]$. {fr}`Elle redescend de`{en}`It falls again from` $x={{ x3 }}$ {fr}`à`{en}`to` $x={{ xMax }}$ : $f$ {fr}`est décroissante sur`{en}`is decreasing on` $[{{ x3 }}\,;\,{{ xMax }}]$.
136
+ ::::
137
+
138
+ ::::{weightDistribution}
139
+ :logic: 15
140
+ :abstraction: 25
141
+ :reasoning: 35
142
+ :calculation: 25
143
+ ::::
144
+ :::::
145
+
146
+ :::::{question}
147
+ :questionType: FGQ
148
+ :solution: [["ord","${{ minValSolAff }}$","${{ x2SolAff }}$","${{ maxValSolAff }}$","${{ x3SolAff }}$"],["0","0","0","0"]]
149
+
150
+ ::::{questionStatement}
151
+ {fr}`Lire le minimum et le maximum de`{en}`Read the minimum and maximum of` $f$ {fr}`sur`{en}`on` $[{{ xMin }}\,;\,{{ xMax }}]$, {fr}`et préciser les abscisses où ils sont atteints.`{en}`and specify the abscissas where they are attained.`
152
+
153
+ {fr}`minimum`{en}`minimum` $=$ {input}`||60` {fr}`atteint en`{en}`attained at` $x =$ {input}`||60` {fr}`; maximum`{en}`; maximum` $=$ {input}`||60` {fr}`atteint en`{en}`attained at` $x =$ {input}`||60`
154
+ ::::
155
+
156
+ ::::{questionHint}
157
+ {fr}`Repérer le point le plus bas et le point le plus haut de la courbe.`{en}`Identify the lowest point and the highest point of the curve.`
158
+ ::::
159
+
160
+ ::::{displayedSolution}
161
+ {fr}`minimum`{en}`minimum` $= {{ minVal }}$ {fr}`atteint en`{en}`attained at` $x = {{ x2 }}$ ; {fr}`maximum`{en}`maximum` $= {{ maxVal }}$ {fr}`atteint en`{en}`attained at` $x = {{ x3 }}$
162
+ ::::
163
+
164
+ ::::{detailedSolution}
165
+ {fr}`Le point le plus bas est`{en}`The lowest point is` $({{ x2 }}\,;\,{{ minVal }})$ : {fr}`le minimum de`{en}`the minimum of` $f$ {fr}`sur`{en}`on` $[{{ xMin }}\,;\,{{ xMax }}]$ {fr}`est`{en}`is` ${{ minVal }}$, {fr}`atteint en`{en}`attained at` $x={{ x2 }}$. {fr}`Le point le plus haut est`{en}`The highest point is` $({{ x3 }}\,;\,{{ maxVal }})$ : {fr}`le maximum est`{en}`the maximum is` ${{ maxVal }}$, {fr}`atteint en`{en}`attained at` $x={{ x3 }}$.
166
+ ::::
167
+
168
+ ::::{weightDistribution}
169
+ :logic: 15
170
+ :abstraction: 25
171
+ :reasoning: 35
172
+ :calculation: 25
173
+ ::::
174
+ :::::
175
+
176
+ :::::{question}
177
+ :questionType: MCQ
178
+
179
+ ::::{questionStatement}
180
+ {fr}`Justifier, à partir du sens de variation, que`{en}`Justify, from the monotonicity, that` $f({{ xComp1 }}) < f({{ xComp2 }})$.
181
+ ::::
182
+
183
+ ::::{questionHint}
184
+ {fr}`Observer que`{en}`Observe that` ${{ xComp1 }}$ {fr}`et`{en}`and` ${{ xComp2 }}$ {fr}`appartiennent au même intervalle de croissance.`{en}`belong to the same interval of increase.`
185
+ ::::
186
+
187
+ ::::{mcqAnswer}
188
+ :isRightAnswer: true
189
+ {fr}`$f$ est croissante sur`{en}`$f$ is increasing on` $[{{ x2 }}\,;\,{{ x3 }}]$ {fr}`qui contient`{en}`which contains` ${{ xComp1 }}$ {fr}`et`{en}`and` ${{ xComp2 }}$, {fr}`et`{en}`and` ${{ xComp1 }}<{{ xComp2 }}$
190
+ ::::
191
+
192
+ ::::{mcqAnswer}
193
+ :isRightAnswer: false
194
+ {fr}`$f$ est décroissante sur cet intervalle`{en}`$f$ is decreasing on this interval`
195
+ ::::
196
+
197
+ ::::{mcqAnswer}
198
+ :isRightAnswer: false
199
+ {fr}`$f$ est constante sur cet intervalle`{en}`$f$ is constant on this interval`
200
+ ::::
201
+
202
+ ::::{mcqAnswer}
203
+ :isRightAnswer: false
204
+ {fr}`Aucune de ces réponses n'est correcte`{en}`None of these answers are correct`
205
+ ::::
206
+
207
+ ::::{detailedSolution}
208
+ {fr}`On a`{en}`We have` ${{ xComp1 }}<{{ xComp2 }}$, {fr}`et`{en}`and` $f$ {fr}`est croissante sur`{en}`is increasing on` $[{{ x2 }}\,;\,{{ x3 }}]$, {fr}`intervalle qui contient`{en}`an interval that contains` ${{ xComp1 }}$ {fr}`et`{en}`and` ${{ xComp2 }}$. {fr}`Par définition de la croissance,`{en}`By definition of increasing function,` ${{ xComp1 }}<{{ xComp2 }}$ {fr}`entraîne`{en}`implies` $f({{ xComp1 }})<f({{ xComp2 }})$.
209
+ ::::
210
+
211
+ ::::{weightDistribution}
212
+ :logic: 25
213
+ :abstraction: 30
214
+ :reasoning: 35
215
+ :calculation: 10
216
+ ::::
217
+ :::::
218
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Monotonie selon la base et comparaison de valeurs`{en}`Monotonicity according to base and comparison of values`.md ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :title: {fr}`Monotonie selon la base et comparaison de valeurs`{en}`Monotonicity according to base and comparison of values`
3
+ :modules:
4
+ :recommendedExecutionTime: 10
5
+ :level: Elementary
6
+ :chap: chap_expLogFunctions_exponentialFunctions_ESCP
7
+ :involvedConcepts:
8
+ :originalSource:
9
+ :visibility: All
10
+ :variations:
11
+ :comment: Échauffement (thème pur) : reconnaissance de la monotonie de quatre fonctions b^x d'après la base, et comparaison de valeurs par la monotonie (sans calcul exact).
12
+ :id: 73274236-74d8-11f1-a8a1-0ed8d3b012a9
13
+ :originalExerciseId: 699e40c5-6597-11f1-a8a1-0ed8d3b012a9
14
+
15
+ ````{python}
16
+ import random as rd
17
+ from fractions import Fraction
18
+
19
+ def frac_latex(fr):
20
+ fr = Fraction(fr)
21
+ if fr.denominator == 1:
22
+ return str(fr.numerator)
23
+ sign = '-' if fr.numerator < 0 else ''
24
+ return sign + r'\dfrac{%d}{%d}' % (abs(fr.numerator), fr.denominator)
25
+
26
+ # Construction déterministe : monotonies garanties par les intervalles des bases.
27
+ b1 = rd.randint(2, 5) # base entière > 1 -> croissante
28
+ b2Den = rd.randint(2, 5)
29
+ b2 = Fraction(1, b2Den) # base dans (0,1) -> décroissante
30
+ b3Den = rd.randint(2, 5)
31
+ b3Num = rd.randint(b3Den + 1, 9)
32
+ b3 = Fraction(b3Num, b3Den) # base > 1 -> croissante
33
+ b4Dec = rd.randint(6, 9)
34
+ b4 = Fraction(b4Dec, 10) # base décimale dans (0,1) -> décroissante
35
+
36
+ # Exposants pour Q2 (>= 2 pour éviter les exposants 0 et 1)
37
+ x1 = rd.randint(2, 4)
38
+ x2 = rd.randint(x1 + 2, x1 + 4)
39
+
40
+ # Rendus
41
+ b1Aff = str(b1)
42
+ b2Aff = frac_latex(b2)
43
+ b3Aff = frac_latex(b3)
44
+ b4Aff = '0{,}%d' % b4Dec
45
+ x1Aff = str(x1)
46
+ x2Aff = str(x2)
47
+ f1x1Aff = frac_latex(b1**x1)
48
+ f1x2Aff = frac_latex(b1**x2)
49
+ f2x1Aff = frac_latex(b2**x1)
50
+ f2x2Aff = frac_latex(b2**x2)
51
+
52
+ globals()
53
+ ````
54
+
55
+ :::::{question}
56
+ :questionType: MCQ
57
+ :questionId: 0
58
+ :questionIndex: 0
59
+
60
+ ::::{questionStatement}
61
+ {fr}`On considère`{en}`Consider` $f_{1}(x)={{ b1Aff }}^{x}$, $f_{2}(x)=\left({{ b2Aff }}\right)^{x}$, $f_{3}(x)=\left({{ b3Aff }}\right)^{x}$ {fr}`et`{en}`and` $f_{4}(x)=({{ b4Aff }})^{x}$. {fr}`Reconnaître lesquelles sont strictement croissantes et lesquelles sont strictement décroissantes, en justifiant à partir de la base.`{en}`Identify which are strictly increasing and which are strictly decreasing, justifying from the base.`
62
+ ::::
63
+
64
+ ::::{questionHint}
65
+ {fr}`Si`{en}`If` $b>1$ {fr}`: croissante ; si`{en}`: increasing; if` $0<b<1$ {fr}`: décroissante.`{en}`: decreasing.`
66
+ ::::
67
+
68
+ ::::{mcqAnswer}
69
+ :isRightAnswer: true
70
+ {fr}`$f_{1}$ et $f_{3}$ strictement croissantes ; $f_{2}$ et $f_{4}$ strictement décroissantes`{en}`$f_{1}$ and $f_{3}$ strictly increasing; $f_{2}$ and $f_{4}$ strictly decreasing`
71
+ ::::
72
+
73
+ ::::{mcqAnswer}
74
+ :isRightAnswer: false
75
+ {fr}`$f_{1}$ et $f_{2}$ strictement croissantes ; $f_{3}$ et $f_{4}$ strictement décroissantes`{en}`$f_{1}$ and $f_{2}$ strictly increasing; $f_{3}$ and $f_{4}$ strictly decreasing`
76
+ ::::
77
+
78
+ ::::{mcqAnswer}
79
+ :isRightAnswer: false
80
+ {fr}`$f_{2}$ et $f_{4}$ strictement croissantes ; $f_{1}$ et $f_{3}$ strictement décroissantes`{en}`$f_{2}$ and $f_{4}$ strictly increasing; $f_{1}$ and $f_{3}$ strictly decreasing`
81
+ ::::
82
+
83
+ ::::{mcqAnswer}
84
+ :isRightAnswer: false
85
+ {fr}`Les quatre fonctions sont strictement croissantes`{en}`All four functions are strictly increasing`
86
+ ::::
87
+
88
+ ::::{mcqAnswer}
89
+ :isRightAnswer: false
90
+ {fr}`Aucune de ces réponses n'est correcte`{en}`None of these answers are correct`
91
+ ::::
92
+
93
+ ::::{detailedSolution}
94
+ $f_{1}$ {fr}`: base`{en}`: base` ${{ b1Aff }}>1$, {fr}`strictement croissante.`{en}`strictly increasing.` $f_{2}$ {fr}`: base`{en}`: base` ${{ b2Aff }}\in(0,1)$, {fr}`strictement décroissante.`{en}`strictly decreasing.` $f_{3}$ {fr}`: base`{en}`: base` ${{ b3Aff }}>1$, {fr}`strictement croissante.`{en}`strictly increasing.` $f_{4}$ {fr}`: base`{en}`: base` ${{ b4Aff }}\in(0,1)$, {fr}`strictement décroissante.`{en}`strictly decreasing.`
95
+ ::::
96
+
97
+ ::::{weightDistribution}
98
+ :logic: 25
99
+ :abstraction: 35
100
+ :reasoning: 30
101
+ :calculation: 10
102
+ ::::
103
+ :::::
104
+
105
+ :::::{question}
106
+ :questionType: MCQ
107
+ :questionId: 1
108
+ :questionIndex: 1
109
+
110
+ ::::{questionStatement}
111
+ {fr}`Comparer`{en}`Compare` $f_{1}({{ x1Aff }})$ {fr}`et`{en}`and` $f_{1}({{ x2Aff }})$, {fr}`puis`{en}`then` $f_{2}({{ x1Aff }})$ {fr}`et`{en}`and` $f_{2}({{ x2Aff }})$, {fr}`sans calculer les valeurs exactes.`{en}`without calculating the exact values.`
112
+ ::::
113
+
114
+ ::::{questionHint}
115
+ {fr}`Utiliser la monotonie :`{en}`Use monotonicity:` ${{ x1Aff }}<{{ x2Aff }}$.
116
+ ::::
117
+
118
+ ::::{mcqAnswer}
119
+ :isRightAnswer: true
120
+ $f_{1}({{ x1Aff }}) < f_{1}({{ x2Aff }})$ {fr}`et`{en}`and` $f_{2}({{ x1Aff }}) > f_{2}({{ x2Aff }})$
121
+ ::::
122
+
123
+ ::::{mcqAnswer}
124
+ :isRightAnswer: false
125
+ $f_{1}({{ x1Aff }}) > f_{1}({{ x2Aff }})$ {fr}`et`{en}`and` $f_{2}({{ x1Aff }}) < f_{2}({{ x2Aff }})$
126
+ ::::
127
+
128
+ ::::{mcqAnswer}
129
+ :isRightAnswer: false
130
+ $f_{1}({{ x1Aff }}) < f_{1}({{ x2Aff }})$ {fr}`et`{en}`and` $f_{2}({{ x1Aff }}) < f_{2}({{ x2Aff }})$
131
+ ::::
132
+
133
+ ::::{mcqAnswer}
134
+ :isRightAnswer: false
135
+ $f_{1}({{ x1Aff }}) > f_{1}({{ x2Aff }})$ {fr}`et`{en}`and` $f_{2}({{ x1Aff }}) > f_{2}({{ x2Aff }})$
136
+ ::::
137
+
138
+ ::::{mcqAnswer}
139
+ :isRightAnswer: false
140
+ {fr}`Aucune de ces réponses n'est correcte`{en}`None of these answers are correct`
141
+ ::::
142
+
143
+ ::::{detailedSolution}
144
+ $f_{1}$ {fr}`est strictement croissante et`{en}`is strictly increasing and` ${{ x1Aff }}<{{ x2Aff }}$, {fr}`donc`{en}`so` $f_{1}({{ x1Aff }})<f_{1}({{ x2Aff }})$ ({fr}`soit`{en}`i.e.` ${{ b1Aff }}^{ {{ x1Aff }} }<{{ b1Aff }}^{ {{ x2Aff }} }$, {fr}`c'est-à-dire`{en}`that is` ${{ f1x1Aff }}<{{ f1x2Aff }}$).\
145
+ \
146
+ $f_{2}$ {fr}`est strictement décroissante et`{en}`is strictly decreasing and` ${{ x1Aff }}<{{ x2Aff }}$, {fr}`donc`{en}`so` $f_{2}({{ x1Aff }})>f_{2}({{ x2Aff }})$ ({fr}`soit`{en}`i.e.` $\left({{ b2Aff }}\right)^{ {{ x1Aff }} }>\left({{ b2Aff }}\right)^{ {{ x2Aff }} }$, {fr}`c'est-à-dire`{en}`that is` ${{ f2x1Aff }}>{{ f2x2Aff }}$).
147
+ ::::
148
+
149
+ ::::{weightDistribution}
150
+ :logic: 25
151
+ :abstraction: 30
152
+ :reasoning: 35
153
+ :calculation: 10
154
+ ::::
155
+ :::::
156
+
157
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Objectif de revenu`{en}`Income Target`.md ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :originalExerciseId: 3fb67117-634b-11f1-a8a1-0ed8d3b012a9
3
+ :title: {fr}`Objectif de revenu`{en}`Income Target`
4
+ :modules:
5
+ :recommendedExecutionTime: 10
6
+ :level: Elementary
7
+ :chap:
8
+ :involvedConcepts: Solving_inequalities, Modeling_with_Functions
9
+ :originalSource: Session 2: Equations and Inequalities - Applications of Linear Inequalities (Erwan Lamy, ESCP Business School)
10
+ :visibility: All
11
+ :variations:
12
+ :comment: Version QCM — inéquation « au moins », résolution et intervalle.
13
+ :id: b3ceb29c-6e47-11f1-a8a1-0ed8d3b012a9
14
+
15
+ ````{python}
16
+ import random as rd
17
+ from pyxiscience.Mes_fctions_generalistes_bis import pxs_config, pxsl_format_number
18
+ from pyxiscience.Classes_Extensions import pxs_Interval
19
+
20
+ config_standard = pxs_config()
21
+
22
+ # Tarif horaire (multiple de 5) et heures cibles ; revenu = rate * hMin (entier)
23
+ rate = rd.choice([50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100, 110, 120, 125, 150])
24
+ hMin = rd.randint(15, 50)
25
+ target = rate * hMin
26
+
27
+ # Précalcul de l'affichage
28
+ rateAff = pxsl_format_number(rate)
29
+ hMinAff = pxsl_format_number(hMin)
30
+ targetAff = pxsl_format_number(target)
31
+
32
+ inter = pxs_Interval(hMin, 168, False, False)
33
+
34
+ # === Distracteurs MCQ (ajouts ; le code ci-dessus est inchangé) ===
35
+ from sympy import oo
36
+ interCorrect = inter.print()
37
+ interD1 = pxs_Interval(hMin, 168, True, True).print()
38
+ interD2 = pxs_Interval(0, hMin, False, False).print()
39
+ interD3 = pxs_Interval(hMin, oo, False, True).print()
40
+
41
+ globals()
42
+ ````
43
+
44
+ {fr}`Une consultante indépendante facture`{en}`An independent consultant charges` ${{rateAff}}$ € {fr}`de l'heure. Elle souhaite que son revenu hebdomadaire soit d'au moins`{en}`per hour. She wants her weekly income to be at least` ${{targetAff}}$ €. {fr}`On note`{en}`Let` $h$ {fr}`le nombre d'heures travaillées par semaine.`{en}`be the number of hours worked per week.`
45
+
46
+ :::::{question}
47
+ :questionType: MCQ
48
+
49
+ ::::{questionStatement}
50
+ {fr}`Quelle inéquation traduit son objectif de revenu ?`{en}`Which inequality represents her income target?`
51
+ ::::
52
+
53
+ ::::{questionHint}
54
+ {fr}`« Au moins » se traduit par`{en}`"At least" translates to` $\geq$. {fr}`Son revenu hebdomadaire est`{en}`Her weekly income is` ${{rateAff}}\,h$.
55
+ ::::
56
+
57
+ ::::{mcqAnswer}
58
+ :isRightAnswer: true
59
+ ${{rateAff}}\,h \geq {{targetAff}}$
60
+ ::::
61
+
62
+ ::::{mcqAnswer}
63
+ :isRightAnswer: false
64
+ ${{rateAff}}\,h \leq {{targetAff}}$
65
+ ::::
66
+
67
+ ::::{mcqAnswer}
68
+ :isRightAnswer: false
69
+ ${{rateAff}}\,h = {{targetAff}}$
70
+ ::::
71
+
72
+ ::::{mcqAnswer}
73
+ :isRightAnswer: false
74
+ ${{targetAff}}\,h \geq {{rateAff}}$
75
+ ::::
76
+
77
+ ::::{mcqAnswer}
78
+ :isRightAnswer: false
79
+ ${{targetAff}}\,h \leq {{rateAff}}$
80
+ ::::
81
+
82
+ ::::{mcqAnswer}
83
+ :isRightAnswer: false
84
+ {fr}`Aucune de ces réponses n'est correcte`{en}`None of these answers are correct`
85
+ ::::
86
+
87
+ ::::{detailedSolution}
88
+ {fr}`Le revenu hebdomadaire est`{en}`The weekly income is` ${{rateAff}}\,h$ {fr}`euros. L'objectif « revenu`{en}`euros. The target "income` $\geq {{targetAff}}$ {fr}`» donne :`{en}`" gives:`
89
+
90
+ \begin{equation*}
91
+ {{rateAff}}\,h \geq {{targetAff}}.
92
+ \end{equation*}
93
+ ::::
94
+
95
+ ::::{weightDistribution}
96
+ :logic: 15
97
+ :abstraction: 30
98
+ :reasoning: 35
99
+ :calculation: 20
100
+ ::::
101
+ :::::
102
+
103
+ :::::{question}
104
+ :questionType: MCQ
105
+
106
+ ::::{questionStatement}
107
+ {fr}`Résoudre l'inéquation`{en}`Solve the inequality` ${{rateAff}}\,h \geq {{targetAff}}$ {fr}`(sachant qu'une semaine compte 168 heures). Quel est l'ensemble des solutions sous forme d'intervalle ?`{en}`(knowing a week has 168 hours). What is the solution set in interval form?`
108
+ ::::
109
+
110
+ ::::{questionHint}
111
+ {fr}`Diviser les deux membres par`{en}`Divide both sides by` ${{rateAff}}$, {fr}`qui est positif : le sens de l'inégalité est conservé.`{en}`which is positive: the inequality sign is preserved.`
112
+ ::::
113
+
114
+ ::::{mcqAnswer}
115
+ :isRightAnswer: true
116
+ ${{interCorrect}}$
117
+ ::::
118
+
119
+ ::::{mcqAnswer}
120
+ :isRightAnswer: false
121
+ ${{interD1}}$
122
+ ::::
123
+
124
+ ::::{mcqAnswer}
125
+ :isRightAnswer: false
126
+ ${{interD2}}$
127
+ ::::
128
+
129
+ ::::{mcqAnswer}
130
+ :isRightAnswer: false
131
+ ${{interD3}}$
132
+ ::::
133
+
134
+ ::::{mcqAnswer}
135
+ :isRightAnswer: false
136
+ {fr}`Aucune de ces réponses n'est correcte`{en}`None of these answers are correct`
137
+ ::::
138
+
139
+ ::::{detailedSolution}
140
+ {fr}`On divise les deux membres par`{en}`We divide both sides by` ${{rateAff}} > 0$ {fr}`(sens conservé) :`{en}`(sign preserved):`
141
+
142
+ \begin{equation*}
143
+ h \geq \dfrac{ {{targetAff}} }{ {{rateAff}} } = {{hMinAff}}.
144
+ \end{equation*}
145
+
146
+ {fr}`L'ensemble des solutions est :`{en}`The solution set is:`
147
+
148
+ \begin{equation*}
149
+ h \in {{inter.print()}}.
150
+ \end{equation*}
151
+
152
+ {fr}`**Conclusion.** La consultante doit travailler au moins`{en}`**Conclusion.** The consultant must work at least` ${{hMinAff}}$ {fr}`heures par semaine pour atteindre son objectif de revenu. Elle ne peut pas travailler plus de 168 heures car cela correspond au nombre d'heures dans une semaine.`{en}`hours per week to reach her income target. She cannot work more than 168 hours as this corresponds to the number of hours in a week.`
153
+ ::::
154
+
155
+ ::::{weightDistribution}
156
+ :logic: 10
157
+ :abstraction: 15
158
+ :reasoning: 25
159
+ :calculation: 50
160
+ ::::
161
+ :::::
162
+
163
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Plafond budgétaire`{en}`Budget cap`.md ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :originalExerciseId: ccace217-633f-11f1-a8a1-0ed8d3b012a9
3
+ :title: {fr}`Plafond budgétaire`{en}`Budget cap`
4
+ :modules:
5
+ :recommendedExecutionTime: 10
6
+ :level: Elementary
7
+ :chap:
8
+ :involvedConcepts: Solving_inequalities
9
+ :originalSource: Session 2: Equations and Inequalities - Applications of Linear Inequalities (Erwan Lamy, ESCP Business School)
10
+ :visibility: All
11
+ :variations:
12
+ :comment: Version QCM — contrainte « ne pas dépasser », résolution et intervalle.
13
+ :id: c8218be2-6e47-11f1-a8a1-0ed8d3b012a9
14
+
15
+ ````{python}
16
+ import random as rd
17
+ from pyxiscience.Mes_fctions_generalistes_bis import pxs_config
18
+ from pyxiscience.Classes_Extensions import pxs_Interval
19
+
20
+ config_standard = pxs_config()
21
+
22
+ # Tirage aléatoire des paramètres
23
+ for _ in range(200):
24
+ budget = rd.randint(80, 200)
25
+ spent = rd.randint(20, budget - 10)
26
+ if spent < budget:
27
+ break
28
+
29
+ # Calcul de la solution
30
+ xMax = budget - spent
31
+
32
+ # === Distracteurs MCQ (ajouts ; le code ci-dessus est inchangé) ===
33
+ from sympy import oo
34
+ interCorrect = pxs_Interval(0, xMax, False, False).print()
35
+ interD1 = pxs_Interval(0, xMax, True, True).print()
36
+ interD2 = pxs_Interval(0, budget, False, False).print()
37
+ interD3 = pxs_Interval(xMax, oo, False, True).print()
38
+
39
+ globals()
40
+ ````
41
+
42
+ :::::{question}
43
+ :questionType: MCQ
44
+
45
+ ::::{questionStatement}
46
+ {fr}`Une étudiante dispose d'un budget hebdomadaire de {{budget}} € pour son alimentation. Elle a déjà dépensé {{spent}} € cette semaine. On note $x$ le montant supplémentaire (en euros) qu'elle peut encore dépenser. Quelle inéquation traduit que sa dépense totale ne doit pas dépasser {{budget}} € ?`{en}`A student has a weekly budget of €{{budget}} for food. She has already spent €{{spent}} this week. Let $x$ denote the additional amount (in euros) she may still spend. Which inequality expresses that her total spending must not exceed €{{budget}}?`
47
+ ::::
48
+
49
+ ::::{questionHint}
50
+ {fr}`« Ne doit pas dépasser » se traduit par $\leq$. La dépense totale est ${{spent}} + x$.`{en}`"Must not exceed" translates to $\leq$. The total spending is ${{spent}} + x$.`
51
+ ::::
52
+
53
+ ::::{mcqAnswer}
54
+ :isRightAnswer: true
55
+ ${{spent}} + x \leq {{budget}}$
56
+ ::::
57
+
58
+ ::::{mcqAnswer}
59
+ :isRightAnswer: false
60
+ ${{spent}} + x \geq {{budget}}$
61
+ ::::
62
+
63
+ ::::{mcqAnswer}
64
+ :isRightAnswer: false
65
+ ${{spent}} + x = {{budget}}$
66
+ ::::
67
+
68
+ ::::{mcqAnswer}
69
+ :isRightAnswer: false
70
+ ${{budget}} + x \leq {{spent}}$
71
+ ::::
72
+
73
+ ::::{mcqAnswer}
74
+ :isRightAnswer: false
75
+ {fr}`Aucune de ces réponses n'est correcte`{en}`None of these answers are correct`
76
+ ::::
77
+
78
+ ::::{detailedSolution}
79
+ {fr}`La dépense totale est ${{spent}} + x$ euros. La contrainte « dépense totale $\leq {{budget}}$ » donne :`{en}`The total spending is ${{spent}} + x$ euros. The constraint "total spending $\leq {{budget}}$" gives:`
80
+
81
+ \begin{equation*}
82
+ {{spent}} + x \leq {{budget}}.
83
+ \end{equation*}
84
+ ::::
85
+
86
+ ::::{weightDistribution}
87
+ :logic: 15
88
+ :abstraction: 30
89
+ :reasoning: 35
90
+ :calculation: 20
91
+ ::::
92
+ :::::
93
+
94
+ :::::{question}
95
+ :questionType: MCQ
96
+
97
+ ::::{questionStatement}
98
+ {fr}`Résoudre l'inéquation ${{spent}} + x \leq {{budget}}$ (on rappelle que $x \geq 0$ puisque $x$ est un montant). Quel est l'ensemble des solutions sous forme d'intervalle ?`{en}`Solve the inequality ${{spent}} + x \leq {{budget}}$ (recall that $x \geq 0$ since $x$ is an amount of money). What is the solution set as an interval?`
99
+ ::::
100
+
101
+ ::::{questionHint}
102
+ {fr}`Soustraire {{spent}} aux deux membres : ajouter ou retrancher une constante ne change pas le sens de l'inégalité.`{en}`Subtract {{spent}} from both sides: adding or subtracting a constant does not change the direction of the inequality.`
103
+ ::::
104
+
105
+ ::::{mcqAnswer}
106
+ :isRightAnswer: true
107
+ ${{interCorrect}}$
108
+ ::::
109
+
110
+ ::::{mcqAnswer}
111
+ :isRightAnswer: false
112
+ ${{interD1}}$
113
+ ::::
114
+
115
+ ::::{mcqAnswer}
116
+ :isRightAnswer: false
117
+ ${{interD2}}$
118
+ ::::
119
+
120
+ ::::{mcqAnswer}
121
+ :isRightAnswer: false
122
+ ${{interD3}}$
123
+ ::::
124
+
125
+ ::::{mcqAnswer}
126
+ :isRightAnswer: false
127
+ {fr}`Aucune de ces réponses n'est correcte`{en}`None of these answers are correct`
128
+ ::::
129
+
130
+ ::::{detailedSolution}
131
+ {fr}`On soustrait {{spent}} aux deux membres (le sens est conservé) :`{en}`Subtract {{spent}} from both sides (the direction is preserved):`
132
+
133
+ \begin{equation*}
134
+ x \leq {{budget}} - {{spent}} = {{xMax}}.
135
+ \end{equation*}
136
+
137
+ {fr}`Comme $x \geq 0$, l'ensemble des solutions est :`{en}`Since $x \geq 0$, the solution set is:`
138
+
139
+ \begin{equation*}
140
+ x \in {{pxs_Interval(0, xMax, False, False).print()}}.
141
+ \end{equation*}
142
+
143
+ {fr}`**Conclusion.** L'étudiante peut encore dépenser au plus {{xMax}} € cette semaine.`{en}`**Conclusion.** The student may spend at most €{{xMax}} more this week.`
144
+ ::::
145
+
146
+ ::::{weightDistribution}
147
+ :logic: 10
148
+ :abstraction: 15
149
+ :reasoning: 25
150
+ :calculation: 50
151
+ ::::
152
+ :::::
153
+
154
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Prix admissible entre marge minimale et plafond réglementaire`{en}`Admissible price between minimum margin and regulatory ceiling`.md ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :originalExerciseId: 95586bb6-6340-11f1-a8a1-0ed8d3b012a9
3
+ :title: {fr}`Prix admissible entre marge minimale et plafond réglementaire`{en}`Admissible price between minimum margin and regulatory ceiling`
4
+ :modules:
5
+ :recommendedExecutionTime: 20
6
+ :level: Elementary
7
+ :chap:
8
+ :involvedConcepts: upp_and_low, Interval_Notation, Solving_inequalities
9
+ :originalSource:
10
+ :visibility: All
11
+ :variations:
12
+ :comment: Version QCM — borne inférieure (marge) puis encadrement/intervalle semi-ouvert.
13
+ :id: e5eb824f-6e47-11f1-a8a1-0ed8d3b012a9
14
+
15
+ ````{python}
16
+ import random as rd
17
+ import math
18
+ from sympy import Rational
19
+ from pyxiscience.Mes_fctions_generalistes_bis import pxs_config, pxs_nvirgzero
20
+ from pyxiscience.Classes_Extensions import pxs_Interval
21
+
22
+ config_standard = pxs_config()
23
+
24
+ def rat_fr(r):
25
+ r = Rational(r)
26
+ if r.q == 1:
27
+ return str(r.p)
28
+ d, twos, fives = r.q, 0, 0
29
+ while d % 2 == 0:
30
+ d //= 2; twos += 1
31
+ while d % 5 == 0:
32
+ d //= 5; fives += 1
33
+ assert d == 1, f"décimale non finie : {r}"
34
+ k = max(twos, fives)
35
+ scaled = (r.p * 10**k) // r.q
36
+ sign = '-' if scaled < 0 else ''
37
+ s = str(abs(scaled)).rjust(k + 1, '0')
38
+ intpart, frac = s[:-k], s[-k:].rstrip('0')
39
+ return sign + intpart + ('{,}' + frac if frac else '')
40
+
41
+ # Coût d'achat et taux de marge (rationnels exacts)
42
+ for _ in range(2000):
43
+ c = rd.randint(20, 100)
44
+ pct = rd.choice([15, 20, 25, 30, 35, 40, 45, 50])
45
+ taux_marge = Rational(pct, 100)
46
+ coeff_marge = 1 + taux_marge
47
+ borneInf = c * coeff_marge
48
+ lo = math.ceil(borneInf)
49
+ if lo + 5 <= 150:
50
+ plafond = rd.randint(lo + 5, 150)
51
+ break
52
+
53
+ p_exemple = (lo + plafond) // 2
54
+
55
+ # Rendus
56
+ tauxPct = pct
57
+ tauxMargeAff = rat_fr(taux_marge)
58
+ coeffMargeAff = rat_fr(coeff_marge)
59
+ borneInfAff = rat_fr(borneInf)
60
+ pExemple = p_exemple
61
+ encadrAff = f"{borneInfAff} \\leq p < {plafond}"
62
+ if pxs_lang == "fr":
63
+ SAff = pxs_Interval(pxs_nvirgzero(float(borneInf)), plafond, False, True).print().replace(".",",")
64
+ else:
65
+ SAff = pxs_Interval(pxs_nvirgzero(float(borneInf)), plafond, False, True).print()
66
+
67
+ # === Distracteurs MCQ (ajouts ; le code ci-dessus est inchangé) ===
68
+ def _dedl(correct_l, cands, n=3):
69
+ seen = {correct_l}; out = []
70
+ for s in cands:
71
+ if s not in seen:
72
+ seen.add(s); out.append(s)
73
+ if len(out) == n:
74
+ break
75
+ return out
76
+
77
+ # Q0 : borne inférieure
78
+ repBI = borneInfAff
79
+ _cand = [Rational(c), c * taux_marge, c + coeff_marge, 2 * c * coeff_marge, c * (1 - taux_marge)]
80
+ _cs = [rat_fr(v) for v in _cand]
81
+ _d = _dedl(repBI, _cs)
82
+ bi1, bi2, bi3 = _d[0], _d[1], _d[2]
83
+
84
+ # Q1 : intervalle semi-ouvert (ouverture/fermeture des bornes)
85
+ def _iv(ol, oh):
86
+ s = pxs_Interval(pxs_nvirgzero(float(borneInf)), plafond, ol, oh).print()
87
+ if pxs_lang == "fr":
88
+ s = s.replace(".", ",")
89
+ return s
90
+
91
+ SCorrect = SAff
92
+ SD1 = _iv(True, False)
93
+ SD2 = _iv(False, False)
94
+ SD3 = _iv(True, True)
95
+
96
+ globals()
97
+ ````
98
+
99
+ :::::{question}
100
+ :questionType: MCQ
101
+
102
+ ::::{questionStatement}
103
+ {fr}`Une entreprise de distribution fixe le prix de vente unitaire`{en}`A distribution company sets the unit selling price` $p$ {fr}`(en euros) de sorte que la marge soit au moins`{en}`(in euros) so that the margin is at least` ${{ tauxPct }}\,\%$ {fr}`du coût d'achat`{en}`of the purchase cost` $c={{ c }}$ €, {fr}`et que le prix reste inférieur au plafond réglementaire de`{en}`and that the price remains below the regulatory ceiling of` ${{ plafond }}$ €. {fr}`Quelle est la borne inférieure issue de la contrainte de marge,`{en}`What is the lower bound from the margin constraint,` $p\geq {{ coeffMargeAff }}\times {{ c }}$ ?
104
+ ::::
105
+
106
+ ::::{questionHint}
107
+ {fr}`Une marge d'au moins`{en}`A margin of at least` ${{ tauxPct }}\,\%$ {fr}`sur le coût d'achat signifie`{en}`on the purchase cost means` $p\geq c+{{ tauxMargeAff }}\,c={{ coeffMargeAff }}\,c$.
108
+ ::::
109
+
110
+ ::::{mcqAnswer}
111
+ :isRightAnswer: true
112
+ ${{ repBI }}$
113
+ ::::
114
+
115
+ ::::{mcqAnswer}
116
+ :isRightAnswer: false
117
+ ${{ bi1 }}$
118
+ ::::
119
+
120
+ ::::{mcqAnswer}
121
+ :isRightAnswer: false
122
+ ${{ bi2 }}$
123
+ ::::
124
+
125
+ ::::{mcqAnswer}
126
+ :isRightAnswer: false
127
+ ${{ bi3 }}$
128
+ ::::
129
+
130
+ ::::{mcqAnswer}
131
+ :isRightAnswer: false
132
+ {fr}`Aucune de ces réponses n'est correcte`{en}`None of these answers are correct`
133
+ ::::
134
+
135
+ ::::{detailedSolution}
136
+ {fr}`On traduit la contrainte de marge en borne inférieure sur le prix.`{en}`We translate the margin constraint into a lower bound on the price.`
137
+
138
+ \begin{equation*}
139
+ p \geq {{ coeffMargeAff }} \times {{ c }} = {{ borneInfAff }}.
140
+ \end{equation*}
141
+
142
+ {fr}`La borne inférieure est`{en}`The lower bound is` ${{ borneInfAff }}$ €.
143
+ ::::
144
+
145
+ ::::{weightDistribution}
146
+ :logic: 15
147
+ :abstraction: 20
148
+ :reasoning: 30
149
+ :calculation: 35
150
+ ::::
151
+ :::::
152
+
153
+ :::::{question}
154
+ :questionType: MCQ
155
+
156
+ ::::{questionStatement}
157
+ {fr}`On combine la double contrainte (marge et plafond). Quel est l'ensemble des prix admissibles en notation intervalle (en tenant compte du type de chaque borne) ?`{en}`We combine the double constraint (margin and ceiling). What is the set of admissible prices in interval notation (taking into account the type of each bound)?`
158
+ ::::
159
+
160
+ ::::{questionHint}
161
+ {fr}`Combiner`{en}`Combine` $p\geq {{ borneInfAff }}$ {fr}`(marge, borne incluse) et`{en}`(margin, included bound) and` $p<{{ plafond }}$ {fr}`(plafond, borne exclue).`{en}`(ceiling, excluded bound).`
162
+ ::::
163
+
164
+ ::::{mcqAnswer}
165
+ :isRightAnswer: true
166
+ $S = {{ SCorrect }}$
167
+ ::::
168
+
169
+ ::::{mcqAnswer}
170
+ :isRightAnswer: false
171
+ $S = {{ SD1 }}$
172
+ ::::
173
+
174
+ ::::{mcqAnswer}
175
+ :isRightAnswer: false
176
+ $S = {{ SD2 }}$
177
+ ::::
178
+
179
+ ::::{mcqAnswer}
180
+ :isRightAnswer: false
181
+ $S = {{ SD3 }}$
182
+ ::::
183
+
184
+ ::::{mcqAnswer}
185
+ :isRightAnswer: false
186
+ {fr}`Aucune de ces réponses n'est correcte`{en}`None of these answers are correct`
187
+ ::::
188
+
189
+ ::::{detailedSolution}
190
+ {fr}`On rassemble les deux conditions en un encadrement ; aucune opération supplémentaire n'est nécessaire car`{en}`We combine the two conditions into an inequality; no additional operation is necessary since` $p$ {fr}`est déjà isolée.`{en}`is already isolated.`
191
+
192
+ \begin{equation*}
193
+ {{ encadrAff }}.
194
+ \end{equation*}
195
+
196
+ {fr}`L'ensemble des solutions est`{en}`The solution set is`
197
+
198
+ \begin{equation*}
199
+ S = {{ SAff }}.
200
+ \end{equation*}
201
+
202
+ {fr}`La borne`{en}`The bound` ${{ borneInfAff }}$ {fr}`est incluse (un prix de`{en}`is included (a price of` ${{ borneInfAff }}$ € {fr}`satisfait exactement la marge). La borne`{en}`exactly satisfies the margin). The bound` ${{ plafond }}$ {fr}`est exclue (un prix de`{en}`is excluded (a price of` ${{ plafond }}$ € {fr}`violerait le plafond).`{en}`would violate the ceiling).`
203
+ ::::
204
+
205
+ ::::{weightDistribution}
206
+ :logic: 20
207
+ :abstraction: 30
208
+ :reasoning: 30
209
+ :calculation: 20
210
+ ::::
211
+ :::::
212
+
213
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Prix plafond et demande décroissante`{en}`Price ceiling and decreasing demand`.md ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :originalExerciseId: 84168f40-6340-11f1-a8a1-0ed8d3b012a9
3
+ :title: {fr}`Prix plafond et demande décroissante`{en}`Price ceiling and decreasing demand`
4
+ :modules:
5
+ :recommendedExecutionTime: 10
6
+ :level: Elementary
7
+ :chap:
8
+ :involvedConcepts: Solving_inequalities
9
+ :originalSource:
10
+ :visibility: All
11
+ :variations:
12
+ :comment: Version QCM — division par un coefficient négatif (inversion du sens), ensemble en intervalle.
13
+ :id: f2cc923d-6e47-11f1-a8a1-0ed8d3b012a9
14
+
15
+ ````{python}
16
+ import random as rd
17
+ from sympy import oo
18
+ from pyxiscience.Mes_fctions_generalistes_bis import pxs_config, pxsl_latex_coefficient as lc, pxsl_format_number
19
+ from pyxiscience.Classes_Extensions import pxs_Interval
20
+
21
+ config_standard = pxs_config()
22
+
23
+ # Tirage contraint : a divise (p_max - p_plafond)
24
+ for _ in range(2000):
25
+ p_max = rd.randint(80, 200)
26
+ p_plafond = rd.randint(max(40, p_max // 3), p_max - 20)
27
+ diviseurs = [d for d in range(2, 11) if (p_max - p_plafond) % d == 0]
28
+ if diviseurs:
29
+ a = rd.choice(diviseurs)
30
+ break
31
+
32
+ q_min = (p_max - p_plafond) // a
33
+ intermediaire = p_plafond - p_max
34
+
35
+ q_verif_ok = q_min
36
+ q_verif_ko = q_min - 1
37
+ p_verif_ko = p_max - a * q_verif_ko
38
+ intervalAff = pxs_Interval(q_min, oo, False, True).print()
39
+
40
+ # Rendus
41
+ negASignAff = lc(-a, sign=True)
42
+ pMax = p_max
43
+ pPlafond = p_plafond
44
+ negAAff = lc(-a)
45
+ qMinAff = pxsl_format_number(q_min)
46
+ qVerifOk = q_verif_ok
47
+ qVerifKo = q_verif_ko
48
+ pVerifKo = p_verif_ko
49
+
50
+ # === Distracteurs MCQ (ajouts ; le code ci-dessus est inchangé) ===
51
+ intervalCorrect = intervalAff
52
+ intD1 = pxs_Interval(q_min, oo, True, True).print()
53
+ intD2 = pxs_Interval(-oo, q_min, True, False).print()
54
+ intD3 = pxs_Interval(0, q_min, False, False).print()
55
+
56
+ globals()
57
+ ````
58
+
59
+ :::::{question}
60
+ :questionType: MCQ
61
+
62
+ ::::{questionStatement}
63
+ {fr}`Sur un marché, la fonction de demande inverse est`{en}`In a market, the inverse demand function is`
64
+ \begin{equation*}
65
+ p(q) = {{ pMax }} {{ negASignAff }}q
66
+ \end{equation*}
67
+ {fr}`Le prix est en euros,`{en}`The price is in euros,` $q$ {fr}`en milliers d'unités. Un régulateur impose un prix plafond`{en}`in thousands of units. A regulator imposes a price ceiling` $p \leq {{ pPlafond }}$ €.\
68
+ \
69
+ {fr}`En traduisant cette contrainte par une inéquation sur`{en}`Translating this constraint into an inequality involving` $q$ {fr}`et en la résolvant, quel est l'ensemble des quantités compatibles ?`{en}`and solving it, what is the set of compatible quantities?`
70
+ ::::
71
+
72
+ ::::{questionHint}
73
+ {fr}`Substituer`{en}`Substitute` $p(q)$ {fr}`dans`{en}`into` $p \leq {{ pPlafond }}${fr}`, puis isoler`{en}`, then isolate` $q$ {fr}`en divisant par`{en}`by dividing by` ${{ negAAff }} < 0$ {fr}`: le sens s'inverse.`{en}`: the direction reverses.`
74
+ ::::
75
+
76
+ ::::{mcqAnswer}
77
+ :isRightAnswer: true
78
+ $S = {{ intervalCorrect }}$
79
+ ::::
80
+
81
+ ::::{mcqAnswer}
82
+ :isRightAnswer: false
83
+ $S = {{ intD1 }}$
84
+ ::::
85
+
86
+ ::::{mcqAnswer}
87
+ :isRightAnswer: false
88
+ $S = {{ intD2 }}$
89
+ ::::
90
+
91
+ ::::{mcqAnswer}
92
+ :isRightAnswer: false
93
+ $S = {{ intD3 }}$
94
+ ::::
95
+
96
+ ::::{mcqAnswer}
97
+ :isRightAnswer: false
98
+ {fr}`Aucune de ces réponses n'est correcte`{en}`None of these answers are correct`
99
+ ::::
100
+
101
+ ::::{detailedSolution}
102
+ {fr}`On soustrait d'abord une quantité, le sens de l'inégalité est alors conservé, puis on divise par un réel strictement négatif, qui inverse le sens de l'inégalité.`{en}`We first subtract a quantity, the direction of the inequality is then preserved, then we divide by a strictly negative real number, which reverses the direction of the inequality.`
103
+
104
+ \begin{equation*}
105
+ {{ pMax }} {{ negASignAff }}q \leq {{ pPlafond }} &\implies {{ negAAff }}q \leq {{ intermediaire }}\\[10pt]
106
+ &\implies \frac{ {{ negAAff }}q }{ {{ negAAff }} } \geq \frac{ {{ intermediaire }} }{ {{ negAAff }} }\\[10pt]
107
+ &\implies q \geq {{ qMinAff }}.
108
+ \end{equation*}
109
+
110
+ {fr}`L'ensemble des solutions est donc`{en}`The solution set is therefore`
111
+
112
+ \begin{equation*}
113
+ S = {{ intervalAff }}.
114
+ \end{equation*}
115
+
116
+ {fr}`Pour que le prix soit au plus`{en}`For the price to be at most` ${{ pPlafond }}$ €{fr}`, la quantité offerte doit être d'au moins`{en}`, the quantity supplied must be at least` ${{ qMinAff }}$ {fr}`milliers d'unités. Ceci est cohérent car la demande étant décroissante, un prix plus bas correspond à une quantité plus grande.`{en}`thousands of units. This is consistent because since demand is decreasing, a lower price corresponds to a larger quantity.`
117
+ ::::
118
+
119
+ ::::{weightDistribution}
120
+ :logic: 20
121
+ :abstraction: 25
122
+ :reasoning: 35
123
+ :calculation: 20
124
+ ::::
125
+ :::::
126
+
127
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Règle des exposants _ simplification et calcul`{en}`Exponent Rules_ Simplification and Calculation`.md ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :title: {fr}`Règle des exposants : simplification et calcul`{en}`Exponent Rules: Simplification and Calculation`
3
+ :modules:
4
+ :recommendedExecutionTime: 8
5
+ :level: Elementary
6
+ :chap: chap_expLogFunctions_exponentialFunctions_ESCP
7
+ :involvedConcepts:
8
+ :originalSource:
9
+ :visibility: All
10
+ :variations:
11
+ :comment: Échauffement (thème pur) : application de la règle b^(x+y)=b^x·b^y et b^(x−y)=b^x/b^y, en simplification littérale et en vérification numérique.
12
+ :id: 7dff868d-74d9-11f1-a8a1-0ed8d3b012a9
13
+ :originalExerciseId: 6da31517-6597-11f1-a8a1-0ed8d3b012a9
14
+
15
+ ````{python}
16
+ import random as rd
17
+ from fractions import Fraction
18
+ from decimal import Decimal, ROUND_HALF_UP
19
+
20
+ def _grp(ip):
21
+ neg = ip.startswith('-'); ip = ip.lstrip('-'); g = []
22
+ while len(ip) > 3:
23
+ g.insert(0, ip[-3:]); ip = ip[:-3]
24
+ g.insert(0, ip)
25
+ return ('-' if neg else '') + '\\,'.join(g)
26
+
27
+ def num_fr(v, maxdec=5):
28
+ f = Fraction(v)
29
+ d = (Decimal(f.numerator) / Decimal(f.denominator)).quantize(Decimal(1).scaleb(-maxdec), rounding=ROUND_HALF_UP)
30
+ s = format(abs(d), 'f'); ip, _, fp = s.partition('.'); fp = fp.rstrip('0')
31
+ return ('-' if d < 0 else '') + _grp(ip) + (('{,}' + fp) if fp else '')
32
+
33
+ # Q1 : simplification symbolique (exposants >= 2 partout)
34
+ sommeA = rd.randint(5, 10)
35
+ diffB = rd.randint(3, 6)
36
+ exp1 = rd.randint(2, sommeA - 2)
37
+ exp2 = sommeA - exp1
38
+ exp4 = rd.randint(2, 5)
39
+ exp3 = exp4 + diffB
40
+ aAff = 'b^{%d}' % sommeA
41
+ bAff = 'b^{%d}' % diffB
42
+
43
+ # Q2 : vérification numérique
44
+ baseC = rd.choice([2, 3])
45
+ sommeC = rd.randint(5, 8)
46
+ exp5 = rd.randint(2, sommeC - 2)
47
+ exp6 = sommeC - exp5
48
+ baseDNum = rd.choice([3, 5]) # base_D = 3/2 ou 5/2
49
+ baseD = Fraction(baseDNum, 2)
50
+ diffD = rd.randint(2, 4)
51
+ exp8 = rd.randint(2, 4)
52
+ exp7 = exp8 + diffD
53
+
54
+ cVal = baseC ** sommeC
55
+ dVal = baseD ** diffD
56
+
57
+ # Rendus
58
+ baseDAff = '%d{,}5' % (baseDNum // 2) # 3/2 -> 1{,}5 ; 5/2 -> 2{,}5
59
+ dValAff = num_fr(dVal, 5)
60
+
61
+ # === Ajouts conversion FGQ ===
62
+ def _solnum(x):
63
+ return x.replace("\\,", "").replace("{,}", ".")
64
+ dValSol = _solnum(dValAff)
65
+ globals()
66
+ ````
67
+
68
+ :::::{question}
69
+ :questionType: FGQ
70
+ :questionId: 0
71
+ :questionIndex: 0
72
+ :solution: [["ord","$b^{ {{ sommeA }} }$","$b^{ {{ diffB }} }$"],["0","0"]]
73
+
74
+ ::::{questionStatement}
75
+ {fr}`Soit`{en}`Let` $b>0$, $b\neq 1$. {fr}`Simplifier`{en}`Simplify` $A=b^{ {{exp1}} }\cdot b^{ {{exp2}} }$ {fr}`et`{en}`and` $B=\dfrac{b^{ {{exp3}} }}{b^{ {{exp4}} }}$ {fr}`à l'aide de la règle des exposants.`{en}`using the exponent rules.`
76
+
77
+ {fr}`$A$ se simplifie en`{en}`$A$ simplifies to` {input}`||110` {fr}`et $B$ en`{en}`and $B$ to` {input}`||110`
78
+ ::::
79
+
80
+ ::::{questionHint}
81
+ $b^{x}\cdot b^{y}=b^{x+y}$ {fr}`et`{en}`and` $\dfrac{b^{x}}{b^{y}}=b^{x-y}$.
82
+ ::::
83
+
84
+ ::::{displayedSolution}
85
+ $A = b^{ {{ sommeA }} }$ $\qquad B = b^{ {{ diffB }} }$
86
+ ::::
87
+
88
+ ::::{detailedSolution}
89
+ {fr}`Par addition des exposants pour le produit, et soustraction pour le quotient :`{en}`By adding exponents for the product, and subtracting for the quotient:`
90
+
91
+ \begin{equation*}
92
+ A &= b^{ {{exp1}} }\cdot b^{ {{exp2}} } \\
93
+ &= b^{ {{exp1}} + {{exp2}} } \\
94
+ &= {{ aAff }}.
95
+ \end{equation*}
96
+
97
+ \begin{equation*}
98
+ B &= \frac{b^{ {{exp3}} }}{b^{ {{exp4}} }} \\
99
+ &= b^{ {{exp3}} - {{exp4}} } \\
100
+ &= {{ bAff }}.
101
+ \end{equation*}
102
+ ::::
103
+
104
+ ::::{weightDistribution}
105
+ :logic: 20
106
+ :abstraction: 30
107
+ :reasoning: 25
108
+ :calculation: 25
109
+ ::::
110
+ :::::
111
+
112
+ :::::{question}
113
+ :questionType: FGQ
114
+ :questionId: 1
115
+ :questionIndex: 1
116
+ :solution: [["ord","${{ cVal }}$","${{ dValSol }}$"],["0","0"]]
117
+
118
+ ::::{questionStatement}
119
+ {fr}`Calculer numériquement`{en}`Compute numerically` $C={{ baseC }}^{ {{exp5}} }\cdot {{ baseC }}^{ {{exp6}} }$ {fr}`et vérifier que`{en}`and verify that` $C={{ baseC }}^{ {{sommeC}} }$, {fr}`puis`{en}`then` $D=\dfrac{({{ baseDAff }})^{ {{exp7}} }}{({{ baseDAff }})^{ {{exp8}} }}$ {fr}`et vérifier que`{en}`and verify that` $D=({{ baseDAff }})^{ {{diffD}} }$.
120
+
121
+ $C =$ {input}`||80` $\qquad D =$ {input}`||80`
122
+ ::::
123
+
124
+ ::::{questionHint}
125
+ {fr}`Calculer chaque puissance, puis comparer au résultat attendu par la règle des exposants.`{en}`Compute each power, then compare to the expected result using the exponent rules.`
126
+ ::::
127
+
128
+ ::::{displayedSolution}
129
+ $C = {{ cVal }}$ $\qquad D = {{ dValAff }}$
130
+ ::::
131
+
132
+ ::::{detailedSolution}
133
+ {fr}`Pour`{en}`For` $C$ :
134
+
135
+ \begin{equation*}
136
+ C &= {{ baseC }}^{ {{exp5}} }\cdot {{ baseC }}^{ {{exp6}} } \\
137
+ &= {{ baseC }}^{ {{exp5}} + {{exp6}} } \\
138
+ &= {{ baseC }}^{ {{sommeC}} } \\
139
+ &= {{ cVal }}.
140
+ \end{equation*}
141
+
142
+ {fr}`Pour`{en}`For` $D$ :
143
+
144
+ \begin{equation*}
145
+ D &= \frac{({{ baseDAff }})^{ {{exp7}} }}{({{ baseDAff }})^{ {{exp8}} }} \\
146
+ &= ({{ baseDAff }})^{ {{exp7}} - {{exp8}} } \\
147
+ &= ({{ baseDAff }})^{ {{diffD}} } \\
148
+ &= {{ dValAff }}.
149
+ \end{equation*}
150
+
151
+ {fr}`Les deux vérifications sont cohérentes avec la règle des exposants.`{en}`Both verifications are consistent with the exponent rules.`
152
+ ::::
153
+
154
+ ::::{weightDistribution}
155
+ :logic: 15
156
+ :abstraction: 25
157
+ :reasoning: 25
158
+ :calculation: 35
159
+ ::::
160
+ :::::
161
+
162
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Résolution de`{en}`Resolution of` $_ax+b_ = k$ {fr}`avec`{en}`with` $k _ 0$.md ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :title: {fr}`Résolution de`{en}`Resolution of` $|ax+b| = k$ {fr}`avec`{en}`with` $k > 0$
3
+ :modules:
4
+ :recommendedExecutionTime: 5
5
+ :level: Elementary
6
+ :chap: chap_equations_Inequalities_absoluteValue_ESCP
7
+ :involvedConcepts: Abs_Val, Simplifying_Algebraic_Expressions, Solving_equalities
8
+ :originalSource: Erwan Lamy, ESCP Business School
9
+ :visibility: All
10
+ :variations:
11
+ :comment: Échauffement 2/10 — résolution d'une équation à valeur absolue, cas k > 0 (deux cas).
12
+ :id: dafee084-70aa-11f1-a8a1-0ed8d3b012a9
13
+ :originalExerciseId: 004ee5bb-634d-11f1-a8a1-0ed8d3b012a9
14
+
15
+ ````{python}
16
+ import random as rd
17
+ from math import gcd
18
+ from sympy import Rational, latex
19
+ from pyxiscience.Mes_fctions_generalistes_bis import pxs_config, pxsl_latex_coefficient as lc
20
+ config_standard = pxs_config()
21
+
22
+ # Équation |b - a x| = k, k > 0.
23
+ # Cas 1 : b - a x = k -> x = (b - k)/a (fraction irréductible, dénominateur >= 2)
24
+ # Cas 2 : b - a x = -k -> x = (b + k)/a (entier par construction)
25
+ for _ in range(200):
26
+ a = rd.randint(2, 9)
27
+ b = rd.randint(5, 15)
28
+ k = rd.randint(2, min(10, b - 1))
29
+ if (b + k) % a == 0 and (b - k) != 0 and gcd(b - k, a) == 1:
30
+ break
31
+
32
+ sol1 = Rational(b - k, a) # exact, irréductible, q >= 2
33
+ sol2 = (b + k) // a # entier
34
+
35
+ aAff = str(a)
36
+ bAff = str(b)
37
+ kAff = str(k)
38
+ negKAff = str(-k)
39
+ coefAff = lc(-a, sign=True) # coefficient signé du terme en x : "- a"
40
+ sol1Aff = latex(sol1) # fraction irréductible
41
+ sol1NumAff = str(b - k)
42
+ sol1DenAff = str(a)
43
+ sol2Aff = str(sol2)
44
+ bPlusKAff = str(b + k) # = a * sol2
45
+ aTimesSol1Aff = str(b - k) # a * sol1
46
+ bMinusASol2Aff = str(b - a * sol2) # = -k
47
+
48
+ globals()
49
+ ````
50
+
51
+ :::::{question}
52
+ :questionType: FGQ
53
+ :questionId: 0
54
+ :questionIndex: 0
55
+ :solution: [["notord","${{ sol1Aff }}$","${{ sol2Aff }}$"],["0","0"]]
56
+
57
+ ::::{questionStatement}
58
+ {fr}`**Résolution d'une équation $|ax + b| = k$ avec $k > 0$.**` {en}`**Solving an equation $|ax + b| = k$ with $k > 0$.**`
59
+
60
+ {fr}`Résoudre l'équation suivante et donner ses deux solutions :` {en}`Solve the following equation and give its two solutions:`
61
+
62
+ \begin{equation*}
63
+ |{{ bAff }} {{ coefAff }}x| = {{ kAff }}.
64
+ \end{equation*}
65
+
66
+ $x =$ {input}`||120` {fr}`ou`{en}`or` $x =$ {input}`||120`
67
+ ::::
68
+
69
+ ::::{questionHint}
70
+ {fr}`Lorsque $k > 0$, l'équation $|f(x)| = k$ se scinde en deux cas : $f(x) = k$ ou $f(x) = -k$.`
71
+
72
+ {en}`When $k > 0$, the equation $|f(x)| = k$ splits into two cases: $f(x) = k$ or $f(x) = -k$.`
73
+ ::::
74
+
75
+ ::::{displayedSolution}
76
+ $\ds x = {{ sol1Aff }}$ {fr}`ou`{en}`or` $\ds x = {{ sol2Aff }}$
77
+ ::::
78
+
79
+ ::::{detailedSolution}
80
+ {fr}`L'équation proposée est $|{{bAff}} {{coefAff}}x| = {{kAff}}$ avec ${{kAff}} > 0$.`
81
+
82
+ {en}`The given equation is $|{{bAff}} {{coefAff}}x| = {{kAff}}$ with ${{kAff}} > 0$.`
83
+
84
+ {fr}`Par définition de la valeur absolue, cette équation se décompose en deux situations distinctes :`{en}`By definition of absolute value, this equation splits into two distinct cases:`
85
+
86
+ $\phantom{-}$
87
+
88
+ {fr}`$\bullet$ **Premier cas :** ${{bAff}} {{coefAff}}x = {{kAff}}$`
89
+
90
+ {en}`$\bullet$ **First case:** ${{bAff}} {{coefAff}}x = {{kAff}}$`
91
+
92
+ {fr}`On isole $x$ en regroupant les termes constants :`
93
+ {en}`We isolate $x$ by grouping the constant terms:`
94
+
95
+ \begin{equation*}
96
+ {{bAff}} {{coefAff}}x = {{kAff}} \quad &\Longleftrightarrow \quad {{aAff}}x = {{sol1NumAff}} \quad \\[6pt]
97
+ &\Longleftrightarrow \quad x = \frac{{{sol1NumAff}}}{{{sol1DenAff}}}.
98
+ \end{equation*}
99
+
100
+ {fr}`$\bullet$ **Deuxième cas :** ${{bAff}} {{coefAff}}x = {{negKAff}}$`
101
+
102
+ {en}`$\bullet$ **Second case:** ${{bAff}} {{coefAff}}x = {{negKAff}}$`
103
+
104
+ {fr}`On isole $x$ de la même façon :`
105
+ {en}`We isolate $x$ in the same way:`
106
+
107
+ \begin{equation*}
108
+ {{bAff}} {{coefAff}}x = {{negKAff}} \quad &\Longleftrightarrow \quad {{aAff}}x = {{bPlusKAff}} \quad \\[6pt]
109
+ &\Longleftrightarrow \quad x = {{sol2Aff}}.
110
+ \end{equation*}
111
+
112
+ {fr}`**Vérification des deux solutions.**`
113
+
114
+ {en}`**Verification of both solutions.**`
115
+
116
+ {fr}`Pour $x = \dfrac{{{sol1NumAff}}}{{{sol1DenAff}}}$ :`
117
+ {en}`For $x = \dfrac{{{sol1NumAff}}}{{{sol1DenAff}}}$:`
118
+
119
+ \begin{equation*}
120
+ \left|{{bAff}} {{coefAff}}\times\frac{{{sol1NumAff}}}{{{sol1DenAff}}}\right| &= \left|{{bAff}} - {{aAff}}\times\frac{{{sol1NumAff}}}{{{sol1DenAff}}}\right| \\[10pt]
121
+ &= \left|{{bAff}} - {{aTimesSol1Aff}}\right| \\[10pt]
122
+ &= {{kAff}}.
123
+ \end{equation*}
124
+
125
+ {fr}`Pour $x = {{sol2Aff}}$ :`
126
+ {en}`For $x = {{sol2Aff}}$:`
127
+
128
+ \begin{equation*}
129
+ \left|{{bAff}} {{coefAff}}\times{{sol2Aff}}\right| &= \left|{{bAff}} - {{aAff}}\times{{sol2Aff}}\right| \\[10pt]
130
+ &= \left|{{bMinusASol2Aff}}\right| \\[10pt]
131
+ &= {{kAff}}.
132
+ \end{equation*}
133
+
134
+ {fr}`Les deux valeurs vérifient l'équation initiale.`
135
+
136
+ {en}`Both values satisfy the original equation.`
137
+
138
+ {fr}`**Conclusion :** L'ensemble des solutions est $\boxed{\left\{\dfrac{{{sol1NumAff}}}{{{sol1DenAff}}},\, {{sol2Aff}}\right\}}$.`
139
+
140
+ {en}`**Conclusion:** The solution set is $\boxed{\left\{\dfrac{{{sol1NumAff}}}{{{sol1DenAff}}},\, {{sol2Aff}}\right\}}$.`
141
+ ::::
142
+
143
+ ::::{weightDistribution}
144
+ :logic: 10
145
+ :abstraction: 15
146
+ :reasoning: 25
147
+ :calculation: 50
148
+ ::::
149
+ :::::
150
+
151
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Seuil de rentabilité d'une boutique`{en}`Break-Even Point of a Store`.md ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :title: {fr}`Seuil de rentabilité d'une boutique`{en}`Break-Even Point of a Store`
3
+ :modules:
4
+ :recommendedExecutionTime: 15
5
+ :level: Elementary
6
+ :chap: chap_lines_Systems_economicEquilibriumBreakeven_ESCP
7
+ :involvedConcepts:
8
+ :originalSource: Session 4 — Erwan Lamy, ESCP Business School
9
+ :visibility: All
10
+ :variations:
11
+ :comment: Appliqué (gestion) : revenu total, coût total, profit, seuil de rentabilité, marge sur coût variable.
12
+ :id: 33724a3b-74c0-11f1-a8a1-0ed8d3b012a9
13
+ :originalExerciseId: 10e7b53f-64e3-11f1-a8a1-0ed8d3b012a9
14
+
15
+ ````{python}
16
+ import random as rd
17
+ from pyxiscience.Mes_fctions_generalistes_bis import pxs_config, pxsl_latex_coefficient as lc, pxsl_format_number
18
+ # --- Helpers PyxiScience (definis inline, autonomes : aucun import externe) ---
19
+
20
+ config_standard = pxs_config()
21
+
22
+ # p > c (marge positive) ; y_FC multiple de 50 divisible par la marge (seuil entier)
23
+ for _ in range(300):
24
+ p = rd.randint(20, 50)
25
+ c = rd.randint(5, 15)
26
+ if p <= c:
27
+ continue
28
+ marge = p - c
29
+ yFc = rd.randint(6, 16) * 50
30
+ if yFc % marge == 0:
31
+ break
32
+
33
+ qStar = yFc // marge
34
+ qTest = qStar + rd.randint(5, 20)
35
+ profitQtest = marge * qTest - yFc
36
+
37
+ # Affichage
38
+ yFcAff = pxsl_format_number(yFc)
39
+ profitQtestAff = pxsl_format_number(profitQtest)
40
+
41
+ globals()
42
+ ````
43
+
44
+ {fr}`Une boutique vend des écharpes faites main au prix unitaire `{en}`A shop sells handmade scarves at the unit price `$p = {{p}}${fr}` €. Le coût variable unitaire est `{en}` €. The unit variable cost is `$c = {{c}}${fr}` € et le coût fixe mensuel `{en}` € and the monthly fixed cost `$y_{FC} = {{yFcAff}}$ €.
45
+
46
+ :::::{question}
47
+ :questionType: FGQ
48
+ :questionId: 0
49
+ :questionIndex: 0
50
+ :solution: [["ord","${{ p }}$","${{ c }}$","${{ yFcAff }}$"],["0","0","0"]]
51
+
52
+ ::::{questionStatement}
53
+ {fr}`Écrire le revenu total `{en}`Write the total revenue `$y_{TR}(q)${fr}` et le coût total `{en}` and the total cost `$y_{TC}(q)$.
54
+ - $y_{TR}(q) =$ {input}`||80` $q$
55
+ - $y_{TC}(q) =$ {input}`||80` $q +$ {input}`||80`
56
+ ::::
57
+
58
+ ::::{questionHint}
59
+ $y_{TR} = p\,q${fr}` et `{en}` and `$y_{TC} = c\,q + y_{FC}$.
60
+ ::::
61
+
62
+ ::::{displayedSolution}
63
+ $y_{TR}(q) = {{ p }}q$ — $y_{TC}(q) = {{ c }}q + {{ yFcAff }}$
64
+ ::::
65
+
66
+ ::::{detailedSolution}
67
+ {fr}`On applique les définitions :`{en}`We apply the definitions:`
68
+
69
+ \begin{equation*}
70
+ y_{TR}(q) = {{p}}\,q, \qquad y_{TC}(q) = {{c}}\,q + {{yFcAff}}.
71
+ \end{equation*}
72
+ ::::
73
+
74
+ ::::{weightDistribution}
75
+ :logic: 10
76
+ :abstraction: 25
77
+ :reasoning: 30
78
+ :calculation: 35
79
+ ::::
80
+ :::::
81
+
82
+ :::::{question}
83
+ :questionType: FGQ
84
+ :questionId: 1
85
+ :questionIndex: 1
86
+ :solution: [["ord","${{ marge }}$","${{ yFcAff }}$"],["0","0"]]
87
+
88
+ ::::{questionStatement}
89
+ {fr}`Calculer le profit `{en}`Compute the profit `$\Pi(q) = y_{TR}(q) - y_{TC}(q)${fr}` et simplifier.`{en}` and simplify.`\
90
+ \
91
+ $\Pi(q) =$ {input}`||80` $q -$ {input}`||80`
92
+ ::::
93
+
94
+ ::::{questionHint}
95
+ {fr}`Soustraire le coût total du revenu total, puis regrouper les termes en `{en}`Subtract the total cost from the total revenue, then group the terms in `$q$.
96
+ ::::
97
+
98
+ ::::{displayedSolution}
99
+ $\Pi(q) = {{ marge }}q - {{ yFcAff }}$
100
+ ::::
101
+
102
+ ::::{detailedSolution}
103
+ {fr}`On développe :`{en}`We expand:`
104
+
105
+ \begin{equation*}
106
+ \Pi(q) = {{p}}\,q - ({{c}}\,q + {{yFcAff}}) = {{marge}}\,q - {{yFcAff}}.
107
+ \end{equation*}
108
+
109
+ {fr}`Le profit est affine, de coefficient directeur `{en}`The profit is affine, with slope `${{marge}}${fr}` : c'est la marge sur coût variable `{en}`: it is the contribution margin `$p - c = {{p}} - {{c}} = {{marge}}${fr}` € par écharpe.`{en}` € per scarf.`
110
+ ::::
111
+
112
+ ::::{weightDistribution}
113
+ :logic: 15
114
+ :abstraction: 25
115
+ :reasoning: 25
116
+ :calculation: 35
117
+ ::::
118
+ :::::
119
+
120
+ :::::{question}
121
+ :questionType: FGQ
122
+ :questionId: 2
123
+ :questionIndex: 2
124
+ :solution: [["ord","${{ qStar }}$"],["0"]]
125
+
126
+ ::::{questionStatement}
127
+ {fr}`Déterminer la quantité de seuil de rentabilité `{en}`Determine the break-even quantity `$q^{*}${fr}` en résolvant `{en}` by solving `$y_{TR}(q) = y_{TC}(q)$.\
128
+ \
129
+ $q^*$ = {input}`||80`
130
+ ::::
131
+
132
+ ::::{questionHint}
133
+ {fr}`Au seuil de rentabilité, le profit est nul : `{en}`At the break-even point, the profit is zero: `$\Pi(q) = 0$.
134
+ ::::
135
+
136
+ ::::{displayedSolution}
137
+ $q^* = {{ qStar }}$
138
+ ::::
139
+
140
+ ::::{detailedSolution}
141
+ {fr}`On résout `{en}`We solve `$\Pi(q) = 0$ :
142
+
143
+ \begin{equation*}
144
+ {{marge}}\,q - {{yFcAff}} = 0 \implies {{marge}}\,q = {{yFcAff}} \implies q^{*} = {{qStar}}.
145
+ \end{equation*}
146
+
147
+ {fr}`La boutique doit vendre au moins `{en}`The shop must sell at least `${{qStar}}${fr}` écharpes par mois pour couvrir ses coûts.`{en}` scarves per month to cover its costs.`
148
+ ::::
149
+
150
+ ::::{weightDistribution}
151
+ :logic: 15
152
+ :abstraction: 20
153
+ :reasoning: 30
154
+ :calculation: 35
155
+ ::::
156
+ :::::
157
+
158
+ :::::{question}
159
+ :questionType: FGQ
160
+ :questionId: 3
161
+ :questionIndex: 3
162
+ :solution: [["ord","${{ profitQtestAff }}$"],["0"]]
163
+
164
+ ::::{questionStatement}
165
+ {fr}`Calculer le profit pour `{en}`Compute the profit for `$q = {{qTest}}${fr}` écharpes vendues. La boutique est-elle rentable à ce niveau ?`{en}` scarves sold. Is the shop profitable at this level?`\
166
+ \
167
+ $\Pi({{ qTest }})$ = {input}`||80`
168
+ ::::
169
+
170
+ ::::{questionHint}
171
+ {fr}`Évaluer `{en}`Evaluate `$\Pi({{qTest}})${fr}` et comparer `{en}` and compare `${{qTest}}${fr}` au seuil `{en}` with the break-even `$q^{*}$.
172
+ ::::
173
+
174
+ ::::{displayedSolution}
175
+ $\Pi({{ qTest }}) = {{ profitQtestAff }}$
176
+ ::::
177
+
178
+ ::::{detailedSolution}
179
+ {fr}`On évalue le profit en `{en}`We evaluate the profit at `$q = {{qTest}}$ :
180
+
181
+ \begin{equation*}
182
+ \Pi({{qTest}}) = {{marge}} \times {{qTest}} - {{yFcAff}} = {{profitQtestAff}}.
183
+ \end{equation*}
184
+
185
+ {fr}`soit`{en}`i.e.` ${{ profitQtestAff }}$ {fr}`€.`{en}`€.`
186
+
187
+ {fr}`Comme `{en}`Since `${{qTest}} > q^{*} = {{qStar}}${fr}`, la boutique est rentable et dégage un profit mensuel de `{en}`, the shop is profitable and earns a monthly profit of `${{profitQtestAff}}$ €.
188
+ ::::
189
+
190
+ ::::{weightDistribution}
191
+ :logic: 15
192
+ :abstraction: 20
193
+ :reasoning: 30
194
+ :calculation: 35
195
+ ::::
196
+ :::::
197
+
198
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Simplifier des expressions avec exponentielle et logarithme`{en}`Simplify expressions with exponential and logarithm`.md ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :title: {fr}`Simplifier des expressions avec exponentielle et logarithme`{en}`Simplify expressions with exponential and logarithm`
3
+ :modules:
4
+ :recommendedExecutionTime: 10
5
+ :level: Elementary
6
+ :chap: chap_expLogFunctions_logarithmicFunctions_ESCP
7
+ :involvedConcepts:
8
+ :originalSource:
9
+ :visibility: All
10
+ :variations:
11
+ :comment: Échauffement (thème pur) : simplification via les relations réciproques e^(ln y)=y, ln(e^x)=x, b^(log_b y)=y, log_b(b^x)=x, y compris en composition. Aucune loi algébrique du logarithme.
12
+ :originalExerciseId: 2f1a6101-6599-11f1-a8a1-0ed8d3b012a9
13
+ :id: 07721958-74bf-11f1-a8a1-0ed8d3b012a9
14
+
15
+ ````{python}
16
+ import random as rd
17
+
18
+ # Q1 : e^(ln y1)=y1, ln(e^x1)=x1, b1^(log_b1 y2)=y2
19
+ y1 = rd.randint(2, 20)
20
+ x1 = rd.randint(2, 15)
21
+ b1 = rd.choice([2, 3, 4, 5, 6, 7, 8, 9])
22
+ y2 = rd.randint(2, 20)
23
+ r1, r2, r3 = y1, x1, y2
24
+
25
+ # Q2 : log_b2(b2^x2)=x2 (x2 != 0, 1 pour éviter ^1), e^(ln(e^x3))=e^x3
26
+ b2 = rd.choice([2, 3, 4, 5, 6, 7, 8, 9])
27
+ x2 = rd.choice(list(range(-10, 0)) + list(range(2, 11)))
28
+ x3 = rd.randint(2, 10)
29
+ r4 = x2
30
+ r5Aff = "e^{%d}" % x3
31
+
32
+ globals()
33
+ ````
34
+
35
+ :::::{question}
36
+ :questionType: FGQ
37
+ :solution: [["ord","{{ r1 }}","{{ r2 }}","{{ r3 }}"],["0","0","0"]]
38
+
39
+ ::::{questionStatement}
40
+ {fr}`Simplifier`{en}`Simplify` $e^{\ln {{ y1 }}}$, $\ln(e^{ {{ x1 }} })$ {fr}`et`{en}`and` ${{ b1 }}^{\log_{ {{ b1 }} }({{ y2 }})}$.
41
+
42
+ $e^{\ln {{ y1 }}} =$ {input}`||70`
43
+
44
+ $\ln(e^{ {{ x1 }} }) =$ {input}`||70`
45
+
46
+ ${{ b1 }}^{\log_{ {{ b1 }} }({{ y2 }})} =$ {input}`||70`
47
+ ::::
48
+
49
+ ::::{questionHint}
50
+ $e^{\ln y}=y$, $\ln(e^{x})=x$, $b^{\log_{b} y}=y$.
51
+ ::::
52
+
53
+ ::::{displayedSolution}
54
+ $e^{\ln {{ y1 }}} = {{ r1 }}$
55
+
56
+ $\ln(e^{ {{ x1 }} }) = {{ r2 }}$
57
+
58
+ ${{ b1 }}^{\log_{ {{ b1 }} }({{ y2 }})} = {{ r3 }}$
59
+ ::::
60
+
61
+ ::::{detailedSolution}
62
+ {fr}`Par les relations réciproques :`{en}`By the reciprocal relations:` $e^{\ln {{ y1 }}}={{ r1 }}$, $\quad \ln(e^{ {{ x1 }} })={{ r2 }}$, $\quad {{ b1 }}^{\log_{ {{ b1 }} }({{ y2 }})}={{ r3 }}$.
63
+
64
+ {fr}`(respectivement`{en}`(respectively` $e^{\ln y}=y$ {fr}`avec`{en}`with` $y={{ y1 }}$ ; $\ln(e^{x})=x$ {fr}`avec`{en}`with` $x={{ x1 }}$ ; $b^{\log_{b} y}=y$ {fr}`avec`{en}`with` $b={{ b1 }}$, $y={{ y2 }}$.)
65
+ ::::
66
+
67
+ ::::{weightDistribution}
68
+ :logic: 20
69
+ :abstraction: 30
70
+ :reasoning: 30
71
+ :calculation: 20
72
+ ::::
73
+ :::::
74
+
75
+ :::::{question}
76
+ :questionType: FGQ
77
+ :solution: [["ord","{{ r4 }}","${{ r5Aff }}$"],["0","0"]]
78
+
79
+ ::::{questionStatement}
80
+ {fr}`Simplifier`{en}`Simplify` $\log_{ {{ b2 }} }({{ b2 }}^{ {{ x2 }} })$ {fr}`et`{en}`and` $e^{\ln(e^{ {{ x3 }} })}$.
81
+
82
+ $\log_{ {{ b2 }} }({{ b2 }}^{ {{ x2 }} }) =$ {input}`||70`
83
+
84
+ $e^{\ln(e^{ {{ x3 }} })} =$ {input}`||90`
85
+ ::::
86
+
87
+ ::::{questionHint}
88
+ {fr}`Pour la seconde, simplifier de l'intérieur vers l'extérieur : d'abord`{en}`For the second, simplify from the inside out: first` $\ln(e^{ {{ x3 }} })$.
89
+ ::::
90
+
91
+ ::::{displayedSolution}
92
+ $\log_{ {{ b2 }} }({{ b2 }}^{ {{ x2 }} }) = {{ r4 }}$
93
+
94
+ $e^{\ln(e^{ {{ x3 }} })} = {{ r5Aff }}$
95
+ ::::
96
+
97
+ ::::{detailedSolution}
98
+ {fr}`Par`{en}`By` $\log_{b}(b^{x})=x$ :
99
+
100
+ \begin{equation*}
101
+ \log_{ {{ b2 }} }({{ b2 }}^{ {{ x2 }} }) = {{ r4 }}.
102
+ \end{equation*}
103
+
104
+ {fr}`Pour la seconde,`{en}`For the second,` $\ln(e^{ {{ x3 }} })={{ x3 }}$, {fr}`puis`{en}`then` $e^{\ln(e^{ {{ x3 }} })}=e^{ {{ x3 }} }$ {fr}`(relation`{en}`(relation` $e^{\ln y}=y$ {fr}`avec`{en}`with` $y=e^{ {{ x3 }} }>0$) :
105
+
106
+ \begin{equation*}
107
+ e^{\ln(e^{ {{ x3 }} })} = {{ r5Aff }}.
108
+ \end{equation*}
109
+ ::::
110
+
111
+ ::::{weightDistribution}
112
+ :logic: 20
113
+ :abstraction: 35
114
+ :reasoning: 30
115
+ :calculation: 15
116
+ ::::
117
+ :::::
118
+
119
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Symétries d'une courbe de demande et restriction économique`{en}`Symmetries of a demand curve and economic restriction`.md ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :title: {fr}`Symétries d'une courbe de demande et restriction économique`{en}`Symmetries of a demand curve and economic restriction`
3
+ :modules:
4
+ :recommendedExecutionTime: 18
5
+ :level: Elementary
6
+ :chap: chap_realFunctions_Graphs_graphSymmetries_ESCP
7
+ :involvedConcepts:
8
+ :originalSource:
9
+ :visibility: All
10
+ :variations:
11
+ :comment: Appliqué (économie & gestion) : quatre tests de symétrie sur une courbe de demande implicite (cercle), puis restriction au premier quadrant et identification de la seule symétrie pertinente (y=x), en reconnaissance uniquement.
12
+ :id: 64470bcd-74b4-11f1-a8a1-0ed8d3b012a9
13
+ :originalExerciseId: e6ac5c47-6409-11f1-a8a1-0ed8d3b012a9
14
+
15
+ ````{python}
16
+ import random as rd
17
+ from pyxiscience.Mes_fctions_generalistes_bis import pxs_config
18
+
19
+ config_standard = pxs_config()
20
+
21
+ rayon = rd.randint(3, 10)
22
+ rayon_carre = rayon**2
23
+
24
+ if pxs_lang == "fr":
25
+ unite_demande = rd.choice(["centaines d'unités", "milliers d'unités", "dizaines d'unités"])
26
+ unite_prix = rd.choice(["dizaines d'euros", "centaines d'euros", "euros"])
27
+ type_produit = rd.choice(["produit de luxe", "produit technologique", "bien de consommation", "service premium"])
28
+ else:
29
+ unite_demande = rd.choice(["hundreds of units", "thousands of units", "tens of units"])
30
+ unite_prix = rd.choice(["tens of dollars", "hundreds of dollars", "dollars"])
31
+ type_produit = rd.choice(["luxury product", "technological product", "consumer good", "premium service"])
32
+
33
+ # Rendus
34
+ rayonCarre = rayon_carre
35
+ uniteDemande = unite_demande
36
+ unitePrix = unite_prix
37
+ typeProduit = type_produit
38
+
39
+ globals()
40
+ ````
41
+
42
+ :::::{question}
43
+ :questionType: MCQ
44
+ :questionId: 0
45
+ :questionIndex: 0
46
+
47
+ ::::{questionStatement}
48
+ {fr}`Un directeur commercial étudie la courbe de demande d'un`{en}`A sales director studies the demand curve of a` {{ typeProduit }} {fr}`: la demande`{en}`: the demand` $D>0$ {fr}`(en`{en}`(in` {{ uniteDemande }}) {fr}`et le prix`{en}`and the price` $p>0$ {fr}`(en`{en}`(in` {{ unitePrix }}) {fr}`vérifient`{en}`satisfy` $p^2+D^2={{ rayonCarre }}$. {fr}`Appliquer les quatre tests algébriques de symétrie (par rapport à l'axe`{en}`Apply the four algebraic symmetry tests (with respect to the axis` $Op$, {fr}`à l'axe`{en}`to the axis` $OD$, {fr}`à l'origine, à la droite`{en}`to the origin, to the line` $D=p$) {fr}`à la courbe complète et conclure pour chacun.`{en}`to the complete curve and conclude for each.`
49
+ ::::
50
+
51
+ ::::{questionHint}
52
+ {fr}`Substituer dans l'équation : les carrés rendent`{en}`Substitute in the equation: the squares make` $p^2$ {fr}`et`{en}`and` $D^2$ {fr}`inchangés par changement de signe ; échanger`{en}`unchanged by sign change; exchanging` $p$ {fr}`et`{en}`and` $D$ {fr}`ne change pas une somme.`{en}`does not change a sum.`
53
+ ::::
54
+
55
+ ::::{mcqAnswer}
56
+ :isRightAnswer: true
57
+ {fr}`Les quatre :`{en}`All four:` $Op$, $OD$, {fr}`l'origine et la droite`{en}`the origin, and the line` $D=p$
58
+ ::::
59
+
60
+ ::::{mcqAnswer}
61
+ :isRightAnswer: false
62
+ {fr}`Seulement les axes`{en}`Only the axes` $Op$ {fr}`et`{en}`and` $OD$
63
+ ::::
64
+
65
+ ::::{mcqAnswer}
66
+ :isRightAnswer: false
67
+ {fr}`Seulement la droite`{en}`Only the line` $D=p$
68
+ ::::
69
+
70
+ ::::{mcqAnswer}
71
+ :isRightAnswer: false
72
+ {fr}`Seulement l'origine`{en}`Only the origin`
73
+ ::::
74
+
75
+ ::::{mcqAnswer}
76
+ :isRightAnswer: false
77
+ {fr}`Aucune de ces réponses n'est correcte`{en}`None of these answers are correct`
78
+ ::::
79
+
80
+ ::::{detailedSolution}
81
+ {fr}`La courbe complète`{en}`The complete curve` $p^2+D^2={{ rayonCarre }}$ {fr}`est un cercle de centre`{en}`is a circle with center` $O$ {fr}`et de rayon`{en}`and radius` ${{ rayon }}$.
82
+
83
+ \begin{equation*}
84
+ p^2 + D^2 = {{ rayonCarre }}.
85
+ \end{equation*}
86
+
87
+ {fr}`**Test axe**`{en}`**Axis test**` $Op$ ($D \to -D$) : {fr}`on remplace`{en}`we replace` $D$ {fr}`par`{en}`by` $-D$ {fr}`dans l'équation :`{en}`in the equation:`
88
+
89
+ \begin{equation*}
90
+ p^2 + (-D)^2 = p^2 + D^2 = {{ rayonCarre }}.
91
+ \end{equation*}
92
+
93
+ {fr}`L'équation est inchangée : la courbe est symétrique par rapport à l'axe`{en}`The equation is unchanged: the curve is symmetric with respect to the axis` $Op$.
94
+
95
+ {fr}`**Test axe**`{en}`**Axis test**` $OD$ ($p \to -p$) : {fr}`on remplace`{en}`we replace` $p$ {fr}`par`{en}`by` $-p$ {fr}`dans l'équation :`{en}`in the equation:`
96
+
97
+ \begin{equation*}
98
+ (-p)^2 + D^2 = p^2 + D^2 = {{ rayonCarre }}.
99
+ \end{equation*}
100
+
101
+ {fr}`L'équation est inchangée : la courbe est symétrique par rapport à l'axe`{en}`The equation is unchanged: the curve is symmetric with respect to the axis` $OD$.
102
+
103
+ {fr}`**Test origine** (`{en}`**Origin test** (` $p \to -p$, $D \to -D$) : {fr}`on remplace`{en}`we replace` $p$ {fr}`par`{en}`by` $-p$ {fr}`et`{en}`and` $D$ {fr}`par`{en}`by` $-D$ :
104
+
105
+ \begin{equation*}
106
+ (-p)^2 + (-D)^2 = p^2 + D^2 = {{ rayonCarre }}.
107
+ \end{equation*}
108
+
109
+ {fr}`L'équation est inchangée : la courbe est symétrique par rapport à l'origine.`{en}`The equation is unchanged: the curve is symmetric with respect to the origin.`
110
+
111
+ {fr}`**Test droite**`{en}`**Line test**` $D=p$ {fr}`(échanger`{en}`(exchange` $p$ {fr}`et`{en}`and` $D$) : {fr}`on échange`{en}`we exchange` $p$ {fr}`et`{en}`and` $D$ {fr}`dans l'équation :`{en}`in the equation:`
112
+
113
+ \begin{equation*}
114
+ D^2 + p^2 = {{ rayonCarre }}.
115
+ \end{equation*}
116
+
117
+ {fr}`Cette équation est identique à l'équation initiale : la courbe est symétrique par rapport à la droite`{en}`This equation is identical to the initial equation: the curve is symmetric with respect to the line` $D=p$.
118
+
119
+ {fr}`**Conclusion** : la courbe complète possède les quatre symétries.`{en}`**Conclusion**: the complete curve has all four symmetries.`
120
+ ::::
121
+
122
+ ::::{weightDistribution}
123
+ :logic: 25
124
+ :abstraction: 35
125
+ :reasoning: 30
126
+ :calculation: 10
127
+ ::::
128
+ :::::
129
+
130
+ :::::{question}
131
+ :questionType: MCQ
132
+ :questionId: 1
133
+ :questionIndex: 1
134
+
135
+ ::::{questionStatement}
136
+ {fr}`Justifier pourquoi, dans le contexte économique (`{en}`Justify why, in the economic context (` $p>0$ {fr}`et`{en}`and` $D>0$), {fr}`la courbe de demande n'est définie que sur un quart de cercle. En déduire la seule symétrie pertinente pour ce modèle, et l'interpréter.`{en}`the demand curve is only defined on a quarter circle. Deduce the only relevant symmetry for this model, and interpret it.`
137
+ ::::
138
+
139
+ ::::{questionHint}
140
+ {fr}`Une symétrie n'est pertinente que si elle renvoie un point du premier quadrant dans le premier quadrant.`{en}`A symmetry is relevant only if it maps a point from the first quadrant to the first quadrant.`
141
+ ::::
142
+
143
+ ::::{mcqAnswer}
144
+ :isRightAnswer: true
145
+ {fr}`La droite`{en}`The line` $D=p$
146
+ ::::
147
+
148
+ ::::{mcqAnswer}
149
+ :isRightAnswer: false
150
+ {fr}`L'axe`{en}`The axis` $Op$
151
+ ::::
152
+
153
+ ::::{mcqAnswer}
154
+ :isRightAnswer: false
155
+ {fr}`L'axe`{en}`The axis` $OD$
156
+ ::::
157
+
158
+ ::::{mcqAnswer}
159
+ :isRightAnswer: false
160
+ {fr}`L'origine`{en}`The origin`
161
+ ::::
162
+
163
+ ::::{mcqAnswer}
164
+ :isRightAnswer: false
165
+ {fr}`Aucune de ces réponses n'est correcte`{en}`None of these answers are correct`
166
+ ::::
167
+
168
+ ::::{detailedSolution}
169
+ {fr}`**Restriction au premier quadrant** : dans le contexte économique, le prix`{en}`**Restriction to the first quadrant**: in the economic context, the price` $p$ {fr}`et la demande`{en}`and the demand` $D$ {fr}`sont des grandeurs strictement positives. On a donc`{en}`are strictly positive quantities. We therefore have` $p>0$ {fr}`et`{en}`and` $D>0$, {fr}`ce qui signifie que seuls les points du premier quadrant sont économiquement pertinents. La courbe de demande se réduit donc à un quart de cercle de rayon`{en}`which means that only points in the first quadrant are economically relevant. The demand curve is therefore reduced to a quarter circle of radius` ${{ rayon }}$ {fr}`situé dans le premier quadrant.`{en}`located in the first quadrant.`
170
+
171
+ {fr}`**Analyse des symétries** :`{en}`**Analysis of symmetries**:`
172
+
173
+ - {fr}`**Symétrie par rapport à l'axe**`{en}`**Symmetry with respect to the axis**` $Op$ ($D \to -D$) : {fr}`cette transformation envoie un point`{en}`this transformation maps a point` $(p_0, D_0)$ {fr}`avec`{en}`with` $p_0>0$ {fr}`et`{en}`and` $D_0>0$ {fr}`sur le point`{en}`to the point` $(p_0, -D_0)$ {fr}`avec`{en}`with` $-D_0<0$. {fr}`Ce point sort du premier quadrant : cette symétrie n'est pas pertinente dans le contexte économique.`{en}`This point leaves the first quadrant: this symmetry is not relevant in the economic context.`
174
+
175
+ - {fr}`**Symétrie par rapport à l'axe**`{en}`**Symmetry with respect to the axis**` $OD$ ($p \to -p$) : {fr}`cette transformation envoie un point`{en}`this transformation maps a point` $(p_0, D_0)$ {fr}`avec`{en}`with` $p_0>0$ {fr}`et`{en}`and` $D_0>0$ {fr}`sur le point`{en}`to the point` $(-p_0, D_0)$ {fr}`avec`{en}`with` $-p_0<0$. {fr}`Ce point sort du premier quadrant : cette symétrie n'est pas pertinente dans le contexte économique.`{en}`This point leaves the first quadrant: this symmetry is not relevant in the economic context.`
176
+
177
+ - {fr}`**Symétrie par rapport à l'origine** (`{en}`**Symmetry with respect to the origin** (` $p \to -p$, $D \to -D$) : {fr}`cette transformation envoie un point`{en}`this transformation maps a point` $(p_0, D_0)$ {fr}`avec`{en}`with` $p_0>0$ {fr}`et`{en}`and` $D_0>0$ {fr}`sur le point`{en}`to the point` $(-p_0, -D_0)$ {fr}`avec`{en}`with` $-p_0<0$ {fr}`et`{en}`and` $-D_0<0$. {fr}`Ce point sort du premier quadrant : cette symétrie n'est pas pertinente dans le contexte économique.`{en}`This point leaves the first quadrant: this symmetry is not relevant in the economic context.`
178
+
179
+ - {fr}`**Symétrie par rapport à la droite**`{en}`**Symmetry with respect to the line**` $D=p$ {fr}`(échanger`{en}`(exchange` $p$ {fr}`et`{en}`and` $D$) : {fr}`cette transformation envoie un point`{en}`this transformation maps a point` $(p_0, D_0)$ {fr}`avec`{en}`with` $p_0>0$ {fr}`et`{en}`and` $D_0>0$ {fr}`sur le point`{en}`to the point` $(D_0, p_0)$ {fr}`avec`{en}`with` $D_0>0$ {fr}`et`{en}`and` $p_0>0$. {fr}`Ce point reste dans le premier quadrant : c'est la seule symétrie pertinente dans le contexte économique.`{en}`This point remains in the first quadrant: this is the only relevant symmetry in the economic context.`
180
+
181
+ {fr}`**Interprétation** : la symétrie par rapport à la droite`{en}`**Interpretation**: the symmetry with respect to the line` $D=p$ {fr}`signifie que si à un prix`{en}`means that if at a price` $p_0$ {fr}`correspond une demande`{en}`corresponds a demand` $D_0$ {fr}`(le point`{en}`(the point` $(p_0, D_0)$ {fr}`est sur la courbe), alors le point`{en}`is on the curve), then the point` $(D_0, p_0)$ {fr}`est également sur la courbe. Autrement dit, si la demande vaut`{en}`is also on the curve. In other words, if the demand equals` $D_0$ {fr}`unités au prix`{en}`units at price` $p_0$, {fr}`alors elle vaudra`{en}`then it will equal` $p_0$ {fr}`unités au prix`{en}`units at price` $D_0$. {fr}`Le modèle est donc réversible vis-à-vis de l'échange entre prix et demande : la relation entre`{en}`The model is therefore reversible with respect to the exchange between price and demand: the relationship between` $p$ {fr}`et`{en}`and` $D$ {fr}`est symétrique.`{en}`is symmetric.`
182
+ ::::
183
+
184
+ ::::{weightDistribution}
185
+ :logic: 30
186
+ :abstraction: 35
187
+ :reasoning: 30
188
+ :calculation: 5
189
+ ::::
190
+ :::::
191
+
192
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Unités minimales pour couvrir les coûts fixes`{en}`Minimum units to cover fixed costs`.md ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :originalExerciseId: 7629a255-634b-11f1-a8a1-0ed8d3b012a9
3
+ :title: {fr}`Unités minimales pour couvrir les coûts fixes`{en}`Minimum units to cover fixed costs`
4
+ :modules:
5
+ :recommendedExecutionTime: 10
6
+ :level: Elementary
7
+ :chap:
8
+ :involvedConcepts: Solving_inequalities, Modeling_with_Functions
9
+ :originalSource: Session 2: Equations and Inequalities - Applications of Linear Inequalities (Erwan Lamy, ESCP Business School)
10
+ :visibility: All
11
+ :variations:
12
+ :comment: Version QCM — condition de non-perte, résolution et arrondi entier.
13
+ :id: a3e299b2-6e47-11f1-a8a1-0ed8d3b012a9
14
+
15
+ ````{python}
16
+ import random as rd
17
+ from sympy import Rational, ceiling, oo, Integer
18
+ from pyxiscience.Mes_fctions_generalistes_bis import pxs_config, pxsl_format_number, pxsl_num
19
+ from pyxiscience.Classes_Extensions import pxs_Interval
20
+
21
+ config_standard = pxs_config()
22
+
23
+ # Coûts fixes entiers, marge unitaire décimale (k/10), quotient non entier -> arrondi utile
24
+ for _ in range(300):
25
+ fixedCost = rd.randint(400, 1000)
26
+ k = rd.randint(5, 30)
27
+ if (10 * fixedCost) % k != 0:
28
+ break
29
+
30
+ margin = Rational(k, 10)
31
+ qExact = Rational(fixedCost * 10, k)
32
+ qMin = int(ceiling(qExact))
33
+
34
+ # Précalcul de l'affichage
35
+ marginAff = pxsl_num(margin, dec=2)
36
+ fixedCostAff = pxsl_format_number(fixedCost)
37
+ qApproxAff = pxsl_num(qExact, dec=2)
38
+ qMinAff = pxsl_format_number(qMin)
39
+ margeFoisQminAff = pxsl_num(margin * qMin, dec=2)
40
+
41
+ # === Distracteurs MCQ (ajouts ; le code ci-dessus est inchangé) ===
42
+ def _dedl(correct_l, cands, n=3):
43
+ seen = {correct_l}; out = []
44
+ for s in cands:
45
+ if s not in seen:
46
+ seen.add(s); out.append(s)
47
+ if len(out) == n:
48
+ break
49
+ return out
50
+
51
+ if isinstance(qExact, Integer):
52
+ borne = qExact
53
+ else:
54
+ borne = int(qExact) + 1
55
+
56
+ # Q2 : entier minimal
57
+ repBorne = pxsl_format_number(int(borne))
58
+ _cB = [pxsl_format_number(int(borne) - 1),
59
+ pxsl_format_number(int(ceiling(Rational(fixedCost, k)))),
60
+ pxsl_format_number(int(borne) + 1),
61
+ pxsl_format_number(int(2 * borne))]
62
+ _d = _dedl(repBorne, _cB)
63
+ b1, b2, b3 = _d[0], _d[1], _d[2]
64
+
65
+ # Q1 : intervalles
66
+ def _ival(lo, hi, ol, oh):
67
+ s = pxs_Interval(lo, hi, ol, oh).print()
68
+ if pxs_lang == "fr":
69
+ s = s.replace(".", ",")
70
+ return s
71
+
72
+ _qa = round(float(qExact), 2)
73
+ interCorrect = _ival(_qa, oo, False, True)
74
+ interD1 = _ival(_qa, oo, True, True)
75
+ interD2 = _ival(-oo, _qa, True, False)
76
+ interD3 = _ival(0, _qa, False, False)
77
+
78
+ globals()
79
+ ````
80
+
81
+ :::::{question}
82
+ :questionType: MCQ
83
+
84
+ ::::{questionStatement}
85
+ {fr}`Une petite boulangerie a des coûts fixes hebdomadaires de`{en}`A small bakery has weekly fixed costs of` ${{fixedCostAff}}$ {fr}`€ (loyer, salaires, etc.). Chaque pain vendu génère une marge de contribution de`{en}`€ (rent, salaries, etc.). Each loaf of bread sold generates a contribution margin of` ${{marginAff}}$ {fr}`€ (prix de vente moins coût variable unitaire). On note`{en}`€ (selling price minus unit variable cost). Let` $q$ {fr}`le nombre de pains vendus par semaine. Quelle inéquation traduit que la marge de contribution totale couvre les coûts fixes (la boulangerie ne fait pas de perte) ?`{en}`be the number of loaves sold per week. Which inequality expresses that the total contribution margin covers the fixed costs (the bakery does not make a loss)?`
86
+ ::::
87
+
88
+ ::::{questionHint}
89
+ {fr}`La marge de contribution totale est égale à`{en}`The total contribution margin is equal to` ${{marginAff}}\,q$. {fr}`La condition de non-perte correspond à une marge au moins égale aux coûts fixes.`{en}`The break-even condition corresponds to a margin at least equal to the fixed costs.`
90
+ ::::
91
+
92
+ ::::{mcqAnswer}
93
+ :isRightAnswer: true
94
+ ${{marginAff}}\,q \geq {{fixedCostAff}}$
95
+ ::::
96
+
97
+ ::::{mcqAnswer}
98
+ :isRightAnswer: false
99
+ ${{marginAff}}\,q \leq {{fixedCostAff}}$
100
+ ::::
101
+
102
+ ::::{mcqAnswer}
103
+ :isRightAnswer: false
104
+ ${{marginAff}}\,q = {{fixedCostAff}}$
105
+ ::::
106
+
107
+ ::::{mcqAnswer}
108
+ :isRightAnswer: false
109
+ ${{fixedCostAff}}\,q \geq {{marginAff}}$
110
+ ::::
111
+
112
+ ::::{mcqAnswer}
113
+ :isRightAnswer: false
114
+ ${{fixedCostAff}}\,q \leq {{marginAff}}$
115
+ ::::
116
+
117
+ ::::{mcqAnswer}
118
+ :isRightAnswer: false
119
+ {fr}`Aucune de ces réponses n'est correcte`{en}`None of these answers are correct`
120
+ ::::
121
+
122
+ ::::{detailedSolution}
123
+ {fr}`La marge de contribution totale est`{en}`The total contribution margin is` ${{marginAff}}\,q$. {fr}`La condition de non-perte s'écrit :`{en}`The break-even condition is written as:`
124
+
125
+ \begin{equation*}
126
+ {{marginAff}}\,q \geq {{fixedCostAff}}.
127
+ \end{equation*}
128
+ ::::
129
+
130
+ ::::{weightDistribution}
131
+ :logic: 15
132
+ :abstraction: 30
133
+ :reasoning: 35
134
+ :calculation: 20
135
+ ::::
136
+ :::::
137
+
138
+ :::::{question}
139
+ :questionType: MCQ
140
+
141
+ ::::{questionStatement}
142
+ {fr}`Résoudre l'inéquation`{en}`Solve the inequality` ${{marginAff}}\,q \geq {{fixedCostAff}}$ {fr}`(pour`{en}`(for` $q \geq 0${fr}`). Quel est l'ensemble des solutions sous forme d'intervalle ?`{en}`). What is the solution set in interval form?`
143
+ ::::
144
+
145
+ ::::{questionHint}
146
+ {fr}`Diviser les deux membres par la marge unitaire, qui est positive : le sens est conservé.`{en}`Divide both sides by the unit margin, which is positive: the direction is preserved.`
147
+ ::::
148
+
149
+ ::::{mcqAnswer}
150
+ :isRightAnswer: true
151
+ ${{interCorrect}}$
152
+ ::::
153
+
154
+ ::::{mcqAnswer}
155
+ :isRightAnswer: false
156
+ ${{interD1}}$
157
+ ::::
158
+
159
+ ::::{mcqAnswer}
160
+ :isRightAnswer: false
161
+ ${{interD2}}$
162
+ ::::
163
+
164
+ ::::{mcqAnswer}
165
+ :isRightAnswer: false
166
+ ${{interD3}}$
167
+ ::::
168
+
169
+ ::::{mcqAnswer}
170
+ :isRightAnswer: false
171
+ {fr}`Aucune de ces réponses n'est correcte`{en}`None of these answers are correct`
172
+ ::::
173
+
174
+ ::::{detailedSolution}
175
+ {fr}`On divise les deux membres par`{en}`We divide both sides by` ${{marginAff}} > 0$, {fr}`le sens est conservé :`{en}`the direction is preserved:`
176
+
177
+ \begin{equation*}
178
+ q \geq \dfrac{ {{fixedCostAff}} }{ {{marginAff}} } \approx {{qApproxAff}}.
179
+ \end{equation*}
180
+
181
+ {fr}`L'ensemble des solutions (pour`{en}`The solution set (for` $q \geq 0${fr}`) est`{en}`) is` ${{interCorrect}}$.
182
+ ::::
183
+
184
+ ::::{weightDistribution}
185
+ :logic: 10
186
+ :abstraction: 15
187
+ :reasoning: 25
188
+ :calculation: 50
189
+ ::::
190
+ :::::
191
+
192
+ :::::{question}
193
+ :questionType: MCQ
194
+
195
+ ::::{questionStatement}
196
+ {fr}`Quel est le nombre minimal de pains entiers que la boulangerie doit vendre pour atteindre le seuil de rentabilité ?`{en}`What is the minimum number of whole loaves that the bakery must sell to reach the break-even point?`
197
+ ::::
198
+
199
+ ::::{questionHint}
200
+ {fr}`Le nombre de pains est un entier. Arrondir la solution à l'entier supérieur.`{en}`The number of loaves is an integer. Round the solution up to the next integer.`
201
+ ::::
202
+
203
+ ::::{mcqAnswer}
204
+ :isRightAnswer: true
205
+ ${{repBorne}}$
206
+ ::::
207
+
208
+ ::::{mcqAnswer}
209
+ :isRightAnswer: false
210
+ ${{b1}}$
211
+ ::::
212
+
213
+ ::::{mcqAnswer}
214
+ :isRightAnswer: false
215
+ ${{b2}}$
216
+ ::::
217
+
218
+ ::::{mcqAnswer}
219
+ :isRightAnswer: false
220
+ ${{b3}}$
221
+ ::::
222
+
223
+ ::::{mcqAnswer}
224
+ :isRightAnswer: false
225
+ {fr}`Aucune de ces réponses n'est correcte`{en}`None of these answers are correct`
226
+ ::::
227
+
228
+ ::::{detailedSolution}
229
+ {fr}`La quantité`{en}`The quantity` $q$ {fr}`doit être un entier. Comme`{en}`must be an integer. Since` $\dfrac{ {{fixedCostAff}} }{ {{marginAff}} } \approx {{qApproxAff}}$, {fr}`on arrondit à l'entier supérieur :`{en}`we round up to the next integer:` $q = {{borne}}$.
230
+
231
+ {fr}`La boulangerie doit vendre au moins`{en}`The bakery must sell at least` ${{borne}}$ {fr}`pains par semaine pour ne pas faire de perte.`{en}`loaves per week to avoid making a loss.`
232
+ ::::
233
+
234
+ ::::{weightDistribution}
235
+ :logic: 20
236
+ :abstraction: 20
237
+ :reasoning: 35
238
+ :calculation: 25
239
+ ::::
240
+ :::::
241
+
242
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Valeurs absolues élémentaires 2`{en}`Elementary Absolute Values 2`.md ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :id: 73dd633c-70a7-11f1-a8a1-0ed8d3b012a9
3
+ :originalExerciseId: d140e232-68cb-11f1-a8a1-0ed8d3b012a9
4
+ :title: {fr}`Valeurs absolues élémentaires 2`{en}`Elementary Absolute Values 2`
5
+ :modules: fund_of_math_I_ESCP
6
+ :recommendedExecutionTime: 20
7
+ :level: Elementary
8
+ :chap: chap_equations_Inequalities_absoluteValue_ESCP
9
+ :involvedConcepts: Abs_Val
10
+ :originalSource: ESCP
11
+ :visibility: All
12
+ :variations:
13
+ :comment:
14
+
15
+ ```{python}
16
+
17
+ import random as rd
18
+ from sympy import *
19
+ from pyxiscience.Mes_fctions_generalistes_bis import pxs_config
20
+ from pyxiscience.Classes_Extensions import pxs_Interval
21
+ from pyxiscience.Mes_fctions_d_analyse import pxsl_pow
22
+ from pyxiscience.Mes_fctions_generalistes_bis import pxsl_latex_avec_formatage
23
+ from pyxiscience.Mes_fctions_probabilistes_bis import pxsl_res_num
24
+
25
+ from pyxiscience.Mes_fctions_generalistes_bis import pxs_config
26
+
27
+ config_standard = pxs_config()
28
+
29
+ n, x, u = symbols('n x u')
30
+
31
+ e1 = rd.randint(-10,-1)
32
+
33
+ e2 = rd.randint(1,11)
34
+
35
+ e31, e32 = rd.randint(1,11), rd.randint(-10,-1)
36
+ e3 = Add(e31, e32, evaluate = False)
37
+ e3_eval = Add(e31, e32, evaluate = True)
38
+
39
+ e41 = rd.randint(1,11)
40
+ e42 = rd.randint(-10,-1)
41
+ e43 = rd.randint(2, 11)
42
+ e4 = Mul(Rational(1,e43), Add(e41, e42, evaluate = False), evaluate = False)
43
+ e4_eval = Mul(Rational(1,e43), Add(e41, e42, evaluate = True), evaluate = True)
44
+
45
+ e51 = rd.choice([3, 5, 7])
46
+ e52 = rd.choice([-2, -4, -8])
47
+ e5 = Mul(e51, Rational(e52, e51), evaluate = False)
48
+ e5_eval = Mul(e51, Rational(e52, e51), evaluate = True)
49
+
50
+ e611, e612 = rd.randint(1,11), rd.randint(-10,-1)
51
+ e61 = Add(e611, e612, evaluate = False)
52
+ e621, e622 = rd.randint(1,11), rd.randint(-10,-1)
53
+ e62 = Add(e621, e622, evaluate = False)
54
+ e61_eval = Add(e611, e612, evaluate = True)
55
+ e62_eval = Add(e621, e622, evaluate = True)
56
+
57
+ e7 = rd.randint(1,11)
58
+
59
+ e8 = rd.randint(1,11)
60
+
61
+ a9 = rd.choice([2, 3, 5])
62
+ if a9 == 2:
63
+ b9 = rd.choice([5, 7, 10])
64
+ elif a9 == 3:
65
+ b9 = rd.choice([10, 11, 13])
66
+ else:
67
+ b9 = rd.choice([26, 29, 31])
68
+
69
+ a9, b9 = Min(a9, b9), Max(a9, b9)
70
+ e9 = a9 - sqrt(b9)
71
+
72
+ a10, b10 = Max(a9, b9), Min(a9, b9)
73
+ e10 = sqrt(b10) - a10
74
+
75
+ ```
76
+
77
+ {en}`Evaluate the absolute value expressions:`{fr}`Evaluer les valeurs abolues suivantes :`
78
+
79
+
80
+
81
+ :::::{question}
82
+ :questionType: FGQ
83
+ :questionId: 5
84
+ :questionIndex: 5
85
+ :solution: [["ord","${{ latex(Abs(e61_eval) - Abs(e62_eval)) }}$"],["0"]]
86
+
87
+ ::::{questionStatement}
88
+ \begin{equation*}
89
+ \Big|{{ latex(e61) }}\Big| - \Big|{{ latex(e62) }}\Big|
90
+ \end{equation*}
91
+
92
+ {fr}`Cette expression vaut :` {en}`This expression equals:` {input}`||120`
93
+ ::::
94
+
95
+ ::::{questionHint}
96
+ {fr}`Calculez chaque valeur absolue séparément avant de soustraire.`{en}`Compute each absolute value separately before subtracting.`
97
+ ::::
98
+
99
+ ::::{displayedSolution}
100
+ ${{ latex(Abs(e61_eval) - Abs(e62_eval)) }}$
101
+ ::::
102
+
103
+ ::::{detailedSolution}
104
+ {fr}`On calcule chaque valeur absolue séparément :`{en}`Compute each absolute value separately:`
105
+
106
+ \begin{equation*}
107
+ |{{ latex(e61) }}| &= |{{ e61_eval }}| \\[6pt]
108
+ &= {{ Abs(e61_eval) }}
109
+ \end{equation*}
110
+
111
+ {fr}`et`{en}`and`
112
+
113
+ \begin{equation*}
114
+ |{{ latex(e62) }}| &= |{{ e62_eval }}| \\[6pt]
115
+ &= {{ Abs(e62_eval) }}.
116
+ \end{equation*}
117
+
118
+ {fr}`La soustraction donne`{en}`Subtracting gives`
119
+
120
+ \begin{equation*}
121
+ |{{ latex(e61) }}| - |{{ latex(e62) }}| &= {{ Abs(e61_eval) }} - {{ Abs(e62_eval) }} \\[6pt]
122
+ &={{ latex(Abs(e61_eval) - Abs(e62_eval)) }}.
123
+ \end{equation*}
124
+ ::::
125
+
126
+ ::::{weightDistribution}
127
+ :reasoning: 25
128
+ :logic: 25
129
+ :abstraction: 15
130
+ :calculation: 35
131
+ ::::
132
+ :::::
133
+
134
+ :::::{question}
135
+ :questionType: FGQ
136
+ :questionId: 8
137
+ :questionIndex: 8
138
+ :solution: [["ord","${{ latex(Abs(e9)) }}$"],["0"]]
139
+
140
+ ::::{questionStatement}
141
+ \begin{equation*}
142
+ |{{ latex(e9) }}|
143
+ \end{equation*}
144
+
145
+ {fr}`Cette expression vaut :` {en}`This expression equals:` {input}`||120`
146
+ ::::
147
+
148
+ ::::{questionHint}
149
+ {fr}`Comparez l'entier avec $\sqrt{b}$ pour déterminer le signe de l'expression à l'intérieur de la valeur absolue.`{en}`Compare the integer with $\sqrt{b}$ to decide the sign of the expression inside the absolute value.`
150
+ ::::
151
+
152
+ ::::{displayedSolution}
153
+ ${{ latex(Abs(e9)) }}$
154
+ ::::
155
+
156
+ ::::{detailedSolution}
157
+ {fr}`L'expression à l'intérieur de la valeur absolue est`{en}`The expression inside the absolute value is` ${{ latex(e9) }}$.\
158
+ {fr}`Puisque`{en}`Since` ${{ a9 }} < \sqrt{{{ b9 }}}$, {fr}`la quantité`{en}`the quantity` ${{ latex(e9) }}$ {fr}`est négative.`{en}`is negative.`\
159
+ {fr}`Par conséquent`{en}`Therefore`
160
+
161
+ \begin{equation*}
162
+ |{{ latex(e9) }}| &= -\big({{ latex(e9) }}\big) \\[6pt]
163
+ &= {{ latex(Abs(e9)) }}.
164
+ \end{equation*}
165
+ ::::
166
+
167
+ ::::{weightDistribution}
168
+ :reasoning: 25
169
+ :logic: 25
170
+ :abstraction: 25
171
+ :calculation: 25
172
+ ::::
173
+ :::::
174
+
175
+ :::::{question}
176
+ :questionType: FGQ
177
+ :questionId: 9
178
+ :questionIndex: 9
179
+ :solution: [["ord","${{ latex(Abs(e10)) }}$"],["0"]]
180
+
181
+ ::::{questionStatement}
182
+ \begin{equation*}
183
+ |{{ latex(e10) }}|
184
+ \end{equation*}
185
+
186
+ {fr}`Cette expression vaut :` {en}`This expression equals:` {input}`||120`
187
+ ::::
188
+
189
+ ::::{questionHint}
190
+ {fr}`Déterminez si $\sqrt{b}$ est plus petit ou plus grand que l'entier devant lui.`{en}`Determine whether $\sqrt{b}$ is smaller or larger than the integer in front of it.`
191
+ ::::
192
+
193
+ ::::{displayedSolution}
194
+ ${{ latex(Abs(e10)) }}$
195
+ ::::
196
+
197
+ ::::{detailedSolution}
198
+ {fr}`À l'intérieur de la valeur absolue on a`{en}`Inside the absolute value one has` ${{ latex(e10) }}$.\
199
+ {fr}`Puisque`{en}`Because` ${{ a10 }} > \sqrt{{{ b10 }}}$, {fr}`l'expression`{en}`the expression` ${{ latex(e10) }}$ {fr}`est négative.`{en}`is negative.`\
200
+ {fr}`En conséquence`{en}`Consequently`
201
+
202
+ \begin{equation*}
203
+ |{{ latex(e10) }}| &= -\big({{ latex(e10) }}\big) \\[6pt]
204
+ &= {{ latex(Abs(e10)) }}.
205
+ \end{equation*}
206
+ ::::
207
+
208
+ ::::{weightDistribution}
209
+ :reasoning: 25
210
+ :logic: 25
211
+ :abstraction: 25
212
+ :calculation: 25
213
+ ::::
214
+ :::::
215
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Valeurs absolues élémentaires 3`{en}`Elementary Absolute Values 3`.md ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :id: 9af8828d-70a7-11f1-a8a1-0ed8d3b012a9
3
+ :originalExerciseId: 2ce83c2b-68cc-11f1-a8a1-0ed8d3b012a9
4
+ :title: {fr}`Valeurs absolues élémentaires 3`{en}`Elementary Absolute Values 3`
5
+ :modules: fund_of_math_I_ESCP
6
+ :recommendedExecutionTime: 5
7
+ :level: Elementary
8
+ :chap: chap_equations_Inequalities_absoluteValue_ESCP
9
+ :involvedConcepts: Abs_Val
10
+ :originalSource: ESCP
11
+ :visibility: All
12
+ :variations:
13
+ :comment:
14
+
15
+ ```{python}
16
+
17
+ import random as rd
18
+ from sympy import *
19
+ from pyxiscience.Mes_fctions_generalistes_bis import pxs_config
20
+ from pyxiscience.Classes_Extensions import pxs_Interval
21
+ from pyxiscience.Mes_fctions_d_analyse import pxsl_pow
22
+ from pyxiscience.Mes_fctions_generalistes_bis import pxsl_latex_avec_formatage
23
+ from pyxiscience.Mes_fctions_probabilistes_bis import pxsl_res_num
24
+
25
+ from pyxiscience.Mes_fctions_generalistes_bis import pxs_config
26
+
27
+ config_standard = pxs_config()
28
+
29
+ n, x, u = symbols('n x u')
30
+
31
+ e1 = rd.randint(-10,-1)
32
+
33
+ e2 = rd.randint(1,11)
34
+
35
+ e31, e32 = rd.randint(1,11), rd.randint(-10,-1)
36
+ e3 = Add(e31, e32, evaluate = False)
37
+ e3_eval = Add(e31, e32, evaluate = True)
38
+
39
+ e41 = rd.randint(1,11)
40
+ e42 = rd.randint(-10,-1)
41
+ e43 = rd.randint(2, 11)
42
+ e4 = Mul(Rational(1,e43), Add(e41, e42, evaluate = False), evaluate = False)
43
+ e4_eval = Mul(Rational(1,e43), Add(e41, e42, evaluate = True), evaluate = True)
44
+
45
+ e51 = rd.choice([3, 5, 7])
46
+ e52 = rd.choice([-2, -4, -8])
47
+ e5 = Mul(e51, Rational(e52, e51), evaluate = False)
48
+ e5_eval = Mul(e51, Rational(e52, e51), evaluate = True)
49
+
50
+ e611, e612 = rd.randint(1,11), rd.randint(-10,-1)
51
+ e61 = Add(e611, e612, evaluate = False)
52
+ e621, e622 = rd.randint(1,11), rd.randint(-10,-1)
53
+ e62 = Add(e621, e622, evaluate = False)
54
+ e61_eval = Add(e611, e612, evaluate = True)
55
+ e62_eval = Add(e621, e622, evaluate = True)
56
+
57
+ e7 = rd.randint(1,11)
58
+
59
+ e8 = rd.randint(1,11)
60
+
61
+ a9 = rd.choice([2, 3, 5])
62
+ if a9 == 2:
63
+ b9 = rd.choice([5, 7, 10])
64
+ elif a9 == 3:
65
+ b9 = rd.choice([10, 11, 13])
66
+ else:
67
+ b9 = rd.choice([26, 29, 31])
68
+
69
+ a9, b9 = Min(a9, b9), Max(a9, b9)
70
+ e9 = a9 - sqrt(b9)
71
+
72
+ a10, b10 = Max(a9, b9), Min(a9, b9)
73
+ e10 = sqrt(b10) - a10
74
+
75
+ ```
76
+
77
+ {en}`Evaluate the absolute value expressions:`{fr}`Evaluer les valeurs abolues suivantes :`
78
+
79
+
80
+
81
+ :::::{question}
82
+ :questionType: FGQ
83
+ :questionId: 6
84
+ :questionIndex: 6
85
+ :solution: [["ord","${{ -e7 }}$","${{ e7 }}$"],["0","0"]]
86
+
87
+ ::::{questionStatement}
88
+ \begin{equation*}
89
+ |x| < {{ e7 }}
90
+ \end{equation*}
91
+
92
+ {fr}`Compléter l'encadrement donnant l'ensemble des solutions :` {en}`Complete the bounds giving the solution set:`\
93
+ \
94
+ {input}`||80` $< x <$ {input}`||80`
95
+ ::::
96
+
97
+ ::::{questionHint}
98
+ {fr}`$|x|<a$ signifie que $x$ se trouve strictement entre $-a$ et $a$.`{en}`$|x|<a$ means $x$ lies strictly between $-a$ and $a$.`
99
+ ::::
100
+
101
+ ::::{displayedSolution}
102
+ $-{{ e7 }} < x < {{ e7 }}$
103
+ ::::
104
+
105
+ ::::{detailedSolution}
106
+ {fr}`L'inéquation`{en}`The inequality` $|x| < {{ e7 }}$ {fr}`signifie que`{en}`states that` $x$ {fr}`se trouve strictement entre`{en}`lies strictly between` $-{{ e7 }}$ {fr}`et`{en}`and` ${{ e7 }}$.\
107
+ {fr}`Ainsi`{en}`Thus`
108
+
109
+ \begin{equation*}
110
+ -{{ e7 }} < x < {{ e7 }}.
111
+ \end{equation*}
112
+ ::::
113
+
114
+ ::::{weightDistribution}
115
+ :reasoning: 30
116
+ :logic: 30
117
+ :abstraction: 30
118
+ :calculation: 10
119
+ ::::
120
+ :::::
121
+
122
+ :::::{question}
123
+ :questionType: FGQ
124
+ :questionId: 7
125
+ :questionIndex: 7
126
+ :solution: [["ord","${{ -e8 }}$","${{ e8 }}$"],["0","0"]]
127
+
128
+ ::::{questionStatement}
129
+ \begin{equation*}
130
+ |x| \leq {{ e8 }}
131
+ \end{equation*}
132
+
133
+ {fr}`Compléter l'encadrement donnant l'ensemble des solutions :` {en}`Complete the bounds giving the solution set:`\
134
+ \
135
+ {input}`||80` $\leq x \leq$ {input}`||80`
136
+ ::::
137
+
138
+ ::::{questionHint}
139
+ {fr}`$|x|\le a$ permet à $x$ d'être égal à $-a$ ou $a$.`{en}`$|x|\le a$ allows $x$ to equal $-a$ or $a$.`
140
+ ::::
141
+
142
+ ::::{displayedSolution}
143
+ $-{{ e8 }} \leq x \leq {{ e8 }}$
144
+ ::::
145
+
146
+ ::::{detailedSolution}
147
+ {fr}`L'inéquation large`{en}`The non-strict inequality` $|x| \leq {{ e8 }}$ {fr}`inclut les bornes, donc`{en}`includes the endpoints, hence`
148
+
149
+ \begin{equation*}
150
+ -{{ e8 }} \leq x \leq {{ e8 }}.
151
+ \end{equation*}
152
+ ::::
153
+
154
+ ::::{weightDistribution}
155
+ :reasoning: 30
156
+ :logic: 30
157
+ :abstraction: 30
158
+ :calculation: 10
159
+ ::::
160
+ :::::
161
+
162
+
163
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Valeurs absolues élémentaires`{en}`Elementary Absolute Values`.md ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :id: c84654fa-70a6-11f1-a8a1-0ed8d3b012a9
3
+ :originalExerciseId: 834b79ee-fc00-47e7-921b-83e4524d24b0
4
+ :title: {fr}`Valeurs absolues élémentaires`{en}`Elementary Absolute Values`
5
+ :modules: fund_of_math_I_ESCP
6
+ :recommendedExecutionTime: 20
7
+ :level: Elementary
8
+ :chap: chap_equations_Inequalities_absoluteValue_ESCP
9
+ :involvedConcepts: Solving_inequalities, Abs_Val
10
+ :originalSource: ESCP
11
+ :visibility: All
12
+ :variations:
13
+ :comment:
14
+
15
+ ```{python}
16
+
17
+ import random as rd
18
+ from sympy import *
19
+ from pyxiscience.Mes_fctions_generalistes_bis import pxs_config
20
+ from pyxiscience.Classes_Extensions import pxs_Interval
21
+ from pyxiscience.Mes_fctions_d_analyse import pxsl_pow
22
+ from pyxiscience.Mes_fctions_generalistes_bis import pxsl_latex_avec_formatage
23
+ from pyxiscience.Mes_fctions_probabilistes_bis import pxsl_res_num
24
+
25
+ from pyxiscience.Mes_fctions_generalistes_bis import pxs_config
26
+
27
+ config_standard = pxs_config()
28
+
29
+ n, x, u = symbols('n x u')
30
+
31
+ e1 = rd.randint(-10,-1)
32
+
33
+ e2 = rd.randint(1,11)
34
+
35
+ e31, e32 = rd.randint(1,11), rd.randint(-10,-1)
36
+ e3 = Add(e31, e32, evaluate = False)
37
+ e3_eval = Add(e31, e32, evaluate = True)
38
+
39
+ e41 = rd.randint(1,11)
40
+ e42 = rd.randint(-10,-1)
41
+ e43 = rd.randint(2, 11)
42
+ e4 = Mul(Rational(1,e43), Add(e41, e42, evaluate = False), evaluate = False)
43
+ e4_eval = Mul(Rational(1,e43), Add(e41, e42, evaluate = True), evaluate = True)
44
+
45
+ e51 = rd.choice([3, 5, 7])
46
+ e52 = rd.choice([-2, -4, -8])
47
+ e5 = Mul(e51, Rational(e52, e51), evaluate = False)
48
+ e5_eval = Mul(e51, Rational(e52, e51), evaluate = True)
49
+
50
+ e611, e612 = rd.randint(1,11), rd.randint(-10,-1)
51
+ e61 = Add(e611, e612, evaluate = False)
52
+ e621, e622 = rd.randint(1,11), rd.randint(-10,-1)
53
+ e62 = Add(e621, e622, evaluate = False)
54
+ e61_eval = Add(e611, e612, evaluate = True)
55
+ e62_eval = Add(e621, e622, evaluate = True)
56
+
57
+ e7 = rd.randint(1,11)
58
+
59
+ e8 = rd.randint(1,11)
60
+
61
+ a9 = rd.choice([2, 3, 5])
62
+ if a9 == 2:
63
+ b9 = rd.choice([5, 7, 10])
64
+ elif a9 == 3:
65
+ b9 = rd.choice([10, 11, 13])
66
+ else:
67
+ b9 = rd.choice([26, 29, 31])
68
+
69
+ a9, b9 = Min(a9, b9), Max(a9, b9)
70
+ e9 = a9 - sqrt(b9)
71
+
72
+ a10, b10 = Max(a9, b9), Min(a9, b9)
73
+ e10 = sqrt(b10) - a10
74
+
75
+ ```
76
+
77
+ {en}`Evaluate the absolute value expressions:`{fr}`Evaluer les valeurs abolues suivantes :`
78
+
79
+ :::::{question}
80
+ :questionType: FGQ
81
+ :solution: [["ord","${{ Abs(e1) }}$"],["0"]]
82
+
83
+ ::::{questionStatement}
84
+ \begin{equation*}
85
+ |{{ e1 }}|
86
+ \end{equation*}
87
+
88
+ {fr}`Cette expression vaut :` {en}`This expression equals:` {input}`||120`
89
+ ::::
90
+
91
+ ::::{questionHint}
92
+ {fr}`Rappel : la valeur absolue d'un nombre négatif est son opposé.`{en}`Remember: the absolute value of a negative number is its opposite.`
93
+ ::::
94
+
95
+ ::::{displayedSolution}
96
+ ${{ Abs(e1) }}$
97
+ ::::
98
+
99
+ ::::{detailedSolution}
100
+ {fr}`La valeur absolue d'un nombre réel est sa distance à zéro sur la droite réelle, elle est donc toujours positive ou nulle.`{en}`The absolute value of a real number is its distance from zero on the real line, hence it is always non–negative.`\
101
+ {fr}`Puisque`{en}`Since` ${{e1}}$ {fr}`est négatif, on change son signe :`{en}`is negative, one changes its sign:`
102
+
103
+ \begin{equation*}
104
+ |{{e1}}| &= -({{e1}}) \\[6pt]
105
+ &= {{ Abs(e1) }}.
106
+ \end{equation*}
107
+ ::::
108
+
109
+ ::::{weightDistribution}
110
+ :reasoning: 10
111
+ :logic: 20
112
+ :abstraction: 10
113
+ :calculation: 60
114
+ ::::
115
+ :::::
116
+
117
+ :::::{question}
118
+ :questionType: FGQ
119
+ :solution: [["ord","${{ Abs(e2) }}$"],["0"]]
120
+
121
+ ::::{questionStatement}
122
+ \begin{equation*}
123
+ |{{ e2 }}|
124
+ \end{equation*}
125
+
126
+ {fr}`Cette expression vaut :` {en}`This expression equals:` {input}`||120`
127
+ ::::
128
+
129
+ ::::{questionHint}
130
+ {fr}`La valeur absolue d'un nombre positif est le nombre lui-même.`{en}`The absolute value of a positive number is the number itself.`
131
+ ::::
132
+
133
+ ::::{displayedSolution}
134
+ ${{ Abs(e2) }}$
135
+ ::::
136
+
137
+ ::::{detailedSolution}
138
+ {fr}`Le nombre`{en}`The number` ${{e2}}$ {fr}`est positif, donc sa valeur absolue est égale à lui-même :`{en}`is positive, therefore its absolute value equals itself:`
139
+
140
+ \begin{equation*}
141
+ |{{e2}}| = {{ Abs(e2) }}.
142
+ \end{equation*}
143
+ ::::
144
+
145
+ ::::{weightDistribution}
146
+ :reasoning: 10
147
+ :logic: 20
148
+ :abstraction: 10
149
+ :calculation: 60
150
+ ::::
151
+ :::::
152
+
153
+ :::::{question}
154
+ :questionType: FGQ
155
+ :solution: [["ord","${{ Abs(e3) }}$"],["0"]]
156
+
157
+ ::::{questionStatement}
158
+ \begin{equation*}
159
+ |{{ e3 }}|
160
+ \end{equation*}
161
+
162
+ {fr}`Cette expression vaut :` {en}`This expression equals:` {input}`||120`
163
+ ::::
164
+
165
+ ::::{questionHint}
166
+ {fr}`Additionnez d'abord les deux entiers, puis prenez la valeur absolue du résultat.`{en}`First add the two integers, then take the absolute value of the result.`
167
+ ::::
168
+
169
+ ::::{displayedSolution}
170
+ $\ds {{ Abs(e3) }}$
171
+ ::::
172
+
173
+ ::::{detailedSolution}
174
+ {fr}`On calcule d'abord la somme à l'intérieur de la valeur absolue :`{en}`First one computes the sum inside the absolute value:`
175
+
176
+ \begin{equation*}
177
+ {{ e3 }} = {{ e3_eval }}.
178
+ \end{equation*}
179
+
180
+ {fr}`La valeur absolue de ce résultat est donc`{en}`The absolute value of this result is therefore`
181
+
182
+ \begin{equation*}
183
+ \left|{{ e3_eval }}\right| = {{ Abs(e3) }}.
184
+ \end{equation*}
185
+ ::::
186
+
187
+ ::::{weightDistribution}
188
+ :reasoning: 15
189
+ :logic: 15
190
+ :abstraction: 15
191
+ :calculation: 55
192
+ ::::
193
+ :::::
194
+
195
+ :::::{question}
196
+ :questionType: FGQ
197
+ :solution: [["ord","${{ latex(Abs(e4)) }}$"],["0"]]
198
+
199
+ ::::{questionStatement}
200
+ \begin{equation*}
201
+ \left|{{ latex(e4) }}\right|
202
+ \end{equation*}
203
+
204
+ {fr}`Cette expression vaut :` {en}`This expression equals:` {input}`||120`
205
+ ::::
206
+
207
+ ::::{questionHint}
208
+ {fr}`Calculez la somme entre parenthèses avant de diviser par le dénominateur, puis appliquez la valeur absolue.`{en}`Compute the sum in parentheses before dividing by the denominator, then apply the absolute value.`
209
+ ::::
210
+
211
+ ::::{displayedSolution}
212
+ $\ds {{ latex(Abs(e4)) }}$
213
+ ::::
214
+
215
+ ::::{detailedSolution}
216
+ {fr}`On commence par évaluer l'expression à l'intérieur de la valeur absolue :`{en}`Begin by evaluating the expression inside the absolute value:`
217
+
218
+ \begin{equation*}
219
+ \frac{1}{{{ e43 }}}\big({{ e42 }}+{{ e41 }}\big) &= \frac{1}{{{ e43 }}}\cdot {{ pxsl_pow(e41+e42) }} \\[10pt]
220
+ &= {{ latex(e4_eval) }}.
221
+ \end{equation*}
222
+
223
+ {fr}`La valeur absolue est alors`{en}`The absolute value is then`
224
+
225
+ \begin{equation*}
226
+ \Big|\frac{1}{{{ e43 }}}\big({{ e42 }}+{{ e41 }}\big)\Big| &= \left|{{ latex(e4_eval) }}\right| \\[10pt]
227
+ &= {{ latex(Abs(e4)) }}.
228
+ \end{equation*}
229
+ ::::
230
+
231
+ ::::{weightDistribution}
232
+ :reasoning: 20
233
+ :logic: 20
234
+ :abstraction: 20
235
+ :calculation: 40
236
+ ::::
237
+ :::::
238
+
239
+ :::::{question}
240
+ :questionType: FGQ
241
+ :solution: [["ord","${{ latex(Abs(e5)) }}$"],["0"]]
242
+
243
+ ::::{questionStatement}
244
+ \begin{equation*}
245
+ \Big|{{ latex(e5) }}\Big|
246
+ \end{equation*}
247
+
248
+ {fr}`Cette expression vaut :` {en}`This expression equals:` {input}`||120`
249
+ ::::
250
+
251
+ ::::{questionHint}
252
+ {fr}`Le facteur commun au numérateur et au dénominateur se simplifie complètement.`{en}`The common factor in numerator and denominator cancels out completely.`
253
+ ::::
254
+
255
+ ::::{displayedSolution}
256
+ ${{ latex(Abs(e5)) }}$
257
+ ::::
258
+
259
+ ::::{detailedSolution}
260
+ {fr}`La fraction se simplifie d'abord :`{en}`The fraction simplifies first:`
261
+
262
+ \begin{equation*}
263
+ \frac{{{ e51 }}\cdot {{ pxsl_pow(e52) }}}{{{ e51 }}} = {{ e5_eval }}.
264
+ \end{equation*}
265
+
266
+ {fr}`La valeur absolue devient donc`{en}`Hence the absolute value becomes`
267
+
268
+ \begin{equation*}
269
+ \Big|\frac{{{ e51 }}\cdot {{ pxsl_pow(e52) }}}{{{ e51 }}}\Big| &= \left|{{ e5_eval }}\right| \\[10pt]
270
+ &= {{ latex(Abs(e5)) }}.
271
+ \end{equation*}
272
+ ::::
273
+
274
+ ::::{weightDistribution}
275
+ :reasoning: 20
276
+ :logic: 20
277
+ :abstraction: 20
278
+ :calculation: 40
279
+ ::::
280
+ :::::
281
+
282
+
283
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Zone de rentabilité symétrique autour de l'optimum`{en}`Symmetric profitability zone around the optimum`.md ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :title: {fr}`Zone de rentabilité symétrique autour de l'optimum`{en}`Symmetric profitability zone around the optimum`
3
+ :modules:
4
+ :recommendedExecutionTime: 15
5
+ :level: Elementary
6
+ :chap: chap_realFunctions_Graphs_graphSymmetries_ESCP
7
+ :involvedConcepts:
8
+ :originalSource:
9
+ :visibility: All
10
+ :variations:
11
+ :comment: Appliqué (économie & gestion) : axe de symétrie et maximum d'une parabole de profit, seuils de rentabilité par factorisation, et symétrie des seuils par rapport à l'optimum (sans discriminant ni Viète).
12
+ :id: df8f8e47-7480-11f1-a8a1-0ed8d3b012a9
13
+ :originalExerciseId: e1c9656e-6409-11f1-a8a1-0ed8d3b012a9
14
+
15
+ ````{python}
16
+ import random as rd
17
+ from sympy import symbols, latex
18
+ from pyxiscience.Mes_fctions_generalistes_bis import pxs_config, pxsl_latex_coefficient as lc
19
+
20
+ config_standard = pxs_config()
21
+ q = symbols('q')
22
+
23
+ # qStar entier, q1 < qStar < q2 symétriques, a<0, racines q1,q2
24
+ # coefficient négatif
25
+ a = rd.choice([-5, -4, -3, -2, -1])
26
+
27
+ # choisir q_star directement compatible avec 20 <= b <= 60
28
+ # b = -2 a q_star
29
+ # donc q_star = b / (-2a)
30
+
31
+ possible_qstars = []
32
+
33
+ for q in range(3, 16):
34
+ b = -2 * a * q
35
+ if 20 <= b <= 60:
36
+ possible_qstars.append(q)
37
+
38
+ qStar = rd.choice(possible_qstars)
39
+
40
+ # construction des racines symétriques
41
+ q1 = rd.randint(1, qStar - 2)
42
+ q2 = 2 * qStar - q1
43
+
44
+ # coefficients
45
+ b = -2 * a * qStar
46
+ c = a * q1 * q2
47
+
48
+ Pi = a*q**2 + b*q + c
49
+ profitMax = a * qStar**2 + b * qStar + c
50
+ somme_racines = q1 + q2
51
+ produit_racines = q1 * q2
52
+ factAff = f"(q-{q1})(q-{q2})"
53
+ distance = qStar - q1
54
+
55
+ # Rendus
56
+ aLcAff = lc(a)
57
+ aMulAff = lc(a, ones=True)
58
+ bSignAff = lc(b, sign=True)
59
+ cSignAff = lc(c, ones=True, sign=True)
60
+ twoAAff = latex(2*a)
61
+ aQStarSqAff = latex(a*qStar**2)
62
+ bQStarSignAff = lc(b*qStar, ones=True, sign=True)
63
+ sommeRacines = somme_racines
64
+ produitRacines = produit_racines
65
+ negSommeSignAff = lc(-somme_racines, sign=True)
66
+ produitSignAff = lc(produit_racines, ones=True, sign=True)
67
+ q1PlusQ2 = q1 + q2
68
+
69
+ globals()
70
+ ````
71
+
72
+ {fr}`Une entreprise modélise son profit (en milliers d'euros) par`{en}`A company models its profit (in thousands of euros) by` $\Pi(q)={{ aLcAff }}q^2{{ bSignAff }}q{{ cSignAff }}$, $q\geq 0$ {fr}`(quantité en centaines d'unités).`{en}`(quantity in hundreds of units).`
73
+
74
+ :::::{question}
75
+ :questionType: FGQ
76
+ :solution: [["ord","${{ qStar }}$"],["0"]]
77
+ :questionId: 0
78
+ :questionIndex: 0
79
+
80
+ ::::{questionStatement}
81
+ {fr}`Déterminer l'axe de symétrie`{en}`Determine the axis of symmetry` $q^*$ {fr}`de la parabole.`{en}`of the parabola.`
82
+
83
+ $q^* =$ {input}`||90`
84
+ ::::
85
+
86
+ ::::{questionHint}
87
+ {fr}`Appliquer`{en}`Apply` $q^*=-\dfrac{b}{2a}$ {fr}`avec`{en}`with` $a={{ a }}$, $b={{ b }}$.
88
+ ::::
89
+
90
+ ::::{displayedSolution}
91
+ $q^* = {{ qStar }}$
92
+ ::::
93
+
94
+ ::::{detailedSolution}
95
+ {fr}`Règle utilisée : axe de symétrie`{en}`Rule used: axis of symmetry` $q^*=-\dfrac{b}{2a}$.
96
+
97
+ \begin{equation*}
98
+ q^* &= -\frac{ {{ b }} }{2\times({{ a }})} \\
99
+ &= -\frac{ {{ b }} }{ {{ twoAAff }} } \\
100
+ &= {{ qStar }}.
101
+ \end{equation*}
102
+
103
+ {fr}`L'axe de symétrie est`{en}`The axis of symmetry is` $q={{ qStar }}$.
104
+ ::::
105
+
106
+ ::::{weightDistribution}
107
+ :logic: 15
108
+ :abstraction: 25
109
+ :reasoning: 30
110
+ :calculation: 30
111
+ ::::
112
+ :::::
113
+
114
+ :::::{question}
115
+ :questionType: FGQ
116
+ :solution: [["ord","${{ profitMax }}$"],["0"]]
117
+ :questionId: 1
118
+ :questionIndex: 1
119
+
120
+ ::::{questionStatement}
121
+ {fr}`Calculer le profit maximal`{en}`Calculate the maximum profit` $\Pi(q^*)$.
122
+
123
+ $\Pi(q^*) =$ {input}`||90`
124
+ ::::
125
+
126
+ ::::{questionHint}
127
+ {fr}`Substituer`{en}`Substitute` $q={{ qStar }}$ {fr}`dans`{en}`in` $\Pi(q)$ ; {fr}`comme`{en}`since` $a<0$, {fr}`c'est un maximum.`{en}`it is a maximum.`
128
+ ::::
129
+
130
+ ::::{displayedSolution}
131
+ $\Pi(q^*) = {{ profitMax }}$
132
+ ::::
133
+
134
+ ::::{detailedSolution}
135
+ {fr}`On évalue`{en}`We evaluate` $\Pi$ {fr}`au sommet.`{en}`at the vertex.`
136
+
137
+ \begin{equation*}
138
+ \Pi({{ qStar }}) &= {{ aMulAff }}\times {{ qStar }}^2 {{ bSignAff }}\times {{ qStar }} {{ cSignAff }} \\
139
+ &= {{ aQStarSqAff }} {{ bQStarSignAff }} {{ cSignAff }} \\
140
+ &= {{ profitMax }}.
141
+ \end{equation*}
142
+
143
+ {fr}`Le profit maximal est de`{en}`The maximum profit is` ${{ profitMax }}$ {fr}`milliers d'euros.`{en}`thousands of euros.`
144
+ ::::
145
+
146
+ ::::{weightDistribution}
147
+ :logic: 15
148
+ :abstraction: 25
149
+ :reasoning: 30
150
+ :calculation: 30
151
+ ::::
152
+ :::::
153
+
154
+ :::::{question}
155
+ :questionType: FGQ
156
+ :solution: [["notord","${{ q1 }}$","${{ q2 }}$"],["0","0"]]
157
+ :questionId: 2
158
+ :questionIndex: 2
159
+
160
+ ::::{questionStatement}
161
+ {fr}`Déterminer les deux seuils de rentabilité`{en}`Determine the two break-even thresholds` $q_1$ {fr}`et`{en}`and` $q_2$ ($\Pi(q)=0$) {fr}`par factorisation.`{en}`by factorization.`
162
+
163
+ $q_1 =$ {input}`||90` {fr}`et`{en}`and` $q_2 =$ {input}`||90`
164
+ ::::
165
+
166
+ ::::{questionHint}
167
+ {fr}`Diviser par`{en}`Divide by` ${{ a }}$ {fr}`pour obtenir`{en}`to obtain` $q^2{{ negSommeSignAff }}q{{ produitSignAff }}=0$, {fr}`puis factoriser (deux nombres de produit`{en}`then factor (two numbers with product` ${{ produitRacines }}$ {fr}`et de somme`{en}`and sum` ${{ sommeRacines }}$).
168
+ ::::
169
+
170
+ ::::{displayedSolution}
171
+ $q_1 = {{ q1 }}$ {fr}`et`{en}`and` $q_2 = {{ q2 }}$
172
+ ::::
173
+
174
+ ::::{detailedSolution}
175
+ {fr}`On résout`{en}`We solve` $\Pi(q)=0$ {fr}`par factorisation (sans discriminant).`{en}`by factorization (without discriminant).`
176
+
177
+ \begin{equation*}
178
+ {{ aLcAff }}q^2 {{ bSignAff }}q {{ cSignAff }} &= 0 \\
179
+ q^2 {{ negSommeSignAff }}q {{ produitSignAff }} &= 0 \\
180
+ {{ factAff }} &= 0.
181
+ \end{equation*}
182
+
183
+ \begin{equation*}
184
+ q_1 &= {{ q1 }} \quad\text{{fr}`et`{en}`and`}\quad q_2 = {{ q2 }}.
185
+ \end{equation*}
186
+ ::::
187
+
188
+ ::::{weightDistribution}
189
+ :logic: 20
190
+ :abstraction: 25
191
+ :reasoning: 30
192
+ :calculation: 25
193
+ ::::
194
+ :::::
195
+
196
+ :::::{question}
197
+ :questionType: FGQ
198
+ :solution: [["ord","${{ qStar }}$"],["0"]]
199
+ :questionId: 3
200
+ :questionIndex: 3
201
+
202
+ ::::{questionStatement}
203
+ {fr}`Montrer que`{en}`Show that` $q_1$ {fr}`et`{en}`and` $q_2$ {fr}`sont symétriques par rapport à`{en}`are symmetric with respect to` $q^*$, {fr}`puis interpréter : que peut-on dire de la zone de rentabilité de l'entreprise ?`{en}`then interpret: what can be said about the company's profitability zone?`
204
+
205
+ $\dfrac{q_1+q_2}{2} =$ {input}`||90`
206
+ ::::
207
+
208
+ ::::{questionHint}
209
+ {fr}`Calculer la moyenne`{en}`Calculate the average` $\dfrac{q_1+q_2}{2}$ {fr}`et la comparer à`{en}`and compare it to` $q^*$.
210
+ ::::
211
+
212
+ ::::{displayedSolution}
213
+ $\dfrac{q_1+q_2}{2} = {{ qStar }}$
214
+ ::::
215
+
216
+ ::::{detailedSolution}
217
+ {fr}`Règle utilisée : l'axe de symétrie d'une parabole passe par le milieu de ses deux zéros.`{en}`Rule used: the axis of symmetry of a parabola passes through the midpoint of its two zeros.`
218
+
219
+ \begin{equation*}
220
+ \frac{q_1 + q_2}{2} &= \frac{ {{ q1 }} + {{ q2 }} }{2} \\
221
+ &= \frac{ {{ q1PlusQ2 }} }{2} \\
222
+ &= {{ qStar }}.
223
+ \end{equation*}
224
+
225
+ {fr}`Les deux seuils sont symétriques par rapport à`{en}`The two thresholds are symmetric with respect to` $q^*={{ qStar }}$. {fr}`L'entreprise est rentable`{en}`The company is profitable` ($\Pi>0$) {fr}`pour`{en}`for` $q$ {fr}`entre`{en}`between` ${{ q1 }}$ {fr}`et`{en}`and` ${{ q2 }}$. {fr}`La zone de rentabilité est symétrique autour de l'optimum : les deux seuils sont à égale distance`{en}`The profitability zone is symmetric around the optimum: the two thresholds are at equal distance` (${{ distance }}$ {fr}`centaines d'unités) de la quantité optimale, donc l'entreprise dispose de la même marge de manœuvre en sous-production ou en surproduction.`{en}`hundreds of units) from the optimal quantity, so the company has the same margin of maneuver in underproduction or overproduction.`
226
+ ::::
227
+
228
+ ::::{weightDistribution}
229
+ :logic: 30
230
+ :abstraction: 30
231
+ :reasoning: 35
232
+ :calculation: 5
233
+ ::::
234
+ :::::
235
+
236
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Équations exponentielles et logarithmiques`{en}`Exponential and Logarithmic Equations`.md ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :title: {fr}`Équations exponentielles et logarithmiques`{en}`Exponential and Logarithmic Equations`
3
+ :modules:
4
+ :recommendedExecutionTime: 15
5
+ :level: Elementary
6
+ :chap: chap_expLogFunctions_solvingExpLogEquations_ESCP
7
+ :involvedConcepts: Domain_of_function, solving_equ_with_ln, propers_of_log_and_exp
8
+ :originalSource: % Exercise source
9
+ :visibility: All
10
+ :comment: % Exercise comment (optional)
11
+ :originalExerciseId: b41f6b56-65a4-11f1-a8a1-0ed8d3b012a9
12
+ :id: 91f28cdc-74bf-11f1-a8a1-0ed8d3b012a9
13
+
14
+ ```{python}
15
+ import random as rd
16
+ from sympy import Symbol, Rational, ln, exp, latex, simplify
17
+ from pyxiscience.Mes_fctions_generalistes_bis import pxs_config, pxsl_latex_coefficient as lc
18
+ config_standard = pxs_config()
19
+
20
+ # ═══════════════════════════════════════════════════════════════════════════
21
+ # MÉTHODE CONSTRUCTIVE — partir de la réponse
22
+ # ═══════════════════════════════════════════════════════════════════════════
23
+
24
+ # Question 1 : e^x = a1
25
+ # Réponse : x = ln(a1) avec a1 > 0, a1 ≠ 1
26
+ # On tire d'abord la réponse (ln(a1)), puis on fabrique l'énoncé (e^x = a1)
27
+ a1 = rd.choice([i for i in range(2, 11) if i != 1])
28
+ sol1 = ln(a1)
29
+ sol1Aff = latex(sol1, **config_standard)
30
+ a1Aff = latex(a1, **config_standard)
31
+
32
+ # Question 2 : e^(coef_exp·x) = a2
33
+ # Réponse : x = ln(a2)/coef_exp avec a2 > 0, a2 ≠ 1, coef_exp ≥ 2
34
+ # On tire d'abord coef_exp et a2, puis on calcule la réponse
35
+ coefExp = rd.randint(2, 5)
36
+ a2 = rd.choice([i for i in range(2, 11) if i != 1])
37
+ sol2 = ln(a2) / coefExp
38
+ sol2Aff = latex(sol2, **config_standard)
39
+ a2Aff = latex(a2, **config_standard)
40
+ coefExpAff = latex(coefExp, **config_standard)
41
+
42
+ # Affichage de l'exposant dans l'énoncé (2x, 3x, etc.)
43
+ x = Symbol('x')
44
+ expExprAff = latex(coefExp * x, **config_standard)
45
+
46
+ globals()
47
+ ```
48
+
49
+ {fr}`Résoudre dans`{en}`Solve in` $\mathbb{R}$ {fr}`les équations suivantes :`{en}`the following equations:`
50
+
51
+ :::::{question}
52
+ :questionType: FGQ
53
+ :solution: [["ord","${{ sol1Aff }}$"],["0"]]
54
+
55
+ ::::{questionStatement}
56
+ $e^x = {{ a1Aff }}$
57
+
58
+ $x =$ {input}`||150`
59
+ ::::
60
+
61
+ ::::{questionHint}
62
+ {fr}`Appliquer l'équivalence fondamentale entre exponentielle et logarithme.`{en}`Apply the fundamental equivalence between exponential and logarithm.`
63
+ ::::
64
+
65
+ ::::{displayedSolution}
66
+ $x = {{ sol1Aff }}$
67
+ ::::
68
+
69
+ ::::{detailedSolution}
70
+ {fr}`On rappelle l'équivalence fondamentale : pour tout`{en}`Recall the fundamental equivalence: for all` $x \in \mathbb{R}$ {fr}`et tout`{en}`and all` $y > 0$,
71
+
72
+ \begin{equation*}
73
+ e^x = y \iff x = \ln y.
74
+ \end{equation*}
75
+
76
+ {fr}`De même, pour`{en}`Similarly, for` $u > 0$ : $\ln u = k \iff u = e^k$.
77
+
78
+ {fr}`L'équation`{en}`The equation` $e^x = {{ a1Aff }}$ {fr}`est définie pour tout`{en}`is defined for all` $x \in \mathbb{R}$. {fr}`Par l'équivalence fondamentale :`{en}`By the fundamental equivalence:`
79
+
80
+ \begin{equation*}
81
+ e^x = {{ a1Aff }} \iff x = \ln {{ a1Aff }}.
82
+ \end{equation*}
83
+
84
+ {fr}`La solution est`{en}`The solution is` $x = {{ sol1Aff }}$.
85
+ ::::
86
+
87
+ ::::{weightDistribution}
88
+ :reasoning: 20
89
+ :logic: 30
90
+ :abstraction: 20
91
+ :calculation: 30
92
+ ::::
93
+ :::::
94
+
95
+ :::::{question}
96
+ :questionType: FGQ
97
+ :solution: [["ord","${{ sol2Aff }}$"],["0"]]
98
+
99
+ ::::{questionStatement}
100
+ $e^{ {{ expExprAff }} } = {{ a2Aff }}$
101
+
102
+ $x =$ {input}`||150`
103
+ ::::
104
+
105
+ ::::{questionHint}
106
+ {fr}`Isoler l'exposant en appliquant le logarithme népérien des deux côtés, puis diviser par le coefficient de`{en}`Isolate the exponent by applying the natural logarithm to both sides, then divide by the coefficient of` $x$.
107
+ ::::
108
+
109
+ ::::{displayedSolution}
110
+ $x = {{ sol2Aff }}$
111
+ ::::
112
+
113
+ ::::{detailedSolution}
114
+ {fr}`L'équation`{en}`The equation` $e^{ {{ expExprAff }} } = {{ a2Aff }}$ {fr}`est définie pour tout`{en}`is defined for all` $x \in \mathbb{R}$. {fr}`On applique l'équivalence fondamentale :`{en}`We apply the fundamental equivalence:`
115
+
116
+ \begin{equation*}
117
+ e^{ {{ expExprAff }} } = {{ a2Aff }} &\iff {{ expExprAff }} = \ln {{ a2Aff }} \\
118
+ &\iff x = \dfrac{\ln {{ a2Aff }} }{ {{ coefExpAff }} }.
119
+ \end{equation*}
120
+
121
+ {fr}`La solution est`{en}`The solution is` $x = {{ sol2Aff }}$.
122
+ ::::
123
+
124
+ ::::{weightDistribution}
125
+ :reasoning: 25
126
+ :logic: 25
127
+ :abstraction: 20
128
+ :calculation: 30
129
+ ::::
130
+ :::::
131
+
132
+ ```{python}
133
+ import random as rd
134
+ from sympy import Symbol, Rational, ln, exp, latex, simplify
135
+ from pyxiscience.Mes_fctions_generalistes_bis import pxs_config, pxsl_latex_coefficient as lc
136
+ config_standard = pxs_config()
137
+
138
+ # ═══════════════════════════════════════════════════════════════════════════
139
+ # MÉTHODE CONSTRUCTIVE — partir de la réponse
140
+ # ═══════════════════════════════════════════════════════════════════════════
141
+
142
+ # Question 3 : ln(x) = k
143
+ # Réponse : x = e^k avec k entier ≥ 2
144
+ # On tire d'abord k (qui détermine la réponse), puis on fabrique l'énoncé
145
+ k = rd.randint(2, 6)
146
+ sol3 = exp(k)
147
+ sol3Aff = latex(sol3, **config_standard)
148
+ kAff = latex(k, **config_standard)
149
+
150
+ # Question 4 : ln(coefLn·x - constLn) = valLn
151
+ # Réponse : x = (e^valLn + constLn) / coefLn
152
+ # On tire d'abord coefLn, valLn, puis constLn tel que la solution soit > constLn/coefLn
153
+ # (pour garantir le domaine de définition)
154
+ coefLn = rd.randint(2, 5)
155
+ valLn = rd.choice([0, 1, -1])
156
+ # Pour que x > constLn/coefLn, on tire constLn puis on vérifie que la solution construite
157
+ # satisfait automatiquement cette condition (construction directe)
158
+ # Solution : x = (e^valLn + constLn) / coefLn
159
+ # Domaine : coefLn·x - constLn > 0 ⟺ x > constLn/coefLn
160
+ # On choisit constLn < coefLn·e^valLn pour garantir que la solution est dans le domaine
161
+ constLn = rd.randint(1, min(4, coefLn - 1))
162
+
163
+ # Calcul de la solution
164
+ sol4Num = exp(valLn) + constLn
165
+ sol4Den = coefLn
166
+ # Simplification si possible
167
+ from math import gcd
168
+ g = gcd(int(sol4Num) if sol4Num == int(sol4Num) else 1, sol4Den)
169
+ sol4NumSimp = sol4Num / g
170
+ sol4DenSimp = sol4Den / g
171
+
172
+ # Calcul de la borne du domaine
173
+ borneNum = constLn
174
+ borneDen = coefLn
175
+ gBorne = gcd(borneNum, borneDen)
176
+ borneNumSimp = borneNum // gBorne
177
+ borneDenSimp = borneDen // gBorne
178
+
179
+ # Affichage de la borne du domaine
180
+ if borneDenSimp == 1:
181
+ borneAff = latex(borneNumSimp, **config_standard)
182
+ else:
183
+ borneAff = latex(Rational(borneNumSimp, borneDenSimp), **config_standard)
184
+
185
+ # Affichage de la solution
186
+ if valLn == 0:
187
+ # e^0 = 1, donc solution = (1 + constLn) / coefLn
188
+ solNum = 1 + constLn
189
+ gSol = gcd(solNum, coefLn)
190
+ solNumFinal = solNum // gSol
191
+ solDenFinal = coefLn // gSol
192
+ if solDenFinal == 1:
193
+ sol4Aff = latex(solNumFinal, **config_standard)
194
+ else:
195
+ sol4Aff = latex(Rational(solNumFinal, solDenFinal), **config_standard)
196
+ else:
197
+ # Solution générale avec e^valLn
198
+ sol4Aff = r"\dfrac{e^{" + latex(valLn, **config_standard) + r"} + " + latex(constLn, **config_standard) + r"}{" + latex(coefLn, **config_standard) + r"}"
199
+
200
+ # Affichage de l'argument du logarithme
201
+ x = Symbol('x')
202
+ argLn = coefLn * x - constLn
203
+ argLnAff = latex(argLn, **config_standard)
204
+
205
+ # Affichage de valLn
206
+ valLnAff = latex(valLn, **config_standard)
207
+
208
+ # Affichage des étapes intermédiaires pour la solution 4
209
+ # Étape 1 : coefLn·x - constLn = e^valLn
210
+ if valLn == 0:
211
+ etape1Aff = latex(coefLn, **config_standard) + r" x - " + latex(constLn, **config_standard) + r" = 1"
212
+ else:
213
+ etape1Aff = latex(coefLn, **config_standard) + r" x - " + latex(constLn, **config_standard) + r" = e^{" + latex(valLn, **config_standard) + r"}"
214
+
215
+ # Étape 2 : coefLn·x = e^valLn + constLn
216
+ if valLn == 0:
217
+ etape2Aff = latex(coefLn, **config_standard) + r" x = " + latex(1 + constLn, **config_standard)
218
+ else:
219
+ etape2Aff = latex(coefLn, **config_standard) + r" x = e^{" + latex(valLn, **config_standard) + r"} + " + latex(constLn, **config_standard)
220
+
221
+ # Affichage de coefLn pour l'inégalité du domaine
222
+ coefLnAff = latex(coefLn, **config_standard)
223
+ constLnAff = latex(constLn, **config_standard)
224
+
225
+ globals()
226
+ ```
227
+
228
+ :::::{question}
229
+ :questionType: FGQ
230
+ :solution: [["ord","${{ sol3Aff }}$"],["0"]]
231
+
232
+ ::::{questionStatement}
233
+ $\ln x = {{ kAff }}$ {fr}`(on précisera le domaine de définition)`{en}`(specify the domain of definition)`
234
+
235
+ $x =$ {input}`||150`
236
+ ::::
237
+
238
+ ::::{questionHint}
239
+ {fr}`Utiliser la définition du logarithme népérien comme réciproque de la fonction exponentielle.`{en}`Use the definition of the natural logarithm as the inverse of the exponential function.`
240
+ ::::
241
+
242
+ ::::{displayedSolution}
243
+ $x = {{ sol3Aff }}$
244
+ ::::
245
+
246
+ ::::{detailedSolution}
247
+ {fr}`La fonction`{en}`The function` $\ln$ {fr}`est définie sur`{en}`is defined on` $\mathbb{R}_{+}^{*}$, {fr}`donc l'équation`{en}`so the equation` $\ln x = {{ kAff }}$ {fr}`impose`{en}`requires` $x > 0$. {fr}`Par définition du logarithme comme réciproque de l'exponentielle :`{en}`By definition of the logarithm as the inverse of the exponential:`
248
+
249
+ \begin{equation*}
250
+ \ln x = {{ kAff }} \iff x = {{ sol3Aff }}.
251
+ \end{equation*}
252
+
253
+ {fr}`La solution est`{en}`The solution is` $x = {{ sol3Aff }}$.
254
+ ::::
255
+
256
+ ::::{weightDistribution}
257
+ :reasoning: 20
258
+ :logic: 30
259
+ :abstraction: 25
260
+ :calculation: 25
261
+ ::::
262
+ :::::
263
+
264
+ :::::{question}
265
+ :questionType: FGQ
266
+ :solution: [["ord","${{ sol4Aff }}$"],["0"]]
267
+
268
+ ::::{questionStatement}
269
+ $\ln({{ argLnAff }}) = {{ valLnAff }}$ {fr}`(on précisera le domaine de définition)`{en}`(specify the domain of definition)`
270
+
271
+ $x =$ {input}`||200`
272
+ ::::
273
+
274
+ ::::{questionHint}
275
+ {fr}`Déterminer le domaine de définition en résolvant une inégalité, puis appliquer l'exponentielle aux deux membres pour éliminer le logarithme.`{en}`Determine the domain of definition by solving an inequality, then apply the exponential to both sides to eliminate the logarithm.`
276
+ ::::
277
+
278
+ ::::{displayedSolution}
279
+ $x = {{ sol4Aff }}$
280
+ ::::
281
+
282
+ ::::{detailedSolution}
283
+ {fr}`La fonction`{en}`The function` $\ln({{ argLnAff }})$ {fr}`est définie si et seulement si`{en}`is defined if and only if` ${{ argLnAff }} > 0$, {fr}`c'est-à-dire`{en}`that is` ${{ coefLnAff }} x > {{ constLnAff }}$, {fr}`soit`{en}`i.e.` $x > {{ borneAff }}$. {fr}`Sur ce domaine :`{en}`On this domain:`
284
+
285
+ \begin{equation*}
286
+ \ln({{ argLnAff }}) = {{ valLnAff }} &\iff {{ etape1Aff }} \\
287
+ &\iff {{ etape2Aff }} \\
288
+ &\iff x = {{ sol4Aff }}.
289
+ \end{equation*}
290
+
291
+ {fr}`On vérifie que`{en}`We verify that` $\ds {{ sol4Aff }} > {{ borneAff }}$ : {fr}`la condition de domaine est bien satisfaite. La solution est`{en}`the domain condition is indeed satisfied. The solution is` $x = {{ sol4Aff }}$.
292
+ ::::
293
+
294
+ ::::{weightDistribution}
295
+ :reasoning: 30
296
+ :logic: 25
297
+ :abstraction: 25
298
+ :calculation: 20
299
+ ::::
300
+ :::::
301
+
302
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Équilibre de marché _ l'offre rencontre la demande`{en}`Market Equilibrium_ Supply Meets Demand`.md ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :title: {fr}`Équilibre de marché : l'offre rencontre la demande`{en}`Market Equilibrium: Supply Meets Demand`
3
+ :modules:
4
+ :recommendedExecutionTime: 10
5
+ :level: Elementary
6
+ :chap: chap_lines_Systems_economicEquilibriumBreakeven_ESCP
7
+ :involvedConcepts:
8
+ :originalSource: Session 4 — Erwan Lamy, ESCP Business School
9
+ :visibility: All
10
+ :variations:
11
+ :comment: Appliqué (micro-économie) : sens de variation de l'offre et de la demande, point d'équilibre par résolution de S(q) = D(q), vérification.
12
+ :id: 0924e9e6-74c0-11f1-a8a1-0ed8d3b012a9
13
+ :originalExerciseId: 0177637c-64e3-11f1-a8a1-0ed8d3b012a9
14
+
15
+ ````{python}
16
+ import random as rd
17
+ from pyxiscience.Mes_fctions_generalistes_bis import pxs_config, pxsl_latex_coefficient as lc, pxsl_format_number
18
+ # --- Helpers PyxiScience (definis inline, autonomes : aucun import externe) ---
19
+
20
+ config_standard = pxs_config()
21
+
22
+ # S croissante (penteS>0), D decroissante (penteD<0), ordD>ordS,
23
+ # equilibre q* entier positif
24
+ for _ in range(300):
25
+ penteS = rd.randint(1, 5)
26
+ ordS = rd.randint(2, 10)
27
+ penteD = rd.randint(-5, -1)
28
+ ordD = rd.randint(15, 30)
29
+ if ordD > ordS and (ordD - ordS) % (penteS - penteD) == 0:
30
+ qStar = (ordD - ordS) // (penteS - penteD)
31
+ if qStar > 0:
32
+ break
33
+
34
+ pStar = penteS * qStar + ordS
35
+ pStarVerif = penteD * qStar + ordD # = pStar
36
+ pots = qStar * 100
37
+
38
+ # Affichage
39
+ penteSCoefAff = lc(penteS); ordSConstAff = lc(ordS, ones=True, sign=True)
40
+ penteDCoefAff = lc(penteD); ordDConstAff = lc(ordD, ones=True, sign=True)
41
+ penteSAff = str(penteS); penteDAff = str(penteD)
42
+ penteDiffAff = lc(penteS - penteD); rhsEq = ordD - ordS
43
+
44
+ globals()
45
+ ````
46
+
47
+ {fr}`Sur le marché du miel bio (prix `{en}`On the organic honey market (price `$p${fr}` en €/pot, quantité `{en}` in €/jar, quantity `$q${fr}` en centaines de pots par semaine), l'offre et la demande sont `{en}` in hundreds of jars per week), supply and demand are `$S(q) = {{penteSCoefAff}}q {{ordSConstAff}}${fr}` et `{en}` and `$D(q) = {{penteDCoefAff}}q {{ordDConstAff}}$.
48
+
49
+ :::::{question}
50
+ :questionType: FGQ
51
+ :questionId: 0
52
+ :questionIndex: 0
53
+ :solution: [["ord","${{ penteS }}$","${{ ordS }}$","${{ penteD }}$","${{ ordD }}$"],["0","0","0","0"]]
54
+
55
+ ::::{questionStatement}
56
+ {fr}`Vérifier que `{en}`Check that `$S${fr}` est croissante et `{en}` is increasing and `$D${fr}` décroissante, et donner leurs coefficients directeurs et ordonnées à l'origine.`{en}` decreasing, and give their slopes and y-intercepts.`
57
+ - $S$ {fr}`: coefficient directeur`{en}`: slope` {input}`||80` {fr}`, ordonnée à l'origine`{en}`, y-intercept` {input}`||80`
58
+ - $D$ {fr}`: coefficient directeur`{en}`: slope` {input}`||80` {fr}`, ordonnée à l'origine`{en}`, y-intercept` {input}`||80`
59
+ ::::
60
+
61
+ ::::{questionHint}
62
+ {fr}`Le signe du coefficient directeur donne le sens de variation.`{en}`The sign of the slope gives the direction of variation.`
63
+ ::::
64
+
65
+ ::::{displayedSolution}
66
+ - $S$ {fr}`: pente`{en}`: slope` ${{ penteS }}$ {fr}`, ordonnée`{en}`, intercept` ${{ ordS }}$
67
+ - $D$ {fr}`: pente`{en}`: slope` ${{ penteD }}$ {fr}`, ordonnée`{en}`, intercept` ${{ ordD }}$
68
+ ::::
69
+
70
+ ::::{detailedSolution}
71
+ {fr}`Pour `{en}`For `$S(q) = {{penteSCoefAff}}q {{ordSConstAff}}${fr}` : coefficient directeur `{en}`: slope `${{penteS}} > 0${fr}`, donc `{en}`, so `$S${fr}` est croissante (les producteurs offrent davantage quand le prix monte) ; ordonnée à l'origine `{en}` is increasing (producers supply more as the price rises); y-intercept `${{ordS}}${fr}` (prix d'offre minimal).`{en}` (minimum supply price).`
72
+
73
+ {fr}`Pour `{en}`For `$D(q) = {{penteDCoefAff}}q {{ordDConstAff}}${fr}` : coefficient directeur `{en}`: slope `${{penteD}} < 0${fr}`, donc `{en}`, so `$D${fr}` est décroissante (les consommateurs demandent moins quand le prix monte) ; ordonnée à l'origine `{en}` is decreasing (consumers demand less as the price rises); y-intercept `${{ordD}}$.
74
+ ::::
75
+
76
+ ::::{weightDistribution}
77
+ :logic: 15
78
+ :abstraction: 25
79
+ :reasoning: 30
80
+ :calculation: 30
81
+ ::::
82
+ :::::
83
+
84
+ :::::{question}
85
+ :questionType: FGQ
86
+ :questionId: 1
87
+ :questionIndex: 1
88
+ :solution: [["ord","${{ qStar }}$"],["0"]]
89
+
90
+ ::::{questionStatement}
91
+ {fr}`Déterminer la quantité d'équilibre `{en}`Determine the equilibrium quantity `$q^{*}${fr}` en résolvant `{en}` by solving `$S(q) = D(q)$.\
92
+ \
93
+ $q^*$ = {input}`||80`
94
+ ::::
95
+
96
+ ::::{questionHint}
97
+ {fr}`À l'équilibre, le prix d'offre égale le prix de demande : `{en}`At equilibrium, the supply price equals the demand price: `${{penteSCoefAff}}q {{ordSConstAff}} = {{penteDCoefAff}}q {{ordDConstAff}}$.
98
+ ::::
99
+
100
+ ::::{displayedSolution}
101
+ $q^* = {{ qStar }}$
102
+ ::::
103
+
104
+ ::::{detailedSolution}
105
+ {fr}`On résout `{en}`We solve `$S(q) = D(q)$ :
106
+
107
+ \begin{equation*}
108
+ {{penteSCoefAff}}q {{ordSConstAff}} &= {{penteDCoefAff}}q {{ordDConstAff}} \\
109
+ {{penteDiffAff}}q &= {{rhsEq}} \\
110
+ q^{*} &= {{qStar}}.
111
+ \end{equation*}
112
+
113
+ {fr}`La quantité d'équilibre est `{en}`The equilibrium quantity is `$q^{*} = {{qStar}}${fr}` (soit `{en}` (i.e. `${{pots}}${fr}` pots par semaine).`{en}` jars per week).`
114
+ ::::
115
+
116
+ ::::{weightDistribution}
117
+ :logic: 15
118
+ :abstraction: 25
119
+ :reasoning: 30
120
+ :calculation: 30
121
+ ::::
122
+ :::::
123
+
124
+ :::::{question}
125
+ :questionType: FGQ
126
+ :questionId: 2
127
+ :questionIndex: 2
128
+ :solution: [["ord","${{ pStar }}$"],["0"]]
129
+
130
+ ::::{questionStatement}
131
+ {fr}`En déduire le prix d'équilibre `{en}`Deduce the equilibrium price `$p^{*} = S(q^{*})$.\
132
+ \
133
+ $p^*$ = {input}`||80`
134
+ ::::
135
+
136
+ ::::{questionHint}
137
+ {fr}`Remplacer `{en}`Replace `$q${fr}` par `{en}` by `$q^{*}${fr}` dans la fonction d'offre.`{en}` in the supply function.`
138
+ ::::
139
+
140
+ ::::{displayedSolution}
141
+ $p^* = {{ pStar }}$
142
+ ::::
143
+
144
+ ::::{detailedSolution}
145
+ {fr}`On utilise la fonction d'offre :`{en}`We use the supply function:`
146
+
147
+ \begin{equation*}
148
+ p^{*} = S({{qStar}}) = {{penteSAff}} \times {{qStar}} {{ordSConstAff}} = {{pStar}}.
149
+ \end{equation*}
150
+
151
+ {fr}`soit`{en}`i.e.` ${{ pStar }}$ {fr}`€/pot.`{en}`€/jar.`
152
+ ::::
153
+
154
+ ::::{weightDistribution}
155
+ :logic: 10
156
+ :abstraction: 20
157
+ :reasoning: 25
158
+ :calculation: 45
159
+ ::::
160
+ :::::
161
+
162
+ :::::{question}
163
+ :questionType: FGQ
164
+ :questionId: 3
165
+ :questionIndex: 3
166
+ :solution: [["ord","${{ pStar }}$"],["0"]]
167
+
168
+ ::::{questionStatement}
169
+ {fr}`Vérifier la réponse à l'aide de `{en}`Check the answer using `$D(q^{*})$.\
170
+ \
171
+ $D(q^*)$ = {input}`||80`
172
+ ::::
173
+
174
+ ::::{questionHint}
175
+ {fr}`À l'équilibre, l'offre et la demande doivent donner le même prix.`{en}`At equilibrium, supply and demand must give the same price.`
176
+ ::::
177
+
178
+ ::::{displayedSolution}
179
+ $D(q^*) = {{ pStar }}$
180
+ ::::
181
+
182
+ ::::{detailedSolution}
183
+ {fr}`On calcule la demande en `{en}`We compute demand at `$q^{*} = {{qStar}}$ :
184
+
185
+ \begin{equation*}
186
+ D({{qStar}}) = {{penteDAff}} \times {{qStar}} {{ordDConstAff}} = {{pStar}}.
187
+ \end{equation*}
188
+
189
+ {fr}`soit`{en}`i.e.` ${{ pStar }}$ {fr}`€/pot.`{en}`€/jar.`
190
+
191
+ {fr}`On a `{en}`We have `$S({{qStar}}) = D({{qStar}}) = {{pStar}}${fr}` : le point d'équilibre est bien `{en}`: the equilibrium point is indeed `$(q^{*},\, p^{*}) = ({{qStar}},\, {{pStar}})$.
192
+ ::::
193
+
194
+ ::::{weightDistribution}
195
+ :logic: 15
196
+ :abstraction: 20
197
+ :reasoning: 30
198
+ :calculation: 35
199
+ ::::
200
+ :::::
201
+
202
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Équilibre offre-demande sur un marché`{en}`Supply-demand equilibrium in a market`.md ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :title: {fr}`Équilibre offre-demande sur un marché`{en}`Supply-demand equilibrium in a market`
3
+ :modules:
4
+ :recommendedExecutionTime: 15
5
+ :level: Elementary
6
+ :chap: chap_lines_Systems_economicEquilibriumBreakeven_ESCP
7
+ :involvedConcepts:
8
+ :originalSource:
9
+ :visibility: All
10
+ :variations:
11
+ :comment: Appliqué éco/gestion — prix et quantité d'équilibre comme intersection de la droite d'offre (croissante) et de la droite de demande (décroissante).
12
+ :id: 730a9473-74c0-11f1-a8a1-0ed8d3b012a9
13
+ :originalExerciseId: 0582ee6e-64e4-11f1-a8a1-0ed8d3b012a9
14
+
15
+ ````{python}
16
+ import random as rd
17
+ from pyxiscience.Mes_fctions_generalistes_bis import pxs_config, pxsl_latex_coefficient as lc
18
+ config_standard = pxs_config()
19
+
20
+ # Offre croissante, demande décroissante, équilibre ENTIER dans le quadrant positif
21
+ for _ in range(300):
22
+ a_offre = rd.randint(1, 5)
23
+ b_offre = rd.randint(2, 10)
24
+ a_demande = rd.randint(-5, -1)
25
+ b_demande = rd.randint(max(15, b_offre + 10), 40)
26
+ denom = a_offre - a_demande
27
+ num = b_demande - b_offre
28
+ if denom > 0 and num > 0 and num % denom == 0:
29
+ repQ = num // denom
30
+ repP = a_offre * repQ + b_offre
31
+ if 2 <= repQ <= 12 and repP > 0:
32
+ break
33
+
34
+ # Limites pour le graphique
35
+ q_max_graph = max(6.5, repQ * 1.3)
36
+ p_max_graph = max(32, b_demande + 3)
37
+
38
+ # Précalcul de l'affichage
39
+ slopeOffreAff = lc(a_offre)
40
+ interOffreAff = lc(b_offre, ones=True, sign=True)
41
+ slopeDemAff = lc(a_demande)
42
+ interDemAff = lc(b_demande, ones=True, sign=True)
43
+ slopeOffreSignAff = lc(a_offre, ones=True, sign=True)
44
+ slopeDemPlainAff = lc(a_demande, ones=True)
45
+ coefDiffAff = lc(a_offre - a_demande)
46
+ rhsAff = str(b_demande - b_offre)
47
+ aOffreAff = str(a_offre)
48
+ aDemandeAff = str(a_demande)
49
+ repQAff = str(repQ)
50
+ repPAff = str(repP)
51
+
52
+ # Libellés multilingues pour matplotlib
53
+ if pxs_lang == "fr":
54
+ label_offre_prefix = "Offre : "
55
+ label_demande_prefix = "Demande : "
56
+ label_q_axis = r"$q$ (milliers)"
57
+ label_p_axis = r"$p$ (euros)"
58
+ else:
59
+ label_offre_prefix = "Supply: "
60
+ label_demande_prefix = "Demand: "
61
+ label_q_axis = r"$q$ (thousands)"
62
+ label_p_axis = r"$p$ (euros)"
63
+
64
+ globals()
65
+ ````
66
+
67
+ :::::{question}
68
+ :questionType: FGQ
69
+ :questionId: 0
70
+ :questionIndex: 0
71
+ :solution: [["ord","${{ a_offre }}$","${{ a_demande }}$"],["0","0"]]
72
+
73
+ ::::{questionStatement}
74
+ {fr}`Sur le marché d'un bien de consommation, l'offre et la demande (prix`{en}`In the market for a consumer good, supply and demand (price` $p$ {fr}`en euros par unité, en fonction de la quantité`{en}`in euros per unit, as a function of quantity` $q$ {fr}`en milliers d'unités) sont`{en}`in thousands of units) are` $\ds \text{{fr}`Offre`{en}`Supply`} : p = {{slopeOffreAff}}q {{interOffreAff}}$ {fr}`et`{en}`and` $\ds \text{{fr}`Demande`{en}`Demand`} : p = {{slopeDemAff}}q {{interDemAff}}$. {fr}`Vérifier que la courbe d'offre est croissante et celle de demande décroissante, et interpréter économiquement.`{en}`Verify that the supply curve is increasing and the demand curve is decreasing, and interpret economically.`
75
+ - {fr}`Pente de l'offre :`{en}`Slope of supply:` {input}`||80`
76
+ - {fr}`Pente de la demande :`{en}`Slope of demand:` {input}`||80`
77
+ ::::
78
+
79
+ ::::{questionHint}
80
+ {fr}`Le signe de la pente donne le sens de variation.`{en}`The sign of the slope gives the direction of variation.`
81
+ ::::
82
+
83
+ ::::{displayedSolution}
84
+ - {fr}`Pente offre`{en}`Supply slope` ${{ a_offre }}$
85
+ - {fr}`Pente demande`{en}`Demand slope` ${{ a_demande }}$
86
+ ::::
87
+
88
+ ::::{detailedSolution}
89
+ {fr}`La droite d'offre`{en}`The supply line` $p = {{slopeOffreAff}}q {{interOffreAff}}$ {fr}`a une pente`{en}`has a slope` ${{slopeOffreSignAff}} > 0$ : {fr}`elle est croissante. Économiquement, les producteurs offrent davantage quand le prix monte. La droite de demande`{en}`it is increasing. Economically, producers supply more when the price rises. The demand line` $p = {{slopeDemAff}}q {{interDemAff}}$ {fr}`a une pente`{en}`has a slope` ${{slopeDemPlainAff}} < 0$ : {fr}`elle est décroissante. Les consommateurs demandent moins quand le prix monte.`{en}`it is decreasing. Consumers demand less when the price rises.`
90
+ ::::
91
+
92
+ ::::{weightDistribution}
93
+ :logic: 18
94
+ :abstraction: 22
95
+ :reasoning: 35
96
+ :calculation: 25
97
+ ::::
98
+ :::::
99
+
100
+ :::::{question}
101
+ :questionType: FGQ
102
+ :questionId: 1
103
+ :questionIndex: 1
104
+ :solution: [["ord","${{ repQAff }}$","${{ repPAff }}$"],["0","0"]]
105
+
106
+ ::::{questionStatement}
107
+ {fr}`Déterminer la quantité d'équilibre`{en}`Determine the equilibrium quantity` $q^*$ {fr}`et le prix d'équilibre`{en}`and the equilibrium price` $p^*$ {fr}`en résolvant le système formé par les deux équations.`{en}`by solving the system formed by the two equations.`
108
+ - $q^*$ = {input}`||80`
109
+ - $p^*$ = {input}`||80`
110
+ ::::
111
+
112
+ ::::{questionHint}
113
+ {fr}`À l'équilibre, offre = demande : égale les deux expressions de`{en}`At equilibrium, supply = demand: equate the two expressions of` $p$.
114
+ ::::
115
+
116
+ ::::{displayedSolution}
117
+ - $q^* = {{ repQAff }}$
118
+ - $p^* = {{ repPAff }}$
119
+ ::::
120
+
121
+ ::::{detailedSolution}
122
+ {fr}`On égale offre et demande :`{en}`We equate supply and demand:`
123
+
124
+ \begin{equation*}
125
+ {{slopeOffreAff}}q^* {{interOffreAff}} &= {{slopeDemAff}}q^* {{interDemAff}} \\
126
+ {{coefDiffAff}}q^* &= {{rhsAff}} \\
127
+ q^* &= {{repQAff}}.
128
+ \end{equation*}
129
+
130
+ {fr}`En reportant dans l'offre :`{en}`Substituting into the supply:`
131
+
132
+ \begin{equation*}
133
+ p^* &= {{aOffreAff}} \times {{repQAff}} {{interOffreAff}} \\
134
+ &= {{repPAff}}.
135
+ \end{equation*}
136
+
137
+ {fr}`Vérification par la demande :`{en}`Verification by demand:` ${{aDemandeAff}} \times {{repQAff}} {{interDemAff}} = {{repPAff}}$. {fr}`L'équilibre est`{en}`The equilibrium is` $q^* = {{repQAff}}$ {fr}`milliers d'unités au prix`{en}`thousands of units at price` $p^* = {{repPAff}}$ {fr}`€ par unité.`{en}`€ per unit.`
138
+ ::::
139
+
140
+ ::::{weightDistribution}
141
+ :logic: 15
142
+ :abstraction: 20
143
+ :reasoning: 30
144
+ :calculation: 35
145
+ ::::
146
+ :::::
147
+
148
+ :::::{question}
149
+ :questionType: FGQ
150
+ :questionId: 2
151
+ :questionIndex: 2
152
+ :solution: [["ord","${{ repQAff }}$","${{ repPAff }}$"],["0","0"]]
153
+
154
+ ::::{questionStatement}
155
+ {fr}`Représenter les deux droites sur un graphique clairement légendé et repérer le point d'équilibre.`{en}`Represent the two lines on a clearly labeled graph and identify the equilibrium point.`\
156
+ \
157
+ {fr}`Point d'équilibre`{en}`Equilibrium point` $E${fr}` `:
158
+ - {fr}`abscisse`{en}`x-coordinate`{fr}` `: {input}`||80`
159
+ - {fr}`ordonnée`{en}`y-coordinate`{fr}` `: {input}`||80`
160
+ ::::
161
+
162
+ ::::{questionHint}
163
+ {fr}`Trace les deux droites ; leur intersection est le point d'équilibre`{en}`Draw the two lines; their intersection is the equilibrium point` $E$.
164
+ ::::
165
+
166
+ ::::{displayedSolution}
167
+ {fr}`Point d'équilibre`{en}`Equilibrium point` $E${fr}` `:
168
+ - {fr}`abscisse`{en}`x-coordinate`{fr}` `: ${{ repQAff }}$
169
+ - {fr}`ordonnée`{en}`y-coordinate`{fr}` `: ${{ repPAff }}$
170
+ ::::
171
+
172
+ ::::{detailedSolution}
173
+ {fr}`Les deux droites se coupent au point d'équilibre`{en}`The two lines intersect at the equilibrium point` $E({{repQAff}}\,;\,{{repPAff}})$ :
174
+
175
+ ```{python}
176
+ import numpy as np
177
+ import matplotlib.pyplot as plt
178
+
179
+ # Libellés pour matplotlib : moteur mathtext, donc PAS de \displaystyle ni \; (non supportés).
180
+ def slopeTerm(a):
181
+ if a == 1: return "q"
182
+ if a == -1: return "-q"
183
+ return f"{a}q"
184
+
185
+ def interTerm(b):
186
+ if b == 0: return ""
187
+ return f" + {b}" if b > 0 else f" - {abs(b)}"
188
+
189
+ q = np.linspace(0, q_max_graph, 200)
190
+ offre = a_offre * q + b_offre
191
+ demande = a_demande * q + b_demande
192
+
193
+ fig, ax = plt.subplots(figsize=(6, 6))
194
+ ax.plot(q, offre, color="blue", lw=2, label=f"{label_offre_prefix}$p = {slopeTerm(a_offre)}{interTerm(b_offre)}$")
195
+ ax.plot(q, demande, color="red", lw=2, label=f"{label_demande_prefix}$p = {slopeTerm(a_demande)}{interTerm(b_demande)}$")
196
+
197
+ ax.plot(repQ, repP, "ko", markersize=6)
198
+ ax.annotate(f"$E({repQ}\\,;\\,{repP})$", xy=(repQ, repP), xytext=(repQ + 0.15, repP + 1.2), fontsize=11)
199
+ ax.plot([repQ, repQ], [0, repP], ls="--", color="0.5", lw=1)
200
+ ax.plot([0, repQ], [repP, repP], ls="--", color="0.5", lw=1)
201
+
202
+ ax.annotate("", xy=(q_max_graph + 0.5, 0), xytext=(0, 0), arrowprops=dict(arrowstyle="->", color="black", lw=1.2))
203
+ ax.annotate("", xy=(0, p_max_graph), xytext=(0, 0), arrowprops=dict(arrowstyle="->", color="black", lw=1.2))
204
+ ax.text(q_max_graph + 0.6, -0.5, label_q_axis, ha="center", fontsize=11)
205
+ ax.text(-0.4, p_max_graph, label_p_axis, ha="center", fontsize=11)
206
+
207
+ q_step = max(1, int(q_max_graph / 6))
208
+ for x in range(q_step, int(q_max_graph) + 1, q_step):
209
+ ax.plot([x, x], [-0.3, 0.3], color="black", lw=1)
210
+ ax.text(x, -1.0, str(x), ha="center", fontsize=9)
211
+
212
+ p_step = max(5, int(p_max_graph / 6))
213
+ for y in range(p_step, int(p_max_graph) + 1, p_step):
214
+ ax.plot([-0.1, 0.1], [y, y], color="black", lw=1)
215
+ ax.text(-0.35, y, str(y), ha="right", va="center", fontsize=9)
216
+ ax.text(-0.35, 0, "0", ha="right", va="center", fontsize=9)
217
+
218
+ ax.legend(loc="upper right", fontsize=10)
219
+ ax.set_xlim(-0.5, q_max_graph + 1)
220
+ ax.set_ylim(-2, p_max_graph + 1)
221
+ ax.axis("off")
222
+ plt.tight_layout()
223
+ plt.show()
224
+ ```
225
+
226
+ {fr}`L'offre (croissante) et la demande (décroissante) se croisent en`{en}`Supply (increasing) and demand (decreasing) intersect at` $E({{repQAff}}\,;\,{{repPAff}})$, {fr}`qui matérialise le prix où le marché s'équilibre.`{en}`which represents the price where the market reaches equilibrium.`
227
+ ::::
228
+
229
+ ::::{weightDistribution}
230
+ :logic: 20
231
+ :abstraction: 25
232
+ :reasoning: 35
233
+ :calculation: 20
234
+ ::::
235
+ :::::
236
+
237
+ `````
app/knowledge/fewshots/declinaisons/{fr}`Évaluer un logarithme en reconnaissant une puissance de la base`{en}`Evaluate a logarithm by recognizing a power of the base`.md ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :title: {fr}`Évaluer un logarithme en reconnaissant une puissance de la base`{en}`Evaluate a logarithm by recognizing a power of the base`
3
+ :modules:
4
+ :recommendedExecutionTime: 10
5
+ :level: Elementary
6
+ :chap: chap_expLogFunctions_logarithmicFunctions_ESCP
7
+ :involvedConcepts:
8
+ :originalSource:
9
+ :visibility: All
10
+ :variations:
11
+ :comment: Échauffement (thème pur) : évaluation de log_b(x) en reconnaissant x comme une puissance de b, via log_b(b^k)=k. Aucune loi algébrique du logarithme.
12
+ :originalExerciseId: 1331c2a5-6599-11f1-a8a1-0ed8d3b012a9
13
+ :id: fee348e8-74be-11f1-a8a1-0ed8d3b012a9
14
+
15
+ ````{python}
16
+ import random as rd
17
+
18
+ # Construction déterministe : on part des exposants (réponses) puis on bâtit les arguments.
19
+ base1 = rd.choice([2, 3, 5, 7, 10])
20
+ exp1 = rd.randint(2, 5)
21
+ arg1 = base1 ** exp1
22
+ base2 = rd.choice([b for b in [2, 3, 5, 7, 10] if b != base1])
23
+ exp2 = rd.randint(2, 4)
24
+ arg2 = base2 ** exp2
25
+ base3 = rd.choice([b for b in [2, 3, 5, 7] if b not in [base1, base2]])
26
+ exp3 = rd.randint(2, 5)
27
+ arg3 = base3 ** exp3
28
+ l1, l2, l3 = exp1, exp2, exp3
29
+
30
+ base4 = rd.choice([2, 3, 5, 7, 10])
31
+ base5 = rd.choice([b for b in [2, 3, 4, 5, 6, 7, 10] if b != base4])
32
+ l4, l5 = 1, 0
33
+
34
+ globals()
35
+ ````
36
+
37
+ :::::{question}
38
+ :questionType: FGQ
39
+ :questionId: 0
40
+ :questionIndex: 0
41
+ :solution: [["ord","{{ l1 }}","{{ l2 }}","{{ l3 }}"],["0","0","0"]]
42
+
43
+ ::::{questionStatement}
44
+ {fr}`Évaluer`{en}`Evaluate` $\log_{ {{ base1 }} }({{ arg1 }})$, $\log_{ {{ base2 }} }({{ arg2 }})$ {fr}`et`{en}`and` $\log_{ {{ base3 }} }({{ arg3 }})$ {fr}`en reconnaissant chaque argument comme une puissance de la base.`{en}`by recognizing each argument as a power of the base.`
45
+
46
+ $\log_{ {{ base1 }} }({{ arg1 }}) =$ {input}`||70`
47
+
48
+ $\log_{ {{ base2 }} }({{ arg2 }}) =$ {input}`||70`
49
+
50
+ $\log_{ {{ base3 }} }({{ arg3 }}) =$ {input}`||70`
51
+ ::::
52
+
53
+ ::::{questionHint}
54
+ ${{ arg1 }}={{ base1 }}^{ {{ exp1 }} }$, ${{ arg2 }}={{ base2 }}^{ {{ exp2 }} }$, ${{ arg3 }}={{ base3 }}^{ {{ exp3 }} }$ ; {fr}`utiliser`{en}`use` $\log_{b}(b^{k})=k$.
55
+ ::::
56
+
57
+ ::::{displayedSolution}
58
+ $\log_{ {{ base1 }} }({{ arg1 }}) = {{ l1 }}$
59
+
60
+ $\log_{ {{ base2 }} }({{ arg2 }}) = {{ l2 }}$
61
+
62
+ $\log_{ {{ base3 }} }({{ arg3 }}) = {{ l3 }}$
63
+ ::::
64
+
65
+ ::::{detailedSolution}
66
+ {fr}`Par la relation réciproque`{en}`By the reciprocal relation` $\log_{b}(b^{k})=k$ :
67
+
68
+ \begin{equation*}
69
+ \log_{ {{ base1 }} }({{ arg1 }}) &= \log_{ {{ base1 }} }({{ base1 }}^{ {{ exp1 }} }) \\
70
+ &= {{ l1 }}, \\
71
+ \log_{ {{ base2 }} }({{ arg2 }}) &= \log_{ {{ base2 }} }({{ base2 }}^{ {{ exp2 }} }) \\
72
+ &= {{ l2 }}, \\
73
+ \log_{ {{ base3 }} }({{ arg3 }}) &= \log_{ {{ base3 }} }({{ base3 }}^{ {{ exp3 }} }) \\
74
+ &= {{ l3 }}.
75
+ \end{equation*}
76
+ ::::
77
+
78
+ ::::{weightDistribution}
79
+ :logic: 15
80
+ :abstraction: 25
81
+ :reasoning: 30
82
+ :calculation: 30
83
+ ::::
84
+ :::::
85
+
86
+ :::::{question}
87
+ :questionType: FGQ
88
+ :questionId: 1
89
+ :questionIndex: 1
90
+ :solution: [["ord","{{ l4 }}","{{ l5 }}"],["0","0"]]
91
+
92
+ ::::{questionStatement}
93
+ {fr}`Évaluer`{en}`Evaluate` $\log_{ {{ base4 }} }({{ base4 }})$ {fr}`et`{en}`and` $\log_{ {{ base5 }} }(1)$, {fr}`et justifier à partir des cas`{en}`and justify from the cases` $b^1=b$ {fr}`et`{en}`and` $b^0=1$.
94
+
95
+ $\log_{ {{ base4 }} }({{ base4 }}) =$ {input}`||70`
96
+
97
+ $\log_{ {{ base5 }} }(1) =$ {input}`||70`
98
+ ::::
99
+
100
+ ::::{questionHint}
101
+ ${{ base4 }}={{ base4 }}^1$ {fr}`et`{en}`and` $1={{ base5 }}^0$.
102
+ ::::
103
+
104
+ ::::{displayedSolution}
105
+ $\log_{ {{ base4 }} }({{ base4 }}) = {{ l4 }}$
106
+
107
+ $\log_{ {{ base5 }} }(1) = {{ l5 }}$
108
+ ::::
109
+
110
+ ::::{detailedSolution}
111
+ {fr}`On a`{en}`We have` ${{ base4 }}={{ base4 }}^1$ {fr}`et`{en}`and` $1={{ base5 }}^0$, {fr}`donc :`{en}`so:`
112
+
113
+ \begin{equation*}
114
+ \log_{ {{ base4 }} }({{ base4 }}) &= \log_{ {{ base4 }} }({{ base4 }}^1) \\
115
+ &= {{ l4 }}, \\
116
+ \log_{ {{ base5 }} }(1) &= \log_{ {{ base5 }} }({{ base5 }}^0) \\
117
+ &= {{ l5 }}.
118
+ \end{equation*}
119
+
120
+ {fr}`En particulier`{en}`In particular` $\log_{b}(1)=0$ {fr}`pour toute base : le graphe d'un logarithme passe toujours par`{en}`for any base: the graph of a logarithm always passes through` $(1,0)$.
121
+ ::::
122
+
123
+ ::::{weightDistribution}
124
+ :logic: 20
125
+ :abstraction: 30
126
+ :reasoning: 30
127
+ :calculation: 20
128
+ ::::
129
+ :::::
130
+
131
+ `````
app/knowledge/fewshots/qat.md ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :id:
3
+ :title: {fr}`Exemple QAT — racines d'un trinôme`{en}`QAT example — roots of a quadratic`
4
+ :modules:
5
+ :recommendedExecutionTime: 4
6
+ :level: Elementary
7
+ :chap:
8
+ :involvedConcepts:
9
+ :originalSource:
10
+ :visibility: All
11
+
12
+ ````{python}
13
+ import random as rd
14
+ from sympy import symbols, latex, Poly
15
+ x = symbols('x')
16
+
17
+ r1 = rd.randint(-6, -1) # plus petite racine (entière)
18
+ r2 = rd.randint(1, 6) # plus grande racine (entière)
19
+ p = -(r1 + r2)
20
+ q = r1*r2
21
+ polyAff = latex(Poly(x**2 + p*x + q, x).as_expr())
22
+ r1Aff, r2Aff = str(r1), str(r2)
23
+
24
+ globals()
25
+ ````
26
+
27
+ :::::{question}
28
+ :questionType: FGQ
29
+ :questionId: 0
30
+ :questionIndex: 0
31
+ :solution: [["ord","${{r1Aff}}$","${{r2Aff}}$"],["0","0"]]
32
+
33
+ ::::{questionStatement}
34
+ {fr}`Résoudre l'équation suivante, en donnant les racines de la plus petite à la plus grande :`{en}`Solve the following equation, giving the roots from smallest to largest:`
35
+ \begin{equation*}
36
+ {{polyAff}} = 0.
37
+ \end{equation*}
38
+
39
+ {fr}`Plus petite racine :`{en}`Smallest root:` {input}`||90` $\qquad$ {fr}`plus grande racine :`{en}`largest root:` {input}`||90`
40
+ ::::
41
+
42
+ ::::{questionHint}
43
+ {fr}`Cherchez deux entiers dont la somme et le produit correspondent aux coefficients.`{en}`Look for two integers whose sum and product match the coefficients.`
44
+ ::::
45
+
46
+ ::::{displayedSolution}
47
+ $x_1 = {{r1Aff}}$, $\quad x_2 = {{r2Aff}}$
48
+ ::::
49
+
50
+ ::::{detailedSolution}
51
+ {fr}`On factorise`{en}`Factoring` ${{polyAff}} = (x - ({{r1Aff}}))(x - ({{r2Aff}}))$, {fr}`d'où les racines`{en}`hence the roots` ${}{{r1Aff}}$ {fr}`et`{en}`and` ${}{{r2Aff}}$.
52
+ ::::
53
+
54
+ ::::{weightDistribution}
55
+ :logic: 15
56
+ :abstraction: 15
57
+ :reasoning: 20
58
+ :calculation: 50
59
+ ::::
60
+ :::::
61
+
62
+ `````
app/knowledge/fewshots/qcm.md ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ `````{exercise}
2
+ :id:
3
+ :title: {fr}`Exemple QCM — dérivée d'un monôme`{en}`MCQ example — derivative of a monomial`
4
+ :modules:
5
+ :recommendedExecutionTime: 3
6
+ :level: Elementary
7
+ :chap:
8
+ :involvedConcepts:
9
+ :originalSource:
10
+ :visibility: All
11
+
12
+ ````{python}
13
+ import random as rd
14
+ from sympy import symbols, diff, latex
15
+ x = symbols('x')
16
+
17
+ a = rd.randint(2, 9) # coefficient (>= 2, jamais 1)
18
+ n = rd.choice([k for k in range(2, 7) if k != a]) # exposant >= 2, ET n != a
19
+ f = a*x**n
20
+ fp = diff(f, x) # bonne réponse : n a x^(n-1)
21
+
22
+ fAff = latex(f)
23
+ correctAff = latex(fp)
24
+ d1Aff = latex(a*x**(n-1)) # oubli du facteur n (dérivée)
25
+ d2Aff = latex(a*n*x**n) # exposant non décrémenté
26
+ d3Aff = latex(n*x**(n-1)) # oubli du coefficient a
27
+ # distincts par construction : a != n garantit d1 != d3 (sinon a x^(n-1) == n x^(n-1)),
28
+ # a,n >= 2 garantit correct != d1/d2/d3 — collision impossible sur toute graine.
29
+
30
+ globals()
31
+ ````
32
+
33
+ {fr}`Soit la fonction`{en}`Let the function` $f(x) = {{fAff}}$.
34
+
35
+ :::::{question}
36
+ :questionType: MCQ
37
+ :questionId: 0
38
+ :questionIndex: 0
39
+
40
+ ::::{questionStatement}
41
+ {fr}`Quelle est la dérivée`{en}`What is the derivative` $f'(x)$ ?
42
+ ::::
43
+
44
+ ::::{questionHint}
45
+ {fr}`Règle de la puissance :`{en}`Power rule:` $\dfrac{d}{dx}\left(x^{p}\right) = p\,x^{p-1}$.
46
+ ::::
47
+
48
+ ::::{mcqAnswer}
49
+ :isRightAnswer: true
50
+ $f'(x) = {{correctAff}}$
51
+ ::::
52
+
53
+ ::::{mcqAnswer}
54
+ :isRightAnswer: false
55
+ $f'(x) = {{d1Aff}}$
56
+ ::::
57
+
58
+ ::::{mcqAnswer}
59
+ :isRightAnswer: false
60
+ $f'(x) = {{d2Aff}}$
61
+ ::::
62
+
63
+ ::::{mcqAnswer}
64
+ :isRightAnswer: false
65
+ $f'(x) = {{d3Aff}}$
66
+ ::::
67
+
68
+ ::::{mcqAnswer}
69
+ :isRightAnswer: false
70
+ {fr}`Aucune de ces réponses n'est correcte`{en}`None of these answers are correct`
71
+ ::::
72
+
73
+ ::::{detailedSolution}
74
+ {fr}`Par la règle de la puissance,`{en}`By the power rule,` $\dfrac{d}{dx}\left(a x^{n}\right) = n\,a\,x^{n-1}$, {fr}`donc`{en}`so` $f'(x) = {{correctAff}}$.
75
+ ::::
76
+
77
+ ::::{weightDistribution}
78
+ :logic: 20
79
+ :abstraction: 20
80
+ :reasoning: 20
81
+ :calculation: 40
82
+ ::::
83
+ :::::
84
+
85
+ `````
app/pipeline/fewshots.py CHANGED
@@ -79,3 +79,16 @@ def fewshot_for(analysis: dict) -> str:
79
  shot = load_fewshot(key)
80
  logger.info("Few-shot sélectionné : %s (%d caractères)", key, len(shot))
81
  return shot or "(aucun exemple canonique disponible pour ce type)"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  shot = load_fewshot(key)
80
  logger.info("Few-shot sélectionné : %s (%d caractères)", key, len(shot))
81
  return shot or "(aucun exemple canonique disponible pour ce type)"
82
+
83
+
84
+ @lru_cache(maxsize=None)
85
+ def fewshot_for_declinaison(decl_type: str) -> str:
86
+ """Exemple canonique COMPLET pour une déclinaison (qcm|qat) — fichiers
87
+ stricts (conventions corpus 222) calibrés sur les 33 exemples validés de
88
+ fewshots/declinaisons/. Complet (pas élagué) : la structure mcqAnswer /
89
+ :solution:/displayedSolution est le cœur de ce qu'il faut imiter."""
90
+ path = FEWSHOTS_DIR / f"{decl_type}.md"
91
+ if not path.exists():
92
+ logger.warning("Few-shot déclinaison absent : %s", path)
93
+ return "(aucun exemple canonique disponible)"
94
+ return path.read_text(encoding="utf-8")
app/pipeline/generate.py CHANGED
@@ -21,7 +21,13 @@ from app.pipeline.postprocess import (
21
  normalize_python_fences,
22
  strip_fences,
23
  )
24
- from app.pipeline.prompts import STEP_PAIR_PROMPT, SYSTEM_PROMPT
 
 
 
 
 
 
25
 
26
  logger = logging.getLogger(__name__)
27
 
@@ -124,6 +130,7 @@ def build_exercise_metadata(
124
  lists_of_notions: str,
125
  analysis: dict | None = None,
126
  level: str = "",
 
127
  ) -> str:
128
  """Construit TOUJOURS un en-tête `{exercise}` complet et bien formé
129
  (5 backticks englobant tout l'exercice — le bloc Python à 4 backticks vient
@@ -142,6 +149,11 @@ def build_exercise_metadata(
142
  src = _parse_source_options(metadata)
143
 
144
  title = src.get("title") or analysis.get("exercise_title") or "Exercice"
 
 
 
 
 
145
 
146
  nb_q = analysis.get("nb_questions") or 0
147
  try:
@@ -170,7 +182,12 @@ def build_exercise_metadata(
170
 
171
  fence = "`" * EXERCISE_FENCE_BACKTICKS
172
  lines = [f"{fence}{{exercise}}"]
173
- lines += [f":{k}: {values[k]}".rstrip() for k in _HEADER_FIELDS]
 
 
 
 
 
174
  return "\n".join(lines)
175
 
176
 
@@ -208,6 +225,10 @@ _LANG_DIRECTIVES = {
208
  }
209
 
210
 
 
 
 
 
211
  def generate_pair_blocks(
212
  content: str,
213
  exercise_header: str,
@@ -222,20 +243,37 @@ def generate_pair_blocks(
222
  model_idx: int,
223
  lang: str = "auto",
224
  set_step: Optional[Callable[[str], None]] = None,
 
225
  ) -> list[str]:
226
- """Boucle de génération par paires (séquentielle). Retourne les blocs."""
 
 
227
  nb_questions = analysis.get("nb_questions", max(1, len(question_segments)))
228
  lang_directive = _LANG_DIRECTIVES.get(lang, _LANG_DIRECTIVES["auto"])
229
- common = dict(
230
- analysis=json.dumps(analysis, ensure_ascii=False, indent=2),
231
- functions=functions_ctx or "Aucune fonction spécifique détectée.",
232
- niveau=level or "non précisé",
233
- targeted_rules=targeted_rules_digest,
234
- property_constraints=property_constraints_text,
235
- fewshot=fewshot,
236
- lang_directive=lang_directive,
237
- nb_total=nb_questions,
238
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
 
240
  generated: list[str] = []
241
 
@@ -244,7 +282,7 @@ def generate_pair_blocks(
244
  if set_step:
245
  set_step("Génération (bloc Python + toutes les questions)…")
246
  raw = process_with_openrouter(
247
- prompt=STEP_PAIR_PROMPT.format(
248
  content=content,
249
  previous_blocks="(aucun — première génération)",
250
  nb_current=nb_questions,
@@ -272,7 +310,7 @@ def generate_pair_blocks(
272
  current_segment = enonce + "\n\n" + current_segment
273
 
274
  raw_pair = process_with_openrouter(
275
- prompt=STEP_PAIR_PROMPT.format(
276
  content=exercise_header,
277
  previous_blocks=("\n\n".join(generated) if generated
278
  else "(aucun — première paire)"),
 
21
  normalize_python_fences,
22
  strip_fences,
23
  )
24
+ from app.pipeline.prompts import (
25
+ FGQ_SPEC,
26
+ MCQ_SPEC,
27
+ STEP_DECLINAISON_PROMPT,
28
+ STEP_PAIR_PROMPT,
29
+ SYSTEM_PROMPT,
30
+ )
31
 
32
  logger = logging.getLogger(__name__)
33
 
 
130
  lists_of_notions: str,
131
  analysis: dict | None = None,
132
  level: str = "",
133
+ decl_type: str | None = None,
134
  ) -> str:
135
  """Construit TOUJOURS un en-tête `{exercise}` complet et bien formé
136
  (5 backticks englobant tout l'exercice — le bloc Python à 4 backticks vient
 
149
  src = _parse_source_options(metadata)
150
 
151
  title = src.get("title") or analysis.get("exercise_title") or "Exercice"
152
+ if decl_type:
153
+ # Suffixe de traçabilité (convention des exemples validés : « - MCQ »).
154
+ suffix = " - MCQ" if decl_type == "qcm" else " - QAT"
155
+ if not title.rstrip().endswith(suffix.strip()):
156
+ title = title.rstrip() + suffix
157
 
158
  nb_q = analysis.get("nb_questions") or 0
159
  try:
 
182
 
183
  fence = "`" * EXERCISE_FENCE_BACKTICKS
184
  lines = [f"{fence}{{exercise}}"]
185
+ for k in _HEADER_FIELDS:
186
+ lines.append(f":{k}: {values[k]}".rstrip())
187
+ # Déclinaison d'un exercice existant : tracer l'id source juste après :id:
188
+ # (convention des exemples validés : :originalExerciseId:).
189
+ if k == "id" and decl_type and src.get("id"):
190
+ lines.append(f":originalExerciseId: {src['id']}")
191
  return "\n".join(lines)
192
 
193
 
 
225
  }
226
 
227
 
228
+ _DECL_LABELS = {"qcm": "QCM (MCQ)", "qat": "QAT (FGQ)"}
229
+ _DECL_SPECS = {"qcm": MCQ_SPEC, "qat": FGQ_SPEC}
230
+
231
+
232
  def generate_pair_blocks(
233
  content: str,
234
  exercise_header: str,
 
243
  model_idx: int,
244
  lang: str = "auto",
245
  set_step: Optional[Callable[[str], None]] = None,
246
+ decl_type: Optional[str] = None,
247
  ) -> list[str]:
248
+ """Boucle de génération par paires (séquentielle). Retourne les blocs.
249
+ `decl_type` (qcm|qat) bascule sur le prompt Déclinaisons — même mécanique
250
+ par paires, 1 question source → 1 question déclinée."""
251
  nb_questions = analysis.get("nb_questions", max(1, len(question_segments)))
252
  lang_directive = _LANG_DIRECTIVES.get(lang, _LANG_DIRECTIVES["auto"])
253
+ if decl_type:
254
+ prompt_tmpl = STEP_DECLINAISON_PROMPT
255
+ common = dict(
256
+ analysis=json.dumps(analysis, ensure_ascii=False, indent=2),
257
+ functions=functions_ctx or "Aucune fonction spécifique détectée.",
258
+ niveau=level or "non précisé",
259
+ fewshot=fewshot,
260
+ lang_directive=lang_directive,
261
+ nb_total=nb_questions,
262
+ decl_label=_DECL_LABELS[decl_type],
263
+ decl_spec=_DECL_SPECS[decl_type],
264
+ )
265
+ else:
266
+ prompt_tmpl = STEP_PAIR_PROMPT
267
+ common = dict(
268
+ analysis=json.dumps(analysis, ensure_ascii=False, indent=2),
269
+ functions=functions_ctx or "Aucune fonction spécifique détectée.",
270
+ niveau=level or "non précisé",
271
+ targeted_rules=targeted_rules_digest,
272
+ property_constraints=property_constraints_text,
273
+ fewshot=fewshot,
274
+ lang_directive=lang_directive,
275
+ nb_total=nb_questions,
276
+ )
277
 
278
  generated: list[str] = []
279
 
 
282
  if set_step:
283
  set_step("Génération (bloc Python + toutes les questions)…")
284
  raw = process_with_openrouter(
285
+ prompt=prompt_tmpl.format(
286
  content=content,
287
  previous_blocks="(aucun — première génération)",
288
  nb_current=nb_questions,
 
310
  current_segment = enonce + "\n\n" + current_segment
311
 
312
  raw_pair = process_with_openrouter(
313
+ prompt=prompt_tmpl.format(
314
  content=exercise_header,
315
  previous_blocks=("\n\n".join(generated) if generated
316
  else "(aucun — première paire)"),
app/pipeline/orchestrator.py CHANGED
@@ -39,7 +39,7 @@ from app.rag.catalogue import catalogue_for
39
  from app.pipeline import postprocess as pp
40
  from app.pipeline.analyze import run_analysis_phase
41
  from app.pipeline.audit import run_audit
42
- from app.pipeline.fewshots import fewshot_for
43
  from app.pipeline.generate import (
44
  assemble_exercise,
45
  build_exercise_metadata,
@@ -105,21 +105,31 @@ def run_exercise(
105
  model_idx: int = 1,
106
  lang: str = "fr",
107
  set_step: Optional[Callable[[str], None]] = None,
 
 
108
  ) -> dict:
109
  """
110
- Pythonise UN exercice. Retourne le dict résultat (contrat UI) :
 
 
 
 
 
111
  exercise, pair_blocks, analysis, functions, notions, audit_patches,
112
  warnings, harness {ok, summary, seeds}, lang {source, target, action},
113
- cost {usd, eur, requests}, duration_s
114
  """
115
  t0 = time.time()
116
  cost_before = cost_snapshot()
117
  _step = set_step or (lambda label: None)
118
 
119
- # ── 1. Analyse + notions + RAG (parallèle) ───────────────────────────────
120
- _step("Analyse + notions + catalogue RAG (en parallèle)…")
121
- analysis, notions_ctx, lists_of_notions, functions_ctx = run_analysis_phase(
122
- content, model_idx)
 
 
 
123
 
124
  step1_targets = [r for r in (analysis.get("target_rules") or []) if isinstance(r, str)]
125
  target_rules = list(dict.fromkeys(TRUNK_RULES + step1_targets))
@@ -133,7 +143,8 @@ def run_exercise(
133
 
134
  # ── 2. Génération par paires ─────────────────────────────────────────────
135
  metadata, enonce, question_segments = split_original_questions(content)
136
- exercise_header = build_exercise_metadata(metadata, lists_of_notions, analysis, level)
 
137
 
138
  # Contexte fonctions = catalogue CURÉ (domaine détecté) + hits RAG FAISS.
139
  # Le catalogue curé donne « quel helper pour quel besoin » + couvre les
@@ -145,6 +156,8 @@ def run_exercise(
145
  if functions_ctx else "",
146
  ])) or "Aucune fonction spécifique détectée."
147
 
 
 
148
  pair_blocks = generate_pair_blocks(
149
  content=content,
150
  exercise_header=exercise_header,
@@ -152,13 +165,14 @@ def run_exercise(
152
  question_segments=question_segments,
153
  analysis=analysis,
154
  functions_ctx=functions_combined,
155
- fewshot=fewshot_for(analysis),
156
  targeted_rules_digest=targeted_rules_digest,
157
  property_constraints_text=property_constraints_text,
158
  level=level,
159
  model_idx=model_idx,
160
  lang=lang,
161
  set_step=_step,
 
162
  )
163
 
164
  # ── 3. Post-traitements déterministes ────────────────────────────────────
@@ -247,6 +261,39 @@ def run_exercise(
247
  myst_exercise, dollar_patches = pp.fix_dollar_digit(myst_exercise)
248
  audit_patches.extend(dollar_patches)
249
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  # Les warnings 6.1 du LLM deviennent du bruit une fois l'auto-lift passé.
251
  if not pp.INJECTION_RE.search(myst_exercise) or not any(
252
  "(" in tok or "**" in tok for tok in pp.INJECTION_RE.findall(myst_exercise)
@@ -363,12 +410,18 @@ def run_exercise(
363
  candidate, _ = pp.auto_lift_injections(candidate)
364
  candidate, _ = pp.rename_underscore_injections(candidate)
365
  candidate, _ = pp.fix_dollar_digit(candidate)
 
 
 
 
 
366
  candidate, _ = pp.renumber_question_ids(candidate)
367
  candidate_report = harness.validate_text(candidate, seeds=HARNESS_GATE_SEEDS)
368
 
369
  def _badness(r: dict) -> int:
370
  return (len(r["static_errors"]) + r["n_exec_errors"]
371
- + r["n_unresolved"] + r["n_forbidden"])
 
372
 
373
  if candidate_report["ok"] or _badness(candidate_report) < _badness(report):
374
  myst_exercise, report = candidate, candidate_report
@@ -401,6 +454,49 @@ def run_exercise(
401
  "summary": harness.format_report(report),
402
  },
403
  "lang": lang_info,
 
404
  "cost": cost_delta(cost_before),
405
  "duration_s": round(time.time() - t0, 1),
406
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  from app.pipeline import postprocess as pp
40
  from app.pipeline.analyze import run_analysis_phase
41
  from app.pipeline.audit import run_audit
42
+ from app.pipeline.fewshots import fewshot_for, fewshot_for_declinaison
43
  from app.pipeline.generate import (
44
  assemble_exercise,
45
  build_exercise_metadata,
 
105
  model_idx: int = 1,
106
  lang: str = "fr",
107
  set_step: Optional[Callable[[str], None]] = None,
108
+ decl_type: Optional[str] = None,
109
+ shared_phase: Optional[tuple] = None,
110
  ) -> dict:
111
  """
112
+ Traite UN exercice. `decl_type=None` = pythonisation (flux historique) ;
113
+ `decl_type ∈ {"qcm","qat"}` = mode déclinaisons (même pipeline, prompt et
114
+ harnais étendus). `shared_phase` = résultat de run_analysis_phase à
115
+ RÉUTILISER (déclinaisons QCM+QAT d'une même source : une seule analyse).
116
+
117
+ Retourne le dict résultat (contrat UI) :
118
  exercise, pair_blocks, analysis, functions, notions, audit_patches,
119
  warnings, harness {ok, summary, seeds}, lang {source, target, action},
120
+ cost {usd, eur, requests}, duration_s [, decl_type]
121
  """
122
  t0 = time.time()
123
  cost_before = cost_snapshot()
124
  _step = set_step or (lambda label: None)
125
 
126
+ # ── 1. Analyse + notions + RAG (parallèle ; partagée en mode QCM+QAT) ────
127
+ if shared_phase is not None:
128
+ analysis, notions_ctx, lists_of_notions, functions_ctx = shared_phase
129
+ else:
130
+ _step("Analyse + notions + catalogue RAG (en parallèle)…")
131
+ analysis, notions_ctx, lists_of_notions, functions_ctx = run_analysis_phase(
132
+ content, model_idx)
133
 
134
  step1_targets = [r for r in (analysis.get("target_rules") or []) if isinstance(r, str)]
135
  target_rules = list(dict.fromkeys(TRUNK_RULES + step1_targets))
 
143
 
144
  # ── 2. Génération par paires ─────────────────────────────────────────────
145
  metadata, enonce, question_segments = split_original_questions(content)
146
+ exercise_header = build_exercise_metadata(metadata, lists_of_notions, analysis,
147
+ level, decl_type=decl_type)
148
 
149
  # Contexte fonctions = catalogue CURÉ (domaine détecté) + hits RAG FAISS.
150
  # Le catalogue curé donne « quel helper pour quel besoin » + couvre les
 
156
  if functions_ctx else "",
157
  ])) or "Aucune fonction spécifique détectée."
158
 
159
+ fewshot = (fewshot_for_declinaison(decl_type) if decl_type
160
+ else fewshot_for(analysis))
161
  pair_blocks = generate_pair_blocks(
162
  content=content,
163
  exercise_header=exercise_header,
 
165
  question_segments=question_segments,
166
  analysis=analysis,
167
  functions_ctx=functions_combined,
168
+ fewshot=fewshot,
169
  targeted_rules_digest=targeted_rules_digest,
170
  property_constraints_text=property_constraints_text,
171
  level=level,
172
  model_idx=model_idx,
173
  lang=lang,
174
  set_step=_step,
175
+ decl_type=decl_type,
176
  )
177
 
178
  # ── 3. Post-traitements déterministes ────────────────────────────────────
 
261
  myst_exercise, dollar_patches = pp.fix_dollar_digit(myst_exercise)
262
  audit_patches.extend(dollar_patches)
263
 
264
+ if decl_type:
265
+ # Filet : alias d'option MCQ mal nommés / :isRightAnswer: manquant
266
+ # (le repli MCQ en QAT est concerné aussi).
267
+ myst_exercise, alias_fixed = pp.fix_mcq_answer_aliases(myst_exercise)
268
+ if alias_fixed:
269
+ audit_patches.append({
270
+ "rule": "MCQ", "location": "(mcqOption / :isRightAnswer:)",
271
+ "fix": f"{alias_fixed} bloc(s) d'option normalisé(s)",
272
+ "message": "Blocs d'options MCQ normalisés (mcqOption→mcqAnswer, :isRightAnswer: false par défaut).",
273
+ "iteration": 0,
274
+ })
275
+ # Déclinaisons : UN SEUL bloc {python} — fusion des blocs additionnels
276
+ # sans re-tirage (re-tirage → laissé au harnais + réparation LLM).
277
+ myst_exercise, merged = pp.merge_decl_python_blocks(myst_exercise)
278
+ if merged:
279
+ audit_patches.append({
280
+ "rule": "3.1", "location": "(blocs python additionnels)",
281
+ "fix": f"{merged} bloc(s) fusionné(s) dans le bloc principal",
282
+ "message": "Déclinaison : blocs {python} additionnels fusionnés (un seul bloc, spec).",
283
+ "iteration": 0,
284
+ })
285
+
286
+ if decl_type == "qcm":
287
+ # Filet MCQ : l'option « None/Aucune » doit être le dernier mcqAnswer.
288
+ myst_exercise, none_moved = pp.fix_none_option_last(myst_exercise)
289
+ if none_moved:
290
+ audit_patches.append({
291
+ "rule": "MCQ", "location": "(option None)",
292
+ "fix": f"{none_moved} option(s) « None » déplacée(s) en dernier",
293
+ "message": "Option « Aucune de ces réponses / None » repositionnée en dernière position.",
294
+ "iteration": 0,
295
+ })
296
+
297
  # Les warnings 6.1 du LLM deviennent du bruit une fois l'auto-lift passé.
298
  if not pp.INJECTION_RE.search(myst_exercise) or not any(
299
  "(" in tok or "**" in tok for tok in pp.INJECTION_RE.findall(myst_exercise)
 
410
  candidate, _ = pp.auto_lift_injections(candidate)
411
  candidate, _ = pp.rename_underscore_injections(candidate)
412
  candidate, _ = pp.fix_dollar_digit(candidate)
413
+ if decl_type:
414
+ candidate, _ = pp.fix_mcq_answer_aliases(candidate)
415
+ candidate, _ = pp.merge_decl_python_blocks(candidate)
416
+ if decl_type == "qcm":
417
+ candidate, _ = pp.fix_none_option_last(candidate)
418
  candidate, _ = pp.renumber_question_ids(candidate)
419
  candidate_report = harness.validate_text(candidate, seeds=HARNESS_GATE_SEEDS)
420
 
421
  def _badness(r: dict) -> int:
422
  return (len(r["static_errors"]) + r["n_exec_errors"]
423
+ + r["n_unresolved"] + r["n_forbidden"]
424
+ + r.get("n_mcq_collisions", 0))
425
 
426
  if candidate_report["ok"] or _badness(candidate_report) < _badness(report):
427
  myst_exercise, report = candidate, candidate_report
 
454
  "summary": harness.format_report(report),
455
  },
456
  "lang": lang_info,
457
+ "decl_type": decl_type,
458
  "cost": cost_delta(cost_before),
459
  "duration_s": round(time.time() - t0, 1),
460
  }
461
+
462
+
463
+ def run_declinaisons(
464
+ content: str,
465
+ filename: str = "exercise.md",
466
+ level: str = "",
467
+ model_idx: int = 1,
468
+ lang: str = "fr",
469
+ types: Optional[list] = None,
470
+ set_step: Optional[Callable[[str], None]] = None,
471
+ ) -> list[tuple[str, dict]]:
472
+ """
473
+ Mode `declinaisons` : produit une déclinaison par type coché (qcm/qat).
474
+ L'analyse + notions + RAG sont calculées UNE SEULE fois et partagées entre
475
+ les types (aucun appel LLM redondant). Retourne [(decl_type, result), …].
476
+ Un échec sur un type n'empêche pas l'autre (l'appelant gère l'erreur).
477
+ """
478
+ _step = set_step or (lambda label: None)
479
+ types = [t for t in (types or []) if t in ("qcm", "qat")] or ["qcm"]
480
+
481
+ _step("Analyse + notions + catalogue RAG (partagés QCM/QAT)…")
482
+ shared = run_analysis_phase(content, model_idx)
483
+
484
+ out: list[tuple[str, dict]] = []
485
+ for decl_type in types:
486
+ label = "QCM" if decl_type == "qcm" else "QAT"
487
+
488
+ def step_with_type(msg: str, _label=label):
489
+ _step(f"[{_label}] {msg}")
490
+
491
+ result = run_exercise(
492
+ content=content,
493
+ filename=filename,
494
+ level=level,
495
+ model_idx=model_idx,
496
+ lang=lang,
497
+ set_step=step_with_type,
498
+ decl_type=decl_type,
499
+ shared_phase=shared,
500
+ )
501
+ out.append((decl_type, result))
502
+ return out
app/pipeline/postprocess.py CHANGED
@@ -324,8 +324,19 @@ def fix_dollar_digit(exercise: str) -> tuple[str, list[dict]]:
324
  • `$` non échappé immédiatement suivi d'un chiffre → `${}` + chiffre
325
  • `$` non échappé immédiatement suivi de `{{` → `${}{{`
326
  Le groupe vide {} est invisible au rendu mais empêche la lecture
327
- « montant en devise » qui désynchronise tout le `$…$`."""
 
 
 
328
  masked, blocks = mask_python_blocks(exercise)
 
 
 
 
 
 
 
 
329
  patches: list[dict] = []
330
 
331
  def _digit(m: re.Match) -> str:
@@ -350,6 +361,8 @@ def fix_dollar_digit(exercise: str) -> tuple[str, list[dict]]:
350
 
351
  masked = re.sub(r"(?<!\\)\$(?!\{)(\d)", _digit, masked)
352
  masked = re.sub(r"(?<!\\)\$\{\{", _inj, masked)
 
 
353
  return unmask_python_blocks(masked, blocks), patches
354
 
355
 
@@ -605,6 +618,102 @@ def check_hardcoded_decimals_in_solutions(exercise: str) -> list[dict]:
605
  return warnings
606
 
607
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
608
  # ─────────────────────────────────────────────────────────────────────────────
609
  # Langues : détection, réduction bilingue → mono, contrôle des décimales
610
  # ─────────────────────────────────────────────────────────────────────────────
 
324
  • `$` non échappé immédiatement suivi d'un chiffre → `${}` + chiffre
325
  • `$` non échappé immédiatement suivi de `{{` → `${}{{`
326
  Le groupe vide {} est invisible au rendu mais empêche la lecture
327
+ « montant en devise » qui désynchronise tout le `$…$`.
328
+ EXCLUSION : les lignes `:solution:` (FGQ) — c'est un motif de
329
+ correspondance de réponse, pas du texte affiché ; un `${}` y casserait
330
+ la correction automatique."""
331
  masked, blocks = mask_python_blocks(exercise)
332
+ # Masque les lignes :solution: (elles ne doivent pas être réécrites).
333
+ sol_lines: list[str] = []
334
+
335
+ def _mask_sol(m: re.Match) -> str:
336
+ sol_lines.append(m.group(0))
337
+ return f"\x00SOLLINE{len(sol_lines) - 1}\x00"
338
+
339
+ masked = re.sub(r"(?m)^:solution:.*$", _mask_sol, masked)
340
  patches: list[dict] = []
341
 
342
  def _digit(m: re.Match) -> str:
 
361
 
362
  masked = re.sub(r"(?<!\\)\$(?!\{)(\d)", _digit, masked)
363
  masked = re.sub(r"(?<!\\)\$\{\{", _inj, masked)
364
+ for i, line in enumerate(sol_lines):
365
+ masked = masked.replace(f"\x00SOLLINE{i}\x00", line)
366
  return unmask_python_blocks(masked, blocks), patches
367
 
368
 
 
618
  return warnings
619
 
620
 
621
+ # ─────────────────────────────────────────────────────────────────────────────
622
+ # Déclinaisons QCM/QAT — filets déterministes (mode `declinaisons`)
623
+ # ─────────────────────────────────────────────────────────────────────────────
624
+
625
+ QUESTION_BLOCK_RE = _QUESTION_BLOCK_RE # réutilisé par le harnais étendu
626
+ MCQ_ANSWER_RE = re.compile(
627
+ r"::::\{mcqAnswer\}\s*\n:isRightAnswer:\s*(true|false)\s*\n(.*?)\n::::",
628
+ re.DOTALL,
629
+ )
630
+ _NONE_OPTION_RE = re.compile(r"Aucune de ces réponses|None of these answers", re.IGNORECASE)
631
+
632
+
633
+ def fix_none_option_last(exercise: str) -> tuple[str, int]:
634
+ """MCQ : l'option « Aucune de ces réponses / None » doit être le DERNIER
635
+ mcqAnswer de sa question — on la déplace si besoin (filet déterministe)."""
636
+ moved = {"n": 0}
637
+
638
+ def _fix_question(qm: re.Match) -> str:
639
+ block = qm.group(0)
640
+ answers = list(MCQ_ANSWER_RE.finditer(block))
641
+ if len(answers) < 2:
642
+ return block
643
+ none_idx = [i for i, a in enumerate(answers) if _NONE_OPTION_RE.search(a.group(2))]
644
+ if not none_idx or none_idx[-1] == len(answers) - 1:
645
+ return block
646
+ i = none_idx[-1]
647
+ none_text = answers[i].group(0)
648
+ # retire l'option None puis la réinsère après le dernier mcqAnswer
649
+ block2 = block.replace(none_text + "\n\n", "", 1).replace(none_text, "", 1)
650
+ last = list(MCQ_ANSWER_RE.finditer(block2))[-1]
651
+ block2 = block2[:last.end()] + "\n\n" + none_text + block2[last.end():]
652
+ moved["n"] += 1
653
+ return block2
654
+
655
+ new = QUESTION_BLOCK_RE.sub(_fix_question, exercise)
656
+ return new, moved["n"]
657
+
658
+
659
+ def fix_mcq_answer_aliases(exercise: str) -> tuple[str, int]:
660
+ """Normalise les blocs d'options MCQ mal nommés/incomplets (vus en prod) :
661
+ • `::::{mcqOption}` / `::::{mcqChoice}` → `::::{mcqAnswer}` ;
662
+ • bloc mcqAnswer SANS ligne `:isRightAnswer:` → insère `false` (défaut)."""
663
+ fixed = {"n": 0}
664
+ out, n_alias = re.subn(r"(?m)^::::\{mcq(?:Option|Choice)\}", "::::{mcqAnswer}", exercise)
665
+ fixed["n"] += n_alias
666
+
667
+ def _ensure_flag(m: re.Match) -> str:
668
+ head, rest = m.group(1), m.group(2)
669
+ if rest.lstrip().startswith(":isRightAnswer:"):
670
+ return m.group(0)
671
+ fixed["n"] += 1
672
+ return f"{head}:isRightAnswer: false\n{rest}"
673
+
674
+ out = re.sub(r"(?ms)^(::::\{mcqAnswer\}\n)(.*?)(?=^::::$)",
675
+ lambda m: _ensure_flag(m), out)
676
+ return out, fixed["n"]
677
+
678
+
679
+ def merge_decl_python_blocks(exercise: str) -> tuple[str, int]:
680
+ """DÉCLINAISONS : la spec impose UN SEUL bloc {python}. Fusionne les blocs
681
+ additionnels dans le bloc principal (ordre préservé, un seul `globals()`
682
+ final) — SAUF si un bloc ultérieur RE-TIRE de l'aléatoire (`rd.`/`random.`) :
683
+ re-tirage = bug sémantique (valeurs incohérentes entre questions), on le
684
+ laisse en place pour que le harnais le signale et que la réparation LLM
685
+ corrige à la source."""
686
+ blocks = list(PYTHON_FENCE_RE.finditer(exercise))
687
+ if len(blocks) < 2:
688
+ return exercise, 0
689
+ extras = blocks[1:]
690
+ if any(re.search(r"\brd\.|(?<!_)\brandom\.", m.group("code")) for m in extras):
691
+ return exercise, 0 # re-tirage suspect → laisser le harnais trancher
692
+
693
+ main = blocks[0]
694
+ parts = [re.sub(r"(?m)^\s*globals\(\)\s*$", "", main.group("code")).rstrip()]
695
+ for m in extras:
696
+ code = re.sub(r"(?m)^\s*globals\(\)\s*$", "", m.group("code")).strip()
697
+ if code:
698
+ parts.append(code)
699
+ merged_code = "\n\n".join(parts) + "\n\nglobals()"
700
+
701
+ # Supprime les blocs additionnels (indices inversés), puis remplace le principal.
702
+ out = exercise
703
+ for m in reversed(extras):
704
+ out = out[:m.start()] + out[m.end():]
705
+ m0 = PYTHON_FENCE_RE.search(out)
706
+ out = (out[:m0.start()]
707
+ + f"{m0.group('open')}{{python}}\n{merged_code}\n{m0.group('open')}"
708
+ + out[m0.end():])
709
+ return re.sub(r"\n{3,}", "\n\n", out), len(extras)
710
+
711
+
712
+ # (Les contrôles statiques MCQ/FGQ — {{}} dans un rôle, :solution:, résidus
713
+ # légacy — vivent dans validation/harness.py::check_declinaison_static :
714
+ # une seule source de vérité pour le verdict.)
715
+
716
+
717
  # ─────────────────────────────────────────────────────────────────────────────
718
  # Langues : détection, réduction bilingue → mono, contrôle des décimales
719
  # ─────────────────────────────────────────────────────────────────────────────
app/pipeline/prompts.py CHANGED
@@ -523,6 +523,279 @@ TRANSLATE_FORMAT_BOTH = (
523
  )
524
 
525
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
  # ─────────────────────────────────────────────────────────────────────────────
527
  # Réparation post-harnais (NOUVEAU — 1 itération max)
528
  # ─────────────────────────────────────────────────────────────────────────────
@@ -554,9 +827,21 @@ RÈGLES DE CORRECTION :
554
  TIRAGE DÉGÉNÉRÉ — exclure la valeur fautive à la source
555
  (ex. `b = rd.randint(2, 5)` au lieu de `randint(1, 5)`, ou boucle de rejet
556
  `if b == 1: continue`). Ne PAS rafistoler le texte : corriger le tirage.
 
 
 
 
557
  • Injection non nue → pré-calculer en variable camelCase `…Aff`.
558
  • Le bloc {{python}} reste à 4 backticks et se termine par `globals()`.
559
  • Ne touche NI à `\\inftys`/`\\ds`/`\\dfrac`, NI à la prose des solutions.
 
 
 
 
 
 
 
 
560
 
561
  Réponds UNIQUEMENT avec l'exercice complet corrigé (de `````{{exercise}} à `````),
562
  sans préambule ni wrapper markdown.
 
523
  )
524
 
525
 
526
+ # ─────────────────────────────────────────────────────────────────────────────
527
+ # DÉCLINAISONS QCM / QAT (NOUVEAU 2026-07 — mode `declinaisons`)
528
+ # Spécification normative fournie par l'équipe (PROMPT_declinaisons_QCM_QAT §4-§6),
529
+ # calibrée sur les 33 exemples validés de knowledge/fewshots/declinaisons/.
530
+ # Divergences wx ↔ conventions strictes (corpus 222) tranchées : format
531
+ # plateforme des wx (mcqAnswer/:solution:/{input}) + conventions strictes de
532
+ # l'app (fences 4, globals(), injections nues camelCase Aff, IDs contigus).
533
+ # ─────────────────────────────────────────────────────────────────────────────
534
+
535
+ MCQ_SPEC = """\
536
+ FORMAT QCM (questionType MCQ) — NORMATIF :
537
+
538
+ :::::{question}
539
+ :questionType: MCQ
540
+ :questionId: N
541
+ :questionIndex: N
542
+
543
+ ::::{questionStatement}
544
+ <énoncé auto-suffisant ; maths et {{ }} HORS des rôles bilingues>
545
+ ::::
546
+
547
+ ::::{questionHint}
548
+ <indice — peut rester vide ; VERBATIM si repris de la source>
549
+ ::::
550
+
551
+ ::::{mcqAnswer}
552
+ :isRightAnswer: true
553
+ <BONNE réponse — TOUJOURS en slot 1>
554
+ ::::
555
+
556
+ ::::{mcqAnswer}
557
+ :isRightAnswer: false
558
+ <distracteur 1 — erreur type>
559
+ ::::
560
+
561
+ ::::{mcqAnswer}
562
+ :isRightAnswer: false
563
+ <distracteur 2 — erreur type>
564
+ ::::
565
+
566
+ ::::{mcqAnswer}
567
+ :isRightAnswer: false
568
+ <distracteur 3 — erreur type>
569
+ ::::
570
+
571
+ ::::{mcqAnswer}
572
+ :isRightAnswer: false
573
+ {fr}`Aucune de ces réponses n'est correcte`{en}`None of these answers are correct`
574
+ ::::
575
+
576
+ ::::{detailedSolution}
577
+ <solution détaillée ; VERBATIM de la source si elle existe, sinon rédigée>
578
+ ::::
579
+
580
+ ::::{weightDistribution}
581
+ :logic: 25
582
+ :abstraction: 25
583
+ :reasoning: 25
584
+ :calculation: 25
585
+ ::::
586
+ :::::
587
+
588
+ RÈGLES MCQ DURES :
589
+ • EXACTEMENT UNE option `:isRightAnswer: true`, EN SLOT 1 (l'affichage est
590
+ mélangé côté plateforme — ne PAS randomiser l'ordre dans le fichier).
591
+ • 5 options par défaut (1 correcte + 3 distracteurs + « None » EN DERNIER).
592
+ Ensembles fermés (vrai/faux, intervalles exhaustifs) : « None » omis
593
+ autorisé (⇒ 4 options ; jamais moins de 3).
594
+ • PAS de `:solution:`, PAS de `{input}`, PAS de displayedSolution en MCQ.
595
+ • Ordre des blocs : questionStatement → questionHint → mcqAnswer×N →
596
+ detailedSolution → weightDistribution.
597
+ • L'option « None » et toute option textuelle sont bilingues si la cible l'est.
598
+
599
+ DISTRACTEURS — ERREURS TYPES UNIQUEMENT (aucune valeur au hasard) :
600
+ • Algèbre : erreur de signe ; coefficient inversé ; terme oublié ; distribution partielle.
601
+ • Dérivées : oubli du facteur de la règle de chaîne ; exposant non décrémenté ;
602
+ primitive au lieu de dérivée ; quotient sans v² ; produit sans la 2e moitié.
603
+ • Intégrales : oubli du +1 sur l'exposant ; oubli du 1/a ; dérivée au lieu de
604
+ primitive ; oubli de |x| dans ln.
605
+ • Matrices : transposée ; colonnes/lignes échangées ; entrée non dérivée ;
606
+ matrice opposée ; oubli du terme +b.
607
+ • Compositions affines : oubli de b_f ; ordre inversé (g∘f vs f∘g) ; signe opposé.
608
+ • Limites : mauvaise forme indéterminée ; mauvais signe d'infini.
609
+
610
+ ANTI-COLLISION (le piège n°1 des QCM randomisés — un distracteur peut devenir
611
+ ÉGAL à la bonne réponse sur certaines graines) — dans CET ordre :
612
+ 1. DISTINCT PAR CONSTRUCTION (préféré) : distracteurs de type différent,
613
+ tirages qui garantissent la non-nullité/non-égalité (coefficients >= 2,
614
+ exposants >= 2, entrées non nulles…). Documente-le en commentaire Python.
615
+ 2. TIRAGE AVEC REJET dans le bloc Python : reboucler tant que les chaînes
616
+ RENDUES (latex) de toutes les options ne sont pas toutes distinctes.
617
+ 3. Le harnais vérifie l'unicité sur 100 graines — un doublon = REJET.
618
+ ⚠️ La collision est aussi SÉMANTIQUE : deux options formulées différemment
619
+ mais mathématiquement ÉQUIVALENTES (« divise x par 2 » ≡ « multiplie x par
620
+ 1/2 » ; « T_{1/a,1/b} » ≡ « division par a et b ») comptent comme un doublon
621
+ pour l'élève. Vérifie l'équivalence MATHÉMATIQUE de chaque paire d'options
622
+ sur TOUT l'espace des tirages (ex. b == 1/a possible ? → l'exclure au tirage).
623
+
624
+ UN SEUL bloc {python} au total : JAMAIS de re-tirage (`rd.`/`random`) hors du
625
+ bloc principal — un second tirage rendrait les valeurs incohérentes entre les
626
+ questions. Les variables des paires suivantes s'ajoutent SANS aléa nouveau.
627
+
628
+ FORMATAGE : STRICTEMENT IDENTIQUE entre bonne réponse et distracteurs (même
629
+ style LaTeX, mêmes helpers, même nombre de décimales, même notation
630
+ matricielle) — sinon la bonne réponse se devine.
631
+
632
+ L'ÉNONCÉ NE DONNE JAMAIS LA RÉPONSE (l'énoncé définit, la question interroge).
633
+ """ # noqa: E501 — texte normatif verbatim (valeur injectée telle quelle, accolades SIMPLES)
634
+
635
+ FGQ_SPEC = """\
636
+ FORMAT QAT (questionType FGQ — question à champ(s) libre(s) ordonné(s)) — NORMATIF :
637
+
638
+ :::::{question}
639
+ :questionType: FGQ
640
+ :questionId: N
641
+ :questionIndex: N
642
+ :solution: [["ord","${{v1Aff}}$","${{v2Aff}}$"],["0","0"]]
643
+
644
+ ::::{questionStatement}
645
+ <énoncé auto-suffisant>
646
+
647
+ <LIGNE VIDE obligatoire avant le premier {input}>
648
+ $x_1 =$ {input}`||110` $\\qquad x_2 =$ {input}`||110`
649
+ ::::
650
+
651
+ ::::{questionHint}
652
+ <indice — vide ou verbatim source>
653
+ ::::
654
+
655
+ ::::{displayedSolution}
656
+ $x_1 = {{v1Aff}}$, $\\quad x_2 = {{v2Aff}}$
657
+ ::::
658
+
659
+ ::::{detailedSolution}
660
+ <solution détaillée>
661
+ ::::
662
+
663
+ ::::{weightDistribution}
664
+ :logic: 15
665
+ :abstraction: 20
666
+ :reasoning: 20
667
+ :calculation: 45
668
+ ::::
669
+ :::::
670
+
671
+ RÈGLES FGQ DURES :
672
+ • `:solution:` DIRECTEMENT dans le champ (jamais construite dans une variable
673
+ Python), juste après `:questionIndex:`. Format [["ord","<v1>",…],["0",…]].
674
+ • ARITÉ STRICTE : nb de {input} == nb de valeurs dans "ord" == nb de
675
+ tolérances. Tolérance TOUJOURS "0" (exacte).
676
+ • Un `{{varAff}}` par valeur dynamique dans `:solution:` (variable NUE).
677
+ • ORDRE : les valeurs de "ord" suivent l'ordre d'apparition des {input}
678
+ dans l'énoncé. Plusieurs solutions (racines…) → ÉNONCER l'ordre (« de la
679
+ plus petite à la plus grande ») et le respecter dans "ord" ET dans
680
+ displayedSolution.
681
+ • Chaque {input} est INTRODUIT PAR UN LABEL ($x =$ {input}`||110`),
682
+ jamais nu, jamais collé à la prose ; LIGNE VIDE avant le premier {input}.
683
+ • Ordre des blocs : questionStatement → questionHint → displayedSolution →
684
+ detailedSolution → weightDistribution.
685
+ • Valeurs EXACTES (fractions, \\ln, \\sqrt, +\\infty…) — JAMAIS de décimales
686
+ approchées dans `:solution:`.
687
+ • REPLI MCQ : si une question n'est PAS auto-corrigeable en champ libre
688
+ (réponse avec fonction abstraite, vrai/faux, matrice à dimension VARIABLE),
689
+ produis cette question en MCQ (format ci-contre) — une sortie QAT peut être
690
+ mixte FGQ + MCQ. Ne force JAMAIS un champ libre ingérable.
691
+
692
+ MATRICES EN QAT (PIÈGE plateforme) :
693
+ • `pxsl_matrix` est INTERDIT dans un champ `:solution:` (le rendu
694
+ \\left[\\begin{array}… ne matche pas le widget). Si champ unique matrice :
695
+ variable calculée avec latex(M, mat_delim='', mat_str='pmatrix') (FR)
696
+ ou mat_str='bmatrix' (EN), injectée nue.
697
+ • Dimension variable ⇒ repli MCQ pour cette question.
698
+
699
+ L'ÉNONCÉ NE DONNE JAMAIS LA RÉPONSE (l'énoncé définit, la question interroge).
700
+ """ # noqa: E501 — texte normatif verbatim (valeur injectée telle quelle, accolades SIMPLES)
701
+
702
+ # Prompt de génération d'une déclinaison (QCM ou QAT) — même mécanique par
703
+ # paires que la pythonisation ; le champ {decl_spec} reçoit MCQ_SPEC ou FGQ_SPEC.
704
+ STEP_DECLINAISON_PROMPT = """\
705
+ Tu déclines un exercice PyxiScience MyST en version {decl_label}, niveau {niveau}.
706
+ L'exercice source (statique OU déjà pythonisé) est fourni ; tu produis la
707
+ déclinaison RANDOMISÉE (bloc Python terminé par `globals()` + injections),
708
+ au format plateforme EXACT ci-dessous.
709
+
710
+ ═══════════════════════════════════════════════════════════════════════════
711
+ CONVENTIONS PLATEFORME (identiques à la pythonisation — NON NÉGOCIABLES)
712
+ ═══════════════════════════════════════════════════════════════════════════
713
+ • Bloc Python : 4 backticks ````{{python}} … ```` terminé par `globals()`.
714
+ • Injections `{{{{ }}}}` : UNIQUEMENT des noms de variables NUS camelCase
715
+ suffixe `Aff` — JAMAIS d'appel de fonction, de calcul, d'underscore, ni
716
+ d'accès dict. Tout se pré-calcule dans le bloc Python.
717
+ • `{{{{ }}}}` et les maths TOUJOURS HORS des rôles {{fr}}`…`{{en}}`…` (un
718
+ placeholder dans un rôle ne s'évalue PAS — texte cassé).
719
+ • Règle du `$` collé à un chiffre : préfixe `${{}}`. Décimales localisées
720
+ (virgule FR / point EN). `latex(expr, **config_standard)`.
721
+ • `:questionId:`/`:questionIndex:` contigus dès 0.
722
+ • Interdits : \\py{{}}, \\qcm, \\qat, \\qcl, \\right/\\wrong (légacy),
723
+ \\begin{{align*}}, \\displaystyle, \\[ \\], $$.
724
+
725
+ FIDÉLITÉ À LA SOURCE :
726
+ • 1 question source → 1 question déclinée. NE JAMAIS inventer de
727
+ sous-questions ni enrichir l'énoncé.
728
+ • L'énoncé ne doit JAMAIS donner la réponse (l'énoncé définit, la question
729
+ interroge).
730
+ • detailedSolution : VERBATIM de la source si elle existe (seules les valeurs
731
+ littérales deviennent des {{{{varAff}}}}), sinon rédigée sobrement.
732
+ • Source DÉJÀ PYTHONISÉE : recopie son bloc Python À L'IDENTIQUE (octet pour
733
+ octet), puis ajoute `# === Ajouts déclinaison {decl_label} ===` suivi des
734
+ NOUVELLES variables (distracteurs/solutions), AVANT le `globals()` final.
735
+ • weightDistribution : repris de la question source si présent, sinon le
736
+ barème par défaut du format ci-dessous (somme = 100 TOUJOURS).
737
+
738
+ ═══════════════════════════════════════════════════════════════════════════
739
+ SPÉCIFICATION DU FORMAT {decl_label}
740
+ ═══════════════════════════════════════════════════════════════════════════
741
+ {decl_spec}
742
+
743
+ ═══════════════════════════════════════════════════════════════════════════
744
+ EXEMPLE CANONIQUE COMPLET (structure et conventions à imiter)
745
+ ═══════════════════════════════════════════════════════════════════════════
746
+ {fewshot}
747
+
748
+ ═══════════════════════════════════════════════════════════════════════════
749
+ CATALOGUE PyxiScience (helpers à utiliser DANS le bloc Python)
750
+ ═══════════════════════════════════════════════════════════════════════════
751
+ {functions}
752
+
753
+ ═══════════════════════════════════════════════════════════════════════════
754
+ CONTEXTE
755
+ ═══════════════════════════════════════════════════════════════════════════
756
+
757
+ EN-TÊTE déjà finalisé (NE PAS reproduire) :
758
+ {content}
759
+
760
+ VARIABLES DÉTECTÉES :
761
+ {analysis}
762
+
763
+ BLOCS PRÉCÉDENTS (ne pas redéfinir leurs variables, ne pas les répéter) :
764
+ {previous_blocks}
765
+
766
+ SECTION À DÉCLINER ({range_label} / {nb_total}) :
767
+ {current_segment}
768
+
769
+ {lang_directive}
770
+
771
+ ═══════════════════════════════════════════════════════════════════════════
772
+ RÈGLES D'ASSEMBLAGE PAR PAIRE
773
+ ═══════════════════════════════════════════════════════════════════════════
774
+ ⚠️ TU PRODUIS UNIQUEMENT LE CONTENU DE CETTE PAIRE (les paires précédentes
775
+ sont concaténées mécaniquement avant ta sortie).
776
+ ⚠️ EXACTEMENT {nb_current} bloc(s) `:::::{{question}}` — questionId/questionIndex
777
+ CONTINUS depuis la paire précédente.
778
+ ⚠️ PAIRE 1 UNIQUEMENT : le bloc ````{{python}}```` (source recopiée si déjà
779
+ pythonisée + ajouts déclinaison + `globals()`) puis l'énoncé général VERBATIM,
780
+ AVANT la première question.
781
+ ⚠️ PAIRES SUIVANTES : ni énoncé, ni ré-imports ; petit bloc ````{{python}}````
782
+ additionnel possible pour les nouvelles variables seulement.
783
+
784
+ ═══════════════════════════════════════════════════════════════════════════
785
+ CHECKLIST FINALE
786
+ ═══════════════════════════════════════════════════════════════════════════
787
+ □ Bloc ````{{python}}```` (4 backticks) terminé par `globals()`
788
+ □ CHAQUE `{{{{ }}}}` = variable NUE camelCase (Aff) — aucun appel/underscore
789
+ □ Aucun `{{{{ }}}}` NI maths à l'intérieur d'un rôle {{fr}}`…`/{{en}}`…`
790
+ □ MCQ : 1 seule bonne réponse (slot 1), « None » en dernier, options toutes
791
+ distinctes SUR TOUTES LES GRAINES, formatage identique
792
+ □ FGQ : arité #input == #valeurs == #tolérances ("0"), :solution: littérale,
793
+ ordre énoncé/"ord"/displayedSolution cohérents, labels devant chaque {{input}}
794
+ □ 1 question source → 1 question ; solutions source VERBATIM ; poids repris
795
+ □ IDs contigus dès 0 ; aucun motif interdit ; `\\%` pour les pourcentages
796
+ """
797
+
798
+
799
  # ─────────────────────────────────────────────────────────────────────────────
800
  # Réparation post-harnais (NOUVEAU — 1 itération max)
801
  # ─────────────────────────────────────────────────────────────────────────────
 
827
  TIRAGE DÉGÉNÉRÉ — exclure la valeur fautive à la source
828
  (ex. `b = rd.randint(2, 5)` au lieu de `randint(1, 5)`, ou boucle de rejet
829
  `if b == 1: continue`). Ne PAS rafistoler le texte : corriger le tirage.
830
+ **EXCEPTION — texte pédagogique FIXE** qui enseigne précisément la règle de
831
+ l'exposant (`$b^{{0}} = 1$`, `$b^{{1}} = b$`) : réécrire SANS accolades
832
+ (`$b^0 = 1$`, `$b^1 = b$`) — rendu LaTeX identique pour un exposant à un
833
+ seul caractère, et conforme au corpus validé.
834
  • Injection non nue → pré-calculer en variable camelCase `…Aff`.
835
  • Le bloc {{python}} reste à 4 backticks et se termine par `globals()`.
836
  • Ne touche NI à `\\inftys`/`\\ds`/`\\dfrac`, NI à la prose des solutions.
837
+ • **QCM — collision d'options** (deux options rendues identiques sur une
838
+ graine) : contraindre le TIRAGE (rejet `while` sur les chaînes rendues) ou
839
+ changer la CONSTRUCTION du distracteur — jamais rafistoler le texte.
840
+ • **QCM — plusieurs/zéro `:isRightAnswer: true`** : exactement une, en slot 1.
841
+ • **FGQ — arité** : le nb de {{{{input}}}} doit égaler le nb de valeurs de
842
+ `"ord"` et le nb de tolérances ("0") dans `:solution:`.
843
+ • **`{{{{ }}}}` dans un rôle {{fr}}`…`/{{en}}`…`** : sortir l'injection du
844
+ rôle (elle ne s'évalue pas dedans) — découper le rôle autour.
845
 
846
  Réponds UNIQUEMENT avec l'exercice complet corrigé (de `````{{exercise}} à `````),
847
  sans préambule ni wrapper markdown.
app/server.py CHANGED
@@ -30,10 +30,11 @@ from flask import Response, jsonify, render_template, request, send_file
30
  from app.config import (
31
  AVAILABLE_MODELS,
32
  DEFAULT_LANG,
 
33
  DEFAULT_MODEL_IDX,
34
  JOB_TTL,
35
  )
36
- from app.pipeline.orchestrator import run_exercise
37
 
38
  logger = logging.getLogger(__name__)
39
 
@@ -42,6 +43,7 @@ _JOBS_LOCK = threading.Lock()
42
 
43
  VALID_LANGS = ("fr", "en", "both")
44
  VALID_LEVELS = ("", "Elementary", "Intermediate", "Advanced")
 
45
 
46
 
47
  def _set_job(job_id: str, **kwargs):
@@ -57,14 +59,16 @@ def _get_job(job_id: str):
57
 
58
 
59
  def _safe_md_name(filename: str, used: set) -> str:
60
- """Nom de fichier .md sûr et unique pour la sortie pythonisée
61
- (suffixe _pythonise pour ne pas écraser la source)."""
 
62
  base = re.sub(r"\.(md|txt)$", "", filename or "", flags=re.IGNORECASE)
63
  base = re.sub(r"[^\w.\-() ]+", "_", base).strip() or "exercice"
64
- name = f"{base}_pythonise.md"
 
65
  i = 2
66
  while name in used:
67
- name = f"{base}_pythonise_{i}.md"
68
  i += 1
69
  used.add(name)
70
  return name
@@ -78,8 +82,17 @@ def _purge_old_jobs():
78
  del _JOBS[jid]
79
 
80
 
81
- def _run_job(job_id: str, files: list[dict], level: str, model_idx: int, lang: str):
82
- """Worker de job : boucle séquentielle sur les fichiers, robuste."""
 
 
 
 
 
 
 
 
 
83
  results: list[dict] = []
84
  for i, f in enumerate(files):
85
  name = f.get("filename") or f"fichier_{i + 1}.md"
@@ -90,19 +103,36 @@ def _run_job(job_id: str, files: list[dict], level: str, model_idx: int, lang: s
90
  _set_job(job_id, step_label=f"[{_i + 1}/{len(files)}] {_name} — {label}")
91
 
92
  try:
93
- result = run_exercise(
94
- content=f["content"],
95
- filename=name,
96
- level=level,
97
- model_idx=model_idx,
98
- lang=lang,
99
- set_step=set_step,
100
- )
101
- results.append({"filename": name, "status": "done", "result": result})
102
- logger.info("Fichier %s : harnais %s, %d warnings, %.1fs, %.4f$",
103
- name, "VERT" if result["harness"]["ok"] else "ROUGE",
104
- len(result["warnings"]), result["duration_s"],
105
- result["cost"]["usd"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  except Exception as exc:
107
  logger.exception("Échec du pipeline sur %s", name)
108
  results.append({"filename": name, "status": "error", "error": str(exc)})
@@ -212,6 +242,18 @@ def register_routes(app):
212
  "error": f"'model_idx' inconnu : {model_idx}. Valides : {sorted(AVAILABLE_MODELS)}."
213
  }), 400
214
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  job_id = uuid.uuid4().hex
216
  with _JOBS_LOCK:
217
  _JOBS[job_id] = {
@@ -229,7 +271,7 @@ def register_routes(app):
229
 
230
  threading.Thread(
231
  target=_run_job,
232
- args=(job_id, clean_files, level, model_idx, lang),
233
  daemon=True,
234
  ).start()
235
  return jsonify({"job_id": job_id}), 202
 
30
  from app.config import (
31
  AVAILABLE_MODELS,
32
  DEFAULT_LANG,
33
+ DEFAULT_MODE,
34
  DEFAULT_MODEL_IDX,
35
  JOB_TTL,
36
  )
37
+ from app.pipeline.orchestrator import run_declinaisons, run_exercise
38
 
39
  logger = logging.getLogger(__name__)
40
 
 
43
 
44
  VALID_LANGS = ("fr", "en", "both")
45
  VALID_LEVELS = ("", "Elementary", "Intermediate", "Advanced")
46
+ VALID_MODES = ("pythonise", "declinaisons")
47
 
48
 
49
  def _set_job(job_id: str, **kwargs):
 
59
 
60
 
61
  def _safe_md_name(filename: str, used: set) -> str:
62
+ """Nom de fichier .md sûr et unique pour le ZIP. Les déclinaisons portent
63
+ déjà leur suffixe (_QCM/_QAT) ; la pythonisation reçoit _pythonise pour
64
+ ne pas écraser la source."""
65
  base = re.sub(r"\.(md|txt)$", "", filename or "", flags=re.IGNORECASE)
66
  base = re.sub(r"[^\w.\-() ]+", "_", base).strip() or "exercice"
67
+ suffix = "" if re.search(r"_(QCM|QAT)$", base) else "_pythonise"
68
+ name = f"{base}{suffix}.md"
69
  i = 2
70
  while name in used:
71
+ name = f"{base}{suffix}_{i}.md"
72
  i += 1
73
  used.add(name)
74
  return name
 
82
  del _JOBS[jid]
83
 
84
 
85
+ def _decl_output_name(source_name: str, decl_type: str) -> str:
86
+ """foo.md + qcm foo_QCM.md (nommage de sortie des déclinaisons)."""
87
+ base = re.sub(r"\.(md|txt)$", "", source_name or "exercice", flags=re.IGNORECASE)
88
+ return f"{base}_{'QCM' if decl_type == 'qcm' else 'QAT'}.md"
89
+
90
+
91
+ def _run_job(job_id: str, files: list[dict], level: str, model_idx: int,
92
+ lang: str, mode: str = "pythonise", decl_types: list | None = None):
93
+ """Worker de job : boucle séquentielle sur les fichiers, robuste.
94
+ En mode `declinaisons`, chaque source produit 1 résultat PAR type coché
95
+ (analyse partagée entre types — aucun appel LLM redondant)."""
96
  results: list[dict] = []
97
  for i, f in enumerate(files):
98
  name = f.get("filename") or f"fichier_{i + 1}.md"
 
103
  _set_job(job_id, step_label=f"[{_i + 1}/{len(files)}] {_name} — {label}")
104
 
105
  try:
106
+ if mode == "declinaisons":
107
+ for decl_type, result in run_declinaisons(
108
+ content=f["content"],
109
+ filename=name,
110
+ level=level,
111
+ model_idx=model_idx,
112
+ lang=lang,
113
+ types=decl_types,
114
+ set_step=set_step,
115
+ ):
116
+ out_name = _decl_output_name(name, decl_type)
117
+ results.append({"filename": out_name, "status": "done", "result": result})
118
+ logger.info("Déclinaison %s : harnais %s, %d warnings, %.1fs, %.4f$",
119
+ out_name, "VERT" if result["harness"]["ok"] else "ROUGE",
120
+ len(result["warnings"]), result["duration_s"],
121
+ result["cost"]["usd"])
122
+ else:
123
+ result = run_exercise(
124
+ content=f["content"],
125
+ filename=name,
126
+ level=level,
127
+ model_idx=model_idx,
128
+ lang=lang,
129
+ set_step=set_step,
130
+ )
131
+ results.append({"filename": name, "status": "done", "result": result})
132
+ logger.info("Fichier %s : harnais %s, %d warnings, %.1fs, %.4f$",
133
+ name, "VERT" if result["harness"]["ok"] else "ROUGE",
134
+ len(result["warnings"]), result["duration_s"],
135
+ result["cost"]["usd"])
136
  except Exception as exc:
137
  logger.exception("Échec du pipeline sur %s", name)
138
  results.append({"filename": name, "status": "error", "error": str(exc)})
 
242
  "error": f"'model_idx' inconnu : {model_idx}. Valides : {sorted(AVAILABLE_MODELS)}."
243
  }), 400
244
 
245
+ mode = (data.get("mode") or DEFAULT_MODE).strip()
246
+ if mode not in VALID_MODES:
247
+ return jsonify({"error": f"'mode' invalide : {mode!r} (pythonise|declinaisons)."}), 400
248
+ decl_types: list = []
249
+ if mode == "declinaisons":
250
+ types_obj = data.get("types") or {}
251
+ if not isinstance(types_obj, dict):
252
+ return jsonify({"error": "'types' doit être un objet {qcm: bool, qat: bool}."}), 400
253
+ decl_types = [t for t in ("qcm", "qat") if types_obj.get(t)]
254
+ if not decl_types:
255
+ return jsonify({"error": "En mode 'declinaisons', cocher au moins un type (qcm/qat)."}), 400
256
+
257
  job_id = uuid.uuid4().hex
258
  with _JOBS_LOCK:
259
  _JOBS[job_id] = {
 
271
 
272
  threading.Thread(
273
  target=_run_job,
274
+ args=(job_id, clean_files, level, model_idx, lang, mode, decl_types),
275
  daemon=True,
276
  ).start()
277
  return jsonify({"job_id": job_id}), 202
app/validation/harness.py CHANGED
@@ -65,18 +65,25 @@ def install_pyxiscience_stubs() -> None:
65
  # sympy.latex (qui rejette toute clé inconnue).
66
  return {}
67
 
 
 
 
 
68
  def _make_module(name: str) -> types.ModuleType:
69
  mod = types.ModuleType(name)
70
 
71
  def __getattr__(attr): # PEP 562
72
  if attr == "pxs_config":
73
  return _pxs_config
 
 
74
  # Classe si CamelCase OU motif pxs_Xxx (pxs_Interval, pxs_Plotable…)
75
  if attr and (attr[0].isupper() or re.match(r"pxs_[A-Z]", attr)):
76
  return _Universal
77
  return _passthrough
78
 
79
  mod.__getattr__ = __getattr__
 
80
  return mod
81
 
82
  root = _make_module("pyxiscience")
@@ -184,6 +191,132 @@ def scan_forbidden(body: str) -> list[str]:
184
  return errs
185
 
186
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  # ── Rendu (substitution des injections) ──────────────────────────────────────
188
 
189
  def render_body(body_tmpl: str, env: dict) -> tuple[str, list[str]]:
@@ -218,6 +351,9 @@ def validate_text(text: str, seeds: int = 100) -> dict:
218
  }
219
  """
220
  static_errors = check_injection_tokens(text) + check_question_ids(text)
 
 
 
221
  code = extract_python_code(text)
222
  if code is None:
223
  static_errors.append("aucun bloc `{python}` trouvé")
@@ -229,6 +365,7 @@ def validate_text(text: str, seeds: int = 100) -> dict:
229
  "n_exec_errors": 0,
230
  "n_unresolved": 0,
231
  "n_forbidden": 0,
 
232
  "first_failures": [],
233
  }
234
  if code is None:
@@ -261,11 +398,22 @@ def validate_text(text: str, seeds: int = 100) -> dict:
261
  report["n_unresolved"] += 1
262
  if len(failures) < 8:
263
  failures.append(f"seed {s} : variable(s) non résolue(s) : {sorted(set(unresolved))}")
264
- fb = scan_forbidden(rendered)
 
 
 
 
 
265
  if fb:
266
  report["n_forbidden"] += 1
267
  if len(failures) < 8:
268
  failures.append(f"seed {s} : motif interdit : {fb[0]}")
 
 
 
 
 
 
269
 
270
  report["first_failures"] = failures
271
  report["ok"] = (
@@ -273,6 +421,7 @@ def validate_text(text: str, seeds: int = 100) -> dict:
273
  and report["n_exec_errors"] == 0
274
  and report["n_unresolved"] == 0
275
  and report["n_forbidden"] == 0
 
276
  )
277
  return report
278
 
@@ -289,5 +438,7 @@ def format_report(report: dict) -> str:
289
  lines.append(f"[INJECTION] {report['n_unresolved']}/{n} graines avec variables non résolues")
290
  if report["n_forbidden"]:
291
  lines.append(f"[INTERDITS] {report['n_forbidden']}/{n} graines avec motif interdit")
 
 
292
  lines.extend(f" • {f}" for f in report["first_failures"])
293
  return "\n".join(lines) if lines else "VERT"
 
65
  # sympy.latex (qui rejette toute clé inconnue).
66
  return {}
67
 
68
+ # Liste canonique partagée avec la sandbox (les deux systèmes de stubs
69
+ # partagent sys.modules — le premier installé sert aux deux).
70
+ from app.validation.sandbox import KNOWN_PXS_HELPERS
71
+
72
  def _make_module(name: str) -> types.ModuleType:
73
  mod = types.ModuleType(name)
74
 
75
  def __getattr__(attr): # PEP 562
76
  if attr == "pxs_config":
77
  return _pxs_config
78
+ if attr == "pxs_variation_number":
79
+ return 1
80
  # Classe si CamelCase OU motif pxs_Xxx (pxs_Interval, pxs_Plotable…)
81
  if attr and (attr[0].isupper() or re.match(r"pxs_[A-Z]", attr)):
82
  return _Universal
83
  return _passthrough
84
 
85
  mod.__getattr__ = __getattr__
86
+ mod.__all__ = list(KNOWN_PXS_HELPERS)
87
  return mod
88
 
89
  root = _make_module("pyxiscience")
 
191
  return errs
192
 
193
 
194
+ # ── Contrôles déclinaisons QCM/QAT (mode `declinaisons`) ─────────────────────
195
+
196
+ _QUESTION_RE = re.compile(r":::::\{question\}.*?:::::", re.DOTALL)
197
+ _MCQ_ANSWER_RE = re.compile(
198
+ r"::::\{mcqAnswer\}\s*\n:isRightAnswer:\s*(true|false)\s*\n(.*?)\n::::",
199
+ re.DOTALL,
200
+ )
201
+ _NONE_OPTION_RE = re.compile(r"Aucune de ces réponses|None of these answers", re.IGNORECASE)
202
+ _SOLUTION_FIELD_RE = re.compile(r"(?m)^:solution:[ \t]*(.+)$")
203
+ _INPUT_RE = re.compile(r"\{input\}`")
204
+ _QTYPE_RE = re.compile(r":questionType:\s*(\w+)")
205
+
206
+
207
+ def has_declinaison_questions(text: str) -> bool:
208
+ return bool(re.search(r":questionType:\s*(MCQ|FGQ)\b", text))
209
+
210
+
211
+ _LEGACY_PATTERNS = [
212
+ (re.compile(r"\\py\{"), r"résidu légacy `\py{`"),
213
+ (re.compile(r"\\qcm\b|\\qat\b|\\qcl\b"), r"résidu légacy `\qcm`/`\qat`/`\qcl`"),
214
+ (re.compile(r"\\input\{null\}"), r"résidu légacy `\input{null}`"),
215
+ (re.compile(r"\\begin\{align\*?\}"), r"`\begin{align*}` interdit (utiliser equation*)"),
216
+ (re.compile(r"(?<!\\)\$\$"), r"`$$` interdit (utiliser equation*)"),
217
+ ]
218
+
219
+
220
+ def check_declinaison_static(text: str) -> list[str]:
221
+ """Contrôles statiques MCQ/FGQ (indépendants des graines)."""
222
+ import json as _json
223
+ errs: list[str] = []
224
+ body = strip_python_blocks(text)
225
+
226
+ # {{ }} dans un rôle bilingue → ne s'évalue pas.
227
+ for m in re.finditer(r"\{(?:fr|en)\}`([^`]*)`", body):
228
+ if "{{" in m.group(1):
229
+ errs.append(f"injection `{{{{…}}}}` dans un rôle bilingue : …{m.group(0)[:80]}…")
230
+ break
231
+
232
+ # Résidus de l'ancienne syntaxe plateforme.
233
+ errs += [msg for rx, msg in _LEGACY_PATTERNS if rx.search(body)]
234
+
235
+ # UN SEUL bloc {python} en déclinaison (un bloc additionnel qui RE-TIRE de
236
+ # l'aléatoire rendrait les valeurs incohérentes entre questions ; les blocs
237
+ # sans re-tirage sont fusionnés en amont par merge_decl_python_blocks).
238
+ n_blocks = len(PY_FENCE_ANY_RE.findall(text))
239
+ if n_blocks > 1:
240
+ errs.append(f"{n_blocks} blocs {{python}} (un seul attendu en déclinaison — "
241
+ "bloc additionnel avec re-tirage aléatoire probable)")
242
+
243
+ # Approximations dans :solution: (valeurs exactes uniquement).
244
+ for m in _SOLUTION_FIELD_RE.finditer(text):
245
+ if "\\approx" in m.group(1) or "≈" in m.group(1):
246
+ errs.append("approximation (≈) dans un champ `:solution:` — valeurs exactes uniquement")
247
+ break
248
+
249
+ for qi, qm in enumerate(_QUESTION_RE.finditer(text)):
250
+ q = qm.group(0)
251
+ tm = _QTYPE_RE.search(q)
252
+ qtype = tm.group(1) if tm else "?"
253
+
254
+ if qtype == "MCQ":
255
+ answers = list(_MCQ_ANSWER_RE.finditer(q))
256
+ n_true = sum(1 for a in answers if a.group(1) == "true")
257
+ if n_true != 1:
258
+ errs.append(f"question {qi} (MCQ) : {n_true} option(s) `:isRightAnswer: true` (attendu : exactement 1)")
259
+ if not (3 <= len(answers) <= 6):
260
+ errs.append(f"question {qi} (MCQ) : {len(answers)} options (attendu : 3 à 5)")
261
+ none_idx = [i for i, a in enumerate(answers) if _NONE_OPTION_RE.search(a.group(2))]
262
+ if none_idx and none_idx[-1] != len(answers) - 1:
263
+ errs.append(f"question {qi} (MCQ) : option « None/Aucune » pas en dernière position")
264
+ if _SOLUTION_FIELD_RE.search(q) or _INPUT_RE.search(q):
265
+ errs.append(f"question {qi} (MCQ) : `:solution:`/`{{input}}` interdits en MCQ")
266
+
267
+ elif qtype == "FGQ":
268
+ sol = _SOLUTION_FIELD_RE.search(q)
269
+ n_inputs = len(_INPUT_RE.findall(q))
270
+ if not sol:
271
+ errs.append(f"question {qi} (FGQ) : champ `:solution:` manquant")
272
+ continue
273
+ if n_inputs == 0:
274
+ errs.append(f"question {qi} (FGQ) : aucun `{{input}}` dans l'énoncé")
275
+ raw = sol.group(1).strip()
276
+ if "pxsl_matrix" in raw:
277
+ errs.append(f"question {qi} (FGQ) : `pxsl_matrix` dans `:solution:` (interdit)")
278
+ # Parse JSON structurel : on remplace les {{var}} par un littéral neutre.
279
+ probe = INJECTION_RE.sub("V", raw)
280
+ try:
281
+ data = _json.loads(probe)
282
+ assert (isinstance(data, list) and len(data) == 2
283
+ and isinstance(data[0], list) and isinstance(data[1], list)
284
+ and data[0] and data[0][0] in ("ord", "CL"))
285
+ n_vals = len(data[0]) - 1
286
+ n_tols = len(data[1])
287
+ if not (n_vals == n_tols == n_inputs):
288
+ errs.append(f"question {qi} (FGQ) : arité incohérente — "
289
+ f"{n_inputs} input(s), {n_vals} valeur(s), {n_tols} tolérance(s)")
290
+ if any(str(t).strip() != "0" for t in data[1]):
291
+ errs.append(f"question {qi} (FGQ) : tolérance ≠ \"0\"")
292
+ except Exception:
293
+ errs.append(f"question {qi} (FGQ) : `:solution:` non parseable en JSON : {raw[:80]}")
294
+
295
+ return errs
296
+
297
+
298
+ def check_mcq_collisions(text: str, env: dict) -> list[str]:
299
+ """Pour UNE graine : deux options d'un même MCQ rendues identiques = échec.
300
+ (Le piège n°1 des QCM randomisés — un distracteur « erreur type » peut
301
+ devenir égal à la bonne réponse sur certaines graines.)"""
302
+ errs: list[str] = []
303
+ for qi, qm in enumerate(_QUESTION_RE.finditer(text)):
304
+ q = qm.group(0)
305
+ if not re.search(r":questionType:\s*MCQ\b", q):
306
+ continue
307
+ rendered: list[str] = []
308
+ for a in _MCQ_ANSWER_RE.finditer(q):
309
+ r, _unres = render_body(a.group(2), env)
310
+ rendered.append(re.sub(r"\s+", " ", r).strip())
311
+ seen: dict[str, int] = {}
312
+ for i, r in enumerate(rendered):
313
+ if r in seen:
314
+ errs.append(f"question {qi} (MCQ) : options {seen[r]} et {i} identiques après rendu : « {r[:70]} »")
315
+ else:
316
+ seen[r] = i
317
+ return errs
318
+
319
+
320
  # ── Rendu (substitution des injections) ──────────────────────────────────────
321
 
322
  def render_body(body_tmpl: str, env: dict) -> tuple[str, list[str]]:
 
351
  }
352
  """
353
  static_errors = check_injection_tokens(text) + check_question_ids(text)
354
+ declinaison = has_declinaison_questions(text)
355
+ if declinaison:
356
+ static_errors += check_declinaison_static(text)
357
  code = extract_python_code(text)
358
  if code is None:
359
  static_errors.append("aucun bloc `{python}` trouvé")
 
365
  "n_exec_errors": 0,
366
  "n_unresolved": 0,
367
  "n_forbidden": 0,
368
+ "n_mcq_collisions": 0,
369
  "first_failures": [],
370
  }
371
  if code is None:
 
398
  report["n_unresolved"] += 1
399
  if len(failures) < 8:
400
  failures.append(f"seed {s} : variable(s) non résolue(s) : {sorted(set(unresolved))}")
401
+ # Les lignes `:solution:` (FGQ) sont des motifs de correspondance de
402
+ # réponse, pas du texte affiché : exclues du scan des motifs interdits
403
+ # (`$5$` y est légitime — confirmé par les exemples validés). Leur
404
+ # format est contrôlé séparément (JSON, arité, exactitude).
405
+ scan_target = re.sub(r"(?m)^:solution:.*$", "", rendered)
406
+ fb = scan_forbidden(scan_target)
407
  if fb:
408
  report["n_forbidden"] += 1
409
  if len(failures) < 8:
410
  failures.append(f"seed {s} : motif interdit : {fb[0]}")
411
+ if declinaison:
412
+ mc = check_mcq_collisions(text, env)
413
+ if mc:
414
+ report["n_mcq_collisions"] += 1
415
+ if len(failures) < 8:
416
+ failures.append(f"seed {s} : collision d'options MCQ : {mc[0]}")
417
 
418
  report["first_failures"] = failures
419
  report["ok"] = (
 
421
  and report["n_exec_errors"] == 0
422
  and report["n_unresolved"] == 0
423
  and report["n_forbidden"] == 0
424
+ and report["n_mcq_collisions"] == 0
425
  )
426
  return report
427
 
 
438
  lines.append(f"[INJECTION] {report['n_unresolved']}/{n} graines avec variables non résolues")
439
  if report["n_forbidden"]:
440
  lines.append(f"[INTERDITS] {report['n_forbidden']}/{n} graines avec motif interdit")
441
+ if report.get("n_mcq_collisions"):
442
+ lines.append(f"[MCQ] {report['n_mcq_collisions']}/{n} graines avec collision d'options")
443
  lines.extend(f" • {f}" for f in report["first_failures"])
444
  return "\n".join(lines) if lines else "VERT"
app/validation/sandbox.py CHANGED
@@ -78,10 +78,62 @@ class _StubObject:
78
  return f"_StubObject({self._args!r})"
79
 
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  def install_pyxiscience_stubs() -> None:
82
  """
83
  Register stub modules for `pyxiscience.*` in sys.modules so that generated
84
  Python blocks can `import` PyxiScience helpers without crashing. Idempotent.
 
 
85
  """
86
  global _STUBS_INSTALLED
87
  if _STUBS_INSTALLED:
@@ -91,7 +143,7 @@ def install_pyxiscience_stubs() -> None:
91
  _STUBS_INSTALLED = True
92
  return
93
 
94
- pyxiscience = types.ModuleType("pyxiscience")
95
  sys.modules["pyxiscience"] = pyxiscience
96
 
97
  submodules = [
@@ -101,25 +153,12 @@ def install_pyxiscience_stubs() -> None:
101
  "Mes_fctions_d_analyse", # alias without _bis (cf. Exo 2 Am. Sud)
102
  "Mes_fctions_d_alg_lineaire_bis",
103
  "Mes_fctions_probabilistes_bis",
 
 
 
104
  ]
105
- helper_names = [
106
- "pxs_config", "pxsl_latex_coefficient", "pxsl_format_number",
107
- "pxsl_res_num", "pxsl_matrix", "pxsl_pow", "pxsl_par", "pxsl_mult",
108
- "pxs_explain_IBP", "pxs_nvirgzero", "_pxsl_choose_udv",
109
- "pxs_variation_number", "myst",
110
- "pxsl_latex_avec_formatage", # cf. Exo 2 Am Sud
111
- "pxsl_choose_udv",
112
- ]
113
- class_names = ["pxs_Interval", "pxs_Plotable"]
114
-
115
  for sub in submodules:
116
- m = types.ModuleType(f"pyxiscience.{sub}")
117
- for name in helper_names:
118
- setattr(m, name, _passthrough)
119
- # Special-case: pxs_config must return a dict (used as **kwargs).
120
- setattr(m, "pxs_config", _config_stub)
121
- for name in class_names:
122
- setattr(m, name, _StubObject)
123
  sys.modules[f"pyxiscience.{sub}"] = m
124
  setattr(pyxiscience, sub, m)
125
 
 
78
  return f"_StubObject({self._args!r})"
79
 
80
 
81
+ # Liste CANONIQUE des helpers connus (catalogue curé + corpus 222 + 33 exemples
82
+ # déclinaisons). Sert au `__all__` des modules stub : `from X import *` ne
83
+ # passe PAS par __getattr__ (PEP 562), il lit __all__. Partagée avec
84
+ # validation/harness.py — NE PAS dupliquer ailleurs.
85
+ KNOWN_PXS_HELPERS = [
86
+ "pxs_config", "pxsl_latex", "pxsl_sign", "pxsl_format_number",
87
+ "pxsl_latex_with_formatting", "pxsl_latex_avec_formatage",
88
+ "pxsl_latex_coefficient", "pxsl_to_rational_or_symbol",
89
+ "pxsl_solve_general_inequality", "pxsl_Rational",
90
+ "pxs_is_reductible_sqrt", "pxs_separate_factors",
91
+ "pxs_explain_IBP", "pxsl_par", "pxsl_final_sentence",
92
+ "pxsl_pow", "pxsl_matrix", "pxsl_mat", "pxsl_sum_matrix",
93
+ "pxsl_prod_scalar_matrix", "pxsl_prod_matrix", "pxsl_ax",
94
+ "pxsl_system_lin", "pxsl_double_matrix", "pxsl_lines_op",
95
+ "pxsl_resol_system", "pxs_steps_invert_matrix", "pxs_compute_ech",
96
+ "pxs_compute_ech_reduite", "pxs_system_simpl", "pxs_commute_matrix",
97
+ "pxsl_pow_matrix", "pxs_invertible_matrix", "pxs_diag_matrix",
98
+ "randmatrixrect", "pxs_finiterv", "pxsl_law", "pxsl_moment",
99
+ "pxsl_scalar_product", "pxs_simul_law", "pxs_fct_finiterv",
100
+ "pxsl_res_num", "pxsl_sum_vector", "pxs_nvirgzero", "pxsl_num",
101
+ "pxs_gauss_jordan", "pxs_construct_RREF",
102
+ "pxs_repeat_generate_sys", "pxs_break_all_colinear_rows",
103
+ "pxsl_mult", "pxsl_choose_udv", "pxs_lang", "myst",
104
+ "pxs_variation_number",
105
+ "pxs_Interval", "pxs_Plotable",
106
+ ]
107
+ _STUB_CLASS_NAMES = ("pxs_Interval", "pxs_Plotable")
108
+
109
+
110
+ def _make_stub_module(name: str) -> types.ModuleType:
111
+ """Module stub PEP 562 : tout attribut inconnu est fourni (passthrough /
112
+ classe universelle), et `__all__` couvre les helpers connus pour que
113
+ `from X import *` fonctionne."""
114
+ import re as _re
115
+ mod = types.ModuleType(name)
116
+
117
+ def __getattr__(attr):
118
+ if attr == "pxs_config":
119
+ return _config_stub
120
+ if attr == "pxs_variation_number":
121
+ return 1 # règle 13.2 : vaut toujours 1
122
+ if attr and (attr[0].isupper() or _re.match(r"pxs_[A-Z]", attr)):
123
+ return _StubObject # ressemble à une classe
124
+ return _passthrough
125
+
126
+ mod.__getattr__ = __getattr__
127
+ mod.__all__ = list(KNOWN_PXS_HELPERS)
128
+ return mod
129
+
130
+
131
  def install_pyxiscience_stubs() -> None:
132
  """
133
  Register stub modules for `pyxiscience.*` in sys.modules so that generated
134
  Python blocks can `import` PyxiScience helpers without crashing. Idempotent.
135
+ UNIFIÉ avec validation/harness.py (même factory PEP 562 + __all__) : les
136
+ deux systèmes partagent sys.modules, le premier installé sert aux deux.
137
  """
138
  global _STUBS_INSTALLED
139
  if _STUBS_INSTALLED:
 
143
  _STUBS_INSTALLED = True
144
  return
145
 
146
+ pyxiscience = _make_stub_module("pyxiscience")
147
  sys.modules["pyxiscience"] = pyxiscience
148
 
149
  submodules = [
 
153
  "Mes_fctions_d_analyse", # alias without _bis (cf. Exo 2 Am. Sud)
154
  "Mes_fctions_d_alg_lineaire_bis",
155
  "Mes_fctions_probabilistes_bis",
156
+ "Mes_fctions_generalistes", # alias historiques
157
+ "Mes_fctions_probabilistes",
158
+ "Mes_fctions_d_alg_lineaire",
159
  ]
 
 
 
 
 
 
 
 
 
 
160
  for sub in submodules:
161
+ m = _make_stub_module(f"pyxiscience.{sub}")
 
 
 
 
 
 
162
  sys.modules[f"pyxiscience.{sub}"] = m
163
  setattr(pyxiscience, sub, m)
164
 
app/web/templates/index.html CHANGED
@@ -343,6 +343,24 @@
343
  <ul class="filelist" id="filelist" hidden></ul>
344
 
345
  <div class="meta">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  <div>
347
  <label class="field-label" for="model-select">Modèle</label>
348
  <select id="model-select" aria-label="Modèle LLM"></select>
@@ -478,6 +496,12 @@ function prettyModelName(slug) {
478
  .replace(/\b\w/g, c => c.toUpperCase()).replace(/Gpt/g, "GPT");
479
  }
480
 
 
 
 
 
 
 
481
  /* ─── Tabs ─── */
482
  function bindTabs() {
483
  document.querySelectorAll("#tabs .tab").forEach(btn => {
@@ -678,7 +702,15 @@ function setMetaResult(entry) {
678
  const cost = r.cost ? `<span class="pill pill--info">${r.cost.usd.toFixed(4)} $ · ${r.cost.requests} appels</span>` : "";
679
  const lang = r.lang ? `<span class="pill pill--info">langue : ${escapeHtml(r.lang.source)} → ${escapeHtml(r.lang.target)} (${escapeHtml(r.lang.action)})</span>` : "";
680
  const dur = r.duration_s ? `<span class="pill pill--info">${r.duration_s}s</span>` : "";
681
- el.innerHTML = verdict + cost + lang + dur;
 
 
 
 
 
 
 
 
682
  }
683
 
684
  /* Sélecteur de fichier (batch) */
@@ -721,12 +753,24 @@ async function run() {
721
  }
722
  if (!files.length) { setStatus("error", "Saisissez un énoncé ou chargez des fichiers .md."); return; }
723
 
 
724
  const payload = {
725
  files,
726
  level: document.getElementById("level").value,
727
  lang: document.getElementById("lang-select").value,
728
  model_idx: Number(document.getElementById("model-select").value || 0),
 
729
  };
 
 
 
 
 
 
 
 
 
 
730
 
731
  if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; }
732
  currentJobId = null;
 
343
  <ul class="filelist" id="filelist" hidden></ul>
344
 
345
  <div class="meta">
346
+ <div>
347
+ <label class="field-label" for="mode-select">Mode</label>
348
+ <select id="mode-select" onchange="onModeChange()">
349
+ <option value="pythonise">Pythonisation</option>
350
+ <option value="declinaisons">Déclinaisons (QCM/QAT)</option>
351
+ </select>
352
+ </div>
353
+ <div id="decl-types" hidden>
354
+ <label class="field-label">Types de déclinaison</label>
355
+ <div style="display:flex;gap:14px;padding:9px 2px;font-size:13px;">
356
+ <label style="display:flex;align-items:center;gap:6px;cursor:pointer;">
357
+ <input type="checkbox" id="type-qcm" checked> QCM
358
+ </label>
359
+ <label style="display:flex;align-items:center;gap:6px;cursor:pointer;">
360
+ <input type="checkbox" id="type-qat"> QAT
361
+ </label>
362
+ </div>
363
+ </div>
364
  <div>
365
  <label class="field-label" for="model-select">Modèle</label>
366
  <select id="model-select" aria-label="Modèle LLM"></select>
 
496
  .replace(/\b\w/g, c => c.toUpperCase()).replace(/Gpt/g, "GPT");
497
  }
498
 
499
+ /* ─── Mode (pythonisation / déclinaisons) ─── */
500
+ function onModeChange() {
501
+ const decl = document.getElementById("mode-select").value === "declinaisons";
502
+ document.getElementById("decl-types").hidden = !decl;
503
+ }
504
+
505
  /* ─── Tabs ─── */
506
  function bindTabs() {
507
  document.querySelectorAll("#tabs .tab").forEach(btn => {
 
702
  const cost = r.cost ? `<span class="pill pill--info">${r.cost.usd.toFixed(4)} $ · ${r.cost.requests} appels</span>` : "";
703
  const lang = r.lang ? `<span class="pill pill--info">langue : ${escapeHtml(r.lang.source)} → ${escapeHtml(r.lang.target)} (${escapeHtml(r.lang.action)})</span>` : "";
704
  const dur = r.duration_s ? `<span class="pill pill--info">${r.duration_s}s</span>` : "";
705
+ let decl = "";
706
+ if (r.decl_type) {
707
+ const label = r.decl_type === "qcm" ? "QCM" : "QAT";
708
+ decl = `<span class="pill pill--info">déclinaison : ${label}</span>`;
709
+ if (r.decl_type === "qat" && (r.exercise || "").includes(":questionType: MCQ")) {
710
+ decl += `<span class="pill pill--info">repli MCQ partiel</span>`;
711
+ }
712
+ }
713
+ el.innerHTML = verdict + decl + cost + lang + dur;
714
  }
715
 
716
  /* Sélecteur de fichier (batch) */
 
753
  }
754
  if (!files.length) { setStatus("error", "Saisissez un énoncé ou chargez des fichiers .md."); return; }
755
 
756
+ const mode = document.getElementById("mode-select").value;
757
  const payload = {
758
  files,
759
  level: document.getElementById("level").value,
760
  lang: document.getElementById("lang-select").value,
761
  model_idx: Number(document.getElementById("model-select").value || 0),
762
+ mode,
763
  };
764
+ if (mode === "declinaisons") {
765
+ payload.types = {
766
+ qcm: document.getElementById("type-qcm").checked,
767
+ qat: document.getElementById("type-qat").checked,
768
+ };
769
+ if (!payload.types.qcm && !payload.types.qat) {
770
+ setStatus("error", "Mode Déclinaisons : coche au moins un type (QCM ou QAT).");
771
+ return;
772
+ }
773
+ }
774
 
775
  if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; }
776
  currentJobId = null;
tests/smoke.py ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Smoke tests minimaux — AUCUN appel réseau (LLM mocké), exécution :
3
+
4
+ .venv/bin/python tests/smoke.py
5
+
6
+ Couvre : import/create_app, /health, 1 run de pipeline complet (mock LLM)
7
+ avec porte harnais VERTE, et les filets déterministes clés.
8
+ """
9
+
10
+ import json
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
15
+
16
+ PASS = []
17
+
18
+
19
+ def check(name, cond):
20
+ PASS.append((name, bool(cond)))
21
+ print(("✓" if cond else "✗"), name)
22
+
23
+
24
+ # ── 1. Import + /health ──────────────────────────────────────────────────────
25
+ from app import create_app # noqa: E402
26
+
27
+ flask_app = create_app()
28
+ client = flask_app.test_client()
29
+ health = client.get("/health").get_json()
30
+ check("create_app + /health", health and health["status"] == "ok")
31
+ models = client.get("/api/models").get_json()
32
+ check("/api/models expose le roster", len(models["models"]) >= 5)
33
+
34
+ # ── 2. Filets déterministes ──────────────────────────────────────────────────
35
+ from app.pipeline import postprocess as pp # noqa: E402
36
+
37
+ t, _ = pp.fix_dollar_digit("Prix : $3$ et ${{nAff}}$.")
38
+ check("fix_dollar_digit", "${}3$" in t and "${}{{nAff}}$" in t)
39
+ check("detect_languages both", pp.detect_languages("{fr}`Calculer`{en}`Compute` $x$") == "both")
40
+ check("strip_language fr", pp.strip_language("{fr}`Bonjour `{en}`Hello `", "fr").strip() == "Bonjour")
41
+
42
+ # ── 3. Pipeline complet avec LLM mocké ───────────────────────────────────────
43
+ SMOKE_SOURCE = """`````{exercise}
44
+ :title: Somme de deux entiers
45
+ :level: Elementary
46
+
47
+ On additionne deux entiers.
48
+
49
+ :::::{question}
50
+ :questionType: STQ
51
+ :questionId: 0
52
+ :questionIndex: 0
53
+
54
+ ::::{questionStatement}
55
+ Calculer $3 + 4$.
56
+ ::::
57
+
58
+ ::::{questionHint}
59
+ Poser l'addition.
60
+ ::::
61
+
62
+ ::::{detailedSolution}
63
+ On trouve $7$.
64
+ ::::
65
+
66
+ ::::{weightDistribution}
67
+ :logic: 25
68
+ :abstraction: 25
69
+ :reasoning: 25
70
+ :calculation: 25
71
+ ::::
72
+ :::::
73
+ `````"""
74
+
75
+ MOCK_ANALYSIS = json.dumps({
76
+ "exercise_type": "équation linéaire",
77
+ "exercise_title": "Somme de deux entiers",
78
+ "nb_questions": 1,
79
+ "variables": [{"nom": "a", "type_python": "int", "description": "1er terme",
80
+ "contraintes": "2..9", "plage_python": "rd.randint(2, 9)",
81
+ "location": "énoncé", "valeur_exemple": "3"}],
82
+ "needs_fraction": False, "needs_sympy": False, "needs_numpy": False,
83
+ "needs_matplolib": False, # typo v1 volontaire : doit être normalisée
84
+ "target_rules": [], "property_constraints": [],
85
+ "has_validated_solution_in_input": False,
86
+ })
87
+
88
+ MOCK_PAIR = """````{python}
89
+ import random as rd
90
+ a = rd.randint(2, 9)
91
+ b = rd.randint(2, 9)
92
+ sumAff = str(a + b)
93
+ globals()
94
+ ````
95
+
96
+ On additionne deux entiers.
97
+
98
+ :::::{question}
99
+ :questionType: STQ
100
+ :questionId: 0
101
+ :questionIndex: 0
102
+
103
+ ::::{questionStatement}
104
+ Calculer ${}{{a}} + {{b}}$.
105
+ ::::
106
+
107
+ ::::{questionHint}
108
+ Poser l'addition.
109
+ ::::
110
+
111
+ ::::{detailedSolution}
112
+ On trouve ${}{{a}} + {{b}} = {{sumAff}}$.
113
+ ::::
114
+
115
+ ::::{weightDistribution}
116
+ :logic: 25
117
+ :abstraction: 25
118
+ :reasoning: 25
119
+ :calculation: 25
120
+ ::::
121
+ :::::"""
122
+
123
+
124
+ MOCK_MCQ_PAIR = """````{python}
125
+ import random as rd
126
+ a = rd.randint(2, 9)
127
+ b = rd.randint(2, 9)
128
+ sumAff = str(a + b)
129
+ d1Aff = str(a + b + 1) # erreur type : +1
130
+ d2Aff = str(a + b - 1) # erreur type : -1
131
+ d3Aff = str(a * b + 100) # produit décalé — toujours > somme+1 (distinct)
132
+ globals()
133
+ ````
134
+
135
+ On additionne deux entiers.
136
+
137
+ :::::{question}
138
+ :questionType: MCQ
139
+ :questionId: 0
140
+ :questionIndex: 0
141
+
142
+ ::::{questionStatement}
143
+ Combien vaut ${}{{a}} + {{b}}$ ?
144
+ ::::
145
+
146
+ ::::{questionHint}
147
+ Poser l'addition.
148
+ ::::
149
+
150
+ ::::{mcqAnswer}
151
+ :isRightAnswer: true
152
+ ${}{{sumAff}}$
153
+ ::::
154
+
155
+ ::::{mcqAnswer}
156
+ :isRightAnswer: false
157
+ ${}{{d1Aff}}$
158
+ ::::
159
+
160
+ ::::{mcqAnswer}
161
+ :isRightAnswer: false
162
+ ${}{{d2Aff}}$
163
+ ::::
164
+
165
+ ::::{mcqAnswer}
166
+ :isRightAnswer: false
167
+ ${}{{d3Aff}}$
168
+ ::::
169
+
170
+ ::::{mcqAnswer}
171
+ :isRightAnswer: false
172
+ {fr}`Aucune de ces réponses n'est correcte`{en}`None of these answers are correct`
173
+ ::::
174
+
175
+ ::::{detailedSolution}
176
+ On trouve ${}{{a}} + {{b}} = {{sumAff}}$.
177
+ ::::
178
+
179
+ ::::{weightDistribution}
180
+ :logic: 25
181
+ :abstraction: 25
182
+ :reasoning: 25
183
+ :calculation: 25
184
+ ::::
185
+ :::::"""
186
+
187
+ MOCK_FGQ_PAIR = """````{python}
188
+ import random as rd
189
+ a = rd.randint(2, 9)
190
+ b = rd.randint(2, 9)
191
+ sumAff = str(a + b)
192
+ globals()
193
+ ````
194
+
195
+ On additionne deux entiers.
196
+
197
+ :::::{question}
198
+ :questionType: FGQ
199
+ :questionId: 0
200
+ :questionIndex: 0
201
+ :solution: [["ord","${{sumAff}}$"],["0"]]
202
+
203
+ ::::{questionStatement}
204
+ Calculer ${}{{a}} + {{b}}$.
205
+
206
+ $s =$ {input}`||110`
207
+ ::::
208
+
209
+ ::::{questionHint}
210
+ Poser l'addition.
211
+ ::::
212
+
213
+ ::::{displayedSolution}
214
+ $s = {{sumAff}}$
215
+ ::::
216
+
217
+ ::::{detailedSolution}
218
+ On trouve ${}{{a}} + {{b}} = {{sumAff}}$.
219
+ ::::
220
+
221
+ ::::{weightDistribution}
222
+ :logic: 15
223
+ :abstraction: 20
224
+ :reasoning: 20
225
+ :calculation: 45
226
+ ::::
227
+ :::::"""
228
+
229
+ ANALYSIS_CALLS = {"n": 0}
230
+
231
+
232
+ def mock_llm(prompt, model_idx=0, temperature=0.0, max_tokens=4096,
233
+ image_b64=None, system_prompt="", reasoning=False):
234
+ if "expert en analyse d'exercices" in prompt:
235
+ ANALYSIS_CALLS["n"] += 1
236
+ return MOCK_ANALYSIS
237
+ if "auditeur PyxiScience" in prompt:
238
+ return json.dumps({"verdict": "OK", "issues": []})
239
+ if "Tu déclines un exercice" in prompt:
240
+ return MOCK_MCQ_PAIR if "QCM (MCQ)" in prompt else MOCK_FGQ_PAIR
241
+ if "RÈGLES D'ASSEMBLAGE PAR PAIRE" in prompt:
242
+ return MOCK_PAIR
243
+ return "{}"
244
+
245
+
246
+ import app.pipeline.analyze as analyze # noqa: E402
247
+ import app.pipeline.audit as audit # noqa: E402
248
+ import app.pipeline.generate as generate # noqa: E402
249
+ import app.pipeline.orchestrator as orchestrator # noqa: E402
250
+
251
+ analyze.process_with_openrouter = mock_llm
252
+ audit.process_with_openrouter = mock_llm
253
+ generate.process_with_openrouter = mock_llm
254
+ orchestrator.process_with_openrouter = mock_llm
255
+ analyze.enrich_exercise_with_notions = lambda *a, **k: ("(notions mockées)", "NOTION_TEST")
256
+ analyze.retrieve_functions_context = lambda **k: {"catalogue": "(catalogue mocké)"}
257
+
258
+ result = orchestrator.run_exercise(
259
+ content=SMOKE_SOURCE, filename="smoke.md", level="", model_idx=0, lang="fr")
260
+
261
+ check("pipeline : exercice produit", bool(result["exercise"].strip()))
262
+ check("pipeline : fence 4 backticks", "````{python}" in result["exercise"])
263
+ check("pipeline : se termine par `````", result["exercise"].rstrip().endswith("`````"))
264
+ check("pipeline : globals() présent", "globals()" in result["exercise"])
265
+ check("pipeline : typo needs_matplolib normalisée",
266
+ "needs_matplolib" not in result["analysis"] and result["analysis"]["needs_matplotlib"] is False)
267
+ check("pipeline : harnais VERT", result["harness"]["ok"])
268
+ check("pipeline : coût exposé", "usd" in result["cost"])
269
+ check("pipeline : langue exposée", result["lang"]["target"] == "fr")
270
+
271
+ # En-tête {exercise} complet et bien formé
272
+ ex = result["exercise"]
273
+ check("header : enveloppe `````{exercise} en tête", ex.lstrip().startswith("`````{exercise}"))
274
+ for field in (":id:", ":title:", ":modules:", ":recommendedExecutionTime:",
275
+ ":level:", ":chap:", ":involvedConcepts:", ":originalSource:", ":visibility:"):
276
+ check(f"header : champ {field} présent", field in ex.split("````{python}")[0])
277
+ check("header : level mappé (level='' → Elementary)", ":level: Elementary" in ex)
278
+ check("header : visibility All", ":visibility: All" in ex)
279
+ check("header : concepts = notions", "NOTION_TEST" in ex)
280
+ check("header : une seule enveloppe {exercise}", ex.count("`````{exercise}") == 1)
281
+
282
+ # ── 3bis. Mode déclinaisons (QCM + QAT, LLM mocké, analyse partagée) ─────────
283
+ ANALYSIS_CALLS["n"] = 0
284
+ decl_results = orchestrator.run_declinaisons(
285
+ content=SMOKE_SOURCE, filename="smoke.md", level="", model_idx=0,
286
+ lang="fr", types=["qcm", "qat"])
287
+ check("déclinaisons : 2 sorties (QCM + QAT)", len(decl_results) == 2)
288
+ check("déclinaisons : analyse partagée (1 seul appel)", ANALYSIS_CALLS["n"] == 1)
289
+
290
+ qcm = dict(decl_results)["qcm"]
291
+ qat = dict(decl_results)["qat"]
292
+ check("QCM : harnais VERT", qcm["harness"]["ok"])
293
+ check("QCM : 5 options, 1 seule bonne", qcm["exercise"].count("{mcqAnswer}") == 5
294
+ and qcm["exercise"].count(":isRightAnswer: true") == 1)
295
+ check("QCM : « None » en dernière option",
296
+ "Aucune de ces réponses" in qcm["exercise"].split(":isRightAnswer: false")[-1])
297
+ check("QCM : titre suffixé - MCQ", " - MCQ" in qcm["exercise"].split("````{python}")[0])
298
+ check("QAT : harnais VERT", qat["harness"]["ok"])
299
+ check("QAT : :solution: + {input} présents",
300
+ ':solution: [["ord"' in qat["exercise"] and "{input}`" in qat["exercise"])
301
+ check("QAT : displayedSolution présent", "{displayedSolution}" in qat["exercise"])
302
+ check("déclinaisons : decl_type exposé", qcm["decl_type"] == "qcm" and qat["decl_type"] == "qat")
303
+
304
+ # Harnais étendu : un MCQ avec collision d'options doit être ROUGE.
305
+ from app.validation import harness as _harness # noqa: E402
306
+
307
+ _collision = qcm["exercise"].replace("{{d1Aff}}", "{{sumAff}}") # distracteur == bonne réponse
308
+ _rep = _harness.validate_text(_collision, seeds=10)
309
+ check("harnais étendu : collision d'options MCQ détectée (ROUGE)",
310
+ not _rep["ok"] and _rep["n_mcq_collisions"] > 0)
311
+ _two_true = qcm["exercise"].replace(":isRightAnswer: false", ":isRightAnswer: true", 1)
312
+ _rep2 = _harness.validate_text(_two_true, seeds=5)
313
+ check("harnais étendu : 2 bonnes réponses détectées (statique)",
314
+ not _rep2["ok"] and any("isRightAnswer" in e for e in _rep2["static_errors"]))
315
+ _bad_arity = qat["exercise"].replace('[["ord","${{sumAff}}$"],["0"]]',
316
+ '[["ord","${{sumAff}}$","$2$"],["0","0"]]')
317
+ _rep3 = _harness.validate_text(_bad_arity, seeds=5)
318
+ check("harnais étendu : arité FGQ incohérente détectée",
319
+ not _rep3["ok"] and any("arité" in e for e in _rep3["static_errors"]))
320
+
321
+ # Validation API du mode (sans lancer de job).
322
+ r_bad_mode = client.post("/api/jobs", json={"content": "x", "mode": "zzz"})
323
+ check("API : mode invalide → 400", r_bad_mode.status_code == 400)
324
+ r_no_types = client.post("/api/jobs", json={"content": "x", "mode": "declinaisons", "types": {}})
325
+ check("API : declinaisons sans type → 400", r_no_types.status_code == 400)
326
+
327
+ # ── 4. Téléchargement ZIP (endpoint, sans LLM) ───────────────────────────────
328
+ import io as _io # noqa: E402
329
+ import zipfile as _zipfile # noqa: E402
330
+
331
+ import app.server as _server # noqa: E402
332
+
333
+ _fake = {
334
+ "status": "done", "step_label": "Terminé", "current_file": "b.md",
335
+ "files_total": 2, "files_done": 2, "error": None, "summary": {},
336
+ "results": [
337
+ {"filename": "a.md", "status": "done",
338
+ "result": {"exercise": result["exercise"], "warnings": [],
339
+ "harness": {"ok": True, "seeds": 100}, "cost": {"usd": 0.01}}},
340
+ {"filename": "a.md", "status": "done", # collision de nom volontaire
341
+ "result": {"exercise": "````{python}\nglobals()\n````\n`````", "warnings": [{}],
342
+ "harness": {"ok": False, "seeds": 100}, "cost": {"usd": 0.02}}},
343
+ {"filename": "c.md", "status": "error", "error": "boom"},
344
+ ],
345
+ }
346
+ with _server._JOBS_LOCK:
347
+ _server._JOBS["smoketest"] = _fake
348
+ resp = client.get("/api/jobs/smoketest/download")
349
+ check("ZIP : 200 + mimetype zip", resp.status_code == 200 and "zip" in resp.mimetype)
350
+ zf = _zipfile.ZipFile(_io.BytesIO(resp.data))
351
+ names = zf.namelist()
352
+ check("ZIP : 2 .md (collision dédupliquée) + récap",
353
+ "a_pythonise.md" in names and "a_pythonise_2.md" in names
354
+ and "_recapitulatif.md" in names)
355
+ check("ZIP : 404 si job inconnu", client.get("/api/jobs/zzz/download").status_code == 404)
356
+
357
+ # ── Bilan ────────────────────────────────────────────────────────────────────
358
+ failed = [n for n, ok in PASS if not ok]
359
+ print(f"\n{len(PASS) - len(failed)}/{len(PASS)} smoke tests verts")
360
+ if failed:
361
+ print("ÉCHECS :", failed)
362
+ sys.exit(1)
363
+ print("✅ SMOKE OK")