diff --git a/src/main.rs b/src/main.rs index 127d2fe..9d11e56 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,9 +20,11 @@ use std::convert::TryFrom; 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_stability_analysis::stability_analysis; +use biodivine_aeon_server::scc::algo_stability_analysis::{ + compute_stability, StabilityVector, VariableStability, +}; use biodivine_aeon_server::scc::algo_xie_beerel::xie_beerel_attractors; -use biodivine_aeon_server::scc::Behaviour::Stability; +use biodivine_aeon_server::util::functional::Functional; use biodivine_aeon_server::util::index_type::IndexType; use biodivine_aeon_server::GraphTaskContext; use biodivine_lib_param_bn::biodivine_std::bitvector::{ArrayBitVector, BitVector}; @@ -126,8 +128,19 @@ fn get_attributes(node_id: String) -> BackendResponse { } } -#[get("/get_stability_data/")] -fn get_stability_data(node_id: String) -> BackendResponse { +#[get("/get_stability_data//")] +fn get_stability_data(node_id: String, behaviour_str: String) -> BackendResponse { + let behaviour = Behaviour::try_from(behaviour_str.as_str()); + let behaviour = match behaviour { + Ok(behaviour) => Some(behaviour), + Err(error) => { + if behaviour_str == "total" { + None + } else { + return BackendResponse::err(error.as_str()); + } + } + }; // First, extract all colors in that tree node. let node_params = { let tree = TREE.clone(); @@ -148,69 +161,43 @@ fn get_stability_data(node_id: String) -> BackendResponse { let cmp = COMPUTATION.read().unwrap(); if let Some(cmp) = &*cmp { let components = if let Some(classifier) = &cmp.classifier { - classifier.export_components() + if let Some(behaviour) = behaviour { + classifier.export_components_with_class(behaviour) + } else { + classifier + .export_components() + .into_iter() + .map(|(c, _)| c) + .collect() + } } 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(); - 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); - all_sink_states = all_sink_states.union(&sink_states); - } - } + let components = components + .into_iter() + .filter_map(|attractor| { + attractor + .intersect_colors(&node_params) + .take_if(|it| !it.is_empty()) + }) + .collect::>(); + + if components.is_empty() { + return BackendResponse::err("No attractors with this property."); } - let sink_count = all_sink_states - .vertices() - .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 stability_data = compute_stability(graph, &components); + let mut response = JsonValue::new_array(); + for variable in graph.as_network().variables() { + response + .push(object! { + "variable": graph.as_network().get_variable_name(variable).clone(), + "data": stability_data[&variable].to_json(), + }) + .unwrap(); } - let var_stability: Vec = if all_sink_states.is_empty() { - Vec::new() - } else { - graph - .as_network() - .variables() - .map(|v| stability_analysis(graph, &all_sink_states, v)) - .collect() - }; - let response = object! { - "sink_count": sink_count, - "always_true": always_true, - "always_false": always_false, - "constant": effectively_constant, - "var_stability": var_stability, - }; BackendResponse::ok(&response.to_string()) } else { BackendResponse::err(&"No attractor data found.") @@ -544,8 +531,31 @@ fn get_tree_witness(node_id: String) -> BackendResponse { }; } -#[get("/get_stability_witness///")] -fn get_stability_witness(node_id: String, variable: String, value: String) -> BackendResponse { +#[get("/get_stability_witness////")] +fn get_stability_witness( + node_id: String, + behaviour_str: String, + variable_str: String, + vector_str: String, +) -> BackendResponse { + let behaviour = Behaviour::try_from(behaviour_str.as_str()); + let behaviour = match behaviour { + Ok(behaviour) => Some(behaviour), + Err(error) => { + if behaviour_str == "total" { + None + } else { + return BackendResponse::err(error.as_str()); + } + } + }; + let vector = StabilityVector::try_from(vector_str.as_str()); + let vector = match vector { + Ok(vector) => vector, + Err(error) => { + return BackendResponse::err(error.as_str()); + } + }; // First, extract all colors in that tree node. let node_params = { let tree = TREE.clone(); @@ -566,48 +576,50 @@ fn get_stability_witness(node_id: String, variable: String, value: String) -> Ba let cmp = COMPUTATION.read().unwrap(); if let Some(cmp) = &*cmp { let components = if let Some(classifier) = &cmp.classifier { - classifier.export_components() + if let Some(behaviour) = behaviour { + classifier.export_components_with_class(behaviour) + } else { + classifier + .export_components() + .into_iter() + .map(|(c, _)| c) + .collect() + } } 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 sinks = graph.mk_empty_vertices(); - for (attractor, behaviour) in components { - let attractor = attractor.intersect_colors(&node_params); - if attractor.is_empty() { - continue; - } - for (b, c) in behaviour { - if b == Stability { - let sink_states = attractor.intersect_colors(&c); - sinks = sinks.union(&sink_states); - } - } - } - let variable = if let Some(id) = graph.as_network().as_graph().find_variable(&variable) - { - id + let variable = graph + .as_network() + .as_graph() + .find_variable(variable_str.as_str()); + let variable = if let Some(variable) = variable { + variable } else { - return BackendResponse::err(&format!("Unknown variable {}.", variable)); - }; - let all_colors = sinks.colors(); - let var_is_true = graph.fix_network_variable(variable, true); - let var_is_false = graph.fix_network_variable(variable, false); - let colors_where_true = sinks.intersect(&var_is_true).colors(); - let colors_where_false = sinks.intersect(&var_is_false).colors(); - let only_true_colors = all_colors.minus(&colors_where_false); - let only_false_colors = all_colors.minus(&colors_where_true); - let mixed_colors = colors_where_true.intersect(&colors_where_false); - let colors = match value.as_str() { - "true" => only_true_colors, - "false" => only_false_colors, - "mixed" => mixed_colors, - _ => { - return BackendResponse::err(&format!("Unrecognized var value {}.", value)); - } + return BackendResponse::err( + format!("Unknown graph variable `{}`.", variable_str).as_str(), + ); }; - get_witness_network(&colors) + + // Now compute which attractors are actually relevant for the node colors + let components = components + .into_iter() + .filter_map(|attractor| { + attractor + .intersect_colors(&node_params) + .take_if(|it| !it.is_empty()) + }) + .collect::>(); + + let variable_stability = + VariableStability::for_attractors(graph, &components, variable); + if let Some(colors) = &variable_stability[vector] { + get_witness_network(colors) + } else { + return BackendResponse::err( + format!("No witness available for vector `{}`.", vector_str).as_str(), + ); + } } else { BackendResponse::err(&"No attractor data found.") } @@ -705,8 +717,31 @@ fn get_tree_attractors(node_id: String) -> BackendResponse { }; } -#[get("/get_stability_attractors///")] -fn get_stability_attractors(node_id: String, variable: String, value: String) -> BackendResponse { +#[get("/get_stability_attractors////")] +fn get_stability_attractors( + node_id: String, + behaviour_str: String, + variable_str: String, + vector_str: String, +) -> BackendResponse { + let behaviour = Behaviour::try_from(behaviour_str.as_str()); + let behaviour = match behaviour { + Ok(behaviour) => Some(behaviour), + Err(error) => { + if behaviour_str == "total" { + None + } else { + return BackendResponse::err(error.as_str()); + } + } + }; + let vector = StabilityVector::try_from(vector_str.as_str()); + let vector = match vector { + Ok(vector) => vector, + Err(error) => { + return BackendResponse::err(error.as_str()); + } + }; // First, extract all colors in that tree node. let node_params = { let tree = TREE.clone(); @@ -727,48 +762,50 @@ fn get_stability_attractors(node_id: String, variable: String, value: String) -> let cmp = COMPUTATION.read().unwrap(); if let Some(cmp) = &*cmp { let components = if let Some(classifier) = &cmp.classifier { - classifier.export_components() + if let Some(behaviour) = behaviour { + classifier.export_components_with_class(behaviour) + } else { + classifier + .export_components() + .into_iter() + .map(|(c, _)| c) + .collect() + } } 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 sinks = graph.mk_empty_vertices(); - for (attractor, behaviour) in components { - let attractor = attractor.intersect_colors(&node_params); - if attractor.is_empty() { - continue; - } - for (b, c) in behaviour { - if b == Stability { - let sink_states = attractor.intersect_colors(&c); - sinks = sinks.union(&sink_states); - } - } - } - let variable = if let Some(id) = graph.as_network().as_graph().find_variable(&variable) - { - id + let variable = graph + .as_network() + .as_graph() + .find_variable(variable_str.as_str()); + let variable = if let Some(variable) = variable { + variable } else { - return BackendResponse::err(&format!("Unknown variable {}.", variable)); - }; - let all_colors = sinks.colors(); - let var_is_true = graph.fix_network_variable(variable, true); - let var_is_false = graph.fix_network_variable(variable, false); - let colors_where_true = sinks.intersect(&var_is_true).colors(); - let colors_where_false = sinks.intersect(&var_is_false).colors(); - let only_true_colors = all_colors.minus(&colors_where_false); - let only_false_colors = all_colors.minus(&colors_where_true); - let mixed_colors = colors_where_true.intersect(&colors_where_false); - let colors = match value.as_str() { - "true" => only_true_colors, - "false" => only_false_colors, - "mixed" => mixed_colors, - _ => { - return BackendResponse::err(&format!("Unrecognized var value {}.", value)); - } + return BackendResponse::err( + format!("Unknown graph variable `{}`.", variable_str).as_str(), + ); }; - get_witness_attractors(&colors) + + // Now compute which attractors are actually relevant for the node colors + let components = components + .into_iter() + .filter_map(|attractor| { + attractor + .intersect_colors(&node_params) + .take_if(|it| !it.is_empty()) + }) + .collect::>(); + + let variable_stability = + VariableStability::for_attractors(graph, &components, variable); + if let Some(colors) = &variable_stability[vector] { + get_witness_attractors(colors) + } else { + return BackendResponse::err( + format!("No witness available for vector `{}`.", vector_str).as_str(), + ); + } } else { BackendResponse::err(&"No attractor data found.") } diff --git a/src/scc/_impl_behaviour.rs b/src/scc/_impl_behaviour.rs new file mode 100644 index 0000000..6c11ac0 --- /dev/null +++ b/src/scc/_impl_behaviour.rs @@ -0,0 +1,15 @@ +use crate::scc::Behaviour; +use std::convert::TryFrom; + +impl TryFrom<&str> for Behaviour { + type Error = String; + + fn try_from(value: &str) -> Result { + match value { + "S" => Ok(Behaviour::Stability), + "D" => Ok(Behaviour::Disorder), + "O" => Ok(Behaviour::Oscillation), + _ => Err(format!("Invalid behaviour string `{}`.", value)), + } + } +} diff --git a/src/scc/_impl_classifier.rs b/src/scc/_impl_classifier.rs index 6fa502d..ca20fee 100644 --- a/src/scc/_impl_classifier.rs +++ b/src/scc/_impl_classifier.rs @@ -57,6 +57,18 @@ impl Classifier { (*data).clone() } + /// Export only components that have the specified behaviour. + pub fn export_components_with_class(&self, class: Behaviour) -> Vec { + let data = self.attractors.lock().unwrap().clone(); + data.into_iter() + .filter_map(|(attractor, behaviour)| { + behaviour + .get(&class) + .map(|colors| attractor.intersect_colors(colors)) + }) + .collect() + } + /// Static function to classify just one component and immediately obtain results. pub fn classify_component( component: &GraphColoredVertices, diff --git a/src/scc/algo_stability_analysis/_impl_attractor_stability_data.rs b/src/scc/algo_stability_analysis/_impl_attractor_stability_data.rs new file mode 100644 index 0000000..11d621a --- /dev/null +++ b/src/scc/algo_stability_analysis/_impl_attractor_stability_data.rs @@ -0,0 +1,62 @@ +use crate::scc::algo_stability_analysis::{AttractorStabilityData, Stability}; +use crate::util::functional::Functional; +use biodivine_lib_param_bn::biodivine_std::traits::Set; +use biodivine_lib_param_bn::symbolic_async_graph::{ + GraphColoredVertices, GraphColors, SymbolicAsyncGraph, +}; +use biodivine_lib_param_bn::VariableId; +use std::ops::Index; + +impl Index for AttractorStabilityData { + type Output = GraphColors; + + fn index(&self, index: Stability) -> &Self::Output { + match index { + Stability::True => &self.stability_true, + Stability::False => &self.stability_false, + Stability::Unstable => &self.unstable, + } + } +} + +impl AttractorStabilityData { + /// Perform stability analysis for one attractor and one variable. + pub fn for_attractor( + graph: &SymbolicAsyncGraph, + attractor: &GraphColoredVertices, + variable: VariableId, + ) -> AttractorStabilityData { + let var_is_true = graph.fix_network_variable(variable, true); + let var_is_false = graph.fix_network_variable(variable, false); + let colors_with_true = attractor.intersect(&var_is_true).colors(); + let colors_with_false = attractor.intersect(&var_is_false).colors(); + let colors_with_both = colors_with_true.intersect(&colors_with_false); + AttractorStabilityData { + stability_true: colors_with_true.minus(&colors_with_both), + stability_false: colors_with_false.minus(&colors_with_both), + unstable: colors_with_both, + } + .also(|data| { + let all = data + .stability_true + .union(&data.stability_false) + .union(&data.unstable); + if all != attractor.colors() { + panic!("Mismatched attractor colors."); + } + if !data + .stability_true + .intersect(&data.stability_false) + .is_empty() + { + panic!("FAIL"); + } + if !data.stability_false.intersect(&data.unstable).is_empty() { + panic!("FAIL"); + } + if !data.unstable.intersect(&data.stability_true).is_empty() { + panic!("FAIL"); + } + }) + } +} diff --git a/src/scc/algo_stability_analysis/_impl_stability.rs b/src/scc/algo_stability_analysis/_impl_stability.rs new file mode 100644 index 0000000..fa7e78c --- /dev/null +++ b/src/scc/algo_stability_analysis/_impl_stability.rs @@ -0,0 +1,49 @@ +use crate::scc::algo_stability_analysis::Stability; +use std::convert::TryFrom; +use std::fmt::{Display, Formatter}; + +impl Display for Stability { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Stability::True => write!(f, "true"), + Stability::False => write!(f, "false"), + Stability::Unstable => write!(f, "unstable"), + } + } +} + +impl TryFrom<&str> for Stability { + type Error = String; + + fn try_from(value: &str) -> Result { + match value { + "true" => Ok(Stability::True), + "false" => Ok(Stability::False), + "unstable" => Ok(Stability::Unstable), + _ => Err(format!("Invalid stability value `{}`.", value)), + } + } +} + +#[cfg(test)] +mod tests { + use crate::scc::algo_stability_analysis::Stability; + use std::convert::TryFrom; + + #[test] + pub fn stability_serialisation() { + assert_eq!( + Stability::True, + Stability::try_from(Stability::True.to_string().as_str()).unwrap() + ); + assert_eq!( + Stability::False, + Stability::try_from(Stability::False.to_string().as_str()).unwrap() + ); + assert_eq!( + Stability::Unstable, + Stability::try_from(Stability::Unstable.to_string().as_str()).unwrap() + ); + assert!(Stability::try_from("TRUE").is_err()); + } +} diff --git a/src/scc/algo_stability_analysis/_impl_stability_vector.rs b/src/scc/algo_stability_analysis/_impl_stability_vector.rs new file mode 100644 index 0000000..8bf1b27 --- /dev/null +++ b/src/scc/algo_stability_analysis/_impl_stability_vector.rs @@ -0,0 +1,155 @@ +use crate::scc::algo_stability_analysis::{Stability, StabilityVector}; +use crate::util::functional::Functional; +use json::JsonValue; +use std::convert::TryFrom; +use std::fmt::{Display, Formatter}; +use std::ops::Shr; + +impl Display for StabilityVector { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "[")?; + if self.has_true { + if self.has_false || self.has_unstable { + write!(f, "true,")?; + } else { + write!(f, "true")?; + } + } + if self.has_false { + if self.has_unstable { + write!(f, "false,")?; + } else { + write!(f, "false")?; + } + } + if self.has_unstable { + write!(f, "unstable")?; + } + write!(f, "]") + } +} + +impl From for usize { + fn from(vector: StabilityVector) -> Self { + 0usize.apply(|id| { + if vector.has_true { + *id |= 0b1; + } + if vector.has_false { + *id |= 0b10; + } + if vector.has_unstable { + *id |= 0b100; + } + }) + } +} + +impl TryFrom for StabilityVector { + type Error = String; + + fn try_from(value: usize) -> Result { + let mut vector = StabilityVector::default(); + if value.shr(3) != 0usize { + return Err(format!("Invalid stability vector id: `{}`.", value)); + } + vector.has_true = (value & 0b1) != 0; + vector.has_false = (value & 0b10) != 0; + vector.has_unstable = (value & 0b100) != 0; + Ok(vector) + } +} + +impl TryFrom<&str> for StabilityVector { + type Error = String; + + fn try_from(value: &str) -> Result { + if value.starts_with('[') && value.ends_with(']') { + let value = &value[1..value.len() - 1]; + let mut vector = StabilityVector::default(); + for part in value.split(',') { + match part { + "true" => { + if vector.has_true { + return Err("Duplicate `true` in a stability vector.".to_string()); + } + vector.has_true = true + } + "false" => { + if vector.has_false { + return Err("Duplicate `false` in a stability vector.".to_string()); + } + vector.has_false = true + } + "unstable" => { + if vector.has_unstable { + return Err("Duplicate `unstable` in a stability vector.".to_string()); + } + vector.has_unstable = true + } + _ => { + if !part.is_empty() { + return Err(format!("Unexpected `{}` in a stability vector.", part)); + } + } + } + } + Ok(vector) + } else { + Err(format!("Invalid stability vector: `{}`.", value)) + } + } +} + +impl StabilityVector { + /// Create a new stability vector which includes the given stability value. + /// + /// If the value is already present, current vector is only copied. + pub fn add(&self, stability: Stability) -> StabilityVector { + self.clone().apply(|out| match stability { + Stability::True => out.has_true = true, + Stability::False => out.has_false = true, + Stability::Unstable => out.has_unstable = true, + }) + } + + pub fn is_empty(&self) -> bool { + !(self.has_unstable || self.has_false || self.has_true) + } + + pub fn to_json(&self) -> JsonValue { + JsonValue::new_array().apply(|array| { + if self.has_true { + array.push("true").unwrap(); + } + if self.has_false { + array.push("false").unwrap(); + } + if self.has_unstable { + array.push("unstable").unwrap(); + } + }) + } +} + +#[cfg(test)] +mod tests { + use crate::scc::algo_stability_analysis::StabilityVector; + use std::convert::TryFrom; + + #[test] + fn stability_to_string() { + for id in 0..8usize { + let vector = StabilityVector::try_from(id).unwrap(); + assert_eq!( + vector, + StabilityVector::try_from(vector.to_string().as_str()).unwrap() + ); + } + assert!(StabilityVector::try_from("true").is_err()); + assert!(StabilityVector::try_from("[true,true]").is_err()); + assert!(StabilityVector::try_from("[true-false]").is_err()); + assert!(StabilityVector::try_from("[true,false,false]").is_err()); + assert!(StabilityVector::try_from("[unstable,unstable,true]").is_err()); + } +} diff --git a/src/scc/algo_stability_analysis/_impl_variable_stability.rs b/src/scc/algo_stability_analysis/_impl_variable_stability.rs new file mode 100644 index 0000000..2218537 --- /dev/null +++ b/src/scc/algo_stability_analysis/_impl_variable_stability.rs @@ -0,0 +1,105 @@ +use crate::scc::algo_stability_analysis::{ + AttractorStabilityData, Stability, StabilityVector, VariableStability, +}; +use crate::util::functional::Functional; +use biodivine_lib_param_bn::biodivine_std::traits::Set; +use biodivine_lib_param_bn::symbolic_async_graph::{ + GraphColoredVertices, GraphColors, SymbolicAsyncGraph, +}; +use biodivine_lib_param_bn::VariableId; +use json::JsonValue; +use std::convert::TryFrom; +use std::ops::{Index, IndexMut}; + +impl Index for VariableStability { + type Output = Option; + + fn index(&self, index: StabilityVector) -> &Self::Output { + let id: usize = index.into(); + &self.0[id] + } +} + +impl IndexMut for VariableStability { + fn index_mut(&mut self, index: StabilityVector) -> &mut Self::Output { + let id: usize = index.into(); + &mut self.0[id] + } +} + +impl VariableStability { + /// Add a value for behaviour if not present, otherwise union with current value. + pub fn push(&mut self, behaviour: StabilityVector, colors: GraphColors) { + if let Some(current) = self[behaviour].as_mut() { + *current = colors.union(current); + } else { + self[behaviour] = Some(colors); + } + } + + /// Convert this stability data to a vector of pairs. + pub fn to_vec(&self) -> Vec<(StabilityVector, GraphColors)> { + self.0 + .iter() + .enumerate() + .filter_map(|(id, p)| p.clone().map(|p| (id, p))) + .map(|(i, colors)| (StabilityVector::try_from(i).unwrap(), colors)) + .collect() + } + + /// Compute stability data for a fixed variable and all available attractors. + pub fn for_attractors( + graph: &SymbolicAsyncGraph, + attractors: &[GraphColoredVertices], + variable: VariableId, + ) -> VariableStability { + let mut stability = VariableStability::default(); + let all_colors = attractors + .iter() + .fold(graph.mk_empty_colors(), |a, b| a.union(&b.colors())); + stability.push(StabilityVector::default(), all_colors); + for attractor in attractors { + let attractor_stability = + AttractorStabilityData::for_attractor(graph, attractor, variable); + let mut updated_stability = VariableStability::default(); + for (vector, colors) in stability.to_vec() { + let add_true = attractor_stability.stability_true.intersect(&colors); + let add_false = attractor_stability.stability_false.intersect(&colors); + let add_unstable = attractor_stability.unstable.intersect(&colors); + let remaining = colors + .minus(&add_true) + .minus(&add_false) + .minus(&add_unstable); + if !add_true.is_empty() { + let new_key = vector.add(Stability::True); + updated_stability.push(new_key, add_true); + } + if !add_false.is_empty() { + updated_stability.push(vector.add(Stability::False), add_false); + } + if !add_unstable.is_empty() { + updated_stability.push(vector.add(Stability::Unstable), add_unstable); + } + if !remaining.is_empty() { + updated_stability.push(vector, remaining); + } + } + stability = updated_stability; + } + + stability + } + + pub fn to_json(&self) -> JsonValue { + JsonValue::new_array().apply(|array| { + for (vector, colors) in self.to_vec() { + array + .push(object! { + "vector": vector.to_json(), + "colors": colors.approx_cardinality(), + }) + .unwrap(); + } + }) + } +} diff --git a/src/scc/algo_stability_analysis/mod.rs b/src/scc/algo_stability_analysis/mod.rs index 8f300b7..9140dcf 100644 --- a/src/scc/algo_stability_analysis/mod.rs +++ b/src/scc/algo_stability_analysis/mod.rs @@ -1,37 +1,65 @@ -use biodivine_lib_param_bn::biodivine_std::traits::Set; -use biodivine_lib_param_bn::symbolic_async_graph::{GraphColoredVertices, SymbolicAsyncGraph}; +use biodivine_lib_param_bn::symbolic_async_graph::{ + GraphColoredVertices, GraphColors, SymbolicAsyncGraph, +}; use biodivine_lib_param_bn::VariableId; -use json::JsonValue; +use std::collections::HashMap; -/// Given a set of model sinks, this computes which variables have a fixed constant value -/// and which variables can depend on parametrisation, while also including the proportion -/// in which they appear with either value (or both). -pub fn stability_analysis( +mod _impl_attractor_stability_data; +mod _impl_stability; +mod _impl_stability_vector; +mod _impl_variable_stability; + +/// A basic enum which defines the stability of a particular variable in one attractor. +/// +/// In such case, a variable can be stable, with either true or false as a value, or unstable. +/// That is, the value of the variable is changing in the attractor. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub enum Stability { + True, + False, + Unstable, +} + +/// For a given attractor, this struct stores the parametrised data about stability of a +/// particular variable. +/// +/// Essentially, it is a mapping from `Stability` values to `GraphColors`. Since there are only +/// three stability values, it is easy to store them explicitly. +#[derive(Clone, PartialEq, Eq, Hash, Debug)] +pub struct AttractorStabilityData { + stability_true: GraphColors, + stability_false: GraphColors, + unstable: GraphColors, +} + +/// Stability vector encodes the possible stability phenotypes of a particular variable in +/// multiple attractors. +/// +/// `StabilityVector` is basically a set of `Stability` values. But since there are only three, +/// we can again implement this a bit more concisely. +#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug, Ord, PartialOrd, Default)] +pub struct StabilityVector { + has_true: bool, + has_false: bool, + has_unstable: bool, +} + +/// For multiple attractors, the stability of a variable is a mapping from possible +/// `StabilityVectors` to `GraphColors`. +#[derive(Clone, Eq, PartialEq, Hash, Debug, Default)] +pub struct VariableStability([Option; 8]); + +/// All stability data for all variables. +pub type StabilityData = HashMap; + +/// Compute all stability data for all variables. +pub fn compute_stability( graph: &SymbolicAsyncGraph, - sinks: &GraphColoredVertices, - variable: VariableId, -) -> JsonValue { - let var_is_true = graph.fix_network_variable(variable, true); - let var_is_false = graph.fix_network_variable(variable, false); - let all_colors = sinks.colors(); - let name = graph.as_network().get_variable_name(variable).clone(); - if sinks.intersect(&var_is_false).is_empty() { - // Every sink is stable and has value true - object! { "name": name, "constant": true } - } else if sinks.intersect(&var_is_true).is_empty() { - object! { "name": name, "constant": false } - } else { - // The variable value depends on attractor structure and parameters. - let colors_where_true = sinks.intersect(&var_is_true).colors(); - let colors_where_false = sinks.intersect(&var_is_false).colors(); - let only_true_colors = all_colors.minus(&colors_where_false); - let only_false_colors = all_colors.minus(&colors_where_true); - let mixed_colors = colors_where_true.intersect(&colors_where_false); - object! { - "name": name, - "only_true": only_true_colors.approx_cardinality(), - "only_false": only_false_colors.approx_cardinality(), - "mixed": mixed_colors.approx_cardinality(), - } - } + components: &[GraphColoredVertices], +) -> StabilityData { + graph + .as_network() + .variables() + .map(|id| (id, VariableStability::for_attractors(graph, components, id))) + .collect() } diff --git a/src/scc/mod.rs b/src/scc/mod.rs index 7b913b5..a3fa8b9 100644 --- a/src/scc/mod.rs +++ b/src/scc/mod.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use std::sync::atomic::AtomicU32; use std::sync::Mutex; +mod _impl_behaviour; /// **(internal)** Utility methods for the behaviour `Class`. mod _impl_class; /// **(internal)** Implementation of `Behaviour` classification in `Classifier`.