Привет!

В этой «статье», а вернее сказать очерке, покажу очень простой способ развлечься зная самые основы 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 скриншота)






Комментарии (35)


  1. kasyachitche
    23.09.2019 10:45

    Все-таки не хватает какого-нибудь введения и вывода.
    Лично мне в первую очередь интересно почитать о том, как такая идея пришла, что автор думает о ее применимости и как можно полученные примеры назвать арифметическими?
    А уже потом, если понравится, можно знакомиться с тем, как это реализовано.


    1. WhiteBlackGoose Автор
      23.09.2019 10:47

      Ща добавлю)


    1. KvanTTT
      23.09.2019 13:04
      +1

      Я использовал генерацию формул для тестирования корректности алгоритма взятия производных и упрощения выражений. Ожидаемые значения брались из WolframAlpha с помощью API, а актуальные — из моего алгоритма. Подробней здесь: Математические выражения в .NET (разбор, дифференцирование, упрощение, дроби, компиляция).


      1. WhiteBlackGoose Автор
        23.09.2019 13:13

        Ничего себе вы заморочились… Интересно, почитаю!


  1. Cobolorum
    23.09.2019 10:50
    +1

    Может я отстал от жизни, но как это посчитать?

    Искусство создания примеров, это Искусство оно должно что то показывать. А случайный набор числе и действий это хаус.


    1. WhiteBlackGoose Автор
      23.09.2019 10:58

      Вы абсолютно правы, есть небольшая ошибочка. Спасибо, хотя лучше это помечать в диалогах.


    1. Refridgerator
      23.09.2019 11:55

      как это посчитать?
      Технически посчитать можно, если представить сумму в замкнутом виде: Sqrt(5.5)*(1 — a + b)*(a + b)/10, где a и b пределы суммирования. В итоге получим -6649*Sqrt(5.5)/1000.


      1. KvanTTT
        23.09.2019 13:05

        Откуда у вас взялось значение 5.5?


        1. Refridgerator
          23.09.2019 13:24

          1. Refridgerator
            23.09.2019 14:57

            Вероятно, вынес 1/100 из под корня.


      1. KvanTTT
        23.09.2019 16:45

        А вообще да: можно обобщить любое вычисление, для этого нужно найти непрерывную функцию зависимости i-того значения от числа i. Для факториала, например, это гамма-функция. В данном случае все проще, так как формула суммы выражается через арифметическую прогрессию, в которую можно вставлять любые числа, не только целые.


    1. kivicode
      23.09.2019 13:37

      Я бы сказал, что в данном случае это не имеет значения ибо тут идёт краткая презентация умений рисовать сложные выражения, а не математические изыскания


  1. andreybotanic
    23.09.2019 12:05

    Вот если бы ещё скобки рисовались всегда нужного размера, было бы гораздо красивее и понятнее. А так получается какое-то нагромождение символов, цифр и операторов… А вообще идея интересная.


    1. WhiteBlackGoose Автор
      23.09.2019 12:26

      Ну так это дело наживное, так сказать). Можно поиграться с параметрами, сделать все попроще, получать что-то типа


      1. Peacemaker
        23.09.2019 16:30

        Тогда неплохо было бы и размеры скобок подгонять для лучшей читаемости


        1. WhiteBlackGoose Автор
          23.09.2019 16:32

          Это, честно говоря, не ко мне. Так рисует латех. Хотя я с вами безусловно согласен.


          1. Peacemaker
            23.09.2019 16:37

            К Вам, к Вам — в латехе есть возможность увеличения скобок, посмотрите на примеры
            \[ ( \big( \Big( \bigg( \Bigg( \],

            \[\left(
            \left[
            \left\langle
            \left\{
            \left\uparrow
            \left\lceil
            \left|
            \left\lfloor
            \right\rfloor
            \right|
            \right\rceil
            \right\downarrow
            \right\}
            \right\rangle
            \right]
            \right)\]


          1. Peacemaker
            23.09.2019 16:41

            Особенно здорово, если скобки автоматически подбирают свой размер под выражение, которое они окружают. Парные команды \left и \right включают режим
            подобной подстройки.

            Е.М. Балдин. Компьютерная типография LaTeX


      1. andreybotanic
        23.09.2019 18:23

        Все равно выглядит не очень эстетично, да и смысл теряется


  1. longclaps
    23.09.2019 13:31

    Вырвиглазный стиль кода. Ну и вспомнилось )


  1. grishkaa
    23.09.2019 20:45
    +1

    Матан-капча 85 уровня.


    1. WhiteBlackGoose Автор
      23.09.2019 20:58

      Прикольно! Гришка.рф вернул 404, впрочем, не удивительно… 8 лет статье.


      1. grishkaa
        24.09.2019 03:37

        Чот у меня какая-то убогая страница 404, надо бы переделать)


  1. vektory79
    23.09.2019 21:24
    +1

    А я вот уже давно ищу какой-нибудь вменяемый генератор примеров по математике, чтобы своих школьников гонять дома дополнительно.


    Пока это была арифметика первых классов — без проблем генерировал.


    Но сейчас это 8-й и 9-й класс… Нагенерировать релевантных примеров становится не так-то и просто...


    Может кто-то сталкивался с чем-то похожим?


    1. WhiteBlackGoose Автор
      23.09.2019 21:27
      +1

      А каких видов должны быть примеры? Наверное восьмиклассникам арифметика уже не нужна? Кастомные уравнения может быть? Ну например за 10 минут пишется штука, которая по n корням выдает многочлен, обладающий такими корнями.


      1. vektory79
        23.09.2019 21:35

        Ну тут уже сложный вопрос. Возможно у меня мозги криво устроены, чтобы быстро решать такие задачки :-)


        А темы постоянно новые. Это и квадратные уравнения. И системы уравнений. И тригонометрия.


        Ну и алгебра с геометрией не за горами :-)


        Вообще неплохая идея кому-нибудь для сайта. Генерировать по учебной программе примеры. И с разным уровнем сложности :-)


        А то по программированию тренажёров уже пруд пруди на любой вкус, а по школьной программе как-то грустно...


        1. WhiteBlackGoose Автор
          23.09.2019 22:19
          +2

          Наляпал для многочленов


          from IPython.display import display, Math
          import random
          
          def mutmul(arr, dep):
              if dep == 1:
                  return sum(arr)
              r = []
              for i in range(len(arr)):
                  r.append(arr[i] * mutmul(arr[i + 1:], dep-1))
              return sum(r)
          
          def spow(v, p):
              if p == 0:
                  return ""
              if p == 1:
                  return v
              return v + "^" + str(p)
          
          def polynom(roots):
              mroots = [-i for i in roots]
              coefs = [mutmul(mroots, i + 1) for i in range(len(mroots))]
              r = [spow("x", len(mroots))]
              for i, c in enumerate(coefs):
                  s = str(c) + spow("x", len(coefs) - i - 1)
                  r += [s]
              r = "+".join(r)
              r = r.replace("+-", "-")
              return r
          
          for i in range(10):
              roots = [random.randint(-10, 10) for i in range(random.randint(1, 5))]
              display(Math("$" + polynom(roots) + "=0" + "$"))


          1. vektory79
            23.09.2019 22:37

            Блин, спасибо!


            Надо поподробнее проникнуться подходом. Полезно!


            Ради такого и выучить таки Python не грех :-D


        1. daiver19
          23.09.2019 22:28

          Вопрос в тому, кому эти примеры нужны в таких количествах, особенно в мире, где существует WolframAlpha.


          1. vektory79
            23.09.2019 22:36

            Учителям, репетиторам, да и просто родителям, которые заботятся о будущем своих детей ;-)


            1. daiver19
              23.09.2019 22:46

              Я имею в виду в более широком смысле: зачем решать десятки однотипных примеров, разница в которых только в числах? Зачем нужно больше 3-4 примеров квадратных уравнений?


              1. vektory79
                23.09.2019 23:15

                1. Не обязательно только в числах. Можно же и комбинировать темы между собой. Причём разнообразным образом.
                2. Разные дети усваивают по разному. Некоторым надо много попыток, прежде чем они смогут уверенно пользоваться конкретными приёмами
                3. Если надо нагенерить контрольную на целый класс? А если перездача и надо новые примеры?
                4. А если надо повторить материал, чтобы не забылся?

                Короче моя практика показывает (хоть я и не учитель), что если примеры каждый раз разные и отличаются не "только числами", то результат гораздо выше. Но вот нагенерировать такие примеры не всегда так уж просто. И ладно я хоть ITшник. А как быть людям вообще далёким от IT, но желающим дать своим детям путёвку в жизнь?


                1. daiver19
                  23.09.2019 23:39

                  Так в том-то и дело, что в школе обычно заваливают однотипными примерами (типа «реши 3 линейных системы» или «реши 3 квадратных уравнения»). Возможно, вы имели в виду что-то другое, но по начальному комментарию показалось, что вы хотите генерировать больше этих однотипных примеров.


              1. saege5b
                24.09.2019 00:27

                Дочь легко считает два-три примера, потом «скисает», и банально «зевает».
                Портянки нужны для автоматизма.
                Но эти примеры очень простые, нужны скобки, комбинация скобок.


                1. daiver19
                  24.09.2019 00:33

                  Не очень понял мысль, вроде как она подтверждает мою позицию по поводу того, что мучать детей однотипными заданиями непродуктивно. Или нет?