Skip to content

Bimodal Z3 Semantics #70

@benbrastmckie

Description

@benbrastmckie

The bimodal semantics takes worlds to be partial functions from the integers to world states (bitvectors).
In order to quantify over worlds, I include a world_function which is variously constrained to enumerate the worlds so that I can quantify over these number (names of worlds) in the semantic clauses in bimodal/operators.py.

The current version of the theory_lib/bimodal/semantic.py sets up the following primitives:

def define_sorts(self):
    """Define the Z3 sorts used in the bimodal logic model.

    Create three sorts:
    - WorldStateSort: BitVecSort for representing world states as bitvectors
    - TimeSort: IntSort for representing time points
    - WorldIdSort: IntSort for mapping world IDs to world arrays
    """            
    self.WorldStateSort = z3.BitVecSort(self.N)
    self.TimeSort = z3.IntSort()
    self.WorldIdSort = z3.IntSort()


def define_primitives(self):
    """Define the Z3 primitive functions and relations used in the bimodal logic model.
    
    In bimodal logic we distinguish between:
    - World States: Instantaneous configurations of the system (e.g., {a, b, c})
    - World Histories: Temporally extended sequences of states that follow lawful transitions
    
    This method initializes:
    - task: A binary relation between world states representing transitions between states
    - world_function: A mapping from world IDs to world histories (arrays mapping time -> world state)
    - truth_condition: A function assigning truth values to atomic propositions at instantaneous world states
    - main_world: The primary world history used for evaluation (world_function applied to ID 0)
    - main_time: The time point at which sentences are evaluated
    - main_point: Dictionary containing the main world history and evaluation time
    - is_world: A boolean function indicating whether a world_id maps to a valid world history
    """
    # Define the task relation between world states
    self.task = z3.Function(
        "Task",
        self.WorldStateSort,
        self.WorldStateSort,
        z3.BoolSort()
    )

    # Mapping from world IDs to world histories (arrays from time to state)
    self.world_function = z3.Function(
        'world_function', 
        self.WorldIdSort,  # Input: world ID 
        z3.ArraySort(self.TimeSort, self.WorldStateSort)  # Output: world history
    )

    # Function to determine if a world_id maps to a valid world history
    self.is_world = z3.Function(
        'is_world',
        self.WorldIdSort,  # Input: world ID
        z3.BoolSort()      # Output: whether it's a valid world history
    )

    # Set a reasonable limit on world IDs for efficiency
    self.max_world_id = self.M * (2 ** (self.M * self.N))  # Number of possible world histories
    
    # Truth condition for atomic propositions at world states
    self.truth_condition = z3.Function(
        "truth_condition",
        self.WorldStateSort,
        syntactic.AtomSort,
        z3.BoolSort()
    )

    # Define interval tracking functions
    self.world_interval_start = z3.Function(
        'world_interval_start',
        self.WorldIdSort,  # World ID
        self.TimeSort      # Start time of interval
    )
    
    self.world_interval_end = z3.Function(
        'world_interval_end',
        self.WorldIdSort,  # World ID
        self.TimeSort      # End time of interval
    )
    
    # Dictionary to store world time intervals after extraction
    self.world_time_intervals = {}
    
    # Main point of evaluation includes a world ID and time
    self.main_world = 0             # Store world ID, not array reference
    self.main_time = z3.IntVal(0)   # Fix the main time to 0 
    self.main_point = {
        "world": self.main_world,  
        "time": self.main_time,
    } 

This works, but assumes that time is bounded above and below where the performance is not great.
I have nevertheless gotten the examples in bimodal/examples.py to work, though with some inconsistencies where there seems to be poor isolation between examples (running two examples can give different result from running just one).
I have tried various things to isolate examples but without success.

Separately, I have begun developing a version of the semantics which does not assume as much about time:

def define_sorts(self):
    """Define the Z3 sorts used in the bimodal logic model.

    Create three sorts:
    - WorldStateSort: BitVecSort for representing world states as bitvectors
    - TimeSort: IntSort for representing time points
    - WorldIdSort: IntSort for mapping world IDs to world arrays
    """            
    self.WorldStateSort = z3.BitVecSort(self.N)
    self.TimeSort = z3.IntSort()
    self.WorldIdSort = z3.IntSort()


def define_primitives(self):
    """Define the Z3 primitive functions and relations used in the bimodal logic model.
    
    In bimodal logic we distinguish between:
    - World States: Instantaneous configurations of the system (e.g., {a, b, c})
    - World Histories: Temporally extended sequences of states that follow lawful transitions
    
    This method initializes:
    - task: A binary relation between world states representing transitions between states
    - world_function: A mapping from world IDs to world histories (arrays mapping time -> world state)
    - truth_condition: A function assigning truth values to atomic propositions at instantaneous world states
    - main_world: The primary world history used for evaluation (world_function applied to ID 0)
    - main_time: The time point at which sentences are evaluated
    - main_point: Dictionary containing the main world history and evaluation time
    - is_world: A boolean function indicating whether a world_id maps to a valid world history
    - defined_at_time: A function indicating whether a world is defined at a specific time
    """
    
    # Define the task relation between world states
    self.task = z3.Function(
        "Task",
        self.WorldStateSort,
        self.WorldStateSort,
        z3.BoolSort()
    )

    # Mapping from world IDs to world histories (arrays from time to state)
    self.world_function = z3.Function(
        'world_function', 
        self.WorldIdSort,  # Input: world ID 
        z3.ArraySort(self.TimeSort, self.WorldStateSort)  # Output: world history
    )
    
    # Function to determine if a world_id maps to a valid world history
    self.is_world = z3.Function(
        'is_world',
        self.WorldIdSort,  # Input: world ID
        z3.BoolSort()      # Output: whether it's a valid world history
    )
    
    # Function to determine if a world is defined at a specific time
    self.defined_at_time = z3.Function(
        'defined_at_time',
        self.WorldIdSort,  # Input: world ID
        self.TimeSort,     # Input: time
        z3.BoolSort()      # Output: whether world is defined at time
    )
    
    # Remove explicit maximum world ID limit
    # We'll rely on Z3's natural minimality instead
    
    # Truth condition for atomic propositions at world states
    self.truth_condition = z3.Function(
        "truth_condition",
        self.WorldStateSort,
        syntactic.AtomSort,
        z3.BoolSort()
    )
    
    # Dictionary to store world time intervals after extraction
    self.world_time_intervals = {}
    
    # Main point of evaluation with world ID and time
    self.main_world = z3.IntVal(0)
    self.main_time = z3.IntVal(0)
    self.main_point = {
        "world": self.main_world,
        "time": self.main_time,
    }

This has proven to be faster in some ways, but much less consistent in its outputs, at least so far.
Here are the frame constraints that I am struggling to optimize to get the right logical consequences to go through without sinking Z3:

def build_frame_constraints(self):
    """Build the core frame constraints for the bimodal logic model.
    
    These constraints define the fundamental structure of legal models
    with a focus on logical correctness and generality.
    
    Returns:
        list: A list of Z3 constraints that define the frame conditions
    """
    constraints = []
    
    # 1. Main point exists - world 0 at time 0 is defined
    main_point_constraint = z3.And(
        self.is_world(self.main_world),
        self.defined_at_time(self.main_world, self.main_time)
    )
    
    # 2. World enumeration starts at 0 and is continuous
    enumerate_world = z3.Int('enumerate_world')
    enumerate_worlds = z3.ForAll(
        [enumerate_world],
        z3.Implies(
            self.is_world(enumerate_world),
            enumerate_world >= 0
        )
    )
    
    # 3. Worlds form a convex ordering (no gaps)
    convex_world = z3.Int('convex_world')
    convex_world_ordering = z3.ForAll(
        [convex_world],
        z3.Implies(
            z3.And(self.is_world(convex_world), convex_world > 0),
            self.is_world(convex_world - 1)
        )
    )
    
    # 4. All worlds have at least one time point
    world_has_time = z3.Int('world_has_time')
    time_existence = z3.Int('time_existence')
    worlds_have_times = z3.ForAll(
        [world_has_time],
        z3.Implies(
            self.is_world(world_has_time),
            z3.Exists(
                [time_existence],
                self.defined_at_time(world_has_time, time_existence)
            )
        )
    )

    # 5. Time intervals are coherent (no gaps)
    coherence_world = z3.Int('coherence_world')
    coherence_time = z3.Int('coherence_time') 
    future_time = z3.Int('future_time')
    between_time = z3.Int('between_time')
    coherence_constraint = z3.ForAll(
        [coherence_world, coherence_time, future_time],
        z3.Implies(
            z3.And(
                self.is_world(coherence_world),
                self.defined_at_time(coherence_world, coherence_time),
                self.defined_at_time(coherence_world, future_time),
                coherence_time < future_time
            ),
            # All times between must also be defined
            z3.ForAll(
                [between_time],
                z3.Implies(
                    z3.And(coherence_time < between_time, between_time < future_time),
                    self.defined_at_time(coherence_world, between_time)
                )
            )
        )
    )
    
    # 6. Each sentence letter has a definite truth value at each state
    world_state = z3.BitVec('world_state', self.N)
    sentence_letter = z3.Const('atom_interpretation', syntactic.AtomSort)
    classical_truth = z3.ForAll(
        [world_state, sentence_letter],
        z3.Or(
            self.truth_condition(world_state, sentence_letter),
            z3.Not(self.truth_condition(world_state, sentence_letter))
        )
    )
    
    # 7. Worlds are lawful (consecutive states follow task relation)
    lawful_world = z3.Int('lawful_world_id')
    lawful_time = z3.Int('lawful_time')
    worlds_are_lawful = z3.ForAll(
        [lawful_world, lawful_time],
        z3.Implies(
            z3.And(
                self.is_world(lawful_world),
                self.defined_at_time(lawful_world, lawful_time),
                self.defined_at_time(lawful_world, lawful_time + 1)
            ),
            self.task(
                z3.Select(self.world_function(lawful_world), lawful_time),
                z3.Select(self.world_function(lawful_world), lawful_time + 1)
            )
        )
    )

    # 8. Every valid world is unique - simple unified approach
    world_one = z3.Int('world_one')
    world_two = z3.Int('world_two')
    diff_time = z3.Int('diff_time')
    
    world_uniqueness = z3.ForAll(
        [world_one, world_two],
        z3.Implies(
            z3.And(
                self.is_world(world_one),
                self.is_world(world_two),
                world_one != world_two  # Different world IDs
            ),
            z3.Or(
                # CASE 1: Worlds differ in state at a shared time point
                z3.Exists([diff_time],
                    z3.And(
                        self.defined_at_time(world_one, diff_time),
                        self.defined_at_time(world_two, diff_time),
                        z3.Select(self.world_function(world_one), diff_time) !=
                        z3.Select(self.world_function(world_two), diff_time)
                    )
                ),
                
                # CASE 2: Worlds differ in temporal domain
                z3.Exists([diff_time],
                    z3.Or(
                        z3.And(
                            self.defined_at_time(world_one, diff_time),
                            z3.Not(self.defined_at_time(world_two, diff_time))
                        ),
                        z3.And(
                            z3.Not(self.defined_at_time(world_one, diff_time)),
                            self.defined_at_time(world_two, diff_time)
                        )
                    )
                )
            )
        )
    )

    # 9. All valid time-shifted worlds exist (abundance constraint) - simplified
    source_world = z3.Int('abundance_source_world')
    source_time = z3.Int('abundance_source_time')
    target_world = z3.Int('abundance_target_world')
    
    # Forward abundance (simplified)
    forward_abundance = z3.ForAll(
        [source_world, source_time],
        z3.Implies(
            z3.And(
                # When source world and time are defined
                self.is_world(source_world),
                self.defined_at_time(source_world, source_time),
                # And the next time is also defined
                self.defined_at_time(source_world, source_time + 1)
            ),
            # Then a witness world exists with that future state at time 0
            z3.Exists([target_world],
                z3.And(
                    self.is_world(target_world),
                    self.defined_at_time(target_world, 0),
                    z3.Select(self.world_function(target_world), 0) ==
                    z3.Select(self.world_function(source_world), source_time + 1)
                )
            )
        )
    )
    
    # Backward abundance (simplified)
    backward_abundance = z3.ForAll(
        [source_world, source_time],
        z3.Implies(
            z3.And(
                # When source world and time are defined
                self.is_world(source_world),
                self.defined_at_time(source_world, source_time),
                # And the previous time is also defined
                self.defined_at_time(source_world, source_time - 1)
            ),
            # Then a witness world exists with that past state at time 0
            z3.Exists([target_world],
                z3.And(
                    self.is_world(target_world),
                    self.defined_at_time(target_world, 0),
                    z3.Select(self.world_function(target_world), 0) ==
                    z3.Select(self.world_function(source_world), source_time - 1)
                )
            )
        )
    )
    
    # 10. Task relation constraint: tasks match actual world transitions
    some_state = z3.BitVec('task_restrict_some_state', self.N)
    next_state = z3.BitVec('task_restrict_next_state', self.N)
    task_world = z3.Int('task_world')
    task_time = z3.Int('task_time')

    task_restriction = z3.ForAll(
        [some_state, next_state],
        z3.Implies(
            # When task relation holds between states
            self.task(some_state, next_state),
            # Those states must appear in sequence in some world history
            z3.Exists([task_world, task_time],
                z3.And(
                    # The witness world must be a valid world
                    self.is_world(task_world),
                    # Both time points must be defined
                    self.defined_at_time(task_world, task_time),
                    self.defined_at_time(task_world, task_time + 1),
                    # The world has the states in sequence
                    some_state == z3.Select(self.world_function(task_world), task_time),
                    next_state == z3.Select(self.world_function(task_world), task_time + 1)
                )
            )
        )
    )

I'm curious if there are any other broad strategies or approaches that I might look into which could help.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions