local CALC_SYMPY = {} local M = require("texecole-math") -- Attente sans fork : le worker est interroge par sondage. « os.execute("sleep") -- » forkait un shell à chaque tour ; socket.sleep (fourni par LuaTeX) attend -- sans creer de processus. Repli sur l'ancien procede si socket est absent. local _sleep do local ok, socket = pcall(require, "socket") if ok and type(socket) == "table" and socket.sleep then _sleep = function(s) socket.sleep(s) end else _sleep = function(s) os.execute("sleep " .. s) end end end local memcache = {} local CACHE_DIR = "_texecole-cache" local cache_dir_ready = nil local PRE = [==[ from sympy import * from sympy.parsing.sympy_parser import (parse_expr, standard_transformations, implicit_multiplication_application, convert_xor) _T = standard_transformations + (implicit_multiplication_application, convert_xor) def lire(s, v): return parse_expr(s, transformations=_T, local_dict={str(v): v}) ]==] CALC_SYMPY.PRE = PRE local function to_sympy(expr) return (expr:gsub("%^", "**")) end local function to_moteur(expr) return (expr:gsub("%*%*", "^"):gsub("%f[%w]oo%f[%W]", "inf")) end CALC_SYMPY.to_sympy, CALC_SYMPY.to_moteur = to_sympy, to_moteur local function hash(s) local h1, h2 = 5381, 52711 for i = 1, #s do local b = s:byte(i) h1 = (h1 * 33 + b) % 4294967296 h2 = (h2 * 31 + b) % 4294967296 end return string.format("%08x%08x-%d", h1, h2, #s) end local function cache_path(script) if cache_dir_ready == nil then local ok, lfs = pcall(require, "lfs") if ok and lfs then local attr = lfs.attributes(CACHE_DIR) if attr and attr.mode == "directory" then cache_dir_ready = true else cache_dir_ready = lfs.mkdir(CACHE_DIR) and true or false end else cache_dir_ready = false end end if not cache_dir_ready then return nil end return CACHE_DIR .. "/" .. hash(script) .. ".txt" end local function cache_read(script) if memcache[script] then return memcache[script] end local p = cache_path(script) if not p then return nil end local f = io.open(p, "r") if not f then return nil end local out = f:read("*a") f:close() memcache[script] = out return out end local function cache_write(script, out) memcache[script] = out local p = cache_path(script) if not p then return end local f = io.open(p, "w") if f then f:write(out); f:close() end end local SERVEUR = [==[ import sys, os, time, io, contextlib, traceback, shutil D = sys.argv[1] dernier = time.time() bat = 0.0 while True: t = time.time() if t - bat >= 0.5: with open(os.path.join(D, "vie.tmp"), "w") as f: f.write(str(t)) os.replace(os.path.join(D, "vie.tmp"), os.path.join(D, "vie")) bat = t reqs = sorted(int(f[:-4]) for f in os.listdir(D) if f.endswith(".req")) if not reqs: if t - dernier > 120: break time.sleep(0.02) continue for k in reqs: base = os.path.join(D, str(k)) try: os.remove(base + ".req") except OSError: continue buf = io.StringIO() try: src = open(base + ".py").read() with contextlib.redirect_stdout(buf): exec(src, {}) out = buf.getvalue() except SystemExit: out = buf.getvalue() except Exception: out = buf.getvalue() + "\nTraceback\n" + traceback.format_exc() with open(base + ".tmp", "w") as f: f.write(out) os.replace(base + ".tmp", base + ".out") with open(base + ".done", "w") as f: f.write("1") dernier = time.time() shutil.rmtree(D, ignore_errors=True) ]==] local WORKER local POINTEUR = CACHE_DIR .. "/worker-actif" local LFS do local ok, lfs = pcall(require, "lfs") if ok then LFS = lfs end end return function(sl) if not sl then return CALC_SYMPY end -- Le serveur écrit son battement de cœur dans « vie » toutes les 0,5 s. -- Un fichier plus vieux que 3 s signale un serveur mort ou mourant. local function worker_vivant(dir) local attr = LFS.attributes(dir .. "/vie") return attr and (os.time() - attr.modification) <= 3 end local function demarrer_worker(python) if WORKER ~= nil then return WORKER end WORKER = false if not cache_path("sonde") then return false end if not LFS then return false end -- Réutiliser le serveur d'une compilation précédente s'il vit encore : -- SymPy y est déjà importé, le premier calcul part sans délai. Le -- fichier pointeur, écrit au lancement, mène à son dossier. local pf = io.open(POINTEUR, "r") if pf then local ancien = pf:read("*l") pf:close() if ancien and LFS.attributes(ancien) and worker_vivant(ancien) then WORKER = { dir = ancien, n = 0, reprise = true } return WORKER end end local dir = CACHE_DIR .. "/worker-" .. tostring(os.time() % 100000) .. "-" .. tostring(math.floor((os.clock() * 1e6) % 100000)) if not LFS.mkdir(dir) then return false end local f = io.open(dir .. "/serveur.py", "w") if not f then return false end f:write(SERVEUR) f:close() local lance = os.execute(python .. " " .. dir .. "/serveur.py " .. dir .. " > " .. dir .. "/serveur.log 2>&1 &") if not lance then return false end local pf2 = io.open(POINTEUR, "w") if pf2 then pf2:write(dir); pf2:close() end WORKER = { dir = dir, n = 0 } return WORKER end -- Soumettre sans attendre : le script est déposé auprès du serveur, -- qui le traitera dans l'ordre. Renvoie le chemin de base de la -- requête, à passer plus tard à worker_attendre. Le numéro de requête -- est réservé par « mkdir » (atomique) : deux compilations peuvent -- ainsi partager le même serveur sans se marcher dessus. local function worker_soumettre(python, script) local w = demarrer_worker(python) if not w then return nil end local base for _ = 1, 100000 do w.n = w.n + 1 if LFS.mkdir(w.dir .. "/" .. w.n .. ".pris") then base = w.dir .. "/" .. w.n break end end if not base then WORKER = false; return nil end local f = io.open(base .. ".py", "w") if not f then WORKER = false; return nil end f:write(script) f:close() local g = io.open(base .. ".req", "w") if not g then WORKER = false; return nil end g:write("1") g:close() return base, w.n end local function worker_attendre(base, premiere) local attente = 0 local pas = 0.02 local limite = premiere and 30 or 90 while attente < limite do local d = io.open(base .. ".done", "r") if d then d:close() local h = io.open(base .. ".out", "r") if not h then break end local out = h:read("*a") h:close() os.remove(base .. ".py"); os.remove(base .. ".out") os.remove(base .. ".done") if LFS then pcall(LFS.rmdir, base .. ".pris") end return out end _sleep(pas) attente = attente + pas if pas < 0.1 then pas = pas + 0.02 end end -- Serveur muet. S'il s'agissait d'un serveur repris d'une compilation -- précédente (mort entre la sonde et la requête), oublier le pointeur -- et laisser « nil » : le prochain appel lancera un serveur neuf. Si -- c'est un serveur que cette compilation vient de lancer, « false » -- bascule définitivement sur le procédé fichier-par-fichier, comme -- avant — jamais plus d'une relance. local reessayer = WORKER and WORKER.reprise os.remove(POINTEUR) WORKER = reessayer and nil or false return nil end local function worker_run(python, script) local base, n = worker_soumettre(python, script) if not base then return nil end return worker_attendre(base, n == 1) end -- Requêtes soumises par le balayage précoce (texecole-prefetch) et pas -- encore collectées : script -> chemin de base auprès du serveur. local PENDING = {} local PREFETCH = false -- L'option de classe « python= » n'existe plus : la commande Python se -- trouve toute seule, une fois par compilation, en essayant dans l'ordre -- python3 (Linux, macOS), python puis py (Windows). Le résultat est mis -- en cache pour toute la durée de la compilation. local PYTHON_TROUVE local function python_cmd() if PYTHON_TROUVE then return PYTHON_TROUVE end for _, cand in ipairs { "python3", "python", "py" } do local h = io.popen(cand .. " --version 2>&1") if h then local sortie = h:read("*a") or "" h:close() if sortie:find("Python%s+3") then PYTHON_TROUVE = cand return cand end end end PYTHON_TROUVE = "python3" return PYTHON_TROUVE end CALC_SYMPY.python_cmd = python_cmd local function run(script) local cached = cache_read(script) if cached then return cached end local python = python_cmd() -- Mode préchargement (balayage précoce) : soumettre sans attendre. -- Le serveur Python calcule pendant que TeX charge son préambule ; -- le vrai appel, à la transpilation, collectera le résultat. if PREFETCH then if not PENDING[script] then PENDING[script] = worker_soumettre(python, script) end return nil, "texecole : préchargement en cours" end local via local attendu = PENDING[script] if attendu then PENDING[script] = nil via = worker_attendre(attendu, false) end if via == nil then via = worker_run(python, script) end if via ~= nil then local out = via:gsub("%s+$", "") if out:find("Traceback") or out:find("No module named") then if out:find("No module named") then return nil, "texecole : SymPy n'est pas installé pour « " .. python .. " ». Installez-le : " .. python .. " -m pip install sympy — " .. "sans SymPy, seuls les calculs automatiques manquent, le " .. "reste de texecole fonctionne intégralement." end return nil, "texecole : le calcul SymPy a échoué.\n" .. "Sortie de Python :\n" .. out end cache_write(script, out) return out end local base = os.tmpname() local inpath, outpath = base .. ".py", base .. ".out" local f = io.open(inpath, "w") if not f then return nil, "texecole : impossible d'écrire le fichier temporaire " .. "du calcul (droits du dossier ?)." end f:write(script) f:close() local cmd = python .. " " .. inpath .. " > " .. outpath .. " 2>&1" local ok = os.execute(cmd) local g = io.open(outpath, "r") local out = g and g:read("*a") or "" if g then g:close() end os.remove(inpath); os.remove(outpath); os.remove(base) out = out:gsub("%s+$", "") if (not ok) or out:find("Traceback") or out:find("command not found") or out:find("No module named") then if out:find("No module named") then return nil, "texecole : SymPy n'est pas installé pour « " .. python .. " ». Installez-le : " .. python .. " -m pip install sympy — " .. "sans SymPy, seuls les calculs automatiques manquent, le " .. "reste de texecole fonctionne intégralement." end return nil, "texecole : le calcul SymPy a échoué.\n" .. "Vérifiez : (1) python3 et sympy installés ; (2) la compilation " .. "avec --shell-escape (TeXstudio : Options > Configurer > " .. "Production, ajoutez --shell-escape à la commande LuaLaTeX).\n" .. "Sortie de Python :\n" .. out end cache_write(script, out) return out end CALC_SYMPY.run = run function CALC_SYMPY.prefetch_debut() PREFETCH = true end function CALC_SYMPY.prefetch_fin() PREFETCH = false end function CALC_SYMPY.eval(expr) return run(PRE .. "x, y, z, t, a, b, c, n = symbols('x y z t a b c n')\n" .. "print(" .. expr .. ")\n") end function CALC_SYMPY.variations(expr, var) var = var or "x" local script = (PRE .. [==[ %s = symbols('%s') f = lire("%s", %s) fp = simplify(diff(f, %s)) if fp == 0: print("TEXECOLE_ERREUR: fonction constante") raise SystemExit sing = singularities(f, %s, domain=S.Reals) if sing is S.EmptySet: poles = [] elif isinstance(sing, FiniteSet): poles = sorted([p for p in sing if p.is_real]) else: print("TEXECOLE_ERREUR: singularites non denombrables") raise SystemExit zs = solveset(fp, %s, domain=S.Reals) if zs is S.EmptySet: zeros = [] elif isinstance(zs, FiniteSet): zeros = sorted([z for z in zs if z.is_real and z not in poles]) else: print("TEXECOLE_ERREUR: zeros de la derivee non denombrables") raise SystemExit pts = sorted(set(zeros) | set(poles)) bounds = [S.NegativeInfinity] + pts + [S.Infinity] signs = [] for i in range(len(bounds) - 1): a_, b_ = bounds[i], bounds[i + 1] if a_ is S.NegativeInfinity and b_ is S.Infinity: m = 0 elif a_ is S.NegativeInfinity: m = b_ - 1 elif b_ is S.Infinity: m = a_ + 1 else: m = (a_ + b_) / 2 v = fp.subs(%s, m) if not v.is_comparable: v = v.evalf() signs.append("+" if v > 0 else "-") def fmt(v): try: v = nsimplify(v) except Exception: pass return str(v) row = [fmt(limit(f, %s, S.NegativeInfinity))] dcells = [] for i, p in enumerate(pts): row.append("/" if signs[i] == "+" else "\\") dcells.append(signs[i]) if p in poles: row.append(fmt(limit(f, %s, p, dir='-'))) row.append("||") row.append(fmt(limit(f, %s, p, dir='+'))) dcells.append("||") else: row.append(fmt(f.subs(%s, p))) row.append("/" if signs[-1] == "+" else "\\") row.append(fmt(limit(f, %s, S.Infinity))) dcells.append(signs[-1]) pts_txt = ["-inf"] + [str(p) for p in pts] + ["+inf"] print("X|" + " | ".join(pts_txt)) print("D|" + " | ".join(dcells)) print("V|" + " ".join(row)) ]==]):format(var, var, expr, var, var, var, var, var, var, var, var, var, var) local out, err = run(script) if not out then return nil, err end if out:find("TEXECOLE_ERREUR") then if out:find("fonction constante") then return nil, "texecole : la fonction « " .. expr .. " » est constante ; " .. "il n'y a pas de variations à dresser." end return nil, "texecole : le calcul automatique n'a pas trouvé un " .. "nombre fini de points remarquables (zéros de la dérivée, " .. "valeurs interdites) pour « " .. expr .. " » — donnez les " .. "rangées à la main (x:, dérivée:, variations:)." end local rows = {} for line in (out .. "\n"):gmatch("(.-)\n") do local k, v = line:match("^(%u)|(.*)$") if k then rows[k] = to_moteur(v) end end if not (rows.X and rows.D and rows.V) then return nil, "texecole : réponse SymPy inattendue :\n" .. out end return { x = rows.X, deriv = rows.D, var = rows.V } end -- Tableau de signes automatique d'une expression quelconque : zéros -- réels du numérateur, valeurs interdites (zéros du dénominateur), -- signe de f sur chaque intervalle. Sert de repli quand l'expression -- n'est pas un produit de facteurs affines. function CALC_SYMPY.signes(expr, var) var = var or "x" local script = (PRE .. [==[ %s = symbols('%s') f = lire("%s", %s) num, den = fraction(together(f)) zs = solveset(num, %s, domain=S.Reals) ps = solveset(den, %s, domain=S.Reals) def denombre(ens, quoi): if ens is S.EmptySet: return [] if isinstance(ens, FiniteSet): return sorted([p for p in ens if p.is_real]) print("TEXECOLE_ERREUR: " + quoi + " non denombrables") raise SystemExit poles = denombre(ps, "valeurs interdites") zeros = [z for z in denombre(zs, "zeros") if z not in poles] pts = sorted(set(zeros) | set(poles)) bounds = [S.NegativeInfinity] + pts + [S.Infinity] cells = [] for i in range(len(bounds) - 1): a_, b_ = bounds[i], bounds[i + 1] if a_ is S.NegativeInfinity and b_ is S.Infinity: m = 0 elif a_ is S.NegativeInfinity: m = b_ - 1 elif b_ is S.Infinity: m = a_ + 1 else: m = (a_ + b_) / 2 v = f.subs(%s, m) if not v.is_comparable: v = v.evalf() cells.append("+" if v > 0 else "-") if i < len(pts): cells.append("||" if pts[i] in poles else "0") pts_txt = ["-inf"] + [str(p) for p in pts] + ["+inf"] print("X|" + " | ".join(pts_txt)) print("S|" + " | ".join(cells)) ]==]):format(var, var, expr, var, var, var, var) local out, err = run(script) if not out then return nil, err end if out:find("TEXECOLE_ERREUR") then return nil, "texecole : le calcul automatique n'a pas trouvé un " .. "nombre fini de zéros et de valeurs interdites pour « " .. expr .. " » — écrivez le tableau à la main avec la forme en bloc " .. "{ rangées }." end local rows = {} for line in (out .. "\n"):gmatch("(.-)\n") do local k, v = line:match("^(%u)|(.*)$") if k then rows[k] = to_moteur(v) end end if not (rows.X and rows.S) then return nil, "texecole : réponse SymPy inattendue :\n" .. out end return { x = rows.X, cells = rows.S } end function CALC_SYMPY.zeros(expr, var) var = var or "x" local out, err = run((PRE .. "%s = symbols('%s')\n" .. "zs = solveset(lire(\"%s\", %s), %s, domain=S.Reals)\n" .. "print(latex(zs))\n"):format(var, var, expr, var, var)) if not out then return nil, err end return out end -- Racines n-ièmes complexes : l'ensemble des z tels que z^n = expr, -- construit explicitement depuis la branche principale — les n valeurs -- w^(1/n)·exp(2ikπ/n), k = 0..n-1, déjà rangées par argument croissant. function CALC_SYMPY.nroots(n, expr) local out, err = run((PRE .. "z = symbols('z')\n" .. "w = lire(\"%s\", z)\n" .. "r0 = Pow(w, Rational(1, %d))\n" .. "rs = FiniteSet(*[simplify(r0 * exp(2*I*pi*k/%d)) for k in range(%d)])\n" .. "print(latex(rs))\n"):format(expr, n, n, n)) if not out then return nil, err end return out end function CALC_SYMPY.primitive(expr, var) var = var or "x" local out, err = run((PRE .. "%s = symbols('%s')\n" .. "print(latex(integrate(lire(\"%s\", %s), %s)))\n") :format(var, var, expr, var, var)) if not out then return nil, err end return out end function CALC_SYMPY.expand(expr) local out, err = run((PRE .. "x, y, z, t, a, b, c = symbols('x y z t a b c')\n" .. "print(latex(expand(lire(\"%s\", x))))\n"):format(expr)) if not out then return nil, err end return out end function CALC_SYMPY.apart(expr, var) var = var or "x" local out, err = run((PRE .. "%s = symbols('%s')\n" .. "print(latex(apart(lire(\"%s\", %s), %s)))\n") :format(var, var, expr, var, var)) if not out then return nil, err end return out end function CALC_SYMPY.pde(eq, f) f = f or "u" local lhs, rhs = eq:match("^(.-)=(.+)$") if not lhs then return nil, "texecole : l'équation aux dérivées partielles demande un signe =." end local script = PRE .. ([==[ x, y = symbols('x y') %s = Function('%s') _d = {'x': x, 'y': y, '%s': %s, 'Derivative': Derivative} l = parse_expr("%s", transformations=_T, local_dict=_d) r = parse_expr("%s", transformations=_T, local_dict=_d) try: print(latex(pdsolve(Eq(l, r), %s(x, y)))) except NotImplementedError: print("TEXECOLE_EDP_NON_RESOLUBLE") ]==]):format(f, f, f, f, lhs:gsub("%s+$", ""), rhs:gsub("^%s+", ""), f) local out, err = run(script) if not out then return nil, err end if out:find("TEXECOLE_EDP_NON_RESOLUBLE") then return nil, "texecole : cette équation aux dérivées partielles dépasse " .. "le résoluble symboliquement (SymPy traite le premier ordre " .. "linéaire) — les EDP d'ordre supérieur relèvent du numérique." end return out end function CALC_SYMPY.ode(eq, f, var) f = f or "y" var = var or "x" local lhs, rhs = eq:match("^(.-)=(.+)$") if not lhs then return nil, "texecole : l'équation différentielle demande un signe =." end local script = PRE .. ([==[ %s = symbols('%s') %s = Function('%s') _d = {'%s': %s, '%s': %s, 'Derivative': Derivative} l = parse_expr("%s", transformations=_T, local_dict=_d) r = parse_expr("%s", transformations=_T, local_dict=_d) print(latex(dsolve(Eq(l, r), %s(%s)))) ]==]):format(var, var, f, f, var, var, f, f, lhs:gsub("%s+$", ""), rhs:gsub("^%s+", ""), f, var) local out, err = run(script) if not out then return nil, err end return out end function CALC_SYMPY.eigen(rows_py) local script = PRE .. [==[ x, y, z, t, a, b, c, n = symbols('x y z t a b c n') M = Matrix(]==] .. rows_py .. [==[) if not M.is_square: print("TEXECOLE_NON_CARREE") raise SystemExit ev = M.eigenvals() parts = [] for v in sorted(ev.keys(), key=default_sort_key): s = latex(nsimplify(v)) if ev[v] > 1: s += r"\;(\times %d)" % ev[v] parts.append(s) print(r"\left\{" + r",\ ".join(parts) + r"\right\}") ]==] local out, err = run(script) if not out then return nil, err end if out:find("TEXECOLE_NON_CARREE") then return nil, "texecole : les valeurs propres demandent une matrice carrée." end return out end function CALC_SYMPY.diagonalize(rows_py) local script = PRE .. [==[ x, y, z, t, a, b, c, n = symbols('x y z t a b c n') M = Matrix(]==] .. rows_py .. [==[) if not M.is_square: print("TEXECOLE_NON_CARREE") raise SystemExit if not M.is_diagonalizable(): print("TEXECOLE_NON_DIAGONALISABLE") raise SystemExit P_, D_ = M.diagonalize() print("P|" + latex(P_)) print("D|" + latex(D_)) ]==] local out, err = run(script) if not out then return nil, err end if out:find("TEXECOLE_NON_CARREE") then return nil, "texecole : la diagonalisation demande une matrice carrée." end if out:find("TEXECOLE_NON_DIAGONALISABLE") then return nil, "texecole : la matrice n'est pas diagonalisable (sur CC)." end local P_ = out:match("P|([^\n]*)") local D_ = out:match("D|([^\n]*)") if not (P_ and D_) then return nil, "texecole : réponse SymPy inattendue :\n" .. out end return P_, D_ end function CALC_SYMPY.closed_sum(kind, expr, var, a, b) local op = (kind == "prod") and "product" or "summation" local head = (kind == "prod") and "Product" or "Sum" local script = PRE .. ([==[ %s = symbols('%s', integer=True) x, y, z, t, n, p, q = symbols('x y z t n p q') e = lire("%s", %s) res = simplify(factor(%s(e, (%s, %s, %s)))) print("S|" + latex(%s(e, (%s, %s, %s)))) print("R|" + latex(res)) ]==]):format(var, var, expr, var, op, var, a, b, head, var, a, b) local out, err = run(script) if not out then return nil, err end local S = out:match("S|([^\n]*)") local R = out:match("R|([^\n]*)") if not (S and R) then return nil, "texecole : réponse SymPy inattendue :\n" .. out end return S, R end function CALC_SYMPY.canonical(expr, var) var = var or "x" local script = PRE .. ([==[ %s = symbols('%s') e = lire("%s", %s) p = Poly(e, %s) if p.degree() != 2: print("TEXECOLE_PAS_DEGRE_2") raise SystemExit a_, b_, c_ = p.all_coeffs() h = nsimplify(-b_ / (2*a_)) k_ = nsimplify(c_ - b_**2 / (4*a_)) print(latex(a_ * (%s - h)**2 + k_)) ]==]):format(var, var, expr, var, var, var) local out, err = run(script) if not out then return nil, err end if out:find("TEXECOLE_PAS_DEGRE_2") then return nil, "texecole : la forme canonique est celle du trinôme du " .. "second degré — l'expression donnée n'est pas de degré 2." end return out end function CALC_SYMPY.nintegrate(expr, var, a, b) var = var or "x" local script = PRE .. ([==[ try: from scipy.integrate import quad except ImportError: print("TEXECOLE_SCIPY_ABSENT") raise SystemExit %s = symbols('%s') f = lambdify(%s, lire("%s", %s), "math") val, err = quad(f, %s, %s) print(val) ]==]):format(var, var, var, expr, var, a, b) local out, e = run(script) if not out then return nil, e end if out:find("TEXECOLE_SCIPY_ABSENT") then return nil, "texecole : SciPy n'est pas installé. Installez-le : " .. python_cmd() .. " -m pip install scipy — l'intégrale numérique en a besoin." end return out end function CALC_SYMPY.limite(expr, var, point, dir) var = var or "x" local pt = point:gsub("infini", "oo") local d = dir == "droite" and ", dir='+'" or dir == "gauche" and ", dir='-'" or "" local out, err = run((PRE .. "%s = symbols('%s')\n" .. "print(latex(limit(lire(\"%s\", %s), %s, %s%s)))\n") :format(var, var, expr, var, var, pt, d)) if not out then return nil, err end return out end function CALC_SYMPY.dl(expr, var, a, n) var = var or "x" local out, err = run((PRE .. "%s = symbols('%s')\n" .. "print(latex(series(lire(\"%s\", %s), %s, %s, %d).removeO()))\n") :format(var, var, expr, var, var, a, n + 1)) if not out then return nil, err end local h = (a == "0") and var or ("(" .. var .. " - " .. a .. ")") return out .. " + o\\left(" .. h .. "^{" .. n .. "}\\right)" end function CALC_SYMPY.factor(expr) return run(PRE .. "x, y, z, t, a, b, c, n = symbols('x y z t a b c n')\n" .. "print(latex(factor(lire(\"" .. expr .. "\", x))))\n") end function CALC_SYMPY.simplify_expr(expr) return run(PRE .. "x, y, z, t, a, b, c, n = symbols('x y z t a b c n')\n" .. "print(latex(simplify(lire(\"" .. expr .. "\", x))))\n") end function CALC_SYMPY.solve_eq(lhs, rhs, var, domain) var = var or "x" domain = domain or "Reals" return run((PRE .. "%s = symbols('%s')\n" .. "print(latex(solveset(lire(\"%s\", %s) - (lire(\"%s\", %s)), %s, domain=S.%s)))\n") :format(var, var, lhs, var, rhs, var, var, domain)) end local function rows_to_py(rows) local pyrows = {} for _, r in ipairs(rows) do local cells = {} for c in (r .. ";"):gmatch("(.-);") do c = c:gsub("^%s+", ""):gsub("%s+$", "") if c ~= "" then cells[#cells + 1] = "lire(\"" .. c .. "\", x)" end end pyrows[#pyrows + 1] = "[" .. table.concat(cells, ", ") .. "]" end return "[" .. table.concat(pyrows, ", ") .. "]" end CALC_SYMPY.rows_to_py = rows_to_py function CALC_SYMPY.matrix_op(rows, op) local body if op == "det" then body = "print(latex(nsimplify(M.det())))" elseif op == "inv" then body = ("if M.det() == 0:\n print('TEXECOLE_NON_INVERSIBLE')\n" .. "else:\n print(latex(M.inv()))") else body = "print(latex(M))" end local out, err = run(PRE .. "x, y, z, t, a, b, c, n = symbols('x y z t a b c n')\n" .. "M = Matrix(" .. rows_to_py(rows) .. ")\n" .. body .. "\n") if not out then return nil, err end if out:find("TEXECOLE_NON_INVERSIBLE") then return nil, "texecole : la matrice n'est pas inversible (déterminant nul)." end return out end function CALC_SYMPY.derivative(expr, var, order) var = var or "x"; order = order or 1 local out, err = run((PRE .. "%s = symbols('%s')\n" .. "print(latex(simplify(diff(lire(\"%s\", %s), %s, %d))))\n") :format(var, var, expr, var, var, order)) if not out then return nil, err end return out end local function frdec(s) if type(s) ~= "string" then return s end return (s:gsub("(%d)%.(%d)", "%1{,}%2")) end -- Arrondit chaque nombre décimal d'une expression LaTeX à la précision -- du document (option de classe precision=..., 3 par défaut) — les -- solutions numériques de SymPy arrivent avec 15 chiffres significatifs, -- illisibles pour un élève. local function arrondir_nombres(s, prec) return (s:gsub("%-?%d+%.%d+", function(num) local n = tonumber(num) if not n then return num end local shown = string.format("%." .. prec .. "f", n) shown = shown:gsub("0+$", ""):gsub("%.$", "") return shown end)) end local function content_str(content) if type(content) == "table" then content = table.concat(content, "\n") end return content:gsub("^%s+", ""):gsub("%s+$", "") end local function content_rows(content) if type(content) == "table" then content = table.concat(content, "\n") end local rows = {} for l in (content .. "\n"):gmatch("(.-)\n") do l = l:gsub("^%s+", ""):gsub("%s+$", "") if l ~= "" then rows[#rows + 1] = l end end return rows end sl.register_block("sympyderiv", function(api, words, content) content = content_str(content) local name, order, var = words:match("^(%S+)%s+(%d+)%s+(%S+)$") if not name then name, var = words:match("^(%S+)%s+(%S+)$"); order = "1" end local latex, err = CALC_SYMPY.derivative(content, var or "x", tonumber(order) or 1) if not latex then error(err, 0) end local primes = string.rep("'", tonumber(order) or 1) api.lit("\\(" .. (name or "f") .. primes .. "(" .. (var or "x") .. ") = " .. frdec(latex) .. "\\)") end) sl.register_block("sympyzeros", function(api, words, content) content = content_str(content) local nom, var = words:match("^(%S+)%s+(%S+)$") var = var or "x" local latex, err = CALC_SYMPY.zeros(content, var) if not latex then error(err, 0) end local prec = tonumber(sl.config and sl.config.precision) or 3 if prec < 0 then prec = 3 end latex = arrondir_nombres(latex, prec) local indice = nom and ("_{" .. nom .. "}") or "" api.lit("\\(\\mathcal{S}" .. indice .. " = " .. frdec(latex) .. "\\)") end) sl.register_block("sympynroots", function(api, words, content) content = content_str(content) local n = tonumber(words:match("^(%d+)")) or 2 local latex, err = CALC_SYMPY.nroots(n, content) if not latex then error(err, 0) end api.lit("\\(\\mathcal{S} = " .. frdec(latex) .. "\\)") end) sl.register_block("sympyprim", function(api, words, content) content = content_str(content) local var = words:match("^%S+%s+(%S+)$") or "x" local latex, err = CALC_SYMPY.primitive(content, var) if not latex then error(err, 0) end api.lit("\\(\\displaystyle\\int " .. (words:match("^(%S+)") or "f") .. "(" .. var .. ")\\,\\mathrm{d}" .. var .. " = " .. frdec(latex) .. " + C\\)") end) sl.register_block("sympyexpand", function(api, words, content) content = content_str(content) local latex, err = CALC_SYMPY.expand(content) if not latex then error(err, 0) end api.lit("\\(" .. frdec(M.mathlite(content)) .. " = " .. frdec(latex) .. "\\)") end) sl.register_block("sympyapart", function(api, words, content) content = content_str(content) local latex, err = CALC_SYMPY.apart(content, "x") if not latex then error(err, 0) end api.lit("\\(" .. frdec(latex) .. "\\)") end) sl.register_block("sympyode", function(api, words, content) content = content_str(content) local f, var = words:match("^(%S+)%s+(%S+)$") if not f then f = words:match("^(%S+)") or "y" end local latex, err = CALC_SYMPY.ode(content, f, var) if not latex then error(err, 0) end api.lit("\\(" .. frdec(latex) .. "\\)") end) sl.register_block("sympypde", function(api, words, content) content = content_str(content) local f = words:match("^(%S+)") or "u" local latex, err = CALC_SYMPY.pde(content, f) if not latex then error(err, 0) end api.lit("\\(" .. frdec(latex) .. "\\)") end) sl.register_block("sympyeigen", function(api, words, content) local rows = content_rows(content) local latex, err = CALC_SYMPY.eigen(rows_to_py(rows)) if not latex then error(err, 0) end api.lit("\\(\\operatorname{Sp}(" .. (words:match("^(%S+)") or "M") .. ") = " .. frdec(latex) .. "\\)") end) sl.register_block("sympydiag", function(api, words, content) local rows = content_rows(content) local P_, D_ = CALC_SYMPY.diagonalize(rows_to_py(rows)) if not P_ then error(D_, 0) end local nom = words:match("^(%S+)") or "M" api.lit("\\(" .. nom .. " = P D P^{-1}\\) avec \\(P = " .. frdec(P_) .. "\\) et \\(D = " .. frdec(D_) .. "\\)") end) sl.register_block("sympysum", function(api, words, content) content = content_str(content) local var, a, b = words:match("^(%S+)%s+(%S+)%s+(%S+)$") local S, R = CALC_SYMPY.closed_sum("sum", content, var, a, b) if not S then error(R, 0) end api.lit("\\(\\displaystyle " .. frdec(S) .. " = " .. frdec(R) .. "\\)") end) sl.register_block("sympyprod", function(api, words, content) content = content_str(content) local var, a, b = words:match("^(%S+)%s+(%S+)%s+(%S+)$") local S, R = CALC_SYMPY.closed_sum("prod", content, var, a, b) if not S then error(R, 0) end api.lit("\\(\\displaystyle " .. frdec(S) .. " = " .. frdec(R) .. "\\)") end) sl.register_block("sympycanon", function(api, words, content) content = content_str(content) local latex, err = CALC_SYMPY.canonical(content, "x") if not latex then error(err, 0) end api.lit("\\(" .. frdec(M.mathlite(content)) .. " = " .. frdec(latex) .. "\\)") end) sl.register_block("sympynint", function(api, words, content) content = content_str(content) local name, var, a, b = words:match("^(%S+)%s+(%S+)%s+(%S+)%s+(%S+)$") local val, err = CALC_SYMPY.nintegrate(content, var, a, b) if not val then error(err, 0) end local prec = tonumber(sl.config and sl.config.precision) or 3 if prec < 0 then prec = 3 end local num = tonumber(val) local shown = num and string.format("%." .. prec .. "f", num) or val shown = shown:gsub("0+$", ""):gsub("%.$", "") shown = shown:gsub("%.", "{,}") api.lit("\\(\\displaystyle\\int_{" .. a .. "}^{" .. b .. "} " .. (name or "f") .. "(" .. var .. ")\\,\\mathrm{d}" .. var .. " \\approx " .. shown .. "\\)") end) local function bloc_expr(nom, calc, prefixe) sl.register_block(nom, function(api, words, content) content = content_str(content) local latex, err = calc(content, words) if not latex then error(err, 0) end api.lit("\\(" .. prefixe(content, words) .. frdec(latex) .. "\\)") end) end bloc_expr("sympyfactor", function(c) return CALC_SYMPY.factor(c) end, function(c) return frdec(M.mathlite(c)) .. " = " end) bloc_expr("sympysimplify", function(c) return CALC_SYMPY.simplify_expr(c) end, function(c) return frdec(M.mathlite(c)) .. " = " end) sl.register_block("sympylimit", function(api, words, content) content = content_str(content) local name, var, pt, dir = words:match("^(%S+)%s+(%S+)%s+(%S+)%s*(%S*)$") local latex, err = CALC_SYMPY.limite(content, var, pt, dir ~= "" and dir or nil) if not latex then error(err, 0) end local ptx = pt:gsub("infini", "\\infty"):gsub("%+\\infty", "+\\infty") local sub = var .. " \\to " .. ptx .. (dir == "droite" and "^{+}" or dir == "gauche" and "^{-}" or "") api.lit("\\(\\displaystyle\\lim_{" .. sub .. "} " .. (name or "f") .. "(" .. var .. ") = " .. frdec(latex) .. "\\)") end) sl.register_block("sympydl", function(api, words, content) content = content_str(content) local name, var, a, n = words:match("^(%S+)%s+(%S+)%s+(%S+)%s+(%d+)$") local latex, err = CALC_SYMPY.dl(content, var, a, tonumber(n)) if not latex then error(err, 0) end api.lit("\\(" .. (name or "f") .. "(" .. var .. ") = " .. frdec(latex) .. "\\)") end) sl.register_block("sympysolveeq", function(api, words, content) content = content_str(content) local lhs, rhs = content:match("^(.-)=(.+)$") if not lhs then error("texecole : attend une équation avec un signe =.", 0) end local domain = words:match("^(%S+)") local latex, err = CALC_SYMPY.solve_eq(lhs, rhs, "x", domain) if not latex then error(err, 0) end api.lit("\\(\\mathcal{S} = " .. frdec(latex) .. "\\)") end) local function bloc_matrice(nom, op, texte) sl.register_block(nom, function(api, words, content) local rows = content_rows(content) local latex, err = CALC_SYMPY.matrix_op(rows, op) if not latex then error(err, 0) end api.lit("\\(" .. texte:gsub("NOM", words:match("^(%S+)") or "M") .. " " .. frdec(latex) .. "\\)") end) end bloc_matrice("sympydet", "det", "\\det(NOM) =") bloc_matrice("sympyinv", "inv", "NOM^{-1} =") sl.sympy = CALC_SYMPY return CALC_SYMPY end