diff --git a/data/test_data/test_model_with_data.aeon b/data/test_data/test_model_with_data.aeon new file mode 100644 index 0000000..0c80b95 --- /dev/null +++ b/data/test_data/test_model_with_data.aeon @@ -0,0 +1,35 @@ +C -?? A +C -| C +D -> B +D -? D +B -> C +A -> C +A -> B +$A: h(C) +$B: f(A, D) +$C: A & g(C, B) +#position:A:346.89832,183.03789 +#position:C:504.7078,101.93903 +#position:B:0,0 +#position:D:642.49677,185.15988 +#!dataset:data_fp:#`{"name":"data_fp","id":"data_fp","annotation":"","observations":[{"id":"ones","name":"ones","annotation":"","dataset":"data_fp","values":"1111"},{"id":"zeros","name":"zeros","annotation":"","dataset":"data_fp","values":"0000"}],"variables":["A","B","C","D"]}`# +#!dataset:data_mts:#`{"name":"data_mts","id":"data_mts","annotation":"","observations":[{"id":"abc","name":"abc","annotation":"","dataset":"data_mts","values":"111*"},{"id":"ab","name":"ab","annotation":"","dataset":"data_mts","values":"11**"}],"variables":["A","B","C","D"]}`# +#!dataset:data_time_series:#`{"name":"data_time_series","id":"data_time_series","annotation":"","observations":[{"id":"a","name":"a","annotation":"","dataset":"data_time_series","values":"1000"},{"id":"b","name":"b","annotation":"","dataset":"data_time_series","values":"1100"},{"id":"c","name":"c","annotation":"","dataset":"data_time_series","values":"1110"},{"id":"d","name":"d","annotation":"","dataset":"data_time_series","values":"1111"}],"variables":["A","B","C","D"]}`# +#!function:f:#`{"id":"f","name":"f","annotation":"","arguments":[["Unknown","Unknown"],["Unknown","Unknown"]],"expression":""}`# +#!function:g:#`{"id":"g","name":"g","annotation":"","arguments":[["Unknown","Unknown"],["Unknown","Unknown"]],"expression":""}`# +#!function:h:#`{"id":"h","name":"h","annotation":"","arguments":[["Unknown","Unknown"]],"expression":""}`# +#!static_property:essentiality_A_B:#`{"id":"essentiality_A_B","name":"Regulation essentiality property","annotation":"","variant":"RegulationEssential","input":"A","target":"B","value":"True","context":null}`# +#!static_property:essentiality_A_C:#`{"id":"essentiality_A_C","name":"Regulation essentiality property","annotation":"","variant":"RegulationEssential","input":"A","target":"C","value":"True","context":null}`# +#!static_property:essentiality_B_C:#`{"id":"essentiality_B_C","name":"Regulation essentiality property","annotation":"","variant":"RegulationEssential","input":"B","target":"C","value":"True","context":null}`# +#!static_property:essentiality_C_C:#`{"id":"essentiality_C_C","name":"Regulation essentiality property","annotation":"","variant":"RegulationEssential","input":"C","target":"C","value":"True","context":null}`# +#!static_property:essentiality_D_B:#`{"id":"essentiality_D_B","name":"Regulation essentiality property","annotation":"","variant":"RegulationEssential","input":"D","target":"B","value":"True","context":null}`# +#!static_property:essentiality_D_D:#`{"id":"essentiality_D_D","name":"Regulation essentiality property","annotation":"","variant":"RegulationEssential","input":"D","target":"D","value":"True","context":null}`# +#!static_property:monotonicity_A_B:#`{"id":"monotonicity_A_B","name":"Regulation monotonicity property","annotation":"","variant":"RegulationMonotonic","input":"A","target":"B","value":"Activation","context":null}`# +#!static_property:monotonicity_A_C:#`{"id":"monotonicity_A_C","name":"Regulation monotonicity property","annotation":"","variant":"RegulationMonotonic","input":"A","target":"C","value":"Activation","context":null}`# +#!static_property:monotonicity_B_C:#`{"id":"monotonicity_B_C","name":"Regulation monotonicity property","annotation":"","variant":"RegulationMonotonic","input":"B","target":"C","value":"Activation","context":null}`# +#!static_property:monotonicity_C_C:#`{"id":"monotonicity_C_C","name":"Regulation monotonicity property","annotation":"","variant":"RegulationMonotonic","input":"C","target":"C","value":"Inhibition","context":null}`# +#!static_property:monotonicity_D_B:#`{"id":"monotonicity_D_B","name":"Regulation monotonicity property","annotation":"","variant":"RegulationMonotonic","input":"D","target":"B","value":"Activation","context":null}`# +#!variable:A:#`{"id":"A","name":"A","annotation":"","update_fn":"h(C)"}`# +#!variable:B:#`{"id":"B","name":"B","annotation":"","update_fn":"f(A, D)"}`# +#!variable:C:#`{"id":"C","name":"C","annotation":"","update_fn":"A & g(C, B)"}`# +#!variable:D:#`{"id":"D","name":"D","annotation":"","update_fn":""}`# diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 952ba63..429ae70 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -12,9 +12,9 @@ readme = "README.md" tauri-build = { version = "1.5", features = [] } [dependencies] -biodivine-lib-bdd = ">=0.5.19, <1.0.0" -biodivine-lib-param-bn = ">=0.5.11, <1.0.0" -biodivine-hctl-model-checker = ">=0.3.0, <1.0.0" +biodivine-lib-bdd = ">=0.5.22, <1.0.0" +biodivine-lib-param-bn = ">=0.5.13, <1.0.0" +biodivine-hctl-model-checker = ">=0.3.1, <1.0.0" chrono = "0.4.38" csv = "1.3" lazy_static = "1.5.0" diff --git a/src-tauri/src/analysis/results_export.rs b/src-tauri/src/analysis/results_export.rs index 436894a..ffb1af3 100644 --- a/src-tauri/src/analysis/results_export.rs +++ b/src-tauri/src/analysis/results_export.rs @@ -9,12 +9,14 @@ use std::path::Path; use zip::write::{FileOptions, ZipWriter}; /// Export archive with complete results to the given path. +/// The output archive is tailored for a case where sketch is satisfiable. +/// /// The results archive include: /// - a summary report (basically information tracked by the `InferenceResults` struct) /// - original sketch in JSON format for replicability in SketchBook /// - BDD with satisfying colors /// - a PSBN model derived from the sketch (in aeon format) that can be used as a context for the BDD -/// - a folder with update function variants per variable +/// - a folder with admissible update function variants per variable pub fn export_results( path: &str, finished_solver: &FinishedInferenceSolver, @@ -81,6 +83,8 @@ fn write_to_zip( Ok(()) } +/// Prepare a formated summary of inference results, basically a "report" on the +/// computation progress and results. fn format_inference_results(results: &InferenceResults) -> String { let mut output = String::new(); @@ -129,7 +133,8 @@ fn format_inference_results(results: &InferenceResults) -> String { } /// For a given variable, get all valid interpretations of its update function present in the -/// satisfying `colors` (taken from the results of the solver). Variable must be present in the network. +/// satisfying `colors` (taken from the results of the solver). +/// Variable must be present in the network. pub fn get_update_fn_variants_from_solver( solver: &FinishedInferenceSolver, var_name: &str, diff --git a/src-tauri/src/sketchbook/_sketch/_impl_export.rs b/src-tauri/src/sketchbook/_sketch/_impl_export.rs index a91ab3d..e4603a9 100644 --- a/src-tauri/src/sketchbook/_sketch/_impl_export.rs +++ b/src-tauri/src/sketchbook/_sketch/_impl_export.rs @@ -1,4 +1,8 @@ -use crate::sketchbook::data_structs::SketchData; +use biodivine_lib_param_bn::ModelAnnotation; + +use crate::sketchbook::data_structs::{ + DatasetData, DynPropertyData, SketchData, StatPropertyData, UninterpretedFnData, VariableData, +}; use crate::sketchbook::{JsonSerde, Sketch}; use std::fs::File; use std::io::Write; @@ -24,3 +28,78 @@ impl Sketch { Ok(()) } } + +impl Sketch { + /// Convert the sketch instance into a customized version of AEON model format. + /// + /// This format includes the standard AEON format for PSBN and layout. + /// This format is compatible with other biodivine tools, but might not cover all + /// parts of the sketch. + /// + /// Apart from that, most remaining details of the sketch are given via model annotations. + /// Currently the annotations are given simpy as + /// #!entity_type: ID: #`json_string`# + /// These entities can be variables, functions, static/dynamic properties, and datasets. + pub fn to_aeon(&self) -> String { + // for standard part of aeon format, we use the transformation into aeon BN + // this loses some info (like new regulation types), but that is preserved via annotations + let bn = self.model.to_bn(); + let mut aeon_str = bn.to_string(); + + // set layout info + let default_layout = self.model.get_default_layout(); + for (var_id, node) in default_layout.layout_nodes() { + // write position in format #position:ID:X,Y + let pos = node.get_position(); + let node_layout_str = format!("#position:{var_id}:{},{}\n", pos.0, pos.1); + aeon_str.push_str(&node_layout_str); + } + + // set the rest using aeon model annotations + let mut annotation = ModelAnnotation::new(); + + // set static properties + for (id, stat_prop) in self.properties.stat_props() { + let prop_data_json = StatPropertyData::from_property(id, stat_prop).to_json_str(); + annotation.ensure_value(&["static_property", id.as_str()], &prop_data_json); + } + // set dynamic properties + for (id, dyn_prop) in self.properties.dyn_props() { + let prop_data_json = DynPropertyData::from_property(id, dyn_prop).to_json_str(); + annotation.ensure_value(&["dynamic_property", id.as_str()], &prop_data_json); + } + // set datasets + for (id, dataset) in self.observations.datasets() { + let dataset_data_json = DatasetData::from_dataset(id, dataset).to_json_str(); + annotation.ensure_value(&["dataset", id.as_str()], &dataset_data_json); + } + // set variable details + for (var_id, variable) in self.model.variables() { + let update_fn = self.model.get_update_fn(var_id).unwrap(); + let var_data_json = VariableData::from_var(var_id, variable, update_fn).to_json_str(); + annotation.ensure_value(&["variable", var_id.as_str()], &var_data_json); + } + // set function details + for (fn_id, uninterpreted_fn) in self.model.uninterpreted_fns() { + let fn_data_json = UninterpretedFnData::from_fn(fn_id, uninterpreted_fn).to_json_str(); + annotation.ensure_value(&["function", fn_id.as_str()], &fn_data_json); + } + + // push the annotations to the aeon string + let annotation_str = annotation.to_string(); + aeon_str.push_str(&annotation_str); + aeon_str + } + + /// Export the sketch instance into a customized version of AEON model format. + /// + /// See [Sketch::to_aeon] for details on the actual conversion. + pub fn export_to_aeon(&self, filepath: &str) -> Result<(), String> { + let aeon_str = self.to_aeon(); + let mut file = File::create(filepath).map_err(|e| e.to_string())?; + // write sketch in AEON to the file + file.write_all(aeon_str.as_bytes()) + .map_err(|e| e.to_string())?; + Ok(()) + } +} diff --git a/src-tauri/src/sketchbook/_sketch/_impl_import.rs b/src-tauri/src/sketchbook/_sketch/_impl_import.rs index 0ea296a..3e45fd6 100644 --- a/src-tauri/src/sketchbook/_sketch/_impl_import.rs +++ b/src-tauri/src/sketchbook/_sketch/_impl_import.rs @@ -1,4 +1,7 @@ -use crate::sketchbook::data_structs::SketchData; +use crate::sketchbook::data_structs::{ + DatasetData, DynPropertyData, SketchData, StatPropertyData, StatPropertyTypeData, + UninterpretedFnData, VariableData, +}; use crate::sketchbook::model::{Essentiality, ModelState, Monotonicity}; use crate::sketchbook::properties::shortcuts::*; use crate::sketchbook::properties::{DynProperty, StatProperty}; @@ -6,19 +9,24 @@ use crate::sketchbook::{JsonSerde, Sketch}; use biodivine_lib_param_bn::{BooleanNetwork, ModelAnnotation}; use regex::Regex; -type NamedProperties = Vec<(String, String)>; - impl Sketch { - /// Create sketch instance from a AEON model format. This variant includes: - /// - variables + /// Create sketch instance from a customized version of AEON model format. + /// The original part of the AEON format (compatible with other biodivine tools) includes: + /// - variable IDs /// - regulations (and corresponding automatically generated static properties) /// - update functions and function symbols /// - layout information - /// - HCTL dynamic properties - /// - FOL static properties /// - // TODO: our variant of aeon format currently does not consider template properties and datasets. - // TODO: our variant of aeon format currently does not consider annotation. + /// The custom extension covers most remaining parts of the sketch via model annotations. + /// Currently the annotations are given simpy as + /// #!entity_type: ID: #`json_string`# + /// These annotations either cover additional information (complementing variables and + /// functions), or completely new components like static/dynamic properties and datasets. + /// + /// We allow a special case for writing HCTL and FOL properties, compatible with the + /// original BN sketches prototype format: + /// #!static_property: ID: #`fol_formula_string`# + /// #!dynamic_property: ID: #`hctl_formula_string`# pub fn from_aeon(aeon_str: &str) -> Result { // set psbn info (variables, functions, regulations and corresponding properties) let bn = BooleanNetwork::try_from(aeon_str)?; @@ -34,16 +42,68 @@ impl Sketch { .update_position(&default_layout, &node_id, x, y)?; } - // set generic static& dynamic properties from aeon model annotations + // recover the remaining components from aeon model annotations let aeon_annotations = ModelAnnotation::from_model_string(aeon_str); - let (stat_props, dyn_props) = Self::extract_model_properties(&aeon_annotations)?; - for (name, formula) in stat_props { - let prop = StatProperty::try_mk_generic(&name, &formula, "")?; - sketch.properties.add_static_by_str(&name, prop)? + let variables = Self::extract_entities(&aeon_annotations, "variable")?; + let functions = Self::extract_entities(&aeon_annotations, "function")?; + let datasets = Self::extract_entities(&aeon_annotations, "dataset")?; + let stat_props = Self::extract_entities(&aeon_annotations, "static_property")?; + let dyn_props = Self::extract_entities(&aeon_annotations, "dynamic_property")?; + + // for variables and functions, there can be additional info (like names, annotations, ...) + for (id, variable_str) in variables { + let var_data = VariableData::from_json_str(&variable_str)?; + let var_id = sketch.model.get_var_id(&id)?; + sketch.model.set_raw_var(&var_id, var_data.to_var()?)?; + sketch.model.set_update_fn(&var_id, &var_data.update_fn)?; + } + for (id, fn_str) in functions { + let fn_data = UninterpretedFnData::from_json_str(&fn_str)?; + let fn_id = sketch.model.get_uninterpreted_fn_id(&id)?; + sketch + .model + .set_raw_function(&fn_id, fn_data.to_uninterpreted_fn(&sketch.model)?)?; + } + + // datasets have to be added from scratch + for (id, dataset_str) in datasets { + let dataset_data = DatasetData::from_json_str(&dataset_str)?; + sketch + .observations + .add_dataset_by_str(&id, dataset_data.to_dataset()?)?; + } + + // properties have to be added from scratch (apart from automatically generated static props) + // we allow two modes - a JSON string for any property, or formula string for HCTL/FOL properties + for (id, content_str) in stat_props { + // try parsing formula + if let Ok(prop) = StatProperty::try_mk_generic(&id, &content_str, "") { + sketch.properties.add_static_by_str(&id, prop)? + } else { + let prop_data = StatPropertyData::from_json_str(&content_str)?; + let property = prop_data.to_property()?; + + // ignore automatically generated static props as they were added before + // todo: this can loose some info as standard AEON format does not cover all regulation types + match prop_data.variant { + StatPropertyTypeData::RegulationEssential(..) + | StatPropertyTypeData::RegulationMonotonic(..) => {} + _ => { + sketch.properties.add_static_by_str(&id, property)?; + } + } + } } - for (name, formula) in dyn_props { - let prop = DynProperty::try_mk_generic(&name, &formula, "")?; - sketch.properties.add_dynamic_by_str(&name, prop)? + for (id, content_str) in dyn_props { + // try parsing formula + if let Ok(prop) = DynProperty::try_mk_generic(&id, &content_str, "") { + sketch.properties.add_dynamic_by_str(&id, prop)? + } else { + let prop_data = DynPropertyData::from_json_str(&content_str)?; + sketch + .properties + .add_dynamic_by_str(&id, prop_data.to_property()?)?; + } } Ok(sketch) @@ -101,8 +161,10 @@ impl Sketch { } /// Extract positions of nodes from the aeon model string. - /// Positions are lines `#position:NODE_ID:X,Y`. - /// Return list of triplets . + /// Positions are expect as lines in forllowing format: + /// #position:NODE_ID:X,Y + /// + /// This funtction returns a list of triplets . fn extract_aeon_layout_info(aeon_str: &str) -> Vec<(String, f32, f32)> { let re = Regex::new(r"^#position:(\w+):([+-]?\d+(\.\d+)?),([+-]?\d+(\.\d+)?)$").unwrap(); @@ -130,60 +192,55 @@ impl Sketch { positions } - /// Extract two lists of named properties (static and dynamic) from an `.aeon` model + /// Extract list of named entities (tuples with id/content) from an `.aeon` model /// annotation object. /// - /// The properties are expected to appear as one of: - /// - `#!dynamic_property: NAME: HCTL_FORMULA` for dynamic properties. - /// - `#!static_property: NAME: FOL_FORMULA` for static properties. + /// The entities are expected to appear as: + /// #!entity_type: ID: #`CONTENT`# + /// So, for example: + /// #!variable:ANT:#`{"id":"ANT","name":"ANT","annotation":"","update_fn":""}`# /// - /// Each list is returned in alphabetic order w.r.t. the property name. - fn extract_model_properties( + /// Each list is returned in alphabetic order w.r.t. the entity name. + fn extract_entities( annotations: &ModelAnnotation, - ) -> Result<(NamedProperties, NamedProperties), String> { - let stat_props = if let Some(property_node) = annotations.get_child(&["static_property"]) { - Self::process_property_node(property_node)? - } else { - Vec::new() - }; - let dyn_props = if let Some(property_node) = annotations.get_child(&["dynamic_property"]) { - Self::process_property_node(property_node)? + entity_type: &str, + ) -> Result, String> { + if let Some(entity_node) = annotations.get_child(&[entity_type]) { + Self::process_entity_node(entity_node, entity_type) } else { - Vec::new() - }; - Ok((stat_props, dyn_props)) + Ok(Vec::new()) + } } - /// Given a `ModelAnnotation` node corresponding to `dynamic_property` or `static_property`, - /// collect all named properties from its child nodes. - /// - /// This is possible because both `dynamic_property` and `static_property` annotations - /// share common structure. + /// Given a `ModelAnnotation` node corresponding to a particular entity type (like 'variable'), + /// collect all entities of given type from the child nodes. /// - /// The properties are expected to appear as one of: - /// - `#!dynamic_property: NAME: FORMULA` for dynamic properties. - /// - `#!static_property: NAME: FORMULA` for static properties. + /// This is general for all entity types as annotations share common structure. + /// #!entity_type: ID: #`CONTENT`# /// - /// Each list is returned in alphabetic order w.r.t. the property name. - fn process_property_node( - property_node: &ModelAnnotation, + /// List is returned in alphabetic order w.r.t. the property name. + fn process_entity_node( + enitity_node: &ModelAnnotation, + enitity_type: &str, ) -> Result, String> { - let mut properties = Vec::with_capacity(property_node.children().len()); - for (name, child) in property_node.children() { + let mut entities = Vec::with_capacity(enitity_node.children().len()); + for (id, child) in enitity_node.children() { if !child.children().is_empty() { - return Err(format!("Property `{name}` contains nested values.")); + return Err(format!("{enitity_type} `{id}` contains nested values.")); } let Some(value) = child.value() else { - return Err(format!("Found empty dynamic property `{name}`.")); + return Err(format!("Found empty {enitity_type} `{id}`.")); }; if value.lines().count() > 1 { - return Err(format!("Found multiple properties named `{name}`.")); + return Err(format!( + "Found multiple entities of type {enitity_type} with id `{id}`." + )); } - properties.push((name.clone(), value.clone())); + entities.push((id.clone(), value.clone())); } // Sort alphabetically to avoid possible non-determinism down the line. - properties.sort_by(|(x, _), (y, _)| x.cmp(y)); - Ok(properties) + entities.sort_by(|(x, _), (y, _)| x.cmp(y)); + Ok(entities) } } @@ -207,8 +264,8 @@ mod tests { #[test] /// Test that importing the same data from different formats results in the same sketch. - /// These models only include PSBN (no additional datasets or porperties). - fn sketch_import() { + /// These models only include PSBN (no additional datasets or properties). + fn basic_import() { let mut aeon_sketch_file = File::open("../data/test_data/test_model.aeon").unwrap(); let mut json_sketch_file = File::open("../data/test_data/test_model.json").unwrap(); let mut sbml_sketch_file = File::open("../data/test_data/test_model.sbml").unwrap(); @@ -227,4 +284,23 @@ mod tests { assert_eq!(sketch1, sketch2); assert_eq!(sketch2, sketch3); } + + #[test] + /// Test that importing the same data from aeon and json format results in the same sketch. + /// This test involves a full sketch format with datasets and properties. + fn full_import() { + let mut aeon_sketch_file = + File::open("../data/test_data/test_model_with_data.aeon").unwrap(); + let mut json_sketch_file = + File::open("../data/test_data/test_model_with_data.json").unwrap(); + + let mut aeon_contents = String::new(); + aeon_sketch_file.read_to_string(&mut aeon_contents).unwrap(); + let mut json_contents = String::new(); + json_sketch_file.read_to_string(&mut json_contents).unwrap(); + + let sketch1 = Sketch::from_aeon(&aeon_contents).unwrap(); + let sketch2 = Sketch::from_custom_json(&json_contents).unwrap(); + assert_eq!(sketch1, sketch2); + } } diff --git a/src-tauri/src/sketchbook/_sketch/_impl_session_state.rs b/src-tauri/src/sketchbook/_sketch/_impl_session_state.rs index de1f351..03434a2 100644 --- a/src-tauri/src/sketchbook/_sketch/_impl_session_state.rs +++ b/src-tauri/src/sketchbook/_sketch/_impl_session_state.rs @@ -31,6 +31,10 @@ impl SessionState for Sketch { let path = Self::clone_payload_str(event, "sketch")?; self.export_to_custom_json(&path)?; Ok(Consumed::NoChange) + } else if Self::starts_with("export_aeon", at_path).is_some() { + let path = Self::clone_payload_str(event, "sketch")?; + self.export_to_aeon(&path)?; + Ok(Consumed::NoChange) } else if Self::starts_with("import_sketch", at_path).is_some() { let file_path = Self::clone_payload_str(event, "sketch")?; // read the file contents diff --git a/src-tauri/src/sketchbook/data_structs/mod.rs b/src-tauri/src/sketchbook/data_structs/mod.rs index 4d25b20..4e2bc54 100644 --- a/src-tauri/src/sketchbook/data_structs/mod.rs +++ b/src-tauri/src/sketchbook/data_structs/mod.rs @@ -27,7 +27,7 @@ mod _uninterpreted_fn_data; mod _variable_data; pub use _dataset_data::{DatasetData, DatasetMetaData}; -pub use _dynamic_prop_data::DynPropertyData; +pub use _dynamic_prop_data::{DynPropertyData, DynPropertyTypeData}; pub use _fn_arg_change_data::{ChangeArgEssentialData, ChangeArgMonotoneData}; pub use _id_change_data::ChangeIdData; pub use _layout_data::{LayoutData, LayoutMetaData}; @@ -36,6 +36,6 @@ pub use _model_data::ModelData; pub use _observation_data::ObservationData; pub use _regulation_data::RegulationData; pub use _sketch_data::SketchData; -pub use _static_prop_data::StatPropertyData; +pub use _static_prop_data::{StatPropertyData, StatPropertyTypeData}; pub use _uninterpreted_fn_data::UninterpretedFnData; pub use _variable_data::{VariableData, VariableWithLayoutData}; diff --git a/src-tauri/src/sketchbook/model/_model_state/_impl_observing.rs b/src-tauri/src/sketchbook/model/_model_state/_impl_observing.rs index 7c5544e..6bfb1aa 100644 --- a/src-tauri/src/sketchbook/model/_model_state/_impl_observing.rs +++ b/src-tauri/src/sketchbook/model/_model_state/_impl_observing.rs @@ -228,6 +228,12 @@ impl ModelState { .ok_or(format!("Layout with ID {id} does not exist in this model.")) } + /// Return a default `Layout`. + pub fn get_default_layout(&self) -> &Layout { + let default_id = ModelState::get_default_layout_id(); + self.layouts.get(&default_id).unwrap() + } + /// Return a valid layout's `LayoutId` corresponding to the Id given by a `String`. /// /// Return `Err` if such variable does not exist (and the ID is invalid). diff --git a/src/aeon_state.ts b/src/aeon_state.ts index 0835d07..e642f9d 100644 --- a/src/aeon_state.ts +++ b/src/aeon_state.ts @@ -175,8 +175,10 @@ interface AeonState { /** Refresh the whole sketch. */ refreshSketch: () => void - /** Export the sketch data to a file. */ + /** Export the sketch data to a file in the custom JSON format. */ exportSketch: (path: string) => void + /** Export the sketch data to a file in the extended AEON format. */ + exportAeon: (path: string) => void /** Import the sketch data from a special sketch JSON file. */ importSketch: (path: string) => void /** Import the sketch data from a AEON file. */ @@ -600,6 +602,12 @@ export const aeonState: AeonState = { payload: path }) }, + exportAeon (path: string): void { + aeonEvents.emitAction({ + path: ['sketch', 'export_aeon'], + payload: path + }) + }, importSketch (path: string): void { aeonEvents.emitAction({ path: ['sketch', 'import_sketch'], diff --git a/src/html/component-analysis/analysis-component/analysis-component.ts b/src/html/component-analysis/analysis-component/analysis-component.ts index a9b3549..d04b73e 100644 --- a/src/html/component-analysis/analysis-component/analysis-component.ts +++ b/src/html/component-analysis/analysis-component/analysis-component.ts @@ -218,21 +218,24 @@ export default class AnalysisComponent extends LitElement { .map(statusReport => statusReport.message) .join('\n') - // prepare the summary with update functions per variable, sorted by var name - const updateFnsSummary = Object.entries(results.num_update_fns_per_var) - .sort(([varNameA], [varNameB]) => varNameA.localeCompare(varNameB)) - .map(([varName, count]) => { - const countDisplay = count >= 1000 ? 'more than 1000' : count.toString() - return `${varName}: ${countDisplay}` - }) - .join('\n') - - return '--------------\nExtended summary:\n--------------\n' + - `${results.summary_message}\n` + - '--------------\nNumber of admissible update functions per variable:\n--------------\n' + - updateFnsSummary + '\n\n' + - '--------------\nDetailed progress report:\n--------------\n' + + let resultsMessage = '--------------\nExtended summary:\n--------------\n' + + `${results.summary_message}\n` + if (results.num_sat_networks > 0) { + // prepare the summary with update functions per variable, sorted by var name + const updateFnsSummary = Object.entries(results.num_update_fns_per_var) + .sort(([varNameA], [varNameB]) => varNameA.localeCompare(varNameB)) + .map(([varName, count]) => { + const countDisplay = count >= 1000 ? 'more than 1000' : count.toString() + return `${varName}: ${countDisplay}` + }) + .join('\n') + + resultsMessage += '--------------\nNumber of admissible update functions per variable:\n--------------\n' + + updateFnsSummary + '\n\n' + } + resultsMessage += '--------------\nDetailed progress report:\n--------------\n' + progressSummary + return resultsMessage } private async resetAnalysis (): Promise { diff --git a/src/html/component-editor/menu/menu.ts b/src/html/component-editor/menu/menu.ts index 36b69fd..e09c1d9 100644 --- a/src/html/component-editor/menu/menu.ts +++ b/src/html/component-editor/menu/menu.ts @@ -38,6 +38,10 @@ export default class Menu extends LitElement { label: 'Export JSON', action: () => { void this.exportSketch() } }, + { + label: 'Export AEON', + action: () => { void this.exportAeon() } + }, { label: 'Quit', action: () => { void this.quit() } @@ -99,7 +103,7 @@ export default class Menu extends LitElement { async exportSketch (): Promise { const filePath = await save({ - title: 'Export sketch...', + title: 'Export sketch in JSON format...', filters: [{ name: '*.json', extensions: ['json'] @@ -108,10 +112,25 @@ export default class Menu extends LitElement { }) if (filePath === null) return - console.log('exporting to', filePath) + console.log('exporting json to', filePath) aeonState.sketch.exportSketch(filePath) } + async exportAeon (): Promise { + const filePath = await save({ + title: 'Export sketch in extended AEON format...', + filters: [{ + name: '*.aeon', + extensions: ['aeon'] + }], + defaultPath: 'project_name_here' + }) + if (filePath === null) return + + console.log('exporting aeon to', filePath) + aeonState.sketch.exportAeon(filePath) + } + async newSketch (): Promise { const confirmation = await dialog.ask('Starting new sketch will erase the current one. Do you want to proceed?', { type: 'warning',