Commit 8211084e authored by Bharat Garhewal's avatar Bharat Garhewal
Browse files

Merge branch 'incomplete_ads'

parents e9b8737c d5a319e5
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug app in the binary 'lsharp-ru'",
"program": "${workspaceRoot}/target/debug/lsharp-ru",
"args": [
"-e",
"iads",
"--rule2",
"ads",
"--rule3",
"ads",
"-m",
"experiment_models/BitVise.dot",
"-k",
"2",
"-x",
"42"
],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in library 'lsharp-ru'",
"cargo": {
"args": [
"test",
"--no-run",
"--lib",
"--package=lsharp-ru"
],
"filter": {
"name": "lsharp-ru",
"kind": "lib"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'lsharp-ru'",
"cargo": {
"args": [
"build",
"--bin=lsharp-ru",
"--package=lsharp-ru"
],
"filter": {
"name": "lsharp-ru",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in executable 'lsharp-ru'",
"cargo": {
"args": [
"test",
"--no-run",
"--bin=lsharp-ru",
"--package=lsharp-ru"
],
"filter": {
"name": "lsharp-ru",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
}
]
}
\ No newline at end of file
{
"cmake.configureOnOpen": false,
"[rust]": {
"editor.inlayHints.enabled": "off"
}
}
\ No newline at end of file
......@@ -5,9 +5,9 @@ authors = ["Bharat Garhewal <bharat@science.ru.nl>"]
edition = "2021"
keywords = ["active automata learning", "apartness relation", "mealy machine"]
description = "Active Automata Learning library implementing the L# algorithm."
homepage = "https://gitlab.science.ru.nl/bharat/automata-lib"
repository = "https://gitlab.science.ru.nl/bharat/automata-lib"
documentation = "https://bharat.pages.science.ru.nl/automata-lib/lsharp_ru/index.html"
homepage = "https://gitlab.science.ru.nl/sws/lsharp"
repository = "https://gitlab.science.ru.nl/sws/lsharp"
documentation = "https://sws.pages.science.ru.nl/lsharp/lsharp_ru/index.html"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
......@@ -21,6 +21,7 @@ rayon = "1.5.1"
log = "0.4.14"
chrono = "0.4.19"
rand = "0.8.5"
rand_distr = "0.4.3"
rand_core = "0.6.3"
rustc-hash = "1.1.0"
fnv = "1.0.7"
......@@ -31,7 +32,13 @@ derive-alias = "0.1.0"
num-format = "0.4.0"
priority-queue = "1.2.1"
[dev-dependencies]
assert_cmd = "2.0"
predicates = "2.1"
assert_fs = "1.0"
rstest = "0.12.0"
[profile.release]
debug = false
debug = true
lto = "fat"
codegen-units = 1
digraph g {
s1 [shape="circle" label="A"];
s2 [shape="circle" label="B"];
s3 [shape="circle" label="C"];
s4 [shape="circle" label="D"];
s5 [shape="circle" label="E"];
s1 -> s5 [label="a/1"];
s1 -> s3 [label="b/0"];
s1 -> s1 [label="c/0"];
s2 -> s4 [label="a/0"];
s2 -> s2 [label="b/1"];
s2 -> s3 [label="c/0"];
s3 -> s2 [label="a/1"];
s3 -> s1 [label="b/0"];
s3 -> s4 [label="c/0"];
s4 -> s5 [label="a/1"];
s4 -> s3 [label="b/0"];
s4 -> s5 [label="c/0"];
s5 -> s2 [label="a/1"];
s5 -> s2 [label="b/1"];
s5 -> s4 [label="c/0"];
__start0 [label="" shape="none" width="0" height="0"];
__start0 -> s1;
}
mod splitting_tree;
use std::collections::{HashMap, VecDeque};
use crate::{
automatadefs::{
mealy::{InputSymbol, Mealy, OutputSymbol, State},
traits::FiniteStateMachine,
},
util::data_structs::arena_tree::ArenaTree,
};
#[derive(Debug, Default, PartialEq, Eq)]
struct Node {
states: Vec<State>,
separator: Option<Vec<InputSymbol>>,
children: Vec<usize>,
depth: usize,
}
impl Node {
pub fn new(
states: impl IntoIterator<Item = State>,
separator: Option<Vec<InputSymbol>>,
depth: usize,
) -> Self {
Self {
states: states.into_iter().collect(),
separator,
children: vec![],
depth,
}
}
#[must_use]
pub fn separator(&self) -> Option<Vec<InputSymbol>> {
self.separator.clone()
}
fn add_separator(&mut self, sep: Vec<InputSymbol>) {
self.separator = Some(sep);
}
fn add_child(&mut self, child: usize) {
self.children.push(child);
}
}
#[derive(Debug, Default)]
struct Tree {
tree: ArenaTree<Node, ()>,
}
impl Tree {
fn new(hyp: &Mealy) -> Self {
let mut ret = Self::default();
let root_node = Node::new(hyp.states(), None, 0);
ret.add_node(root_node);
ret.construct_splitting_tree(hyp);
ret
}
fn construct_splitting_tree(&mut self, hyp: &Mealy) {
let input_alphabet = hyp.input_alphabet();
let mut current_days = 0;
let mut work_list = VecDeque::from([0]); // root_node is always at idx 0.
while let Some(curr_node_idx) = work_list.pop_front() {
let curr_block = &self.tree.arena[curr_node_idx].val.states;
let split = determine_split(hyp, self, curr_block);
match split {
Split::Output(input) => {
// Output split construction.
self.tree.arena[curr_node_idx]
.val
.add_separator(vec![input]);
}
Split::Destination(input, dest_idx) => todo!(),
Split::Invalid => {
work_list.push_back(curr_node_idx);
}
}
if current_days >= work_list.len() {
break;
}
current_days += 1;
}
todo!()
}
fn add_node(&mut self, node: Node) -> usize {
self.tree.node(node)
}
}
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
enum Split {
Output(InputSymbol),
Destination(InputSymbol, usize),
Invalid,
}
fn determine_split(hyp: &Mealy, tree: &Tree, curr_block: &[State]) -> Split {
// First, try to split on output.
for input in hyp.input_alphabet() {
let mut output_dest_map = HashMap::<OutputSymbol, HashMap<State, State>>::default();
let dest_overlap = curr_block
.iter()
.map(|&s| (s, hyp.step_from(s, input)))
.map(|(s, (d, o))| (s, o, d))
// Insert the triple as Output -> (Dest -> Src)
.map(|(s, o, d)| output_dest_map.entry(o).or_default().insert(d, s))
// Was another src state (with the same output) mapped to the same dest state?
.all(|x| x.is_none());
// All states have the same output, OR
// Two source states have the same output and destination for this input.
if output_dest_map.len() == 1 || dest_overlap {
continue;
}
return Split::Output(input);
}
// Second, try to split on states.
for input in hyp.input_alphabet() {
let mut output_dest_map = HashMap::<State, State>::default();
let valid_dest_split = curr_block
.iter()
.map(|&s| (s, hyp.step_from(s, input).0))
.map(|(s, d)| output_dest_map.insert(d, s))
.all(|x| x.is_none()); // Will not be none if two src states are mapped to same dest state.
if !valid_dest_split {
continue;
}
}
Split::Invalid
}
fn partition_on_output(
hyp: &Mealy,
block: &[State],
input: InputSymbol,
) -> HashMap<OutputSymbol, HashMap<State, Vec<State>>> {
let mut ret = HashMap::<OutputSymbol, HashMap<State, Vec<State>>>::default();
block
.iter()
.map(|&s| (s, hyp.step_from(s, input)))
.map(|(s, (d, o))| (s, o, d))
// Insert the triple as Output -> (Dest -> Src)
.for_each(|(s, o, d)| ret.entry(o).or_default().entry(d).or_default().push(s));
ret
}
fn partition<K, V>(hyp: &Mealy, src: State, input: InputSymbol, pos: usize) -> HashMap<K, V> {
let edge = hyp.step_from(src, input);
HashMap::default()
}
......@@ -12,7 +12,7 @@ use crate::{
use fnv::FnvHashMap;
use std::collections::VecDeque;
#[derive(PartialEq, Eq, Debug, Clone, Default)]
#[derive(PartialEq, Eq, Debug, Default)]
struct AdsNode {
/// The input to use for this node.
input: Option<InputSymbol>,
......@@ -22,7 +22,7 @@ struct AdsNode {
children: FnvHashMap<OutputSymbol, usize>,
}
#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct AdsTree {
ads: ArenaTree<AdsNode, ()>,
curr: usize,
......
use crate::automatadefs::mealy::{InputSymbol, Mealy, State};
use crate::automatadefs::traits::{FiniteStateMachine, InputWord};
use crate::automatadefs::traits::FiniteStateMachine;
use crate::util::data_structs::arena_tree::ArenaTree;
use fnv::FnvHashMap;
use std::collections::VecDeque;
......@@ -11,7 +11,7 @@ pub struct SplittingNode {
// Index of the child nodes.
children: Vec<usize>,
// Separating word for the block.
separator: Option<Box<InputWord>>,
separator: Option<Box<[InputSymbol]>>,
// Distance between root node and current node.
depth: usize,
}
......@@ -21,7 +21,7 @@ impl SplittingNode {
fn new(
states: Vec<State>,
children: Vec<usize>,
separator: Option<Box<InputWord>>,
separator: Option<Box<[InputSymbol]>>,
depth: usize,
) -> Self {
Self {
......@@ -33,10 +33,10 @@ impl SplittingNode {
}
#[must_use]
pub fn get_separator(&self) -> Option<Box<InputWord>> {
pub fn get_separator(&self) -> Option<Box<[InputSymbol]>> {
self.separator.clone()
}
fn add_separator(&mut self, sep: Box<InputWord>) {
fn add_separator(&mut self, sep: Box<[InputSymbol]>) {
self.separator = Some(sep);
}
fn add_child(&mut self, child: usize) {
......@@ -218,7 +218,7 @@ pub fn new(hyp: &Mealy) -> ArenaTree<SplittingNode, ()> {
fn refine_block(
curr_block: &[State],
hyp: &Mealy,
sep: Box<InputWord>,
sep: Box<[InputSymbol]>,
tree: &mut ArenaTree<SplittingNode, ()>,
curr_node_idx: usize,
) -> Vec<usize> {
......
......@@ -12,11 +12,13 @@ pub enum AdsStatus {
pub trait AdaptiveDistinguishingSequence {
/// Given the previous output, returns the next input, or an [`AdsStatus`].
///
/// Initially, prev_output will be ``None``, but after that, it is
/// Initially, `prev_output` will be ``None``, but after that, it is
/// required to provide the previous output, wrapped in a ``Some``.
/// ## [`AdsStatus`]
/// 1. DONE &Implies; No more inputs to send, and
/// 2. UNEXPECTED &Implies; Previous output was not expected.
/// # Errors
/// None.
fn next_input(&mut self, prev_output: Option<OutputSymbol>) -> Result<InputSymbol, AdsStatus>;
fn get_print_tree(&self) -> Box<[u8]>;
......
use super::utils::{compute_score, partition_on_output};
use super::utils::{compute_score, partition_on_output_reuse};
use crate::{
ads::traits::{AdaptiveDistinguishingSequence, AdsStatus},
automatadefs::{
mealy::{InputSymbol, OutputSymbol, State},
traits::ObservationTree,
},
util::data_structs::arena_tree::ArenaTree,
learner::apartness::compute_witness,
util::{data_structs::arena_tree::ArenaTree, toolbox},
};
use fnv::{FnvHashMap, FnvHashSet};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use std::{collections::VecDeque, io::Write};
use std::{
collections::{BTreeSet, VecDeque},
io::Write,
};
type InnerTree = ArenaTree<AdsNode, ()>;
#[derive(Debug, Default, Serialize, Deserialize)]
#[derive(Debug, Default)]
pub struct AdsTree {
tree: InnerTree,
curr_idx: usize,
initial_idx: usize,
seen: FnvHashMap<BTreeSet<State>, usize>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
......@@ -60,7 +66,12 @@ impl AdsTree {
#[must_use]
pub fn new<T: ObservationTree>(o_tree: &T, current_block: &[State]) -> Self {
let mut ret = Self::default();
let _initial_idx = ret.construct_ads(o_tree, current_block);
if current_block.len() == 1 {
let _ = ret.new_leaf();
} else {
let current_block: BTreeSet<_> = current_block.iter().copied().collect();
let _initial_idx = ret.construct_ads(o_tree, &current_block);
}
ret
}
......@@ -70,41 +81,47 @@ impl AdsTree {
}
#[must_use]
fn construct_ads<T: ObservationTree>(&mut self, o_tree: &T, current_block: &[State]) -> usize {
if current_block.len() == 1 {
let leaf_node = AdsNode::leaf_node();
return self.tree.node(leaf_node);
}
fn construct_ads<T: ObservationTree>(
&mut self,
o_tree: &T,
current_block: &BTreeSet<State>,
) -> usize {
let block_size = current_block.len();
let mut max_score = 0.0_f32;
assert_ne!(block_size, 1, "Leaves shouldn't be base-cases anymore.");
let mut max_score = 0.0_f64;
let mut max_idx = 0;
let mut num_undef = 0;
if let Some(&ret) = self.seen.get(current_block) {
return ret;
}
// For each input, compute the different outputs
// and their sub-trees.
let input_size = o_tree.input_size();
for i in (0..input_size)
.into_iter()
.map(|x| x as u16)
.map(InputSymbol::from)
{
let input_alphabet = toolbox::inputs_iterator(input_size);
let mut i_children = FnvHashMap::default();
let mut o_partitions = FnvHashMap::<OutputSymbol, BTreeSet<State>>::default();
for i in input_alphabet {
let mut i_score = 0.0;
let mut i_children = FnvHashMap::default();
i_children.clear();
// Get the outputs and the parititions for input i.
let o_partitions = partition_on_output(o_tree, current_block, i);
o_partitions.clear();
partition_on_output_reuse(o_tree, current_block, i, &mut o_partitions);
if o_partitions.is_empty() {
num_undef += 1;
continue;
}
// Compute the sub-trees for the parititions.
let u_i = o_partitions.values().map(|x| x.len()).sum::<usize>();
for (o, o_part) in o_partitions {
let i_subtree_idx = self.construct_ads(o_tree, &o_part);
let child_score = self.tree.arena[i_subtree_idx].val.get_score();
let u_i: usize = o_partitions
.values()
.map(std::collections::BTreeSet::len)
.sum();
for (o, o_part) in &o_partitions {
let u_i_o = o_part.len();
let i_subtree_idx = self.ads_recurse(o_tree, o_part);
let child_score = self.tree.arena[i_subtree_idx].val.get_score();
// assert!(u_i_o <= u_i);
i_children.insert(o, i_subtree_idx);
i_children.insert(*o, i_subtree_idx);
i_score += compute_score(
// block_size as f32,
u_i as f64,
......@@ -113,32 +130,118 @@ impl AdsTree {
(0.99 * child_score) as f64,
);
}
if i_score <= max_score.into() {
if i_score <= max_score {
continue;
}
if perfect_split(i_score, block_size) {
let i_tree_node = AdsNode {
score: i_score as f32,
input: Some(i),
children: i_children.clone(),
block_size,
};
let i_node_idx = self.tree.node(i_tree_node);
max_idx = i_node_idx;
break;
}
let i_tree_node = AdsNode {
score: i_score as f32,
input: Some(i),
children: i_children,
children: i_children.clone(),
block_size,
};
let i_node_idx = self.tree.node(i_tree_node);
// if i_score > max_score {
max_idx = i_node_idx;
max_score = i_score as f32;
// }
max_score = i_score;
}
if num_undef == input_size {
let tree_node = AdsNode {
block_size: current_block.len(),
..Default::default()
};
return self.tree.node(tree_node);
let node_idx = self.tree.node(tree_node);
self.seen
.insert(current_block.iter().copied().collect(), node_idx);
return node_idx;
}
self.curr_idx = max_idx;
self.initial_idx = self.curr_idx;
self.seen
.insert(current_block.iter().copied().collect(), max_idx);
max_idx
}
fn ads_recurse<T: ObservationTree>(&mut self, o_tree: &T, o_part: &BTreeSet<State>) -> usize {
match o_part.len() {
0 => unreachable!("Impossible case, as o_partitions is not empty."),
1 => {
// If we have a single state, just make a leaf.
self.new_leaf()
}
2 => {
// For a pair of states, simply compute the witness (if it exists)
// and then manually construct the nodes.
let (&s1, &s2) = o_part.iter().collect_tuple().expect("Safe");
let pair_wit = compute_witness(o_tree, s1, s2);
if let Some(wit) = pair_wit {
let out_1 = o_tree.get_observation(Some(s1), &wit).expect("safe");
let out_2 = o_tree.get_observation(Some(s2), &wit).expect("safe");
self.construct_witness_subtree(&wit, &out_1, &out_2)
} else {
self.tree.node(AdsNode {
block_size: 2,
..Default::default()
})
}
}
// For all other cases, recurse.
_ => self.construct_ads(o_tree, o_part),
}
}
/// Construct an ADS given a witness and a pair of outputs.
/// Returns the index of the root of the ADS.
fn construct_witness_subtree(
&mut self,
pair_wit: &[InputSymbol],
out_1: &[OutputSymbol],
out_2: &[OutputSymbol],
) -> usize {
let (&split, prefix) = pair_wit.split_last().expect("Safe");
let c1 = self.new_leaf();
let c2 = self.new_leaf();
let (&split_out1, prefix_out) = out_1.split_last().expect("safe");
let (&split_out2, _) = out_2.split_last().expect("safe");
let split_node = self.tree.node(AdsNode {
input: Some(split),