Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ The complementation and the inclusion checking might be adjusted by the followin
| `tela` | `yes`,`no` | If `yes`, use TELA-specific algorithms for complementation (if `no`, the input is converted to TBA) |
| `sh-break` | `yes`,`no` | Enable shared breakpoint across partial algorithms; when `yes` partial algorithms that support shared breakpoints propagate a common breakpoint across partitions |
| `tela_det_alg` | `inductive`, `dnf-tela` | Algorithm selection for complementing deterministic TELA components (default `dnf-tela`) |
| `sd_ind_sh_break` | `yes`, `no` | Shared breakpoint for inductive TELA procedure (assumes `tela_det_alg=inductive`) |
| `sd_ind_sh_break` | `root`, `inf_tree` | Shared breakpoint mode for inductive TELA procedure (assumes `tela_det_alg=inductive`) |

## Testing

Expand Down
85 changes: 60 additions & 25 deletions src/algorithms/complement_alg_sd_inductive.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,45 @@ check_macrostate assign_leaf_ids(const check_macrostate& tree, unsigned& next_id
return check_macrostate::make(tree.get_options_ptr(), node_type, assign_leaf_ids(left_ms, next_id), assign_leaf_ids(right_ms, next_id), tree.node_value().context);
}

/**
* @brief Internal helper for initializing `NodeContext` payloads inside a check tree.
*
* @param tree Check tree to update in-place.
* @param opts Options controlling whether shared-breakpoint contexts are used.
* @param is_root Whether this node is the root of the entire tree.
*
* @warning Same caveats apply as in `init_contexts_in_tree()`.
*/
static void init_contexts_in_tree_impl(check_macrostate& tree, options_ptr opts, bool is_root) {
if (tree.is_leaf()) {
return;
}

auto& base = static_cast<base_tree&>(tree);
init_contexts_in_tree_impl(static_cast<check_macrostate&>(base.left()), opts, false);
init_contexts_in_tree_impl(static_cast<check_macrostate&>(base.right()), opts, false);

// if root_shb, only root gets the shared-breakpoint context; if inf_tree_shb, every internal And-node gets a shared-breakpoint context
if ( (opts->use_root_shared_breakpoint && is_root) || opts->use_inf_tree_shared_breakpoint ) {
NodeContext ctx = NodeContext::create_subtree_sh_context(tree.type(), tree, opts->use_inf_tree_shared_breakpoint);
tree.node_value().set_context(ctx);
}
}

/**
* @brief Initialize (or re-initialize) `NodeContext` payloads inside a check tree.
*
* When shared-breakpoint mode is enabled (`opts->use_shared_breakpoint == true`),
* each internal node's `NodeContext` is recomputed from the subtree so that it
* references the current set of `Inf` leaf IDs.
* Supports two mutually exclusive shared-breakpoint modes:
* - **Root-only mode** (`opts->use_root_shared_breakpoint == true`): computes the
* root internal node's `NodeContext` from the subtree to reference the current
* set of `Inf` leaf IDs. Non-root nodes do not receive shared-breakpoint contexts.
* - **Inf-tree mode** (`opts->use_inf_tree_shared_breakpoint == true`): computes
* `NodeContext` for every internal And-node in the tree, enabling per-subtree
* shared-breakpoint tracking.
*
* @param tree Check tree to update in-place.
* @param opts Options controlling whether shared-breakpoint contexts are used.
* Must be non-null.
* @param opts Options controlling whether, and which shared-breakpoint strategy is used.
* Must be non-null and valid (both modes cannot be enabled simultaneously).
*
* @warning This function traverses children by casting `base_tree::left()/right()`
* to `check_macrostate&`. The underlying tree stores children as
Expand All @@ -62,19 +91,7 @@ check_macrostate assign_leaf_ids(const check_macrostate& tree, unsigned& next_id
* traversal.
*/
void init_contexts_in_tree(check_macrostate& tree, options_ptr opts) {
if (tree.is_leaf()) {
return;
}

auto& base = static_cast<base_tree&>(tree);
init_contexts_in_tree(static_cast<check_macrostate&>(base.left()), opts);
init_contexts_in_tree(static_cast<check_macrostate&>(base.right()), opts);

if (opts->use_shared_breakpoint) {
NodeContext ctx = NodeContext::create_subtree_sh_context(tree.type(), tree);
tree.node_value().set_context(ctx);
}

init_contexts_in_tree_impl(tree, opts, true);
}

/**
Expand Down Expand Up @@ -335,6 +352,14 @@ std::vector<std::pair<check_macrostate, NodeContext>> check_macrostate::get_succ
}

if (node_type == TreeType::Or) {
// In inf_tree_shb mode, each And-node scope is independent. SHB context must
// not bleed through Or nodes: Inf leaves under Or nodes need the standard
// per-leaf breakpoint mechanism, and nested And nodes must see NONE as their
// parent context so that is_scope_root fires correctly.
if (this->opts_ && this->opts_->use_inf_tree_shared_breakpoint) {
context_sent = NodeContext{};
}

// OR-FIN optimization: when the left subtree contains only FIN leaves (no inner Or
// nodes), send ALL check_states to the left subtree with collect_violating=true.
// States that fire a Fin-colored transition are collected as "violating" and are
Expand Down Expand Up @@ -366,12 +391,20 @@ std::vector<std::pair<check_macrostate, NodeContext>> check_macrostate::get_succ

for (const auto& [right_tree, right_node_ctx] : right_succ) {
// Propagate any further violations from right up to the parent.
NodeContext out_ctx{};
out_ctx.violating_states = right_node_ctx.violating_states;
out_ctx.violating_predecessors = right_node_ctx.violating_predecessors;
NodeContext local = actual_node_context;

NodeContext merge = left_node_ctx.union_contexts(right_node_ctx);
merge.violating_states = right_node_ctx.violating_states;
merge.violating_predecessors = right_node_ctx.violating_predecessors;

if (is_scope_root) {
if (!merge.is_none()) local = merge;
merge = NodeContext{};
}

auto combined = check_macrostate::make(this->opts_, TreeType::Or,
left_restricted, right_tree, actual_node_context);
out.insert({combined.reduce(), out_ctx});
left_restricted, right_tree, local);
out.insert({combined.reduce(), merge});
}
}
return std::vector<std::pair<check_macrostate, NodeContext>>(out.begin(), out.end());
Expand Down Expand Up @@ -790,7 +823,7 @@ std::vector<std::pair<check_macrostate, NodeContext>> inf_leaf::get_succ(
std::set<unsigned> succ_break {};


if (opts && opts->use_shared_breakpoint && context.is_shared_breakpoint()) {
if (opts && opts->use_shared_breakpoint() && context.is_shared_breakpoint()) {
if (!context.targets_leaf(this->id)) {
return {{check_macrostate::inf(std::move(opts), std::move(succs), std::move(succ_break), this->color, this->id), NodeContext{}}};
}
Expand Down Expand Up @@ -886,9 +919,11 @@ complement_sd_inductive::complement_sd_inductive(const cmpl_info& info, unsigned
spot::acc_cond::acc_code acc = this->info_.part_to_acc_map_.at(part_index_).get_acceptance();
this->acc_cond_ = acc.complement();
this->opts_ = std::make_shared<sd_inductive::options>(sd_inductive::options{
.use_shared_breakpoint = (kofola::OPTIONS.params["sd_ind_sh_break"] == "yes"),
.use_root_shared_breakpoint = (kofola::OPTIONS.params["sd_ind_sh_break"] == "root"),
.use_inf_tree_shared_breakpoint = (kofola::OPTIONS.params["sd_ind_sh_break"] == "inf_tree"),
.use_or_fin_opt = (kofola::OPTIONS.params["sd_ind_or_opt"] == "yes")
});
assert(this->opts_->is_valid() && "options: both breakpoint types cannot be enabled simultaneously");
}

/**
Expand Down
44 changes: 32 additions & 12 deletions src/algorithms/complement_alg_sd_inductive.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,17 @@ namespace kofola { // {{{
namespace sd_inductive {

struct options {
bool use_shared_breakpoint{false};
bool use_root_shared_breakpoint{false};
bool use_inf_tree_shared_breakpoint{false};
bool use_shared_breakpoint() const {
return use_root_shared_breakpoint || use_inf_tree_shared_breakpoint;
}
bool use_or_fin_opt{false};

/// Validation: both breakpoint types are mutually exclusive.
bool is_valid() const {
return !(use_root_shared_breakpoint && use_inf_tree_shared_breakpoint);
}
};

using options_ptr = std::shared_ptr<const options>;
Expand Down Expand Up @@ -58,16 +67,21 @@ namespace sd_inductive {
class check_macrostate;

/**
* @brief Collect IDs of `Inf` leaves under `And` nodes.
* @brief Collect IDs of all `Inf` leaves (or only under AND nodes in case of inf_tree_shared_breakpoint).
*
* Traverses the check tree @p t and appends each encountered
* `inf_leaf::id` to @p out. For internal nodes, only `TreeType::And`
* `inf_leaf::id` to @p out.
*
* root_shared_breakpoint: For internal nodes, all subtrees are traversed and all `Inf` leaf IDs collected.
*
* inf_tree_shared_breakpoint:For internal nodes, only `TreeType::And`
* subtrees are traversed; `Or` subtrees are intentionally ignored.
*
* @param t Check tree (subtree root) to traverse.
* @param out Output vector to append leaf IDs into.
* @param use_inf_tree_shb Whether to use the Inf tree shared breakpoint optimization, root is default.
*/
inline void collect_inf_leaf_ids(const check_macrostate& t, std::vector<unsigned>& out);
inline void collect_inf_leaf_ids(const check_macrostate& t, std::vector<unsigned>& out, bool use_inf_tree_shb = false);

struct NodeContext;
std::ostream& operator<<(std::ostream& os, const NodeContext& ctx);
Expand Down Expand Up @@ -174,10 +188,14 @@ namespace sd_inductive {
* @param subtree_ Subtree to inspect for `Inf` leaf IDs.
* @return A freshly initialized context for that subtree.
*/
static NodeContext create_subtree_sh_context(TreeType t, const check_macrostate& subtree_) {
static NodeContext create_subtree_sh_context(TreeType t, const check_macrostate& subtree_, bool inf_tree_shb) {
NodeContext ctx;
collect_inf_leaf_ids(subtree_, ctx.leaf_ids_);
if (t == TreeType::And && ctx.leaf_ids_.size() > 0) {
collect_inf_leaf_ids(subtree_, ctx.leaf_ids_, inf_tree_shb);

bool not_leaf = (t == TreeType::And) || (t == TreeType::Or);
bool inf_tree_shb_then_and_node = (!inf_tree_shb) || (t == TreeType::And); // inf_tree_shb ==> (t == TreeType::And)

if (not_leaf && inf_tree_shb_then_and_node && ctx.leaf_ids_.size() > 0) {
ctx.type = NodeContextType::SHARED_BREAKPOINT;
ctx.leaf_id = ctx.leaf_ids_[ctx.leaf_index_ % ctx.leaf_ids_.size()];
}
Expand Down Expand Up @@ -615,7 +633,7 @@ namespace sd_inductive {
* @return String representation of this macrostate check tree.
*/
std::string to_string() const {
return to_string_impl(static_cast<const base_tree&>(*this), opts_ && opts_->use_shared_breakpoint);
return to_string_impl(static_cast<const base_tree&>(*this), opts_ && opts_->use_shared_breakpoint());
}

/**
Expand Down Expand Up @@ -696,7 +714,7 @@ namespace sd_inductive {
options_ptr opts_;
};

inline void collect_inf_leaf_ids(const check_macrostate& t, std::vector<unsigned>& out) {
inline void collect_inf_leaf_ids(const check_macrostate& t, std::vector<unsigned>& out, bool use_inf_tree_shb) {
using base_tree = kofola::types::binary_tree<TreeType, AndOrNode, fin_leaf, inf_leaf>;
const base_tree& bt = static_cast<const base_tree&>(t);
if (bt.is_leaf()) {
Expand All @@ -705,11 +723,13 @@ namespace sd_inductive {
}
return;
}
if(bt.type() != TreeType::And) {

if(use_inf_tree_shb && (bt.type() != TreeType::And)) {
return;
}
collect_inf_leaf_ids(check_macrostate(nullptr, base_tree(bt.left())), out);
collect_inf_leaf_ids(check_macrostate(nullptr, base_tree(bt.right())), out);

collect_inf_leaf_ids(check_macrostate(nullptr, base_tree(bt.left())), out, use_inf_tree_shb);
collect_inf_leaf_ids(check_macrostate(nullptr, base_tree(bt.right())), out, use_inf_tree_shb);
}

/**
Expand Down
86 changes: 80 additions & 6 deletions tests/tela/test_complement_tela.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@ TEST_CASE("complement_tela with tela_det_alg=inductive produces language-equival
kofola::OPTIONS.params.erase("tela_det_alg");
}

TEST_CASE("complement_tela with tela_det_alg=inductive and sd_ind_sh_break=yes produces language-equivalent results to Spot",
TEST_CASE("complement_tela with tela_det_alg=inductive and sd_ind_sh_break=root produces language-equivalent results to Spot",
"[complement_tela][tela_det_alg][inductive][sd_ind_sh_break]") {
// Set up TELA options and enable inductive SD-TELA determinization with shared breakpoint
test_utils::setup_tela_options();
kofola::OPTIONS.params["tela_det_alg"] = "inductive";
kofola::OPTIONS.params["sd_ind_sh_break"] = "yes";
kofola::OPTIONS.params["sd_ind_sh_break"] = "root";

for (const std::string& filename : test_utils::COMMON_TEST_FILES) {
SECTION("Testing file (inductive det, shared breakpoint): " + filename) {
Expand All @@ -74,12 +74,34 @@ TEST_CASE("complement_tela with tela_det_alg=inductive and sd_ind_sh_break=yes p
kofola::OPTIONS.params.erase("tela_det_alg");
}

TEST_CASE("complement_tela with tela_det_alg=inductive and sd_ind_or_opt=yes produces language-equivalent results to Spot",
TEST_CASE("complement_tela with tela_det_alg=inductive and sd_ind_sh_break=inf_tree produces language-equivalent results to Spot",
"[complement_tela][tela_det_alg][inductive][sd_ind_sh_break]") {
// Set up TELA options and enable inductive SD-TELA determinization with shared breakpoint
test_utils::setup_tela_options();
kofola::OPTIONS.params["tela_det_alg"] = "inductive";
kofola::OPTIONS.params["sd_ind_sh_break"] = "inf_tree";

for (const std::string& filename : test_utils::COMMON_TEST_FILES) {
SECTION("Testing file (inductive det, shared breakpoint): " + filename) {
spot::twa_graph_ptr aut = test_utils::load_automaton_from_file(filename);
REQUIRE(aut != nullptr);

bool equivalent = test_utils::test_complement_equivalence(aut, false);
CHECK(equivalent);
}
}

// Clean up to avoid side-effects on other tests
kofola::OPTIONS.params.erase("sd_ind_sh_break");
kofola::OPTIONS.params.erase("tela_det_alg");
}

TEST_CASE("complement_tela with tela_det_alg=inductive and sd_ind_or_opt=root produces language-equivalent results to Spot",
"[complement_tela][tela_det_alg][inductive][sd_ind_or_opt]") {
// Set up TELA options and enable inductive SD-TELA determinization with OR-FIN optimization
test_utils::setup_tela_options();
kofola::OPTIONS.params["tela_det_alg"] = "inductive";
kofola::OPTIONS.params["sd_ind_or_opt"] = "yes";
kofola::OPTIONS.params["sd_ind_or_opt"] = "root";

for (const std::string& filename : test_utils::COMMON_TEST_FILES) {
SECTION("Testing file (inductive det, OR-FIN opt): " + filename) {
Expand All @@ -96,15 +118,67 @@ TEST_CASE("complement_tela with tela_det_alg=inductive and sd_ind_or_opt=yes pro
kofola::OPTIONS.params.erase("tela_det_alg");
}

TEST_CASE("complement_tela with tela_det_alg=inductive and both sd_ind_sh_break=yes and sd_ind_or_opt=yes",
TEST_CASE("complement_tela with tela_det_alg=inductive and sd_ind_or_opt=inf_tree produces language-equivalent results to Spot",
"[complement_tela][tela_det_alg][inductive][sd_ind_or_opt]") {
// Set up TELA options and enable inductive SD-TELA determinization with OR-FIN optimization
test_utils::setup_tela_options();
kofola::OPTIONS.params["tela_det_alg"] = "inductive";
kofola::OPTIONS.params["sd_ind_or_opt"] = "inf_tree";

for (const std::string& filename : test_utils::COMMON_TEST_FILES) {
SECTION("Testing file (inductive det, OR-FIN opt): " + filename) {
spot::twa_graph_ptr aut = test_utils::load_automaton_from_file(filename);
REQUIRE(aut != nullptr);

bool equivalent = test_utils::test_complement_equivalence(aut, false);
CHECK(equivalent);
}
}

// Clean up to avoid side-effects on other tests
kofola::OPTIONS.params.erase("sd_ind_or_opt");
kofola::OPTIONS.params.erase("tela_det_alg");
}

TEST_CASE("complement_tela with tela_det_alg=inductive and both sd_ind_sh_break=root and sd_ind_or_opt=yes",
"[complement_tela][tela_det_alg][inductive][sd_ind_sh_break][sd_ind_or_opt]") {
// Set up TELA options with both shared breakpoint and OR-FIN optimizations
test_utils::setup_tela_options();
kofola::OPTIONS.params["tela_det_alg"] = "inductive";
kofola::OPTIONS.params["sd_ind_sh_break"] = "root";
kofola::OPTIONS.params["sd_ind_or_opt"] = "yes";

for (const std::string& filename : test_utils::COMMON_TEST_FILES) {
if(filename == "tests/test_data/random_sd_streett_006.hoa") {
continue; // skip this file which is a known outlier for the OR-FIN optimization
}
SECTION("Testing file (inductive det, shared breakpoint + OR-FIN opt): " + filename) {
spot::twa_graph_ptr aut = test_utils::load_automaton_from_file(filename);
REQUIRE(aut != nullptr);

bool equivalent = test_utils::test_complement_equivalence(aut, false);
CHECK(equivalent);
}
}

// Clean up to avoid side-effects on other tests
kofola::OPTIONS.params.erase("sd_ind_or_opt");
kofola::OPTIONS.params.erase("sd_ind_sh_break");
kofola::OPTIONS.params.erase("tela_det_alg");
}

TEST_CASE("complement_tela with tela_det_alg=inductive and both sd_ind_sh_break=inf_tree and sd_ind_or_opt=yes",
"[complement_tela][tela_det_alg][inductive][sd_ind_sh_break][sd_ind_or_opt]") {
// Set up TELA options with both shared breakpoint and OR-FIN optimizations
test_utils::setup_tela_options();
kofola::OPTIONS.params["tela_det_alg"] = "inductive";
kofola::OPTIONS.params["sd_ind_sh_break"] = "yes";
kofola::OPTIONS.params["sd_ind_sh_break"] = "inf_tree";
kofola::OPTIONS.params["sd_ind_or_opt"] = "yes";

for (const std::string& filename : test_utils::COMMON_TEST_FILES) {
if(filename == "tests/test_data/random_sd_streett_006.hoa") {
continue; // skip this file which is a known outlier for the OR-FIN optimization
}
SECTION("Testing file (inductive det, shared breakpoint + OR-FIN opt): " + filename) {
spot::twa_graph_ptr aut = test_utils::load_automaton_from_file(filename);
REQUIRE(aut != nullptr);
Expand Down
Loading