diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..8313ef1 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,119 @@ +name: build +on: [push] + +## At the moment, everything is running on nightly. Once Rocket 0.5 is out, stable/beta can be enabled. + +jobs: + # Check formatting + fmt: + name: Rustfmt + runs-on: ubuntu-latest + env: + RUSTFLAGS: "-D warnings" + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + - run: rustup component add rustfmt + - uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + + # Run basic code validity check. + check: + needs: fmt + name: Check + runs-on: ubuntu-latest + env: + RUSTFLAGS: "-D warnings" + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + - uses: actions-rs/cargo@v1 + with: + command: check + + # Run all tests + #test: + # needs: check + # name: Test Suite + # runs-on: ubuntu-latest + # env: + # RUSTFLAGS: "-D warnings" + # steps: + # - uses: actions/checkout@v2 + # - uses: actions-rs/toolchain@v1 + # with: + # profile: minimal + # toolchain: stable + # override: true + # - uses: actions-rs/cargo@v1 + # with: + # command: test + # args: --features "shields_up" + + # Run all tests, but with beta + #test-beta: + # needs: check + # name: Test Suite (Beta) + # runs-on: ubuntu-latest + # env: + # RUSTFLAGS: "-D warnings" + # steps: + # - uses: actions/checkout@v2 + # - uses: actions-rs/toolchain@v1 + # with: + # profile: minimal + # toolchain: beta + # override: true + # - uses: actions-rs/cargo@v1 + # with: + # command: test + # args: --features "shields_up" + + # Run all tests, but with nightly + test-nightly: + needs: check + name: Test Suite (Nightly) + runs-on: ubuntu-latest + env: + RUSTFLAGS: "-D warnings" + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + - uses: actions-rs/cargo@v1 + with: + command: test + args: --features "shields_up" + + # Check code style + clippy: + needs: check + name: Clippy + runs-on: ubuntu-latest + env: + RUSTFLAGS: "-D warnings" + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + - run: rustup component add clippy + - uses: actions-rs/cargo@v1 + with: + command: clippy \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index d5b8c1d..0b95c18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,15 +16,15 @@ debug = true #lto = true [dependencies] -biodivine-lib-std = { git = "https://github.com/sybila/biodivine-lib-std.git" } -biodivine-lib-param-bn = { git = "https://github.com/sybila/biodivine-lib-param-bn.git", tag = "v0.1.0-beta.2" } -biodivine-lib-bdd = "0.2.0" +biodivine-lib-bdd = "0.2.1" +biodivine-lib-param-bn = "0.1.0" rocket = "0.4.6" rayon = "1.5.0" crossbeam = "0.8.0" lazy_static = "1.4.0" json = "0.12.4" regex = "1.4.2" +rand = "0.8.1" [features] # With shields_up you enable explicit assertions of pre/post conditions and invariants in critical diff --git a/bench.py b/bench.py index 07371f6..81b55a6 100644 --- a/bench.py +++ b/bench.py @@ -4,6 +4,7 @@ re_bench = re.compile("\[v(\d+)\]__\[r(\d+)\]__\[(.+?)\]__\[(.+?)\]") re_var_count = re.compile("\[v(\d+)\]") +re_elapsed = re.compile("Elapsed time: (\d+\.?\d*)s") def is_bench(benchmark): return re_bench.match(benchmark) != None @@ -16,11 +17,15 @@ def bench_cmp(benchmark): benchmarks = sorted(benchmarks, key=bench_cmp) -out_dir = "bench" + str(int(time.time())) +out_dir = "bench_run_" + str(int(time.time())) os.mkdir(out_dir) +csv = open(out_dir + "/stats.csv", "w") +csv.write("Benchmark, Time[s]\n") + +elapsed_times = {} i = 1 -for benchmark in benchmarks: +for benchmark in benchmarks: bench_name = re_bench.match(benchmark).group(3) print("Starting "+bench_name) in_file = "./benchmark/" + benchmark + "/model.aeon" @@ -30,5 +35,21 @@ def bench_cmp(benchmark): with open(out_file, 'r') as f: lines = f.read().splitlines() time_line = lines[-1] - class_line = lines[-3] - print(class_line + " " + time_line) \ No newline at end of file + if re_elapsed.match(time_line): + class_line = lines[-3] + print("Success: " + class_line + " " + time_line) + time = re_elapsed.match(time_line).group(1) + elapsed_times[bench_name] = time + csv.write(bench_name + ", " + str(time) + "\n") + else: + print("Failed!") + elapsed_times[bench_name] = "Fail" + csv.write(bench_name + ", " + "Fail" + "\n") + csv.flush() + +print "FINISHED" +print "Benchmark, Time[s]" +for bench, time in elapsed_times.items(): + print bench + ", " + str(time) + +csv.close() diff --git a/src/_impl_graph_task_context.rs b/src/_impl_graph_task_context.rs new file mode 100644 index 0000000..5f801d1 --- /dev/null +++ b/src/_impl_graph_task_context.rs @@ -0,0 +1,49 @@ +use crate::scc::ProgressTracker; +use crate::GraphTaskContext; +use biodivine_lib_param_bn::symbolic_async_graph::{GraphColoredVertices, SymbolicAsyncGraph}; +use std::sync::atomic::{AtomicBool, Ordering}; + +impl Default for GraphTaskContext { + fn default() -> Self { + GraphTaskContext::new() + } +} + +impl GraphTaskContext { + /// Create a new task context. + pub fn new() -> GraphTaskContext { + GraphTaskContext { + is_cancelled: AtomicBool::new(false), + progress: ProgressTracker::new(), + } + } + + /// Re-initialize the task context with a fresh graph. + pub fn restart(&self, graph: &SymbolicAsyncGraph) { + self.progress.init_from_graph(graph); + self.is_cancelled.store(false, Ordering::SeqCst); + } + + /// True if the task is cancelled. + pub fn is_cancelled(&self) -> bool { + self.is_cancelled.load(Ordering::SeqCst) + } + + /// Set the status of this task to cancelled. + /// + /// Return true if the computation was set to cancelled by this call, false if it was + /// cancelled previously. + pub fn cancel(&self) -> bool { + !self.is_cancelled.swap(true, Ordering::SeqCst) + } + + /// Indicate that the given set still needs to be processed by the task. + pub fn update_remaining(&self, remaining: &GraphColoredVertices) { + self.progress.update_remaining(remaining); + } + + /// Output a string which represent the percentage of remaining state space. + pub fn get_percent_string(&self) -> String { + self.progress.get_percent_string() + } +} diff --git a/src/all/mod.rs b/src/all/mod.rs index 97297b3..e2ac3c0 100644 --- a/src/all/mod.rs +++ b/src/all/mod.rs @@ -29,7 +29,7 @@ pub enum AttractorAtom { } #[derive(Clone, Debug, Eq, PartialEq)] -pub enum ALLAtom { +pub enum AllAtom { AllAttractors(AttractorFormula), SomeAttractor(AttractorFormula), } @@ -38,7 +38,7 @@ pub type StateFormula = BooleanFormula; pub type AttractorFormula = BooleanFormula; -pub type ALLFormula = BooleanFormula; +pub type AllFormula = BooleanFormula; /* impl ALLFormula { pub fn eval( diff --git a/src/bdt/_attributes_for_network.rs b/src/bdt/_attributes_for_network.rs index 0e98fcb..3e9de4d 100644 --- a/src/bdt/_attributes_for_network.rs +++ b/src/bdt/_attributes_for_network.rs @@ -1,12 +1,12 @@ -use crate::bdt::{Attribute, BifurcationFunction, BDT}; +use crate::bdt::{Attribute, Bdt, BifurcationFunction}; use crate::util::functional::Functional; use biodivine_lib_bdd::Bdd; +use biodivine_lib_param_bn::biodivine_std::traits::Set; use biodivine_lib_param_bn::symbolic_async_graph::SymbolicAsyncGraph; use biodivine_lib_param_bn::{FnUpdate, VariableId}; -use biodivine_lib_std::param_graph::Params; -impl BDT { - pub fn new_from_graph(classes: BifurcationFunction, graph: &SymbolicAsyncGraph) -> BDT { +impl Bdt { + pub fn new_from_graph(classes: BifurcationFunction, graph: &SymbolicAsyncGraph) -> Bdt { let mut attributes = Vec::new(); attributes_for_network_inputs(graph, &mut attributes); attributes_for_constant_parameters(graph, &mut attributes); @@ -25,22 +25,22 @@ impl BDT { is_not_empty }) .collect(); - BDT::new(classes, attributes) + Bdt::new(classes, attributes) } } /// **(internal)** Construct basic attributes for all input variables. fn attributes_for_network_inputs(graph: &SymbolicAsyncGraph, out: &mut Vec) { - for v in graph.network().variables() { + for v in graph.as_network().variables() { // v is input if it has no update function and no regulators - let is_input = graph.network().regulators(v).is_empty(); - let is_input = is_input && graph.network().get_update_function(v).is_none(); + let is_input = graph.as_network().regulators(v).is_empty(); + let is_input = is_input && graph.as_network().get_update_function(v).is_none(); if is_input { let bdd = graph .symbolic_context() - .mk_implicit_function_is_true(v, &vec![]); + .mk_implicit_function_is_true(v, &[]); out.push(Attribute { - name: graph.network().get_variable_name(v).clone(), + name: graph.as_network().get_variable_name(v).clone(), negative: graph.empty_colors().copy(bdd.not()), positive: graph.empty_colors().copy(bdd), }) @@ -50,14 +50,14 @@ fn attributes_for_network_inputs(graph: &SymbolicAsyncGraph, out: &mut Vec) { - for p in graph.network().parameters() { - if graph.network()[p].get_arity() == 0 { + for p in graph.as_network().parameters() { + if graph.as_network()[p].get_arity() == 0 { // Parameter is a constant let bdd = graph .symbolic_context() - .mk_uninterpreted_function_is_true(p, &vec![]); + .mk_uninterpreted_function_is_true(p, &[]); out.push(Attribute { - name: graph.network()[p].get_name().clone(), + name: graph.as_network()[p].get_name().clone(), negative: graph.empty_colors().copy(bdd.not()), positive: graph.empty_colors().copy(bdd), }) @@ -68,9 +68,9 @@ fn attributes_for_constant_parameters(graph: &SymbolicAsyncGraph, out: &mut Vec< /// **(internal)** If some regulation has a missing static constraint, try to build it /// and add it as an attribute. fn attributes_for_missing_constraints(graph: &SymbolicAsyncGraph, out: &mut Vec) { - let network = graph.network(); + let network = graph.as_network(); let context = graph.symbolic_context(); - for reg in graph.network().as_graph().regulations() { + for reg in graph.as_network().as_graph().regulations() { // This is straight up copied from static constraint analysis in lib-param-bn. // For more context, go there. let target = reg.get_target(); @@ -143,26 +143,26 @@ fn attributes_for_missing_constraints(graph: &SymbolicAsyncGraph, out: &mut Vec< /// **(internal)** Make an explicit attributes (like `f[1,0,1] = 1`) for every implicit update /// function row in the network. fn attributes_for_implicit_function_tables(graph: &SymbolicAsyncGraph, out: &mut Vec) { - for v in graph.network().variables() { - let is_implicit_function = graph.network().get_update_function(v).is_none(); + for v in graph.as_network().variables() { + let is_implicit_function = graph.as_network().get_update_function(v).is_none(); let is_implicit_function = - is_implicit_function && !graph.network().regulators(v).is_empty(); + is_implicit_function && !graph.as_network().regulators(v).is_empty(); if is_implicit_function { let table = graph.symbolic_context().get_implicit_function_table(v); for (ctx, var) in table { let bdd = graph.symbolic_context().bdd_variable_set().mk_var(var); let ctx: Vec = ctx .into_iter() - .zip(graph.network().regulators(v)) + .zip(graph.as_network().regulators(v)) .map(|(b, r)| { format!( "{}{}", if b { "" } else { "¬" }, - graph.network().get_variable_name(r) + graph.as_network().get_variable_name(r) ) }) .collect(); - let name = format!("{}{:?}", graph.network().get_variable_name(v), ctx); + let name = format!("{}{:?}", graph.as_network().get_variable_name(v), ctx); out.push(Attribute { name: name.replace("\"", ""), negative: graph.mk_empty_colors().copy(bdd.not()), @@ -175,8 +175,8 @@ fn attributes_for_implicit_function_tables(graph: &SymbolicAsyncGraph, out: &mut /// **(internal)** Make an explicit argument for every explicit parameter function row in the network. fn attributes_for_explicit_function_tables(graph: &SymbolicAsyncGraph, out: &mut Vec) { - for p in graph.network().parameters() { - let parameter = graph.network().get_parameter(p); + for p in graph.as_network().parameters() { + let parameter = graph.as_network().get_parameter(p); if parameter.get_arity() > 0 { let table = graph.symbolic_context().get_explicit_function_table(p); let arg_names = (0..parameter.get_arity()) @@ -203,9 +203,9 @@ fn attributes_for_explicit_function_tables(graph: &SymbolicAsyncGraph, out: &mut /// Create "conditional observability" attributes for both implicit and explicit update functions. fn attributes_for_conditional_observability(graph: &SymbolicAsyncGraph, out: &mut Vec) { let context = graph.symbolic_context(); - let network = graph.network(); - for v in graph.network().variables() { - let regulators = graph.network().regulators(v); + let network = graph.as_network(); + for v in graph.as_network().variables() { + let regulators = graph.as_network().regulators(v); // Bdd that is true when update function for this variable is true let fn_is_true = if let Some(function) = network.get_update_function(v) { diff --git a/src/bdt/_impl_attribute.rs b/src/bdt/_impl_attribute.rs index 47161db..34fca09 100644 --- a/src/bdt/_impl_attribute.rs +++ b/src/bdt/_impl_attribute.rs @@ -1,7 +1,7 @@ use crate::bdt::{Attribute, BifurcationFunction}; use crate::util::functional::Functional; +use biodivine_lib_param_bn::biodivine_std::traits::Set; use biodivine_lib_param_bn::symbolic_async_graph::GraphColors; -use biodivine_lib_std::param_graph::Params; use std::collections::HashMap; impl Attribute { diff --git a/src/bdt/_impl_bdt.rs b/src/bdt/_impl_bdt.rs index c875e33..c33152d 100644 --- a/src/bdt/_impl_bdt.rs +++ b/src/bdt/_impl_bdt.rs @@ -1,17 +1,19 @@ use crate::bdt::{ - entropy, information_gain, AppliedAttribute, Attribute, AttributeId, AttributeIds, BDTNode, - BDTNodeId, BDTNodeIds, BifurcationFunction, BDT, + entropy, information_gain, AppliedAttribute, Attribute, AttributeId, AttributeIds, Bdt, + BdtNode, BdtNodeId, BdtNodeIds, BifurcationFunction, }; use crate::scc::Class; use crate::util::functional::Functional; use crate::util::index_type::IndexType; +use biodivine_lib_param_bn::biodivine_std::traits::Set; use biodivine_lib_param_bn::symbolic_async_graph::GraphColors; use std::collections::{HashMap, HashSet}; +use std::option::Option::Some; -impl BDT { +impl Bdt { /// Create a new single-node tree for given classification and attributes. - pub fn new(classes: BifurcationFunction, attributes: Vec) -> BDT { - BDT { + pub fn new(classes: BifurcationFunction, attributes: Vec) -> Bdt { + Bdt { attributes, storage: HashMap::new(), next_id: 0, @@ -20,43 +22,61 @@ impl BDT { } /// Node ID of the tree root. - pub fn root_id(&self) -> BDTNodeId { - BDTNodeId(0) + pub fn root_id(&self) -> BdtNodeId { + BdtNodeId(0) } /// Iterator over all valid node ids in this tree. - pub fn nodes(&self) -> BDTNodeIds { - self.storage.keys().map(|x| BDTNodeId(*x)) + pub fn nodes(&self) -> BdtNodeIds { + self.storage.keys().map(|x| BdtNodeId(*x)) } /// Iterator over all attribute ids in this tree. pub fn attributes(&self) -> AttributeIds { - (0..self.attributes.len()).map(|x| AttributeId(x)) + (0..self.attributes.len()).map(AttributeId) } /// Get leaf parameter set if the given node is a leaf. - pub fn params_for_leaf(&self, node: BDTNodeId) -> Option<&GraphColors> { - if let BDTNode::Leaf { params, .. } = &self[node] { + pub fn params_for_leaf(&self, node: BdtNodeId) -> Option<&GraphColors> { + if let BdtNode::Leaf { params, .. } = &self[node] { Some(params) } else { None } } + /// Compute all parameters that are stored in the given tree node. + pub fn all_node_params(&self, node: BdtNodeId) -> GraphColors { + match &self[node] { + BdtNode::Leaf { params, .. } => params.clone(), + BdtNode::Unprocessed { classes, .. } => Self::class_union(classes), + BdtNode::Decision { classes, .. } => Self::class_union(classes), + } + } + + fn class_union(classes: &BifurcationFunction) -> GraphColors { + let mut iterator = classes.values(); + let mut result = iterator.next().unwrap().clone(); + for value in iterator { + result = result.union(value) + } + result + } + /// **(internal)** Get next available node id in this tree. - fn next_id(&mut self) -> BDTNodeId { - BDTNodeId(self.next_id).also(|_| self.next_id += 1) + fn next_id(&mut self) -> BdtNodeId { + BdtNodeId(self.next_id).also(|_| self.next_id += 1) } /// **(internal)** Replace an EXISTING node in this tree. - pub(super) fn replace_node(&mut self, id: BDTNodeId, node: BDTNode) { + pub(super) fn replace_node(&mut self, id: BdtNodeId, node: BdtNode) { if self.storage.insert(id.0, node).is_none() { panic!("Replaced a non-existing node."); } } /// **(internal)** Save the given node in this tree and assign it a node id. - pub(super) fn insert_node(&mut self, node: BDTNode) -> BDTNodeId { + pub(super) fn insert_node(&mut self, node: BdtNode) -> BdtNodeId { self.next_id().also(|id| { if self.storage.insert(id.0, node).is_some() { panic!("Inserted a duplicate node."); @@ -65,22 +85,22 @@ impl BDT { } /// **(internal)** Create a leaf/unprocessed node for the given class list. - pub(super) fn insert_node_with_classes(&mut self, classes: BifurcationFunction) -> BDTNodeId { + pub(super) fn insert_node_with_classes(&mut self, classes: BifurcationFunction) -> BdtNodeId { assert!(!classes.is_empty(), "Inserting empty node."); - return if classes.len() == 1 { + if classes.len() == 1 { let (class, params) = classes.into_iter().next().unwrap(); - self.insert_node(BDTNode::Leaf { class, params }) + self.insert_node(BdtNode::Leaf { class, params }) } else { - self.insert_node(BDTNode::Unprocessed { classes }) - }; + self.insert_node(BdtNode::Unprocessed { classes }) + } } /// Compute the list of applied attributes (sorted by information gain) for a given node. - pub fn applied_attributes(&self, node: BDTNodeId) -> Vec { + pub fn applied_attributes(&self, node: BdtNodeId) -> Vec { let classes: HashMap = match &self[node] { - BDTNode::Leaf { .. } => HashMap::new(), - BDTNode::Decision { classes, .. } => classes.clone(), - BDTNode::Unprocessed { classes, .. } => classes.clone(), + BdtNode::Leaf { .. } => HashMap::new(), + BdtNode::Decision { classes, .. } => classes.clone(), + BdtNode::Unprocessed { classes, .. } => classes.clone(), }; if classes.is_empty() { return Vec::new(); @@ -109,27 +129,27 @@ impl BDT { /// Replace an unprocessed node with a decision node using the given attribute. pub fn make_decision( &mut self, - node: BDTNodeId, + node: BdtNodeId, attribute: AttributeId, - ) -> Result<(BDTNodeId, BDTNodeId), String> { + ) -> Result<(BdtNodeId, BdtNodeId), String> { if !self.storage.contains_key(&node.to_index()) { - return Err(format!("Node not found.")); + return Err("Node not found.".to_string()); } if attribute.to_index() >= self.attributes.len() { - return Err(format!("Attribute not found")); + return Err("Attribute not found".to_string()); } - if let BDTNode::Unprocessed { classes } = &self[node] { + if let BdtNode::Unprocessed { classes } = &self[node] { let attr = &self[attribute]; let (left, right) = attr.split_function(classes); if left.is_empty() || right.is_empty() { - return Err(format!("No decision based on given attribute.")); + return Err("No decision based on given attribute.".to_string()); } let classes = classes.clone(); let left_node = self.insert_node_with_classes(left); let right_node = self.insert_node_with_classes(right); self.replace_node( node, - BDTNode::Decision { + BdtNode::Decision { classes, attribute, left: left_node, @@ -138,19 +158,19 @@ impl BDT { ); Ok((left_node, right_node)) } else { - Err(format!("Cannot make decision on a resolved node.")) + Err("Cannot make decision on a resolved node.".to_string()) } } /// Replace given decision node with an unprocessed node and delete all child nodes. /// /// Returns list of deleted nodes. - pub fn revert_decision(&mut self, node: BDTNodeId) -> Vec { + pub fn revert_decision(&mut self, node: BdtNodeId) -> Vec { let mut deleted = vec![]; - if let BDTNode::Decision { classes, .. } = self[node].clone() { + if let BdtNode::Decision { classes, .. } = self[node].clone() { let mut dfs = vec![node]; while let Some(expand) = dfs.pop() { - if let BDTNode::Decision { left, right, .. } = &self[expand] { + if let BdtNode::Decision { left, right, .. } = &self[expand] { deleted.push(*left); deleted.push(*right); dfs.push(*left); @@ -160,7 +180,7 @@ impl BDT { deleted.iter().for_each(|n| { self.storage.remove(&n.to_index()); }); - self.replace_node(node, BDTNode::Unprocessed { classes }); + self.replace_node(node, BdtNode::Unprocessed { classes }); } deleted } @@ -169,7 +189,7 @@ impl BDT { /// up to the given `depth`. /// /// Returns the list of changed node ids. - pub fn auto_expand(&mut self, node: BDTNodeId, depth: usize) -> Vec { + pub fn auto_expand(&mut self, node: BdtNodeId, depth: usize) -> Vec { let mut changed = HashSet::new(); self.auto_expand_recursive(&mut changed, node, depth); changed.into_iter().collect() @@ -177,8 +197,8 @@ impl BDT { fn auto_expand_recursive( &mut self, - changed: &mut HashSet, - node: BDTNodeId, + changed: &mut HashSet, + node: BdtNodeId, depth: usize, ) { if depth == 0 { @@ -199,7 +219,7 @@ impl BDT { } } // For expanded nodes, just follow. - if let BDTNode::Decision { left, right, .. } = &self[node] { + if let BdtNode::Decision { left, right, .. } = &self[node] { let (left, right) = (*left, *right); self.auto_expand_recursive(changed, left, depth - 1); self.auto_expand_recursive(changed, right, depth - 1); diff --git a/src/bdt/_impl_bdt_dot_export.rs b/src/bdt/_impl_bdt_dot_export.rs index dc4211b..25f838c 100644 --- a/src/bdt/_impl_bdt_dot_export.rs +++ b/src/bdt/_impl_bdt_dot_export.rs @@ -1,9 +1,9 @@ -use crate::bdt::{BDTNode, BDTNodeId, BDT}; +use crate::bdt::{Bdt, BdtNode, BdtNodeId}; use crate::util::functional::Functional; use std::io::Write; /// Export to .dot format. -impl BDT { +impl Bdt { /// Convert this tree to a .dot graph string. pub fn to_dot(&self) -> String { Vec::::new() @@ -25,10 +25,10 @@ impl BDT { fn format_dot_recursive( &self, out: &mut dyn Write, - node: BDTNodeId, + node: BdtNodeId, ) -> Result<(), std::io::Error> { match &self[node] { - BDTNode::Leaf { class, params } => { + BdtNode::Leaf { class, params } => { let class = format!("{}", class).replace("\"", ""); writeln!( out, @@ -38,7 +38,7 @@ impl BDT { params.approx_cardinality() )?; } - BDTNode::Unprocessed { classes } => { + BdtNode::Unprocessed { classes } => { let classes: Vec = classes .iter() .map(|(c, p)| format!("({},{})", c, p.approx_cardinality()).replace("\"", "")) @@ -46,7 +46,7 @@ impl BDT { let classes = format!("{:?}", classes).replace("\"", ""); writeln!(out, "{}[label=\"Unprocessed({})\"]", node, classes)?; } - BDTNode::Decision { + BdtNode::Decision { attribute, left, right, diff --git a/src/bdt/_impl_bdt_json.rs b/src/bdt/_impl_bdt_json.rs index 680a352..9c07345 100644 --- a/src/bdt/_impl_bdt_json.rs +++ b/src/bdt/_impl_bdt_json.rs @@ -1,5 +1,5 @@ use crate::bdt::_impl_bdt_node::class_list_cardinality; -use crate::bdt::{AttributeId, BDTNode, BDTNodeId, BDT}; +use crate::bdt::{AttributeId, Bdt, BdtNode, BdtNodeId}; use crate::scc::Class; use crate::util::functional::Functional; use crate::util::index_type::IndexType; @@ -7,27 +7,27 @@ use biodivine_lib_param_bn::symbolic_async_graph::GraphColors; use json::JsonValue; use std::collections::HashMap; -impl BDTNode { +impl BdtNode { /// Convert this BDT node to json value with all available information stored in the node. pub fn to_json(&self) -> JsonValue { match self { - BDTNode::Leaf { class, params } => object! { - "type" => format!("leaf"), + BdtNode::Leaf { class, params } => object! { + "type" => "leaf".to_string(), "cardinality" => params.approx_cardinality(), "class" => format!("{}", class), }, - BDTNode::Unprocessed { classes } => object! { - "type" => format!("unprocessed"), + BdtNode::Unprocessed { classes } => object! { + "type" => "unprocessed".to_string(), "cardinality" => class_list_cardinality(classes), "classes" => class_list_to_json(classes), }, - BDTNode::Decision { + BdtNode::Decision { attribute, left, right, classes, } => object! { - "type" => format!("decision"), + "type" => "decision".to_string(), "cardinality" => class_list_cardinality(classes), "classes" => class_list_to_json(classes), "attribute_id" => attribute.0, @@ -38,7 +38,7 @@ impl BDTNode { } } -impl BDT { +impl Bdt { /// Convert the whole tree into one json array. pub fn to_json(&self) -> JsonValue { JsonValue::from( @@ -51,7 +51,7 @@ impl BDT { /// Convert a BDT node to json, including extra info compared to `BDTNode::to_json`. /// /// The extra info covers the node id as well as attribute name for decision nodes. - pub fn node_to_json(&self, id: BDTNodeId) -> JsonValue { + pub fn node_to_json(&self, id: BdtNodeId) -> JsonValue { self[id].to_json().apply(|result| { result.insert("id", id.0).unwrap(); if result.has_key("attribute_id") { @@ -67,7 +67,7 @@ impl BDT { } /// Compute attribute gains for the given tree node. - pub fn attribute_gains_json(&self, id: BDTNodeId) -> JsonValue { + pub fn attribute_gains_json(&self, id: BdtNodeId) -> JsonValue { self.applied_attributes(id) .into_iter() .map(|it| { @@ -80,7 +80,7 @@ impl BDT { } }) .collect::>() - .and_then(|it| JsonValue::from(it)) + .and_then(JsonValue::from) } } diff --git a/src/bdt/_impl_bdt_node.rs b/src/bdt/_impl_bdt_node.rs index 943ac98..9d09c3e 100644 --- a/src/bdt/_impl_bdt_node.rs +++ b/src/bdt/_impl_bdt_node.rs @@ -1,28 +1,28 @@ -use crate::bdt::BDTNode; +use crate::bdt::BdtNode; use crate::scc::Class; use biodivine_lib_param_bn::symbolic_async_graph::GraphColors; use std::collections::HashMap; -impl BDTNode { +impl BdtNode { /// Computes the cardinality of the parameter set covered by this tree node. pub fn approx_cardinality(&self) -> f64 { match self { - BDTNode::Leaf { params, .. } => params.approx_cardinality(), - BDTNode::Decision { classes, .. } => class_list_cardinality(classes), - BDTNode::Unprocessed { classes, .. } => class_list_cardinality(classes), + BdtNode::Leaf { params, .. } => params.approx_cardinality(), + BdtNode::Decision { classes, .. } => class_list_cardinality(classes), + BdtNode::Unprocessed { classes, .. } => class_list_cardinality(classes), } } pub fn is_leaf(&self) -> bool { - matches!(self, BDTNode::Leaf { .. }) + matches!(self, BdtNode::Leaf { .. }) } pub fn is_decision(&self) -> bool { - matches!(self, BDTNode::Decision { .. }) + matches!(self, BdtNode::Decision { .. }) } pub fn is_unprocessed(&self) -> bool { - matches!(self, BDTNode::Unprocessed { .. }) + matches!(self, BdtNode::Unprocessed { .. }) } } diff --git a/src/bdt/_impl_indexing.rs b/src/bdt/_impl_indexing.rs index 6a92dcd..8d68e3c 100644 --- a/src/bdt/_impl_indexing.rs +++ b/src/bdt/_impl_indexing.rs @@ -1,38 +1,38 @@ -use crate::bdt::{Attribute, AttributeId, BDTNode, BDTNodeId, BDT}; +use crate::bdt::{Attribute, AttributeId, Bdt, BdtNode, BdtNodeId}; use crate::util::functional::Functional; use crate::util::index_type::IndexType; use std::fmt::{Display, Formatter}; use std::ops::Index; -impl IndexType for BDTNodeId { +impl IndexType for BdtNodeId { fn to_index(&self) -> usize { self.0 } - fn try_from(index: usize, collection: &BDT) -> Option { - BDTNodeId(index).take_if(|i| collection.storage.contains_key(&i.0)) + fn try_from(index: usize, collection: &Bdt) -> Option { + BdtNodeId(index).take_if(|i| collection.storage.contains_key(&i.0)) } } -impl IndexType for AttributeId { +impl IndexType for AttributeId { fn to_index(&self) -> usize { self.0 } - fn try_from(index: usize, collection: &BDT) -> Option { + fn try_from(index: usize, collection: &Bdt) -> Option { AttributeId(index).take_if(|i| i.0 < collection.attributes.len()) } } -impl Index for BDT { - type Output = BDTNode; +impl Index for Bdt { + type Output = BdtNode; - fn index(&self, index: BDTNodeId) -> &Self::Output { + fn index(&self, index: BdtNodeId) -> &Self::Output { &self.storage[&index.to_index()] } } -impl Index for BDT { +impl Index for Bdt { type Output = Attribute; fn index(&self, index: AttributeId) -> &Self::Output { @@ -40,7 +40,7 @@ impl Index for BDT { } } -impl Display for BDTNodeId { +impl Display for BdtNodeId { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { write!(f, "{}", self.0) } diff --git a/src/bdt/mod.rs b/src/bdt/mod.rs index 739a233..74e2986 100644 --- a/src/bdt/mod.rs +++ b/src/bdt/mod.rs @@ -27,15 +27,15 @@ type BifurcationFunction = HashMap; /// set of parametrisations), a decision node with a fixed attribute, or an unprocessed node /// with a remaining bifurcation function. #[derive(Clone)] -pub enum BDTNode { +pub enum BdtNode { Leaf { class: Class, params: GraphColors, }, Decision { attribute: AttributeId, - left: BDTNodeId, - right: BDTNodeId, + left: BdtNodeId, + right: BdtNodeId, classes: BifurcationFunction, }, Unprocessed { @@ -49,7 +49,7 @@ pub enum BDTNode { /// I might want to delete a node - to avoid specifying a full path from root to the deleted node, /// I can use the ID which will automatically "jump" to the correct position in the tree. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct BDTNodeId(usize); +pub struct BdtNodeId(usize); /// An attribute id is used to identify a specific attribute used in a decision tree. /// @@ -61,13 +61,13 @@ pub struct AttributeId(usize); /// A Bifurcation decision tree. It stores the BDT nodes, mapping IDs to actual structures. /// /// It is the owner of the tree memory, so every addition/deletion in the tree must happen here. -pub struct BDT { - storage: HashMap, +pub struct Bdt { + storage: HashMap, attributes: Vec, next_id: usize, } -type BDTNodeIds<'a> = Map, fn(&usize) -> BDTNodeId>; +type BdtNodeIds<'a> = Map, fn(&usize) -> BdtNodeId>; type AttributeIds<'a> = Map, fn(usize) -> AttributeId>; /// Attribute is an abstract property of the boolean network that can be applied to partition @@ -103,7 +103,7 @@ pub fn entropy(classes: &BifurcationFunction) -> f64 { let proportion = *c / total; result += -proportion * proportion.log2(); } - return result; + result } /// Complete information gain from original and divided dataset cardinality. diff --git a/src/bin/benchmark_filter.rs b/src/bin/benchmark_filter.rs new file mode 100644 index 0000000..fe95165 --- /dev/null +++ b/src/bin/benchmark_filter.rs @@ -0,0 +1,63 @@ +use biodivine_lib_param_bn::{BooleanNetwork, RegulatoryGraph}; + +fn main() { + let benchmarks = std::fs::read_dir("./benchmark").unwrap(); + let current_dir = std::env::current_dir().unwrap(); + let aeon_benchmarks = current_dir.join("aeon_models"); + if !aeon_benchmarks.exists() { + std::fs::create_dir_all(&aeon_benchmarks).unwrap(); + } + + let mut i = 0; + for bench_dir in benchmarks { + let bench_dir = bench_dir.unwrap(); + if !bench_dir.file_type().unwrap().is_dir() { + continue; + } + i += 1; + + let model_path = bench_dir.path().join("model.sbml"); + let model_string = std::fs::read_to_string(model_path).unwrap(); + let (sbml_model, _) = BooleanNetwork::try_from_sbml(&model_string).unwrap(); + let model = erase_regulation_bounds(&sbml_model); + + let bench_name = bench_dir.file_name().to_str().unwrap().to_string(); + let aeon_file = aeon_benchmarks.join(&format!("{}_{}.aeon", i, bench_name)); + std::fs::write(aeon_file, model.to_string()).unwrap(); + } +} + +/// This will erase the observability/monotonicity requirements for regulations, because we don't +/// need them in systems without parameters. +/// +/// Also erases any pre-set input constants. +fn erase_regulation_bounds(network: &BooleanNetwork) -> BooleanNetwork { + let variable_names = network + .variables() + .map(|v| network.get_variable_name(v).to_string()) + .collect(); + let mut rg = RegulatoryGraph::new(variable_names); + for old_reg in network.as_graph().regulations() { + rg.add_regulation( + network.get_variable_name(old_reg.get_regulator()), + network.get_variable_name(old_reg.get_target()), + false, + None, + ) + .unwrap(); + } + let mut bn = BooleanNetwork::new(rg); + for old_v in network.variables() { + let new_v = bn + .as_graph() + .find_variable(network.get_variable_name(old_v)) + .unwrap(); + let is_input = network.regulators(old_v).is_empty(); + if !is_input { + if let Some(fn_update) = network.get_update_function(old_v).clone() { + bn.add_update_function(new_v, fn_update).unwrap(); + } + } + } + bn +} diff --git a/src/bin/benchmark_translate.rs b/src/bin/benchmark_translate.rs new file mode 100644 index 0000000..36a194e --- /dev/null +++ b/src/bin/benchmark_translate.rs @@ -0,0 +1,71 @@ +use biodivine_lib_param_bn::{BinaryOp, BooleanNetwork, FnUpdate}; +use std::convert::TryFrom; + +fn main() { + let current_dir = std::env::current_dir().unwrap(); + let bnet_benchmarks = current_dir.join("random_bnet_models_1000"); + if !bnet_benchmarks.exists() { + std::fs::create_dir_all(&bnet_benchmarks).unwrap(); + } + + let benchmarks = std::fs::read_dir("./random_aeon_models_1000").unwrap(); + for bench_dir in benchmarks.into_iter().map(|it| it.unwrap()) { + let bench_name = bench_dir.file_name().to_str().unwrap().to_string(); + let model_path = bench_dir.path(); + let model_string = std::fs::read_to_string(model_path); + if model_string.is_err() { + continue; + } + let model_string = model_string.unwrap(); + let r = BooleanNetwork::try_from(model_string.as_str()); + if r.is_err() { + continue; + } + let model = r.unwrap(); + let bnet_file = bnet_benchmarks.join(&format!("{}.bnet", bench_name)); + std::fs::write(bnet_file, network_to_bnet(&model)).unwrap(); + } +} + +fn network_to_bnet(network: &BooleanNetwork) -> String { + let mut model = "targets,factors\n".to_string(); + for v in network.variables() { + let v_id: usize = v.into(); + let line = format!( + "v{}, {}\n", + v_id, + fnupdate_to_bnet_string(network.get_update_function(v).as_ref().unwrap()) + ); + model.push_str(&line); + } + model +} + +fn fnupdate_to_bnet_string(fn_update: &FnUpdate) -> String { + match fn_update { + FnUpdate::Param(_, _) => panic!("Parameters not allowed."), + FnUpdate::Const(value) => { + if *value { + // There is always v1 + "v1 | !v1".to_string() + } else { + "v1 & !v1".to_string() + } + } + FnUpdate::Var(id) => { + let id: usize = (*id).into(); + format!("v{}", id) + } + FnUpdate::Not(inner) => format!("!{}", fnupdate_to_bnet_string(inner)), + FnUpdate::Binary(op, l, r) => { + let left = fnupdate_to_bnet_string(l); + let right = fnupdate_to_bnet_string(r); + let op = match *op { + BinaryOp::And => "&", + BinaryOp::Or => "|", + _ => panic!("{:?} not supported.", op), + }; + format!("({} {} {})", left, op, right) + } + } +} diff --git a/src/bin/benchmark_validator.rs b/src/bin/benchmark_validator.rs index ad847c1..15daf21 100644 --- a/src/bin/benchmark_validator.rs +++ b/src/bin/benchmark_validator.rs @@ -32,7 +32,7 @@ fn main() { } else { // Check that the sbml model is readable: let model_string = std::fs::read_to_string(sbml_model_path).unwrap(); - let model = BooleanNetwork::from_sbml(&model_string); + let model = BooleanNetwork::try_from_sbml(&model_string); match model { Err(err) => { errors += 1; @@ -107,7 +107,7 @@ fn main() { } let mut inputs = 0; for v in model.variables() { - if model.regulators(v).len() == 0 { + if model.regulators(v).is_empty() { inputs += 1; } } @@ -134,7 +134,9 @@ fn main() { let graph = SymbolicAsyncGraph::new(model); match graph { Ok(graph) => { - if graph.unit_colors().approx_cardinality() != 1.0 { + if graph.unit_colors().as_bdd() + != graph.unit_colors().pick_singleton().as_bdd() + { errors += 1; eprintln!( "ERROR: Default model has {} colors in {}.", diff --git a/src/bin/experiment.rs b/src/bin/experiment.rs index 6204c25..5f55417 100644 --- a/src/bin/experiment.rs +++ b/src/bin/experiment.rs @@ -1,5 +1,7 @@ -use biodivine_aeon_server::scc::algo_symbolic_components::components_2; -use biodivine_aeon_server::scc::{Classifier, ProgressTracker}; +use biodivine_aeon_server::scc::algo_interleaved_transition_guided_reduction::interleaved_transition_guided_reduction; +use biodivine_aeon_server::scc::algo_xie_beerel::xie_beerel_attractors; +use biodivine_aeon_server::scc::Classifier; +use biodivine_aeon_server::GraphTaskContext; use biodivine_lib_param_bn::symbolic_async_graph::SymbolicAsyncGraph; use biodivine_lib_param_bn::BooleanNetwork; use std::convert::TryFrom; @@ -32,17 +34,36 @@ fn main() { ); println!( "State space: {}", - graph.unit_vertices().approx_cardinality() + graph.unit_colored_vertices().approx_cardinality() ); let classifier = Classifier::new(&graph); - let progress = ProgressTracker::new(&graph); - components_2( + let task_context = GraphTaskContext::new(); + task_context.restart(&graph); + + // Now we can actually start the computation... + + // First, perform ITGR reduction. + let (universe, active_variables) = interleaved_transition_guided_reduction( + &task_context, + &graph, + graph.mk_unit_colored_vertices(), + ); + + // Then run Xie-Beerel to actually detect the components. + xie_beerel_attractors( + &task_context, &graph, - /*&progress, &AtomicBool::new(false), */ + &universe, + &active_variables, |component| { - println!("Found attractor..."); - println!("Remaining: {}", progress.get_percent_string()); + println!("Found attractor... {}", component.approx_cardinality()); + println!("Remaining: {}", task_context.get_percent_string()); + println!( + "Unique states: {}", + component.vertices().approx_cardinality() + ); + println!("Unique colors: {}", component.colors().approx_cardinality()); classifier.add_component(component, &graph); }, ); @@ -61,28 +82,4 @@ fn main() { classifier.export_result().len() ); println!("Elapsed time: {}s", (elapsed as f64) / 1000.0); - - //let ctra = model.graph().find_variable("CtrA").unwrap(); - //let dnaa = model.graph().find_variable("DnaA").unwrap(); - - /* let filter: ALLFormula = BooleanFormula::Atom(ALLAtom::SomeAttractor( - BooleanFormula::Atom(AttractorAtom::AllStates( - BooleanFormula::Binary { op: BinaryOp::And, - left: Box::new(BooleanFormula::Atom(StateAtom::IsSet(ctra))), - right: Box::new(BooleanFormula::Atom(StateAtom::IsNotSet(dnaa))) - } - )) - )); - - */ - - /*let filter = parse_filter(&model, "SomeAttractor(AllStates(CtrA & !DnaA))").unwrap(); - - let valid = filter.eval(&classifier.export_components(), &graph); - for (c, p) in classifier.export_result() { - println!("Class {:?}, cardinality: {}", c, p.intersect(&valid).cardinality()); - }*/ - - //println!("Start learning tree...\n\n"); - //make_decision_tree(&model, &classifier.export_result()); } diff --git a/src/bin/rbn_generator.rs b/src/bin/rbn_generator.rs new file mode 100644 index 0000000..d2b2137 --- /dev/null +++ b/src/bin/rbn_generator.rs @@ -0,0 +1,161 @@ +use biodivine_lib_param_bn::{ + BinaryOp, BooleanNetwork, FnUpdate, Monotonicity, RegulatoryGraph, VariableId, +}; +use rand::rngs::StdRng; +use rand::{Rng, SeedableRng}; + +const V_COUNT: usize = 1000; +const R_COUNT: usize = 3000; + +fn main() { + //let mut random = StdRng::seed_from_u64(1234567890); + let mut random = StdRng::seed_from_u64(123456789); + + let current_dir = std::env::current_dir().unwrap(); + let aeon_benchmarks = current_dir.join("random_aeon_models_1000"); + if !aeon_benchmarks.exists() { + std::fs::create_dir_all(&aeon_benchmarks).unwrap(); + } + + for i_model in 1..101 { + let variables = (0..V_COUNT).map(|i| format!("x{}", i)).collect::>(); + let mut rg = RegulatoryGraph::new(variables.clone()); + let mut remaining_regs = 0; + // Each variable must have in/out-degree at least one... + for source in rg.variables() { + if !rg.regulators(source).is_empty() || !rg.targets(source).is_empty() { + continue; + } + let source: usize = source.into(); + let target: usize = random.gen_range(0..V_COUNT); + let monotonicity = if random.gen_bool(0.7) { + Monotonicity::Activation + } else { + Monotonicity::Inhibition + }; + if random.gen_bool(0.5) { + rg.add_regulation( + &variables[source], + &variables[target], + true, + Some(monotonicity), + ) + .unwrap(); + } else { + rg.add_regulation( + &variables[target], + &variables[source], + true, + Some(monotonicity), + ) + .unwrap(); + } + remaining_regs += 1; + } + // Each variable must have in-degree at least one... + /*for target in rg.variables() { + if rg.regulators(target).len() > 0 { continue; } + let source: usize = random.gen_range(0..V_COUNT); + let target: usize = target.into(); + let monotonicity = if random.gen_bool(0.7) { Monotonicity::Activation } else { Monotonicity::Inhibition }; + rg.add_regulation(&variables[source], &variables[target], true, Some(monotonicity)).unwrap(); + remaining_regs += 1; + }*/ + // Finally, the whole thing must be one SCC + /*while rg.components().len() > 1 { + let components = rg.components(); + let c1 = &components[0]; + let c2 = &components[1]; + let i_source = random.gen_range(0..c1.len()); + let t_source = random.gen_range(0..c2.len()); + let source: usize = c1.iter().skip(i_source).next().unwrap().clone().into(); + let target: usize = c2.iter().skip(t_source).next().unwrap().clone().into(); + if rg.find_regulation(VariableId::from(source), VariableId::from(target)).is_none() { + let monotonicity = if random.gen_bool(0.7) { Monotonicity::Activation } else { Monotonicity::Inhibition }; + rg.add_regulation(&variables[source], &variables[target], true, Some(monotonicity)).unwrap(); + remaining_regs += 1; + } + let i_source = random.gen_range(0..c2.len()); + let t_source = random.gen_range(0..c1.len()); + let source: usize = c2.iter().skip(i_source).next().unwrap().clone().into(); + let target: usize = c1.iter().skip(t_source).next().unwrap().clone().into(); + if rg.find_regulation(VariableId::from(source), VariableId::from(target)).is_none() { + let monotonicity = if random.gen_bool(0.7) { Monotonicity::Activation } else { Monotonicity::Inhibition }; + rg.add_regulation(&variables[source], &variables[target], true, Some(monotonicity)).unwrap(); + remaining_regs += 1; + } + }*/ + // Rest is truly random + while remaining_regs <= R_COUNT { + let source = random.gen_range(0..V_COUNT); + let target = random.gen_range(0..V_COUNT); + if rg + .find_regulation( + VariableId::try_from_usize(&rg, source).unwrap(), + VariableId::try_from_usize(&rg, target).unwrap(), + ) + .is_none() + { + let monotonicity = if random.gen_bool(0.7) { + Monotonicity::Activation + } else { + Monotonicity::Inhibition + }; + rg.add_regulation( + &variables[source], + &variables[target], + true, + Some(monotonicity), + ) + .unwrap(); + remaining_regs += 1; + } + } + + let max_degree_var = rg + .variables() + .max_by_key(|v| rg.regulators(*v).len()) + .unwrap(); + let max_degree = rg.regulators(max_degree_var).len(); + eprintln!("Generated network has max degree {}", max_degree); + + let mut bn = BooleanNetwork::new(rg.clone()); + + for v in bn.variables() { + let regulators = bn.regulators(v); + if regulators.is_empty() { + bn.add_update_function(v, FnUpdate::Const(random.gen_bool(0.5))) + .unwrap(); + } else { + let r = regulators[0]; + let fst_is_activation = rg.find_regulation(r, v).unwrap().get_monotonicity() + == Some(Monotonicity::Activation); + let mut fn_update = if fst_is_activation { + FnUpdate::Var(r) + } else { + FnUpdate::Not(Box::new(FnUpdate::Var(r))) + }; + for r in regulators.iter().cloned().skip(1) { + let op = if random.gen_bool(0.5) { + BinaryOp::And + } else { + BinaryOp::Or + }; + let is_activation = rg.find_regulation(r, v).unwrap().get_monotonicity() + == Some(Monotonicity::Activation); + let var = if is_activation { + FnUpdate::Var(r) + } else { + FnUpdate::Not(Box::new(FnUpdate::Var(r))) + }; + fn_update = FnUpdate::Binary(op, Box::new(fn_update), Box::new(var)); + } + bn.add_update_function(v, fn_update).unwrap(); + } + } + + let aeon_file = aeon_benchmarks.join(&format!("{}_{}_{}.aeon", i_model, V_COUNT, R_COUNT)); + std::fs::write(aeon_file, bn.to_string()).unwrap(); + println!("{} generated...", i_model); + } +} diff --git a/src/lib.rs b/src/lib.rs index 04386eb..cdd5b10 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,23 @@ #[macro_use] extern crate json; +use crate::scc::ProgressTracker; +use std::sync::atomic::AtomicBool; + pub mod all; pub mod bdt; -pub mod scc; // bifurcation decision trees +pub mod scc; /// Some utility methods which we can later move to std-lib pub mod util; + +mod _impl_graph_task_context; + +/// A context object which aggregates all necessary information about a running task working with +/// a symbolic graph. +/// +/// We use this to avoid passing each context variable as a (mutable) reference. It is also easier +/// to implement some utility methods this way. +pub struct GraphTaskContext { + is_cancelled: AtomicBool, + progress: ProgressTracker, +} diff --git a/src/main.rs b/src/main.rs index c2ccb1f..44b588e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,22 +13,26 @@ use rocket::http::{ContentType, Header}; use rocket::request::Request; use rocket::response::{self, Responder, Response}; -use biodivine_aeon_server::scc::{Behaviour, Class, Classifier, ProgressTracker}; +use biodivine_aeon_server::scc::{Behaviour, Class, Classifier}; use biodivine_lib_param_bn::{BooleanNetwork, FnUpdate}; use regex::Regex; use std::convert::TryFrom; -use biodivine_aeon_server::bdt::{AttributeId, BDTNodeId, BDT}; +use biodivine_aeon_server::bdt::{AttributeId, Bdt, BdtNodeId}; +use biodivine_aeon_server::scc::algo_interleaved_transition_guided_reduction::interleaved_transition_guided_reduction; +use biodivine_aeon_server::scc::algo_xie_beerel::xie_beerel_attractors; +use biodivine_aeon_server::scc::Behaviour::Stability; use biodivine_aeon_server::util::index_type::IndexType; +use biodivine_aeon_server::GraphTaskContext; +use biodivine_lib_param_bn::biodivine_std::bitvector::{ArrayBitVector, BitVector}; +use biodivine_lib_param_bn::biodivine_std::traits::Set; use biodivine_lib_param_bn::symbolic_async_graph::{GraphColors, SymbolicAsyncGraph}; -use biodivine_lib_std::collections::bitvectors::{ArrayBitVector, BitVector}; use json::JsonValue; use rocket::config::Environment; use rocket::{Config, Data}; use std::cmp::max; use std::collections::{HashMap, HashSet}; use std::io::Read; -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, RwLock}; use std::thread::JoinHandle; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -36,11 +40,10 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; /// Computation keeps all information struct Computation { timestamp: SystemTime, - is_cancelled: Arc, // indicate to the server that the computation should be cancelled - input_model: String, // .aeon string representation of the model + input_model: String, // .aeon string representation of the model + task: GraphTaskContext, // A task context which keeps track of progress and cancellation. graph: Option>, // Model graph - used to create witnesses classifier: Option>, // Classifier used to store the results of the computation - progress: Option>, // Used to access progress of the computation thread: Option>, // A thread that is actually doing the computation (so that we can check if it is still running). If none, the computation is done. error: Option, // A string error from the computation finished_timestamp: Option, // A timestamp when the computation was completed (if done) @@ -48,26 +51,25 @@ struct Computation { impl Computation { pub fn start_timestamp(&self) -> u128 { - return self - .timestamp + self.timestamp .duration_since(UNIX_EPOCH) .expect("Time error") - .as_millis(); + .as_millis() } pub fn end_timestamp(&self) -> Option { - return self.finished_timestamp.map(|t| { + self.finished_timestamp.map(|t| { t.duration_since(UNIX_EPOCH) .expect("Time error") .as_millis() - }); + }) } } lazy_static! { static ref COMPUTATION: Arc>> = Arc::new(RwLock::new(None)); static ref CHECK_UPDATE_FUNCTION_LOCK: Arc> = Arc::new(RwLock::new(true)); - static ref TREE: Arc>> = Arc::new(RwLock::new(None)); + static ref TREE: Arc>> = Arc::new(RwLock::new(None)); } /// Decision tree API design: @@ -99,19 +101,19 @@ lazy_static! { fn get_bifurcation_tree() -> BackendResponse { let tree = TREE.clone(); let tree = tree.read().unwrap(); - return if let Some(tree) = &*tree { + if let Some(tree) = &*tree { BackendResponse::ok(&tree.to_json().to_string()) } else { BackendResponse::err(&"No tree present. Run computation first.".to_string()) - }; + } } #[get("/get_attributes/")] fn get_attributes(node_id: String) -> BackendResponse { let tree = TREE.clone(); let tree = tree.read().unwrap(); - return if let Some(tree) = &*tree { - let node = BDTNodeId::try_from_str(&node_id, tree); + if let Some(tree) = &*tree { + let node = BdtNodeId::try_from_str(&node_id, tree); let node = if let Some(node) = node { node } else { @@ -120,7 +122,86 @@ fn get_attributes(node_id: String) -> BackendResponse { BackendResponse::ok(&tree.attribute_gains_json(node).to_string()) } else { BackendResponse::err(&"No tree present. Run computation first.".to_string()) + } +} + +#[get("/get_stability_data/")] +fn get_stability_data(node_id: String) -> BackendResponse { + // First, extract all colors in that tree node. + let node_params = { + let tree = TREE.clone(); + let tree = tree.read().unwrap(); + if let Some(tree) = &*tree { + let node = BdtNodeId::try_from_str(&node_id, tree); + let node = if let Some(n) = node { + n + } else { + return BackendResponse::err(&format!("Invalid node id {}.", node_id)); + }; + tree.all_node_params(node) + } else { + return BackendResponse::err("No bifurcation tree found."); + } }; + // Then find all attractors of the graph + let cmp = COMPUTATION.read().unwrap(); + if let Some(cmp) = &*cmp { + let components = if let Some(classifier) = &cmp.classifier { + classifier.export_components() + } else { + return BackendResponse::err("No attractor data found."); + }; + if let Some(graph) = &cmp.graph { + // Now compute which attractors are actually relevant for the node colors + let mut all_attractor_states = graph.mk_empty_vertices(); + let mut all_sink_states = graph.mk_empty_vertices().vertices(); + for (attractor, behaviour) in components { + let attractor = attractor.intersect_colors(&node_params); + if attractor.is_empty() { + continue; + } + all_attractor_states = all_attractor_states.union(&attractor); + for (b, c) in behaviour { + if b == Stability { + let sink_states = attractor.intersect_colors(&c).vertices(); + all_sink_states = all_sink_states.union(&sink_states); + } + } + } + let sink_count = all_sink_states.materialize().iter().take(101).count(); + let sink_count = if sink_count == 101 { + "100+".to_string() + } else { + format!("{}", sink_count) + }; + let mut always_true = Vec::new(); + let mut always_false = Vec::new(); + let mut effectively_constant = Vec::new(); + for v in graph.as_network().variables() { + let name = graph.as_network().get_variable_name(v); + let v_is_true = graph.fix_network_variable(v, true); + let v_is_false = graph.fix_network_variable(v, false); + if all_attractor_states.intersect(&v_is_true).is_empty() { + always_false.push(name.clone()); + } else if all_attractor_states.intersect(&v_is_false).is_empty() { + always_true.push(name.clone()); + } else if graph.var_can_post(v, &all_attractor_states).is_empty() { + effectively_constant.push(name.clone()); + } + } + let response = object! { + "sink_count": sink_count, + "always_true": always_true, + "always_false": always_false, + "constant": effectively_constant, + }; + BackendResponse::ok(&response.to_string()) + } else { + BackendResponse::err(&"No attractor data found.") + } + } else { + BackendResponse::err(&"No attractor data found.") + } } #[post("/apply_attribute//")] @@ -128,7 +209,7 @@ fn apply_attribute(node_id: String, attribute_id: String) -> BackendResponse { let tree = TREE.clone(); let mut tree = tree.write().unwrap(); return if let Some(tree) = tree.as_mut() { - let node = BDTNodeId::try_from_str(&node_id, tree); + let node = BdtNodeId::try_from_str(&node_id, tree); let node = if let Some(node) = node { node } else { @@ -160,7 +241,7 @@ fn revert_decision(node_id: String) -> BackendResponse { let tree = TREE.clone(); let mut tree = tree.write().unwrap(); return if let Some(tree) = tree.as_mut() { - let node = BDTNodeId::try_from_str(&node_id, tree); + let node = BdtNodeId::try_from_str(&node_id, tree); let node = if let Some(node) = node { node } else { @@ -182,7 +263,7 @@ fn revert_decision(node_id: String) -> BackendResponse { } fn max_parameter_cardinality(function: &FnUpdate) -> usize { - return match function { + match function { FnUpdate::Const(_) | FnUpdate::Var(_) => 0, FnUpdate::Param(_, args) => args.len(), FnUpdate::Not(inner) => max_parameter_cardinality(inner), @@ -190,7 +271,7 @@ fn max_parameter_cardinality(function: &FnUpdate) -> usize { max_parameter_cardinality(left), max_parameter_cardinality(right), ), - }; + } } /// Accept a partial model containing only the necessary regulations and one update function. @@ -244,7 +325,7 @@ fn check_update_function(data: Data) -> BackendResponse { }; } -const VERSION: &'static str = env!("CARGO_PKG_VERSION"); +const VERSION: &str = env!("CARGO_PKG_VERSION"); #[get("/ping")] fn ping() -> BackendResponse { @@ -264,10 +345,8 @@ fn ping() -> BackendResponse { let cmp = cmp.read().unwrap(); if let Some(computation) = &*cmp { response["timestamp"] = (computation.start_timestamp() as u64).into(); - response["is_cancelled"] = computation.is_cancelled.load(Ordering::SeqCst).into(); - if let Some(progress) = &computation.progress { - response["progress"] = progress.get_percent_string().into(); - } + response["is_cancelled"] = computation.task.is_cancelled().into(); + response["progress"] = computation.task.get_percent_string().into(); response["is_running"] = computation.thread.is_some().into(); if let Some(error) = &computation.error { response["error"] = error.clone().into(); @@ -281,7 +360,7 @@ fn ping() -> BackendResponse { } } } - return BackendResponse::ok(&response.to_string()); + BackendResponse::ok(&response.to_string()) } // Try to obtain current class data or none if classifier is busy @@ -304,7 +383,7 @@ fn try_get_class_params(classifier: &Classifier, class: &Class) -> Option