From 74baa346ea1e8a603746b695c0367111256f86b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Huvar?= <492849@mail.muni.cz> Date: Tue, 29 Oct 2024 21:04:15 +0100 Subject: [PATCH 1/4] Add time-series handling, indices to observations on FE, automatic consistency check result removal. --- .../src/algorithms/eval_dynamic/encode.rs | 9 +++++--- src-tauri/src/algorithms/eval_dynamic/eval.rs | 4 +--- .../eval_dynamic/processed_props.rs | 22 +++++++++++++------ src-tauri/src/analysis/inference_solver.rs | 4 ---- src-tauri/src/app/state/mod.rs | 2 +- src-tauri/src/sketchbook/bn_utils.rs | 2 +- .../sketchbook/observations/_observation.rs | 11 ++-------- src/aeon_state.ts | 2 +- .../analysis-tab/analysis-tab.ts | 13 +++++++++-- .../edit-observation/edit-observation.ts | 2 +- .../observations-set/observations-set.ts | 9 +++++--- .../observations-editor/tabulator-utility.ts | 13 ++++++++++- 12 files changed, 57 insertions(+), 36 deletions(-) diff --git a/src-tauri/src/algorithms/eval_dynamic/encode.rs b/src-tauri/src/algorithms/eval_dynamic/encode.rs index be37419..92be78a 100644 --- a/src-tauri/src/algorithms/eval_dynamic/encode.rs +++ b/src-tauri/src/algorithms/eval_dynamic/encode.rs @@ -7,12 +7,14 @@ use std::fmt::Write; /// Encode a dataset of observations as a single HCTL formula. The particular formula /// template is chosen depending on the type of data (attractor data, fixed-points, ...). /// -/// a) Fixed-point dataset is encoded as a conjunction of "steady-state formulas", +/// a) Fixed-point dataset is encoded with a conjunction of "steady-state formulas" /// (see [mk_formula_fixed_point_list]) that ensures each observation correspond to a fixed point. -/// b) Attractor dataset is encoded as a conjunction of "attractor formulas", +/// b) Attractor dataset is encoded with a conjunction of "attractor formulas" /// (see [mk_formula_attractor_list]) that ensures each observation correspond to an attractor. -/// b) Trap-space dataset is encoded as a conjunction of "trap-space formulas", +/// c) Trap-space dataset is encoded with a conjunction of "trap-space formulas" /// (see [mk_formula_trap_space_list]) that ensures each observation correspond to a trap space. +/// d) Time-series dataset is encoded with a "reachability chain" formula, +/// (see [mk_formula_reachability_chain]) ensuring there is path between each consecutive observations. pub fn encode_dataset_hctl_str( dataset: &Dataset, observation_id: Option, @@ -36,6 +38,7 @@ pub fn encode_dataset_hctl_str( DataEncodingType::Attractor => Ok(mk_formula_attractor_list(&encoded_observations)), DataEncodingType::FixedPoint => Ok(mk_formula_fixed_point_list(&encoded_observations)), DataEncodingType::TrapSpace => Ok(mk_formula_trap_space_list(&encoded_observations)), + DataEncodingType::TimeSeries => Ok(mk_formula_reachability_chain(&encoded_observations)), } } diff --git a/src-tauri/src/algorithms/eval_dynamic/eval.rs b/src-tauri/src/algorithms/eval_dynamic/eval.rs index 64a94b3..41cdc04 100644 --- a/src-tauri/src/algorithms/eval_dynamic/eval.rs +++ b/src-tauri/src/algorithms/eval_dynamic/eval.rs @@ -9,9 +9,7 @@ use biodivine_hctl_model_checker::model_checking::model_check_formula_dirty; use biodivine_lib_param_bn::biodivine_std::traits::Set; use biodivine_lib_param_bn::symbolic_async_graph::{GraphColors, SymbolicAsyncGraph}; -/// Evaluate given dynamic property given the transition graph. -/// -/// TODO: We still need to handle time-series properties. +/// Evaluate given dynamic property given the symbolic transition graph. pub fn eval_dyn_prop( dyn_prop: ProcessedDynProp, graph: &SymbolicAsyncGraph, diff --git a/src-tauri/src/algorithms/eval_dynamic/processed_props.rs b/src-tauri/src/algorithms/eval_dynamic/processed_props.rs index 9313c50..e517b7a 100644 --- a/src-tauri/src/algorithms/eval_dynamic/processed_props.rs +++ b/src-tauri/src/algorithms/eval_dynamic/processed_props.rs @@ -3,12 +3,13 @@ use crate::sketchbook::observations::Dataset; use crate::sketchbook::properties::dynamic_props::DynPropertyType; use crate::sketchbook::Sketch; -/// Enum of possible variants of data to encode. +/// Enum of possible variants of data encodings via HCTL. #[derive(Clone, Debug, Eq, PartialEq, Copy)] pub enum DataEncodingType { Attractor, FixedPoint, TrapSpace, + TimeSeries, } /// Property requiring that a particular HCTL formula is satisfied. @@ -102,8 +103,6 @@ pub fn process_dynamic_props(sketch: &Sketch) -> Result, S let mut processed_props = Vec::new(); for (id, dyn_prop) in dynamic_props { - // TODO: currently, some types of properties (like time-series) are still not implemented - // we translate as many types of properties into HCTL, but we also treat some // as special cases (these will have their own optimized evaluation) @@ -136,7 +135,7 @@ pub fn process_dynamic_props(sketch: &Sketch) -> Result, S DynPropertyType::GenericDynProp(prop) => { ProcessedDynProp::mk_hctl(id.as_str(), prop.processed_formula.as_str()) } - // translate to generic HCTL + // encode fixed-points HCTL formula DynPropertyType::ExistsFixedPoint(prop) => { // TODO: maybe encode as multiple formulae if we have more than one observation (instead of a conjunction)? let dataset_id = prop.dataset.clone().unwrap(); @@ -148,7 +147,7 @@ pub fn process_dynamic_props(sketch: &Sketch) -> Result, S )?; ProcessedDynProp::mk_hctl(id.as_str(), &formula) } - // translate to generic HCTL + // encode attractors with HCTL formula DynPropertyType::HasAttractor(prop) => { // TODO: maybe encode as multiple formulae if we have more than one observation (instead of a conjunction)? let dataset_id = prop.dataset.clone().unwrap(); @@ -160,8 +159,17 @@ pub fn process_dynamic_props(sketch: &Sketch) -> Result, S )?; ProcessedDynProp::mk_hctl(id.as_str(), &formula) } - // TODO: finish handling of time-series - DynPropertyType::ExistsTrajectory(..) => todo!(), + // encode time series with HCTL formula + DynPropertyType::ExistsTrajectory(prop) => { + let dataset_id = prop.dataset.clone().unwrap(); + let dataset = sketch.observations.get_dataset(&dataset_id)?; + let formula = encode_dataset_hctl_str( + dataset, + None, + DataEncodingType::TimeSeries, + )?; + ProcessedDynProp::mk_hctl(id.as_str(), &formula) + } }; processed_props.push(dyn_prop_processed); } diff --git a/src-tauri/src/analysis/inference_solver.rs b/src-tauri/src/analysis/inference_solver.rs index 56978e4..9c36135 100644 --- a/src-tauri/src/analysis/inference_solver.rs +++ b/src-tauri/src/analysis/inference_solver.rs @@ -346,8 +346,6 @@ impl InferenceSolver { /// /// The results are saved to sepcific fields of the provided solver and can be retrieved later. /// They are also returned, which is now used for logging later. - /// - /// TODO: Some parts (like evaluation for time-series properties) are still not implemented. pub async fn run_inference_async( solver: Arc>, sketch: Sketch, @@ -485,8 +483,6 @@ impl InferenceSolver { /// Internal modular variant of the inference. You can choose which parts to select. /// For example, you can only consider static properties, only dynamic properties, or all. - /// - /// TODO: Some parts (like evaluation for time-series properties) are still not implemented. pub(crate) fn run_inference_modular( &mut self, analysis_type: InferenceType, diff --git a/src-tauri/src/app/state/mod.rs b/src-tauri/src/app/state/mod.rs index b63d17d..a8078c6 100644 --- a/src-tauri/src/app/state/mod.rs +++ b/src-tauri/src/app/state/mod.rs @@ -249,7 +249,7 @@ pub trait StackSession: SessionState { let perform = UserAction { events: perform }; let reverse = UserAction { events: reverse }; if !self.undo_stack_mut().do_action(perform, reverse) { - // TODO: Not match we can do here, maybe except issuing a warning. + // TODO: Not much we can do here, maybe except issuing a warning. self.undo_stack_mut().clear(); } diff --git a/src-tauri/src/sketchbook/bn_utils.rs b/src-tauri/src/sketchbook/bn_utils.rs index 61f9815..332ec94 100644 --- a/src-tauri/src/sketchbook/bn_utils.rs +++ b/src-tauri/src/sketchbook/bn_utils.rs @@ -23,7 +23,7 @@ pub fn sign_to_monotonicity(regulation_sign: &Monotonicity) -> Option Some(Lib_Pbn_Monotonicity::Activation), Monotonicity::Inhibition => Some(Lib_Pbn_Monotonicity::Inhibition), Monotonicity::Unknown => None, - // todo: maybe put "unimplemented" here? + // TODO: maybe put "unimplemented" here? Monotonicity::Dual => None, } } diff --git a/src-tauri/src/sketchbook/observations/_observation.rs b/src-tauri/src/sketchbook/observations/_observation.rs index 416fbab..6262fb8 100644 --- a/src-tauri/src/sketchbook/observations/_observation.rs +++ b/src-tauri/src/sketchbook/observations/_observation.rs @@ -14,7 +14,7 @@ pub struct Observation { /// Creating observations. impl Observation { - /// Create `Observation` object from a vector with values, and string ID (which must be + /// Create `Observation` object from a vector of values, and string ID (which must be /// a valid identifier). /// /// Name is initialized same as ID, and annotation is empty. @@ -61,24 +61,19 @@ impl Observation { /// Create `Observation` object from string encoding of its (ordered) values. /// Values are encoded using characters `1`, `0`, or `*`. /// - /// Observation cannot be empty. Name is initialized same as ID, and annotation is empty. + /// Name is initialized same as ID, and annotation is empty. /// For full initializer with name and annotation, check [Self::try_from_str_annotated]. pub fn try_from_str(observation_str: &str, id: &str) -> Result { let mut observation_vec: Vec = Vec::new(); for c in observation_str.chars() { observation_vec.push(VarValue::from_str(&c.to_string())?) } - if observation_vec.is_empty() { - return Err("Observation can't be empty.".to_string()); - } Self::new(observation_vec, id) } /// Create `Observation` object from string encoding of its (ordered) values. /// Values are encoded using characters `1`, `0`, or `*`. - /// - /// Observation cannot be empty. pub fn try_from_str_annotated( observation_str: &str, id: &str, @@ -336,11 +331,9 @@ mod tests { fn test_err_observation_from_str() { let observation_str1 = "0 1**"; let observation_str2 = "0**a"; - let observation_str3 = ""; assert!(Observation::try_from_str(observation_str1, "obs1").is_err()); assert!(Observation::try_from_str(observation_str2, "obs2").is_err()); - assert!(Observation::try_from_str(observation_str3, "obs3").is_err()); } #[test] diff --git a/src/aeon_state.ts b/src/aeon_state.ts index e1fa1f3..23fa38a 100644 --- a/src/aeon_state.ts +++ b/src/aeon_state.ts @@ -395,7 +395,7 @@ interface AeonState { /** ObservationData for a newly pushed observation (also contains corresponding dataset ID). */ observationPushed: Observable - /** Push a new observation with into a specified dataset. If observation data are not provided, + /** Push a new observation into a specified dataset. If observation data are not provided, * the observation is newly generated on backend (with unspecified values). */ pushObservation: (datasetId: string, observation?: ObservationData) => void /** ObservationData for a popped (removed from the end) observation (also contains corresponding dataset ID). */ diff --git a/src/html/component-editor/analysis-tab/analysis-tab.ts b/src/html/component-editor/analysis-tab/analysis-tab.ts index dad84ae..e9e8862 100644 --- a/src/html/component-editor/analysis-tab/analysis-tab.ts +++ b/src/html/component-editor/analysis-tab/analysis-tab.ts @@ -1,4 +1,4 @@ -import { css, html, LitElement, type TemplateResult, unsafeCSS } from 'lit' +import { css, html, LitElement, unsafeCSS, type TemplateResult, type PropertyValues } from 'lit' import { customElement, property, state } from 'lit/decorators.js' import style_less from './analysis-tab.less?inline' import { ContentData } from '../../util/data-interfaces' @@ -18,6 +18,16 @@ export class AnalysisTab extends LitElement { ) } + protected updated (_changedProperties: PropertyValues): void { + super.updated(_changedProperties) + + // Once some part of the actual sketch is updated, hide the consistency check results + // as they are no longer valid. + if (_changedProperties.has('contentData')) { + this.consistency_results = null + } + } + runInference (): void { aeonState.new_session.createNewAnalysisSession() } @@ -29,7 +39,6 @@ export class AnalysisTab extends LitElement { #onConsistencyResults (results: string): void { this.consistency_results = results console.log('Received consistency check results.') - console.log(results) } closeConsistencyResults (): void { diff --git a/src/html/component-editor/observations-editor/edit-observation/edit-observation.ts b/src/html/component-editor/observations-editor/edit-observation/edit-observation.ts index 4b76f44..663e547 100644 --- a/src/html/component-editor/observations-editor/edit-observation/edit-observation.ts +++ b/src/html/component-editor/observations-editor/edit-observation/edit-observation.ts @@ -46,7 +46,7 @@ export default class EditObservation extends LitElement {
- ${map(Object.keys(this.data ?? {}), (key) => { + ${map(Object.keys(this.data ?? {}).filter(key => key !== 'index'), (key) => { return html`
diff --git a/src/html/component-editor/observations-editor/observations-set/observations-set.ts b/src/html/component-editor/observations-editor/observations-set/observations-set.ts index 70015d9..93aaf10 100644 --- a/src/html/component-editor/observations-editor/observations-set/observations-set.ts +++ b/src/html/component-editor/observations-editor/observations-set/observations-set.ts @@ -6,7 +6,7 @@ import { Tabulator, type ColumnDefinition, type CellComponent } from 'tabulator- import { type IObservation, type IObservationSet } from '../../../util/data-interfaces' import { appWindow, WebviewWindow } from '@tauri-apps/api/window' import { type Event as TauriEvent } from '@tauri-apps/api/helpers/event' -import { checkboxColumn, dataCell, loadTabulatorPlugins, idColumn, nameColumn, tabulatorOptions } from '../tabulator-utility' +import { checkboxColumn, dataCell, loadTabulatorPlugins, idColumn, nameColumn, tabulatorOptions, indexColumn } from '../tabulator-utility' import { icon } from '@fortawesome/fontawesome-svg-core' import { faAdd, faEdit, faTrash } from '@fortawesome/free-solid-svg-icons' @@ -36,10 +36,9 @@ export default class ObservationsSet extends LitElement { // check if variables changed - if so, it means adding/removing columns, which requires whole init() const newData = _changedProperties.get('data') if (newData !== undefined && newData.variables !== undefined && this.data !== undefined && !this.areVariablesEqual(this.data.variables, newData.variables)) { - console.log(newData.variables) - console.log(this.data.variables) await this.init() } else if (this.tabulatorReady) { + this.data.observations = this.data.observations.map((obs, idx) => ({ ...obs, index: idx + 1 })); void this.tabulator?.setData(this.data.observations) } } @@ -54,8 +53,12 @@ export default class ObservationsSet extends LitElement { } private async init (): Promise { + // Add index to each observation based on its original position + this.data.observations = this.data.observations.map((obs, idx) => ({ ...obs, index: idx + 1 })); + const columns: ColumnDefinition[] = [ checkboxColumn, + indexColumn(), nameColumn(false), idColumn(false) ] diff --git a/src/html/component-editor/observations-editor/tabulator-utility.ts b/src/html/component-editor/observations-editor/tabulator-utility.ts index d115d63..ce31d50 100644 --- a/src/html/component-editor/observations-editor/tabulator-utility.ts +++ b/src/html/component-editor/observations-editor/tabulator-utility.ts @@ -59,6 +59,17 @@ export const idColumn = (editable: boolean): ColumnDefinition => { } } +export const indexColumn = (): ColumnDefinition => { + return { + title: 'Index', + field: 'index', + width: 75, + sorter: 'number', + headerFilter: 'input', + editable: false, + } +} + export const tabulatorOptions: Options = { layout: 'fitData', // resizableColumnFit: true, @@ -66,7 +77,7 @@ export const tabulatorOptions: Options = { renderVerticalBuffer: 300, sortMode: 'local', initialSort: [{ - column: 'name', + column: 'index', dir: 'asc' }], headerSort: true, From 4be0113fa3fc5ce3059a6bc8ff70e517c04f7c2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Huvar?= <492849@mail.muni.cz> Date: Tue, 29 Oct 2024 21:09:37 +0100 Subject: [PATCH 2/4] Formatting --- src-tauri/src/algorithms/eval_dynamic/processed_props.rs | 6 +----- .../observations-set/observations-set.ts | 4 ++-- .../observations-editor/tabulator-utility.ts | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src-tauri/src/algorithms/eval_dynamic/processed_props.rs b/src-tauri/src/algorithms/eval_dynamic/processed_props.rs index e517b7a..ed032a5 100644 --- a/src-tauri/src/algorithms/eval_dynamic/processed_props.rs +++ b/src-tauri/src/algorithms/eval_dynamic/processed_props.rs @@ -163,11 +163,7 @@ pub fn process_dynamic_props(sketch: &Sketch) -> Result, S DynPropertyType::ExistsTrajectory(prop) => { let dataset_id = prop.dataset.clone().unwrap(); let dataset = sketch.observations.get_dataset(&dataset_id)?; - let formula = encode_dataset_hctl_str( - dataset, - None, - DataEncodingType::TimeSeries, - )?; + let formula = encode_dataset_hctl_str(dataset, None, DataEncodingType::TimeSeries)?; ProcessedDynProp::mk_hctl(id.as_str(), &formula) } }; diff --git a/src/html/component-editor/observations-editor/observations-set/observations-set.ts b/src/html/component-editor/observations-editor/observations-set/observations-set.ts index 93aaf10..56f0b29 100644 --- a/src/html/component-editor/observations-editor/observations-set/observations-set.ts +++ b/src/html/component-editor/observations-editor/observations-set/observations-set.ts @@ -38,7 +38,7 @@ export default class ObservationsSet extends LitElement { if (newData !== undefined && newData.variables !== undefined && this.data !== undefined && !this.areVariablesEqual(this.data.variables, newData.variables)) { await this.init() } else if (this.tabulatorReady) { - this.data.observations = this.data.observations.map((obs, idx) => ({ ...obs, index: idx + 1 })); + this.data.observations = this.data.observations.map((obs, idx) => ({ ...obs, index: idx + 1 })) void this.tabulator?.setData(this.data.observations) } } @@ -54,7 +54,7 @@ export default class ObservationsSet extends LitElement { private async init (): Promise { // Add index to each observation based on its original position - this.data.observations = this.data.observations.map((obs, idx) => ({ ...obs, index: idx + 1 })); + this.data.observations = this.data.observations.map((obs, idx) => ({ ...obs, index: idx + 1 })) const columns: ColumnDefinition[] = [ checkboxColumn, diff --git a/src/html/component-editor/observations-editor/tabulator-utility.ts b/src/html/component-editor/observations-editor/tabulator-utility.ts index ce31d50..702c8a8 100644 --- a/src/html/component-editor/observations-editor/tabulator-utility.ts +++ b/src/html/component-editor/observations-editor/tabulator-utility.ts @@ -66,7 +66,7 @@ export const indexColumn = (): ColumnDefinition => { width: 75, sorter: 'number', headerFilter: 'input', - editable: false, + editable: false } } From 7975f36be3000b93ede2ee519ba31a2ad22d56f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Huvar?= <492849@mail.muni.cz> Date: Tue, 29 Oct 2024 21:30:18 +0100 Subject: [PATCH 3/4] Add test case for trajectory properties. --- data/test_data/data_time_series.csv | 5 + data/test_data/test_model_with_data.json | 159 +++++++++++------- .../analysis/_test_inference/_test_dynamic.rs | 14 ++ 3 files changed, 119 insertions(+), 59 deletions(-) create mode 100644 data/test_data/data_time_series.csv diff --git a/data/test_data/data_time_series.csv b/data/test_data/data_time_series.csv new file mode 100644 index 0000000..aa120b3 --- /dev/null +++ b/data/test_data/data_time_series.csv @@ -0,0 +1,5 @@ +ID,A,B,C,D +a,1,0,0,0 +b,1,1,0,0 +c,1,1,1,0 +d,1,1,1,1 \ No newline at end of file diff --git a/data/test_data/test_model_with_data.json b/data/test_data/test_model_with_data.json index 4214cb8..32a4930 100644 --- a/data/test_data/test_model_with_data.json +++ b/data/test_data/test_model_with_data.json @@ -1,5 +1,4 @@ { - "annotation": "", "model": { "variables": [ { @@ -124,9 +123,9 @@ "nodes": [ { "layout": "default", - "variable": "D", - "px": 642.49677, - "py": 185.15988 + "variable": "B", + "px": 0.0, + "py": 0.0 }, { "layout": "default", @@ -136,15 +135,15 @@ }, { "layout": "default", - "variable": "A", - "px": 346.89832, - "py": 183.03789 + "variable": "D", + "px": 642.49677, + "py": 185.15988 }, { "layout": "default", - "variable": "B", - "px": 0.0, - "py": 0.0 + "variable": "A", + "px": 346.89832, + "py": 183.03789 } ] } @@ -152,23 +151,37 @@ }, "datasets": [ { - "id": "data_mts", - "name": "data_mts", - "annotation":"", + "name": "data_time_series", + "id": "data_time_series", + "annotation": "", "observations": [ { - "id": "abc", - "name": "abc", - "annotation":"", - "dataset": "data_mts", - "values": "111*" + "id": "a", + "name": "a", + "annotation": "", + "dataset": "data_time_series", + "values": "1000" }, { - "id": "ab", - "name": "ab", - "annotation":"", - "dataset": "data_mts", - "values": "11**" + "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": [ @@ -179,21 +192,21 @@ ] }, { - "id": "data_fp", "name": "data_fp", - "annotation":"", + "id": "data_fp", + "annotation": "", "observations": [ { "id": "ones", "name": "ones", - "annotation":"", + "annotation": "", "dataset": "data_fp", "values": "1111" }, { "id": "zeros", "name": "zeros", - "annotation":"", + "annotation": "", "dataset": "data_fp", "values": "0000" } @@ -204,58 +217,75 @@ "C", "D" ] + }, + { + "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" + ] } ], "dyn_properties": [], "stat_properties": [ { - "id": "essentiality_D_B", + "id": "essentiality_A_B", "name": "Regulation essentiality property", "annotation": "", "variant": "RegulationEssential", - "input": "D", + "input": "A", "target": "B", "value": "True", "context": null }, { - "id": "essentiality_A_B", + "id": "essentiality_D_D", "name": "Regulation essentiality property", "annotation": "", "variant": "RegulationEssential", - "input": "A", - "target": "B", + "input": "D", + "target": "D", "value": "True", "context": null }, { - "id": "monotonicity_A_B", - "name": "Regulation monotonicity property", - "annotation": "", - "variant": "RegulationMonotonic", - "input": "A", - "target": "B", - "value": "Activation", - "context": null - }, - { - "id": "essentiality_D_D", + "id": "essentiality_D_B", "name": "Regulation essentiality property", "annotation": "", "variant": "RegulationEssential", "input": "D", - "target": "D", + "target": "B", "value": "True", "context": null }, { - "id": "essentiality_B_C", - "name": "Regulation essentiality property", + "id": "monotonicity_B_C", + "name": "Regulation monotonicity property", "annotation": "", - "variant": "RegulationEssential", + "variant": "RegulationMonotonic", "input": "B", "target": "C", - "value": "True", + "value": "Activation", "context": null }, { @@ -269,13 +299,13 @@ "context": null }, { - "id": "monotonicity_B_C", - "name": "Regulation monotonicity property", + "id": "essentiality_B_C", + "name": "Regulation essentiality property", "annotation": "", - "variant": "RegulationMonotonic", + "variant": "RegulationEssential", "input": "B", "target": "C", - "value": "Activation", + "value": "True", "context": null }, { @@ -288,6 +318,16 @@ "value": "Inhibition", "context": null }, + { + "id": "monotonicity_A_B", + "name": "Regulation monotonicity property", + "annotation": "", + "variant": "RegulationMonotonic", + "input": "A", + "target": "B", + "value": "Activation", + "context": null + }, { "id": "essentiality_A_C", "name": "Regulation essentiality property", @@ -299,24 +339,25 @@ "context": null }, { - "id": "monotonicity_A_C", + "id": "monotonicity_D_B", "name": "Regulation monotonicity property", "annotation": "", "variant": "RegulationMonotonic", - "input": "A", - "target": "C", + "input": "D", + "target": "B", "value": "Activation", "context": null }, { - "id": "monotonicity_D_B", + "id": "monotonicity_A_C", "name": "Regulation monotonicity property", "annotation": "", "variant": "RegulationMonotonic", - "input": "D", - "target": "B", + "input": "A", + "target": "C", "value": "Activation", "context": null } - ] + ], + "annotation": "" } \ No newline at end of file diff --git a/src-tauri/src/analysis/_test_inference/_test_dynamic.rs b/src-tauri/src/analysis/_test_inference/_test_dynamic.rs index 73f5365..388986b 100644 --- a/src-tauri/src/analysis/_test_inference/_test_dynamic.rs +++ b/src-tauri/src/analysis/_test_inference/_test_dynamic.rs @@ -116,3 +116,17 @@ fn inference_template_trap_space() { let property = DynProperty::mk_trap_space(id, Some(data_id), Some(obs_id), true, true, ""); assert_eq!(add_dyn_prop_and_infer(sketch, property, id), 2); } + +#[test] +/// Test inference using the test model with added trajectory template properties. +fn inference_template_time_series() { + // There is a trajectory 1000 -> 1100 -> 1110 -> 1111 + let sketch = load_test_model(); + let id = "time_serie"; + let data_id = sketch + .observations + .get_dataset_id("data_time_series") + .unwrap(); + let property = DynProperty::mk_trajectory(id, Some(data_id), ""); + assert_eq!(add_dyn_prop_and_infer(sketch, property, id), 16); +} From 6a3fa404dc2bdbb92fc1b1024db53d5c0ae1e627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Huvar?= <492849@mail.muni.cz> Date: Wed, 30 Oct 2024 21:23:40 +0100 Subject: [PATCH 4/4] Allow editing dataset variables. --- .../observations/_dataset/_impl_dataset.rs | 25 +++++++++ .../observations/_manager/_impl_manager.rs | 29 ++++++++++ .../_manager/_impl_session_state.rs | 5 +- .../edit-dataset/edit-dataset.less | 3 ++ .../edit-dataset/edit-dataset.ts | 45 ++++++++++++---- .../observations-editor.ts | 53 +++++++++++++++++-- .../observations-set/observations-set.ts | 2 +- .../properties-editor/properties-editor.ts | 3 -- .../static/static-selectors-property.ts | 1 - 9 files changed, 146 insertions(+), 20 deletions(-) diff --git a/src-tauri/src/sketchbook/observations/_dataset/_impl_dataset.rs b/src-tauri/src/sketchbook/observations/_dataset/_impl_dataset.rs index caa5af4..d6c8895 100644 --- a/src-tauri/src/sketchbook/observations/_dataset/_impl_dataset.rs +++ b/src-tauri/src/sketchbook/observations/_dataset/_impl_dataset.rs @@ -230,6 +230,31 @@ impl Dataset { self.set_var_id(&original_id, new_id) } + /// Set the list of all variable IDs (essentially renaming some/all of them). + /// The length of the new list must be the same as existing one (only renaming, not adding/removing variables). + pub fn set_all_variables(&mut self, new_variables_list: Vec) -> Result<(), String> { + if new_variables_list.len() != self.num_variables() { + return Err("Vectors of old and new variables differ in length.".to_string()); + } + assert_ids_unique(&new_variables_list)?; + + self.variables = new_variables_list; + Ok(()) + } + + /// Set the list with all variable IDs (essentially renaming some/all of them) using string names. + /// The length of the new list must be the same as existing one (only renaming, not adding/removing variables). + pub fn set_all_variables_by_str( + &mut self, + new_variables_list: Vec<&str>, + ) -> Result<(), String> { + if new_variables_list.len() != self.num_variables() { + return Err("Vectors of old and new variables differ in length.".to_string()); + } + let variables = Self::try_convert_vars(&new_variables_list)?; + self.set_all_variables(variables) + } + /// Set the id of an observation with `original_id` to `new_id`. pub fn set_obs_id( &mut self, diff --git a/src-tauri/src/sketchbook/observations/_manager/_impl_manager.rs b/src-tauri/src/sketchbook/observations/_manager/_impl_manager.rs index 4d75997..31b7696 100644 --- a/src-tauri/src/sketchbook/observations/_manager/_impl_manager.rs +++ b/src-tauri/src/sketchbook/observations/_manager/_impl_manager.rs @@ -169,6 +169,35 @@ impl ObservationManager { self.set_var_id(&dataset_id, &original_id, new_id) } + /// Set the list of all variable IDs (in a given dataset), essentially renaming some/all of them. + /// The length of the new list must be the same as existing one (only renaming, not adding/removing variables). + pub fn set_all_variables( + &mut self, + dataset_id: &DatasetId, + new_variables_list: Vec, + ) -> Result<(), String> { + self.assert_valid_dataset(dataset_id)?; + self.datasets + .get_mut(dataset_id) + .unwrap() + .set_all_variables(new_variables_list) + } + + /// Set the list of all variable IDs (in a given dataset), essentially renaming some/all of them. + /// The length of the new list must be the same as existing one (only renaming, not adding/removing variables). + pub fn set_all_variables_by_str( + &mut self, + dataset_id: &str, + new_variables_list: Vec<&str>, + ) -> Result<(), String> { + let dataset_id = DatasetId::new(dataset_id)?; + self.assert_valid_dataset(&dataset_id)?; + self.datasets + .get_mut(&dataset_id) + .unwrap() + .set_all_variables_by_str(new_variables_list) + } + /// Remove variable and all the values corresponding to it from a dataset (decrementing /// dimension of the dataset in process). pub fn remove_var(&mut self, dataset_id: &DatasetId, var_id: &VarId) -> Result<(), String> { diff --git a/src-tauri/src/sketchbook/observations/_manager/_impl_session_state.rs b/src-tauri/src/sketchbook/observations/_manager/_impl_session_state.rs index ab80cab..3caee2f 100644 --- a/src-tauri/src/sketchbook/observations/_manager/_impl_session_state.rs +++ b/src-tauri/src/sketchbook/observations/_manager/_impl_session_state.rs @@ -215,12 +215,13 @@ impl ObservationManager { let reverse_event = mk_obs_event(&reverse_at_path, Some(&payload)); Ok(make_reversible(state_change, event, reverse_event)) } else if Self::starts_with("set_metadata", at_path).is_some() { - // get the payload - json string encoding metadata with (potentially) a new name or annotation + // get the payload - json string encoding metadata with (potentially) new name/annotation/variables let payload = Self::clone_payload_str(event, component_name)?; let new_metadata = DatasetMetaData::from_json_str(&payload)?; let orig_dataset = self.get_dataset(&dataset_id)?; if orig_dataset.get_name() == new_metadata.name && orig_dataset.get_annotation() == new_metadata.annotation + && orig_dataset.variable_names() == new_metadata.variables { return Ok(Consumed::NoChange); } @@ -229,6 +230,8 @@ impl ObservationManager { let orig_metadata = DatasetMetaData::from_dataset(&dataset_id, orig_dataset); self.set_dataset_name(&dataset_id, &new_metadata.name)?; self.set_dataset_annot(&dataset_id, &new_metadata.annotation)?; + let variables = new_metadata.variables.iter().map(|v| v.as_str()).collect(); + self.set_all_variables_by_str(dataset_id.as_str(), variables)?; let state_change = mk_obs_state_change(&["set_metadata"], &new_metadata); // prepare the reverse event (setting the original ID back) diff --git a/src/html/component-editor/observations-editor/edit-dataset/edit-dataset.less b/src/html/component-editor/observations-editor/edit-dataset/edit-dataset.less index d9dbed3..1ceac74 100644 --- a/src/html/component-editor/observations-editor/edit-dataset/edit-dataset.less +++ b/src/html/component-editor/observations-editor/edit-dataset/edit-dataset.less @@ -4,4 +4,7 @@ .uk-form-label { color: @text-dark; } + h4 { + color: white; + } } diff --git a/src/html/component-editor/observations-editor/edit-dataset/edit-dataset.ts b/src/html/component-editor/observations-editor/edit-dataset/edit-dataset.ts index ca482c1..f4e671c 100644 --- a/src/html/component-editor/observations-editor/edit-dataset/edit-dataset.ts +++ b/src/html/component-editor/observations-editor/edit-dataset/edit-dataset.ts @@ -11,12 +11,14 @@ export default class EditDataset extends LitElement { static styles = css`${unsafeCSS(style_less)}` @query('#dataset-id') idField: HTMLInputElement | undefined @state() data: IObservationSet | undefined + @state() origData: IObservationSet | undefined id = '' async firstUpdated (): Promise { await once('edit_dataset_update', (event: TauriEvent) => { this.id = event.payload.id this.data = event.payload + this.origData = event.payload }) await emit('loaded', {}) this.idField?.focus() @@ -46,17 +48,40 @@ export default class EditDataset extends LitElement {
- - ${map(this.data !== undefined ? ['id', 'name', 'annotation'] : [], (key) => { + + ${map(this.data !== undefined ? ['id', 'name', 'annotation'] : [], (key) => { return html` -
- -
- ${key === 'annotation' - ? html`` - : html``} -
-
` +
+ +
+ ${key === 'annotation' + ? html`` + : html``} +
+
` + })} + + +

Variables

+ ${map(this.origData?.variables ?? [], (origVariable, index) => { + const currentVariable = this.data?.variables?.[index] ?? origVariable + return html` +
+ +
+ +
+
` })}
diff --git a/src/html/component-editor/observations-editor/observations-editor.ts b/src/html/component-editor/observations-editor/observations-editor.ts index 8742aed..3009a07 100644 --- a/src/html/component-editor/observations-editor/observations-editor.ts +++ b/src/html/component-editor/observations-editor/observations-editor.ts @@ -190,10 +190,46 @@ export default class ObservationsEditor extends LitElement { if (datasetIndex === -1) return const datasets = structuredClone(this.contentData.observations) - datasets[datasetIndex] = { - ...datasets[datasetIndex], - name: data.name, - annotation: data.annotation + if (!this.areVariablesEqual(data.variables, datasets[datasetIndex].variables)) { + // if variable names changed, we need to update the corresponding fields in all observations + const oldVariables = datasets[datasetIndex].variables + const newVariables = data.variables + + const observations = datasets[datasetIndex].observations.map((obs) => { + // Create a new observation object with updated keys + const updatedObservation: IObservation = { + selected: obs.selected, + name: obs.name, + annotation: obs.annotation, + id: obs.id + } + + oldVariables.forEach((oldVar, index) => { + const newVar = newVariables[index] + // If the variable name has changed, use the new name; otherwise, keep the original + if (oldVar !== newVar) { + updatedObservation[newVar] = obs[oldVar] + } else { + updatedObservation[oldVar] = obs[oldVar] + } + }) + return updatedObservation + }) + + datasets[datasetIndex] = { + ...datasets[datasetIndex], + name: data.name, + annotation: data.annotation, + variables: data.variables, + observations + } + } else { + // otherwise only change the name and annotation + datasets[datasetIndex] = { + ...datasets[datasetIndex], + name: data.name, + annotation: data.annotation + } } this.updateObservations(datasets) } @@ -359,6 +395,15 @@ export default class ObservationsEditor extends LitElement { this.updateObservations(datasets) } + // Helper method to compare the variable name arrays + private areVariablesEqual (prev: string[] | undefined, current: string[]): boolean { + if (prev === undefined || prev.length !== current.length) { + return false + } + // Compare each element + return prev.every((value, index) => value === current[index]) + } + toggleDataset (index: number): void { const dsIndex = this.shownDatasets.indexOf(index) if (dsIndex === -1) { diff --git a/src/html/component-editor/observations-editor/observations-set/observations-set.ts b/src/html/component-editor/observations-editor/observations-set/observations-set.ts index 56f0b29..cffe62a 100644 --- a/src/html/component-editor/observations-editor/observations-set/observations-set.ts +++ b/src/html/component-editor/observations-editor/observations-set/observations-set.ts @@ -31,10 +31,10 @@ export default class ObservationsSet extends LitElement { } protected async updated (_changedProperties: PropertyValues): Promise { + const newData = _changedProperties.get('data') super.updated(_changedProperties) // check if variables changed - if so, it means adding/removing columns, which requires whole init() - const newData = _changedProperties.get('data') if (newData !== undefined && newData.variables !== undefined && this.data !== undefined && !this.areVariablesEqual(this.data.variables, newData.variables)) { await this.init() } else if (this.tabulatorReady) { diff --git a/src/html/component-editor/properties-editor/properties-editor.ts b/src/html/component-editor/properties-editor/properties-editor.ts index 27d0e8c..59e5612 100644 --- a/src/html/component-editor/properties-editor/properties-editor.ts +++ b/src/html/component-editor/properties-editor/properties-editor.ts @@ -194,7 +194,6 @@ export default class PropertiesEditor extends LitElement { private async editDynProperty (event: Event): Promise { const detail = (event as CustomEvent).detail - console.log(detail) const propertyIndex = this.contentData.dynamicProperties.findIndex(p => p.id === detail.id) if (propertyIndex === -1) return const property = this.contentData.dynamicProperties[propertyIndex] @@ -342,7 +341,6 @@ export default class PropertiesEditor extends LitElement { } #onDynamicIdChanged (data: DynPropIdUpdateData): void { - console.log(data) const index = this.contentData.dynamicProperties.findIndex(d => d.id === data.original_id) if (index === -1) return const properties = [...this.contentData.dynamicProperties] @@ -359,7 +357,6 @@ export default class PropertiesEditor extends LitElement { } #onStaticIdChanged (data: StatPropIdUpdateData): void { - console.log(data) const index = this.contentData.staticProperties.findIndex(d => d.id === data.original_id) if (index === -1) return const properties = [...this.contentData.staticProperties] diff --git a/src/html/component-editor/properties-editor/static/static-selectors-property.ts b/src/html/component-editor/properties-editor/static/static-selectors-property.ts index 1661057..eba0e0c 100644 --- a/src/html/component-editor/properties-editor/static/static-selectors-property.ts +++ b/src/html/component-editor/properties-editor/static/static-selectors-property.ts @@ -36,7 +36,6 @@ export default class StaticSelectorsProperty extends AbstractStaticProperty { targetChanged (event: Event): void { let value: string | null = (event.target as HTMLSelectElement).value value = value === '' ? null : value - console.log(value) if (this.isFunctionInput()) { this.updateProperty({ ...this.property,