Skip to content

Commit 63d9ee0

Browse files
committed
Add annotations that can be used to verify network integrity.
1 parent 6c5e9cc commit 63d9ee0

File tree

4 files changed

+259
-8
lines changed

4 files changed

+259
-8
lines changed

src/_aeon_parser/_from_string_for_boolean_network.rs

+209-5
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,69 @@
11
use crate::_aeon_parser::{FnUpdateTemp, RegulationTemp};
2-
use crate::{BooleanNetwork, Parameter, RegulatoryGraph};
2+
use crate::{BooleanNetwork, ModelAnnotation, Parameter, RegulatoryGraph};
33
use regex::Regex;
4-
use std::collections::HashSet;
4+
use std::collections::{HashMap, HashSet};
55
use std::convert::TryFrom;
66

77
impl TryFrom<&str> for BooleanNetwork {
88
type Error = String;
99

1010
fn try_from(value: &str) -> Result<Self, Self::Error> {
11+
// This parsing should never fail, so it should be safe to do this...
12+
let annotations = ModelAnnotation::from_model_string(value);
13+
14+
// If the model is requesting a declaration check, we should perform it. Otherwise,
15+
// declarations are only informative.
16+
let check_declarations = annotations
17+
.get_child(&["check_declarations"])
18+
.and_then(|it| it.value())
19+
.map(|it| it.as_str() == "true")
20+
.unwrap_or(false);
21+
22+
// Read declared variables. Variable is declared either as a string name in the "variable"
23+
// annotation, or by a corresponding child annotation.
24+
let expected_variables = if let Some(decl) = annotations.get_child(&["variable"]) {
25+
let mut data = decl.read_multiline_value();
26+
for (child, _) in decl.children() {
27+
data.push(child.clone());
28+
}
29+
data
30+
} else {
31+
Vec::new()
32+
};
33+
34+
// Try to read parameter declarations from the model annotation data. A parameter is
35+
// declared if it is present as a name inside "function" annotation, or if it is present
36+
// as one of its children. If arity is not present, it is zero.
37+
let expected_parameters = if let Some(decl) = annotations.get_child(&["function"]) {
38+
let all_names = decl.read_multiline_value();
39+
let mut expected = HashMap::new();
40+
for name in all_names {
41+
let arity = decl
42+
.get_value(&[name.as_str(), "arity"])
43+
.cloned()
44+
.unwrap_or_else(|| "0".to_string());
45+
expected.insert(name, arity);
46+
}
47+
for (child, data) in decl.children() {
48+
if !expected.contains_key(child) {
49+
let arity = data
50+
.get_value(&["arity"])
51+
.cloned()
52+
.unwrap_or_else(|| "0".to_string());
53+
expected.insert(child.clone(), arity);
54+
}
55+
}
56+
expected
57+
} else {
58+
HashMap::new()
59+
};
60+
61+
if (!expected_variables.is_empty() || !expected_parameters.is_empty())
62+
&& !check_declarations
63+
{
64+
eprintln!("WARNING: Network contains variable or function declarations, but integrity checking is turned off.");
65+
}
66+
1167
// trim lines and remove comments
1268
let lines = value.lines().filter_map(|l| {
1369
let line = l.trim();
@@ -50,6 +106,25 @@ impl TryFrom<&str> for BooleanNetwork {
50106
let mut variable_names: Vec<String> = variable_names.into_iter().collect();
51107
variable_names.sort();
52108

109+
// If a variable is declared, but not present in the graph, we can still create it.
110+
// But if it is present yet not declared, that's a problem.
111+
if check_declarations {
112+
for var in &variable_names {
113+
if !expected_variables.contains(var) {
114+
return Err(format!(
115+
"Variable `{}` used, but not declared in annotations.",
116+
var
117+
));
118+
}
119+
}
120+
for var in &expected_variables {
121+
if !variable_names.contains(var) {
122+
variable_names.push(var.clone());
123+
}
124+
}
125+
variable_names.sort();
126+
}
127+
53128
let mut rg = RegulatoryGraph::new(variable_names);
54129

55130
for reg in regulations {
@@ -76,9 +151,35 @@ impl TryFrom<&str> for BooleanNetwork {
76151

77152
// Add the parameters (if there is a cardinality clash, here it will be thrown).
78153
for parameter in &parameters {
154+
if check_declarations {
155+
if let Some(expected_arity) = expected_parameters.get(&parameter.name) {
156+
if &format!("{}", parameter.arity) != expected_arity {
157+
return Err(format!(
158+
"Parameter `{}` is declared with arity `{}`, but used with arity `{}`.",
159+
parameter.name, expected_arity, parameter.arity
160+
));
161+
}
162+
} else {
163+
return Err(format!(
164+
"Network has parameter `{}` that is not declared in annotations.",
165+
parameter.name
166+
));
167+
}
168+
}
79169
bn.add_parameter(&parameter.name, parameter.arity)?;
80170
}
81171

172+
if check_declarations {
173+
for param_name in expected_parameters.keys() {
174+
if bn.find_parameter(param_name).is_none() {
175+
return Err(format!(
176+
"Parameter `{}` declared in annotations, but not found in the network.",
177+
param_name
178+
));
179+
}
180+
}
181+
}
182+
82183
// Actually build and add the functions
83184
for (name, function) in update_functions {
84185
bn.add_template_update_function(&name, function)?;
@@ -199,19 +300,122 @@ mod tests {
199300

200301
#[test]
201302
fn test_bn_from_and_to_string() {
202-
let bn_string = "a -> b
303+
// Without parameters:
304+
let bn_string = format!(
305+
"#!check_declarations:true
306+
#!variable:a
307+
#!variable:b
308+
#!variable:c
309+
#!variable:d
310+
#!version:lib_param_bn:{}
311+
312+
a -> b
313+
a -?? a
314+
b -|? c
315+
c -? a
316+
c -> d
317+
$a: a & !c
318+
$b: a
319+
$c: !b
320+
",
321+
env!("CARGO_PKG_VERSION")
322+
);
323+
324+
assert_eq!(
325+
bn_string,
326+
BooleanNetwork::try_from(bn_string.as_str())
327+
.unwrap()
328+
.to_string()
329+
);
330+
331+
// With parameters:
332+
let bn_string = format!(
333+
"#!check_declarations:true
334+
#!function:k:arity:0
335+
#!function:p:arity:1
336+
#!function:q:arity:2
337+
#!variable:a
338+
#!variable:b
339+
#!variable:c
340+
#!variable:d
341+
#!version:lib_param_bn:{}
342+
343+
a -> b
203344
a -?? a
204345
b -|? c
205346
c -? a
206347
c -> d
207348
$a: a & (p(c) => (c | c))
208349
$b: p(a) <=> q(a, a)
209350
$c: q(b, b) => !(b ^ k)
210-
";
351+
",
352+
env!("CARGO_PKG_VERSION")
353+
);
211354

212355
assert_eq!(
213356
bn_string,
214-
BooleanNetwork::try_from(bn_string).unwrap().to_string()
357+
BooleanNetwork::try_from(bn_string.as_str())
358+
.unwrap()
359+
.to_string()
215360
);
216361
}
362+
363+
#[test]
364+
fn test_bn_with_parameter_declarations() {
365+
let bn_string = r"
366+
#! check_declarations:true
367+
#! function: f: arity: 2
368+
#! variable: a
369+
#! variable: b
370+
#! variable: x
371+
372+
a -> x
373+
b -> x
374+
$x: f(a, b)
375+
";
376+
assert!(BooleanNetwork::try_from(bn_string).is_ok());
377+
378+
// Wrong arity
379+
let bn_string = r"
380+
#! check_declarations:true
381+
#! function: f: arity: 1
382+
#! variable: a
383+
#! variable: b
384+
#! variable: x
385+
386+
a -> x
387+
b -> x
388+
$x: f(a, b)
389+
";
390+
assert!(BooleanNetwork::try_from(bn_string).is_err());
391+
392+
// Unused declaration
393+
let bn_string = r"
394+
#! check_declarations:true
395+
#! function: f: arity: 2
396+
#! function: g: arity: 1
397+
#! variable: a
398+
#! variable: b
399+
#! variable: x
400+
401+
a -> x
402+
b -> x
403+
$x: f(a, b)
404+
";
405+
assert!(BooleanNetwork::try_from(bn_string).is_err());
406+
407+
// Parameter not declared
408+
let bn_string = r"
409+
#! check_declarations:true
410+
#! function: f: arity: 2
411+
#! variable: a
412+
#! variable: b
413+
#! variable: x
414+
415+
a -> x
416+
b -> x
417+
$x: g(a, b)
418+
";
419+
assert!(BooleanNetwork::try_from(bn_string).is_err());
420+
}
217421
}

src/_impl_annotations/_impl_annotation.rs

+20
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,26 @@ impl ModelAnnotation {
125125
pub fn children_mut(&mut self) -> &mut HashMap<String, ModelAnnotation> {
126126
&mut self.inner
127127
}
128+
129+
/// A utility method to read values that store data per-line as a vector of lines. If the
130+
/// value is `None`, this returns an empty vector.
131+
pub fn read_multiline_value(&self) -> Vec<String> {
132+
if let Some(value) = self.value.as_ref() {
133+
value.lines().map(|line| line.to_string()).collect()
134+
} else {
135+
Vec::new()
136+
}
137+
}
138+
139+
/// A utility method to write a list of values, one per-line. If the list is empty,
140+
/// the value is set to `None`.
141+
pub fn write_multiline_value(&mut self, lines: &[String]) {
142+
if lines.is_empty() {
143+
self.value = None;
144+
} else {
145+
self.value = Some(lines.join("\n"));
146+
}
147+
}
128148
}
129149

130150
impl Default for ModelAnnotation {

src/_impl_boolean_network_display.rs

+28-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,35 @@
1-
use crate::BooleanNetwork;
1+
use crate::{BooleanNetwork, ModelAnnotation};
22
use std::fmt::{Display, Error, Formatter};
33

44
impl Display for BooleanNetwork {
55
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
6+
let mut ann = ModelAnnotation::new();
7+
8+
// Save the library version for later, and declare that we want basic integrity checks.
9+
ann.ensure_value(&["version", "lib_param_bn"], env!("CARGO_PKG_VERSION"));
10+
ann.ensure_value(&["check_declarations"], "true");
11+
12+
// Write variable declarations:
13+
let variable_declarations = ann.ensure_child(&["variable"]);
14+
let var_names = self
15+
.variables()
16+
.map(|it| self.get_variable_name(it).clone())
17+
.collect::<Vec<_>>();
18+
// Write all variable names as a multi-line value.
19+
variable_declarations.write_multiline_value(&var_names);
20+
21+
// Write parameter declarations:
22+
let function_declarations = ann.ensure_child(&["function"]);
23+
// Write names and arities together:
24+
for param in &self.parameters {
25+
let p_arity = function_declarations
26+
.ensure_child(&[param.name.as_str(), "arity"])
27+
.value_mut();
28+
*p_arity = Some(param.arity.to_string());
29+
}
30+
31+
writeln!(f, "{}", ann)?;
32+
633
write!(f, "{}", self.graph)?;
734
for var in self.variables() {
835
// print all update functions

src/lib.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ pub struct Space(Vec<ExtendedBoolean>);
301301
/// properties that are not directly recognized by the main AEON toolbox.
302302
///
303303
/// Annotations are comments which start with `#!`. After the `#!` "preamble", each annotation
304-
/// can contains a "path prefix" with path segments separated using `:` (path segments can be
304+
/// can contain a "path prefix" with path segments separated using `:` (path segments can be
305305
/// surrounded by white space that is automatically trimmed). Based on these path
306306
/// segments, the parser will create an annotation tree. If there are multiple annotations with
307307
/// the same path, their values are concatenated using newlines.
@@ -329,7 +329,7 @@ pub struct Space(Vec<ExtendedBoolean>);
329329
/// You can use "empty" path (e.g. `#! is_multivalued`), and you can use an empty annotation
330330
/// value with a non-empty path (e.g. `#!is_multivalued:var_1:`). Though this is not particularly
331331
/// encouraged: it is better to just have `var_1` as the annotation value if you can do that.
332-
/// An exception to this may be a case where `is_multivalued:var_1:` has an "optional" value and
332+
/// An exception to this may be a case where `is_multivalued:var_1:` has an "optional" value, and
333333
/// you want to express that while the "key" is provided, the "value" is missing. Similarly, for
334334
/// the sake of completeness, it is technically allowed to use empty path names (e.g. `a::b:value`
335335
/// translates to `["a", "", "b"] = "value"`), but it is discouraged.

0 commit comments

Comments
 (0)