From 63d9ee0d299c15c59fcb569d9e2dffcbbcb46900 Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Fri, 14 Feb 2025 19:39:26 +0100 Subject: [PATCH 1/2] Add annotations that can be used to verify network integrity. --- .../_from_string_for_boolean_network.rs | 214 +++++++++++++++++- src/_impl_annotations/_impl_annotation.rs | 20 ++ src/_impl_boolean_network_display.rs | 29 ++- src/lib.rs | 4 +- 4 files changed, 259 insertions(+), 8 deletions(-) diff --git a/src/_aeon_parser/_from_string_for_boolean_network.rs b/src/_aeon_parser/_from_string_for_boolean_network.rs index 228cddc..cfb08fb 100644 --- a/src/_aeon_parser/_from_string_for_boolean_network.rs +++ b/src/_aeon_parser/_from_string_for_boolean_network.rs @@ -1,13 +1,69 @@ use crate::_aeon_parser::{FnUpdateTemp, RegulationTemp}; -use crate::{BooleanNetwork, Parameter, RegulatoryGraph}; +use crate::{BooleanNetwork, ModelAnnotation, Parameter, RegulatoryGraph}; use regex::Regex; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::convert::TryFrom; impl TryFrom<&str> for BooleanNetwork { type Error = String; fn try_from(value: &str) -> Result { + // This parsing should never fail, so it should be safe to do this... + let annotations = ModelAnnotation::from_model_string(value); + + // If the model is requesting a declaration check, we should perform it. Otherwise, + // declarations are only informative. + let check_declarations = annotations + .get_child(&["check_declarations"]) + .and_then(|it| it.value()) + .map(|it| it.as_str() == "true") + .unwrap_or(false); + + // Read declared variables. Variable is declared either as a string name in the "variable" + // annotation, or by a corresponding child annotation. + let expected_variables = if let Some(decl) = annotations.get_child(&["variable"]) { + let mut data = decl.read_multiline_value(); + for (child, _) in decl.children() { + data.push(child.clone()); + } + data + } else { + Vec::new() + }; + + // Try to read parameter declarations from the model annotation data. A parameter is + // declared if it is present as a name inside "function" annotation, or if it is present + // as one of its children. If arity is not present, it is zero. + let expected_parameters = if let Some(decl) = annotations.get_child(&["function"]) { + let all_names = decl.read_multiline_value(); + let mut expected = HashMap::new(); + for name in all_names { + let arity = decl + .get_value(&[name.as_str(), "arity"]) + .cloned() + .unwrap_or_else(|| "0".to_string()); + expected.insert(name, arity); + } + for (child, data) in decl.children() { + if !expected.contains_key(child) { + let arity = data + .get_value(&["arity"]) + .cloned() + .unwrap_or_else(|| "0".to_string()); + expected.insert(child.clone(), arity); + } + } + expected + } else { + HashMap::new() + }; + + if (!expected_variables.is_empty() || !expected_parameters.is_empty()) + && !check_declarations + { + eprintln!("WARNING: Network contains variable or function declarations, but integrity checking is turned off."); + } + // trim lines and remove comments let lines = value.lines().filter_map(|l| { let line = l.trim(); @@ -50,6 +106,25 @@ impl TryFrom<&str> for BooleanNetwork { let mut variable_names: Vec = variable_names.into_iter().collect(); variable_names.sort(); + // If a variable is declared, but not present in the graph, we can still create it. + // But if it is present yet not declared, that's a problem. + if check_declarations { + for var in &variable_names { + if !expected_variables.contains(var) { + return Err(format!( + "Variable `{}` used, but not declared in annotations.", + var + )); + } + } + for var in &expected_variables { + if !variable_names.contains(var) { + variable_names.push(var.clone()); + } + } + variable_names.sort(); + } + let mut rg = RegulatoryGraph::new(variable_names); for reg in regulations { @@ -76,9 +151,35 @@ impl TryFrom<&str> for BooleanNetwork { // Add the parameters (if there is a cardinality clash, here it will be thrown). for parameter in ¶meters { + if check_declarations { + if let Some(expected_arity) = expected_parameters.get(¶meter.name) { + if &format!("{}", parameter.arity) != expected_arity { + return Err(format!( + "Parameter `{}` is declared with arity `{}`, but used with arity `{}`.", + parameter.name, expected_arity, parameter.arity + )); + } + } else { + return Err(format!( + "Network has parameter `{}` that is not declared in annotations.", + parameter.name + )); + } + } bn.add_parameter(¶meter.name, parameter.arity)?; } + if check_declarations { + for param_name in expected_parameters.keys() { + if bn.find_parameter(param_name).is_none() { + return Err(format!( + "Parameter `{}` declared in annotations, but not found in the network.", + param_name + )); + } + } + } + // Actually build and add the functions for (name, function) in update_functions { bn.add_template_update_function(&name, function)?; @@ -199,7 +300,47 @@ mod tests { #[test] fn test_bn_from_and_to_string() { - let bn_string = "a -> b + // Without parameters: + let bn_string = format!( + "#!check_declarations:true +#!variable:a +#!variable:b +#!variable:c +#!variable:d +#!version:lib_param_bn:{} + +a -> b +a -?? a +b -|? c +c -? a +c -> d +$a: a & !c +$b: a +$c: !b +", + env!("CARGO_PKG_VERSION") + ); + + assert_eq!( + bn_string, + BooleanNetwork::try_from(bn_string.as_str()) + .unwrap() + .to_string() + ); + + // With parameters: + let bn_string = format!( + "#!check_declarations:true +#!function:k:arity:0 +#!function:p:arity:1 +#!function:q:arity:2 +#!variable:a +#!variable:b +#!variable:c +#!variable:d +#!version:lib_param_bn:{} + +a -> b a -?? a b -|? c c -? a @@ -207,11 +348,74 @@ c -> d $a: a & (p(c) => (c | c)) $b: p(a) <=> q(a, a) $c: q(b, b) => !(b ^ k) -"; +", + env!("CARGO_PKG_VERSION") + ); assert_eq!( bn_string, - BooleanNetwork::try_from(bn_string).unwrap().to_string() + BooleanNetwork::try_from(bn_string.as_str()) + .unwrap() + .to_string() ); } + + #[test] + fn test_bn_with_parameter_declarations() { + let bn_string = r" + #! check_declarations:true + #! function: f: arity: 2 + #! variable: a + #! variable: b + #! variable: x + + a -> x + b -> x + $x: f(a, b) + "; + assert!(BooleanNetwork::try_from(bn_string).is_ok()); + + // Wrong arity + let bn_string = r" + #! check_declarations:true + #! function: f: arity: 1 + #! variable: a + #! variable: b + #! variable: x + + a -> x + b -> x + $x: f(a, b) + "; + assert!(BooleanNetwork::try_from(bn_string).is_err()); + + // Unused declaration + let bn_string = r" + #! check_declarations:true + #! function: f: arity: 2 + #! function: g: arity: 1 + #! variable: a + #! variable: b + #! variable: x + + a -> x + b -> x + $x: f(a, b) + "; + assert!(BooleanNetwork::try_from(bn_string).is_err()); + + // Parameter not declared + let bn_string = r" + #! check_declarations:true + #! function: f: arity: 2 + #! variable: a + #! variable: b + #! variable: x + + a -> x + b -> x + $x: g(a, b) + "; + assert!(BooleanNetwork::try_from(bn_string).is_err()); + } } diff --git a/src/_impl_annotations/_impl_annotation.rs b/src/_impl_annotations/_impl_annotation.rs index 45451cb..2605845 100644 --- a/src/_impl_annotations/_impl_annotation.rs +++ b/src/_impl_annotations/_impl_annotation.rs @@ -125,6 +125,26 @@ impl ModelAnnotation { pub fn children_mut(&mut self) -> &mut HashMap { &mut self.inner } + + /// A utility method to read values that store data per-line as a vector of lines. If the + /// value is `None`, this returns an empty vector. + pub fn read_multiline_value(&self) -> Vec { + if let Some(value) = self.value.as_ref() { + value.lines().map(|line| line.to_string()).collect() + } else { + Vec::new() + } + } + + /// A utility method to write a list of values, one per-line. If the list is empty, + /// the value is set to `None`. + pub fn write_multiline_value(&mut self, lines: &[String]) { + if lines.is_empty() { + self.value = None; + } else { + self.value = Some(lines.join("\n")); + } + } } impl Default for ModelAnnotation { diff --git a/src/_impl_boolean_network_display.rs b/src/_impl_boolean_network_display.rs index 5f4b416..2fb9db8 100644 --- a/src/_impl_boolean_network_display.rs +++ b/src/_impl_boolean_network_display.rs @@ -1,8 +1,35 @@ -use crate::BooleanNetwork; +use crate::{BooleanNetwork, ModelAnnotation}; use std::fmt::{Display, Error, Formatter}; impl Display for BooleanNetwork { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { + let mut ann = ModelAnnotation::new(); + + // Save the library version for later, and declare that we want basic integrity checks. + ann.ensure_value(&["version", "lib_param_bn"], env!("CARGO_PKG_VERSION")); + ann.ensure_value(&["check_declarations"], "true"); + + // Write variable declarations: + let variable_declarations = ann.ensure_child(&["variable"]); + let var_names = self + .variables() + .map(|it| self.get_variable_name(it).clone()) + .collect::>(); + // Write all variable names as a multi-line value. + variable_declarations.write_multiline_value(&var_names); + + // Write parameter declarations: + let function_declarations = ann.ensure_child(&["function"]); + // Write names and arities together: + for param in &self.parameters { + let p_arity = function_declarations + .ensure_child(&[param.name.as_str(), "arity"]) + .value_mut(); + *p_arity = Some(param.arity.to_string()); + } + + writeln!(f, "{}", ann)?; + write!(f, "{}", self.graph)?; for var in self.variables() { // print all update functions diff --git a/src/lib.rs b/src/lib.rs index 51b873c..4dec448 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -301,7 +301,7 @@ pub struct Space(Vec); /// properties that are not directly recognized by the main AEON toolbox. /// /// Annotations are comments which start with `#!`. After the `#!` "preamble", each annotation -/// can contains a "path prefix" with path segments separated using `:` (path segments can be +/// can contain a "path prefix" with path segments separated using `:` (path segments can be /// surrounded by white space that is automatically trimmed). Based on these path /// segments, the parser will create an annotation tree. If there are multiple annotations with /// the same path, their values are concatenated using newlines. @@ -329,7 +329,7 @@ pub struct Space(Vec); /// You can use "empty" path (e.g. `#! is_multivalued`), and you can use an empty annotation /// value with a non-empty path (e.g. `#!is_multivalued:var_1:`). Though this is not particularly /// encouraged: it is better to just have `var_1` as the annotation value if you can do that. -/// An exception to this may be a case where `is_multivalued:var_1:` has an "optional" value and +/// An exception to this may be a case where `is_multivalued:var_1:` has an "optional" value, and /// you want to express that while the "key" is provided, the "value" is missing. Similarly, for /// the sake of completeness, it is technically allowed to use empty path names (e.g. `a::b:value` /// translates to `["a", "", "b"] = "value"`), but it is discouraged. From adf0043e6a02ffefd7a5ed348836e59628141197 Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Fri, 14 Feb 2025 19:44:56 +0100 Subject: [PATCH 2/2] Clippy. --- src/_aeon_parser/_from_string_for_boolean_network.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_aeon_parser/_from_string_for_boolean_network.rs b/src/_aeon_parser/_from_string_for_boolean_network.rs index cfb08fb..97dd1c6 100644 --- a/src/_aeon_parser/_from_string_for_boolean_network.rs +++ b/src/_aeon_parser/_from_string_for_boolean_network.rs @@ -23,7 +23,7 @@ impl TryFrom<&str> for BooleanNetwork { // annotation, or by a corresponding child annotation. let expected_variables = if let Some(decl) = annotations.get_child(&["variable"]) { let mut data = decl.read_multiline_value(); - for (child, _) in decl.children() { + for child in decl.children().keys() { data.push(child.clone()); } data