Привет!

В этой «статье», а вернее сказать очерке, покажу очень простой способ развлечься зная самые основы latex и python.




Зачем?


Ну, можно генерировать простые выражения для детей чтобы считали. Или просто так. Да хоть на обои поставить, если вы такой же фанатик, как и я.

Как это по идее должно работать?


Идея действительно очень простая, написать такую программу может абсолютно каждый. Мы хотим сгенерировать выражение, равное некоторому числу n (которое вводит пользователь). Любое число можно заменить на арифметическое выражение, например, 3 = 1 + 2. А 2 это 4 / 2. Вот так мы сгенерировали 3 = 1 + 4/2. Аналогично, мы введем несколько разных операций и завернем это в LaTeX, язык формул.

Вам понадобится...
Одна неделя опыта в python и matplotlib. Я серьезно.

Основной механизм


Нам нужно распарсит�� выражение так, чтобы вытащить оттуда числа. Назовем наш класс как генератор проблем (нам всем его так не хватает!)

import random
from math import log
import math
import sys
sys.setrecursionlimit(1000)   # Эта магия делает нерабочий код рабочим


class ProblemGenerator:
    def extract_nums(self, exp):
        symbols = list(exp)
        NUM = "1234567890."
        for i in range(len(symbols)):
            symbols[i] = "N" if symbols[i] in NUM else "T"
        begins = []
        ends = []
        for i in range(len(symbols) - 1):
            fn = symbols[i] + symbols[i + 1]
            if fn == "TN":
                begins.append(i)
            elif fn == "NT":
                ends.append(i)
        if exp[-1] in NUM:
            ends.append(len(exp) - 1)
        if exp[0] in NUM:
            begins = [-1] + begins
        return [(x + 1, y + 1) for x, y in zip(begins, ends)]


Смысл функции extract_nums в том, чтобы получить n пар чисел (a, b), где a — позиция первого символа, b — позиция последнего + 1.

Например, если мы запустим следующий код:

gen = ProblemGenerator()
print(gen.extract_nums("13+256/355+25"))

Увидим:

[(0, 2), (3, 6), (7, 10), (11, 13)]

То есть это массив tuple. (0, 2) означает, что есть число между 0 (включительно) и 2 (не включительно).

Теперь нам хотелось бы сделать разные операторы, начнем с умножения и суммы. Объявим три функции

def unmin(*args, acc=2):
    r = []
    for arg in args:
        f = round(arg, acc)
        if f > 0:
            f = str(f)
        else:
            f = "(" + str(f) + ")"
        r.append(f)
    return r

def __c_sum(num):
    a = round(random.random() * 100, 3)
    b = num - a
    a, b = unmin(a, b)
    return a + " + " + b

def __c_mul(num):
    a = num / (random.random() * 100 + 10)
    if a == 0.0:
        b = random.random()
    else:
        b = num / a
    a, b = unmin(a, b)
    return a + " * " + b


Суть функции unmin не только в том, чтобы просто преобразовать все аргументы в строки, но и в том, чтобы заключить в скобки какой-то из операндов, если он меньше нуля. К примеру, мы получили числа a=3, b=-4. Если мы напишем

a = 3
b = -4
a, b = unmin(a, b)

То a=«3», b="(-4)"

Ну а остальные функции понятные: __c_sum возвращает строку вида «13 + 4», а __c_mul «13 * 4».
Остается соединить эти две штуки и заменять каждое число в выражении на выражение.
Добавим в ProblemGenerator следующий код:

class ProblemGenerator:
...
    def __init__(self):
        self.funcs = []
    
    def add_expander(self, func):
        self.funcs.append(func)
    
    def complexify(self, num):
        return random.choice(self.funcs)(num)
    
    def __rxp__(self, exp):
        x, y = random.choice(self.extract_nums(exp))
        exp = exp[:x] + "(" + self.complexify(float(exp[x:y])) + ")" + exp[y:]
        return exp
    
    def randexpr(self, ans, steps):
        e = str(ans)
        for i in range(steps):
            e = self.__rxp__(e)
        return e


complexify принимает какое-то число, а возвращает строку — усложненное выражение. ��апример, если напишем:

gen = ProblemGenerator()
gen.add_expander(__c_sum)
print(gen.complexify(13))

Получим:

31.2 + (-18.2)

Как работает __rxp__? Мы выбираем позицию случайно числа из выражения (к примеру, если есть выражение «13+35/45», то допустим мы выбрали (3, 5)) и заменяем это число на выражение, равное этому числу. То есть хотелось бы:

«13+35/45» — рандомное число (3, 5)
«13+» + "(12 + 23)" + "/45"
«13+(12+23)/45»

Так и работает __rxp__
Ну а randexpr работает совсем просто. Например, если у нас четыре шага, то раскрывать выражение будет так:

13
(5.62 + 7.38)
((20.63 + (-15.01)) + 7.38)
((20.63 + (-(67.5 + (-52.49)))) + 7.38)
((20.63 + (-((15.16 + 52.34) + (-52.49)))) + 7.38)

Попробуем запустить:

gen = ProblemGenerator()
gen.add_expander(__c_sum)
gen.add_expander(__c_mul)
exp = gen.randexpr(1, 5)
print(exp)

Результат:

((6.63 + (56.62 + 16.8)) + (-((60.53 + 3.61) + 14.91)))

LaTeX


Как ни странно, осталось самое простое. Объявим целый ряд разных операторов LaTeX:

def __l_sum(num):
    a = 100 ** (random.random() * 2)
    b = num - a
    a, b = unmin(a, b)
    return a + " + " + b

def __l_div(num):
    a = num * (random.random() * 100 + 10)
    if a == 0.0:
        b = random.random()
    else:
        b = a / num
    a, b = unmin(a, b)
    return "\\frac{" + a + "}{" + b + "}"

def __l_pow(num):
    if num == 0:
        return str(random.randint(2, 7)) + "^{-\\infty}"
    a = random.randint(0, 10) + 3
    b = math.log(abs(num), a)
    a, b = unmin(a, b)
    return ("-" if num < 0 else "") + a + "^{" + b + "}"

def __l_sqrt(num):
    a = num ** 0.5
    a = unmin(a)[0]
    return "\\sqrt{" + a + "}"

def __l_int(num):
    patterns = [
        ("x^{2}", (3 * num) ** (1/3), "dx"),
        ("y^{3}", (4 * num) ** (1/4), "dy"),
        ("\sqrt{t}", (1.5 * num) ** (2/3), "dt")
    ]
    p, b, f = random.choice(patterns)
    b = str(round(b, 3))
    return "\\int_{0}^{" + b + "} " + p + " " + f

def __l_sig(num):
    a = random.randint(1, 10)
    b = random.randint(1, 10) + a
    s = sum([i for i in range(a, b + 1)])
    c = num / s
    a, b, c = unmin(a, b, c)
    return "\\sum_{i=" + a + "}^{" + b + "} i*" + c


Добавим все функции в gen:

gen = ProblemGenerator()
gen.add_expander(__l_sum) # Сумма двух чисел
gen.add_expander(__l_div)   # Дробь
gen.add_expander(__l_pow) # Степень
gen.add_expander(__l_sqrt) # Квадратный корень
gen.add_expander(__l_int)   # Определенный интеграл
gen.add_expander(__l_sig)   # Оператор сигма

И наконец добавим вывод результата:

import matplotlib.pyplot as plt
plt.axis("off")
latex_expression = gen.randexpr(1, 30)  # 30 раз заменяем. Выражение будет равно 1
plt.text(0.5, 0.5, "$" + latex_expression + "$", horizontalalignment='center', verticalalignment='center', fontsize=20)
plt.show()


Вот и всё.

Весь код
import random
from math import log
import math
import sys
sys.setrecursionlimit(1000)


class ProblemGenerator:
    def extract_nums(self, exp):
        symbols = list(exp)
        NUM = "1234567890."
        for i in range(len(symbols)):
            symbols[i] = "N" if symbols[i] in NUM else "T"
        begins = []
        ends = []
        for i in range(len(symbols) - 1):
            fn = symbols[i] + symbols[i + 1]
            if fn == "TN":
                begins.append(i)
            elif fn == "NT":
                ends.append(i)
        if exp[-1] in NUM:
            ends.append(len(exp) - 1)
        if exp[0] in NUM:
            begins = [-1] + begins
        return [(x + 1, y + 1) for x, y in zip(begins, ends)]
    
    def __init__(self):
        self.funcs = []
    
    def add_expander(self, func):
        self.funcs.append(func)
    
    def complexify(self, num):
        return random.choice(self.funcs)(num)
    
    def __rxp__(self, exp):
        x, y = random.choice(self.extract_nums(exp))
        exp = exp[:x] + "(" + self.complexify(float(exp[x:y])) + ")" + exp[y:]
        return exp
    
    def randexpr(self, ans, steps):
        e = str(ans)
        for i in range(steps):
            e = self.__rxp__(e)
        return e

def unmin(*args, acc=2):
    r = []
    for arg in args:
        f = round(arg, acc)
        if f > 0:
            f = str(f)
        else:
            f = "(" + str(f) + ")"
        r.append(f)
    return r

def __c_sum(num):
    a = round(random.random() * 100, 3)
    b = num - a
    a, b = unmin(a, b)
    return a + " + " + b

def __c_mul(num):
    a = num / (random.random() * 100 + 10)
    if a == 0.0:
        b = random.random()
    else:
        b = num / a
    a, b = unmin(a, b, acc=5)
    return a + " * " + b

def __c_sub(num):
    a = num + 100 ** (random.random() * 2)
    b = (a - num)
    a, b = unmin(a, b)
    return a + " - " + b

def __c_log(num):
    fr = random.randint(300, 500)
    a = math.e ** (num / fr)
    a, fr = unmin(a, fr, acc=5)
    return "log(" + a + ") * " + fr

def __l_sum(num):
    a = 100 ** (random.random() * 2)
    b = num - a
    a, b = unmin(a, b)
    return a + " + " + b

def __l_div(num):
    a = num * (random.random() * 100 + 10)
    if a == 0.0:
        b = random.random()
    else:
        b = a / num
    a, b = unmin(a, b)
    return "\\frac{" + a + "}{" + b + "}"

def __l_pow(num):
    if num == 0:
        return str(random.randint(2, 7)) + "^{-\\infty}"
    a = random.randint(0, 10) + 3
    b = math.log(abs(num), a)
    a, b = unmin(a, b)
    return ("-" if num < 0 else "") + a + "^{" + b + "}"

def __l_sqrt(num):
    a = num ** 0.5
    a = unmin(a)[0]
    return "\\sqrt{" + a + "}"

def __l_int(num):
    patterns = [
        ("x^{2}", (3 * num) ** (1/3), "dx"),
        ("y^{3}", (4 * num) ** (1/4), "dy"),
        ("\sqrt{t}", (1.5 * num) ** (2/3), "dt")
    ]
    p, b, f = random.choice(patterns)
    b = str(round(b, 3))
    return "\\int_{0}^{" + b + "} " + p + " " + f

def __l_sig(num):
    a = random.randint(1, 10)
    b = random.randint(1, 10) + a
    s = sum([i for i in range(a, b + 1)])
    c = num / s
    a, b, c = unmin(a, b, c)
    return "\\sum_{i=" + a + "}^{" + b + "} i*" + c

gen = ProblemGenerator()
gen.add_expander(__l_sum)
gen.add_expander(__l_div)
gen.add_expander(__l_pow)
gen.add_expander(__l_sqrt)
gen.add_expander(__l_int)
gen.add_expander(__l_sig)

import matplotlib.pyplot as plt
plt.axis("off")
latex_expression = gen.randexpr(1, 30)  # 30 раз заменяем. Выражение будет равно 1
plt.text(0.5, 0.5, "$" + latex_expression + "$", horizontalalignment='center', verticalalignment='center', fontsize=15)
plt.show()



Результат (3 скриншота)






Only registered users can participate in poll. Log in, please.
Продолжать писать «фановые очерки» вроде этого?
75.29%Да64
12.94%Нет11
11.76%НЛО прилетело и опубликовало эту надпись здесь10
85 users voted. 15 users abstained.