Skip to content

Commit 90ce062

Browse files
authored
Daemontus/feat/set samplers (#50)
* feat: Add working samplers for VertexSet and ColorSet. * feat: Add samplers for SpaceSet, ColoredVertexSet, and ColoredSpaceSet. * feat: Add samplers for PerturbationSet and ColoredPerturbationSet. * feat: Add example script for uniform random sampling.
1 parent 4ddf409 commit 90ce062

File tree

12 files changed

+1023
-296
lines changed

12 files changed

+1023
-296
lines changed

Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@ serde = { version = "1.0", features = ["derive"] }
2828
serde_json = "1.0"
2929
regex = "1.12"
3030
pyo3 = { version = "0.27", features = ["abi3-py37", "extension-module", "num-bigint", "py-clone"] }
31-
biodivine-lib-param-bn = { version="0.7.0", features=["solver-z3", "serde"] }
31+
biodivine-lib-param-bn = { version="0.7.1", features=["solver-z3", "serde"] }
3232
biodivine-lib-bdd = { version = "0.6.2", features = ["serde"] }
3333
#biodivine-pbn-control = "0.3.1"
3434
biodivine-pbn-control = { git = "https://github.com/sybila/biodivine-pbn-control", rev = "9e31bab9d000266ea25c35cc752c69077973d43c" }
35-
biodivine-hctl-model-checker = "0.3.4"
35+
biodivine-hctl-model-checker = "0.3.5"
3636
biodivine-lib-io-bma = "0.1.2"
37-
biodivine-algo-bdd-scc = "0.3.1"
37+
biodivine-algo-bdd-scc = "0.3.2"
3838
# Cannot update `rand` because lib-bdd uses it to generate random big-int values.
3939
rand = "0.8"
4040
cancel-this = { version = "0.4.0", features = ["pyo3"] }

biodivine_aeon/__init__.pyi

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,6 +1163,7 @@ class ColorSet:
11631163
def extend_with_spaces(self, set: SpaceSet) -> ColoredSpaceSet: ...
11641164
def extend_with_perturbations(self, set: PerturbationSet) -> ColoredPerturbationSet: ...
11651165
def items(self, retained: Optional[Sequence[Union[VariableId, ParameterId, str]]] = None) -> Iterator[ColorModel]: ...
1166+
def sample_items(self, retained: Optional[Sequence[Union[VariableId, ParameterId, str]]] = None, seed: Optional[int] = None) -> Iterator[ColorModel]: ...
11661167

11671168
class ColorModel:
11681169
def __ctx__(self) -> SymbolicContext: ...
@@ -1218,6 +1219,7 @@ class VertexSet:
12181219
def to_bdd(self) -> Bdd: ...
12191220
def extend_with_colors(self, set: ColorSet) -> ColoredVertexSet: ...
12201221
def items(self, retained: Optional[Sequence[Union[str, VariableId]]] = None) -> Iterator[VertexModel]: ...
1222+
def sample_items(self, retained: Optional[Sequence[Union[str, VariableId]]] = None, seed: Optional[int] = None) -> Iterator[VertexModel]: ...
12211223
def enclosing_subspace(self) -> dict[VariableId, bool] | None: ...
12221224
def enclosing_named_subspace(self) -> dict[str, bool] | None: ...
12231225
def enclosed_subspace(self) -> dict[VariableId, bool] | None: ...
@@ -1269,6 +1271,7 @@ class SpaceSet:
12691271
def to_bdd(self) -> Bdd: ...
12701272
def extend_with_colors(self, set: ColorSet) -> ColoredSpaceSet: ...
12711273
def items(self, retained: Optional[Sequence[Union[str, VariableId]]] = None) -> Iterator[SpaceModel]: ...
1274+
def sample_items(self, retained: Optional[Sequence[Union[str, VariableId]]] = None, seed: Optional[int] = None) -> Iterator[SpaceModel]: ...
12721275
def with_all_sub_spaces(self) -> SpaceSet: ...
12731276
def with_all_super_spaces(self) -> SpaceSet: ...
12741277

@@ -1329,6 +1332,11 @@ class ColoredVertexSet:
13291332
retained_variables: Optional[Sequence[VariableIdType]] = None,
13301333
retained_functions: Optional[Sequence[Union[VariableId, ParameterId, str]]] = None
13311334
) -> Iterator[tuple[ColorModel, VertexModel]]: ...
1335+
def sample_items(self,
1336+
retained_variables: Optional[Sequence[VariableIdType]] = None,
1337+
retained_functions: Optional[Sequence[Union[VariableId, ParameterId, str]]] = None,
1338+
seed: Optional[int] = None
1339+
) -> Iterator[tuple[ColorModel, VertexModel]]: ...
13321340

13331341
class ColoredSpaceSet:
13341342
def __init__(self, ctx: SymbolicContext, bdd: Bdd):
@@ -1372,6 +1380,11 @@ class ColoredSpaceSet:
13721380
retained_variables: Optional[Sequence[VariableIdType]] = None,
13731381
retained_functions: Optional[Sequence[Union[VariableId, ParameterId, str]]] = None
13741382
) -> Iterator[tuple[ColorModel, SpaceModel]]: ...
1383+
def sample_items(self,
1384+
retained_variables: Optional[Sequence[VariableIdType]] = None,
1385+
retained_functions: Optional[Sequence[Union[VariableId, ParameterId, str]]] = None,
1386+
seed: Optional[int] = None
1387+
) -> Iterator[tuple[ColorModel, SpaceModel]]: ...
13751388
def with_all_sub_spaces(self) -> ColoredSpaceSet: ...
13761389
def with_all_super_spaces(self) -> ColoredSpaceSet: ...
13771390

@@ -1740,6 +1753,10 @@ class PerturbationSet:
17401753
def to_internal(self) -> ColoredVertexSet: ...
17411754
def extend_with_colors(self, set: ColorSet) -> ColoredPerturbationSet: ...
17421755
def items(self, retained: Optional[Sequence[VariableIdType]] = None) -> Iterator[PerturbationModel]: ...
1756+
def sample_items(self,
1757+
retained: Optional[Sequence[VariableIdType]] = None,
1758+
seed: Optional[int] = None
1759+
) -> Iterator[PerturbationModel]: ...
17431760

17441761
class PerturbationModel:
17451762
def __ctx__(self) -> AsynchronousPerturbationGraph: ...
@@ -1805,6 +1822,11 @@ class ColoredPerturbationSet:
18051822
retained_variables: Optional[Sequence[VariableIdType]] = None,
18061823
retained_functions: Optional[Sequence[Union[VariableId, ParameterId, str]]] = None
18071824
) -> Iterator[tuple[ColorModel, PerturbationModel]]: ...
1825+
def sample_items(self,
1826+
retained_variables: Optional[Sequence[VariableIdType]] = None,
1827+
retained_functions: Optional[Sequence[Union[VariableId, ParameterId, str]]] = None,
1828+
seed: Optional[int] = None
1829+
) -> Iterator[tuple[ColorModel, PerturbationModel]]: ...
18081830

18091831
class AsynchronousPerturbationGraph(AsynchronousGraph):
18101832
def __init__(self, network: BooleanNetwork, perturb: Optional[Sequence[VariableIdType]] = None):
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from biodivine_aeon import *
2+
3+
# This script showcases the ability to randomly uniformly sample
4+
# from symbolic sets. This property then allows building more complex
5+
# statistics workflows on top of it.
6+
7+
bn = BooleanNetwork.from_file('../case-study/butanol-production/butanol-pathway-f1-f2-f3.aeon')
8+
graph = AsynchronousGraph(bn)
9+
10+
print("Total number of interpretations:", graph.mk_unit_colors().cardinality())
11+
12+
# The network has three unknown functions: f_1(2), f_2(2) and f_3(3).
13+
# First, let us find all colors that exhibit the `butanol` phenotype:
14+
15+
butanol = graph.mk_subspace({ 'butanol': 1 })
16+
print("Colors where butanol=1: ", butanol.colors())
17+
butanol_phenotype = Reachability.forward_subset(graph, butanol)
18+
print("Colors where exists trap set with butanol=1: ", butanol_phenotype.colors())
19+
20+
# Now, we want to test a hypothesis that f_1 and f_2 are independent of each other.
21+
# In particular, both f_1 and f_2 can be either conjunction or disjunction. We thus
22+
# want to test whether the choice in f_1 influences the choice in f_2.
23+
24+
# (Note that we could also test this particular hypothesis exactly through
25+
# careful manipulation of the symbolic set, but it is a useful motivating example)
26+
27+
color_sampler = butanol_phenotype.colors().sample_items(retained=['f_1', 'f_2'], seed=1)
28+
samples = 0
29+
same = 0
30+
different = 0
31+
while samples < 10000:
32+
samples += 1
33+
sample = next(color_sampler)
34+
f_1_expr = sample['f_1']
35+
f_2_expr = sample['f_2']
36+
if f_1_expr.is_and() == f_2_expr.is_and():
37+
same += 1
38+
else:
39+
different += 1
40+
if samples % 1000 == 0:
41+
print(f"{same/samples:.2f} / {different/samples:.2f}")
42+
43+
# If all went according to plan, we should get a roughly 50/50 distribution in the end.
44+
45+
# Note that similar random sampling functions are available on all symbolic sets.

src/bindings/lib_param_bn/symbolic/set_color.rs

Lines changed: 129 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,17 @@ use std::collections::hash_map::DefaultHasher;
22
use std::hash::{Hash, Hasher};
33
use std::ops::Not;
44

5-
use biodivine_lib_bdd::Bdd as RsBdd;
5+
use crate::AsNative;
6+
use crate::bindings::lib_bdd::bdd::Bdd;
7+
use crate::bindings::lib_param_bn::symbolic::model_color::ColorModel;
8+
use crate::bindings::lib_param_bn::symbolic::set_colored_space::ColoredSpaceSet;
9+
use crate::bindings::lib_param_bn::symbolic::set_colored_vertex::ColoredVertexSet;
10+
use crate::bindings::lib_param_bn::symbolic::set_spaces::SpaceSet;
11+
use crate::bindings::lib_param_bn::symbolic::set_vertex::VertexSet;
12+
use crate::bindings::lib_param_bn::symbolic::symbolic_context::SymbolicContext;
13+
use crate::bindings::pbn_control::{ColoredPerturbationSet, PerturbationSet};
14+
use biodivine_lib_bdd::random_sampling::UniformValuationSampler;
15+
use biodivine_lib_bdd::{Bdd as RsBdd, BddPartialValuation};
616
use biodivine_lib_param_bn::biodivine_std::traits::Set;
717
use biodivine_lib_param_bn::symbolic_async_graph::projected_iteration::{
818
OwnedRawSymbolicIterator, RawProjection,
@@ -15,16 +25,8 @@ use pyo3::IntoPyObjectExt;
1525
use pyo3::basic::CompareOp;
1626
use pyo3::prelude::*;
1727
use pyo3::types::PyList;
18-
19-
use crate::AsNative;
20-
use crate::bindings::lib_bdd::bdd::Bdd;
21-
use crate::bindings::lib_param_bn::symbolic::model_color::ColorModel;
22-
use crate::bindings::lib_param_bn::symbolic::set_colored_space::ColoredSpaceSet;
23-
use crate::bindings::lib_param_bn::symbolic::set_colored_vertex::ColoredVertexSet;
24-
use crate::bindings::lib_param_bn::symbolic::set_spaces::SpaceSet;
25-
use crate::bindings::lib_param_bn::symbolic::set_vertex::VertexSet;
26-
use crate::bindings::lib_param_bn::symbolic::symbolic_context::SymbolicContext;
27-
use crate::bindings::pbn_control::{ColoredPerturbationSet, PerturbationSet};
28+
use rand::SeedableRng;
29+
use rand::prelude::StdRng;
2830

2931
/// A symbolic representation of a set of "colors", i.e., interpretations of explicit and
3032
/// implicit parameters within a particular `BooleanNetwork`.
@@ -44,6 +46,20 @@ pub struct _ColorModelIterator {
4446
retained_implicit: Vec<biodivine_lib_param_bn::VariableId>,
4547
}
4648

49+
/// An internal class used for random uniform sampling of `ColorModel` instances from a `ColorSet`.
50+
/// On the Python side, it just looks like an infinite iterator.
51+
///
52+
/// Similar to [`_ColorModelIterator`], but uses a sampler to get valuations from the projection
53+
/// BDD instead of iterating it fully.
54+
#[pyclass(module = "biodivine_aeon")]
55+
pub struct _ColorModelSampler {
56+
ctx: Py<SymbolicContext>,
57+
projection: RawProjection,
58+
sampler: UniformValuationSampler<StdRng>,
59+
retained_explicit: Vec<biodivine_lib_param_bn::ParameterId>,
60+
retained_implicit: Vec<biodivine_lib_param_bn::VariableId>,
61+
}
62+
4763
impl AsNative<GraphColors> for ColorSet {
4864
fn as_native(&self) -> &GraphColors {
4965
&self.native
@@ -238,6 +254,75 @@ impl ColorSet {
238254
#[pyo3(signature = (retained = None))]
239255
pub fn items(&self, retained: Option<&Bound<'_, PyList>>) -> PyResult<_ColorModelIterator> {
240256
let ctx = self.ctx.get();
257+
let (retained, implicit, explicit) = Self::read_retained_functions(ctx, retained)?;
258+
259+
let projection = RawProjection::new(retained, self.as_native().as_bdd());
260+
Ok(_ColorModelIterator {
261+
ctx: self.ctx.clone(),
262+
native: projection.into_iter(),
263+
retained_implicit: implicit,
264+
retained_explicit: explicit,
265+
})
266+
}
267+
268+
/// Returns a sampler for random uniform sampling of colors from this `ColorSet` with an
269+
/// optional projection to a subset of uninterpreted functions. **If a projection is specified,
270+
/// the sampling is uniform with respect to the projected set.**
271+
///
272+
/// See also the `items` method regarding the `retained` projection set.
273+
///
274+
/// You can specify an optional seed to make the sampling random but deterministic.
275+
#[pyo3(signature = (retained = None, seed = None))]
276+
pub fn sample_items(
277+
&self,
278+
retained: Option<&Bound<'_, PyList>>,
279+
seed: Option<u64>,
280+
) -> PyResult<_ColorModelSampler> {
281+
let ctx = self.ctx.get();
282+
let (retained, implicit, explicit) = Self::read_retained_functions(ctx, retained)?;
283+
let projection = RawProjection::new(retained, self.as_native().as_bdd());
284+
let rng = StdRng::seed_from_u64(seed.unwrap_or_default());
285+
let sampler = projection.bdd().mk_uniform_valuation_sampler(rng);
286+
Ok(_ColorModelSampler {
287+
ctx: self.ctx.clone(),
288+
projection,
289+
sampler,
290+
retained_explicit: explicit,
291+
retained_implicit: implicit,
292+
})
293+
}
294+
}
295+
296+
impl ColorSet {
297+
pub fn mk_native(ctx: Py<SymbolicContext>, native: GraphColors) -> Self {
298+
Self { ctx, native }
299+
}
300+
301+
pub fn mk_derived(&self, native: GraphColors) -> ColorSet {
302+
ColorSet {
303+
ctx: self.ctx.clone(),
304+
native,
305+
}
306+
}
307+
308+
pub fn semantic_eq(a: &ColorSet, b: &ColorSet) -> bool {
309+
let a = a.as_native().as_bdd();
310+
let b = b.as_native().as_bdd();
311+
if a.num_vars() != b.num_vars() {
312+
return false;
313+
}
314+
315+
RsBdd::binary_op_with_limit(1, a, b, biodivine_lib_bdd::op_function::xor).is_some()
316+
}
317+
318+
pub fn read_retained_functions(
319+
ctx: &SymbolicContext,
320+
retained: Option<&Bound<'_, PyList>>,
321+
) -> PyResult<(
322+
Vec<biodivine_lib_bdd::BddVariable>,
323+
Vec<biodivine_lib_param_bn::VariableId>,
324+
Vec<biodivine_lib_param_bn::ParameterId>,
325+
)> {
241326
let mut retained_explicit = Vec::new();
242327
let mut retained_implicit = Vec::new();
243328
let retained = if let Some(retained) = retained {
@@ -266,41 +351,12 @@ impl ColorSet {
266351
} else {
267352
retained_explicit.append(&mut ctx.as_native().network_parameters().collect::<Vec<_>>());
268353
retained_implicit.append(&mut ctx.as_native().network_implicit_parameters());
269-
self.ctx.get().as_native().parameter_variables().clone()
354+
ctx.as_native().parameter_variables().clone()
270355
};
271356
retained_explicit.sort();
272357
retained_implicit.sort();
273358

274-
let projection = RawProjection::new(retained, self.as_native().as_bdd());
275-
Ok(_ColorModelIterator {
276-
ctx: self.ctx.clone(),
277-
native: projection.into_iter(),
278-
retained_implicit,
279-
retained_explicit,
280-
})
281-
}
282-
}
283-
284-
impl ColorSet {
285-
pub fn mk_native(ctx: Py<SymbolicContext>, native: GraphColors) -> Self {
286-
Self { ctx, native }
287-
}
288-
289-
pub fn mk_derived(&self, native: GraphColors) -> ColorSet {
290-
ColorSet {
291-
ctx: self.ctx.clone(),
292-
native,
293-
}
294-
}
295-
296-
pub fn semantic_eq(a: &ColorSet, b: &ColorSet) -> bool {
297-
let a = a.as_native().as_bdd();
298-
let b = b.as_native().as_bdd();
299-
if a.num_vars() != b.num_vars() {
300-
return false;
301-
}
302-
303-
RsBdd::binary_op_with_limit(1, a, b, biodivine_lib_bdd::op_function::xor).is_some()
359+
Ok((retained, retained_implicit, retained_explicit))
304360
}
305361
}
306362

@@ -326,3 +382,34 @@ impl _ColorModelIterator {
326382
self.__next__()
327383
}
328384
}
385+
386+
#[pymethods]
387+
impl _ColorModelSampler {
388+
fn __iter__(self_: Py<Self>) -> Py<Self> {
389+
self_
390+
}
391+
392+
fn __next__(&mut self) -> Option<ColorModel> {
393+
self.projection
394+
.bdd()
395+
.random_valuation_sample(&mut self.sampler)
396+
.map(|it| {
397+
let mut retained = BddPartialValuation::empty();
398+
for x in self.projection.retained_variables() {
399+
retained.set_value(*x, it[*x]);
400+
}
401+
402+
ColorModel::new_native(
403+
self.ctx.clone(),
404+
retained,
405+
self.retained_implicit.clone(),
406+
self.retained_explicit.clone(),
407+
)
408+
})
409+
}
410+
411+
#[allow(clippy::should_implement_trait)]
412+
pub fn next(&mut self) -> Option<ColorModel> {
413+
self.__next__()
414+
}
415+
}

0 commit comments

Comments
 (0)