Skip to content

Add annotations that can be used to verify network integrity. #61

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 209 additions & 5 deletions src/_aeon_parser/_from_string_for_boolean_network.rs
Original file line number Diff line number Diff line change
@@ -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<Self, Self::Error> {
// 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().keys() {
data.push(child.clone());

Check warning on line 27 in src/_aeon_parser/_from_string_for_boolean_network.rs

View check run for this annotation

Codecov / codecov/patch

src/_aeon_parser/_from_string_for_boolean_network.rs#L27

Added line #L27 was not covered by tests
}
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"])

Check warning on line 42 in src/_aeon_parser/_from_string_for_boolean_network.rs

View check run for this annotation

Codecov / codecov/patch

src/_aeon_parser/_from_string_for_boolean_network.rs#L41-L42

Added lines #L41 - L42 were not covered by tests
.cloned()
.unwrap_or_else(|| "0".to_string());
expected.insert(name, arity);

Check warning on line 45 in src/_aeon_parser/_from_string_for_boolean_network.rs

View check run for this annotation

Codecov / codecov/patch

src/_aeon_parser/_from_string_for_boolean_network.rs#L44-L45

Added lines #L44 - L45 were not covered by tests
}
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.");

Check warning on line 64 in src/_aeon_parser/_from_string_for_boolean_network.rs

View check run for this annotation

Codecov / codecov/patch

src/_aeon_parser/_from_string_for_boolean_network.rs#L64

Added line #L64 was not covered by tests
}

// trim lines and remove comments
let lines = value.lines().filter_map(|l| {
let line = l.trim();
Expand Down Expand Up @@ -50,6 +106,25 @@
let mut variable_names: Vec<String> = 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!(

Check warning on line 114 in src/_aeon_parser/_from_string_for_boolean_network.rs

View check run for this annotation

Codecov / codecov/patch

src/_aeon_parser/_from_string_for_boolean_network.rs#L114

Added line #L114 was not covered by tests
"Variable `{}` used, but not declared in annotations.",
var
));
}
}
for var in &expected_variables {
if !variable_names.contains(var) {
variable_names.push(var.clone());

Check warning on line 122 in src/_aeon_parser/_from_string_for_boolean_network.rs

View check run for this annotation

Codecov / codecov/patch

src/_aeon_parser/_from_string_for_boolean_network.rs#L122

Added line #L122 was not covered by tests
}
}
variable_names.sort();
}

let mut rg = RegulatoryGraph::new(variable_names);

for reg in regulations {
Expand All @@ -76,9 +151,35 @@

// Add the parameters (if there is a cardinality clash, here it will be thrown).
for parameter in &parameters {
if check_declarations {
if let Some(expected_arity) = expected_parameters.get(&parameter.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(&parameter.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)?;
Expand Down Expand Up @@ -199,19 +300,122 @@

#[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
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());
}
}
20 changes: 20 additions & 0 deletions src/_impl_annotations/_impl_annotation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,26 @@
pub fn children_mut(&mut self) -> &mut HashMap<String, ModelAnnotation> {
&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<String> {
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;

Check warning on line 143 in src/_impl_annotations/_impl_annotation.rs

View check run for this annotation

Codecov / codecov/patch

src/_impl_annotations/_impl_annotation.rs#L143

Added line #L143 was not covered by tests
} else {
self.value = Some(lines.join("\n"));
}
}
}

impl Default for ModelAnnotation {
Expand Down
29 changes: 28 additions & 1 deletion src/_impl_boolean_network_display.rs
Original file line number Diff line number Diff line change
@@ -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::<Vec<_>>();
// 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
Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ pub struct Space(Vec<ExtendedBoolean>);
/// 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.
Expand Down Expand Up @@ -329,7 +329,7 @@ pub struct Space(Vec<ExtendedBoolean>);
/// 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.
Expand Down