Commit fbb9e437 authored by Alexandre Duret-Lutz's avatar Alexandre Duret-Lutz

dot: add x option for dot2tex

* spot/twa/acc.cc, spot/twa/acc.hh: Add a LaTeX output for acceptance
conditions.
* spot/twaalgos/dot.cc: Implement the 'x' option and refactor the code
a bit to limit duplication.
* tests/core/dot2tex.test: New test case (requires dot2tex).
* tests/Makefile.am: Add dot2tex.test.
* tests/core/alternating.test, tests/core/readsave.test,
tests/python/automata-io.ipynb: Adjust expected output.
* NEWS, doc/org/oaut.org: Mention the new option.
parent b242122c
......@@ -75,6 +75,11 @@ New in spot 2.3.5.dev (not yet released)
We plan to enable 'a' by default in a future release, so a new
option 'A' has been added to hide the acceptance condition.
- The print_dot() function has a new experimental option 'x' to
output labels are LaTeX formulas. This is meant to be used in
conjunction with the dot2tex tool. See
https://spot.lrde.epita.fr/oaut.html#dot2tex
- A new named property for automata called "original-states" can be
used to record the origin of a state before transformation. It is
currently defined by the degeneralization algorithms, and by
......
......@@ -579,6 +579,8 @@ digraph G {
}
#+end_example
** Converting dot output to images or pdf
This output should be processed with =dot= to be converted into a
picture. For instance use =dot -Tpng= or =dot -Tpdf=.
......@@ -589,6 +591,8 @@ $txt
#+RESULTS:
[[file:oaut-dot1.png]]
** Customizing the dot output
This output can be customized by passing optional characters to the
=--dot= option. For instance =v= requests a vertical layout (instead
of the default horizontal layout), =c= requests circle states, =s=
......@@ -866,6 +870,63 @@ export SPOT_DOTDEFAULT='Brf(Lato)C(#ffffa0)'
export SPOT_DOTEXTRA='edge[arrowhead=vee, arrowsize=.7]'
#+END_SRC
** Working with =dot2tex=
:PROPERTIES:
:CUSTOM_ID: dot2tex
:END:
The [[https://github.com/kjellmf/dot2tex][=dot2tex= program]] interacts with GraphViz to converts dot files
into TeX figures. The layout is still done by tools provided by
GraphViz (i.e. =dot=, =neato=, =circo=, ...) but the actual rendering
is done using LaTeX with the TikZ or PSTricks packages. One advantage
is that this allows embedding math formulas into the graph, something
GraphViz alone cannot do. Another advantage is that you can then
easily edit the LaTeX figure, for instance to add additional graphical
elements.
The =dot= formater of Spot has an option =x=, that is convenient to
use with =dot2tex=. This option causes labels to be rendered as LaTeX
mathematical formulas instead of ASCII text.
#+BEGIN_SRC sh :exports code
ltl2tgba 'p0 U p1' --dot=x | dot2tex --autosize --nominsize > out.tex
#+END_SRC
The above command should give you a LaTeX file that compiles to the
following figure:
#+BEGIN_SRC sh :results silent :exports results
ltl2tgba 'p0 U p1' --dot=x | dot2tex --autosize --nominsize > dot2tex.tex
latexmk --pdf dot2tex.tex
convert -density 150 -trim dot2tex.pdf dot2tex.png
latexmk -C dot2tex.tex
rm -f dot2tex.tex
#+END_SRC
[[file:dot2tex.png]]
Caveats:
- =dot2tex= should be called with option =--autosize= in order to
compute the size of each label before calling GraphViz to layout the
graph. This is because GraphViz cannot compute the correct size of
mathematical formulas. Unfortunately, the release of =dot2tex
2.9.0= contains a bug where sizes are intepreted as integers instead
of floats. This can cause labels or shapes to disappear. This bug
of =dot2tex= was fixed in 2014, but at the time of writing
(summer 2017) no new release of =dot2tex= has been made. To work around this,
make sure you install =dot2tex= from its git repository:
#+BEGIN_SRC sh
git clone https://github.com/kjellmf/dot2tex.git
cd dot2tex
sudo python setup.py install
#+END_SRC
- The default size of nodes seems slightly too big for our usage.
Using =--nominsize= is just one way around it. Refer to the
[[https://dot2tex.readthedocs.io/en/latest/][=dot2tex= manual]] for finer ways to set the node size.
- Currently the =x= option of Spot's =--dot= output cannot yet be
combined with the =r=, =R=, an =b= options used to display colored
bullets. (Patches are welcome.)
* Statistics
:PROPERTIES:
:CUSTOM_ID: stats
......
......@@ -63,20 +63,45 @@ namespace spot
os << v;
}
template<bool html>
enum code_output {HTML, TEXT, LATEX};
template<enum code_output style>
static void
print_code(std::ostream& os,
const acc_cond::acc_code& code, unsigned pos,
std::function<void(std::ostream&, int)> set_printer)
{
const char* op = " | ";
const char* op_ = style == LATEX ? " \\lor " : " | ";
auto& w = code[pos];
const char* negated = "";
const char* negated_pre = "";
const char* negated_post = "";
auto set_neg = [&]() {
if (style == LATEX)
{
negated_pre = "\\overline{";
negated_post = "}";
}
else
{
negated_pre = "!";
}
};
bool top = pos == code.size() - 1;
switch (w.sub.op)
{
case acc_cond::acc_op::And:
op = html ? " &amp; " : " & ";
switch (style)
{
case HTML:
op_ = " &amp; ";
break;
case TEXT:
op_ = " & ";
break;
case LATEX:
op_ = " \\land ";
break;
}
SPOT_FALLTHROUGH;
case acc_cond::acc_op::Or:
{
......@@ -90,8 +115,8 @@ namespace spot
if (first)
first = false;
else
os << op;
print_code<html>(os, code, pos, set_printer);
os << op_;
print_code<style>(os, code, pos, set_printer);
pos -= code[pos].sub.size;
}
if (!top)
......@@ -99,14 +124,17 @@ namespace spot
}
break;
case acc_cond::acc_op::InfNeg:
negated = "!";
set_neg();
SPOT_FALLTHROUGH;
case acc_cond::acc_op::Inf:
{
auto a = code[pos - 1].mark.id;
if (a == 0U)
{
os << 't';
if (style == LATEX)
os << "\\mathsf{t}";
else
os << 't';
}
else
{
......@@ -115,16 +143,32 @@ namespace spot
top = code[pos - 1].mark.count() == 1;
unsigned level = 0;
const char* and_ = "";
const char* and_next_ = []() {
// The lack of surrounding space in HTML and
// TEXT is on purpose: we want to
// distinguish those grouped "Inf"s from
// other terms that are ANDed together.
switch (style)
{
case HTML:
return "&amp;";
case TEXT:
return "&";
case LATEX:
return " \\land ";
}
}();
if (!top)
os << '(';
const char* inf_ = (style == LATEX) ? "\\mathsf{Inf}(" : "Inf(";
while (a)
{
if (a & 1)
{
os << and_ << "Inf(" << negated;
os << and_ << inf_ << negated_pre;
set_printer(os, level);
os << ')';
and_ = html ? "&amp;" : "&";
os << negated_post << ')';
and_ = and_next_;
}
a >>= 1;
++level;
......@@ -135,14 +179,17 @@ namespace spot
}
break;
case acc_cond::acc_op::FinNeg:
negated = "!";
set_neg();
SPOT_FALLTHROUGH;
case acc_cond::acc_op::Fin:
{
auto a = code[pos - 1].mark.id;
if (a == 0U)
{
os << 'f';
if (style == LATEX)
os << "\\mathsf{f}";
else
os << 'f';
}
else
{
......@@ -153,14 +200,19 @@ namespace spot
const char* or_ = "";
if (!top)
os << '(';
const char* fin_ = (style == LATEX) ? "\\mathsf{Fin}(" : "Fin(";
while (a)
{
if (a & 1)
{
os << or_ << "Fin(" << negated;
os << or_ << fin_ << negated_pre;
set_printer(os, level);
os << ')';
or_ = "|";
os << negated_post << ')';
// The lack of surrounding space in HTML and
// TEXT is on purpose: we want to distinguish
// those grouped "Fin"s from other terms that
// are ORed together.
or_ = style == LATEX ? " \\lor " : "|";
}
a >>= 1;
++level;
......@@ -1407,7 +1459,7 @@ namespace spot
if (empty())
os << 't';
else
print_code<true>(os, *this, size() - 1,
print_code<HTML>(os, *this, size() - 1,
set_printer ? set_printer : default_set_printer);
return os;
}
......@@ -1420,7 +1472,20 @@ namespace spot
if (empty())
os << 't';
else
print_code<false>(os, *this, size() - 1,
print_code<TEXT>(os, *this, size() - 1,
set_printer ? set_printer : default_set_printer);
return os;
}
std::ostream&
acc_cond::acc_code::to_latex(std::ostream& os,
std::function<void(std::ostream&, int)>
set_printer) const
{
if (empty())
os << "\\mathsf{t}";
else
print_code<LATEX>(os, *this, size() - 1,
set_printer ? set_printer : default_set_printer);
return os;
}
......
......@@ -892,6 +892,13 @@ namespace spot
std::function<void(std::ostream&, int)>
set_printer = nullptr) const;
// Print the acceptance as Latex. The set_printer function can
// be used to implement customized output for set numbers.
std::ostream&
to_latex(std::ostream& os,
std::function<void(std::ostream&, int)>
set_printer = nullptr) const;
/// \brief Construct an acc_code from a string.
///
......
......@@ -23,6 +23,7 @@
#include <ostream>
#include <sstream>
#include <stdexcept>
#include <spot/tl/print.hh>
#include <spot/twa/twagraph.hh>
#include <spot/twaalgos/dot.hh>
#include <spot/twa/bddprint.hh>
......@@ -100,6 +101,10 @@ namespace spot
bool opt_numbered_edges_ = false;
bool opt_orig_show_ = false;
bool max_states_given_ = false; // related to max_states_
bool opt_latex_ = false;
const char* nl_ = "\\n";
const char* label_pre_ = "label=\"";
char label_post_ = '"';
const_twa_graph_ptr aut_;
std::string opt_font_;
......@@ -265,6 +270,9 @@ namespace spot
case 't':
opt_force_acc_trans_ = true;
break;
case 'x':
opt_latex_ = true;
break;
case 'y':
opt_shared_univ_dest_ = false;
break;
......@@ -272,6 +280,23 @@ namespace spot
throw std::runtime_error
(std::string("unknown option for print_dot(): ") + c);
}
if (opt_html_labels_)
{
nl_ = "<br/>";
label_pre_ = "label=<";
label_post_ = '>';
}
if (opt_latex_)
{
if (opt_latex_ && opt_html_labels_)
throw std::runtime_error
(std::string("print_dot(): options 'r' and 'R' "
"are incompatible with 'x'"));
nl_ = "\\\\{}";
label_pre_ = "texlbl=\"";
label_post_ = '"';
}
}
dotty_output(std::ostream& os, const char* options)
......@@ -306,7 +331,12 @@ namespace spot
output_set(acc_cond::mark_t a) const
{
if (!opt_all_bullets)
os_ << '{';
{
if (opt_latex_)
os_ << "\\{";
else
os_ << '{';
}
const char* space = "";
for (auto v: a.sets())
{
......@@ -316,7 +346,12 @@ namespace spot
space = ",";
}
if (!opt_all_bullets)
os_ << '}';
{
if (opt_latex_)
os_ << "\\}";
else
os_ << '}';
}
}
const char*
......@@ -369,8 +404,30 @@ namespace spot
os_ << '}';
}
std::string
state_label(unsigned s) const
std::ostream&
escape_for_output(std::ostream& os, const std::string& s) const
{
if (opt_html_labels_)
return escape_html(os, s);
if (opt_latex_)
return escape_latex(os, s);
return escape_str(os, s);
}
std::ostream&
format_label(std::ostream& os, bdd label) const
{
formula f = bdd_to_formula(label, aut_->get_dict());
if (opt_latex_)
{
print_sclatex_psl(os << '$', f) << '$';
return os;
}
return escape_for_output(os, str_psl(f));
}
std::ostream&
format_state_label(std::ostream& os, unsigned s) const
{
bdd label = bddfalse;
for (auto& t: aut_->out(s))
......@@ -380,8 +437,8 @@ namespace spot
}
if (label == bddfalse
&& incomplete_ && incomplete_->find(s) != incomplete_->end())
return "...";
return bdd_format_formula(aut_->get_dict(), label);
return os << "...";
return format_label(os, label);
}
std::string string_dst(int dst, int color_num = -1)
......@@ -420,26 +477,27 @@ namespace spot
}
}
void print_acceptance_for_human()
std::string get_acceptance_for_human()
{
const char* nl = opt_html_labels_ ? "<br/>" : "\\n";
std::ostringstream os;
if (aut_->acc().is_generalized_buchi())
{
if (aut_->acc().is_all())
os_ << nl << "[all]";
os << "all";
else if (aut_->acc().is_buchi())
os_ << nl << "[Büchi]";
os << "Büchi";
else
os_ << nl << "[gen. Büchi " << aut_->num_sets() << ']';
os << "gen. Büchi " << aut_->num_sets();
}
else if (aut_->acc().is_generalized_co_buchi())
{
if (aut_->acc().is_none())
os_ << nl << "[none]";
os << "none";
else if (aut_->acc().is_co_buchi())
os_ << nl << "[co-Büchi]";
os << "co-Büchi";
else
os_ << nl << "[gen. co-Büchi " << aut_->num_sets() << ']';
os << "gen. co-Büchi " << aut_->num_sets();
}
else
{
......@@ -447,7 +505,7 @@ namespace spot
assert(r != 0);
if (r > 0)
{
os_ << nl << "[Rabin " << r << ']';
os << "Rabin " << r;
}
else
{
......@@ -455,14 +513,14 @@ namespace spot
assert(r != 0);
if (r > 0)
{
os_ << nl << "[Streett " << r << ']';
os << "Streett " << r;
}
else
{
std::vector<unsigned> pairs;
if (aut_->acc().is_generalized_rabin(pairs))
{
os_ << nl << "[gen. Rabin " << pairs.size() << ']';
os << "gen. Rabin " << pairs.size();
}
else
{
......@@ -470,10 +528,10 @@ namespace spot
bool odd = false;
if (aut_->acc().is_parity(max, odd))
{
os_ << nl << "[parity "
<< (max ? "max " : "min ")
<< (odd ? "odd " : "even ")
<< aut_->num_sets() << ']';
os << "parity "
<< (max ? "max " : "min ")
<< (odd ? "odd " : "even ")
<< aut_->num_sets();
}
else
{
......@@ -484,14 +542,23 @@ namespace spot
bool s_like = aut_->acc().is_streett_like(s_pairs);
unsigned ssz = s_pairs.size();
if (r_like && (!s_like || (rsz <= ssz)))
os_ << nl << "[Rabin-like " << rsz << ']';
else if (s_like && (!r_like || (ssz < rsz)))
os_ << nl << "[Streett-like " << ssz << ']';
os << "Rabin-like " << rsz;
else if (s_like && (!r_like || (ssz < rsz)))
os << "Streett-like " << ssz;
}
}
}
}
}
return os.str();
}
void print_acceptance_for_human()
{
std::string accstr = get_acceptance_for_human();
if (accstr.empty())
return;
os_ << nl_ << '[' << accstr << ']';
}
void
......@@ -503,56 +570,39 @@ namespace spot
if (opt_bullet && aut_->num_sets() <= MAX_BULLET)
opt_all_bullets = true;
os_ << "digraph G {\n";
if (opt_latex_)
os_ << " d2tgraphstyle=\"every node/.style={align=center}\"\n";
if (!opt_vertical_)
os_ << " rankdir=LR\n";
if (name_ || opt_show_acc_)
{
if (!opt_html_labels_)
{
os_ << " label=\"";
if (name_)
{
escape_str(os_, *name_);
if (opt_show_acc_)
os_ << "\\n";
}
if (opt_show_acc_)
{
if (!dcircles_)
{
aut_->get_acceptance().to_text
(os_, [this](std::ostream& os, int v)
{
this->output_set(os, v);
});
}
print_acceptance_for_human();
}
os_ << "\"\n";
}
else
os_ << " " << label_pre_;
if (name_)
escape_for_output(os_, *name_);
if (opt_show_acc_)
{
os_ << " label=<";
if (name_)
if (!dcircles_)
{
escape_html(os_, *name_);
if (opt_show_acc_)
os_ << "<br/>";
}
if (opt_show_acc_)
{
if (!dcircles_)
{
aut_->get_acceptance().to_html
(os_, [this](std::ostream& os, int v)
{
this->output_html_set_aux(os, v);
});
}
print_acceptance_for_human();
if (name_)
os_ << nl_;
auto& acc = aut_->get_acceptance();
if (opt_html_labels_)
acc.to_html(os_, [this](std::ostream& os, int v)
{
this->output_html_set_aux(os, v);
});
else if (opt_latex_)
acc.to_latex(os_ << '$') << '$';
else
acc.to_text(os_, [this](std::ostream& os, int v)
{
this->output_set(os, v);
});
}
os_ << ">\n";
print_acceptance_for_human();
}
os_ << label_post_ << '\n';
os_ << " labelloc=\"t\"\n";
}
switch (opt_shape_)
......@@ -607,6 +657,16 @@ namespace spot
void
process_state(unsigned s)
{
os_ << " " << s << " [" << label_pre_;
if (sn_ && s < sn_->size() && !(*sn_)[s].empty())
escape_for_output(os_, (*sn_)[s]);
else if (sprod_)
os_ << (*sprod_)[s].first << ',' << (*sprod_)[s].second;
else
os_ << s;
if (orig_ && s < orig_->size())
os_ << " (" << (*orig_)[s] << ')';
if (mark_states_ && !dcircles_)
{
acc_cond::mark_t acc = 0U;
......@@ -616,64 +676,23 @@ namespace spot
break;
}
bool has_name = sn_ && s < sn_->size() && !(*sn_)[s].empty();
os_ << " " << s << " [label=";
if (!opt_html_labels_)
{
os_ << '"';
if (has_name)
escape_str(os_, (*sn_)[s]);
else if (sprod_)
os_ << (*sprod_)[s].first << ',' << (*sprod_)[s].second;
else
os_ << s;
if (orig_ && s < orig_->size())
os_ << " (" << (*orig_)[s] << ')';
if (acc)
{
os_ << "\\n";
output_set(acc);
}
if (opt_state_labels_)
escape_str(os_ << "\\n", state_label(s));
os_ << '"';
}
else
if (acc)
{