Автоматизация сборки LaTeX-документов

Big talk о некоторых аспектах автоматизированной генерации документов. В ролях: LaTeX, Metapost, GNU Emacs, Make, m4, bash :-) Здесь не написано о том, как пользоваться Латехом, Метапостом, Емаксом, как писать мейкфалы, сценарии командной оболочки и не рассказано о хитростях макропроцессора m4. Текст длинный и справочно-унылый, читать долго.

Конкретно, описано следующее:

  • как вставлять (удобно) куски исходников в LaTeX-документ

  • как (автоматически) внедрять (с удобством) результаты численных расчётов в документ

  • как организовать простую визуализацию данных с включением графиков в документ

Prelude: документирование кода

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

Генераторы справочников

Популярным решением для связывания исходного кода и документации являются утилиты типа Doxygen (C, С++, Java, PHP и т. д.), gtk-doc (используется в проекте Gtk+), SchemeDoc (аналогичное Doxygen решение для Scheme), Texinfo и десятки других. Их сущность — в написании специально оформленных комментариев в исходном коде, которые впоследствии обрабатываются программой-документером, в результате чего получается хорошо оформленная справочная документация по программному интерфейсу (см. к примеру доки по PyGTK). Такая документация неизменно носит оттенок механоидности (хотя это не всегда минус), но очень удобна в генерации и использовании. Doxygen, кстати, ещё может при помощи Graphviz рисовать графы зависимостей между структурами кода.

«Ъ™ программирование»

Более высокий уровень — «Literate Programming» («грамотное программирование» («глупый» перевод «литературное» тоже неплохо отражает сущность)): ведущим принципом в написании исходного кода программы является его читабельность и понятность человеку, документация и компилируемый код генерируются из высокоуровневого описания — это путь, который используют различные отпрыски WEB (сегодня, в частности, на Web2C написан TeX, Metafont). Подход не только грандиозно увеличивает понимаемость и читабельность программы, но и переносит сам процесс программирования на более высокий уровень, заодно делая его ещё более осмысленным.

Курсовая работа

В этом семестре я выполнил небольшую курсовую работу по дифференциальным уравнениям. В качестве языка реализации — Scheme. Результатом программы является приближённое решение u(x) уравнения d²u/dx² + n(x)u(x) = 0 (представленное в виде набора точек графиков действительной и мнимой части u(x)) и ещё два коэффициента, которые нужно по задаче найти. К реализации предлагается два метода решения задачи (описать надо оба, заодно проверив друг другом результат каждого из них).

Оформить всё я решил при помощи LaTeX.

Требования

  1. Результат работы нужно (графики u(x) и два числа) представить в отчете по курсовой.

  2. Сорцы программы нужно включить в курсач (на бумаге) (тупость, но так надо; у меня не очень большой исходник получился)

  3. Исходные данные (определение заданной по условию функции n(x) на Лиспе и ещё некоторые числа) я просто записывал в отдельный файл «постановки задачи». Он используется при вычислениях, и из него же можно извлечь исходные данные и включить их в бумажный отчёт. Тогда можно будет просто поменять постановку задачи (один файлик), набрать make doc и получить отчёт по курсовой работе для новых исходных данных без каких-либо дополнительных действий руками. Итак, ещё одна нужность — выдернуть из постановки задачи нужные чиселки и включить их в LaTeX-документ.

  4. Для развёрнутого комментирования работы программы мне так же понадобилась возможность простого включения в LaTeX определения произвольной функции из произвольного файла в моём проектике.

Основным принципом является связывание бумажного отчёта и исходного кода программы (а так же её исходных данных и результатов) так, чтобы любое изменение программы немедленно и корректно отражалось в документации. В целом для обеспечения обновления определённых частей бумажного отчёта при изменении соответствующих аспектов программного кода использовалась сборочная система GNU make.

Файлы проекта полностью доступны в моём Mercurial-репозитории.

Доступен и скомпилированный мной PDF с курсачом.

Texdepend

Texdepend — небольшая утилита на Perl, являющаяся аналогом makedepend для TeX-исходников. Как makedepend(1) распознаёт в исходниках на Си директивы #include, так texdepend(1) рекурсивно обрабатывает конструкции \input{}, \include{}, \usepackage{}, \includegraphics{} и другие. Из всех обрабатываемых texdepend конструкций мне потребовались две — \input{} и \includegraphics{}.

В целом

Общий подход, который был применён для выполнения вышеобозначенных требований — в исходнике на LaTeX пишется конструкция \input{} с аргументом определённого вида, на исходник кастуется вызывается texdepend, который выводит список «зависимостей». Полученные зависимости формируются согласно своему типу, после чего движок TeX просто включает сформированные файлы в документ.

Для управления процессом был написан Makefile + несколько простых и коротких скриптов на bash для генерации зависимостей.

В самом начале моего Makefile я написал:

include ${DOCNAME}-deps.mk

А в списке возможных целей описал правило сборки:

${DOCNAME}-deps.mk: ${DOCNAME}.tex
    texdepend -o $@ -print=if $<

Команда texdepend -o $@ -print=if $< генерирует списки зависимостей для файла ${DOCNAME.tex}, рассовывая их по переменным ${INCLUDES}, ${FIGS} и т. п. Включая при помощи include сгенированный файл в свой Makefile я получаю возможность указать эти переменные в зависимостях PDF-ки с курсачом:

${DOCNAME}.aux: ${INCLUDES} ${FIGS} ${DOCNAME}.tex ${DOCNAME}.bib
    pdflatex ${DOCNAME}
    bibtex8 -B ${DOCNAME}

${DOCNAME}.pdf: report.aux
    pdflatex ${DOCNAME}
    pdflatex ${DOCNAME}

doc: ${DOCNAME}.pdf

Итак, когда Make получает задание обновить цель doc, по зависимостям обновляется report.aux, а следовательно и списки зависимостей из ${INCLUDES}.

Остаток Makefile составляют, собственно, правила для сборки зависимостей. О них и пойдёт речь.

Кроме того, другим обобщением здесь является сходность всех правил сборки этих зависимостей: сценарии shell подготавливают данные для включения и при помощи макропроцессора m4 подставляют их в шаблоны. Полученное и внедряется в LaTeX-документ!

Детали по порядку

Включение исходных файлов целиком

Конкретизирую задачу. Допустим, я хочу, чтобы, написав в нужном мне месте моего документа конструкцию

\input{matrices.scm-full-listing}

получить в этом самом месте красиво оформленный полный листинг файла matrices.scm из моего проекта. Кроме того, я хочу уметь ссылаться на этот листинг из других мест моего документа при помощи \pageref{matrices.scm-full-listing}.

Listings: оформление кода в LaTeX

listings — замечательный пакет для LaTeX, дающих кучу возможностей по оформлению включаемых в документ исходников. Умеет подсвечивать синтаксис или выбранные лексемы в сорце, рисовать разные гламурные рамки, добавлять индексные ссылки в листинг и многое другое. Для включения исходника целиком я использовал определяемое этим пакетом окружение lstlisting:

\begin{lstlisting}[label={__BASENAME-full-listing},title={Содержимое файла \filename{__BASENAME}}]
__SOURCE
\end{lstlisting}

Параметр label создаёт в месте этого листинг метку, на которую потом можно будет ссылаться при помощи \pageref{__BASENAME-full-listing}. Происхождение строчек __BASENAME и __SOURCE пока неясно, хотя смысл их должен быть очевиден — это имя включаемого файла и его содержимое. Механизм подмены этих имён реальными значениями рассмотрен далее.

Включаем!

Конструкция \input{matrices.scm-full-listing} посредством texdepend породит на уровне make необходимость обновления цели matrices.scm-full-listing.tex; для таких целей в моём Makefile прописано такое правило с действием по маске («Pattern rule») такого вида:

%-full-listing.tex: ${SOURCEDIR}/% \
                    source-full-listing.sh source-full-listing.tpl.tex
    $(SHELL) source-full-listing.sh $< > $@

Единственная команда в списке правил обновления цели вызывает простой шелл-скрипт source-full-listing.sh с аргументом, равным полному имени включаемого файла. Сам скрипт выглядит так:

SOURCE=$(cat $1 | grep -v "[ ]*;; .*" | sed -e "s/^\([ ]*\);;@ /\1;; /")

BASENAME=$(basename $1)

m4 --define="__SOURCE"="${SOURCE}" \
   --define="__BASENAME"="${BASENAME}" \
   source-full-listing.tpl.tex

Его действие понятно: при помощи m4(1) он определяет два макроса __SOURCE и __BASENAME, и выполняет замену их вхождений в файле source-full-listing.tpl.tex на содержание исходника и его имя, соответственно. Содержимое source-full-listing.tpl.tex — как раз вышеприведённый фрагмент с \begin{lstlisting} .. \end{lstlisting}.

Видно, что решения этой задачи почти нет — он состоит из пяти простых строчек. На самом деле, самое главное — в этой строке из source-full-listing.sh:

SOURCE=$(cat $1 | grep -v "[ ]*;; .*" | sed -e "s/^\([ ]*\);;@ /\1;; /")

По странному стечению обстоятельств, я пишу комментарии к коду на английском, что, вероятно, может некоторым образом смутить преподавателя. Простейшим из универсальных языков является язык математики, поэтому я применил такое правило: обычные комментарии пишутся с ;;, а особые «математические» — с ;;@.

Математический комментарий оформляется напрямую теховским окружением $ .. $. При работе по включению сорца в TeX, обычные комментарии тупо вырезаются, а математические — оставляются. При этом в преамбуле документа указана команда \lstset{mathescape=true}, заставляющая пакет listings полностью обрабатывать (а не просто «дословно» включать) выделенные при помощи $ .. $ части кода. Например, при включении исходника со следующим текстом:

;; Check whether found a, b coefficients meet the conservation of
;; energy law:
;;@ $|A|^2 + |B|^2 = 1$
(define (energy-conserves? A B eps)
  (< (abs (- 1
             (+ (expt (magnitude A) 2)
                (expt (magnitude B) 2))))
     eps))

Комментарии с ;; просто удалятся, а формула |A|²+|B|²=1 корректно заверстается в математическое окружение.

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

Извлечение отдельных участков кода

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

\input{general-horner-eval__shared.scm-tag-listing}

И получить в этом месте содержимое функции general-horner-eval из файла shared.scm (в дальнейшем описании для примера я буду использовать именно эту функцию из этого файла)

Texdepend обработает эту конструкцию, потребовав от сборочной системы обновления зависимости general-horner-eval__shared.scm-tag-listing.tex. В правиле сборки можно указать соответствующих файл с исходником (shared.scm) в качестве зависимости — тогда листинг будет обновляться при изменениях в коде. Оформлять листинги можно при помощи уже названного listings.

Остаётся одно — как-то вытащить из определённого файла тело определённой функции. Дальнейшие манипуляции с ним и оформление — дел нехитрой техники.

Сначала я посмотрел на Scheme Elucidator — ещё одно решение для документирования кода на Scheme: в отличие от того же SchemeDoc, Elucidator не просто генерит доки по интерфейсу, а плотно связывает особенности вычислительного процесса с документацией. Изначально я собирался использовать это решение для курсача, однако отказался по нескольким причинам (пришлось бы перелопачивать вывод Elucidator (это HTML) в TeX; LAML, на котором Elucidator основан, не совсем удобен и негибок в установке; лишние значительные зависимости; исходная задача слишком простая для Elucidator).

Однако потом я вдруг вспомнил, что работаю в Emacs, где есть Semantic, так что написал коротенькую программку на Emacs Lisp с использованием Семантика, которая просто выводит на стандартный вывод содержимое заданного тега в заданном файле. Использование Semantic для этой задачи раскрывается в другой моей записи, сейчас будем считать, что команда

$ emacs --batch --load grok-lisp.el \
  --exec '(print-tag-from-file "general-horner-eval" "shared.scm")' \
  2> /dev/null

подаёт на стандартный вывод содержимое функции general-horner-eval в файле shared.scm.

Я уже условился называть файлы, содержащие тела процедур для включения, по схеме function__file-tag-listing.tex, так что получается вот такое правило в Makefile:

%-tag-listing.tex: ${SOURCEDIR}/$$(call get-target-source,$$*) \
                   tag-listing.sh tag-listing.tpl.tex grok-lisp.el
    $(SHELL) tag-listing.sh $(shell echo $* | sed -e "s/__.*//") $< > $@

Первая зависимость в этой хитроумной комбинации вычленяет из имени цели general-horner-eval__shared.scm-tag-listing.tex часть shared.scm (имя исходника, откуда берётся функция — для обновления листинга при изменении сорца). Функция get-target-source задаётся в Makefile выше по тексту таким образом:

define get-target-source
$(shell echo $1 | sed -e "s/.*__//")
endef

Остальные зависимости заставляют обновлять цель при изменении самих механизмов обновления, а собственно правило сборки вызывает команду

tag-listing.sh general-horner-eval shared.scm

В свою очередь, tag-listing.sh при помощи Emacs извлекает определение нужной функции из файла и подставляет его (при помощи m4(1)) вместо строки __SOURCE в файл-шаблон tag-listing.tpl.tex, общий для всех листингов и имеющий такое содержание:

\begin{lstlisting}[frame=tbrl,frameround=tttt,aboveskip=0.5cm, belowskip=0.5cm]
__SOURCE
\end{lstlisting}

Он сходен с уже рассмотренным ранее шаблоном для включения сорца целиком и тоже использует пакет listings.

Ввиду указанного в Makefile перенаправления > $@ в конце вызова tag-listing.sh, извлечённая из исходника и обрамлённая в конструкции LaTeX функция попадает в general-horner-eval__shared.scm-tag-listing.tex, который и включается в документ.

В итоге, вся цепочка вызовов порождается лишь самим существованием команды

\input{general-horner-eval__shared.scm-tag-listing.tex}

в TeX-исходнике.

Нельзя сказать, что применение Elisp здесь обязательно — того же эффекта получения кода произвольной функции можно добиться и при помощи утилиты etags(1) + тех же grep(1), head(1) и sed(1); однако выразительность такого решения была бы меньше. Кроме того, код на Elisp очень быстро пишется и быстро отлаживается.

Lisp2TeX

После решения задачи на Elisp я обнаружил Lisp2TeX — похожее решение ровно для таких же целей — включать определённые части кода на Scheme в TeX при помощи удобных конструкций. L2T очень гибок и позволяет не просто включать сырец в TeX, но и делать pretty-print листинга (красиво расставлять отступы), и включать результаты вычисления произвольных S-выражений в него, и даже визуализировать S-выражения при помощи pic — это просто замечательный уровень работы. Кроме того, L2T умеет приводить включённые выражения Лиспа в ещё более читабельный формат (превращать (f x y) в \varphi(x, y) и гораздо более сложные выражения гибко преобразовывать по шаблонам).

Без сомнения, Lisp2TeX в связке с оформлением из уже упомянутого LaTeX-пакета listings даёт гораздо более качественное решение, нежели описанные выше костыли на основе Emacs+Semantic и обёрток на shell. Lisp2TeX имеет возможность привязки к себе любых парсеров, что делает его пригодным к использованию с сорцами на любых языках, а не только всяких Лиспах.

Впрочем, я в итоге не стал сейчас юзать L2T. Имелись проблемы с компиляцией Lisp2TeX (плюс он подвязан на определённые реализации Scheme), и мне нравилось использовать texdepend для получения списка нужных для сборки курсача файлов данных, а так пришлось бы юзать и Lisp2Tex, и texdepend.

Буду писать большой серьёзный папир, обязательно применю Lisp2TeX, а сейчас он показался мне лишним — да и в решении через Elisp основные сложности лишь в написании правила для Makefile (остальные скрипты там по две строки максимум). Да, мне стыдно и у меня легкая форма NIH-синдрома.

Включение исходных данных в отчёт

Это совсем тупая мини-задача, но её решение я тоже опишу.

В проекте исходные данные хранятся в отдельных текстовых файлах и содержат определение одной-единственной функции, необходимой для расчёта, а также несколько определений числовых параметров. Определение функции на Scheme также предварено TeX-комментарием (о которых я писал выше), который также предполагается вставлять в документ. Выглядит такой файл примерно так:

;;@ $n(x) = \begin{cases} 35+3(x-1)^2 & 0<x<2\\ 36 & x \leq 0,\ x \geq 2 \end{cases}$
(define (f x)
  (if (and (> x 0) (< x 2))
      (+ 35 (* 3 (expt (- x 1) 2)))
      36))

(define right-bound 2)
(define subintervals 100)
(define test-epsilon 0.0001)

Вставка нужного текста в документ происходит по команде \input{statement.scm-initial-data}. Получается что-то вроде:

screen1196171299

Созданию требуемого файла отвечает следующее правило Makefile:

%-initial-data.tex: ${SOURCEDIR}/% initial-data.sh initial-data.tpl.tex
    $(SHELL) initial-data.sh $< > $@

Видно, что маленький скрипт initial-data.sh всего лишь натравливается на файл с исходными данными. При помощи cat(1), grep(1), sed(1) он втупую извлекает две цифирьки и размеченное TeX-ом определение функции. Его работа завершается вызовом m4(1) на очередной шаблон:

Рассматривается отрезок $[0; __RIGHTBOUND]$.

Функция показателя преломления среды $n(x)$ задана следующим
образом:
\begin{equation}\label{__LABEL}
__NTEX
\end{equation}

И вновь при помощи \label{__LABEL} генерируется метка вида file.scm-initial-data, на которую я потому могу сослаться.

Включение результатов расчёта в текст работы

Тут снова пригодился бы Lisp2TeX, но непонятно, как он обработает код с явными Guile-измами (я использовал для реализации Guile) и помимо включаемых напрямую в TeX-документ двух несчастных коэффициентиков мне требовалось включить в него же созданный промежуточным преобразованием график, отрисованный Metapost.

Расчёты тривиальные и нересурсоёмкие, поэтому лучшее решение — самое простое: выводить результаты (построенное решение и информацию о нём) на стандартный вывод в формате, удобном для обработки обычными средствами текстового преобразования (grep(1), tail(1), sed(1)), разрезать этот вывод на удобные кусочки, которые впоследствии передаются в каком-то виде на дальнейшую обработку:

  • приближённое решение в виде трёх столбцов вида x Re(u(x)) Im(u(x))

  • два искомых по условию задачи числа A и B

  • некоторые дополнительные данные, также обрабатываемые и включаемые в отчёт

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

Как говорит расчётная программа

Сначала идут три столбика (аргумент, действительная часть значения, мнимая часть значения) из пары сотен строк. Они скучные, поэтом я привожу лишь последние строки вывода расчётной программы. Блоки разделяются классическим %%.

1.9452736318408 0.642259739680568 -0.761159271604867
1.95522388059701 0.687848180515163 -0.721539051510589
1.96517412935323 0.730907823000578 -0.679195358916038
1.97512437810945 0.771268488932788 -0.634284101266255
1.98507462686567 0.808769344860717 -0.586971358440383
1.99502487562189 0.843259561005711 -0.537432806000248
%%
A: -0.00478-0.00750i
B: 0.99996-0.00104i
conserves: yes

eps: 0.0001
method: fundmatrix
time: 2.987552

Текстовый обмен данными прозрачен и прост. Используйте простой текст!

«Где же вы, числа?»

Я пишу в своём документе:

\input{fundmatrix__statement.scm-results}

Это означает: вставить результаты расчёта методом fundmatrix с исходными данными statement.scm. Это позволяет без лишних движений вставлять в документ результаты расчётов с разными исходными данными и разными методами.

В итоге вставляется текст такого вида (я скопировал этот отрывок из финальной PDF-ки):

В результате использования метода были получены следующие результаты: A = −0.00478 − 0.00750i (4.10) B = 0.99996 − 0.00104i Подтверждено, что значения A и B удовлетворяют условию 2.1: 2 2 |A| + |B| − 1 < 0.0001 Расчёт занял 2.987552 с.

В Makefile на эту тему представляют интерес два правила:

%-results: ${SOURCEDIR}/$$(call get-target-source,$$*) \
           ${SOURCEDIR}/$$(call get-target-method,$$*)-solution.scm \
           ${SHARED_SOURCES} results.sh
    $(SHELL) results.sh $< $(call get-target-method,$*) dispatcher.scm ${SOURCEDIR} > $@

%-results.tex: %-results %-plot.mps texify-results.sh results.tpl.tex
    $(SHELL) texify-results.sh $< > $@

Из этих двух первая цель описывает зависимость второй. Первая цель довольно скучная, достаточно сказать лишь о том, что она запихивает в файл с именем вида method_statement.scm-results результаты работы расчётной программы (пример этих результатов приводился выше) с методом method и исходными данными statement.scm. А вторая цель, как видно, уже на этот файл натравливает пока неизвестный скрипт texify-results.sh, который гламурно оформляет результаты расчёта.

Принцип его работы уже знаком: извлечь нужные данные, при помощи m4(1) подставить их в шаблон results.tpl.tex, который состоит из следующих букв:

define(`__CONSERVE_yes', `Подтверждено')
define(`__CONSERVE_no', `Не подтверждено')

В результате использования метода были получены следующие результаты:
\begin{equation}\label{__LABEL}
  \begin{aligned}
    &A=__A \\
    &B=__B
  \end{aligned}
\end{equation}

\emph{__CONSERVE_STATUS}, что значения $A$ и $B$ удовлетворяют условию
\ref{energy}:

\begin{displaymath}
  \abs{\left( \abs{A}^2 + \abs{B}^2 \right) - 1} < __EPS
\end{displaymath}

Расчёт занял __TIME с.

Сам скрипт texify-results.sh определяет макросы __A, __B, __TIME, __EPS, значения которых извлекаются из вывода расчётной программы (строки на A:, B:, time:, eps: соответственно). Их семантика очевидна. Макрос __LABEL заменяется на выражение вида method__statement.scm-results, так что потом в документе я могу сослаться на конкретно эти результаты при помощи \eqref или \pageref.

Любопытен метод подстановки словосочетания «Подтверждено»/«Не подтверждено» в зависимости от результатов расчёта (строчка conserves: yes): __CONSERVE_STATUS раскрывается в один из двух других макросов __CONSERVE_yes или __CONSERVE_no, которые уже содержат желанные слова на русском языке. Это сделано для того, чтобы вся информация об оформлении полностью содержалась в шаблонной части.

Графики решений с MetaPost

Metapost является замечательным средством для подготовки различной графики для LaTeX и вообще основанных на TeX решениях (впрочем, может использоваться и отдельно). Макропакет mpgraph для Metapost позволяет без труда выводить несложные графики по заданному набору точек.

Семантика включения графика решения немного отличается от уже отработанной схемы «texdepend — make — скрипты».

В исходнике отчёта о курсовой работе задана удобная команда \includeplot:

\newcommand{\includeplot}[2]{\begin{figure}[hb]
    \centering
    \includegraphics{#1__#2-plot.mps}
    \caption{График $u(x)$ для функции преломления \eqref{#2-initial-data}}
\end{figure}}

Это сделано исключительно во избежание дублирования одинаковых конструкций в исходнике. Кстати, в подпись в картинке внедряется ссылка на ранее включённые в документ исходные данные, которым соответствует график: \eqref{#2-initial-data}.

Включение графика решения, построенного при помощи метода, скажем fundmatrix с исходными данными из statement.scm, осуществляется так:

\includeplot{fundmatrix}{statement.scm}

Обычный texdepend(1) уже не может отследить такую зависимость. Можно, однако, поступить так: график логично включать в курсач после численных результатов расчёта. Как раз при включении этих численных результатов можно генерировать и график (указав его в качестве зависимости цели %-results.tex). Тогда конструкция с \includeplot успешно найдёт картинку в файлике с именем вида method__statement.scm-plot.mps. Итак, правила для его генерации таковы:

%-results.tex: %-results %-plot.mps texify-results.sh results.tpl.tex
    $(SHELL) texify-results.sh $< > $@

%-plot.mps: %-results plot-results.sh plot.tpl.mp
    $(SHELL) plot-results.sh $<

Видно, что plot-results.shсценарий, обрабатывающий всё тот же текстовый вывод работы расчётной программы. Однако, в то время как texify-results.sh интересовался разными данными после разделителя %%, plot-results.sh концентрируется на куче чисел до него. Принцип же работы этого скрипта уже знаком: он всего-навсего заставляет m4(1) подготовить результаты расчётной программы к вычерчиванию (а именно, отрезать всё после %% :-) и выполнить несколько макроподстановок в Metapost-шаблоне, которые я перечислю далее.

Во-первых, нужно заставить Metapost давать выходному файлу желаемое имя изменением переменной filenametemplate:

filenametemplate "__PLOT_PREFIX.mps";

Макрос __PLOT_PREFIX определяется в управляющем сценарии plot-results.sh и заменяется на требуемую по условию строку вида method_file.scm-plot, так что mpost(1) на выходе даст картинку с нормальным именем, которую впоследствии увидит \includeplot в моём LaTeX-исходнике.

На самом деле, шаблон plot.tpl.mp заточен под вывод графиков произвольного количества функций в одной области. Мне это также потребовалось для курсовой — в конце нужно сопоставить результаты вычислений разными методами.

Далее, cобственно вычерчиванием графиков занимается следующий Metapost-код:

for file = __DATA:
  color real_color, imag_color;
  path real_plot, imag_plot;

  real_color = blue*(0.5+uniformdeviate(5)/10)+green*uniformdeviate(2)/10+white*uniformdeviate(15)/100;
  imag_color = red*(0.5+uniformdeviate(5)/10)+white*uniformdeviate(15)/100;

  gdata(file, f,
    augment.real_plot(f1, f2);
    augment.imag_plot(f1, f3););

  gdraw real_plot withpen __PEN withcolor real_color;
  __LABEL(glabel.ulft(btex $\Re$ etex, 1) withcolor real_color*0.8;)

  gdraw imag_plot withpen  __PEN withcolor imag_color;
  __LABEL(glabel.llft(btex $\Im$ etex, 1) withcolor imag_color*0.8;)

endfor;

В глаза здесь сразу бросаются другие три макроса:

  1. __DATA, который заменяется на список файлов с описанием данных для вычерчивания (или, в рассматриваемом случае, одно-единственное имя файла с уже знакомыми столбцами x Re(u(x)) Im(u(x))).

  2. __PEN определяется в зависимости от количества вычерчиваемых наборов точек (когда строится один график, линия жирная, когда много — тоньше). Применяется такой же подход, как в случае с макросом __CONSERVE_STATUS в шаблоне для вывода численных результатов счёта: __PEN раскрывается в один из двух других макросов __PEN_single или __PEN_multi, которые определены уже в самом шаблоне для разделения работы по подготовке данных и непосредственному оформлению графика:

    define(__PEN_single',pencircle scaled 1.5’) define(__PEN_multi',pencircle scaled 0.8’)

  3. __LABEL, по аналогии с __PEN, определяется как нулевой (раскрывающийся в пустоту) макрос, если я вывожу несколько графиков, и как тождественный (раскрывающийся в свой аргумент без изменений), если я вывожу график одного решения.

Конструкция

 gdata(file, f,
   augment.real_plot(f1, f2);
   augment.imag_plot(f1, f3););

построчно читает из файла строки, записывая элементы первого и второго столбца как (x, y) в первую кривую, а элементы первого и третьего — во вторую кривую. Получаются две кривые, соответствующие действительной и мнимой части вычерчиваемого решения. После этого две команды gdraw выводят собранные кривые в область построения.

Адаптируйте!

Я использовал LaTeX, Scheme, Metapost, однако описанные подходы применимы и к совершенно другим комбинациям средств:

  • я мог бы автоматизировать генерацию документа на любом другом языке, будь то XHTML или Texinfo (для них было бы нетрудно написать свои аналоги Texdepend)

  • я мог бы вставлять в свой документ гораздо более сложные результаты расчётов, полученные не простой программой на Scheme, а при помощи мощных средств численного анализа вроде Octave

  • я мог бы и использовать другое средство построения графиков: gnuplot, ePiX, pgf, PyXPlot. Главное, чтобы поддерживался простой текстовый командный интерфейс

Уроки простоты

Всё описанное очень просто в реализации благодаря использованию прозрачных и открытых форматов данных. LaTeX — это простой текст. Описания картинок MetaPost — простой текст. С простым текстом легко работать, его легко создавать простыми средствами и изменять при помощи, скажем, m4. Посчитал — разрезал вывод, отфильтровал нужное — подставил в шаблон — заинклудил в документ. А текстовый доступный Makefile позволяет быстро понять структуру процесса сборки документа.

Однако, говоря «простой», я не подразумеваю «лёгкий» :-)

Наверное, как-то так: UNIX — это просто, но не легко!

git.md