diff --git a/src/model/_impl_bma_model.rs b/src/model/_impl_bma_model.rs index 8f2a494..cb0aa30 100644 --- a/src/model/_impl_bma_model.rs +++ b/src/model/_impl_bma_model.rs @@ -1,7 +1,10 @@ use crate::enums::{RelationshipType, VariableType}; use crate::model::bma_model::*; use crate::update_fn::bma_fn_tree::BmaFnUpdate; -use biodivine_lib_param_bn::{BooleanNetwork, RegulatoryGraph}; +use crate::update_fn::enums::{AggregateFn, ArithOp}; + +use biodivine_lib_param_bn::{BooleanNetwork, Monotonicity, RegulatoryGraph}; + use regex::Regex; use std::cmp::max; use std::collections::HashMap; @@ -56,17 +59,85 @@ impl BmaModel { /// regulations. The update functions are transformed using [BmaFnUpdate::to_update_fn]. pub fn to_boolean_network(&self) -> Result { // TODO: for now, we do not handle multi-valued models + if !self.is_boolean_model() { return Err("Cannot convert multi-valued model to a Boolean network.".to_string()); } let graph = self.to_regulatory_graph()?; - let bn = BooleanNetwork::new(graph); + let mut bn = BooleanNetwork::new(graph); + + // TODO: this will have to change to handle multi-valued models + let mut max_levels = HashMap::new(); + for var in &self.model.variables { + let var_name = BmaModel::canonical_var_name(var); + max_levels.insert(var_name, var.range_to); + } // add update functions - self.model.variables.iter().for_each(|_var| { - // todo - convert the formula to update functions - }); + for var in &self.model.variables { + let var_name = BmaModel::canonical_var_name(var); + let var_id = bn.as_graph().find_variable(&var_name).unwrap(); + + if var.range_to == 0 { + // We can have zero constants and we must deal with these accordingly. + bn.add_string_update_function(&var_name, "false").unwrap() + } + + if let Some(bma_formula) = var.formula.clone() { + let update_fn = bma_formula.to_update_fn(&max_levels); + bn.set_update_function(var_id, Some(update_fn))?; + } else { + // The formula is empty, which means we have to build a default one + // the same way as BMA is doing this. + // We then convert this default BMA expression to a logical formula. + + let regulators = bn.regulators(var_id); + if regulators.is_empty() { + // This is an undetermined input, in which case we set it to zero, + // because that's what BMA does. + bn.add_string_update_function(&var_name, "false").unwrap() + } + + // We build the default function the same way as BMA does. + let mut positive = Vec::new(); + let mut negative = Vec::new(); + for regulator in regulators { + let regulator_name = bn.get_variable_name(regulator); + let reg = bn.as_graph().find_regulation(regulator, var_id).unwrap(); + // BMA variables must be monotonic + match reg.monotonicity.unwrap() { + Monotonicity::Activation => positive.push(regulator_name), + Monotonicity::Inhibition => negative.push(regulator_name), + } + } + + let p_avr = if !positive.is_empty() { + let p_args = positive + .iter() + .map(|x| BmaFnUpdate::mk_variable(x)) + .collect(); + BmaFnUpdate::mk_aggregation(AggregateFn::Avg, p_args) + } else { + // This does not make much sense, because it means any variable with only negative + // regulators is ALWAYS a constant zero. But this is how BMA seems to be doing it, so + // that's what we are doing as well... + BmaFnUpdate::mk_constant(0) + }; + let n_avr = if !negative.is_empty() { + let n_args = negative + .iter() + .map(|x| BmaFnUpdate::mk_variable(x)) + .collect(); + BmaFnUpdate::mk_aggregation(AggregateFn::Avg, n_args) + } else { + BmaFnUpdate::mk_constant(0) + }; + let default_bma_formula = BmaFnUpdate::mk_arithmetic(p_avr, n_avr, ArithOp::Minus); + let update_fn = default_bma_formula.to_update_fn(&max_levels); + bn.set_update_function(var_id, Some(update_fn))?; + } + } Ok(bn) } diff --git a/src/update_fn/_impl_to_update_fn.rs b/src/update_fn/_impl_to_update_fn.rs index 328d903..40806a5 100644 --- a/src/update_fn/_impl_to_update_fn.rs +++ b/src/update_fn/_impl_to_update_fn.rs @@ -3,14 +3,18 @@ use crate::update_fn::enums::{AggregateFn, ArithOp, Literal, UnaryFn}; use biodivine_lib_param_bn::FnUpdate; use num_rational::Rational32; use num_traits::sign::Signed; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; impl BmaFnUpdate { /// Convert the BMA expression into corresponding `FnUpdate` instance of /// [biodivine_lib_param_bn] library. /// /// TODO: implementation via explicit construction of the function table - pub fn to_update_fn(&self) -> FnUpdate { + pub fn to_update_fn(&self, max_levels: &HashMap) -> FnUpdate { + let mut variables: Vec = self.collect_variables().into_iter().collect(); + variables.sort(); + let _function_table = self.build_function_table(&variables, max_levels); + todo!() } @@ -75,6 +79,86 @@ impl BmaFnUpdate { } } } + + fn collect_variables(&self) -> HashSet { + match &self.expression_tree { + Expression::Terminal(Literal::Str(name)) => { + let mut set = HashSet::new(); + set.insert(name.clone()); + set + } + Expression::Terminal(Literal::Int(_)) => HashSet::new(), + Expression::Arithmetic(_, left, right) => { + let left_set = left.collect_variables(); + let right_set = right.collect_variables(); + left_set.union(&right_set).cloned().collect() + } + Expression::Unary(_, child_node) => child_node.collect_variables(), + Expression::Aggregation(_, arguments) => arguments + .iter() + .map(|arg| arg.collect_variables()) + .fold(HashSet::new(), |x, y| x.union(&y).cloned().collect()), + } + } + + /// Build a function table that maps all input combinations (valuations) to output values. + pub fn build_function_table( + &self, + variables: &[String], + max_levels: &HashMap, + ) -> Vec<(HashMap, Rational32)> { + let input_combinations = generate_input_combinations(variables, max_levels); + let mut function_table = Vec::new(); + + // Evaluate the function for each combination. + for combination in input_combinations { + match self.evaluate_in_valuation(&combination) { + Ok(output_value) => function_table.push((combination, output_value)), + Err(err) => eprintln!("Error evaluating function: {err}"), + } + } + + function_table + } +} + +/// Generate all possible input combinations for the given variables, respecting their possible levels. +pub fn generate_input_combinations( + variables: &[String], + max_levels: &HashMap, +) -> Vec> { + let mut results = Vec::new(); + let mut current_combination = HashMap::new(); + recursive_combinations( + variables, + max_levels, + &mut current_combination, + 0, + &mut results, + ); + results +} + +/// Recursive helper function to generate input combinations. +pub fn recursive_combinations( + variables: &[String], + max_levels: &HashMap, + current: &mut HashMap, + index: usize, + results: &mut Vec>, +) { + if index == variables.len() { + results.push(current.clone()); + return; + } + + let var_name = &variables[index]; + let max_level = max_levels.get(var_name).cloned().unwrap_or(0); + + for level in 0..=max_level { + current.insert(var_name.clone(), Rational32::new(level as i32, 1)); + recursive_combinations(variables, max_levels, current, index + 1, results); + } } #[cfg(test)]