Skip to content

Conversation

@mlaveaux
Copy link
Owner

@mlaveaux mlaveaux commented Dec 6, 2025

This is a weak bisimulation algorithm based on partition refinement due to Eduardo, without explicitly computing the signatures explicitly.

@mlaveaux mlaveaux self-assigned this Dec 6, 2025
@mlaveaux mlaveaux added the enhancement New feature or request label Dec 6, 2025
@mlaveaux mlaveaux requested a review from Copilot December 6, 2025 15:36
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a naive weak bisimulation algorithm based on partition refinement, proposed by Eduardo Costa Martins. The implementation avoids explicitly computing signatures for efficiency.

Key changes:

  • Adds a new weak_bisimulation module implementing the partition refinement algorithm
  • Introduces SimpleBlockPartition data structure for managing block partitions during reduction
  • Integrates the new algorithm into the existing reduction and comparison infrastructure

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
crates/reduction/src/weak_bisimulation.rs New file implementing the core weak bisimulation algorithm with partition refinement
crates/reduction/src/simple_block_partition.rs New data structure for managing block partitions with split operations
crates/reduction/src/reduce.rs Implements the WeakBisim equivalence case using the new algorithm
crates/reduction/src/lib.rs Exports the new modules
crates/reduction/src/compare.rs Integrates weak bisimulation for LTS comparison

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +112 to +122
fn block_number(&self, state_index: StateIndex) -> BlockIndex {
for (block_index, block) in self.blocks.iter().enumerate() {
for element in block.iter(&self.elements) {
if element == state_index {
return BlockIndex::new(block_index);
}
}
}

panic!("State index {:?} not found in partition {:?}", state_index, self);
}
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation has O(n*m) complexity where n is the number of blocks and m is the average block size. This could be optimized by maintaining a reverse mapping from state indices to block indices, making lookups O(1) instead of requiring iteration through all blocks and their elements.

Copilot uses AI. Check for mistakes.
@mlaveaux mlaveaux force-pushed the feature/weak-bisim branch 2 times, most recently from 4730728 to 07a9994 Compare December 6, 2025 19:00
@mlaveaux mlaveaux requested a review from Copilot December 6, 2025 19:28
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.

);

for block_prime in (0usize..blocks.num_of_blocks()).map(BlockIndex::new) {
// This cannot be a reference since the blocks are updated.
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment "This cannot be a reference since the blocks are updated" is misleading. The block_prime variable is a BlockIndex (which is a Copy type), not a reference to a block. The comment seems to be explaining why we can't hold a reference to the actual block data structure (from blocks.block()), but the code doesn't attempt to do that. Consider clarifying or removing this comment.

Suggested change
// This cannot be a reference since the blocks are updated.

Copilot uses AI. Check for mistakes.
Comment on lines +133 to +138
/// A block stores a subset of the elements in a partition. It uses start,
/// middle and end to indicate a range start..end of elements in the partition.
/// The middle is used such that marked_split..end are the marked elements. This
/// is useful to be able to split off new blocks cheaply.
///
/// Invariant: start <= middle <= end && start < end.
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation mentions "start, middle and end" and "marked_split..end" fields, but the SimpleBlock struct only has begin, end, and stable fields. This appears to be outdated documentation, possibly copied from a different block implementation that uses a middle marker for efficient splitting. Update the documentation to accurately describe this implementation's approach using begin and end ranges.

Suggested change
/// A block stores a subset of the elements in a partition. It uses start,
/// middle and end to indicate a range start..end of elements in the partition.
/// The middle is used such that marked_split..end are the marked elements. This
/// is useful to be able to split off new blocks cheaply.
///
/// Invariant: start <= middle <= end && start < end.
/// A block stores a subset of the elements in a partition. It uses `begin` and
/// `end` to indicate a contiguous range of elements in the partition, represented
/// by the indices `begin..end` into the partition's element list.
///
/// Invariant: begin <= end && begin < end.

Copilot uses AI. Check for mistakes.
Comment on lines +113 to +120
for transition in lts.outgoing_transitions(s) {
if transition.label == label {
// s.act_mark := true iff a != tau && tau_mark[t]
if !lts.is_hidden_label(transition.label) && tau_mark[*transition.to] {
act_mark.set(
*s,
true,
);
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition !lts.is_hidden_label(transition.label) is redundant here since transition.label == label (from line 114). This is equivalent to checking !lts.is_hidden_label(label), which could be moved outside the loop for better clarity and efficiency. Consider restructuring this logic to check the label type once before the loop.

Suggested change
for transition in lts.outgoing_transitions(s) {
if transition.label == label {
// s.act_mark := true iff a != tau && tau_mark[t]
if !lts.is_hidden_label(transition.label) && tau_mark[*transition.to] {
act_mark.set(
*s,
true,
);
if !lts.is_hidden_label(label) {
for transition in lts.outgoing_transitions(s) {
if transition.label == label {
// s.act_mark := true iff a != tau && tau_mark[t]
if tau_mark[*transition.to] {
act_mark.set(
*s,
true,
);
}

Copilot uses AI. Check for mistakes.
Comment on lines +95 to +96
/// Sets s.act_mark to true iff exists t: S. s =!a=> t
/// If a = tau, then also updates s.tau_mark
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment uses mathematical notation "s =!a=> t" but doesn't explain what it means. For readers unfamiliar with weak bisimulation theory, consider expanding this to explain that "s =!a=> t" represents a weak transition (zero or more tau transitions, followed by an 'a' transition, followed by zero or more tau transitions) from state s to state t.

Suggested change
/// Sets s.act_mark to true iff exists t: S. s =!a=> t
/// If a = tau, then also updates s.tau_mark
/// Sets s.act_mark to true iff there exists t ∈ S such that s =!a=> t.
/// Here, "s =!a=> t" denotes a weak transition: there is a sequence of zero or more tau (silent) transitions from s,
/// followed by an 'a' transition, followed by zero or more tau transitions, leading to t.
/// If a = tau, then also updates s.tau_mark.

Copilot uses AI. Check for mistakes.
Comment on lines +133 to +138
/// A block stores a subset of the elements in a partition. It uses start,
/// middle and end to indicate a range start..end of elements in the partition.
/// The middle is used such that marked_split..end are the marked elements. This
/// is useful to be able to split off new blocks cheaply.
///
/// Invariant: start <= middle <= end && start < end.
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The invariant documentation references a "middle" field that doesn't exist in the struct. The actual invariant should be begin < end (as enforced by the debug_assert on line 149). Update this comment to reflect the actual struct fields.

Suggested change
/// A block stores a subset of the elements in a partition. It uses start,
/// middle and end to indicate a range start..end of elements in the partition.
/// The middle is used such that marked_split..end are the marked elements. This
/// is useful to be able to split off new blocks cheaply.
///
/// Invariant: start <= middle <= end && start < end.
/// A block stores a subset of the elements in a partition. It uses `begin` and
/// `end` to indicate a range `begin..end` of elements in the partition.
///
/// Invariant: begin < end.

Copilot uses AI. Check for mistakes.
for t in lts.iter_states() {
// t.tau_mark := t.act_mark if a == tau
if lts.is_hidden_label(label) {
tau_mark.set(*t, act_mark[*t]);
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tau_mark bitvector is never cleared and persists across different blocks and outer loop iterations. On line 129, tau_mark is set when processing the tau label for one block, but this value persists when processing other blocks and in subsequent outer loop iterations. This could lead to incorrect tau-reachability information being used. Consider clearing tau_mark at appropriate points (e.g., before processing each block, or at the start of compute_weak_act when lts.is_hidden_label(label)).

Copilot uses AI. Check for mistakes.
Comment on lines +112 to +122
fn block_number(&self, state_index: StateIndex) -> BlockIndex {
for (block_index, block) in self.blocks.iter().enumerate() {
for element in block.iter(&self.elements) {
if element == state_index {
return BlockIndex::new(block_index);
}
}
}

panic!("State index {:?} not found in partition {:?}", state_index, self);
}
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The block_number implementation uses a linear search through all blocks and their elements, resulting in O(n*m) complexity where n is the number of blocks and m is the average block size. This could be a significant performance bottleneck since partition refinement algorithms frequently query block membership. Consider maintaining an auxiliary state-to-block mapping (similar to IndexedPartition) for O(1) lookups, or document this performance characteristic if the linear search is intentional for the "naive" implementation.

Copilot uses AI. Check for mistakes.
/// Apply weak bisimulation reduction
pub fn weak_bisimulation(lts: impl LTS, timing: &mut Timing) -> (LabelledTransitionSystem, SimpleBlockPartition) {
let mut time_pre = timing.start("preprocessing");
let scc_partition = tau_scc_decomposition(&lts);
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scc_partition variable is computed but never used. The preprocess_branching function (line 32) internally calls tau_scc_decomposition as well, making this line redundant. Consider removing this unused variable.

Suggested change
let scc_partition = tau_scc_decomposition(&lts);

Copilot uses AI. Check for mistakes.
Comment on lines +165 to +174
let expected = reduce_lts(lts.clone(), Equivalence::WeakBisim, &mut timing);
let reduced = reduce_lts(lts, Equivalence::WeakBisimSigref, &mut timing);

assert_eq!(expected.num_of_states(), reduced.num_of_states());
assert_eq!(expected.num_of_transitions(), reduced.num_of_transitions());

println!("Expected: {expected:?}");
println!("Reduced: {reduced:?}");

assert!(compare_lts(Equivalence::StrongBisim, expected, &reduced, &mut timing));
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable names expected and reduced are misleading. Line 165 uses Equivalence::WeakBisim (the new implementation being added in this PR) and assigns it to expected, while line 166 uses Equivalence::WeakBisimSigref (the reference implementation) and assigns it to reduced. These names should be swapped, or better yet, use more descriptive names like new_impl and reference_impl to clearly indicate which is the implementation under test.

Suggested change
let expected = reduce_lts(lts.clone(), Equivalence::WeakBisim, &mut timing);
let reduced = reduce_lts(lts, Equivalence::WeakBisimSigref, &mut timing);
assert_eq!(expected.num_of_states(), reduced.num_of_states());
assert_eq!(expected.num_of_transitions(), reduced.num_of_transitions());
println!("Expected: {expected:?}");
println!("Reduced: {reduced:?}");
assert!(compare_lts(Equivalence::StrongBisim, expected, &reduced, &mut timing));
let new_impl = reduce_lts(lts.clone(), Equivalence::WeakBisim, &mut timing);
let reference_impl = reduce_lts(lts, Equivalence::WeakBisimSigref, &mut timing);
assert_eq!(new_impl.num_of_states(), reference_impl.num_of_states());
assert_eq!(new_impl.num_of_transitions(), reference_impl.num_of_transitions());
println!("New implementation: {new_impl:?}");
println!("Reference implementation: {reference_impl:?}");
assert!(compare_lts(Equivalence::StrongBisim, new_impl, &reference_impl, &mut timing));

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants