Что нового в AngouriMath 1.2?

    Приветствую. Последние 7 месяцев я работал над крупнейшим обновлением AngouriMath. И есть о чем рассказать.

    В двух словах о том, что происходит

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

    Но ведь есть же...

    Слыша про то, что я делаю, разные люди предлагают разные решения. Переписать SymPy, сделать обертку над SageMath для дотнета, спиратить Wolfram|Alpha, использовать примитивный mathnet.symbolics (о примитивности говорят они сами).

    Но все это имеет либо ограничения, либо сложности. То же, над чем работаю я, это очень легковесная библиотека, написанная и оптимизированная для .NET. Опен-сорсная, конечно. (под MIT)

    Обновление 1.2

    В августе один из главных контрибьюторов @HappyPig375 помог переписать на тото момент существенную часть библиотеки на нормальную иерархию типов. Теперь на каждый оператор или фукцию есть отдельный тип. Это был переломный момент, до которого библиотека была медленная, неуклюжая, и совершенно неочевидная. А теперь пройдемся по тому, что с тех пор было сделано.

    Выражение - это record

    Например, вот так выглядит объявление оператора суммы

    public sealed partial record Sumf(Entity Augend, Entity Addend) : NumericNode

    Благодаря этому мы можем легко применять новый pattern matching:

    internal static Entity CommonRules(Entity x) => x switch
    {
        // (a * f(x)) * g(x) = a * (f(x) * g(x))
        Mulf(Mulf(Number const1, Function func1), Function func2) => func1 * func2 * const1,
    
        // (a/b) * (c/d) = (a*c)/(b*d)
        Mulf(Divf(var any1, var any2), Divf(var any3, var any4)) => any1 * any3 / (any2 * any4),
    
        // a / (b / c) = a * c / b
        Divf(var any1, Divf(var any2, var any3)) => any1 * any3 / any2,

    (это пример паттернов, которые работают при упрощении выражения)

    Математика

    Здесь будут описаны фичи, которые связаны непосредственно с математикой.

    Новые функции

    Добавлены секанс, косеканс, арксеканс, арккосеканс как отдельные ноды.

    Добавлены 12 гиперболических функций, которые не имеют своих нод и возвращают свое символьное представление (sinh(x)как (e.Pow(x) - e.Pow(-x)) / 2).

    Добавлены Abs и Signum. Синтаксис abs решено было сделать так: (|x|). Это позволяет делать похоже на то, как мы делаем ручками на бумаге, и в то же время позволяет избежать двусмысленности (так как | симметричная, и с ней могут быть проблемы).

    Добавлена функция Phi (функция Эйлера).

    WriteLine(@"phi(8)".EvalNumerical());
    WriteLine(@"(|-3 + 4i|)".EvalNumerical());
    WriteLine(@"sinh(3)".Simplify());
    WriteLine(@"sec(0.5)".Simplify());

    Выведет

    4
    5
    (e ^ 3 - 1 / e ^ 3) / 2
    sec(1/2)

    Домены

    Они позволяют ограничивать область определения каждой ноды. Если нода вышла за домен, она превращается в NaN, который делает все выражение неопределенным. В качестве доступных доменов - SpecialSet, о которых позже.

    Булева алгебра и логика

    Наконец-то добавлены логические операторы. Хотя я думал сначала сделать через какие-то символы, в какой-то момент стало очевидно, что операторы должны быть буквенные. Поэтому синтаксис таков: not, or, xor, and, implies.

    Чтобы проверить, вычисляемое ли выражение в Boolean, нужно использовать EvaluableBoolean. Аналогично тому, как мы используем EvaluableNumerical чтобы понять, коллапсится ли выражение в Number.

    WriteLine(@"(true or b) implies c".Simplify());

    (выведет c)

    Знаки равенства и неравенства

    Они сами по себе коллапсятся в Boolean, если их значение очевидно. Добавлены очевидные знаки: =, <, >, <=, >=.

    А еще можно делать комбинации неравенств при парсинге. Например, a > b > c как парсится, так и понимается (a > b > c то же, что a > b and b > c).

    WriteLine(@"a < b >= c".Simplify());

    (Выведет a < b and b >= c)

    Множества

    Добавлены множества. Они парсятся и имеют свои ноды.

    Самый простой тип множества - FiniteSet, как можно догадаться, конечное множество. С ним все просто. Вот пример синтаксиса: { 1, 2, 3 }.

    Интервалы/отрезки тоже имеют привычный синтаксис: [1; 2] для отрезка, (1; 2) для интервала, [1; 2) для полуинтервала, как и (1; 2]. Комплексные интервалы были убраны за ненадобностью и громоздкостью.

    SpecialSet это "захардкоденные" множества. Сейчас есть CC, RR, QQ, ZZ, BB для комплексных, действительных, рациональных, целых, и булевых соответственно.

    ConditionalSet записывается в set-builder notation, вот пример: { x : x > 0 and x^2 = y } (все такие x, что положительные и их квадрат равен y).

    WriteLine(@"({ 1, 2 } \/ { 5 }) /\ { x : x in [2; 3] and x > 0 } ".Simplify());

    (Выведет { 2 })

    Пределы улучшены

    Добавлены правила преобразования по первому замечательному, второму замечательному. И правило Лопиталя.

    WriteLine("tan(a x) / (b x)".Limit("x", 0));
    WriteLine("(sin(t) - t) / t3".Limit("t", 0));

    (Выведет a / b и -1/6 соответственно)

    Нода "Provided"

    Позволяет установить условия на выражение, в каких случаях оно вообще определено. Например, квадратный корень на действительных мы определяем как sqrt(x) provided x >= 0. Если подставить отрицательный x, выражение станет NaN.

    Если такая нода встречается в конечном множестве, то если она станет NaN, то просто исключится из множества. Это единственное место, где NaN как ребенок не превращает родительское выражение в NaN.

    Кусочно-заданная функция

    Piecewise - это последовательность из Provided. В отличии от классической кусочно-заданной, в этой есть порядок, и при вычислении значения Piecewise, мы возвращаем первый Provided такой, что его предикат истинен.

    Вот пример того, как мы можем определить действительный модуль числа с помощью Piecewise:

    Entity abs = "piecewise(x provided x > 0, -x provided x <= 0)";
    WriteLine(abs.Substitute("x", 3).EvalNumerical());
    WriteLine(abs.Substitute("x", -3).EvalNumerical());

    (Выведет 3 в обоих случаях)

    Проще и удобнее использовать

    А здесь фичи, благодаря которым библиотеку стало проще и/или безопаснее использовать.

    Исключения обобщены и отрефакторены

    Теперь все исключения, которые выбрасывает библиотека, так или иначе наследуются от AngouriMathBaseException. Так как никакого p/invoke-а или чего-то еще страшного библиотека не производит, можно быть уверенными, что все то, что находится под AngouriMathBaseException, не вредит системе или пользовательским данным. Иначе говоря, при обработке исключений, теперь можно последним catch-ем указать именно это исключение (если надо).

    Производительность улучшена

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

    F# поддерживается

    Теперь API AngouriMath доступен и для замечательного языка F#. Впрочем, вряд ли оно настолько же функциональное, но многие фичи обертка над F# имеет. Если что-то сложное, из него можно обратиться к самой библиотеке.

    Interactive

    Некоторое время назад я делал пост о том, как AngouriMath выглядит в Jupyter. Сам по себе AngouriMath.Interactive просто преобразывает ILatexiseable в LaTeX-код и рендерит его с помощью MathJax (подробности в той статье).

    Простой пример использования AngouriMath.Interactive в Jupyter
    Простой пример использования AngouriMath.Interactive в Jupyter

    Многопоточность

    Все вычисления происходят строго в одном потоке. Кто я такой, чтобы ваши ядра воровать? Более того, все методы потокобезопасны. Настройки локальны для каждого потока ([ThreadStatic]), о которых чуть позже.

    Основная фича обновления - теперь можно прервать вычисления в случае, если вам уже давно не нужен результат работы Solve или Simplify, и вам нужно просто вернуться уже и выдать пользователю ошибку.

    Новые настройки

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

    using var _ = MaxExpansionTermCount.Set(10);
    // какой-то код

    Тогда эта настройка автоматически откатится до предыдущей по окончании области (то, что возвращает Set, является IDisposable).

    Пока все

    Спасибо за внимание. Если эта статья помогла хотя бы одному человеку, значит я уже не зря писал. Задавайте вопросы, будет приятно ответить. Это очень кратенькое изложение, потому что лонгриды по узкой теме вряд ли кому зайдут.

    Если кто-то хочет стать контрибьютором, я буду этому очень рад (пишите мне или сразу форкайте и улучшайте).

    Ссылки

    1. Гитхаб проекта.

    2. Сайт проекта.

    3. Более детальное What's new.

    4. Планы на следующие обновления.

    5. Мой профиль на GitHub.

    6. SymPy - вдохновляют и подкидывают идеи.

    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    Писать о проекте дальше?

    • 59,1%Об обновлениях13
    • 86,4%О том, как работают фичи, в профильные хабы19
    • 18,2%О том, как работают фичи, в «Я пиарюсь»4
    • 4,6%Не писать1

    Комментарии 2

      0

      Добавил примеры по совету MomoDev. Если нужны еще, могу написать их здесь. Ну и конечно есть примеры в тестах.


      Публикацию разместил в "Я пиарюсь", хоть искренне надеюсь, что когда-нибудь я по праву смогу размещать подобное в профильные хабы :).

        +3
        Очень здорово, спасибо! С удовольствием почитаю новые статьи, сейчас как раз занимаюсь вспоминанием подзабытой алгербы, очень интересно попутно смотреть на реализацию «под капотом».

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое