dotty: colored acceptance sets

This implement several new options for --dot in order to
allow emptiness sets to be output as colored ⓿ or ❶...
Also add a SPOT_DOTDEFAULT environment variable.

* NEWS, src/bin/man/spot-x.x, src/bin/common_aoutput.cc,
src/bin/dstar2tgba.cc: Document the new options.
* doc/org/.dir-locals.el, doc/org/init.el.in: Setup
SPOT_DOTEXTRA and SPOT_DOTDEFAULT for all documents.
* doc/org/autfilt.org, doc/org/dstar2tgba.org, doc/org/ltl2tgba.org,
doc/org/ltldo.org, doc/org/oaut.org, doc/org/randaut.org,
doc/org/satmin.org: Adjust to this new setup.
* src/misc/escape.cc, src/misc/escape.hh (escape_html): New function.
* src/tgba/acc.cc, src/tgba/acc.hh (to_text, to_html): New method.
* src/tgbaalgos/dotty.cc: Implement the new options.
* src/tgbatest/readsave.test, wrap/python/tests/automata.ipynb: More
tests.
* wrap/python/spot.py: Make sure the default argument for
dotty_reachable is None, so that SPOT_DOTDEFAULT is honored.
This commit is contained in:
Alexandre Duret-Lutz 2015-03-17 09:08:20 +01:00
parent 7caf2b83d6
commit 838bfb2ae3
21 changed files with 1500 additions and 1193 deletions

View file

@ -51,10 +51,13 @@ static const argp_option options[] =
{
/**************************************************/
{ 0, 0, 0, 0, "Output format:", 3 },
{ "dot", OPT_DOT, "a|c|h|n|N|s|t|v", OPTION_ARG_OPTIONAL,
{ "dot", OPT_DOT, "a|b|c|f(FONT)|h|n|N|r|R|s|t|v", OPTION_ARG_OPTIONAL,
"GraphViz's format (default). Add letters for "
"(a) acceptance display, (c) circular nodes, (h) horizontal layout, "
"(v) vertical layout, (n) with name, (N) without name, (s) with SCCs, "
"(a) acceptance display, (b) acceptance sets as bullets,"
"(c) circular nodes, (f(FONT)) use FONT, (h) horizontal layout, "
"(v) vertical layout, (n) with name, (N) without name, "
"(r) rainbow colors for acceptance set, "
"(R) color acceptance set by Inf/Fin, (s) with SCCs, "
"(t) force transition-based acceptance.", 0 },
{ "hoaf", 'H', "i|s|t|m|l", OPTION_ARG_OPTIONAL,
"Output the automaton in HOA format. Add letters to select "

View file

@ -73,10 +73,13 @@ static const argp_option options[] =
"of the given property)", 0 },
/**************************************************/
{ 0, 0, 0, 0, "Output format:", 3 },
{ "dot", OPT_DOT, "a|c|h|n|N|s|t|v", OPTION_ARG_OPTIONAL,
"GraphViz's format (default). Add letters for (a) acceptance display, "
"(c) circular nodes, (h) horizontal layout, (v) vertical layout, "
"(n) with name, (N) without name, (s) with SCCs, "
{ "dot", OPT_DOT, "a|b|c|f(FONT)|h|n|N|r|R|s|t|v", OPTION_ARG_OPTIONAL,
"GraphViz's format (default). Add letters for "
"(a) acceptance display, (b) acceptance sets as bullets,"
"(c) circular nodes, (f(FONT)) use FONT, (h) horizontal layout, "
"(v) vertical layout, (n) with name, (N) without name, "
"(r) rainbow colors for acceptance set, "
"(R) color acceptance set by Inf/Fin, (s) with SCCs, "
"(t) force transition-based acceptance.", 0 },
{ "hoaf", 'H', "i|s|t|m|l", OPTION_ARG_OPTIONAL,
"Output the automaton in HOA format. Add letters to select "

View file

@ -11,6 +11,15 @@ spot-x \- Common fine-tuning options.
[ENVIRONMENT VARIABLES]
.TP
\fBSPOT_DOTDEFAULT\fR
Whenever the \f(CW--dot\fR option is used without argument (even
implicitely), the contents of this variable is used as default
argument. If you have some default setting in \fBSPOT_DOTDEFAULT\fR
but want to alter them temporarily for one call, use
\f(CW--dot=.yyy\fR: the dot character will be replaced by the contents
of the \f(CWSPOT_DOTDEFAULT\fR environment variable.
.TP
\fBSPOT_DOTEXTRA\fR
The contents of this variable is added to any dot output, immediately

View file

@ -77,6 +77,31 @@ namespace spot
return os;
}
std::ostream&
escape_html(std::ostream& os, const std::string& str)
{
for (auto i: str)
switch (i)
{
case '&':
os << "&amp;";
break;
case '"':
os << "&quot;";
break;
case '<':
os << "&lt;";
break;
case '>':
os << "&gt;";
break;
default:
os << i;
break;
}
return os;
}
std::ostream&
escape_str(std::ostream& os, const std::string& str)
{

View file

@ -46,6 +46,13 @@ namespace spot
SPOT_API std::ostream&
escape_latex(std::ostream& os, const std::string& str);
/// \brief Escape special HTML characters.
///
/// The following characters are rewritten:
/// <code>&gt; &lt; &quot; &amp;</code>
SPOT_API std::ostream&
escape_html(std::ostream& os, const std::string& str);
/// \brief Escape characters <code>"</code>, <code>\\</code>, and
/// <code>\\n</code> in \a str.
SPOT_API std::ostream&

View file

@ -48,9 +48,16 @@ namespace spot
namespace
{
void default_set_printer(std::ostream& os, int v)
{
os << v;
}
template<bool html>
static void
print_code(std::ostream& os,
const acc_cond::acc_code& code, unsigned pos)
const acc_cond::acc_code& code, unsigned pos,
std::function<void(std::ostream&, int)> set_printer)
{
const char* op = " | ";
auto& w = code[pos];
@ -59,7 +66,7 @@ namespace spot
switch (w.op)
{
case acc_cond::acc_op::And:
op = " & ";
op = html ? " &amp; " : " & ";
case acc_cond::acc_op::Or:
{
unsigned sub = pos - w.size;
@ -73,7 +80,7 @@ namespace spot
first = false;
else
os << op;
print_code(os, code, pos);
print_code<html>(os, code, pos, set_printer);
pos -= code[pos].size;
}
if (!top)
@ -102,8 +109,10 @@ namespace spot
{
if (a & 1)
{
os << and_ << "Inf(" << negated << level << ')';
and_ = "&";
os << and_ << "Inf(" << negated;
set_printer(os, level);
os << ')';
and_ = html ? "&amp;" : "&";
}
a >>= 1;
++level;
@ -135,7 +144,9 @@ namespace spot
{
if (a & 1)
{
os << or_ << "Fin(" << negated << level << ')';
os << or_ << "Fin(" << negated;
set_printer(os, level);
os << ')';
or_ = "|";
}
a >>= 1;
@ -746,13 +757,69 @@ namespace spot
return used_in_cond;
}
std::pair<acc_cond::mark_t, acc_cond::mark_t>
acc_cond::acc_code::used_inf_fin_sets() const
{
if (is_true() || is_false())
return {0U, 0U};
acc_cond::mark_t used_fin = 0U;
acc_cond::mark_t used_inf = 0U;
auto pos = &back();
auto end = &front();
while (pos > end)
{
switch (pos->op)
{
case acc_cond::acc_op::And:
case acc_cond::acc_op::Or:
--pos;
break;
case acc_cond::acc_op::Fin:
case acc_cond::acc_op::FinNeg:
used_fin |= pos[-1].mark;
pos -= 2;
break;
case acc_cond::acc_op::Inf:
case acc_cond::acc_op::InfNeg:
used_inf |= pos[-1].mark;
pos -= 2;
break;
}
}
return {used_inf, used_fin};
}
std::ostream&
acc_cond::acc_code::to_html(std::ostream& os,
std::function<void(std::ostream&, int)>
set_printer) const
{
if (empty())
os << 't';
else
print_code<true>(os, *this, size() - 1,
set_printer ? set_printer : default_set_printer);
return os;
}
std::ostream&
acc_cond::acc_code::to_text(std::ostream& os,
std::function<void(std::ostream&, int)>
set_printer) const
{
if (empty())
os << 't';
else
print_code<false>(os, *this, size() - 1,
set_printer ? set_printer : default_set_printer);
return os;
}
std::ostream& operator<<(std::ostream& os,
const spot::acc_cond::acc_code& code)
{
if (code.empty())
os << 't';
else
print_code(os, code, code.size() - 1);
return os;
return code.to_text(os);
}
}

View file

@ -660,6 +660,24 @@ namespace spot
// Return the set of sets appearing in the condition.
acc_cond::mark_t used_sets() const;
// Return the sets used as Inf or Fin in the acceptance condition
std::pair<acc_cond::mark_t, acc_cond::mark_t> used_inf_fin_sets() const;
// Print the acceptance as HTML. The set_printer function can
// be used to implement customized output for set numbers.
std::ostream&
to_html(std::ostream& os,
std::function<void(std::ostream&, int)>
set_printer = nullptr) const;
// Print the acceptance as text. The set_printer function can
// be used to implement customized output for set numbers.
std::ostream&
to_text(std::ostream& os,
std::function<void(std::ostream&, int)>
set_printer = nullptr) const;
// Calls to_text
SPOT_API
friend std::ostream& operator<<(std::ostream& os, const acc_code& code);
};

View file

@ -31,11 +31,16 @@
#include "tgba/formula2bdd.hh"
#include "tgbaalgos/sccinfo.hh"
#include <cstdlib>
#include <cstring>
#include <ctype.h>
namespace spot
{
namespace
{
constexpr int MAX_BULLET = 20;
class dotty_output
{
std::ostream& os_;
@ -46,74 +51,229 @@ namespace spot
bool opt_show_acc_ = false;
bool mark_states_ = false;
bool opt_scc_ = false;
bool opt_html_labels_ = false;
const_tgba_digraph_ptr aut_;
std::vector<std::string>* sn_;
std::string* name_ = nullptr;
acc_cond::mark_t inf_sets_ = 0U;
acc_cond::mark_t fin_sets_ = 0U;
bool opt_rainbow = false;
bool opt_bullet = false;
bool opt_all_bullets = false;
std::string opt_font_;
const char* const palette9[9] =
{
"#5DA5DA", /* blue */
"#F17CB0", /* pink */
"#FAA43A", /* orange */
"#B276B2", /* purple */
"#60BD68", /* green */
"#F15854", /* red */
"#B2912F", /* brown */
"#4D4D4D", /* gray */
"#DECF3F", /* yellow */
};
const char*const* palette = palette9;
int palette_mod = 9;
public:
void
parse_opts(const char* options)
{
const char* orig = options;
while (char c = *options++)
switch (c)
{
case '.':
{
static const char* def = getenv("SPOT_DOTDEFAULT");
// Prevent infinite recursions...
if (orig == def)
throw std::runtime_error
(std::string("SPOT_DOTDEFAULT should not contain '.'"));
if (def)
parse_opts(def);
break;
}
case 'a':
opt_show_acc_ = true;
break;
case 'b':
opt_bullet = true;
break;
case 'c':
opt_circles_ = true;
break;
case 'h':
opt_horizontal_ = true;
break;
case 'f':
if (*options != '(')
throw std::runtime_error
(std::string("invalid font specification for dotty()"));
{
auto* end = strchr(++options, ')');
if (!end)
throw std::runtime_error
(std::string("invalid font specification for dotty()"));
opt_font_ = std::string(options, end - options);
options = end + 1;
}
break;
case 'n':
opt_name_ = true;
break;
case 'N':
opt_name_ = false;
break;
case 'r':
opt_html_labels_ = true;
opt_rainbow = true;
break;
case 'R':
opt_html_labels_ = true;
opt_rainbow = false;
break;
case 's':
opt_scc_ = true;
break;
case 'v':
opt_horizontal_ = false;
break;
case 't':
opt_force_acc_trans_ = true;
break;
default:
throw std::runtime_error
(std::string("unknown option for dotty(): ") + c);
}
}
dotty_output(std::ostream& os, const char* options)
: os_(os)
{
if (options)
while (char c = *options++)
switch (c)
{
case 'a':
opt_show_acc_ = true;
break;
case 'c':
opt_circles_ = true;
break;
case 'h':
opt_horizontal_ = true;
break;
case 'n':
opt_name_ = true;
break;
case 'N':
opt_name_ = false;
break;
case 's':
opt_scc_ = true;
break;
case 'v':
opt_horizontal_ = false;
break;
case 't':
opt_force_acc_trans_ = true;
break;
default:
throw std::runtime_error
(std::string("unknown option for dotty(): ") + c);
}
parse_opts(options ? options : ".");
}
void
output_set(std::ostream& os, int v) const
{
if (opt_bullet && (v >= 0) & (v <= MAX_BULLET))
{
static const char* const tab[MAX_BULLET + 1] = {
"", "", "", "",
"", "", "", "",
"", "", "", "",
"", "", "", "",
"", "", "", "",
"",
};
os << tab[v];
}
else
{
os << v;
}
}
const char*
html_set_color(int v) const
{
if (opt_rainbow)
return palette[v % palette_mod];
// Color according to Fin/Inf
if (inf_sets_.has(v))
{
if (fin_sets_.has(v))
return palette[2];
else
return palette[0];
}
else
{
return palette[1];
}
}
void
output_html_set_aux(std::ostream& os, int v) const
{
os << "<font color=\"" << html_set_color(v) << "\">";
output_set(os, v);
os << "</font>";
}
void
output_html_set(int v) const
{
output_html_set_aux(os_, v);
}
void
start()
{
if (opt_html_labels_)
std::tie(inf_sets_, fin_sets_) =
aut_->get_acceptance().used_inf_fin_sets();
if (opt_bullet && aut_->acc().num_sets() <= MAX_BULLET)
opt_all_bullets = true;
os_ << "digraph G {\n";
if (opt_horizontal_)
os_ << " rankdir=LR\n";
if (name_ || opt_show_acc_)
{
os_ << " label=\"";
if (name_)
if (!opt_html_labels_)
{
escape_str(os_, *name_);
os_ << " label=\"";
if (name_)
{
escape_str(os_, *name_);
if (opt_show_acc_)
os_ << "\\n";
}
if (opt_show_acc_)
os_ << "\\n";
aut_->get_acceptance().to_text
(os_, [this](std::ostream& os, int v)
{
this->output_set(os, v);
});
os_ << "\"\n";
}
if (opt_show_acc_)
os_ << aut_->get_acceptance();
os_ << "\"\n labelloc=\"t\"\n";
else
{
os_ << " label=<";
if (name_)
{
escape_html(os_, *name_);
if (opt_show_acc_)
os_ << "<br/>";
}
if (opt_show_acc_)
aut_->get_acceptance().to_html
(os_, [this](std::ostream& os, int v)
{
this->output_html_set_aux(os, v);
});
os_ << ">\n";
}
os_ << " labelloc=\"t\"\n";
}
if (opt_circles_)
os_ << " node [shape=\"circle\"]\n";
if (!opt_font_.empty())
os_ << " fontname=\"" << opt_font_
<< "\"\n node [fontname=\"" << opt_font_
<< "\"]\n edge [fontname=\"" << opt_font_
<< "\"]\n";
// Any extra text passed in the SPOT_DOTEXTRA environment
// variable should be output at the end of the "header", so
// that our setup can be overridden.
static const char* extra = getenv("SPOT_DOTEXTRA");
if (extra)
if (extra && *extra)
os_ << " " << extra << '\n';
os_ << " I [label=\"\", style=invis, ";
os_ << (opt_horizontal_ ? "width" : "height");
@ -144,15 +304,53 @@ namespace spot
process_link(const tgba_digraph::trans_storage_t& t)
{
std::string label = bdd_format_formula(aut_->get_dict(), t.cond);
label = escape_str(label);
if (!mark_states_)
if (auto a = t.acc)
{
label += "\\n";
label += aut_->acc().format(a);
}
os_ << " " << t.src << " -> " << t.dst
<< " [label=\"" + label + "\"]\n";
os_ << " " << t.src << " -> " << t.dst;
if (!opt_html_labels_)
{
os_ << " [label=\"";
escape_str(os_, label);
if (!mark_states_)
if (auto a = t.acc)
{
os_ << "\\n";
if (!opt_all_bullets)
os_ << '{';
const char* space = "";
for (auto v: a.sets())
{
if (!opt_all_bullets)
os_ << space;
output_set(os_, v);
space = ",";
}
if (!opt_all_bullets)
os_ << '}';
}
os_ << "\"]\n";
}
else
{
os_ << " [label=<";
escape_html(os_, label);
if (!mark_states_)
if (auto a = t.acc)
{
os_ << "<br/>";
if (!opt_all_bullets)
os_ << '{';
const char* space = "";
for (auto v: a.sets())
{
if (!opt_all_bullets)
os_ << space;
output_html_set(v);
space = ",";
}
if (!opt_all_bullets)
os_ << '}';
}
os_ << ">]\n";
}
}
void print(const const_tgba_digraph_ptr& aut)

View file

@ -308,7 +308,7 @@ State: 3 "s3"
EOF
$autfilt -H input |
SPOT_DOTEXTRA='/* hello world */' $autfilt --dot=vcsn >output
SPOT_DOTDEFAULT=vcsn SPOT_DOTEXTRA='/* hello world */' $autfilt >output
cat >expected <<EOF
digraph G {
@ -360,20 +360,46 @@ digraph G {
EOF
diff output expected
$ltl2tgba --dot=an 'GFa & GFb' >output
$ltl2tgba --dot=ban 'GFa & GFb' >output
cat output
cat >expected <<EOF
digraph G {
rankdir=LR
label="G(Fa & Fb)\\nInf(0)&Inf(1)"
label="G(Fa & Fb)\nInf(⓿)&Inf(❶)"
labelloc="t"
I [label="", style=invis, width=0]
I -> 0
0 [label="0"]
0 -> 0 [label="a & b\n{0,1}"]
0 -> 0 [label="a & b\n⓿❶"]
0 -> 0 [label="!a & !b"]
0 -> 0 [label="!a & b\n{1}"]
0 -> 0 [label="a & !b\n{0}"]
0 -> 0 [label="!a & b\n❶"]
0 -> 0 [label="a & !b\n⓿"]
}
EOF
diff output expected
SPOT_DOTDEFAULT=bra $ltl2tgba --dot='c.f(Lato)' 'GFa & GFb' >output
cat output
zero='<font color="#5DA5DA">⓿</font>'
one='<font color="#F17CB0">❶</font>'
cat >expected <<EOF
digraph G {
rankdir=LR
label=<Inf($zero)&amp;Inf($one)>
labelloc="t"
node [shape="circle"]
fontname="Lato"
node [fontname="Lato"]
edge [fontname="Lato"]
I [label="", style=invis, width=0]
I -> 0
0 [label="0"]
0 -> 0 [label=<a &amp; b<br/>$zero$one>]
0 -> 0 [label=<!a &amp; !b>]
0 -> 0 [label=<!a &amp; b<br/>$one>]
0 -> 0 [label=<a &amp; !b<br/>$zero>]
}
EOF
diff output expected