12 июля 2021

🛠 Сложноструктурные аналитические отчеты с Python и LaTeX

Разработчик прикладного программно-математического обеспечения и специалист по численному анализу прочности объектов общего и нефтегазового машиностроения; инженер с опорой на численные методы и машинное обучение; к.т.н. по специальности "Динамика и прочность машин, приборов и аппаратуры"
В руководстве подробно рассматривается пример использования Python-библиотеки облачного представления приложений Streamlit и системы компьютерной вёрстки LaTeX для подготовки сложных аналитических отчетов с математическими, программными и графическими вставками.
🛠 Сложноструктурные аналитические отчеты с Python и LaTeX

Экосистема LaTeX. Быстрый старт

  • TeX (произносится как «тех») – система компьютерной верстки, предназначенная для подготовки научно-технических материалов высокого полиграфического качества.
  • LaTeX (произносится «латех») – это, строго говоря, набор макросов на языке разметки TeX, но в зависимости от контекста под LaTeX может пониматься макропакет, издательская система или язык, служащий для разметки документа. LaTeX 2e – наиболее полная, стабильная версия LaTeX.
  • MikTеX – свободно распространяемая реализация TeX под основные операционные системы – Windows, macOS, Linux (Ubuntu, Debian, CentOS и пр.) – включающая в себя практически все наиболее значимые расширения.

Установить реализацию MikTеX под нужную операционную систему можно по инструкциям на странице проекта. При желании с системой можно взаимодействовать и без установки дистрибутива, просто запустив Docker-образ:

miktex_install.sh
        $ docker pull miktex/miktex
$ docker volume create --name miktex
$ docker run -it \
  -v miktex:/miktex/.miktex \
  -v $(pwd):/miktex/work \
  miktex/miktex \
  pdflatex main.tex
    

Здесь с помощью команды docker pull в локальное хранилище образов скачивается Docker-образ miktex/miktex. Затем командой docker volume в файловой системе хоста создается директория miktex/. Последним шагом с помощью команды docker run остается запустить контейнер Docker на базе образа miktex/miktex. Флаг -it создает сеанс интерактивной работы на подключаемом терминальном устройстве, а флаг -v отображает директорию файловой системы хоста на директорию внутри контейнера.

Контейнер ожидает получить аргументы командной строки указывающие на выбранный компилятор (в данном случае PDFLaTeX) и собственно tex-файл (размеченный документ, на основании которого позже будет создан pdf-файл).

В качестве альтернативы MikTEX можно использовать TeX Live – это еще один свободно распространяемый и наиболее полный дистрибутив TeX. Для операционной системы MacOS X можно пользоваться специализированным решением MacTeX.

Опорный документ LaTeX (tex-файл) представляет собой обычный текстовый файл с разметкой. Для редактирования такого рода файлов можно пользоваться и обычным текстовым Unix-редактором Vim, но практичнее использовать специальные решения, например, открытую среду разработки TeXstudio (поддерживает все основные операционные системы). Однако TeXstudio не единственный вариант. Вот наиболее распространенные альтернативы: TeXmaker, Kile, TeXCenter.

LaTeX состоит из пакетов, получить доступ к которым можно на странице CTAN (The Comprehensive TeX Archive Network). CTAN – центральный репозиторий, в который стекаются все сколько-нибудь стоящие наработки, имеющие отношение к TeX.

Каждый пакет репозитория содержит документацию (с описанием зависимостей, примерами и контекстом использования, деталями реализации и пр.), исходные файлы, описание синтаксических особенностей, макродемонстрацию и другие компоненты, позволяющие быстро понять устройство пакета и порядок работы с ним. С общей структурой пакетов CTAN можно познакомиться на примере пакета для оформления программного кода listings.

Итак, для запуска примеров из настоящего руководства потребуется установить дистрибутив TeX (например, MikTеX) и LaTeX-редактор (например, TeXstudio).

Введение в язык разметки LaTeX

В текстовых процессорах, которые позволяют в момент набора текста видеть его на экране дисплея точно таким, как он будет выглядеть на бумаге, используется концепция визуального проектирования (WYSIWYG – What You See Is What You Get). В LaTeX же используется концепция логического проектирования, когда внешний вид документа становится понятен только после компиляции.

Внешний вид документа определяется инструкциями, описанными на языке разметки TeX или LaTeX. Между этими языками сложно провести четкую границу. Сам LaTeX написан в командах TeX и в LaTeX-документе можно использовать практические любые TeX-команды. Упрощенно можно считать, что язык разметки LaTeX это высокоуровневая «синтаксически-сахарная» обертка для низкоуровневого языка TeX. Таким образом, TeX в основном применяется при разработке классов и пакетов, в то время как LaTeX – для подготовки печатного документа.

Теперь рассмотрим общий синтаксис языка LaTeX и познакомимся с базовой структурой tex-документа.

  • LaTeX-файл должен начинаться с команды \documentclass, задающей стиль оформления документа, например, \documentclass{article}. Аргумент команды article означает, что документ будет оформлен в соответствии с наиболее общими правилами оформления статей. При желании можно изменить (с незначительными оговорками) любой элемент макета документа.
  • Команда \documentclass поддерживает и другие классы, доступные «из коробки»: book (для оформления книг), report (для оформления отчетов – нечто среднее между book и article), proc (для оформления трудов конференций) и letter (для деловых писем со сложной структурой).
  • После команды \documentclass могут следовать команды, относящиеся ко всему документу. Далее с помощью окружения\begin{document}\end{document} (условимся называть его документарным окружением) указывается тело документа. В общем случае окружением называют сложные конструкции вида \begin{}\end{}.
  • Часть tex-файла между командой \documentclass и \begin{document}\end{document} называют преамбулой. Команды указанные после закрывающей скобки окружения документа \end{document} LaTeX проигнорирует.

В итоге наипростейший шаблон документа будет выглядеть так:

base_example.tex
        \documentclass{article}
% область преамбулы
\begin{document} % тело документа: начало
% текст
\end{document} % тело документа: конец
% команды в этой части документа не будут учитываться при сборке документа
    

Инструкции, управляющие макетом страницы и прочие настройки tex-документа, могут быть размещены и в преамбуле, но когда инструкций становится много, будет удобнее вынести их в отдельный стилевой файл с расширением *.sty. Позже стилевой файл можно будет подключить командой \usepackage{}, принимающей в качестве аргумента путь до этого файла.

Сам стилевой файл содержит импорты пакетов \RequirePackage{}, пользовательские команды \newcommand{}, псевдонимы математических операторов \DeclareMathOperator и прочие элементы кастомизации:

style_template.sty
        % начало стилевого файла
\RequirePackage[english,russian]{babel}
\RequirePackage[utf8]{inputenc}
\RequirePackage{amsmath, amsfonts, amssymb, latexsym}
\RequirePackage[
	left=2cm,
	right=2cm,
	top=2cm,
	bottom=2cm
		]{geometry}
... 
\newcommand{\str}[1]{cтр.~\pageref{#1}}
\newcommand{\strbook}[1]{стр.~{#1}}
...
\DeclareMathOperator*{\argmin}{arg\,min}
\DeclareMathOperator*{\argmax}{arg\,max}
\DeclareMathOperator*{\sign}{sign}
\DeclareMathOperator*{\const}{const}
...
    

С подключенным стилевым файлом tex-документ будет выглядеть так:

base_example_with_usepackage.tex
        \documentclass{article}
\usepackage{style_template} % подключаем стилевой файл style_template.sty, расположенный в той же директории, что и tex-файл
\begin{document}
% текст
\end{document}
    

Аргумент команды \usepackage может включать не только имя стилевого файла, но и путь до него относительно корня проекта (без указания расширения файла).

Далее предполагается, что все настройки макета документа описаны в стилевом файле style_template.sty, который расположен в той же директории, что и tex-файл.

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

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

Наберем в документарном окружении следующие строки:

simple_onepage_template.tex
        ...
\begin{document}

\title{Аналитический отчет по ...} % заголовок отчета
\author{\itshape Иванов И.И.} % автор работы
\date{} % просим LaTeX не указывать дату, так как будет
        % использован наш вариант оформления даты, описанный в стилевом файле
\maketitle % создает заголовок

\thispagestyle{fancy} % задает стиль страницы

В этой части можно разместить аннотацию к отчету. \TeX -- это издательская система компьютерной верстки, предназначенная для набора ...

\tableofcontents % создает оглавление

\section{Пример многострочной формулы}
    Для набора сложных многострочных формул используются различные окружения, например, окружение \texttt{multline}
	\begin{multline}
		F_{\zeta}(z)=P[\,\zeta\leqslant z\,] = \int\!\!\!\int_{x/y\leqslant z}f_X(x;n)f_Y(y;m)\,dxdy =\\ \dfrac{1}{2^{(n+m)/2}\Gamma(n/2)\Gamma(m/2)}\int\!\!\!\int_{x/y\leqslant z}x^{n/2-1}y^{m/2-1}\exp\left( -\frac{x}{2} \right) \exp\left( -\frac{y}{2} \right) \,\mathrm{d}x \, \mathrm{d}y.
	\end{multline}

\section{Пример группового размещения формул}

Несколько формул можно разместить в одной группе с помощью окружения \texttt{gather}
\begin{gather}
	\sum_{j \in \mathbf{N}} b_{ij} \hat{y}_{j} = \sum_{j \in \mathbf{N}} b_{ij}^\lambda \hat{y}_j + (b_{ii} - \lambda_i)\hat{y}_i \hat{y},\notag \\
	\det \mathbf{K}(t=1, t_1, \ldots, t_n) = \sum_{I \in \mathbf{n} } (-1)^{|I|} \prod_{i \in I} t_i \prod_{j \in I} (D_j + \lambda_j t_j) \det \mathbf{A}^{(\lambda)} (\, \overline{I} | \overline{I} \,) = 0,\tag{$a$} \\
	\mathbb{F} = \sum_{i=1}^{\left[ \frac{n}{2}\right] } \binom{ x_{i,i+1}^{i^2}}{ \left[ \frac{i+3}{3} \right]} {{\sqrt{\mu(i)^\frac{3}{2} (i^2-1)}} \over\displaystyle {\sqrt[3]{\rho(i)-2} + \sqrt[3]{\rho(i)-1}} }, \tag{$b$}
\end{gather}

\section{Простая однострочная формула}

Теорема Хинчина-Винера утверждает, что спектральная плотность мощности стационарного в широком смысле случайного процесса представляет собой преобразование Фурье от соответствующей автокорреляционной функции
\begin{equation*} % без нумерации
    S_{xx}(f) = \int\limits_{-\infty}^{\infty}  \, r_{xx} (\tau) e^{-j 2 \pi f \tau} \mathrm{d} \tau,\ \text{где}\ r_{xx}(\tau) = \mathbb{E}[\,x(t) \, x^{*}(t - \tau)\,].
\end{equation*}

\end{document}
    

Теперь можно вызвать компилятор PDFLaTeX через командную оболочку и передать ему имя tex-файла. Результат компиляции приведен на рисунке ниже.

Результат компиляции файла <code class="inline-code">analyt_report_template.tex</code>
Результат компиляции файла analyt_report_template.tex

Рассмотрим более подробно код файла analyt_report_template.tex.

  • В первых строках файла с помощью команд \title и \author мы объявляем заголовок отчета и имя автора работы соответственно, а с помощью команды \maketitleсоздаем заголовок.
  • Затем с помощью команды \date, вызванной без аргументов, подавляем вывод временной метки. Здесь для привязки ко времени будет использоваться специальная низкоуровневая командная вставка в стилевом файле style_template.sty (этот фрагмент кода приводится только для справки, так как конечному пользователю приложения нет необходимости вносить в стилевой файл изменения напрямую):
fragment_style_template.sty
        ... 
\def\@maketitle{
    \begin{flushright}
	\footnotesize\itshape
	Дата последней сборки документа:\\ \today\ в \currenttime
    \end{flushright}

    \begin{center}
	\let \footnote \thanks
	\begin{spacing}{1.5}
	    {\Large\bfseries\@title}
	\end{spacing}\vskip 1mm

	{\normalsize
	    \begin{tabular}[t]{l}
		\@author
	    \end{tabular}\par	
	}
    \end{center}
    \par
    \vskip 1.5em
}
...
    

Команда \thispagestyle задает стиль страницы, а команда \tableofcontents создает оглавление документа. С помощью команды \section создаются разделы документа высшего уровня (с учетом класса документа). Для того чтобы создать раздел более низкого уровня следующей ступени можно воспользоваться командой \subsection.

LaTeX обладает широким набором окружений для оформления математических конструкций произвольной сложности, однако здесь мы ограничимся рассмотрением только трех типов окружения: equation, multline и gather.

Как должно быть понятно из приведенного выше рисунка, equation используется для однострочных формул, multline – для многострочных, а gather – для группового размещения формул. Звездочка после имени окружения означает, что LaTeX не станет присваивать номера формулам, попавшим в это окружение.

При наборе формул используются специальные LaTeX-команды с названиями созвучными набираемому элементу, например, для того чтобы добавить в формулу знак суммы с нижним lowindex и верхним upindex индексом используется команда \sum_{lowindex}^{upindex}, чтобы вставить символ большой омеги – \Omega, а для вставки символа гамма потребуется \gamma и т.д.

Для быстрого поиска нужных математических LaTeX-команд, специальных символов, приемов оформления псевдокода и пр. удобно пользоваться компактным, но очень содержательным сборником Константина Воронцова «LaTeX 2e в примерах».

Прочие детали работы с издательской системой LaTeX можно выяснить из следующих работ:

Введение в Python-библиотеку Streamlit

Библиотека Streamlit – это простая, лаконичная и в тоже время очень мощная библиотека для прототипирования браузерных решений с графическим интерфейсом. Streamlit включает поддержку всех основных элементов стека, ориентированных на компьютерное зрение, машинное и глубокое обучение.

Установить библиотеку проще всего с помощью менеджера пакетов pip: pip install streamlit.

Запускается приложение командой streamlit run:

streamlit_run.sh
        $ streamlit run streamlit_simple_example.py

  You can now view your Streamlit app in your browser.

  Local URL: http://localhost:8502 # <-- 
  Network URL: http://192.168.1.247:8502
    

После запуска сценария в браузере (http://localhost:8502) откроется вкладка с приложением. Завершить работу приложения можно, закрыв вкладку браузера и набрав Ctrl+C в командной оболочке.

В Streamlit реализован сравнительно небольшой набор «выразительных средств», но все элементы продуманы и покрывают значительную часть требований к «гибкому динамическому прототипу».

Начать работу со Streamlit можно с вводного руководства, которое подробно описывает все возможности библиотеки, включая Markdown-разметку, аудио и видео объекты, различные графические объекты (Bokeh, Altair, Plotly и др.), а также оптимизацию загрузки объемных наборов данных.

Ниже приводится пример использования библиотеки Streamlit для построения двух интерактивных графиков (на базе библиотеки Plotly) гауссовских процессов с автокорреляционной функцией экспоненциального типа.

streamlit_simple_example.py
        import streamlit as st
import math
import pandas as pd
from pandas import Series
import plotly.graph_objects as go
import numpy as np
import numpy.random as rnd

title_app = "Простой пример использования библиотеки Streamlit"
# чтобы на вкладке браузера отображалось имя приложения, а не имя файла
st.set_page_config(
    layout="wide",
    page_title=title_app,
    initial_sidebar_state="expanded",
)


def gauss_with_exp_acf_gen(
    *,
    sigma: float = 2,
    w_star: float = 1.25,
    delta_t: float = 0.05,
    N: int = 1000,
) -> np.array:
    """
    Описание
    --------
    Генерирует дискретную реализацию
    стационарного гауссовского ПСП
    с КФ экспоненциального типа

    Параметры
    ---------
    sigma : стандартное отклонение ординат ПСП.
    w_star : параметр модели ПСП.
    delta_t : шаг по времени.
    N : число отсчетов ПСП.

    Возвращает
    ----------
    xi : массив элементов ПСП с заданной КФ
    """
    gamma_star = w_star * delta_t
    rho = math.exp(-gamma_star)
    b1 = rho
    a0 = sigma * math.sqrt(1 - rho ** 2)

    xi = np.zeros(N)
    xi[0] = rnd.rand()
    x = rnd.randn(N)

    for n in range(1, N):
        xi[n] = a0 * x[n] + b1 * xi[n - 1]

    return xi


def main(N: int = 100):
    timestmp = np.arange(N)
    # временной ряд №1
    time_series_1 = gauss_with_exp_acf_gen(sigma=5, w_star=1.25, N=N)
    # временной ряд №2
    time_series_2 = gauss_with_exp_acf_gen(sigma=6.5, w_star=1.75, N=N)

    fig = go.Figure()

    fig.add_trace(
        go.Scatter(
            x=timestmp,
            y=time_series_1,
            name="Временной ряд (объект-1)",
            opacity=0.8,
            mode="lines",
            line=dict(
                color="#E84A5F",
            ),
        )
    )

    fig.add_trace(
        go.Scatter(
            x=timestmp,
            y=Series(time_series_1).rolling(window=7).mean(),
            name="Скользящее среднее (объект-1)",
            mode="lines",
            opacity=0.6,
            line=dict(
                color="#FF847C",
            ),
        )
    )

    fig.add_trace(
        go.Scatter(
            x=timestmp,
            y=time_series_2,
            name="Временной ряд (объект-2)",
            mode="lines",
            opacity=0.8,
            line=dict(
                color="#5E63B6",
            ),
        )
    )

    fig.add_trace(
        go.Scatter(
            x=timestmp,
            y=Series(time_series_2).rolling(window=7).mean(),
            name="Скользящее среднее (объект-2)",
            mode="lines",
            opacity=0.6,
            line=dict(
                color="#6EB6FF",
            ),
        )
    )

    fig.update_layout(
        title=dict(
            # text="<i>Временной ряд</i>",
            font=dict(
                family="Arial",
                size=18,
                color="#07689F",
            ),
        ),
        xaxis_title=dict(
            text="<i>Временная метка</i>",
            font=dict(
                family="Arial",
                size=13,
                color="#537791",
            ),
        ),
        yaxis_title=dict(
            text="<i>Продолжительность простоя, час</i>",
            font=dict(
                family="Arial",
                size=13,
                color="#537791",
            ),
        ),
        xaxis=dict(
            showline=True,
        ),
        yaxis=dict(
            showline=True,
        ),
        autosize=False,
        showlegend=True,
        margin=dict(
            autoexpand=False,
            l=70,
            r=10,
            t=50,
        ),
        legend=dict(
            orientation="v",
            yanchor="bottom",
            y=0.01,
            xanchor="right",
            x=0.99,
            font=dict(family="Arial", size=12, color="black"),
        ),
        plot_bgcolor="white",
    )

    st.plotly_chart(fig, use_container_width=True)


if __name__ == "__main__":
    main(N=350)
    

В этом примере к библиотеке Streamlit имеет отношение только:

  • функция st.set_page_config(), которая переопределяет имя вкладки браузера – отображается не имя файла, а имя приложения;
  • функция st.plotly_chart(), которая принимает сконфигурированный Plotly-объект и выводит его в окно браузера.
Результат работы сценария <code class="inline-code">streamlit_simple_example.py</code>
Результат работы сценария streamlit_simple_example.py

Прочие элементы сценария streamlit_simple_example.py играют лишь вспомогательную роль.

Пример-шаблон аналитического отчета

Предлагается в качестве примера, сочетающего приемы работы с библиотекой Streamlit (браузерный интерфейс приложения) и LaTeX-разметку (опорный tex-файл), рассмотреть подготовку отчета на тему «Оценка усталостной долговечности силовых элементов транспортных машин под воздействием стационарных гауссовских процессов с автокорреляционной функцией экспоненциально-косинусного семейства».

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

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

Часто возникает необходимость выгрузить подготовленный с помощью Streamlit файл результатов (например, табличных объект Pandas), но сам Streamlit не предлагает никаких решений. Тем не менее это ограничение можно обойти с помощью следующей функции:

downloader_for_streamlit.py
        def text_downloader(multiline: str) -> NoReturn:
    """
    Принимает LaTeX-шаблон документа в виде многострочной строки и
    создает на странице ссылку для скачивания шаблона
    """
    OUTPUT_TEX_FILENAME = "base_template_for_latex.tex"
    
    b64 = base64.b64encode(multiline.encode()).decode()
    # создает ссылку для скачивания файла
    href = (f'<a href="data:file/txt;base64,{b64}" '
            f'download="{OUTPUT_TEX_FILENAME}">Скачать ...</a>')
    st.markdown(href, unsafe_allow_html=True)
    

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

При наборе текстового шаблона для Python (latex_template_for_python.txt) – по сути файл представляет собой каркас документа с заглушками под строковую интерполяцию – важно помнить о синтаксических особенностях LaTeX. Дело в том, что в контексте строковой интерполяции фигурные скобки означают «место подстановки», а в контексте LaTeX – обязательный аргумент команды. То есть, чтобы Python корректно «прочитал» строку следует фигурные скобки, относящиеся к синтаксису LaTeX, удвоить.

В результате получится текстовый файл вида:

fragment_latex_template_for_python.txt
        \documentclass[
    11pt,
    a4paper,
    utf8,
]{{article}}

\usepackage{{style_template}}

\begin{{document}}
...
Усталостная долговечность по модели \eqref{{eq:miles}} составляет $ Y_{{NB}} = {Y_NB:.2f} $, сек.

\section{{Оценка усталостной долговечности по модели P.H. Wirsching и C.L.~Light}}
...
    

Ниже на рисунках приводится общий вид приложения.

Стартовая страница приложения
Стартовая страница приложения

Скачав подготовленный tex-файл в директорию проекта, останется только запустить компилятор (дважды!).

pdflatex_start.sh
        # для сборки каркаса
$ pdflatex base_template_for_latex.tex
# для вычисления конечных ссылок на страницы, формулы и пр.
$ pdflatex base_template_for_latex.tex

    

После сборки документа в рабочей директории проекта будет создан pdf-файл.

Выгрузка подготовленного LaTeX-шаблона
Выгрузка подготовленного LaTeX-шаблона
Результат работы <code class="inline-code">pdflatex base_template_for_latex.tex</code>. Фрагмент собранного аналитического отчета
Результат работы pdflatex base_template_for_latex.tex. Фрагмент собранного аналитического отчета

Чтобы развернуть приложение на свободной облачной платформе Streamlit, достаточно кликнуть на Deploy this app в правой верхней части панели запущенного приложения, как изображено на рисунке. Однако предварительно необходимо зарегистрироваться на Streamlit и отправить заявку на допуск к ресурсам (обычно обработка заявки занимает несколько дней).

Развертывание приложения на облачной платформе Streamlit
Развертывание приложения на облачной платформе Streamlit

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

Заключение

Из руководства вы узнали:

  • что собой представляет система компьютерной вёрстки LaTeX, где найти ее дистрибутивы и пакеты, как правильно читать документацию, как начать работать с системой;
  • как разрабатывать гибкие, масштабируемые шаблоны документов с помощью языка разметки TeX/LaTeX;
  • как использовать приемы разработки приложений с помощью Python-библиотеки Streamlit;
  • как может выглядеть пример связки «LaTeX + Streamlit»;
  • и, наконец, как развернуть приложение на облачной платформе Streamlit.

Полезные источники:

Связные материалы с платформы Proglib:

Комментарии

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ