diff --git a/src/app.rs b/src/gui.rs similarity index 96% rename from src/app.rs rename to src/gui.rs index d39902b..5b44e39 100644 --- a/src/app.rs +++ b/src/gui.rs @@ -1,3 +1,10 @@ +mod egui_app; +mod plot; +pub mod tooltips; +pub const PARAMETER_MIN: f64 = -50.0; +pub const PARAMETER_MAX: f64 = 50.0; +pub use self::egui_app::*; + /// We derive Deserialize/Serialize so we can persist app state on shutdown. #[derive(serde::Deserialize, serde::Serialize)] // #[serde(default)] // if we add new fields, give them default values when deserializing old state diff --git a/src/gui/egui_app.rs b/src/gui/egui_app.rs new file mode 100644 index 0000000..bded233 --- /dev/null +++ b/src/gui/egui_app.rs @@ -0,0 +1,288 @@ +mod conf_panels; +mod egui_utils; +mod main_panels; +use self::conf_panels::*; +pub use self::egui_utils::*; +use self::main_panels::*; +use crate::gui::tooltips::*; +use crate::chaos::{benchmark::ChaosInitSchema, *}; +use anyhow::{bail, Error}; +use egui::{style::Interaction, Align2, Context, CursorIcon, FontFamily, FontId, TextStyle, Ui}; +#[derive(Default)] +pub struct ChaosApp { + open_conf_panel: ConfPanel, + initial_panel: InitialPanel, + execute_panel: ExecutionPanel, + plot_panel: PlotPanel, + benchmark_panel: BenchmarkPanel, + open_main_panel: MainPanel, + chaos_controller: ChaosExecutionController, + init_chaotic_function: bool, + executes: bool, +} + +impl ChaosApp { + pub fn new(cc: &eframe::CreationContext<'_>) -> Self { + let ctx = &cc.egui_ctx; + ctx.style_mut(|style| { + style.text_styles = [ + ( + TextStyle::Heading, + FontId::new(18.0, FontFamily::Proportional), + ), + (TextStyle::Body, FontId::new(12.0, FontFamily::Proportional)), + ( + TextStyle::Monospace, + FontId::new(12.0, FontFamily::Monospace), + ), + (TextStyle::Button, FontId::new(14.0, FontFamily::Monospace)), + (TextStyle::Small, FontId::new(8.0, FontFamily::Monospace)), + ] + .into(); + style.interaction = Interaction { + resize_grab_radius_side: 6.0, + resize_grab_radius_corner: 12.0, + show_tooltips_only_when_still: true, + tooltip_delay: 1.0, + // selectable_labels: false, + // multi_widget_text_select: false + }; + style.visuals.interact_cursor = Some(CursorIcon::PointingHand); + }); // Disable feathering as it causes artifacts with Plotters backend + ctx.tessellation_options_mut(|tess_options| { + tess_options.feathering = false; + }); + Self::default() + } + + fn add_point_series(&mut self) { + if let Ok(data) = self.chaos_controller.get_chaos_data() { + self.plot_panel.add_point_series(data) + } + } + + fn chaos_data_loop(&mut self) -> Result<(), Error> { + if self.plot_panel.generate_new_data { + self.plot_panel.generate_new_data = false; + self.generate_initial_chaos_data()?; + } else if self.init_chaotic_function { + self.init_chaotic_function = false; + self.initialize_chaotic_functions()?; + } else if self.executes && self.plot_panel.check_frame_rate() { + self.execute_chaotic_function()?; + }; + Ok(()) + } + + fn benchmark_loop(&mut self) -> Result<(), Error> { + if self.benchmark_panel.benchmark_toggle() { + let mut chaos_init = ChaosInitSchema { + num_samples: self.initial_panel.number_of_samples(), + num_executions: self.execute_panel.num_executions(), + init_distr: self.initial_panel.initial_distributions(), + ..Default::default() + }; + match self.execute_panel.chosen_chaotic_function() { + SelectedChaoticFunction::SingleDiscreteMap(map_vec) => { + chaos_init.discrete_map_vec = Some(map_vec); + } + SelectedChaoticFunction::SingleDifferentialSystem(diff_system_vec) => { + chaos_init.diff_system_vec = Some(diff_system_vec); + } + SelectedChaoticFunction::ParametrizedDiscreteMaps(map_vec, par, par_values) => { + chaos_init.discrete_map_vec = Some(map_vec); + chaos_init.pars = (par, par_values); + } + SelectedChaoticFunction::ParametrizedDifferentialSystems( + diff_system_vec, + par, + par_values, + ) => { + chaos_init.diff_system_vec = Some(diff_system_vec); + chaos_init.pars = (par, par_values); + } + SelectedChaoticFunction::Nothing => { + bail!("Cannot init chaotic function as it is not set in the execute panel!") + } + }; + + self.benchmark_panel.chaos_benchmark(chaos_init); + }; + Ok(()) + } + + fn generate_initial_chaos_data(&mut self) -> Result<(), Error> { + let init_distr = self.initial_panel.initial_distributions(); + self.executes = self + .execute_panel + .check_compatible_chaotic_function(&init_distr.dimensionality()) + && self.executes; + let chaos_data_gen_result = self + .chaos_controller + .generate_initial_chaos_data(self.initial_panel.number_of_samples(), init_distr); + self.plot_panel.reset_plot_trajectory(); + self.add_point_series(); + chaos_data_gen_result + } + fn initialize_chaotic_functions(&mut self) -> Result<(), Error> { + match self.execute_panel.chosen_chaotic_function() { + SelectedChaoticFunction::SingleDiscreteMap(map_vec) => { + self.chaos_controller.set_discrete_mappers(map_vec)?; + self.plot_panel.set_no_parametrized_plotting(); + } + SelectedChaoticFunction::SingleDifferentialSystem(diff_system_vec) => { + self.chaos_controller + .set_differential_solvers(diff_system_vec)?; + self.plot_panel.set_no_parametrized_plotting(); + } + SelectedChaoticFunction::ParametrizedDiscreteMaps(map_vec, par, par_values) => { + self.chaos_controller.set_discrete_mappers(map_vec)?; + self.plot_panel.set_parametrized_plotting(par, par_values); + } + SelectedChaoticFunction::ParametrizedDifferentialSystems( + diff_system_vec, + par, + par_values, + ) => { + self.chaos_controller + .set_differential_solvers(diff_system_vec)?; + self.plot_panel.set_parametrized_plotting(par, par_values); + } + SelectedChaoticFunction::Nothing => { + bail!("Cannot init chaotic function as it is not set in the execute panel!") + } + }; + Ok(()) + } + fn execute_chaotic_function(&mut self) -> Result<(), Error> { + self.chaos_controller + .execute(self.execute_panel.num_executions())?; + if self.plot_panel.reinit_data() { + self.chaos_controller.reinit_states()?; + } + self.add_point_series(); + Ok(()) + } + + fn add_general_conf(&mut self, ui: &mut Ui) { + group_horizontal(ui, |ui| { + #[cfg(not(target_arch = "wasm32"))] + { + if clickable_button("Quit", false, true, ui, "Close the application.") { + ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close); + } + } + egui::widgets::global_dark_light_mode_buttons(ui); + // egui::reset_button(ui, self); // TODO: derive PartialEq for ChaosApp + }); + group_horizontal(ui, |ui| { + combo_box( + LABEL_MAIN_MODE, + &mut self.open_main_panel, + ui, + TIP_MAIN_MODE, + ); + add_checkbox(LABEL_RUN, &mut self.executes, ui, TIP_RUN); + }); + ui.vertical(|ui| { + match self.open_main_panel { + MainPanel::ChaoticPlot => { + self.plot_panel.conf_ui(ui); + } + MainPanel::Benchmark => { + self.benchmark_panel + .conf_ui(self.execute_panel.chaotic_function_is_chosen(), ui); + } + }; + }); + } + + fn add_chaos_conf(&mut self, ui: &mut Ui) { + ui.horizontal(|ui| { + select_group(&mut self.open_conf_panel, ui, TIP_INIT_PANEL); + }); + if self.open_main_panel == MainPanel::ChaoticPlot { + group_horizontal(ui, |ui| { + let init_data_button_selected = self.chaos_controller.dimensionality() + == self.initial_panel.dimensionality() + || self.plot_panel.generate_new_data; + if clickable_button( + LABEL_INIT_DATA, + init_data_button_selected, + true, + ui, + TIP_INIT_DATA, + ) { + self.plot_panel.generate_new_data = true; + } + let init_fct_button_selected = + self.execute_panel.selected_function_was_set || self.init_chaotic_function; + let init_fct_button_enabled = self.execute_panel.chaotic_function_is_chosen(); + if clickable_button( + LABEL_INIT_FUNCTION, + init_fct_button_selected, + init_fct_button_enabled, + ui, + TIP_INIT_FUNCTION, + ) { + self.init_chaotic_function = true; + self.executes = true; + }; + }); + } + match self.open_conf_panel { + ConfPanel::Initial => { + self.initial_panel.ui(ui); + } + ConfPanel::Execution => { + let (dims, num_exec_limit) = match self.open_main_panel { + MainPanel::ChaoticPlot => (self.chaos_controller.dimensionality(), 100), + MainPanel::Benchmark => (self.initial_panel.dimensionality(), 10_000), + }; + self.execute_panel.ui(ui, dims, num_exec_limit); + } + }; + } + + fn main_loop_and_ui(&mut self, mouse_over_main_panel: bool, ui: &mut Ui) { + match self.open_main_panel { + MainPanel::ChaoticPlot => { + let _ = self.chaos_data_loop(); + self.plot_panel.ui(mouse_over_main_panel, ui); + } + MainPanel::Benchmark => { + let _ = self.benchmark_loop(); + self.benchmark_panel.ui(ui); + } + } + } +} + +impl eframe::App for ChaosApp { + fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) { + let mut mouse_over_main_panel = true; + conf_window("Configuration", ctx, Align2::LEFT_TOP).show(ctx, |ui| { + let response = ui + .vertical(|ui| { + self.add_general_conf(ui); + }) + .response; + if response.hovered() { + mouse_over_main_panel = false; + } + }); + conf_window("Chaos Creation", ctx, Align2::RIGHT_TOP).show(ctx, |ui| { + let response = ui + .vertical(|ui| { + self.add_chaos_conf(ui); + }) + .response; + if response.hovered() { + mouse_over_main_panel = false; + } + }); + egui::CentralPanel::default().show(ctx, |ui| { + self.main_loop_and_ui(mouse_over_main_panel, ui); + }); + } +} diff --git a/src/gui/egui_app/conf_panels.rs b/src/gui/egui_app/conf_panels.rs new file mode 100644 index 0000000..1da3d1e --- /dev/null +++ b/src/gui/egui_app/conf_panels.rs @@ -0,0 +1,24 @@ +mod execute; +mod execute_chaotic_function_view; +mod initial; +mod initial_distribution_view; +use strum_macros::EnumIter; + +pub use self::execute::*; +pub use self::initial::InitialPanel; + +#[derive(PartialEq, Eq, Default, Clone, Copy, EnumIter)] +pub enum ConfPanel { + #[default] + Initial, + Execution, +} + +impl From for &'static str { + fn from(val: ConfPanel) -> Self { + match val { + ConfPanel::Initial => "Initial Distribution", + ConfPanel::Execution => "Chaotic Functions", + } + } +} diff --git a/src/gui/egui_app/conf_panels/execute.rs b/src/gui/egui_app/conf_panels/execute.rs new file mode 100644 index 0000000..654d934 --- /dev/null +++ b/src/gui/egui_app/conf_panels/execute.rs @@ -0,0 +1,242 @@ +use crate::{ + gui::{add_hyperlink, integer_slider, tooltips::*}, + chaos::data::{DistributionDimensions, FractalDimensions}, + chaos::{DiscreteMapVec, OdeSystemSolverVec}, +}; + +use super::execute_chaotic_function_view::{ + ChaosFunctionViewData, DifferentialSystemView, DiscreteMapView, +}; +use egui::{ScrollArea, Ui}; +use strum::IntoEnumIterator; +#[derive(PartialEq)] +pub struct ExecutionPanel { + num_executions: usize, + chaotic_discrete_map: Option, + chaotic_diff_system: Option, + view_data: ChaosFunctionViewData, + pub selected_function_was_set: bool, +} + +impl Default for ExecutionPanel { + fn default() -> Self { + Self { + num_executions: 1, + chaotic_discrete_map: None, + chaotic_diff_system: None, + view_data: Default::default(), + selected_function_was_set: false, + } + } +} +pub enum SelectedChaoticFunction { + Nothing, + SingleDiscreteMap(DiscreteMapVec), + ParametrizedDiscreteMaps(DiscreteMapVec, &'static str, Vec), + SingleDifferentialSystem(OdeSystemSolverVec), + ParametrizedDifferentialSystems(OdeSystemSolverVec, &'static str, Vec), +} +impl ExecutionPanel { + pub fn chaotic_function_is_chosen(&self) -> bool { + self.chaotic_discrete_map.is_some() || self.chaotic_diff_system.is_some() + } + + pub fn check_compatible_chaotic_function(&mut self, dims: &DistributionDimensions) -> bool { + self.check_compatible_discrete_map(dims) | self.check_compatible_diff_system(dims) + } + + fn check_compatible_discrete_map(&mut self, dims: &DistributionDimensions) -> bool { + if let Some(map) = &self.chaotic_discrete_map { + let is_compatible = map.dimensionality() == *dims; + if is_compatible { + return true; + } else { + self.chaotic_discrete_map = None; + } + } + false // None or not compatible + } + + fn check_compatible_diff_system(&mut self, dims: &DistributionDimensions) -> bool { + if let Some(system) = &self.chaotic_diff_system { + let is_compatible = system.dimensionality() == *dims; + if is_compatible { + return true; + } else { + self.chaotic_diff_system = None; + } + } + false // None or not compatible + } + + pub fn num_executions(&self) -> usize { + self.num_executions + } + + pub fn chosen_chaotic_function(&mut self) -> SelectedChaoticFunction { + self.selected_function_was_set = true; + if let Some(view) = self.chaotic_discrete_map.as_ref() { + return self.view_data.map_discrete_view_to_maps_vec_variant(view); + }; + if let Some(view) = self.chaotic_diff_system.as_ref() { + return self + .view_data + .map_continuous_view_to_solver_vec_variant(view); + } + SelectedChaoticFunction::Nothing + } + + fn discrete_map_listing(&mut self, ui: &mut Ui, dims: &DistributionDimensions) { + ui.vertical(|ui| { + ui.heading("Discrete Map"); + ui.group(|ui| { + DiscreteMapView::iter().for_each(|view| { + if *dims == view.dimensionality() { + self.discrete_map_selection(ui, view); + } + }); + }); + }); + } + fn discrete_map_selection(&mut self, ui: &mut Ui, view: DiscreteMapView) { + let view_name: &'static str = view.into(); + let on_hover_description = |ui: &mut Ui| { + let (description, ref_link) = self.view_data.discrete_description(&view); + ui.vertical(|ui| { + ui.label(description); + ui.horizontal(|ui| { + ui.label("Reference: "); + ui.hyperlink(ref_link); + }); + }); + }; + if ui + .selectable_value(&mut self.chaotic_discrete_map, Some(view), view_name) + .on_hover_ui(on_hover_description) + .changed() + { + self.chaotic_diff_system = None; + self.selected_function_was_set = false; + } + } + fn fractal_ui(&mut self, ui: &mut Ui, dims: &DistributionDimensions) { + let all_fractals_with_dims: Vec = DiscreteMapView::iter() + .filter(|view| *dims == view.dimensionality()) + .collect(); + ui.vertical(|ui| { + ui.heading("Mandelbrot"); + ui.group(|ui| { + all_fractals_with_dims.iter().for_each(|view| { + if view.is_mandelbrot() { + self.discrete_map_selection(ui, *view); + } + }); + }); + }); + ui.separator(); + ui.vertical(|ui| { + ui.heading("Julia"); + ui.group(|ui| { + all_fractals_with_dims.iter().for_each(|view| { + if view.is_julia() { + self.discrete_map_selection(ui, *view); + } + }); + }); + }); + } + fn diff_system_ui(&mut self, ui: &mut Ui, dims: &DistributionDimensions) { + ui.vertical(|ui| { + ui.heading("Differential System"); + ui.group(|ui| { + DifferentialSystemView::iter().for_each(|view| { + let view_name: &'static str = view.into(); + let on_hover_description = |ui: &mut Ui| { + let (description, ref_link) = self.view_data.continuous_description(&view); + ui.vertical(|ui| { + ui.label(description); + ui.horizontal(|ui| { + ui.label("Reference: "); + ui.hyperlink(ref_link); + }); + }); + }; + if *dims == view.dimensionality() + && ui + .selectable_value(&mut self.chaotic_diff_system, Some(view), view_name) + .on_hover_ui(on_hover_description) + .changed() + { + self.chaotic_discrete_map = None; + self.selected_function_was_set = false; + } + }) + }); + }); + } + + fn particle_ui(&mut self, _ui: &mut Ui, num_dims: usize) { + // since there is only one version implemented for 2 and 3 each we select it + if !self.chaotic_function_is_chosen() { + self.chaotic_diff_system = Some(if num_dims == 3 { + DifferentialSystemView::ParticleXYZ + } else { + DifferentialSystemView::ParticleXY + }); + } + } + + pub fn ui(&mut self, ui: &mut Ui, dims: DistributionDimensions, num_execution_limit: usize) { + self.check_compatible_chaotic_function(&dims); + ui.vertical(|ui| { + ScrollArea::vertical().show(ui, |ui| { + match dims { + DistributionDimensions::State(_) => { + ui.horizontal(|ui| { + self.discrete_map_listing(ui, &dims); + ui.separator(); + self.diff_system_ui(ui, &dims); + }); + } + DistributionDimensions::Particle(n) => { + ui.heading(format!("{n}D Particles")); + ui.separator(); + self.particle_ui(ui, n); + } + DistributionDimensions::Fractal(ref fractal_ring) => { + let ring_type: &'static str = fractal_ring.into(); + ui.horizontal(|ui| { + ui.heading(format!("{ring_type} Fractals ")); + let (reference, tooltip) = match fractal_ring { + FractalDimensions::Complex => (LINK_COMPLEX, TIP_COMPLEX), + FractalDimensions::Dual => (LINK_DUAL, TIP_DUAL), + FractalDimensions::Perplex => (LINK_PERPLEX, TIP_PERPLEX), + FractalDimensions::Quaternion => (LINK_QUATERNION, TIP_QUATERNION), + }; + add_hyperlink("Info", reference, ui, tooltip); + }); + ui.horizontal(|ui| { + self.fractal_ui(ui, &dims); + }); + } + }; + }); + }); + ScrollArea::both().show(ui, |ui| { + if let Some(open) = &self.chaotic_discrete_map { + self.view_data.discrete_view_ui(open, ui); + } else if let Some(open) = &self.chaotic_diff_system { + self.view_data.continuous_view_ui(open, ui); + }; + ui.horizontal(|ui| { + integer_slider( + LABEL_NUM_EXECS, + &mut self.num_executions, + num_execution_limit, + ui, + TIP_NUM_EXECS, + ); + }); + }); + } +} diff --git a/src/gui/egui_app/conf_panels/execute_chaotic_function_view.rs b/src/gui/egui_app/conf_panels/execute_chaotic_function_view.rs new file mode 100644 index 0000000..99b0a15 --- /dev/null +++ b/src/gui/egui_app/conf_panels/execute_chaotic_function_view.rs @@ -0,0 +1,632 @@ +use super::execute::SelectedChaoticFunction; +use crate::gui::tooltips::*; +use crate::gui::*; +use crate::chaos::functions as chaotic_function_configs; +use crate::chaos::{ + data::*, + fractal::*, + labels::{ChaosDescription, ChaosFormula}, + DiscreteMapVec, OdeSolver, OdeSystemSolverVec, ParticleXYSystemSolver, ParticleXYZSystemSolver, + SimpleDiscreteMap, +}; + +use egui::Ui; +use paste::paste; +use strum_macros::EnumIter; + +const PARAMETER_DELTA: f64 = 0.1; +const PARAMETER_RANGE_NUM: usize = 200; + +fn parameter_view_single( + par: &mut f64, + suffix: &str, + (par_min, par_max): (f64, f64), + ui: &mut Ui, +) -> bool { + let (par_min, par_max) = limit_par_range(par_min, par_max); + let response = ui.add( + egui::DragValue::new(par) + .speed(PARAMETER_DELTA) + .clamp_range(par_min..=par_max) // Range inclusive + .suffix(format!(" {}", suffix)), + ); + response.changed() +} + +fn limit_par_range(par_min: f64, par_max: f64) -> (f64, f64) { + let (mut par_min, par_max) = (par_min.max(PARAMETER_MIN), par_max.min(PARAMETER_MAX)); + if par_min == par_max { + par_min = par_max - PARAMETER_DELTA; + } + (par_min, par_max) +} +fn parameter_view_ranged( + chosen_par_range: &mut (f64, f64), + num_params: &mut usize, + suffix: &str, + (total_par_min, total_par_max): (f64, f64), + ui: &mut Ui, +) -> bool { + let (total_par_min, total_par_max) = limit_par_range(total_par_min, total_par_max); + let response = ui.add( + egui::DragValue::new(&mut chosen_par_range.0) + .speed(PARAMETER_DELTA) // TODO depend on num params + .clamp_range(total_par_min..=total_par_max) // Range inclusive + .suffix(format!("Min {}", suffix)), + ); + let par_range_min_changed = response.changed(); + let response = ui.add( + egui::DragValue::new(&mut chosen_par_range.1) + .speed(PARAMETER_DELTA) + .clamp_range(total_par_min..=total_par_max) // Range inclusive + .suffix(format!("Max {}", suffix)), + ); + integer_slider( + LABEL_NUM_PARAMS, + num_params, + PARAMETER_RANGE_NUM, + ui, + TIP_NUM_PARAMS, + ); + par_range_min_changed || response.changed() +} + +fn parameter_linspace(par_min: f64, par_max: f64, num_params: usize) -> Vec { + let conf = Linspace { + low: par_min, + high: par_max, + }; + linspace(num_params, &conf) +} + +macro_rules! create_and_implement_map_view_variants { + ([$( $discrete_map:ident $discrete_state:expr),*] [$( $fractal_fn:ident),*] [$( $continuous_ode:ident $continuous_state:expr),*] [$( $particle_dim:ident),*]) => { + paste!{ + #[derive(PartialEq, Eq, Default, Clone, Copy, EnumIter)] + pub enum DiscreteMapView { + #[default] + $( + $discrete_map, + )* + $( + [], + [], + [], + [], + [], + [], + [], + [], + )* + } + impl From for &'static str { + fn from(val: DiscreteMapView) -> Self { + match val { + $( + DiscreteMapView::$discrete_map => stringify!($discrete_map), + )* + $( + DiscreteMapView::[] => stringify!([<$fractal_fn>]), + DiscreteMapView::[] => stringify!([<$fractal_fn>]), + DiscreteMapView::[] => stringify!([<$fractal_fn>]), + DiscreteMapView::[] => stringify!([<$fractal_fn>]), + DiscreteMapView::[] => stringify!([<$fractal_fn>]), + DiscreteMapView::[] => stringify!([<$fractal_fn>]), + DiscreteMapView::[] => stringify!([<$fractal_fn>]), + DiscreteMapView::[] => stringify!([<$fractal_fn>]), + )* + } + } + } + + impl DiscreteMapView { + pub fn dimensionality(&self) -> DistributionDimensions { + match self { + $( + Self::$discrete_map => [], + )* + $( + Self::[] => DIMS_FRACTALCOMPLEX, + Self::[] => DIMS_FRACTALDUAL, + Self::[] => DIMS_FRACTALPERPLEX, + Self::[] => DIMS_FRACTALQUATERNION, + Self::[] => DIMS_FRACTALCOMPLEX, + Self::[] => DIMS_FRACTALDUAL, + Self::[] => DIMS_FRACTALPERPLEX, + Self::[] => DIMS_FRACTALQUATERNION, + )* + } + } + pub fn is_mandelbrot(&self) -> bool { + match self { + $( + Self::[] => true, + Self::[] => true, + Self::[] => true, + Self::[] => true, + )* + _ => false + } + } + pub fn is_julia(&self) -> bool { + match self { + $( + Self::[] => true, + Self::[] => true, + Self::[] => true, + Self::[] => true, + )* + _ => false + } + } + } + + #[derive(PartialEq, Eq, Default, Clone, Copy, EnumIter)] + pub enum DifferentialSystemView { + #[default] + $( + $continuous_ode, + )* + $( + [], + )* + } + + impl From for &'static str { + fn from(val: DifferentialSystemView) -> Self { + match val { + $( + DifferentialSystemView::$continuous_ode => stringify!($continuous_ode), + )* + $( + DifferentialSystemView::[] => stringify!($particle_dim), + )* + } + } + } + + impl DifferentialSystemView { + pub fn dimensionality(&self) -> DistributionDimensions { + match self { + $( + Self::$continuous_ode => [], + )* + $( + Self::[] => [], + )* + } + } + } + #[allow(non_snake_case)] // for ease of copy paste + #[derive(Default, PartialEq)] + pub struct ChaosFunctionViewData { + $( + [<$discrete_map:lower>]: $discrete_map, + )* + $( + []: [], + []: [], + []: [], + []: [], + []: [], + []: [], + []: [], + []: [], + )* + $( + [<$continuous_ode:lower>]: $continuous_ode, + )* + $( + []: [], + )* + } + + + impl ChaosFunctionViewData { + pub fn discrete_description(& self, view: &DiscreteMapView) -> (String, &'static str){ + match view { + $( + DiscreteMapView::$discrete_map => { + let data = &self.[<$discrete_map:lower>].data; + (data.description(), data.reference()) + }, + )* + $( + DiscreteMapView::[] => { + let data = &self.[].data; + (data.description(), data.reference()) + }, + DiscreteMapView::[] => { + let data = &self.[].data; + (data.description(), data.reference()) + }, + DiscreteMapView::[] => { + let data = &self.[].data; + (data.description(), data.reference()) + }, + DiscreteMapView::[] => { + let data = &self.[].data; + (data.description(), data.reference()) + }, + DiscreteMapView::[] => { + let data = &self.[].data; + (data.description(), data.reference()) + }, + DiscreteMapView::[] => { + let data = &self.[].data; + (data.description(), data.reference()) + }, + DiscreteMapView::[] => { + let data = &self.[].data; + (data.description(), data.reference()) + }, + DiscreteMapView::[] => { + let data = &self.[].data; + (data.description(), data.reference()) + }, + )* + } + } + pub fn discrete_view_ui(&mut self, view: &DiscreteMapView, ui: &mut Ui) { + match view { + $( + DiscreteMapView::$discrete_map => self.[<$discrete_map:lower>].ui(ui), + )* + $( + DiscreteMapView::[] => self.[].ui(ui), + DiscreteMapView::[] => self.[].ui(ui), + DiscreteMapView::[] => self.[].ui(ui), + DiscreteMapView::[] => self.[].ui(ui), + DiscreteMapView::[] => self.[].ui(ui), + DiscreteMapView::[] => self.[].ui(ui), + DiscreteMapView::[] => self.[].ui(ui), + DiscreteMapView::[] => self.[].ui(ui), + )* + } + } + pub fn map_discrete_view_to_maps_vec_variant( + &self, + view: &DiscreteMapView, + ) -> SelectedChaoticFunction { + match view { + $( + DiscreteMapView::$discrete_map => SelectedChaoticFunction::from(self.[<$discrete_map:lower>].clone()), + )* + $( + DiscreteMapView::[] => SelectedChaoticFunction::from(self.[].clone()), + DiscreteMapView::[] => SelectedChaoticFunction::from(self.[].clone()), + DiscreteMapView::[] => SelectedChaoticFunction::from(self.[].clone()), + DiscreteMapView::[] => SelectedChaoticFunction::from(self.[].clone()), + DiscreteMapView::[] => SelectedChaoticFunction::from(self.[].clone()), + DiscreteMapView::[] => SelectedChaoticFunction::from(self.[].clone()), + DiscreteMapView::[] => SelectedChaoticFunction::from(self.[].clone()), + DiscreteMapView::[] => SelectedChaoticFunction::from(self.[].clone()), + )* + } + } + pub fn continuous_description(&self, view: &DifferentialSystemView) -> (String, &'static str) { + match view { + $( + DifferentialSystemView::$continuous_ode => { + let data = &self.[<$continuous_ode:lower>].data; + (data.description(), data.reference()) + }, + )* + $( + DifferentialSystemView::[] =>{ + let data = &self.[].data; + (data.description(), data.reference()) + }, + )* + } + } + pub fn continuous_view_ui(&mut self, view: &DifferentialSystemView, ui: &mut Ui) { + match view { + $( + DifferentialSystemView::$continuous_ode => self.[<$continuous_ode:lower>].ui(ui), + )* + $( + DifferentialSystemView::[] => self.[].ui(ui), + )* + } + } + + pub fn map_continuous_view_to_solver_vec_variant( + &self, + view: &DifferentialSystemView, + ) -> SelectedChaoticFunction { + match view { + $( + DifferentialSystemView::$continuous_ode => SelectedChaoticFunction::from(self.[<$continuous_ode:lower>].clone()), + )* + $( + DifferentialSystemView::[] => SelectedChaoticFunction::from(self.[].clone()), + )* + } + } + + } + } // paste + }; +} +create_and_implement_map_view_variants! { + [ + Logistic 1, + Tent 1, + Gauss 1, + Circle 1, + Chirikov 2, + Henon 2, + ArnoldsCat 2, + Bogdanov 2, + Chialvo 2, + DeJongRing 2, + Duffing 2, + Tinkerbell 2, + Baker 2, + Clifford 2, + Ikeda 2, + Gingerbreadman 2, + KaplanYorke 2, + Rulkov 2, + Zaslavskii 2, + Shah 3, + Memristive 3, + Sfsimm 4 + ] + [Power, Probability, Sinus, Sinh, Zubieta, Picard, Biomorph] + [ + Brusselator 2, + VanDerPol 2, + QuadrupTwoOrbit 2, + Lorenz 3, + Rossler 3, + Chen 3, + Aizawa 3, + ChuasCircuit 3, + RabinovichFabrikant 3, + GenesioTesi 3, + BurkeShaw 3, + Halvorsen 3, + ThreeSpeciesLotkaVolterra 3, + Rikitake 3, + HindmarshRose 3, + Ababneh 4, + WeiWang 4 + ] + [XY, XYZ] +} + +macro_rules! generate_view_variant { + ($variant:ident { $([$field:ident, $field_label:expr]),* }) => { + paste!{ + #[derive(PartialEq, Clone)] + pub struct $variant { + data: chaotic_function_configs::$variant, + num_params: usize, + $([]: Option<(f64,f64)>,)* + } + + impl $variant { + #[allow(dead_code)] + fn reset_ranges(&mut self){ + $( self.[] = None ;)* + } + #[allow(unused)] + pub fn ui(&mut self, ui: &mut Ui) { + ui.collapsing("Info", |ui| { + ui.label(self.data.description()); + group_vertical(ui, |ui|{ + ui.horizontal(|ui|{ + ui.heading("Formula "); + ui.hyperlink_to(stringify!($variant), self.data.reference()); + }); + self.data.formula().iter().for_each(|l| { + ui.label(*l); + }) + }); + }); + #[allow(unused_mut)] + let mut par_changed = false; + $( + group_horizontal(ui,|ui| { + let par_label = $field_label; + let allowed_range = chaotic_function_configs::$variant::[]; + let is_no_range = self.[].is_none(); + let range_label = format!("Range {}", par_label); + let tooltip = if is_no_range{ + format!("Toggle to specify an evenly spaced range over {} (Linspace). This may create a bifurcation diagram. The current chaotic data distribution is cloned for each parameter value.", par_label) + } else{ + format!("Toggle to deactivate the range over {}. Toggling off takes the data set from the parameter with the smallest value and continuous with the previously selected single parameter.", par_label) + }; + if clickable_button(range_label.as_str(), !is_no_range,true, ui, tooltip.as_str()){ + self.reset_ranges(); + if is_no_range{ + self.[] = Some(allowed_range); + } + } + let field_par_changed = if let Some(par_range) = self.[].as_mut(){ + parameter_view_ranged(par_range, &mut self.num_params, par_label, allowed_range, ui) + }else{ + parameter_view_single(&mut self.data.$field, par_label, allowed_range, ui) + }; + par_changed = par_changed || field_par_changed; + }); + )* + if par_changed { + self.data.par_range_check(); + }; + } + } + impl Default for $variant{ + fn default()->Self{ + Self{ + data: Default::default(), + num_params: 10, + $([]: None,)* + } + } + } + } + }; +} +macro_rules! impl_discrete_variants { + ($($variant:ident, $mapper:ident, { $([$field:ident, $field_label:expr]),* }),*) => { + $( + paste!{ + generate_view_variant!{ + $variant { $([$field, $field_label]),* } + } + impl From<$variant> for SelectedChaoticFunction{ + fn from(val: $variant)->Self{ + $( + if let Some((par_min, par_max)) = val.[]{ + let par_values = parameter_linspace(par_min, par_max, val.num_params); + let discrete_maps = par_values.iter().map(|par|{ + let mut pars = val.data.clone(); + pars.$field = *par; + $mapper::new(pars) + }).collect(); + let discrete_vec = DiscreteMapVec::$variant(discrete_maps); + return SelectedChaoticFunction::ParametrizedDiscreteMaps(discrete_vec, stringify!($field), par_values); + } + )* + SelectedChaoticFunction::SingleDiscreteMap(DiscreteMapVec::$variant(vec![$mapper::new(val.data.clone())])) + } + } + } + )* + }; +} + +impl_discrete_variants! { + Logistic, SimpleDiscreteMap, { [r, "r"] }, + Tent, SimpleDiscreteMap, { [mu, "μ"] }, + Gauss, SimpleDiscreteMap, { [alpha, "α"], [beta, "β"] }, + Circle, SimpleDiscreteMap, { [omega, "ω"], [k, "k"] }, + Chirikov, SimpleDiscreteMap, { [k, "k"] }, + Henon, SimpleDiscreteMap, { [a, "a"], [b, "b"] }, + ArnoldsCat, SimpleDiscreteMap, { }, + Bogdanov, SimpleDiscreteMap, { [eps, "ε"], [k, "k"], [mu, "μ"] }, + Chialvo, SimpleDiscreteMap, { [a, "a"], [b, "b"] }, + DeJongRing, SimpleDiscreteMap, { }, + Duffing, SimpleDiscreteMap, { [a, "a"], [b, "b"] }, + Tinkerbell, SimpleDiscreteMap, { [a, "a"], [b, "b"], [c, "c"], [d, "d"] }, + Baker, SimpleDiscreteMap, { }, + Clifford, SimpleDiscreteMap, { [a, "a"], [b, "b"], [c, "c"], [d, "d"] }, + Ikeda, SimpleDiscreteMap, { [u, "u"] }, + Gingerbreadman, SimpleDiscreteMap, { }, + KaplanYorke, SimpleDiscreteMap, { [alpha, "α"] }, + Rulkov, SimpleDiscreteMap, { [alpha, "α"], [mu, "μ"], [delta, "δ"] }, + Zaslavskii, SimpleDiscreteMap, { [eps, "ε"], [nu, "ν"], [r, "r"] }, + Shah, SimpleDiscreteMap, { [alpha, "α"], [beta, "β"], [gamma, "γ"], [delta, "δ"] }, + Memristive, SimpleDiscreteMap, { [k, "k"], [a, "a"] }, + Sfsimm, SimpleDiscreteMap, { [p, "p"], [b, "b"], [r, "r"] }, + MandelbrotPowerComplex, MandelbrotPower, { [r, "r"], [n, "n"] }, + MandelbrotProbabilityComplex, MandelbrotProbability, { [a, "a"], [r, "r"], [n, "n"] }, + MandelbrotSinusComplex, MandelbrotSinus, { [r, "r"], [n, "n"] }, + MandelbrotSinhComplex, MandelbrotSinh, { [r, "r"], [n, "n"] }, + MandelbrotZubietaComplex, MandelbrotZubieta, { [r, "r"], [n, "n"] }, + MandelbrotPicardComplex, MandelbrotPicard, { [a, "a"], [alpha, "α"], [n, "n"] }, + MandelbrotBiomorphComplex, MandelbrotBiomorph, { [r, "r"], [m_re, "m re"], [m_im, "m i"], [a_re, "a re"], [a_im, "a i"], [b_re, "b re"], [b_im, "b i"], [alpha, "α"], [n, "n"] }, + JuliaPowerComplex, JuliaPower, { [c_re, "c re"], [c_im, "c i"], [r, "r"], [n, "n"] }, + JuliaProbabilityComplex, JuliaProbability, { [c_re, "c re"], [c_im, "c i"], [a, "a"], [r, "r"], [n, "n"] }, + JuliaSinusComplex, JuliaSinus, { [c_re, "c re"], [c_im, "c i"], [r, "r"], [n, "n"] }, + JuliaSinhComplex, JuliaSinh, { [c_re, "c re"], [c_im, "c i"], [r, "r"], [n, "n"] }, + JuliaZubietaComplex, JuliaZubieta, { [c_re, "c re"], [c_im, "c i"], [r, "r"], [n, "n"] }, + JuliaPicardComplex, JuliaPicard, { [a, "a"], [c_re, "c re"], [c_im, "c i"], [alpha, "α"], [n, "n"] }, + JuliaBiomorphComplex, JuliaBiomorph, { [r, "r"], [c_re, "c re"], [c_im, "c i"], [m_re, "m re"], [m_im, "m i"], [a_re, "a re"], [a_im, "a i"], [b_re, "b re"], [b_im, "b i"], [alpha, "α"], [n, "n"] }, + MandelbrotPowerDual, MandelbrotPower, { [r, "r"], [n, "n"] }, + MandelbrotProbabilityDual, MandelbrotProbability, { [a, "a"], [r, "r"], [n, "n"] }, + MandelbrotSinusDual, MandelbrotSinus, { [r, "r"], [n, "n"]}, + MandelbrotSinhDual, MandelbrotSinh, { [r, "r"], [n, "n"]}, + MandelbrotZubietaDual, MandelbrotZubieta, { [r, "r"], [n, "n"]}, + MandelbrotPicardDual, MandelbrotPicard, { [a, "a"], [alpha, "α"], [n, "n"] }, + MandelbrotBiomorphDual, MandelbrotBiomorph, { [r, "r"], [m_re, "m re"], [m_im, "m ε"], [a_re, "a re"], [a_im, "a ε"], [b_re, "b re"], [b_im, "b ε"], [alpha, "α"], [n, "n"] }, + JuliaPowerDual, JuliaPower, { [c_re, "c re"], [c_im, "c ε"], [r, "r"], [n, "n"] }, + JuliaProbabilityDual, JuliaProbability, { [c_re, "c re"], [c_im, "c ε"], [a, "a"], [r, "r"], [n, "n"] }, + JuliaSinusDual, JuliaSinus, { [c_re, "c re"], [c_im, "c ε"], [r, "r"], [n, "n"]}, + JuliaSinhDual, JuliaSinh, { [c_re, "c re"], [c_im, "c ε"], [r, "r"], [n, "n"]}, + JuliaZubietaDual, JuliaZubieta, { [c_re, "c re"], [c_im, "c ε"], [r, "r"], [n, "n"]}, + JuliaPicardDual, JuliaPicard, { [a, "a"], [c_re, "c re"], [c_im, "c ε"], [alpha, "α"], [n, "n"] }, + JuliaBiomorphDual, JuliaBiomorph, { [r, "r"], [c_re, "c re"], [c_im, "c ε"], [m_re, "m re"], [m_im, "m ε"], [a_re, "a re"], [a_im, "a ε"], [b_re, "b re"], [b_im, "b ε"], [alpha, "α"], [n, "n"] }, + MandelbrotPowerPerplex, MandelbrotPower, { [r, "r"], [n, "n"] }, + MandelbrotProbabilityPerplex, MandelbrotProbability, { [a, "a"], [r, "r"], [n, "n"] }, + MandelbrotSinusPerplex, MandelbrotSinus, { [r, "r"], [n, "n"]}, + MandelbrotSinhPerplex, MandelbrotSinh, { [r, "r"], [n, "n"]}, + MandelbrotZubietaPerplex, MandelbrotZubieta, { [r, "r"], [n, "n"]}, + MandelbrotPicardPerplex, MandelbrotPicard, { [a, "a"], [alpha, "α"], [n, "n"] }, + MandelbrotBiomorphPerplex, MandelbrotBiomorph, { [r, "r"], [m_re, "m t"], [m_im, "m x"], [a_re, "a t"], [a_im, "a x"], [b_re, "b t"], [b_im, "b x"], [alpha, "α"], [n, "n"] }, + JuliaPowerPerplex, JuliaPower, { [c_re, "c t"], [c_im, "c x"], [r, "r"], [n, "n"] }, + JuliaProbabilityPerplex, JuliaProbability, { [c_re, "c t"], [c_im, "c x"], [a, "a"], [r, "r"], [n, "n"] }, + JuliaSinusPerplex, JuliaSinus, { [c_re, "c t"], [c_im, "c x"], [r, "r"], [n, "n"]}, + JuliaSinhPerplex, JuliaSinh, { [c_re, "c t"], [c_im, "c x"], [r, "r"], [n, "n"]}, + JuliaZubietaPerplex, JuliaZubieta, { [c_re, "c t"], [c_im, "c x"], [r, "r"], [n, "n"]}, + JuliaPicardPerplex, JuliaPicard, { [a, "a"], [c_re, "c t"], [c_im, "c x"], [alpha, "α"], [n, "n"] }, + JuliaBiomorphPerplex, JuliaBiomorph, { [r, "r"], [c_re, "c t"], [c_im, "c x"], [m_re, "m t"], [m_im, "m x"], [a_re, "a t"], [a_im, "a x"], [b_re, "b t"], [b_im, "b x"], [alpha, "α"], [n, "n"] }, + MandelbrotPowerQuaternion, MandelbrotPower, { [r, "r"], [n, "n"] }, + MandelbrotProbabilityQuaternion, MandelbrotProbability, { [a, "a"], [r, "r"], [n, "n"] }, + MandelbrotSinusQuaternion, MandelbrotSinus, { [r, "r"], [n, "n"]}, + MandelbrotSinhQuaternion, MandelbrotSinh, { [r, "r"], [n, "n"]}, + MandelbrotZubietaQuaternion, MandelbrotZubieta, { [r, "r"], [n, "n"]}, + MandelbrotPicardQuaternion, MandelbrotPicard, { [a, "a"], [alpha, "α"], [n, "n"] }, + MandelbrotBiomorphQuaternion, MandelbrotBiomorph, { [r, "r"], [m_w, "m w"], [m_i, "m i"], [m_j, "m j"], [m_k, "m k"], [a_w, "a w"], [a_i, "a i"], [a_j, "a j"], [a_k, "a k"], [b_w, "b w"], [b_i, "b i"], [b_j, "b j"], [b_k, "b k"], [alpha, "α"], [n, "n"] }, + JuliaPowerQuaternion, JuliaPower, { [c_w, "c w"], [c_i, "c i"], [c_j, "c j"], [c_k, "c k"], [r, "r"], [n, "n"] }, + JuliaProbabilityQuaternion, JuliaProbability, { [c_w, "c w"], [c_i, "c i"], [c_j, "c j"], [c_k, "c k"], [a, "a"], [r, "r"], [n, "n"] }, + JuliaSinusQuaternion, JuliaSinus, { [c_w, "c w"], [c_i, "c i"], [c_j, "c j"], [c_k, "c k"], [r, "r"], [n, "n"]}, + JuliaSinhQuaternion, JuliaSinh, { [c_w, "c w"], [c_i, "c i"], [c_j, "c j"], [c_k, "c k"], [r, "r"], [n, "n"]}, + JuliaZubietaQuaternion, JuliaZubieta, { [c_w, "c w"], [c_i, "c i"], [c_j, "c j"], [c_k, "c k"], [r, "r"], [n, "n"]}, + JuliaPicardQuaternion, JuliaPicard, { [a, "a"], [c_w, "c w"], [c_i, "c i"], [c_j, "c j"], [c_k, "c k"], [alpha, "α"], [n, "n"] }, + JuliaBiomorphQuaternion, JuliaBiomorph, { [r, "r"], [c_w, "c w"], [c_i, "c i"], [c_j, "c j"], [c_k, "c k"], [m_w, "m w"], [m_i, "m i"], [m_j, "m j"], [m_k, "m k"], [a_w, "a w"], [a_i, "a i"], [a_j, "a j"], [a_k, "a k"], [b_w, "b w"], [b_i, "b i"], [b_j, "b j"], [b_k, "b k"], [alpha, "α"], [n, "n"] } +} + +macro_rules! impl_continuous_variants { + ($($variant:ident, $solver:ident, { $([$field:ident, $field_label:expr]),* }),*) => { + $( + paste!{ + generate_view_variant!{ + $variant { $([$field, $field_label]),* } + } + impl From<$variant> for SelectedChaoticFunction{ + fn from(val: $variant)->Self{ + $( + if let Some((par_min, par_max)) = val.[]{ + let par_values = parameter_linspace(par_min, par_max, val.num_params); + let ode_solvers = par_values.iter().map(|par|{ + let mut pars = val.data.clone(); + pars.$field = *par; + $solver::new(pars) + }).collect(); + let ode_solver_vec = OdeSystemSolverVec::$variant(ode_solvers); + return SelectedChaoticFunction::ParametrizedDifferentialSystems(ode_solver_vec, stringify!($field), par_values); + } + )* + SelectedChaoticFunction::SingleDifferentialSystem(OdeSystemSolverVec::$variant(vec![$solver::new(val.data.clone())])) + } + } + } + )* + }; +} + +impl_continuous_variants! { + Brusselator, OdeSolver, { [a, "a"] , [b, "b"] }, + VanDerPol, OdeSolver, { [mu, "μ"] }, + QuadrupTwoOrbit, OdeSolver, { [a, "a"] , [b, "b"] , [c, "c"] }, + Lorenz, OdeSolver, { [sigma, "σ"] , [beta, "β"] , [rho, "ρ"] }, + Rossler, OdeSolver, { [a, "a"] , [b, "b"] , [c, "c"] }, + Chen, OdeSolver, { [a, "a"] , [b, "b"] , [c, "c"] }, + Aizawa, OdeSolver, { [a, "a"] , [b, "b"] , [c, "c"] , [d, "d"] , [e, "e"] , [f, "f"] }, + ChuasCircuit, OdeSolver, { [alpha, "α"] , [beta, "β"] }, + RabinovichFabrikant, OdeSolver, { [alpha, "α"] , [gamma, "γ"] }, + GenesioTesi, OdeSolver, { [a, "a"] , [b, "b"] , [c, "c"] }, + BurkeShaw, OdeSolver, { [s, "s"] , [v, "v"] }, + Halvorsen, OdeSolver, { [a, "a"] }, + ThreeSpeciesLotkaVolterra, OdeSolver, { [b, "b"] , [d1, "d1"] , [d2, "d2"] , [a11, "a11"] , [a12, "a12"] , [a13, "a13"] , [a21, "a21"] , [a23, "a23"] , [a31, "a31"] , [a32, "a32"] }, + Rikitake, OdeSolver, { [a, "a"] , [mu, "μ"] }, + HindmarshRose, OdeSolver, { [a, "a"] , [b, "b"] , [c, "c"] , [d, "d"] , [r, "r"] , [i, "i"] }, + Ababneh, OdeSolver, { [a, "a"] , [b, "b"] }, + WeiWang, OdeSolver, { [a, "a"] , [b, "b"] , [c, "c"] , [d, "d"] , [k, "k"] }, + ParticleXY, ParticleXYSystemSolver, { [s, "s"] , [m, "m"] , [l, "l"] }, + ParticleXYZ, ParticleXYZSystemSolver, { [s, "s"] , [m, "m"] , [l, "l"] } +} diff --git a/src/gui/egui_app/conf_panels/initial.rs b/src/gui/egui_app/conf_panels/initial.rs new file mode 100644 index 0000000..f63c9df --- /dev/null +++ b/src/gui/egui_app/conf_panels/initial.rs @@ -0,0 +1,884 @@ +use crate::chaos::data::*; +use egui::Ui; +use strum_macros::{EnumIter, IntoStaticStr}; + +use super::initial_distribution_view::{ + InitialDistributionView, InitialDistributionViewData, INITIAL_DETERMINISTIC, INITIAL_MESHES, + INITIAL_PROBABILISTIC, +}; +use crate::gui::tooltips::*; +use crate::gui::*; +use crate::chaos::labels::*; + +const MAX_NUM_STATE_DIMS: usize = 4; + +fn generate_initital_distribution_variant( + open_initial_distributions: &[InitialDistributionViewSelection], + all_initial_distributions: &[InitialDistributionViewData], + i: usize, +) -> InitialDistributionVariant { + let open = open_initial_distributions[i]; + let initial_distributions = &all_initial_distributions[i]; + initial_distributions.map_initial_distribution_view_to_data(&open.view) +} +fn distribution_selection( + open: &mut InitialDistributionViewSelection, + all_initital_distributions: &mut InitialDistributionViewData, + label: &str, + show_space_distributions: bool, + ui: &mut Ui, + distributions_tooltip: &str, +) { + ui.group(|ui| { + ui.vertical(|ui| { + ui.horizontal(|ui| { + let (groups, tooltip): (&[InitialDistributionGroup], &str) = + if show_space_distributions { + (&DISTRIBUTIONS_ALL, TIP_DISTRIBUTIONS_ALL) + } else { + (&DISTRIBUTIONS_NO_MESH, TIP_DISTRIBUTIONS_NO_MESH) + }; + select_group_filtered(open.group_mut(), ui, groups, tooltip); + }); + ui.horizontal(|ui| { + let group_variants: &[InitialDistributionView] = match open.group { + InitialDistributionGroup::Probabilistic => &INITIAL_PROBABILISTIC, + InitialDistributionGroup::Deterministic => &INITIAL_DETERMINISTIC, + InitialDistributionGroup::Mesh => &INITIAL_MESHES, + }; + let group_with_labels: Vec<(InitialDistributionView, String)> = group_variants + .iter() + .map(|view| (*view, String::from(*view))) + .collect(); + let current_label = String::from(open.view); + combo_box_from_string( + label, + (open.view_mut(), current_label), + ui, + group_with_labels, + distributions_tooltip, + ); + all_initital_distributions.view_ui(open.view, ui); + }); + }); + }); +} + +#[derive(PartialEq, Clone, Copy, Default, EnumIter, IntoStaticStr)] +enum InitialMode { + #[default] + States, + Particle, + Fractals, +} +#[derive(PartialEq, Default)] +struct InitialModeData { + pub states: InitialStateData, + pub particles: InitialParticleData, + pub fractals: InitialFractalData, +} + +#[derive(PartialEq)] +struct InitialStateData { + pub num_state_dims: usize, + open_initial_distributions: [InitialDistributionViewSelection; MAX_NUM_STATE_DIMS], + all_initital_distributions: [InitialDistributionViewData; MAX_NUM_STATE_DIMS], +} + +impl Default for InitialStateData { + fn default() -> Self { + let open: [InitialDistributionViewSelection; MAX_NUM_STATE_DIMS] = [ + InitialDistributionViewSelection::default(), + InitialDistributionViewSelection::default(), + InitialDistributionViewSelection::default(), + InitialDistributionViewSelection::new( + InitialDistributionView::Fixed, + InitialDistributionGroup::Deterministic, + ), + ]; + let initial_distributions: [InitialDistributionViewData; MAX_NUM_STATE_DIMS] = + std::array::from_fn(|_| InitialDistributionViewData::default()); + Self { + num_state_dims: 2, + open_initial_distributions: open, + all_initital_distributions: initial_distributions, + } + } +} + +impl InitialStateData { + fn num_states_selection_ui(&mut self, ui: &mut Ui) { + let minus_button_activated = self.num_state_dims > 1; + if clickable_button( + "-", + false, + minus_button_activated, + ui, + TIP_BUTTON_DECREASE_NUM_STATES, + ) { + self.num_state_dims -= 1; + } + add_label( + format!("Dims: {}", self.num_state_dims).as_str(), + ui, + TIP_DIMS, + ); + let plus_button_activated = self.num_state_dims < MAX_NUM_STATE_DIMS; + if clickable_button( + "+", + false, + plus_button_activated, + ui, + TIP_BUTTON_INCREASE_NUM_STATES, + ) { + self.num_state_dims += 1; + } + } + fn open_selections(&self) -> &[InitialDistributionViewSelection] { + self.open_initial_distributions + .split_at(self.num_state_dims) + .0 + } + fn initial_distributions(&self) -> InitialDistributionConfig { + InitialDistributionConfig::States( + (0..self.num_state_dims) + .map(|i| { + generate_initital_distribution_variant( + &self.open_initial_distributions, + &self.all_initital_distributions, + i, + ) + }) + .collect(), + ) + } + fn selection_ui(&mut self, ui: &mut Ui) { + for i in 0..self.num_state_dims { + let label = format!("S{}", i + 1); + let tooltip = format!(" Select initital distribution for state {} ", i + 1); + let open = &mut self.open_initial_distributions[i]; + let all_initial_distributions = &mut self.all_initital_distributions[i]; + distribution_selection( + open, + all_initial_distributions, + label.as_str(), + true, + ui, + tooltip.as_str(), + ); + } + } +} + +#[derive(PartialEq)] +struct InitialParticleData { + open_initial_distributions_xy: [InitialDistributionViewSelection; DIMS_INIT_PARTICLEXY], + all_initital_distributions_xy: [InitialDistributionViewData; DIMS_INIT_PARTICLEXY], + open_initial_distributions_xyz: [InitialDistributionViewSelection; DIMS_INIT_PARTICLEXYZ], + all_initital_distributions_xyz: [InitialDistributionViewData; DIMS_INIT_PARTICLEXYZ], +} + +impl Default for InitialParticleData { + fn default() -> Self { + let open_xy: [InitialDistributionViewSelection; DIMS_INIT_PARTICLEXY] = [ + InitialDistributionViewSelection::new( + InitialDistributionView::Normal, + InitialDistributionGroup::Probabilistic, + ), // "Parity (collision)", + InitialDistributionViewSelection::new( + InitialDistributionView::Normal, + InitialDistributionGroup::Probabilistic, + ), // "Mid-range (charge)", + InitialDistributionViewSelection::new( + InitialDistributionView::Exponential, + InitialDistributionGroup::Probabilistic, + ), // "Mass (absolute)", + InitialDistributionViewSelection::new( + InitialDistributionView::Uniform, + InitialDistributionGroup::Probabilistic, + ), // "Position X", + InitialDistributionViewSelection::new( + InitialDistributionView::Uniform, + InitialDistributionGroup::Probabilistic, + ), // "Position Y", + InitialDistributionViewSelection::new( + InitialDistributionView::Fixed, + InitialDistributionGroup::Deterministic, + ), // "Velocity X", + InitialDistributionViewSelection::new( + InitialDistributionView::Fixed, + InitialDistributionGroup::Deterministic, + ), // "Velocity Y", + ]; + let mut initial_distributions_xy: [InitialDistributionViewData; DIMS_INIT_PARTICLEXY] = + std::array::from_fn(|_| InitialDistributionViewData::default()); + initial_distributions_xy[3].uniform.data = Uniform { + low: -100.0, + high: 100.0, + }; + initial_distributions_xy[4].uniform.data = Uniform { + low: -100.0, + high: 100.0, + }; + let open_xyz: [InitialDistributionViewSelection; DIMS_INIT_PARTICLEXYZ] = [ + InitialDistributionViewSelection::new( + InitialDistributionView::Normal, + InitialDistributionGroup::Probabilistic, + ), // "Parity (collision)", + InitialDistributionViewSelection::new( + InitialDistributionView::Normal, + InitialDistributionGroup::Probabilistic, + ), // "Mid-range (charge)", + InitialDistributionViewSelection::new( + InitialDistributionView::Exponential, + InitialDistributionGroup::Probabilistic, + ), // "Mass (absolute)", + InitialDistributionViewSelection::new( + InitialDistributionView::Uniform, + InitialDistributionGroup::Probabilistic, + ), // "Position X", + InitialDistributionViewSelection::new( + InitialDistributionView::Uniform, + InitialDistributionGroup::Probabilistic, + ), // "Position Y", + InitialDistributionViewSelection::new( + InitialDistributionView::Uniform, + InitialDistributionGroup::Probabilistic, + ), // "Position Z", + InitialDistributionViewSelection::new( + InitialDistributionView::Fixed, + InitialDistributionGroup::Deterministic, + ), // "Velocity X", + InitialDistributionViewSelection::new( + InitialDistributionView::Fixed, + InitialDistributionGroup::Deterministic, + ), // "Velocity Y", + InitialDistributionViewSelection::new( + InitialDistributionView::Fixed, + InitialDistributionGroup::Deterministic, + ), // "Velocity Z", + ]; + let mut initial_distributions_xyz: [InitialDistributionViewData; DIMS_INIT_PARTICLEXYZ] = + std::array::from_fn(|_| InitialDistributionViewData::default()); + initial_distributions_xyz[3].uniform.data = Uniform { + low: -100.0, + high: 100.0, + }; + initial_distributions_xyz[4].uniform.data = Uniform { + low: -100.0, + high: 100.0, + }; + initial_distributions_xyz[5].uniform.data = Uniform { + low: -100.0, + high: 100.0, + }; + + Self { + open_initial_distributions_xy: open_xy, + all_initital_distributions_xy: initial_distributions_xy, + open_initial_distributions_xyz: open_xyz, + all_initital_distributions_xyz: initial_distributions_xyz, + } + } +} + +impl InitialParticleData { + fn open_selections(&self, particle_mode: ParticleMode) -> &[InitialDistributionViewSelection] { + match particle_mode { + ParticleMode::XY => self.open_initial_distributions_xy.as_slice(), + ParticleMode::XYZ => self.open_initial_distributions_xyz.as_slice(), + } + } + fn initial_distributions(&self, particle_mode: ParticleMode) -> InitialDistributionConfig { + match particle_mode { + ParticleMode::XY => { + let initial_distributions: [InitialDistributionVariant; DIMS_INIT_PARTICLEXY] = + std::array::from_fn(|i| { + generate_initital_distribution_variant( + &self.open_initial_distributions_xy, + &self.all_initital_distributions_xy, + i, + ) + }); + InitialDistributionConfig::ParticleXY(initial_distributions) + } + ParticleMode::XYZ => { + let initial_distributions: [InitialDistributionVariant; DIMS_INIT_PARTICLEXYZ] = + std::array::from_fn(|i| { + generate_initital_distribution_variant( + &self.open_initial_distributions_xyz, + &self.all_initital_distributions_xyz, + i, + ) + }); + InitialDistributionConfig::ParticleXYZ(initial_distributions) + } + } + } + + fn selection_ui(&mut self, particle_mode: ParticleMode, ui: &mut Ui) { + let labels: Vec<&'static str> = particle_mode.labels().into(); + let meshable: Vec = particle_mode.meshable_dims().into(); + let tooltips: Vec<&'static str> = particle_mode.tooltips().into(); + labels + .into_iter() + .zip(meshable) + .zip(tooltips) + .enumerate() + .for_each(|(i, ((label, show_space_distributions), tooltip))| { + let (open, all_initial_distributions) = match particle_mode { + ParticleMode::XY => ( + &mut self.open_initial_distributions_xy[i], + &mut self.all_initital_distributions_xy[i], + ), + ParticleMode::XYZ => ( + &mut self.open_initial_distributions_xyz[i], + &mut self.all_initital_distributions_xyz[i], + ), + }; + distribution_selection( + open, + all_initial_distributions, + label, + show_space_distributions, + ui, + tooltip, + ); + }); + } +} + +#[allow(clippy::upper_case_acronyms)] +#[derive(PartialEq, Clone, Copy, Default, EnumIter, IntoStaticStr)] +enum ParticleMode { + #[default] + XY, + XYZ, +} + +impl ParticleMode { + const PARTICLE_2D_LABELS: [&'static str; DIMS_INIT_PARTICLEXY] = [ + LABEL_PARTICLE_PARITY, + LABEL_PARTICLE_CHARGE, + LABEL_PARTICLE_MASS, + LABEL_PARTICLE_PX, + LABEL_PARTICLE_PY, + LABEL_PARTICLE_VX, + LABEL_PARTICLE_VY, + ]; + const PARTICLE_3D_LABELS: [&'static str; DIMS_INIT_PARTICLEXYZ] = [ + LABEL_PARTICLE_PARITY, + LABEL_PARTICLE_CHARGE, + LABEL_PARTICLE_MASS, + LABEL_PARTICLE_PX, + LABEL_PARTICLE_PY, + LABEL_PARTICLE_PZ, + LABEL_PARTICLE_VX, + LABEL_PARTICLE_VY, + LABEL_PARTICLE_VZ, + ]; + const PARTICLE_2D_MESHABLE: [bool; DIMS_INIT_PARTICLEXY] = + [false, false, false, true, true, false, false]; + const PARTICLE_3D_MESHABLE: [bool; DIMS_INIT_PARTICLEXYZ] = + [false, false, false, true, true, true, false, false, false]; + const PARTICLE_2D_TOOLTIPS: [&'static str; DIMS_INIT_PARTICLEXY] = [ + // TODO also describe how particles are drawn! + TIP_PARTICLE_PARITY, + TIP_PARTICLE_CHARGE, + TIP_PARTICLE_MASS, + TIP_PARTICLE_PX, + TIP_PARTICLE_PY, + TIP_PARTICLE_VX, + TIP_PARTICLE_VY, + ]; + const PARTICLE_3D_TOOLTIPS: [&'static str; DIMS_INIT_PARTICLEXYZ] = [ + TIP_PARTICLE_PARITY, + TIP_PARTICLE_CHARGE, + TIP_PARTICLE_MASS, + TIP_PARTICLE_PX, + TIP_PARTICLE_PY, + TIP_PARTICLE_PZ, + TIP_PARTICLE_VX, + TIP_PARTICLE_VY, + TIP_PARTICLE_VZ, + ]; + fn dimensionality(&self) -> DistributionDimensions { + match self { + ParticleMode::XY => DIMS_PARTICLEXY, + ParticleMode::XYZ => DIMS_PARTICLEXYZ, + } + } + + fn labels(&self) -> &[&'static str] { + match self { + ParticleMode::XY => &Self::PARTICLE_2D_LABELS, + ParticleMode::XYZ => &Self::PARTICLE_3D_LABELS, + } + } + + fn meshable_dims(&self) -> &[bool] { + match self { + ParticleMode::XY => &Self::PARTICLE_2D_MESHABLE, + ParticleMode::XYZ => &Self::PARTICLE_3D_MESHABLE, + } + } + + fn tooltips(&self) -> &[&'static str] { + match self { + ParticleMode::XY => &Self::PARTICLE_2D_TOOLTIPS, + ParticleMode::XYZ => &Self::PARTICLE_3D_TOOLTIPS, + } + } +} + +#[derive(PartialEq)] +struct InitialFractalData { + open_initial_distributions_complex: + [InitialDistributionViewSelection; DIMS_INIT_FRACTALCOMPLEX], + all_initital_distributions_complex: [InitialDistributionViewData; DIMS_INIT_FRACTALCOMPLEX], + open_initial_distributions_dual: [InitialDistributionViewSelection; DIMS_INIT_FRACTALDUAL], + all_initital_distributions_dual: [InitialDistributionViewData; DIMS_INIT_FRACTALDUAL], + open_initial_distributions_perplex: + [InitialDistributionViewSelection; DIMS_INIT_FRACTALPERPLEX], + all_initital_distributions_perplex: [InitialDistributionViewData; DIMS_INIT_FRACTALPERPLEX], + open_initial_distributions_quaternion: + [InitialDistributionViewSelection; DIMS_INIT_FRACTALQUATERNION], + all_initital_distributions_quaternion: + [InitialDistributionViewData; DIMS_INIT_FRACTALQUATERNION], +} + +impl Default for InitialFractalData { + fn default() -> Self { + let open_complex: [InitialDistributionViewSelection; DIMS_INIT_FRACTALCOMPLEX] = [ + InitialDistributionViewSelection::new( + InitialDistributionView::Mesh, + InitialDistributionGroup::Mesh, + ), + InitialDistributionViewSelection::new( + InitialDistributionView::Mesh, + InitialDistributionGroup::Mesh, + ), + ]; + let initial_distributions_complex: [InitialDistributionViewData; DIMS_INIT_FRACTALCOMPLEX] = + std::array::from_fn(|_| InitialDistributionViewData::default()); + let open_dual: [InitialDistributionViewSelection; DIMS_INIT_FRACTALDUAL] = [ + InitialDistributionViewSelection::new( + InitialDistributionView::Mesh, + InitialDistributionGroup::Mesh, + ), + InitialDistributionViewSelection::new( + InitialDistributionView::Mesh, + InitialDistributionGroup::Mesh, + ), + ]; + let initial_distributions_dual: [InitialDistributionViewData; DIMS_INIT_FRACTALDUAL] = + std::array::from_fn(|_| InitialDistributionViewData::default()); + let open_perplex: [InitialDistributionViewSelection; DIMS_INIT_FRACTALPERPLEX] = [ + InitialDistributionViewSelection::new( + InitialDistributionView::Mesh, + InitialDistributionGroup::Mesh, + ), + InitialDistributionViewSelection::new( + InitialDistributionView::Mesh, + InitialDistributionGroup::Mesh, + ), + ]; + let initial_distributions_perplex: [InitialDistributionViewData; DIMS_INIT_FRACTALPERPLEX] = + std::array::from_fn(|_| InitialDistributionViewData::default()); + let open_quaternion: [InitialDistributionViewSelection; DIMS_INIT_FRACTALQUATERNION] = [ + InitialDistributionViewSelection::new( + InitialDistributionView::Uniform, + InitialDistributionGroup::Probabilistic, + ), + InitialDistributionViewSelection::new( + InitialDistributionView::Uniform, + InitialDistributionGroup::Probabilistic, + ), + InitialDistributionViewSelection::new( + InitialDistributionView::Uniform, + InitialDistributionGroup::Probabilistic, + ), + InitialDistributionViewSelection::new( + InitialDistributionView::Fixed, + InitialDistributionGroup::Deterministic, + ), + ]; + let initial_distributions_quaternion: [InitialDistributionViewData; + DIMS_INIT_FRACTALQUATERNION] = + std::array::from_fn(|_| InitialDistributionViewData::default()); + Self { + open_initial_distributions_complex: open_complex, + all_initital_distributions_complex: initial_distributions_complex, + open_initial_distributions_dual: open_dual, + all_initital_distributions_dual: initial_distributions_dual, + open_initial_distributions_perplex: open_perplex, + all_initital_distributions_perplex: initial_distributions_perplex, + open_initial_distributions_quaternion: open_quaternion, + all_initital_distributions_quaternion: initial_distributions_quaternion, + } + } +} + +impl InitialFractalData { + fn open_selections(&self, fractal_mode: FractalMode) -> &[InitialDistributionViewSelection] { + match fractal_mode { + FractalMode::Complex => self.open_initial_distributions_complex.as_slice(), + FractalMode::Dual => self.open_initial_distributions_dual.as_slice(), + FractalMode::Perplex => self.open_initial_distributions_perplex.as_slice(), + FractalMode::Quaternion => self.open_initial_distributions_quaternion.as_slice(), + } + } + fn initial_distributions(&self, fractal_mode: FractalMode) -> InitialDistributionConfig { + match fractal_mode { + FractalMode::Complex => { + let initial_distributions: [InitialDistributionVariant; DIMS_INIT_FRACTALCOMPLEX] = + std::array::from_fn(|i| { + generate_initital_distribution_variant( + &self.open_initial_distributions_complex, + &self.all_initital_distributions_complex, + i, + ) + }); + InitialDistributionConfig::FractalComplex(initial_distributions) + } + FractalMode::Dual => { + let initial_distributions: [InitialDistributionVariant; DIMS_INIT_FRACTALDUAL] = + std::array::from_fn(|i| { + generate_initital_distribution_variant( + &self.open_initial_distributions_dual, + &self.all_initital_distributions_dual, + i, + ) + }); + InitialDistributionConfig::FractalDual(initial_distributions) + } + FractalMode::Perplex => { + let initial_distributions: [InitialDistributionVariant; DIMS_INIT_FRACTALPERPLEX] = + std::array::from_fn(|i| { + generate_initital_distribution_variant( + &self.open_initial_distributions_perplex, + &self.all_initital_distributions_perplex, + i, + ) + }); + InitialDistributionConfig::FractalPerplex(initial_distributions) + } + FractalMode::Quaternion => { + let initial_distributions: [InitialDistributionVariant; + DIMS_INIT_FRACTALQUATERNION] = std::array::from_fn(|i| { + generate_initital_distribution_variant( + &self.open_initial_distributions_quaternion, + &self.all_initital_distributions_quaternion, + i, + ) + }); + InitialDistributionConfig::FractalQuaternion(initial_distributions) + } + } + } + + fn selection_ui(&mut self, fractal_mode: FractalMode, ui: &mut Ui) { + let labels: Vec<&'static str> = fractal_mode.labels().into(); + let tooltips: Vec<&'static str> = fractal_mode.tooltips().into(); + labels + .into_iter() + .zip(tooltips) + .enumerate() + .for_each(|(i, (label, tooltip))| { + let (open, all_initial_distributions) = match fractal_mode { + FractalMode::Complex => ( + &mut self.open_initial_distributions_complex[i], + &mut self.all_initital_distributions_complex[i], + ), + FractalMode::Dual => ( + &mut self.open_initial_distributions_dual[i], + &mut self.all_initital_distributions_dual[i], + ), + FractalMode::Perplex => ( + &mut self.open_initial_distributions_perplex[i], + &mut self.all_initital_distributions_perplex[i], + ), + FractalMode::Quaternion => ( + &mut self.open_initial_distributions_quaternion[i], + &mut self.all_initital_distributions_quaternion[i], + ), + }; + distribution_selection(open, all_initial_distributions, label, true, ui, tooltip); + }); + } +} + +#[derive(PartialEq, Clone, Copy, Default, EnumIter, IntoStaticStr)] +enum FractalMode { + #[default] + Complex, + Dual, + Perplex, + Quaternion, +} + +impl FractalMode { + const LABELS_COMPLEX: [&'static str; DIMS_INIT_FRACTALCOMPLEX] = ["a", "b"]; + const LABELS_DUAL: [&'static str; DIMS_INIT_FRACTALDUAL] = ["a", "b"]; + const LABELS_PERPLEX: [&'static str; DIMS_INIT_FRACTALPERPLEX] = ["t", "x"]; + const LABELS_QUATERNION: [&'static str; DIMS_INIT_FRACTALQUATERNION] = ["a", "b", "c", "d"]; + const TIPS_COMPLEX: [&'static str; DIMS_INIT_FRACTALCOMPLEX] = + [TIP_FRACTAL_COMPLEX_RE, TIP_FRACTAL_COMPLEX_IM]; + const TIPS_DUAL: [&'static str; DIMS_INIT_FRACTALDUAL] = + [TIP_FRACTAL_DUAL_RE, TIP_FRACTAL_DUAL_IM]; + const TIPS_PERPLEX: [&'static str; DIMS_INIT_FRACTALPERPLEX] = + [TIP_FRACTAL_PERPLEX_RE, TIP_FRACTAL_PERPLEX_IM]; + const TIPS_QUATERNION: [&'static str; DIMS_INIT_FRACTALQUATERNION] = [ + TIP_FRACTAL_QUATERNION_RE, + TIP_FRACTAL_QUATERNION_I, + TIP_FRACTAL_QUATERNION_J, + TIP_FRACTAL_QUATERNION_K, + ]; + fn dimensionality(&self) -> DistributionDimensions { + match self { + FractalMode::Complex => DIMS_FRACTALCOMPLEX, + FractalMode::Dual => DIMS_FRACTALDUAL, + FractalMode::Perplex => DIMS_FRACTALPERPLEX, + FractalMode::Quaternion => DIMS_FRACTALQUATERNION, + } + } + fn tip_basic(&self) -> &'static str { + match self { + FractalMode::Complex => TIP_COMPLEX, + FractalMode::Dual => TIP_DUAL, + FractalMode::Perplex => TIP_PERPLEX, + FractalMode::Quaternion => TIP_QUATERNION, + } + } + fn reference(&self) -> &'static str { + match self { + FractalMode::Complex => LINK_COMPLEX, + FractalMode::Dual => LINK_DUAL, + FractalMode::Perplex => LINK_PERPLEX, + FractalMode::Quaternion => LINK_QUATERNION, + } + } + fn element_basis(&self) -> &'static str { + match self { + FractalMode::Complex => LABEL_BASIS_COMPLEX, + FractalMode::Dual => LABEL_BASIS_DUAL, + FractalMode::Perplex => LABEL_BASIS_PERPLEX, + FractalMode::Quaternion => LABEL_BASIS_QUATERNION, + } + } + fn labels(&self) -> &[&'static str] { + match self { + FractalMode::Complex => &Self::LABELS_COMPLEX, + FractalMode::Dual => &Self::LABELS_DUAL, + FractalMode::Perplex => &Self::LABELS_PERPLEX, + FractalMode::Quaternion => &Self::LABELS_QUATERNION, + } + } + fn tooltips(&self) -> &[&'static str] { + match self { + FractalMode::Complex => &Self::TIPS_COMPLEX, + FractalMode::Dual => &Self::TIPS_DUAL, + FractalMode::Perplex => &Self::TIPS_PERPLEX, + FractalMode::Quaternion => &Self::TIPS_QUATERNION, + } + } +} + +#[derive(PartialEq, Copy, Clone, Default, EnumIter, IntoStaticStr)] +enum InitialDistributionGroup { + #[default] + Probabilistic, + Mesh, + Deterministic, +} + +const DISTRIBUTIONS_ALL: [InitialDistributionGroup; 3] = [ + InitialDistributionGroup::Probabilistic, + InitialDistributionGroup::Mesh, + InitialDistributionGroup::Deterministic, +]; + +const DISTRIBUTIONS_NO_MESH: [InitialDistributionGroup; 2] = [ + InitialDistributionGroup::Probabilistic, + InitialDistributionGroup::Deterministic, +]; + +#[derive(PartialEq, Default, Copy, Clone)] +struct InitialDistributionViewSelection { + pub view: InitialDistributionView, + pub group: InitialDistributionGroup, +} + +impl InitialDistributionViewSelection { + fn new(view: InitialDistributionView, group: InitialDistributionGroup) -> Self { + Self { view, group } + } + fn view_mut(&mut self) -> &mut InitialDistributionView { + &mut self.view + } + fn group_mut(&mut self) -> &mut InitialDistributionGroup { + &mut self.group + } +} + +#[derive(PartialEq)] +pub struct InitialPanel { + num_samples: usize, + init_mode: InitialMode, + particle_mode: ParticleMode, + fractal_mode: FractalMode, + initial_mode_data: InitialModeData, +} +impl Default for InitialPanel { + fn default() -> Self { + Self { + num_samples: 10, + init_mode: Default::default(), + particle_mode: Default::default(), + fractal_mode: Default::default(), + initial_mode_data: Default::default(), + } + } +} + +impl InitialPanel { + pub fn number_of_samples(&self) -> usize { + self.num_samples + } + + pub fn initial_distributions(&self) -> InitialDistributionConfig { + match self.init_mode { + InitialMode::States => self.initial_mode_data.states.initial_distributions(), + InitialMode::Particle => self + .initial_mode_data + .particles + .initial_distributions(self.particle_mode), + InitialMode::Fractals => self + .initial_mode_data + .fractals + .initial_distributions(self.fractal_mode), + } + } + + fn num_state_dims(&self) -> usize { + self.initial_mode_data.states.num_state_dims + } + + fn count_open_meshes(&self) -> usize { + let open_selections: &[InitialDistributionViewSelection] = match self.init_mode { + InitialMode::States => self.initial_mode_data.states.open_selections(), + InitialMode::Particle => self + .initial_mode_data + .particles + .open_selections(self.particle_mode), + InitialMode::Fractals => self + .initial_mode_data + .fractals + .open_selections(self.fractal_mode), + }; + let mut ct = 0; + for open in open_selections { + if INITIAL_MESHES.contains(&open.view) { + ct += 1; + } + } + ct + } + + fn max_num_samples(&self) -> usize { + let mesh_control = match self.count_open_meshes() { + 0 | 1 => 1, + 2 => 10, + 3 => 100, + 4 => 1000, + _ => { + return 10; + } + }; + let mode_num_samples = match self.init_mode { + InitialMode::States => 10_000 - self.num_state_dims() * 1000, + InitialMode::Particle => 1_000, + InitialMode::Fractals => 10_000, + }; + mode_num_samples / mesh_control + } + + pub fn dimensionality(&self) -> DistributionDimensions { + match self.init_mode { + InitialMode::States => DistributionDimensions::State(self.num_state_dims()), + InitialMode::Particle => self.particle_mode.dimensionality(), + InitialMode::Fractals => self.fractal_mode.dimensionality(), + } + } + + pub fn ui(&mut self, ui: &mut Ui) { + group_horizontal(ui, |ui| { + let _ = combo_box(LABEL_INIT_MODE, &mut self.init_mode, ui, TIP_INIT_MODE); + let max_num_samples = self.max_num_samples(); + integer_slider( + LABEL_NUM_SAMPLES, + &mut self.num_samples, + max_num_samples, + ui, + TIP_NUM_SAMPLES, + ); + }); + + match self.init_mode { + InitialMode::States => { + group_horizontal(ui, |ui| { + self.initial_mode_data.states.num_states_selection_ui(ui); + }); + self.states_selection_ui(ui); + } + InitialMode::Particle => { + ui.horizontal(|ui| { + let _ = select_group(&mut self.particle_mode, ui, TIP_PARTICLE_MODE); + }); + self.particle_selection_ui(ui); + } + InitialMode::Fractals => { + ui.horizontal(|ui| { + let _ = select_group(&mut self.fractal_mode, ui, TIP_FRACTAL_MODE); + }); + self.fractal_selection_ui(ui); + } + }; + } + + fn states_selection_ui(&mut self, ui: &mut Ui) { + egui::ScrollArea::horizontal().show(ui, |ui| { + ui.vertical(|ui| { + self.initial_mode_data.states.selection_ui(ui); + }); + }); + } + + fn particle_selection_ui(&mut self, ui: &mut Ui) { + egui::ScrollArea::both().show(ui, |ui| { + ui.vertical(|ui| { + self.initial_mode_data + .particles + .selection_ui(self.particle_mode, ui); + }); + }); + } + fn fractal_selection_ui(&mut self, ui: &mut Ui) { + group_horizontal(ui, |ui| { + ui.heading(self.fractal_mode.element_basis()); + add_hyperlink( + "Info", + self.fractal_mode.reference(), + ui, + self.fractal_mode.tip_basic(), + ); + }); + egui::ScrollArea::horizontal().show(ui, |ui| { + ui.vertical(|ui| { + self.initial_mode_data + .fractals + .selection_ui(self.fractal_mode, ui); + }); + }); + } +} diff --git a/src/gui/egui_app/conf_panels/initial_distribution_view.rs b/src/gui/egui_app/conf_panels/initial_distribution_view.rs new file mode 100644 index 0000000..2a1888a --- /dev/null +++ b/src/gui/egui_app/conf_panels/initial_distribution_view.rs @@ -0,0 +1,163 @@ +use crate::gui::add_hyperlink; +use crate::chaos::data::{self as chaos_creation}; // avoid name collision +use crate::chaos::ChaosDescription; +use egui::{Response, Ui}; +use paste::paste; +use strum_macros::EnumIter; + +fn parameter_view( + par: &mut f64, + suffix: &str, + range: std::ops::RangeInclusive, + ui: &mut Ui, +) -> Response { + let response = ui.add( + egui::DragValue::new(par) + .speed(0.1) + .clamp_range(range) // Range inclusive + .suffix(format!(" {}", suffix)), + ); + response +} + +macro_rules! generate_initial_distribution_views { + ($($variant:ident { $($field:ident),* }),*) => { + paste!{ + + #[derive(PartialEq, Eq, Default, Copy, Clone, Debug, EnumIter)] + pub enum InitialDistributionView { + #[default] + $( + $variant, + )* + } + #[derive(PartialEq, Default)] + pub struct InitialDistributionViewData { + $( + pub [<$variant:lower>]: $variant, + )* + } + + impl InitialDistributionViewData { + pub fn map_initial_distribution_view_to_data( + &self, + view: &InitialDistributionView, + ) -> chaos_creation::InitialDistributionVariant { + match view { + $( + InitialDistributionView::$variant => self.[<$variant:lower>].to_initial_variant(), + )* + } + } + + pub fn view_ui(&mut self, view: InitialDistributionView, ui: &mut Ui) { + match view { + $( + InitialDistributionView::$variant => self.[<$variant:lower>].ui(ui), + )* + } + } + } + $( + #[derive(PartialEq, Default)] + pub struct $variant { + pub data: chaos_creation::$variant, + } + impl $variant { + fn to_initial_variant(&self) -> chaos_creation::InitialDistributionVariant { + chaos_creation::InitialDistributionVariant::$variant(self.data) + } + pub fn ui(&mut self, ui: &mut Ui) { + $( + let range = chaos_creation::$variant::[]; + let response = parameter_view(&mut self.data.$field, stringify!($field), range, ui); + if response.changed() { + self.data.par_range_check(); + }; + )* + add_hyperlink("Info", self.data.reference(), ui, self.data.description().as_str()); + } + } + )* + } // paste + }; +} + +generate_initial_distribution_views! { + Normal { mean, std_dev }, + Cauchy { median, scale }, + Uniform { low, high }, + Exponential { lambda }, + LogNormal { mean, std_dev }, + Poisson { mean }, + Pareto { scale, shape }, + StudentT { dof }, + Weibull { lambda, k }, + Gamma { shape, scale }, + Beta { alpha, beta }, + Triangular { low, high, mode }, + ChiSquared { dof }, + Fixed { value }, + Linspace { low, high }, + Mesh { start, end }, + Geomspace { start, end }, + Eye { value }, + Logspace { start, end, base } +} + +pub const INITIAL_MESHES: [InitialDistributionView; 1] = [InitialDistributionView::Mesh]; +pub const INITIAL_DETERMINISTIC: [InitialDistributionView; 5] = [ + InitialDistributionView::Fixed, + InitialDistributionView::Linspace, + InitialDistributionView::Geomspace, + InitialDistributionView::Eye, + InitialDistributionView::Logspace, +]; +pub const INITIAL_PROBABILISTIC: [InitialDistributionView; 13] = [ + InitialDistributionView::Normal, + InitialDistributionView::Cauchy, + InitialDistributionView::Uniform, + InitialDistributionView::Exponential, + InitialDistributionView::LogNormal, + InitialDistributionView::Poisson, + InitialDistributionView::Pareto, + InitialDistributionView::StudentT, + InitialDistributionView::Weibull, + InitialDistributionView::Gamma, + InitialDistributionView::Beta, + InitialDistributionView::Triangular, + InitialDistributionView::ChiSquared, +]; + +impl From for &'static str { + fn from(val: InitialDistributionView) -> Self { + match val { + InitialDistributionView::Normal => "Normal Distribution", + InitialDistributionView::Cauchy => "Cauchy Distribution", + InitialDistributionView::Uniform => "Uniform Distribution", + InitialDistributionView::Exponential => "Exponential Distribution", + InitialDistributionView::LogNormal => "Log-Normal Distribution", + InitialDistributionView::Poisson => "Poisson Distribution", + InitialDistributionView::Pareto => "Pareto Distribution", + InitialDistributionView::StudentT => "Student's t-Distribution", + InitialDistributionView::Weibull => "Weibull Distribution", + InitialDistributionView::Gamma => "Gamma Distribution", + InitialDistributionView::Beta => "Beta Distribution", + InitialDistributionView::Triangular => "Triangular Distribution", + InitialDistributionView::ChiSquared => "Chi-squared Distribution", + InitialDistributionView::Fixed => "Fixed Value", + InitialDistributionView::Linspace => "Linspace", + InitialDistributionView::Mesh => "(Hyper) Mesh", + InitialDistributionView::Geomspace => "Geometric Space", + InitialDistributionView::Eye => "Identity Matrix (Eye)", + InitialDistributionView::Logspace => "Logarithmic Space", + } + } +} + +impl From for String { + fn from(val: InitialDistributionView) -> Self { + let val_label: &'static str = val.into(); + String::from(val_label) // val.into() does panic ?! + } +} diff --git a/src/gui/egui_app/egui_utils.rs b/src/gui/egui_app/egui_utils.rs new file mode 100644 index 0000000..ca35e69 --- /dev/null +++ b/src/gui/egui_app/egui_utils.rs @@ -0,0 +1,181 @@ +use egui::{Align2, Color32, ComboBox, Context, InnerResponse, Ui, Window}; +use strum::IntoEnumIterator; +pub fn conf_window(title: &'static str, ctx: &Context, pivot: Align2) -> Window<'static> { + let default_pos = match pivot { + Align2::CENTER_TOP => ctx.screen_rect().center_top(), + Align2::LEFT_TOP => ctx.screen_rect().left_top(), + Align2::LEFT_BOTTOM => ctx.screen_rect().left_bottom(), + Align2::RIGHT_TOP => ctx.screen_rect().right_top(), + Align2::RIGHT_BOTTOM => ctx.screen_rect().right_bottom(), + _ => ctx.screen_rect().center(), + }; + Window::new(title) + .collapsible(true) + .pivot(pivot) + .fixed_pos(default_pos) + .default_open(true) + .resizable(true) +} +pub fn combo_box_from_string( + label: &str, + (current_value, current_label): (&mut E, String), + ui: &mut Ui, + variants: Vec<(E, String)>, + tooltip: &str, +) -> bool { + let mut changed = false; + ui.monospace(format!("{}:", label)).on_hover_text(tooltip); + ComboBox::from_id_source(label) + .selected_text(current_label) + .show_ui(ui, |ui| { + variants.into_iter().for_each(|(var, var_label)| { + let selection = ui.selectable_value(current_value, var, var_label); + if selection.changed() { + changed = true; + } + }) + }); + changed +} +pub fn combo_box + IntoEnumIterator + std::cmp::PartialEq>( + label: &str, + current_value: &mut E, + ui: &mut Ui, + tooltip: &str, +) -> bool { + let all_variants: Vec = E::iter().collect(); + let mut changed = false; + ui.monospace(format!("{}:", label)).on_hover_text(tooltip); + let current_label: &'static str = (*current_value).into(); + ComboBox::from_id_source(label) + .selected_text(current_label) + .show_ui(ui, |ui| { + variants_selection(&all_variants, ui, current_value, &mut changed) + }); + changed +} + +pub fn select_group_filtered + std::cmp::PartialEq>( + current_value: &mut E, + ui: &mut Ui, + filter_variants: &[E], + tooltip: &str, +) -> bool { + let mut changed = false; + ui.group(|ui| { + variants_selection(filter_variants, ui, current_value, &mut changed); + }) + .response + .on_hover_text(tooltip); + changed +} + +fn variants_selection + std::cmp::PartialEq>( + variants: &[E], + ui: &mut Ui, + current_value: &mut E, + changed: &mut bool, +) { + variants.iter().cloned().for_each(|var| { + let var_label: &'static str = var.into(); + let selection = ui.selectable_value(current_value, var, var_label); + if selection.changed() { + *changed = true; + } + }) +} +pub fn select_group + IntoEnumIterator + std::cmp::PartialEq>( + current_value: &mut E, + ui: &mut Ui, + tooltip: &str, +) -> bool { + let all_variants: Vec = E::iter().collect(); + select_group_filtered(current_value, ui, &all_variants, tooltip) +} + +pub fn integer_slider( + label: &str, + current_value: &mut usize, + upper_limit: usize, + ui: &mut Ui, + tooltip: &str, +) -> bool { + ui.add( + egui::Slider::new(current_value, 1..=upper_limit) + .clamp_to_range(true) + .step_by(1.0) + .integer() + .suffix(format!(" {}", label)), + ) + .on_hover_text(tooltip) + .changed() +} +pub fn float_slider( + label: &str, + current_value: &mut f64, + upper_limit: f64, + ui: &mut Ui, + tooltip: &str, +) -> bool { + ui.add( + egui::Slider::new(current_value, (0.0)..=upper_limit) + .clamp_to_range(true) + .step_by(0.1) + .suffix(format!(" {}", label)), + ) + .on_hover_text(tooltip) + .changed() +} +pub fn clickable_button( + label: &str, + selected: bool, + enabled: bool, + ui: &mut Ui, + tooltip: &str, +) -> bool { + let (bg_color, text_color) = if ui.visuals().dark_mode { + (Color32::DARK_GRAY, Color32::RED) + } else { + (Color32::GOLD, Color32::BLACK) + }; + let widget = egui::Button::new(label).fill(bg_color).selected(selected); + ui.add_enabled_ui(enabled, |ui| { + ui.visuals_mut().override_text_color = Some(text_color); + ui.add(widget) + }) + .inner + .on_hover_text(tooltip) + .clicked() +} +pub fn add_hyperlink(label: &str, link: &str, ui: &mut Ui, tooltip: &str) { + ui.hyperlink_to(label, link).on_hover_ui(|ui| { + ui.vertical(|ui| { + ui.label(tooltip); + ui.horizontal(|ui| { + ui.label("Reference: "); + ui.hyperlink(link); + }); + }); + }); +} +pub fn add_label(label: &str, ui: &mut Ui, tooltip: &str) { + ui.monospace(label).on_hover_text(tooltip); +} +pub fn add_checkbox(label: &str, value: &mut bool, ui: &mut Ui, tooltip: &str) -> bool { + let widget = egui::Checkbox::new(value, label); + ui.add(widget).on_hover_text(tooltip).clicked() +} + +pub fn group_horizontal( + ui: &mut Ui, + f: impl FnOnce(&mut Ui) -> R, +) -> egui::InnerResponse> { + ui.group(|ui| ui.horizontal(|ui| f(ui))) +} + +pub fn group_vertical( + ui: &mut Ui, + f: impl FnOnce(&mut Ui) -> R, +) -> egui::InnerResponse> { + ui.group(|ui| ui.vertical(|ui| f(ui))) +} diff --git a/src/gui/egui_app/main_panels.rs b/src/gui/egui_app/main_panels.rs new file mode 100644 index 0000000..5391935 --- /dev/null +++ b/src/gui/egui_app/main_panels.rs @@ -0,0 +1,21 @@ +mod benchmark; +mod chaotic_plot; + +pub use benchmark::BenchmarkPanel; +pub use chaotic_plot::PlotPanel; +use strum_macros::EnumIter; +#[derive(PartialEq, Eq, Default, Clone, Copy, EnumIter)] +pub enum MainPanel { + #[default] + ChaoticPlot, + Benchmark, +} + +impl From for &'static str { + fn from(val: MainPanel) -> Self { + match val { + MainPanel::ChaoticPlot => "Plot", + MainPanel::Benchmark => "Benchmark", + } + } +} diff --git a/src/gui/egui_app/main_panels/benchmark.rs b/src/gui/egui_app/main_panels/benchmark.rs new file mode 100644 index 0000000..3fb01a6 --- /dev/null +++ b/src/gui/egui_app/main_panels/benchmark.rs @@ -0,0 +1,217 @@ +use crate::gui::tooltips::*; +use crate::gui::*; +use crate::chaos::benchmark::*; +use egui::Ui; +use egui_plot::{HLine, Plot, PlotPoint, PlotPoints, Points, VLine}; + +pub struct BenchmarkPanel { + run_benchmark: bool, + benchmark_result: ChaosBenchmarkResult, + bench_config: ChaosInitSchema, + use_warmup: bool, + num_iterations: usize, + num_warmups: usize, +} +impl Default for BenchmarkPanel { + fn default() -> Self { + Self { + run_benchmark: false, + benchmark_result: Default::default(), + bench_config: Default::default(), + use_warmup: true, + num_iterations: 50, + num_warmups: 2, + } + } +} + +impl BenchmarkPanel { + pub fn benchmark_toggle(&mut self) -> bool { + if self.run_benchmark { + self.run_benchmark = false; + true + } else { + false + } + } + + pub fn chaos_benchmark(&mut self, bench_config: ChaosInitSchema) { + self.bench_config = bench_config; + let num_warmups = if self.use_warmup { self.num_warmups } else { 0 }; + self.benchmark_result = + chaos_benchmark(&self.bench_config, self.num_iterations, num_warmups); + } + + fn valid_runtimes(&self) -> (Vec, usize) { + let num_warmups = self.benchmark_result.number_of_warmups(); + let mut failed_warmups = 0; + let runtimes = self + .benchmark_result + .runs() + .iter() + .enumerate() + .filter_map(|(i, res)| match res { + Ok(run) => Some(run.runtime_nanos() as f64), + Err(_) => { + if i < num_warmups { + failed_warmups += 1; + } + None + } + }) + .collect(); + let valid_warmups = num_warmups - failed_warmups; + (runtimes, valid_warmups) + } + pub fn conf_ui(&mut self, is_ready: bool, ui: &mut Ui) { + ui.heading("Benchmark Configuration"); + group_horizontal(ui, |ui| { + add_checkbox(LABEL_WARMUP, &mut self.use_warmup, ui, TIP_WARMUP); + if self.use_warmup { + integer_slider( + LABEL_NUM_WARMUPS, + &mut self.num_warmups, + 50, + ui, + TIP_NUM_WARMUPS, + ); + } + }); + group_horizontal(ui, |ui| { + integer_slider( + LABEL_NUM_ITERATIONS, + &mut self.num_iterations, + 200, + ui, + TIP_NUM_ITERATIONS, + ); + if clickable_button(LABEL_BENCHMARK, false, is_ready, ui, TIP_BENCHMARK) { + self.run_benchmark = true; + }; + }); + } + fn show_summary(&self, ui: &mut Ui) { + let ChaosInitSchema { + num_samples, + num_executions, + init_distr, + discrete_map_vec, + diff_system_vec, + pars, + } = &self.bench_config; + let (parameter, par_values) = pars; + group_vertical(ui, |ui| { + ui.heading("Latest Benchmark Summary"); + group_horizontal(ui, |ui| { + ui.label(format!("Number of Samples: {num_samples}",)); + ui.label(format!("Number of Executions: {num_executions}")); + }); + group_horizontal(ui, |ui| { + if let Some(map_vec) = discrete_map_vec { + let map_str: &'static str = map_vec.into(); + ui.label(format!("Discrete Map: {map_str}")); + }; + if let Some(diff_system_vec) = diff_system_vec { + let diff_system_str: &'static str = diff_system_vec.into(); + ui.label(format!("Differential System: {diff_system_str}")); + }; + if !par_values.is_empty() { + ui.label(format!("{} Parameter {parameter} values", par_values.len())); + }; + }); + ui.label(format!("Distributions: {}", String::from(init_distr))); + }); + } + + fn show_results(&self, ui: &mut Ui) { + let num_warmups = self.benchmark_result.number_of_warmups(); + ui.collapsing("Results", |ui| { + let result_labels: Vec = self + .benchmark_result + .runs() + .iter() + .enumerate() + .map(|(i, res)| { + let warmup_tag = if i < num_warmups { "!" } else { "" }; + match res { + Ok(run) => { + let runtime = run.runtime_millis(); + let num_states = run.num_valid_end_states(); + format!("{i} {warmup_tag} States: {num_states} Runtime: {runtime} ms") + } + Err(e) => format!("{i} {warmup_tag} Error: {e}"), + } + }) + .collect(); + egui::ScrollArea::vertical().show(ui, |ui| { + group_vertical(ui, |ui| { + result_labels.into_iter().for_each(|l| { + ui.label(l); + }) + }); + }); + }); + + let (runtimes, num_valid_warmups) = self.valid_runtimes(); + if !runtimes.is_empty() { + let runtime_mean = { + let (_, valid_runtimes) = runtimes.split_at(num_valid_warmups); + let num_valid_runtimes = valid_runtimes.len(); + if num_valid_runtimes == 0 { + 0.0 + } else { + let mut runtime_sum = 0.0; + for runtime in valid_runtimes { + runtime_sum += *runtime; + } + runtime_sum / (num_valid_runtimes as f64) + } + }; + group_horizontal(ui, |ui| { + ui.label(format!( + "Mean: {runtime_mean} ns Valid warmups: {num_valid_warmups}" + )); + }); + let plot = Plot::new("bench_plot") + .set_margin_fraction(egui::Vec2::new(0.01, 0.01)) + .legend(Default::default()); + plot.show(ui, |plot_ui| { + let points = runtimes + .iter() + .enumerate() + .map(|(i, y)| PlotPoint::new(i as f64, *y)) + .collect(); + plot_ui.points(Points::new(PlotPoints::Owned(points)).name("Runtime ns")); + if num_valid_warmups > 0 { + plot_ui.vline(VLine::new((num_valid_warmups - 1) as f64).name("Warm-Ups")); + }; + plot_ui.hline(HLine::new(runtime_mean).name("Mean")); + }); + } + } + pub fn ui(&mut self, ui: &mut Ui) { + self.show_summary(ui); + self.show_results(ui); + powered_by_egui_and_eframe(ui); + } +} +fn powered_by_egui_and_eframe(ui: &mut egui::Ui) { + ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { + ui.add(egui::github_link_file!( + "https://github.com/tomtuamnuq/egui-chaos/blob/master/", + "Source code." + )); + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.label("Powered by "); + ui.hyperlink_to("egui", "https://github.com/emilk/egui"); + ui.label(" and "); + ui.hyperlink_to( + "eframe", + "https://github.com/emilk/egui/tree/master/crates/eframe", + ); + ui.label("."); + }); + egui::warn_if_debug_build(ui); + }); +} diff --git a/src/gui/egui_app/main_panels/chaotic_plot.rs b/src/gui/egui_app/main_panels/chaotic_plot.rs new file mode 100644 index 0000000..adb1767 --- /dev/null +++ b/src/gui/egui_app/main_panels/chaotic_plot.rs @@ -0,0 +1,195 @@ +use crate::gui::plot::*; +use crate::gui::tooltips::*; +use crate::gui::*; +use crate::chaos::data::ChaosDataVec; +use crate::utils::Timer; +use egui::Ui; + +pub struct PlotPanel { + pub generate_new_data: bool, + reinit_data: bool, + plot_2_d: Plot2D, + plot_3_d: Plot3D, + plot_backend: PlotBackendVariant, + save_trajectory: bool, + max_num_series: usize, + point_colormap: SeriesColors, + frame_rate: usize, + timer: Timer, +} + +const MAX_NUM_SERIES: usize = 20; +impl Default for PlotPanel { + fn default() -> Self { + Self { + generate_new_data: false, + reinit_data: false, + plot_2_d: Default::default(), + plot_3_d: Default::default(), + plot_backend: Default::default(), + save_trajectory: true, + max_num_series: MAX_NUM_SERIES, + point_colormap: Default::default(), + frame_rate: 1, + timer: Default::default(), + } + } +} +impl PlotPanel { + pub fn reinit_data(&self) -> bool { + self.reinit_data + } + pub fn add_point_series(&mut self, data: ChaosDataVec<'_>) { + match self.plot_backend { + PlotBackendVariant::EguiPlot2D => { + self.plot_2_d.set_point_colormap(self.point_colormap); + self.plot_2_d.add_point_series(data); + } + PlotBackendVariant::Plotters => { + self.plot_3_d.set_point_colormap(self.point_colormap); + self.plot_3_d.add_point_series(data); + } + } + } + + pub fn reset_plot_trajectory(&mut self) { + self.plot_2_d.reset_data(); + self.plot_3_d.reset_data(); + } + + fn set_plot_trajectory_steps(&mut self) { + self.plot_2_d.set_max_num_series(self.max_num_series); + self.plot_3_d.set_max_num_series(self.max_num_series); + } + + pub fn set_no_parametrized_plotting(&mut self) { + self.plot_2_d.remove_parameter(); + self.plot_3_d.remove_parameter(); + } + + pub fn set_parametrized_plotting(&mut self, par: &'static str, par_values: Vec) { + self.plot_2_d.set_parameter(par, par_values.to_owned()); + self.plot_3_d.set_parameter(par, par_values); + } + + pub fn check_frame_rate(&mut self) -> bool { + self.timer.check_elapsed() + } + + fn add_general_plot_options(&mut self, ui: &mut Ui) { + ui.horizontal(|ui| { + if combo_box( + LABEL_PLOT_BACKEND, + &mut self.plot_backend, + ui, + TIP_PLOT_BACKEND, + ) { + self.reset_plot_trajectory(); + }; + }); + ui.horizontal(|ui| { + if clickable_button( + LABEL_INIT_DATA, + self.generate_new_data, + true, + ui, + TIP_INIT_DATA, + ) { + self.generate_new_data = true; + } + add_checkbox( + LABEL_REINIT_DATA, + &mut self.reinit_data, + ui, + TIP_REINIT_DATA, + ); + }); + ui.horizontal(|ui| { + if integer_slider( + LABEL_NUM_FRAMES, + &mut self.frame_rate, + 50, + ui, + TIP_NUM_FRAMES, + ) { + self.timer.set_frequency(self.frame_rate as f64); + }; + }); + ui.horizontal(|ui| { + if add_checkbox( + LABEL_TRAJECTORY, + &mut self.save_trajectory, + ui, + TIP_TRAJECTORY, + ) { + if !self.save_trajectory { + self.max_num_series = 1; + self.set_plot_trajectory_steps(); + } else { + self.max_num_series = MAX_NUM_SERIES; + self.set_plot_trajectory_steps(); + } + } + if self.save_trajectory + && integer_slider( + LABEL_NUM_SERIES, + &mut self.max_num_series, + 100, + ui, + TIP_NUM_SERIES, + ) + { + self.set_plot_trajectory_steps(); + } + }); + ui.horizontal(|ui| { + combo_box(LABEL_COLORMAP, &mut self.point_colormap, ui, TIP_COLORMAP); + }); + ui.horizontal(|ui| { + let color_choice = match self.plot_backend { + PlotBackendVariant::EguiPlot2D => self.plot_2_d.series_color_mut(), + PlotBackendVariant::Plotters => self.plot_3_d.series_color_mut(), + }; + combo_box(LABEL_COLOR_PER_POINT, color_choice, ui, TIP_COLOR_PER_POINT); + }); + } + + pub fn conf_ui(&mut self, ui: &mut Ui) { + group_vertical(ui, |ui| { + ui.heading("Plot Configuration"); + self.add_general_plot_options(ui); + }); + egui::ScrollArea::vertical().show(ui, |ui| { + ui.collapsing("Plot Visualization Info", |ui| { + group_vertical(ui, |ui| { + self.add_plot_explanation(ui); + }); + }); + group_vertical(ui, |ui| { + ui.heading("State Projections"); + self.add_plot_backend_options(ui); + }); + }); + } + + fn add_plot_backend_options(&mut self, ui: &mut Ui) { + match self.plot_backend { + PlotBackendVariant::EguiPlot2D => self.plot_2_d.options_ui(ui), + PlotBackendVariant::Plotters => self.plot_3_d.options_ui(ui), + }; + } + fn add_plot_explanation(&mut self, ui: &mut Ui) { + match self.plot_backend { + PlotBackendVariant::EguiPlot2D => self.plot_2_d.explanation(ui), + PlotBackendVariant::Plotters => self.plot_3_d.explanation(ui), + }; + } + + pub fn ui(&mut self, mouse_is_over_plot: bool, ui: &mut Ui) { + ui.ctx().request_repaint(); // animate + match self.plot_backend { + PlotBackendVariant::EguiPlot2D => self.plot_2_d.ui(ui), + PlotBackendVariant::Plotters => self.plot_3_d.ui(mouse_is_over_plot, ui), + }; + } +} diff --git a/src/gui/plot.rs b/src/gui/plot.rs new file mode 100644 index 0000000..964c019 --- /dev/null +++ b/src/gui/plot.rs @@ -0,0 +1,26 @@ +mod plot_2_d; +mod plot_3_d; +mod plot_backend; +mod plot_colors; +mod plot_data_variants; +mod plot_styles; +mod plot_utils; +pub use plot_2_d::Plot2D; +pub use plot_3_d::Plot3D; +pub use plot_colors::SeriesColors; +use strum_macros::EnumIter; + +#[derive(PartialEq, Eq, Default, Clone, Copy, EnumIter)] +pub enum PlotBackendVariant { + #[default] + EguiPlot2D, + Plotters, +} +impl From for &'static str { + fn from(val: PlotBackendVariant) -> Self { + match val { + PlotBackendVariant::EguiPlot2D => "Egui 2D", + PlotBackendVariant::Plotters => "Plotters 3D", + } + } +} diff --git a/src/gui/plot/plot_2_d.rs b/src/gui/plot/plot_2_d.rs new file mode 100644 index 0000000..820ebb5 --- /dev/null +++ b/src/gui/plot/plot_2_d.rs @@ -0,0 +1,493 @@ +use crate::gui::{float_slider, group_horizontal, tooltips::*, PARAMETER_MAX, PARAMETER_MIN}; +use crate::chaos::data::*; +use delegate::delegate; +use egui::Ui; +use egui::{Color32, Shape, Stroke}; +use egui_plot::{format_number, log_grid_spacer, Plot, PlotPoint, PlotPoints, PlotUi, Points}; + +use super::plot_backend::PlotBackend; +use super::plot_colors::{FromRGB, SeriesColorChoice, SeriesColors, RGB}; +use super::plot_utils::{StateProjection, StateProjectionSelection, MAX_NUM_PROJECTIONS}; + +use super::plot_styles::DEFAULT_RADIUS; +pub type Point2D = PlotPoint; +pub type Points2D = Vec>; +impl FromRGB for Color32 { + fn from_rgb(rgb: RGB) -> Self { + Color32::from_rgb(rgb.0, rgb.1, rgb.2) + } +} + +pub struct Plot2D { + plot_backend: PlotBackend, + projection_x: StateProjection, + selection_x: StateProjectionSelection, + projection_y: StateProjection, + selection_y: StateProjectionSelection, + selection_color: StateProjectionSelection, + mean_number_of_shapes_guess: usize, + point_size: f64, +} + +impl PartialEq for Plot2D { + fn eq(&self, other: &Self) -> bool { + self.projection_x == other.projection_x + && self.projection_y == other.projection_y + && self.plot_backend.get_parameter() == other.plot_backend.get_parameter() + && self.plot_backend.get_parameter_values() == other.plot_backend.get_parameter_values() + } +} + +impl Eq for Plot2D {} + +impl Default for Plot2D { + fn default() -> Self { + Self { + plot_backend: Default::default(), + // chaos app starts without params and 2 states + projection_x: StateProjection::S(0), + selection_x: StateProjectionSelection::S0, + projection_y: StateProjection::S(1), + selection_y: StateProjectionSelection::S1, + selection_color: StateProjectionSelection::S0, + mean_number_of_shapes_guess: 10, + point_size: DEFAULT_RADIUS, + } + } +} + +impl Plot2D { + fn parameters_are_shown(&self) -> bool { + self.selection_x == StateProjectionSelection::Par + } + pub fn set_parameter(&mut self, parameter: &'static str, par_values: Vec) { + let had_parameter = self.plot_backend.with_parameter(); + self.plot_backend.set_parameter(parameter, par_values); + if !had_parameter { + self.reset_projections(); + } else if self.parameters_are_shown() { + self.projection_x = StateProjection::Par(parameter) + } + } + pub fn remove_parameter(&mut self) { + if self.plot_backend.with_parameter() { + self.plot_backend.remove_parameter(); + self.reset_projections(); + } + } + + fn reset_projections(&mut self) { + let dim = self.number_of_dimensions(); + let projection_color = if let Some(p) = self.plot_backend.get_parameter() { + self.projection_x = StateProjection::Par(p); + self.projection_y = StateProjection::S(0); + if dim > 1 { + StateProjection::S(1) + } else { + StateProjection::S(0) + } + } else { + self.projection_x = StateProjection::S(0); + self.projection_y = StateProjection::S(1); + if dim > 2 { + StateProjection::S(2) + } else { + StateProjection::S(0) + } + }; + self.plot_backend.set_projection_color(projection_color); + self.selection_color = StateProjectionSelection::from(projection_color); + self.selection_x = StateProjectionSelection::from(self.projection_x); + self.selection_y = StateProjectionSelection::from(self.projection_y); + } + + pub fn add_point_series(&mut self, data: ChaosDataVec<'_>) { + let dimensionality = data.dimensionality(); + if dimensionality != *self.plot_backend.dimensionality() { + self.plot_backend.remove_parameter(); + self.plot_backend.set_dimensionality(dimensionality); + self.reset_projections(); + } + let styles = self.plot_backend.create_styles_for_chaos_data(&data); + let series = if self.parameters_are_shown() { + self.create_point_series_with_parameters(data) + } else { + self.create_point_series_without_parameters(data) + }; + let extrema = Self::get_extrema_from_series(&series); + self.plot_backend.add_series(series, styles, extrema); + } + + pub fn transform_points_1_d(&self, states: &[Option]) -> Points2D { + match self.plot_backend.latest_series() { + None => states + .iter() + .map(|v| v.map(|v| PlotPoint::new(0.0, v[0]))) + .collect(), + Some((last_states, _)) => states + .iter() + .zip(last_states.iter()) + .map(|(new_state, last_state)| { + new_state.map(|new_state| { + let new_y = new_state[0]; + if let Some(last_state) = last_state { + PlotPoint::new(last_state.y, new_y) + } else { + // new_state was reinitialized + PlotPoint::new(0.0, new_y) + } + }) + }) + .collect(), + } + } + pub fn transform_points_n_d(&self, states: &[Option]) -> Points2D { + let (ind_x, ind_y) = (self.projection_x.index(), self.projection_y.index()); + states + .iter() + .map(|v| { + v.as_ref() + .map(|v| PlotPoint::new(v.ind(ind_x), v.ind(ind_y))) + }) + .collect() + } + + pub fn points_with_parameter_n_d( + &self, + states: &[Option], + par: &f64, + ) -> Points2D { + let ind_y = self.projection_y.index(); + states + .iter() + .map(|v| v.as_ref().map(|v| PlotPoint::new(*par, v.ind(ind_y)))) + .collect() + } + + const SQRT_3: f32 = 1.732_050_8; // 3_f32.sqrt() = 1.73205080757; + const FRAC_1_SQRT_2: f32 = std::f32::consts::FRAC_1_SQRT_2; // 1.0 / 2_f32.sqrt(); + + fn get_shapes_for_all_states(&self, plot_ui: &mut PlotUi) -> Vec { + let n = self.plot_backend.num_series() as f32; + let point_size = (self.point_size as f32) / n; + self.plot_backend + .styled_series_iter() + .enumerate() + .flat_map(|(i, (points, styles))| { + let i = (i + 1) as f32; + let radius = i * point_size; + styles + .iter() + .zip(points.iter().filter_map(|p| p.as_ref())) + .map(|(s, p)| { + let center = plot_ui.screen_from_plot(*p); // in screen coords + Shape::circle_filled(center, radius, s.color) + }) + .collect::>() + }) + .collect() + } + fn get_shapes_for_all_particles( + &mut self, + default_fill: Color32, + plot_ui: &mut PlotUi, + ) -> Vec { + let num_series = self.plot_backend.num_series(); + let mut shapes = Vec::with_capacity(num_series * self.mean_number_of_shapes_guess); + let screen_translate = plot_ui.transform().dpos_dvalue_x() as f32; + self.plot_backend + .all_styles_and_points_iter() + .for_each(|(s, p)| { + let center = plot_ui.screen_from_plot(*p); // in screen coords + let stroke = Stroke::new(1.0, s.color); + let radius = (s.radius as f32) * screen_translate; // screen sized + shapes.push(Shape::circle_stroke(center, radius, stroke)); + // copied from: + // https://github.com/emilk/egui/blob/a815923717365b2e49b18d238f5dc2b72d023ee0/crates/egui_plot/src/items/mod.rs#L903 + let tf = |dx: f32, dy: f32| -> egui::Pos2 { center + radius * egui::vec2(dx, dy) }; + if s.markers.positive { + let points = vec![ + tf(0.0, -1.0), + tf(0.5 * Self::SQRT_3, 0.5), + tf(-0.5 * Self::SQRT_3, 0.5), + ]; + shapes.push(Shape::convex_polygon(points, default_fill, stroke)); + } + if s.markers.negative { + let points = vec![ + tf(0.0, 1.0), + tf(-0.5 * Self::SQRT_3, -0.5), + tf(0.5 * Self::SQRT_3, -0.5), + ]; + shapes.push(Shape::convex_polygon(points, default_fill, stroke)); + } + if s.markers.special { + let diagonal1 = [ + tf(-Self::FRAC_1_SQRT_2, -Self::FRAC_1_SQRT_2), + tf(Self::FRAC_1_SQRT_2, Self::FRAC_1_SQRT_2), + ]; + let diagonal2 = [ + tf(Self::FRAC_1_SQRT_2, -Self::FRAC_1_SQRT_2), + tf(-Self::FRAC_1_SQRT_2, Self::FRAC_1_SQRT_2), + ]; + shapes.push(Shape::line_segment(diagonal1, stroke)); + shapes.push(Shape::line_segment(diagonal2, stroke)); + } + }); + self.mean_number_of_shapes_guess = shapes.len().div_ceil(num_series); + shapes + } + + fn get_shapes_for_fractal(&self, plot_ui: &mut PlotUi) -> Vec { + let (special_color, positive_color) = ( + self.plot_backend.special_color(), + self.plot_backend.positive_color(), + ); + let fractal_size = self.point_size as f32; + self.plot_backend + .all_styles_and_points_iter() + .map(|(s, p)| { + let center = plot_ui.screen_from_plot(*p); // in screen coords + if s.markers.special { + Shape::circle_filled(center, fractal_size / 2.0, special_color) + } else if s.markers.positive { + Shape::circle_filled(center, fractal_size / 2.0, positive_color) + } else if s.markers.negative { + let stroke = Stroke::new(fractal_size / 4.0, s.color); + Shape::circle_stroke(center, fractal_size / 2.0, stroke) + } else { + Shape::circle_filled(center, fractal_size, s.color) + } + }) + .collect::>() + } + + fn get_extrema_from_series(points: &Points2D) -> (Point2D, Point2D) { + let (mut x_min, mut x_max, mut y_min, mut y_max) = + (VALID_MAX, VALID_MIN, VALID_MAX, VALID_MIN); + points.iter().for_each(|p| { + if let Some(p) = p { + x_min = x_min.min(p.x); + x_max = x_max.max(p.x); + y_min = y_min.min(p.y); + y_max = y_max.max(p.y); + } + }); + (Point2D::new(x_min, y_min), Point2D::new(x_max, y_max)) + } + + fn set_bounds_from_points(&self, plot_ui: &mut PlotUi) { + let mut extrema = Vec::with_capacity(self.plot_backend.num_series() * 2); + self.plot_backend.extrema_iter().for_each(|(p_min, p_max)| { + extrema.push(*p_min); + extrema.push(*p_max); + }); + let bounds = PlotPoints::Owned(extrema); + plot_ui.points(Points::new(bounds).radius(0.01)); + } + + pub fn explanation(&self, ui: &mut Ui) { + if self.plot_backend.with_parameter() { + let param_select_label = if self.parameters_are_shown() { + LABEL_PARAMS_SHOWN + } else { + LABEL_PARAMS_NOT_SHOWN + }; + ui.label(param_select_label); + }; + let distribution_label = if self.parameters_are_shown() { + match self.plot_backend.dimensionality() { + DistributionDimensions::State(n) => { + if *n == 1 { + LABEL_PLOT2D_PAR_STATE_1 + } else { + LABEL_PLOT2D_PAR_STATE_N + } + } + DistributionDimensions::Particle(_) => LABEL_PLOT_PAR_PARTICLE, + DistributionDimensions::Fractal(_) => LABEL_PLOT2D_PAR_FRACTAL, + } + } else { + match self.plot_backend.dimensionality() { + DistributionDimensions::State(n) => { + if *n == 1 { + LABEL_PLOT2D_STATE_1 + } else { + LABEL_PLOT_STATE_N + } + } + DistributionDimensions::Particle(_) => LABEL_PLOT2D_PARTICLE, + DistributionDimensions::Fractal(_) => LABEL_PLOT2D_FRACTAL, + } + }; + ui.label(distribution_label); + } + + pub fn options_ui(&mut self, ui: &mut Ui) { + let dims = self.plot_backend.dimensionality().clone(); + let num_dims = dims.number_of_dimensions(); + let mut projection_vars_to_show = Vec::with_capacity(MAX_NUM_PROJECTIONS); + let par = self.plot_backend.get_parameter(); + if let Some(p) = par { + projection_vars_to_show.push(StateProjection::Par(p)); + } + StateProjection::add_state_projection_vars(num_dims, &mut projection_vars_to_show); + group_horizontal(ui, |ui| { + let has_x_selected = StateProjection::projection_vars_selection( + "X", + self.projection_x.mode_string_choice(&dims), + &mut self.selection_x, + &projection_vars_to_show, + &dims, + ui, + ); + if has_x_selected { + self.projection_x = if self.parameters_are_shown() { + StateProjection::Par(par.unwrap()) + } else { + StateProjection::state(self.selection_x) + } + } + projection_vars_to_show.clear(); + StateProjection::add_state_projection_vars(num_dims, &mut projection_vars_to_show); + if num_dims > 1 { + let has_y_selected = StateProjection::projection_vars_selection( + "Y", + self.projection_y.mode_string_choice(&dims), + &mut self.selection_y, + &projection_vars_to_show, + &dims, + ui, + ); + if has_y_selected { + self.projection_y = StateProjection::state(self.selection_y); + } + } else if has_x_selected { + self.plot_backend.clear(); + } + }); + if let Some(p) = par { + projection_vars_to_show.push(StateProjection::Par(p)); + } + group_horizontal(ui, |ui| { + let has_color_selected = StateProjection::projection_vars_selection( + "Color", + self.plot_backend + .projection_color() + .mode_string_choice(&dims), + &mut self.selection_color, + &projection_vars_to_show, + &dims, + ui, + ); + if has_color_selected { + let projection_color = if self.selection_color == StateProjectionSelection::Par { + StateProjection::Par(par.unwrap()) + } else { + StateProjection::state(self.selection_color) + }; + self.plot_backend.set_projection_color(projection_color); + } + }); + if let DistributionDimensions::Particle(_) = dims { + } else { + group_horizontal(ui, |ui| { + float_slider( + LABEL_POINT_SIZE, + &mut self.point_size, + 10.0, + ui, + TIP_POINT_SIZE, + ); + }); + } + } + + pub fn ui(&mut self, ui: &mut Ui) { + let plot = self.axis_configured_plot(); + let num_series = self.plot_backend.num_series(); + if num_series > 0 { + let default_fill_color = if ui.visuals().dark_mode { + Color32::DARK_BLUE + } else { + Color32::LIGHT_BLUE + }; + // add all shapes to the Vec and draw at once + let mut shapes: Vec = Vec::new(); + let egui::Response { rect, .. } = plot + .show(ui, |plot_ui| { + // set the auto-bounds functionality in plot_ui since we draw directly to screen + self.set_bounds_from_points(plot_ui); + shapes = match self.plot_backend.dimensionality() { + DistributionDimensions::State(_) => self.get_shapes_for_all_states(plot_ui), + DistributionDimensions::Particle(_) => { + self.get_shapes_for_all_particles(default_fill_color, plot_ui) + } + DistributionDimensions::Fractal(_) => self.get_shapes_for_fractal(plot_ui), + }; + }) + .response; + // ctx.layer_painter(layer_id).extend(shapes); // avoids the clipping so that points overlay the options etc. + ui.painter().with_clip_rect(rect).extend(shapes); + } + } + + fn axis_configured_plot(&self) -> Plot { + let (x_min, x_max) = if self.parameters_are_shown() { + (2.0 * PARAMETER_MIN, 2.0 * PARAMETER_MAX) + } else { + (VALID_MIN, VALID_MAX) + }; + let (y_min, y_max) = (VALID_MIN, VALID_MAX); + let dims = self.plot_backend.dimensionality(); + let (x_label, y_label) = if let DistributionDimensions::State(1) = dims { + (String::from("S'"), String::from("S")) + } else { + ( + self.projection_x.mode_string_axis(dims), + self.projection_y.mode_string_axis(dims), + ) + }; + + let mut plot = Plot::new("plot_2_d") + .set_margin_fraction(egui::Vec2::new(0.01, 0.01)) + .x_grid_spacer(log_grid_spacer(2)) + .x_axis_formatter(move |x, _, range| { + if *range.start() <= x_min || *range.end() >= x_max { + "".to_string() + } else if x.abs() > 0.1 { + format_number(x, 1).to_string() + } else { + format!("{}={}", x_label, format_number(x, 1)) + } + }) + .y_grid_spacer(log_grid_spacer(2)) + .y_axis_formatter(move |y, _, range| { + if *range.start() <= y_min || *range.end() >= y_max { + "".to_string() + } else if y.abs() > 0.1 { + format_number(y, 1).to_string() + } else { + format!("{}={}", y_label, format_number(y, 1)) + } + }); + if !self.parameters_are_shown() { + plot = plot.data_aspect(1.0); + } + plot + } + + delegate! { + to self.plot_backend{ + pub fn series_color_mut(&mut self)-> &mut SeriesColorChoice; + pub fn number_of_dimensions(&self) -> usize; + pub fn get_parameter_values(&self) -> &Vec; + #[call(clear)] + pub fn reset_data(&mut self); + #[call(set_max_series)] + pub fn set_max_num_series(&mut self, max_num_series: usize); + #[call(set_colormap)] + pub fn set_point_colormap(&mut self, colormap: SeriesColors); + } + } +} diff --git a/src/gui/plot/plot_3_d.rs b/src/gui/plot/plot_3_d.rs new file mode 100644 index 0000000..8211dc6 --- /dev/null +++ b/src/gui/plot/plot_3_d.rs @@ -0,0 +1,691 @@ +use delegate::delegate; +use egui::Ui; +use egui_plotter::{Chart, EguiBackend, MouseConfig}; + +use plotters::coord::ranged3d::Cartesian3d; +use plotters::coord::types::RangedCoordf64; +use plotters::prelude::*; + +use crate::gui::tooltips::*; +use crate::gui::*; +use crate::chaos::data::*; +use std::ops::Range; + +use super::plot_backend::PlotBackend; +use super::plot_colors::{FromRGB, SeriesColorChoice, SeriesColors, RGB}; +use super::plot_utils::{StateProjection, StateProjectionSelection, MAX_NUM_PROJECTIONS}; + +pub type Point3D = (ChaosFloat, ChaosFloat, ChaosFloat); +pub type Points3D = Vec>; +type BackendData = PlotBackend; +type Chart3D<'a, 'b> = + ChartContext<'a, EguiBackend<'b>, Cartesian3d>; +struct AxisData { + pub x_label: String, + pub y_label: String, + pub z_label: String, +} +impl Default for AxisData { + fn default() -> Self { + Self { + x_label: String::from("x"), + y_label: String::from("y"), + z_label: String::from("z"), + } + } +} +struct Options3D { + pub point_size: f64, + pub point_opacity: f64, + pub show_particle_radius: bool, + pub show_fractal_set: bool, +} +impl Default for Options3D { + fn default() -> Self { + Self { + point_size: 1.0, + point_opacity: 1.0, + show_particle_radius: true, + show_fractal_set: true, + } + } +} +impl FromRGB for RGBColor { + fn from_rgb(rgb: RGB) -> Self { + RGBColor(rgb.0, rgb.1, rgb.2) + } +} +pub struct Plot3D { + chart: Chart<(BackendData, AxisData, Options3D)>, + projection_x: StateProjection, + selection_x: StateProjectionSelection, + projection_y: StateProjection, + selection_y: StateProjectionSelection, + projection_z: StateProjection, + selection_z: StateProjectionSelection, + selection_color: StateProjectionSelection, +} + +impl PartialEq for Plot3D { + fn eq(&self, other: &Self) -> bool { + let self_data = self.series_holder(); + let other_data = other.series_holder(); + self.projection_x == other.projection_x + && self.projection_y == other.projection_y + && self.projection_z == other.projection_z + && self_data.get_parameter() == other_data.get_parameter() + && self_data.get_parameter_values() == other_data.get_parameter_values() + } +} + +impl Eq for Plot3D {} +impl Default for Plot3D { + fn default() -> Self { + let chart = Chart::new(( + BackendData::default(), + AxisData::default(), + Options3D::default(), + )) + .yaw(0.5) + .pitch(0.15) + .scale(0.9) // TODO scale + .builder_cb(Box::new(|area, transform, data| { + let (x_range, y_range, z_range) = Self::get_ranges_from_extrema(&data.0); + let chart_build_res = ChartBuilder::on(area) + .margin(10) + .build_cartesian_3d(x_range, y_range, z_range); + match chart_build_res { + Err(_) => (), + Ok(mut chart) => { + chart.with_projection(|mut pb| { + pb.yaw = transform.yaw; + pb.pitch = transform.pitch; + pb.scale = transform.scale; + pb.into_matrix() + }); + Self::configure_axis(&mut chart, &data.1); + Self::plot_data(&mut chart, &data.0, &data.2); + } + }; + })); + Self { + chart, + // chaos app starts without params and 2 states (without using z) + projection_x: StateProjection::S(0), + selection_x: StateProjectionSelection::S0, + projection_y: StateProjection::S(1), + selection_y: StateProjectionSelection::S1, + projection_z: StateProjection::S(1), + selection_z: StateProjectionSelection::S1, + selection_color: StateProjectionSelection::S0, + } + } +} + +impl Plot3D { + fn series_holder(&self) -> &BackendData { + &self.chart.get_data().0 + } + fn series_holder_mut(&mut self) -> &mut BackendData { + &mut self.chart.get_data_mut().0 + } + fn axis_data_mut(&mut self) -> &mut AxisData { + &mut self.chart.get_data_mut().1 + } + fn options_mut(&mut self) -> &mut Options3D { + &mut self.chart.get_data_mut().2 + } + + fn set_x_label(&mut self, x_label: impl Into) { + self.axis_data_mut().x_label = x_label.into(); + } + fn set_y_label(&mut self, y_label: impl Into) { + self.axis_data_mut().y_label = y_label.into(); + } + fn set_z_label(&mut self, z_label: impl Into) { + self.axis_data_mut().z_label = z_label.into(); + } + + fn configure_axis(chart: &mut Chart3D<'_, '_>, axis_data: &AxisData) { + let (lx, ly, lz) = (&axis_data.x_label, &axis_data.y_label, &axis_data.z_label); + let _ = chart + .configure_axes() + .label_style(("sans-serif", 12.0).into_font().color(&RED)) + .tick_size(5) + .x_labels(3) + .y_labels(3) + .z_labels(3) + .max_light_lines(2) + .axis_panel_style(GREEN.mix(0.10)) + .bold_grid_style(BLACK.mix(0.2)) + .light_grid_style(BLACK.mix(0.10)) + .x_formatter(&|x| format!("{lx}={x}")) + .y_formatter(&|y| format!("{ly}={y}")) + .z_formatter(&|z| format!("{lz}={z}")) + .draw(); + } + + fn plot_chaotic_states( + chart: &mut Chart3D<'_, '_>, + series_holder: &BackendData, + options: &Options3D, + ) { + let _ = chart.draw_series(series_holder.all_styles_and_points_iter().map(|(s, p)| { + Circle::new( + *p, + options.point_size, + ShapeStyle::from(s.color.mix(options.point_opacity)).filled(), + ) + })); + } + + fn plot_particles( + chart: &mut Chart3D<'_, '_>, + series_holder: &BackendData, + options: &Options3D, + ) { + let particle_size = 2.0 * options.point_size; + let particle_stroke = 1; + let particle_opacity = options.point_opacity; + if options.show_particle_radius { + let _ = chart.draw_series(series_holder.all_styles_and_points_iter().map(|(s, p)| { + let (x, y, z) = *p; + let r = s.radius; + Rectangle::new( + [(x - r, y - r, z - r), (x + r, y + r, z + r)], + ShapeStyle::from(s.color.mix(particle_opacity)).stroke_width(particle_stroke), + ) + })); + } + let guest_coord_zero = (0, 0); + let particle_marker_shift = { + let half_size = (particle_size / 2.0).round() as i32; + (-half_size, -half_size) + }; + let phantom_size = 0.3; + let _ = chart.draw_series(series_holder.all_styles_and_points_iter().map(|(s, p)| { + let color = + ShapeStyle::from(s.color.mix(particle_opacity)).stroke_width(particle_stroke); + let positive_marker = if s.markers.positive { + Text::new( + "P", + particle_marker_shift, + ("sans-serif", particle_size).into_font(), + ) + } else { + Text::new( + "", + guest_coord_zero, + ("sans-serif", phantom_size).into_font(), + ) + }; + let negative_marker = if s.markers.negative { + Text::new( + "N", + particle_marker_shift, + ("sans-serif", particle_size).into_font(), + ) + } else { + Text::new( + "", + guest_coord_zero, + ("sans-serif", phantom_size).into_font(), + ) + }; + let special_marker = if s.markers.special { + Cross::new(guest_coord_zero, particle_size / 2.0, color) + } else { + Cross::new(guest_coord_zero, phantom_size, BLACK) + }; + EmptyElement::at(*p) + positive_marker + negative_marker + special_marker + })); + } + fn plot_fractal(chart: &mut Chart3D<'_, '_>, series_holder: &BackendData, options: &Options3D) { + let fractal_size = options.point_size; + let fractal_stroke = 2; + let fractal_opacity = options.point_opacity; + let (positive_size, positive_color) = if options.show_fractal_set { + ( + fractal_size, + ShapeStyle::from(series_holder.positive_color().mix(fractal_opacity)) + .stroke_width(fractal_stroke), + ) + } else { + (0.01, ShapeStyle::from(BLACK.mix(0.01))) + }; + let special_color = ShapeStyle::from(series_holder.special_color().mix(fractal_opacity)) + .stroke_width(fractal_stroke); + + let _ = chart.draw_series(series_holder.all_styles_and_points_iter().map(|(s, p)| { + if s.markers.special { + Circle::new(*p, fractal_size, special_color) + } else if s.markers.positive { + Circle::new(*p, positive_size, positive_color) + } else if s.markers.negative { + Circle::new( + *p, + fractal_size, + s.color + .mix(fractal_opacity / 2.0) + .stroke_width(fractal_stroke / 2), + ) + } else { + Circle::new( + *p, + fractal_size, + s.color.mix(fractal_opacity).stroke_width(fractal_stroke), + ) + } + })); + } + fn plot_data(chart: &mut Chart3D<'_, '_>, series_holder: &BackendData, options: &Options3D) { + match series_holder.dimensionality() { + DistributionDimensions::State(_) => { + Self::plot_chaotic_states(chart, series_holder, options) + } + DistributionDimensions::Particle(_) => { + Self::plot_particles(chart, series_holder, options) + } + DistributionDimensions::Fractal(_) => Self::plot_fractal(chart, series_holder, options), + }; + } + + fn get_ranges_from_extrema( + plot_backend: &BackendData, + ) -> (Range, Range, Range) { + let (mut x_min, mut x_max, mut y_min, mut y_max, mut z_min, mut z_max) = ( + VALID_MAX, VALID_MIN, VALID_MAX, VALID_MIN, VALID_MAX, VALID_MIN, + ); + plot_backend.extrema_iter().for_each(|(p_min, p_max)| { + x_min = x_min.min(p_min.0); + x_max = x_max.max(p_max.0); + y_min = y_min.min(p_min.1); + y_max = y_max.max(p_max.1); + z_min = z_min.min(p_min.2); + z_max = z_max.max(p_max.2); + }); + ( + Range { + start: x_min, + end: x_max, + }, + Range { + start: y_min, + end: y_max, + }, + Range { + start: z_min, + end: z_max, + }, + ) + } + + fn get_extrema_from_series(points: &Points3D) -> (Point3D, Point3D) { + let (mut x_min, mut x_max, mut y_min, mut y_max, mut z_min, mut z_max) = ( + VALID_MAX, VALID_MIN, VALID_MAX, VALID_MIN, VALID_MAX, VALID_MIN, + ); + points.iter().for_each(|p| { + if let Some(p) = p { + x_min = x_min.min(p.0); + x_max = x_max.max(p.0); + y_min = y_min.min(p.1); + y_max = y_max.max(p.1); + z_min = z_min.min(p.2); + z_max = z_max.max(p.2); + } + }); + ((x_min, y_min, z_min), (x_max, y_max, z_max)) + } + + pub fn set_parameter(&mut self, parameter: &'static str, par_values: Vec) { + let had_parameter = self.with_parameter(); + self.series_holder_mut() + .set_parameter(parameter, par_values); + if !had_parameter { + self.reset_projections(); + } else if self.parameters_are_shown() { + self.projection_x = StateProjection::Par(parameter); + } + } + pub fn remove_parameter(&mut self) { + if self.with_parameter() { + self.series_holder_mut().remove_parameter(); + self.reset_projections(); + } + } + + fn reset_projections(&mut self) { + let num_dims = self.number_of_dimensions(); + let projection_color = if let Some(p) = self.get_parameter() { + self.projection_x = StateProjection::Par(p); + self.projection_y = StateProjection::S(0); + self.projection_z = StateProjection::S(1); + if num_dims > 2 { + StateProjection::S(2) + } else if num_dims > 1 { + StateProjection::S(1) + } else { + StateProjection::S(0) + } + } else { + self.projection_x = StateProjection::S(0); + self.projection_y = StateProjection::S(1); + self.projection_z = StateProjection::S(2); + if num_dims > 3 { + StateProjection::S(3) + } else if num_dims > 2 { + StateProjection::S(2) + } else if num_dims > 1 { + StateProjection::S(1) + } else { + StateProjection::S(0) + } + }; + self.set_projection_color(projection_color); + self.selection_color = StateProjectionSelection::from(projection_color); + self.selection_x = StateProjectionSelection::from(self.projection_x); + self.selection_y = StateProjectionSelection::from(self.projection_y); + self.selection_z = StateProjectionSelection::from(self.projection_z); + } + + pub fn add_point_series(&mut self, data: ChaosDataVec<'_>) { + let dimensionality = data.dimensionality(); + if dimensionality != *self.series_holder().dimensionality() { + self.remove_parameter(); + self.set_dimensionality(dimensionality); + self.reset_projections(); + } + let styles = self.series_holder_mut().create_styles_for_chaos_data(&data); + let series = if self.parameters_are_shown() { + self.set_x_label( + self.get_parameter() + .expect("Parameter exists if projection is Par"), + ); + self.create_point_series_with_parameters(data) + } else { + self.create_point_series_without_parameters(data) + }; + let extrema = Self::get_extrema_from_series(&series); + self.series_holder_mut().add_series(series, styles, extrema); + } + + pub fn transform_points_1_d(&self, states: &[Option]) -> Points3D { + match self.series_holder().latest_series() { + None => states.iter().map(|v| v.map(|v| (0.0, 0.0, v[0]))).collect(), + Some((last_states, _)) => states + .iter() + .zip(last_states.iter()) + .map(|(new_state, last_state)| { + if let (Some(new_state), Some(last_state)) = (new_state, last_state) { + Some((last_state.1, last_state.2, new_state[0])) + } else { + None + } + }) + .collect(), // x=S1'', y=S1', z=S1 + } + } + pub fn transform_points_2_d(&self, states: &[Option]) -> Points3D { + let t = self.series_holder().num_series(); + states + .iter() + .map(|v| v.map(|v| (t as ChaosFloat, v[0], v[1]))) + .collect() // x=t, y=S1', z=S1 + } + + pub fn transform_points_n_d(&self, states: &[Option]) -> Points3D { + let (i_x, i_y, i_z) = ( + self.projection_x.index(), + self.projection_y.index(), + self.projection_z.index(), + ); + states + .iter() + .map(|v| v.as_ref().map(|v| (v.ind(i_x), v.ind(i_y), v.ind(i_z)))) + .collect() + } + + fn parameters_are_shown(&self) -> bool { + self.selection_x == StateProjectionSelection::Par + } + + pub fn points_with_parameter_1_d(&self, states: &[Option], par: &f64) -> Points3D { + let t = self.series_holder().num_series(); + states + .iter() + .map(|v| v.map(|v| (*par, t as ChaosFloat, v[0]))) + .collect() // x=par, y=t, z=S1 + } + + pub fn points_with_parameter_n_d( + &self, + states: &[Option], + par: &f64, + ) -> Points3D { + let (ind_y, ind_z) = (self.projection_y.index(), self.projection_z.index()); + states + .iter() + .map(|v| v.as_ref().map(|v| (*par, v.ind(ind_y), v.ind(ind_z)))) + .collect() + } + + fn set_axis_labels(&mut self) { + let dims = self.dimensionality().clone(); + let num_dims = dims.number_of_dimensions(); + if self.parameters_are_shown() { + self.set_x_label( + self.get_parameter() + .expect("Parameter exists if projection is Par"), + ); + if num_dims == 1 { + self.set_y_label("t"); + self.set_z_label("S1"); + } else { + self.set_y_label(self.projection_y.mode_string_axis(&dims)); + self.set_z_label(self.projection_z.mode_string_axis(&dims)); + } + } else if num_dims == 1 { + self.set_x_label("S''"); + self.set_y_label("S'"); + self.set_z_label("S"); + } else if num_dims == 2 { + self.set_x_label("t"); + self.set_y_label("S1"); + self.set_z_label("S2"); + } else { + self.set_x_label(self.projection_x.mode_string_axis(&dims)); + self.set_y_label(self.projection_y.mode_string_axis(&dims)); + self.set_z_label(self.projection_z.mode_string_axis(&dims)); + }; + } + + pub fn explanation(&self, ui: &mut Ui) { + if self.with_parameter() { + let param_select_label = if self.parameters_are_shown() { + LABEL_PARAMS_SHOWN + } else { + LABEL_PARAMS_NOT_SHOWN + }; + ui.label(param_select_label); + }; + let distribution_label = if self.parameters_are_shown() { + match self.dimensionality() { + DistributionDimensions::State(n) => { + if *n == 1 { + LABEL_PLOT3D_PAR_STATE_1 + } else { + LABEL_PLOT3D_PAR_STATE_N + } + } + DistributionDimensions::Particle(_) => LABEL_PLOT_PAR_PARTICLE, + DistributionDimensions::Fractal(_) => LABEL_PLOT3D_PAR_FRACTAL, + } + } else { + match self.dimensionality() { + DistributionDimensions::State(n) => match *n { + 1 => LABEL_PLOT3D_STATE_1, + 2 => LABEL_PLOT3D_STATE_2, + _ => LABEL_PLOT_STATE_N, + }, + DistributionDimensions::Particle(_) => LABEL_PLOT3D_PARTICLE, + DistributionDimensions::Fractal(_) => LABEL_PLOT3D_FRACTAL, + } + }; + ui.label(distribution_label); + } + + pub fn options_ui(&mut self, ui: &mut Ui) { + let dims = self.dimensionality().clone(); + let num_dims = dims.number_of_dimensions(); + let mut projection_vars_to_show = Vec::with_capacity(MAX_NUM_PROJECTIONS); + let par = self.get_parameter(); + if let Some(p) = par { + projection_vars_to_show.push(StateProjection::Par(p)); + } + StateProjection::add_state_projection_vars(num_dims, &mut projection_vars_to_show); + let mut has_x_selected = false; + group_horizontal(ui, |ui| { + has_x_selected = StateProjection::projection_vars_selection( + "X", + self.projection_x.mode_string_choice(&dims), + &mut self.selection_x, + &projection_vars_to_show, + &dims, + ui, + ); + if has_x_selected { + self.projection_x = if self.parameters_are_shown() { + StateProjection::Par(par.unwrap()) + } else { + StateProjection::state(self.selection_x) + } + } + }); + projection_vars_to_show.clear(); + StateProjection::add_state_projection_vars(num_dims, &mut projection_vars_to_show); + if num_dims > 2 { + group_horizontal(ui, |ui| { + let has_y_selected = StateProjection::projection_vars_selection( + "Y", + self.projection_y.mode_string_choice(&dims), + &mut self.selection_y, + &projection_vars_to_show, + &dims, + ui, + ); + if has_y_selected { + self.projection_y = StateProjection::state(self.selection_y); + } + }); + group_horizontal(ui, |ui| { + let has_z_selected = StateProjection::projection_vars_selection( + "Z", + self.projection_z.mode_string_choice(&dims), + &mut self.selection_z, + &projection_vars_to_show, + &dims, + ui, + ); + if has_z_selected { + self.projection_z = StateProjection::state(self.selection_z); + } + }); + } else if has_x_selected { + self.reset_data(); + } + if let Some(p) = par { + projection_vars_to_show.push(StateProjection::Par(p)); + } + group_horizontal(ui, |ui| { + let has_color_selected = StateProjection::projection_vars_selection( + "Color", + self.series_holder() + .projection_color() + .mode_string_choice(&dims), + &mut self.selection_color, + &projection_vars_to_show, + &dims, + ui, + ); + if has_color_selected { + let projection_color = if self.selection_color == StateProjectionSelection::Par { + StateProjection::Par(par.unwrap()) + } else { + StateProjection::state(self.selection_color) + }; + self.set_projection_color(projection_color); + } + }); + let options = self.options_mut(); + group_horizontal(ui, |ui| { + float_slider( + LABEL_POINT_SIZE, + &mut options.point_size, + 10.0, + ui, + TIP_POINT_SIZE, + ); + }); + group_horizontal(ui, |ui| { + float_slider( + LABEL_POINT_OPACITY, + &mut options.point_opacity, + 1.0, + ui, + TIP_POINT_OPACITY, + ); + }); + match dims { + DistributionDimensions::State(_) => (), + DistributionDimensions::Particle(_) => { + group_horizontal(ui, |ui| { + add_checkbox( + "Square Radius", + &mut options.show_particle_radius, + ui, + TIP_PARTICLE_RADIUS, + ); + }); + } + DistributionDimensions::Fractal(_) => { + group_horizontal(ui, |ui| { + add_checkbox( + "show set", + &mut options.show_fractal_set, + ui, + TIP_FRACTAL_SET, + ); + }); + } + } + } + + pub fn ui(&mut self, mouse_is_over_plot: bool, ui: &mut Ui) { + self.set_axis_labels(); + let mouse_config = MouseConfig::default() + .rotate(mouse_is_over_plot) + .pitch_scale(0.02); // TODO test drag and zoom + self.chart.set_mouse(mouse_config); + self.chart.draw(ui); + } + delegate! { + to self.series_holder(){ + pub fn dimensionality(&self) -> &DistributionDimensions; + pub fn number_of_dimensions(&self) -> usize; + pub fn get_parameter(&self) -> Option<&'static str>; + pub fn get_parameter_values(&self) -> &Vec; + pub fn with_parameter(&self)->bool; + } + to self.series_holder_mut(){ + pub fn series_color_mut(&mut self)-> &mut SeriesColorChoice; + pub fn set_dimensionality(&mut self, dims: DistributionDimensions); + pub fn set_projection_color(&mut self, projection_color: StateProjection); + #[call(clear)] + pub fn reset_data(&mut self); + #[call(set_max_series)] + pub fn set_max_num_series(&mut self, max_num_series: usize); + #[call(set_colormap)] + pub fn set_point_colormap(&mut self, colormap: SeriesColors); + } + } +} diff --git a/src/gui/plot/plot_backend.rs b/src/gui/plot/plot_backend.rs new file mode 100644 index 0000000..3779d21 --- /dev/null +++ b/src/gui/plot/plot_backend.rs @@ -0,0 +1,308 @@ +use super::plot_colors::{FromRGB, SeriesColorChoice, SeriesColorer, SeriesColors}; +use super::plot_styles::{ColoredStyle, Style}; +use super::plot_utils::StateProjection; +use crate::chaos::data::{ChaosData, DistributionDimensions, StateIndex}; +use delegate::delegate; +use std::collections::vec_deque::{self, VecDeque}; +pub const DEFAULT_MAX_SERIES: usize = 20; +struct PlotSeriesHolder { + series_collection: VecDeque, + max_num_series: usize, +} + +impl PlotSeriesHolder { + pub fn set_max_series(&mut self, max_num_series: usize) { + while self.series_collection.len() > max_num_series { + self.series_collection.pop_front(); + } + self.max_num_series = max_num_series; + } + + pub fn add_series(&mut self, series: S) { + if self.series_collection.len() == self.max_num_series { + self.series_collection.pop_front(); + } + self.series_collection.push_back(series); + } + + delegate! { + to self.series_collection{ + pub fn clear(&mut self); + #[call(len)] + pub fn num_series(&self) -> usize; + #[call(back)] + pub fn latest_series(&self) -> Option<&S>; + #[call(iter)] + pub fn series_iter( + &self, + ) -> vec_deque::Iter<'_, S>; + } + } +} + +impl Default for PlotSeriesHolder { + fn default() -> Self { + Self { + series_collection: VecDeque::new(), + max_num_series: DEFAULT_MAX_SERIES, + } + } +} + +struct PlotDimensions { + dimensions: DistributionDimensions, + parameter: Option<&'static str>, + par_values: Vec, +} + +impl Default for PlotDimensions { + fn default() -> Self { + Self { + dimensions: DistributionDimensions::State(2), + parameter: None, + par_values: Vec::new(), + } + } +} + +impl PlotDimensions { + pub fn set_parameter(&mut self, parameter: &'static str, par_values: Vec) { + self.parameter = Some(parameter); + self.par_values = par_values; + } + pub fn remove_parameter(&mut self) { + self.parameter = None; + self.par_values.clear(); + } + pub fn with_parameter(&self) -> bool { + self.get_parameter().is_some() + } + pub fn get_parameter(&self) -> Option<&'static str> { + self.parameter + } + pub fn get_parameter_values(&self) -> &Vec { + &self.par_values + } + + pub fn dimensionality(&self) -> &DistributionDimensions { + &self.dimensions + } + pub fn set_dimensionality(&mut self, dims: DistributionDimensions) { + self.dimensions = dims; + } + pub fn number_of_dimensions(&self) -> usize { + self.dimensions.number_of_dimensions() + } +} + +type StyledSeries = (Vec>, Vec>); +pub struct PlotBackend { + point_holder: PlotSeriesHolder>, + extrema_holder: PlotSeriesHolder<(P, P)>, + series_colorer: SeriesColorer, + plot_dimensions: PlotDimensions, + projection_color: StateProjection, + selection_series_color: SeriesColorChoice, +} + +impl Default for PlotBackend { + fn default() -> Self { + Self { + point_holder: Default::default(), + extrema_holder: Default::default(), + series_colorer: Default::default(), + plot_dimensions: Default::default(), + projection_color: StateProjection::S(0), + selection_series_color: Default::default(), + } + } +} + +impl PlotBackend { + pub fn projection_color(&self) -> StateProjection { + self.projection_color + } + pub fn set_projection_color(&mut self, projection_color: StateProjection) { + self.projection_color = projection_color; + self.selection_series_color = SeriesColorChoice::StateProjection; + } + pub fn series_color_mut(&mut self) -> &mut SeriesColorChoice { + &mut self.selection_series_color + } + pub fn clear(&mut self) { + self.point_holder.clear(); + self.extrema_holder.clear(); + self.series_colorer.reset(); + } + + pub fn set_max_series(&mut self, max_num_series: usize) { + self.point_holder.set_max_series(max_num_series); + self.extrema_holder.set_max_series(max_num_series); + self.series_colorer.set_max_number_of_colors(max_num_series); + } + pub fn add_series(&mut self, series: Vec>, styles: Vec>, extrema: (P, P)) { + self.point_holder.add_series((series, styles)); + self.extrema_holder.add_series(extrema); + } + + pub fn all_styles_and_points_iter(&self) -> impl Iterator, &P)> { + self.styled_series_iter().flat_map(|(points, styles)| { + styles.iter().zip(points.iter().filter_map(|p| p.as_ref())) + }) + } + + delegate! { + to self.series_colorer{ + pub fn set_colormap(&mut self, colormap: SeriesColors); + } + to self.plot_dimensions{ + pub fn set_parameter(&mut self, parameter: &'static str, par_values: Vec); + pub fn remove_parameter(&mut self); + pub fn with_parameter(&self) -> bool; + pub fn get_parameter(&self) -> Option<&'static str>; + pub fn get_parameter_values(&self) -> &Vec; + pub fn dimensionality(&self) -> &DistributionDimensions; + pub fn set_dimensionality(&mut self, dims: DistributionDimensions); + pub fn number_of_dimensions(&self) -> usize; + } + to self.point_holder{ + pub fn num_series(&self) -> usize; + pub fn latest_series(&self) -> Option<&StyledSeries>; + #[call(series_iter)] + pub fn styled_series_iter( + &self, + ) -> vec_deque::Iter<'_, StyledSeries>; + } + to self.extrema_holder{ + #[call(series_iter)] + pub fn extrema_iter( + &self, + ) -> vec_deque::Iter<'_, (P, P)>; + } + } +} +fn generic_flattened_states<'a, V>( + chaos_data_vec: &[&'a ChaosData], +) -> (Vec<&'a V>, Vec) { + let mut num_valid_states_per_param = Vec::with_capacity(chaos_data_vec.len()); + let flattened_valid_states = chaos_data_vec + .iter() + .flat_map(|chaos_data| { + let valid_states = chaos_data.data_filtered(); + num_valid_states_per_param.push(valid_states.len()); + valid_states + }) + .collect(); + (flattened_valid_states, num_valid_states_per_param) +} +impl PlotBackend { + delegate! { + to self.series_colorer{ + pub fn special_color(&self) -> C; + pub fn positive_color(&self) -> C; + } + } + + pub fn create_styles_for_chaos_data_generic>( + &mut self, + chaos_data_vec: &[&ChaosData], + ) -> Vec> { + let color_parameters = matches!(self.projection_color, StateProjection::Par(_)); + let (flattened_state_refs, num_valid_states_per_param) = + generic_flattened_states(chaos_data_vec); + let num_existing_states = flattened_state_refs.len(); + let color_vec = match self.selection_series_color { + SeriesColorChoice::Same => self.series_colorer.same_series_color(num_existing_states), + SeriesColorChoice::PerSeries => { + self.series_colorer.single_color_series(num_existing_states) + } + SeriesColorChoice::PerPoint => { + self.series_colorer.color_series_by_points(chaos_data_vec) + } + SeriesColorChoice::StateProjection => { + let projected_floats = if color_parameters { + num_valid_states_per_param + .into_iter() + .zip(self.get_parameter_values().iter()) + .flat_map(|(num_valid_states, par)| vec![*par; num_valid_states]) + .collect() + } else { + let color_projection_index = self.projection_color.index(); + flattened_state_refs + .iter() + .map(|v| v.ind(color_projection_index)) + .collect() + }; + self.series_colorer.color_series_projected(projected_floats) + } + }; + flattened_state_refs + .into_iter() + .zip(color_vec) + .map(|(v, c)| v.colored_style(c)) + .collect() + } +} +mod tests { + + use super::*; + use crate::{gui::plot::plot_colors::RGB, chaos::data::*}; + #[test] + fn test_state2_style_creation() { + let num_samples = 2; + let init_distr_1 = vec![ + InitialDistributionVariant::Fixed(Fixed { value: 1.1 }), + InitialDistributionVariant::Fixed(Fixed { value: 1.2 }), + ]; + let init_distr_2 = vec![ + InitialDistributionVariant::Linspace(Linspace { + low: 0.0, + high: 0.5, + }), + InitialDistributionVariant::Fixed(Fixed { value: 2.2 }), + ]; + let chaos_data_1: ChaosData = ChaosData::new(num_samples, &init_distr_1); + let chaos_data_2: ChaosData = ChaosData::new(num_samples, &init_distr_2); + let chaos_data_vec = vec![&chaos_data_1, &chaos_data_2]; + let mut plot_backend: PlotBackend<(ChaosFloat, ChaosFloat), RGB> = Default::default(); + plot_backend.set_colormap(SeriesColors::BlackWhite); + *plot_backend.series_color_mut() = SeriesColorChoice::Same; + let styles_same = plot_backend.create_styles_for_chaos_data_generic(&chaos_data_vec); + assert_eq!( + styles_same[1], styles_same[2], + "Styles must have the same color and default values!" + ); + *plot_backend.series_color_mut() = SeriesColorChoice::PerPoint; + let styles_point = plot_backend.create_styles_for_chaos_data_generic(&chaos_data_vec); + assert_eq!(styles_point[0], styles_point[2], "Styles must have same value when they represent the same initial state for two parameter configurations!"); + assert_eq!(styles_point[1], styles_point[3], "Styles must have same value when they represent the same initial state for two parameter configurations"); + assert_ne!( + styles_point[0], styles_point[1], + "Styles must be different when represent different states!" + ); + *plot_backend.series_color_mut() = SeriesColorChoice::PerSeries; + let styles_series = plot_backend.create_styles_for_chaos_data_generic(&chaos_data_vec); + assert_eq!( + styles_series[0], styles_series[1], + "Styles must have same values since they were plotted in the same series(even though different parameter configurations)!" + ); + plot_backend.remove_parameter(); + plot_backend.set_projection_color(StateProjection::S(0)); + *plot_backend.series_color_mut() = SeriesColorChoice::StateProjection; + let styles_projection = plot_backend.create_styles_for_chaos_data_generic(&chaos_data_vec); + assert_ne!( + styles_projection[0], styles_projection[2], + "Styles must be different when color represents the different state values!" + ); + plot_backend.set_parameter("t", vec![-1.0, 1.0]); + let styles_projection = plot_backend.create_styles_for_chaos_data_generic(&chaos_data_vec); + assert_eq!( + styles_projection[0], styles_projection[1], + "Styles must be equal when color represents the same parameter value!" + ); + assert_ne!( + styles_projection[0], styles_projection[2], + "Styles must be different when color represents the different parameter value!" + ); + } +} diff --git a/src/gui/plot/plot_colors.rs b/src/gui/plot/plot_colors.rs new file mode 100644 index 0000000..26a5f3f --- /dev/null +++ b/src/gui/plot/plot_colors.rs @@ -0,0 +1,227 @@ +use crate::chaos::data::*; + +use plotters::style::colors::colormaps::*; +use plotters::style::{Color, HSLColor, RGBColor}; +use strum_macros::EnumIter; + +use super::plot_backend::DEFAULT_MAX_SERIES; +#[allow(clippy::upper_case_acronyms)] +pub type RGB = (u8, u8, u8); +pub trait FromRGB { + fn from_rgb(rgb: RGB) -> Self; +} + +impl FromRGB for RGB { + fn from_rgb(rgb: RGB) -> Self { + rgb + } +} + +#[derive(PartialEq, Eq, Default, Clone, Copy, EnumIter)] +pub enum SeriesColors { + BlackWhite, + Bone, + Copper, + // TODO DerivedColorMap(Vec<(f64, RGBColor)>), + #[default] + MandelbrotHSL, + ViridisRGB, + VulcanoHSL, +} + +impl From for &'static str { + fn from(val: SeriesColors) -> Self { + match val { + SeriesColors::BlackWhite => "Black-White", + SeriesColors::Bone => "Bone", + SeriesColors::Copper => "Copper", + SeriesColors::MandelbrotHSL => "Mandelbrot", + SeriesColors::ViridisRGB => "Viridis", + SeriesColors::VulcanoHSL => "Vulcano", + } + } +} + +impl SeriesColors { + const BLACKWHITE: BlackWhite = BlackWhite {}; + const BONE: Bone = Bone {}; + const COPPER: Copper = Copper {}; + const MANDELBROT: MandelbrotHSL = MandelbrotHSL {}; + const VIRIDIS: ViridisRGB = ViridisRGB {}; + const VULCANO: VulcanoHSL = VulcanoHSL {}; + pub fn special_color(&self) -> RGB { + match self { + SeriesColors::BlackWhite | SeriesColors::Bone | SeriesColors::Copper => { + SeriesColors::MANDELBROT.get_color(0.5).rgb() + } + _ => SeriesColors::COPPER.get_color(1.0).rgb(), + } + } + pub fn positive_color(&self) -> RGB { + match self { + SeriesColors::BlackWhite | SeriesColors::Bone | SeriesColors::Copper => { + SeriesColors::MANDELBROT.get_color(0.0).rgb() + } + _ => SeriesColors::COPPER.get_color(0.0).rgb(), + } + } + pub fn color(&self, h: f32) -> RGB { + match self { + SeriesColors::BlackWhite => SeriesColors::BLACKWHITE.get_color(h).rgb(), + SeriesColors::Bone => SeriesColors::BONE.get_color(h).rgb(), + SeriesColors::Copper => SeriesColors::COPPER.get_color(h).rgb(), + SeriesColors::MandelbrotHSL => SeriesColors::MANDELBROT.get_color(h).rgb(), + SeriesColors::ViridisRGB => SeriesColors::VIRIDIS.get_color(h).rgb(), + SeriesColors::VulcanoHSL => SeriesColors::VULCANO.get_color(h).rgb(), + } + } + pub fn color_vec(&self, h_vec: Vec) -> Vec { + match self { + SeriesColors::BlackWhite => { + SeriesColors::color_vec_trait::(SeriesColors::BLACKWHITE, h_vec) + } + SeriesColors::Bone => { + SeriesColors::color_vec_trait::(SeriesColors::BONE, h_vec) + } + SeriesColors::Copper => { + SeriesColors::color_vec_trait::(SeriesColors::COPPER, h_vec) + } + SeriesColors::MandelbrotHSL => { + SeriesColors::color_vec_trait::(SeriesColors::MANDELBROT, h_vec) + } + SeriesColors::ViridisRGB => { + SeriesColors::color_vec_trait::(SeriesColors::VIRIDIS, h_vec) + } + SeriesColors::VulcanoHSL => { + SeriesColors::color_vec_trait::(SeriesColors::VULCANO, h_vec) + } + } + } + fn color_vec_trait(colormap: impl ColorMap, h_vec: Vec) -> Vec { + h_vec + .into_iter() + .map(|h| colormap.get_color(h).rgb()) + .collect() + } +} + +#[derive(PartialEq, Eq, Default, Clone, Copy, EnumIter)] +pub enum SeriesColorChoice { + Same, + #[default] + PerSeries, + PerPoint, + StateProjection, +} +impl From for &'static str { + fn from(val: SeriesColorChoice) -> Self { + match val { + SeriesColorChoice::Same => "Same", + SeriesColorChoice::PerSeries => "Series", + SeriesColorChoice::PerPoint => "Point", + SeriesColorChoice::StateProjection => "State", + } + } +} + +pub struct SeriesColorer { + colormap: SeriesColors, + color_counter: usize, + max_num_colors: usize, +} + +impl SeriesColorer { + pub fn set_colormap(&mut self, colormap: SeriesColors) { + self.colormap = colormap; + } + + pub fn reset(&mut self) { + self.color_counter = 0; + } + + pub fn set_max_number_of_colors(&mut self, max_num_colors: usize) { + self.max_num_colors = max_num_colors; + } + + fn cloned_color_vec(&self, h: f32, num_colors: usize) -> Vec { + vec![C::from_rgb(self.colormap.color(h)); num_colors] + } + + fn convert_h_to_c(&self, h_vec: Vec) -> Vec { + self.colormap + .color_vec(h_vec) + .into_iter() + .map(C::from_rgb) + .collect() + } + + pub fn same_series_color(&self, num_colors: usize) -> Vec { + self.cloned_color_vec(0.5, num_colors) + } + + pub fn single_color_series(&mut self, num_colors: usize) -> Vec { + if self.color_counter > self.max_num_colors { + self.color_counter = 0; + } else { + self.color_counter += 1; + } + let h = self.color_counter as f32 / self.max_num_colors as f32; + self.cloned_color_vec(h, num_colors) + } + pub fn color_series_by_points( + &self, + chaos_data_vec: &[&ChaosData], + ) -> Vec { + let total_num_points = chaos_data_vec + .first() + .map_or(0.0, |chaos_data| chaos_data.total_num_points() as f32); + let h_vec = chaos_data_vec + .iter() + .flat_map(|chaos_data| { + let state_colors_per_param: Vec = chaos_data + .data() + .iter() + .enumerate() + .filter_map(|(i, v)| v.as_ref().map(|_| i as f32 / total_num_points)) + .collect(); + state_colors_per_param + }) + .collect(); + self.convert_h_to_c(h_vec) + } + pub fn color_series_projected( + &self, + projected_state_vec: Vec, + ) -> Vec { + let (mut min, mut max) = (ChaosFloat::INFINITY, ChaosFloat::NEG_INFINITY); + projected_state_vec.iter().for_each(|x| { + min = min.min(*x); + max = max.max(*x); + }); + let diff = max - min; + if !min.is_finite() || !max.is_finite() || diff <= ChaosFloat::EPSILON { + return self.cloned_color_vec(0.5, projected_state_vec.len()); + } + let h_vec = projected_state_vec + .into_iter() + .map(|x| ((x - min) / diff) as f32) + .collect(); + self.convert_h_to_c(h_vec) + } + pub fn special_color(&self) -> C { + C::from_rgb(self.colormap.special_color()) + } + pub fn positive_color(&self) -> C { + C::from_rgb(self.colormap.positive_color()) + } +} + +impl Default for SeriesColorer { + fn default() -> Self { + Self { + colormap: Default::default(), + color_counter: 0, + max_num_colors: DEFAULT_MAX_SERIES, + } + } +} diff --git a/src/gui/plot/plot_data_variants.rs b/src/gui/plot/plot_data_variants.rs new file mode 100644 index 0000000..1d600e3 --- /dev/null +++ b/src/gui/plot/plot_data_variants.rs @@ -0,0 +1,85 @@ +use super::plot_2_d::*; +use super::plot_3_d::*; +use super::plot_backend::*; +use super::plot_colors::*; +use super::plot_styles::*; +use super::plot_utils::{flat_map_data_vec, flat_map_data_vec_and_parameter}; +use crate::chaos::data::*; +use paste::paste; +macro_rules! impl_data_variant_plot { + ($($variant:ident, $d2:expr, $d2_par:expr, $d3:expr, $d3_par:expr),*) => { + paste!{ + impl Plot2D { + pub fn create_point_series_without_parameters(&self, data: ChaosDataVec<'_>) -> Points2D { + match data { + $( + ChaosDataVec::$variant(data_vec) => flat_map_data_vec(data_vec, |x| self.[](x)), + )* + } + } + + pub fn create_point_series_with_parameters(&mut self, data: ChaosDataVec<'_>) -> Points2D { + let par_values = self.get_parameter_values(); + match data { + $( + ChaosDataVec::$variant(data_vec) => { + flat_map_data_vec_and_parameter(data_vec, par_values, |x, p| { + self.[](x, p) + }) + }, + )* + } + } + } + + impl Plot3D { + pub fn create_point_series_without_parameters(&self, data: ChaosDataVec<'_>) -> Points3D { + match data { + $( + ChaosDataVec::$variant(data_vec) => flat_map_data_vec(data_vec, |x| self.[](x)), + )* + } + } + + pub fn create_point_series_with_parameters(&mut self, data: ChaosDataVec<'_>) -> Points3D { + let par_values = self.get_parameter_values(); + match data { + $( + ChaosDataVec::$variant(data_vec) => { + flat_map_data_vec_and_parameter(data_vec, par_values, |x, p| { + self.[](x, p) + }) + }, + )* + } + } + } + + impl PlotBackend { + pub fn create_styles_for_chaos_data( + &mut self, + data: &ChaosDataVec<'_>, + ) -> Vec> { + match data { + $( + ChaosDataVec::$variant(data_vec) => self.create_styles_for_chaos_data_generic(data_vec), + )* + } + } + } + } + }; +} + +impl_data_variant_plot! { + State1, 1, n, 1, 1, + State2, n, n, 2, n, + State3, n, n, n, n, + State4, n, n, n, n, + ParticleXY, n, n, n, n, + ParticleXYZ, n, n, n, n, + FractalComplex, n, n, n, n, + FractalDual, n, n, n, n, + FractalPerplex, n, n, n, n, + FractalQuaternion, n, n, n, n +} diff --git a/src/gui/plot/plot_styles.rs b/src/gui/plot/plot_styles.rs new file mode 100644 index 0000000..cd22dc1 --- /dev/null +++ b/src/gui/plot/plot_styles.rs @@ -0,0 +1,63 @@ +use crate::chaos::data::*; +use crate::chaos::fractal::FractalData; +use crate::chaos::Particle; + +pub const DEFAULT_RADIUS: ChaosFloat = 2.0; +#[derive(Debug, PartialEq, Default)] +pub struct Markers { + pub positive: bool, + pub negative: bool, + pub special: bool, +} + +pub trait ColoredStyle { + fn colored_style(&self, color: C) -> Style { + Style { + color, + radius: DEFAULT_RADIUS, + markers: Default::default(), + } + } +} +impl ColoredStyle for State1 {} +impl ColoredStyle for State2 {} +impl ColoredStyle for State3 {} +impl ColoredStyle for State4 {} +impl ColoredStyle for State5 {} +impl ColoredStyle for State6 {} +#[derive(Debug, PartialEq)] +pub struct Style { + pub color: C, + pub radius: ChaosFloat, + pub markers: Markers, +} +impl ColoredStyle for Particle { + fn colored_style(&self, color: C) -> Style { + let positive = self.mid > 0.0; + let negative = self.mid < 0.0; + let special = self.short < 0.0; + let radius = self.radius; + Style { + color, + radius, + markers: Markers { + positive, + negative, + special, + }, + } + } +} +impl ColoredStyle for FractalData { + fn colored_style(&self, color: C) -> Style { + Style { + color, + radius: DEFAULT_RADIUS, + markers: Markers { + positive: self.last(), + negative: self.first(), + special: self.biomorph(), + }, + } + } +} diff --git a/src/gui/plot/plot_utils.rs b/src/gui/plot/plot_utils.rs new file mode 100644 index 0000000..2c6fa45 --- /dev/null +++ b/src/gui/plot/plot_utils.rs @@ -0,0 +1,165 @@ +use egui::Ui; + +use crate::{ + gui::combo_box_from_string, + chaos::{data::*, labels::*}, +}; +pub fn flat_map_data_vec( + data_vec: Vec<&ChaosData>, + f: impl Fn(&Vec>) -> Vec>, +) -> Vec> { + data_vec.iter().flat_map(|data| f(data.data())).collect() +} +pub fn flat_map_data_vec_and_parameter( + data_vec: Vec<&ChaosData>, + par_values: &[f64], + f: impl Fn(&Vec>, &f64) -> Vec>, +) -> Vec> { + data_vec + .iter() + .zip(par_values.iter()) + .flat_map(|(data, par)| f(data.data(), par)) + .collect() +} + +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum StateProjection { + Par(&'static str), + S(usize), +} + +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum StateProjectionSelection { + Par, + S0, + S1, + S2, + S3, + S4, + S5, + S6, + S7, + S8, + S9, +} + +impl From for StateProjectionSelection { + fn from(value: StateProjection) -> Self { + match value { + StateProjection::Par(_) => Self::Par, + StateProjection::S(s) => match s { + 0 => Self::S0, + 1 => Self::S1, + 2 => Self::S2, + 3 => Self::S3, + 4 => Self::S4, + 5 => Self::S5, + 6 => Self::S6, + 7 => Self::S7, + 8 => Self::S8, + 9 => Self::S9, + _ => Self::S0, + }, + } + } +} +pub const MAX_NUM_PROJECTIONS: usize = 10; +impl StateProjection { + pub fn index(&self) -> usize { + match self { + StateProjection::Par(_) => 0, + StateProjection::S(s) => *s, + } + } + pub fn add_state_projection_vars(dims: usize, variants: &mut Vec) { + let mut i = 0; + while dims > i { + variants.push(StateProjection::S(i)); + i += 1; + } + } + pub fn mode_string_choice(&self, dims: &DistributionDimensions) -> String { + match self { + Self::Par(p) => format!("Par {p}"), + Self::S(s) => match dims { + DistributionDimensions::State(_) => { + format!("State {}", *s + 1) + } + DistributionDimensions::Particle(cartesian_dims) => match cartesian_dims { + 2 => LABELS_PARTICLE_2D[*s].into(), + 3 => LABELS_PARTICLE_3D[*s].into(), + _ => String::from("Error"), + }, + DistributionDimensions::Fractal(fractal_mode) => match fractal_mode { + FractalDimensions::Complex => LABELS_COMPLEX[*s].into(), + FractalDimensions::Dual => LABELS_DUAL[*s].into(), + FractalDimensions::Perplex => LABELS_PERPLEX[*s].into(), + FractalDimensions::Quaternion => LABELS_QUATERNION[*s].into(), + }, + }, + } + } + pub fn mode_string_axis(&self, dims: &DistributionDimensions) -> String { + match self { + Self::Par(p) => String::from(*p), + Self::S(s) => match dims { + DistributionDimensions::State(_) => { + format!("S{}", *s + 1) + } + DistributionDimensions::Particle(cartesian_dims) => match cartesian_dims { + 2 => LABELS_SHORT_PARTICLE_2D[*s].into(), + 3 => LABELS_SHORT_PARTICLE_3D[*s].into(), + _ => String::from("Error"), + }, + DistributionDimensions::Fractal(fractal_mode) => match fractal_mode { + FractalDimensions::Complex => LABELS_SHORT_COMPLEX[*s].into(), + FractalDimensions::Dual => LABELS_SHORT_DUAL[*s].into(), + FractalDimensions::Perplex => LABELS_SHORT_PERPLEX[*s].into(), + FractalDimensions::Quaternion => LABELS_SHORT_QUATERNION[*s].into(), + }, + }, + } + } + pub fn state(var: StateProjectionSelection) -> Self { + let s = match var { + StateProjectionSelection::Par => 0, + StateProjectionSelection::S0 => 0, + StateProjectionSelection::S1 => 1, + StateProjectionSelection::S2 => 2, + StateProjectionSelection::S3 => 3, + StateProjectionSelection::S4 => 4, + StateProjectionSelection::S5 => 5, + StateProjectionSelection::S6 => 6, + StateProjectionSelection::S7 => 7, + StateProjectionSelection::S8 => 8, + StateProjectionSelection::S9 => 9, + }; + Self::S(s) + } + pub fn projection_vars_selection( + label: &'static str, + var_label: String, + var_selection: &mut StateProjectionSelection, + vars: &[StateProjection], + dims: &DistributionDimensions, + ui: &mut Ui, + ) -> bool { + let projection_with_labels = vars + .iter() + .map(|v| { + ( + StateProjectionSelection::from(*v), + v.mode_string_choice(dims), + ) + }) + .collect(); + let tooltip = format!("Choose which data feature to project on {}", label); + combo_box_from_string( + label, + (var_selection, var_label), + ui, + projection_with_labels, + tooltip.as_str(), + ) + } +} diff --git a/src/gui/tooltips.rs b/src/gui/tooltips.rs new file mode 100644 index 0000000..f1fab6d --- /dev/null +++ b/src/gui/tooltips.rs @@ -0,0 +1,127 @@ +pub const LABEL_INIT_DATA: &str = "Init Data"; +pub const TIP_INIT_DATA: &str = "Generate chaotic data with the selected distributions."; +pub const LABEL_REINIT_DATA: &str = "Reinit Data"; +pub const TIP_REINIT_DATA: &str = "Data points with a feature higher than 32767 (or lower than -32768) are removed. This toggle generates new samples with the same initial distribution for data points that escaped the simulation."; +pub const LABEL_INIT_FUNCTION: &str = "Init Function"; +pub const TIP_INIT_FUNCTION: &str = "Apply the parametrized chaotic function."; + +pub const LABEL_NUM_PARAMS: &str = "Nr Params"; +pub const TIP_NUM_PARAMS: &str = "Set the number of parameters. They are evenly spaced (Linspace)."; + +pub const LABEL_NUM_EXECS: &str = "Nr Executions"; +pub const TIP_NUM_EXECS: &str = "Set the number of executions per frame. Defines how many times a discrete map is applied between two frames, and how many infinitesimal steps an ODE solver performs. Set to 1 and use the number of frames for visualizations."; +pub const LABEL_RUN: &str = "Run"; +pub const TIP_RUN: &str = "Run or pause the execution of a chaotic function. Useful for immediately stopping a high CPU load to reconfigure."; +pub const LABEL_MAIN_MODE: &str = "Main Mode"; +pub const TIP_MAIN_MODE: &str = "Show a plot or run a benchmark of chaotic data."; +pub const LABEL_INIT_MODE: &str = "Mode"; +pub const TIP_INIT_MODE: &str = "Choose the general type of chaotic data to initialize. Chaotic functions are selectable based on this selection."; +pub const LABEL_NUM_SAMPLES: &str = "Nr Samples"; +pub const TIP_NUM_SAMPLES: &str = "Set the number of samples. This defines how many times a distribution is sampled from. For meshes, it is the number per axis (the total number of samples is the number of chosen samples to the power of selected meshes)."; +pub const TIP_BUTTON_DECREASE_NUM_STATES: &str = "Decrease the dimensionality of the data points."; +pub const TIP_BUTTON_INCREASE_NUM_STATES: &str = "Increase the dimensionality of the data points."; +pub const TIP_INIT_PANEL: &str = + "Configure initial chaotic data. Then choose which function to apply."; +pub const TIP_DISTRIBUTIONS_ALL: &str = "Select the general class of initial distributions to choose from. Mesh (deterministic as well) creates a grid with (#samples)^(#meshes) points in total."; +pub const TIP_DISTRIBUTIONS_NO_MESH: &str = + "Select between probabilistic and deterministic initial distributions."; +pub const TIP_DIMS: &str = "Select the dimensionality of the data points. Affects which discrete maps or systems of differential equations are selectable."; +pub const TIP_PARTICLE_MODE: &str = "Choose between a 2D or 3D particle simulation."; +pub const TIP_FRACTAL_MODE: &str = + "Choose between 2D rings (Complex, Dual, Perplex) or 4D Quaternions."; + +pub const LABEL_PLOT_BACKEND: &str = "Plot Backend"; +pub const TIP_PLOT_BACKEND: &str = "Select the plotting backend to use. Egui has a 2D backend for plots. 3D plots are available through Plotters and the egui_plotters crate."; + +pub const LABEL_NUM_FRAMES: &str = "Frame Rate"; +pub const TIP_NUM_FRAMES: &str = "Set the number of frames per second."; +pub const LABEL_NUM_SERIES: &str = ""; +pub const TIP_NUM_SERIES: &str = "Set the number of series (trajectory length) to display."; +pub const LABEL_TRAJECTORY: &str = "Trajectory"; +pub const TIP_TRAJECTORY: &str = "Display a trajectory with a set length. This defines how points are colored if the point coloring mode is series-based. The size of points for states is calculated dynamically to highlight the latest series."; // TODO only Plot2D +pub const LABEL_COLORMAP: &str = "Color Map"; +pub const TIP_COLORMAP: &str = "Select the color map that creates colors for the plot points."; +pub const LABEL_COLOR_PER_POINT: &str = "Coloring Mode"; +pub const TIP_COLOR_PER_POINT: &str = "How to color the points: \n- A single color. \n- A color per series to follow distribution evolution. \n- A color per point to follow its trajectory. \n- Mapping of a feature to a color space (min => 0, max => 1)."; +pub const LABEL_POINT_SIZE: &str = "Point Size"; +pub const TIP_POINT_SIZE: &str = "Set a fixed size for shapes such as points."; + +// Plot +pub const LABEL_PARAMS_SHOWN: &str = "A parameter range is always on the X-Axis."; +pub const LABEL_PARAMS_NOT_SHOWN: &str = + "The parameter range can be visualized over the X-Axis projection or color."; +pub const LABEL_PLOT_STATE_N: &str = + "A multidimensional state is visualized by the selected feature projections."; +pub const LABEL_PLOT_PAR_PARTICLE: &str = "Particles are shown with the same radius and markers for each parameter value. Useful to explore the evolution of the particle world under different settings, e.g., the long-range influence of l (gravitational constants)."; +// 2D +pub const LABEL_PLOT2D_STATE_1: &str = "A single state S is plotted as S prev (X-Axis=S') against S new (Y-Axis=S). Useful for discovering fixpoints and circles in a trajectory."; +pub const LABEL_PLOT2D_PARTICLE: &str = "Particles are circles with a radius of √mass, containing markers that represent the charge and parity. Charge is visualized by triangles pointing up or down. Marker size is given by radius and not the actual value of the charge or parity."; +pub const LABEL_PLOT2D_FRACTAL: &str = "Fractals should be plotted with color representing the number of iterations. There should be a sufficiently large number of points to create vibrant fractals. The final set (reached at maximum number of iterations) is highlighted in black for the colorful maps and red for the greyish ones."; +pub const LABEL_PLOT2D_PAR_STATE_1: &str = + "The one-dimensional state is projected on the Y-Axis to produce a bifurcation diagram."; +pub const LABEL_PLOT2D_PAR_STATE_N: &str = "The state is visualized by the selected feature projection on the Y-Axis to produce a bifurcation diagram."; +pub const LABEL_PLOT2D_PAR_FRACTAL: &str = "The color should represent the number of iterations. Interesting parameter values may be identified by colorful fractal projections. A 3D plot is better suited."; +// 3D +pub const LABEL_PLOT3D_STATE_1: &str = "A one-dimensional state S is plotted as S prev prev (X-Axis=S'') against S prev (Y-Axis=S') and S new (Z-Axis). Useful for discovering fixpoints or circles in the trajectory."; +pub const LABEL_PLOT3D_STATE_2: &str = "A two-dimensional state S is plotted by a fixed assignment of S1 to Y and S2 to Z. The X-Axis shows time t for a specified trajectory length."; +pub const LABEL_PLOT3D_PARTICLE: &str = "Particles are visualized by markers representing charge and parity. 'P' and 'N' mark positively or negatively charged ones. A cross marks anti-parity, meaning it will perform an inelastic collision with particles of a different parity (without a cross). Marker size can be modified by pointer-size for better visibility."; +pub const LABEL_PLOT3D_FRACTAL: &str = "Fractals should be plotted with color representing the number of iterations. There should be a sufficiently large number of points to create vibrant fractals. The final set (reached at maximum number of iterations) is highlighted in black for the colorful maps and red for the greyish ones. Small point sizes increase performance. Opacity and the show set option are useful to avoid overlapping. Setting the iteration count as Z-Axis allows to follow how points are marked as not being part of the set."; +pub const LABEL_PLOT3D_PAR_STATE_1: &str = "The one-dimensional state is projected on the Z-Axis to produce a bifurcation diagram. The Y-Axis shows time t for a specified trajectory length. Allows investigation of the evolution of the state with different parameter values and a final 2D bifurcation diagram."; +pub const LABEL_PLOT3D_PAR_STATE_N: &str = "The state is visualized by the selected feature projections on the Y- and Z-Axis. This may produce a multidimensional bifurcation diagram and provides insights into interesting regions of the parameter space."; + +pub const LABEL_PLOT3D_PAR_FRACTAL: &str = "The color should represent the number of iterations. Interesting regions of the parameter space can be identified by a 2D fractal for each parameter value."; +pub const LABEL_POINT_OPACITY: &str = "Point Opacity"; +pub const TIP_POINT_OPACITY: &str = "Adjust the opacity for coloring shapes like points, useful for visualizing overlapping shapes."; + +// particles +pub const TIP_PARTICLE_RADIUS: &str = "Particle radius is √mass. Egui displays a circle with this radius, while Plotters shows a rectangle around the circle's origin with the correct radius but displays shapes with the chosen size."; +pub const TIP_PARTICLE_PARITY: &str = "Parity determines collision behavior. Same parity causes inelastic collisions, combining features. Different parity causes elastic collisions, exchanging momentum. The system's collision factor 's' influences the impulse of elastic collisions."; +pub const TIP_PARTICLE_CHARGE: &str = "Charge determines attraction (opposite charges) or repulsion (same charges) due to mid-range forces."; +pub const TIP_PARTICLE_MASS: &str = + "Mass influences gravitational force over long ranges and defines the particle's radius."; +pub const TIP_PARTICLE_PX: &str = "Particle's position in the x-direction."; +pub const TIP_PARTICLE_PY: &str = "Particle's position in the y-direction."; +pub const TIP_PARTICLE_PZ: &str = "Particle's position in the z-direction."; +pub const TIP_PARTICLE_VX: &str = "Particle's velocity in the x-direction."; +pub const TIP_PARTICLE_VY: &str = "Particle's velocity in the y-direction."; +pub const TIP_PARTICLE_VZ: &str = "Particle's velocity in the z-direction."; + +// fractal +pub const TIP_FRACTAL_SET: &str = + "Show the points that are in the set. Useful to avoid overlapping."; +pub const LINK_COMPLEX: &str = "https://wikipedia.org/wiki/Fractal"; +pub const TIP_COMPLEX: &str = "Complex numbers, solutions to equations like x²=-1, are used to generate fractals such as the Mandelbrot and Julia sets."; +pub const LABEL_BASIS_COMPLEX: &str = "Complex Number: a + b i"; +pub const TIP_FRACTAL_COMPLEX_RE: &str = "Real part 'a' of the complex number c = a + b i, determining the x-value of the pixel and the real part of z0."; +pub const TIP_FRACTAL_COMPLEX_IM: &str = "Imaginary part 'b' of the complex number c = a + b i, determining the y-value of the pixel and the imaginary part of z0."; + +pub const LINK_DUAL: &str = "http://dx.doi.org/10.1080/10236190412331334482"; +pub const TIP_DUAL: &str = "Dual numbers, also known as parabolic numbers, are akin to complex numbers but use the element ε (ε≠0, ε²=0) instead of the imaginary unit i. They're applied in automatic differentiation and geometrical transformations. For more on quadratic dynamics in binary systems, see 'Quadratic dynamics in binary number systems' by Fishback. Dual numbers are also used with other algebraic structures to create high-dimensional fractal structures, as seen in 'Dual-Quaternion Julia Fractals' by Ben Kenwright."; +pub const LABEL_BASIS_DUAL: &str = "Dual Number: a + b ε"; +pub const TIP_FRACTAL_DUAL_RE: &str = "The real part 'a' of the dual number c = a + b ε, defining the x-value of the pixel and the real part of z0."; +pub const TIP_FRACTAL_DUAL_IM: &str = "The dual part 'b' of the dual number c = a + b ε, defining the y-value of the pixel and the dual part of z0."; + +pub const LINK_PERPLEX: &str = "https://doi.org/10.3390/fractalfract3010006"; +pub const TIP_PERPLEX: &str = "Perplex numbers, also known as split-complex, double, or hyperbolic numbers, extend real numbers by introducing a new element h (h≠±1, h²=1). Julia and Mandelbrot sets over hyperbolic numbers are simpler than those over complex numbers. For details, see 'Julia and Mandelbrot Sets for Dynamics over the Hyperbolic Numbers'."; +pub const LABEL_BASIS_PERPLEX: &str = "Perplex Number: t + x h"; +pub const TIP_FRACTAL_PERPLEX_RE: &str = "The time component 't' of the perplex number c = t + x h, defining the x-value of the pixel and the time component of z0."; +pub const TIP_FRACTAL_PERPLEX_IM: &str = "The space component 'x' of the perplex number c = t + x h, defining the y-value of the pixel and the space component of z0."; + +pub const LINK_QUATERNION: &str = "https://doi.org/10.1007/s11071-023-08785-0"; +pub const TIP_QUATERNION: &str = "Quaternions extend complex numbers with two additional units j and k, alongside the imaginary unit i, to generate 4D fractal structures. Mandelbrot and Julia sets are visualized by selecting a 3D subspace, revealing intricate structures. See 'On the quaternion Julia sets via Picard–Mann iteration' for more information."; +pub const LABEL_BASIS_QUATERNION: &str = "Quaternion: a + b i + c j + d k"; +pub const TIP_FRACTAL_QUATERNION_RE: &str = "The real part 'a' of the quaternion z = a + b i + c j + d k, defining the x-value of the pixel and the real part of z0."; +pub const TIP_FRACTAL_QUATERNION_I: &str = "The i-component 'b' of the quaternion z = a + b i + c j + d k, defining the y-value of the pixel and the i-component of z0."; +pub const TIP_FRACTAL_QUATERNION_J: &str = "The j-component 'c' of the quaternion z = a + b i + c j + d k, defining the z-value of the pixel and the j-component of z0."; +pub const TIP_FRACTAL_QUATERNION_K: &str = "The k-component 'd' of the quaternion z = a + b i + c j + d k, usually fixed to visualize the dimensions a, b, and c."; + +pub const LABEL_WARMUP: &str = "Warm-Up Usage"; +pub const TIP_WARMUP: &str = "Choose whether to run the chaotic function several times before measurement for benchmarking purposes."; +pub const LABEL_NUM_WARMUPS: &str = "Number of Warm-Ups"; +pub const TIP_NUM_WARMUPS: &str = + "Specify the number of warm-up executions before starting the actual benchmark."; +pub const LABEL_NUM_ITERATIONS: &str = "Number of Iterations"; +pub const TIP_NUM_ITERATIONS: &str = "Determine the number of iterations for each benchmark run."; + +pub const LABEL_BENCHMARK: &str = "Benchmark Execution"; +pub const TIP_BENCHMARK: &str = "Execute the benchmark with the current settings."; diff --git a/src/lib.rs b/src/lib.rs index 4d7ce0b..06eefeb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ #![warn(clippy::all, rust_2018_idioms)] -mod app; +mod gui; mod chaos; -pub use app::TemplateApp; +mod utils; +pub use gui::ChaosApp; diff --git a/src/main.rs b/src/main.rs index 6a0466f..6c5c5a9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,7 +20,7 @@ fn main() -> eframe::Result<()> { eframe::run_native( "Rusty Chaos Craftor", native_options, - Box::new(|cc| Box::new(rusty_chaos_craftor::TemplateApp::new(cc))), + Box::new(|cc| Box::new(rusty_chaos_craftor::ChaosApp::new(cc))), ) } @@ -37,7 +37,7 @@ fn main() { .start( "chaos_canvas", // hardcode it web_options, - Box::new(|cc| Box::new(rusty_chaos_craftor::TemplateApp::new(cc))), + Box::new(|cc| Box::new(rusty_chaos_craftor::ChaosApp::new(cc))), ) .await .expect("failed to start eframe"); diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..988e228 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,62 @@ +use web_time::{Duration, Instant}; + +/// A simple timer for managing time intervals and execution timing. +#[derive(PartialEq, Eq)] +pub struct Timer { + interval: Duration, + last_triggered: Instant, +} + +impl Timer { + /// Creates a new timer with the specified interval. + /// + /// # Arguments + /// + /// * `interval` - The time interval between trigger events. + pub fn new(seconds: usize) -> Self { + Self { + interval: Duration::from_secs(seconds.try_into().unwrap()), + last_triggered: Instant::now(), + } + } + + /// Sets the frequency of the timer by specifying how many triggers per second are desired. + /// + /// # Arguments + /// + /// * `frequency` - The desired frequency in triggers per second. + /// + /// # Notes + /// + /// - If `frequency` is greater than 0, the timer's interval is set accordingly. + /// - If `frequency` is 0 or negative, the timer interval is set to a default value of 1 second. + pub fn set_frequency(&mut self, frequency: f64) { + if frequency > 0.0 { + let interval = Duration::from_secs_f64(1.0 / frequency); + self.interval = interval; + } else { + // Here, we set a default interval of 1 second. + self.interval = Duration::from_secs(1); + } + } + + /// Checks if the timer interval has elapsed and triggers the timer. + /// + /// # Returns + /// + /// Returns `true` if the interval has elapsed and `false` otherwise. + pub fn check_elapsed(&mut self) -> bool { + let now = Instant::now(); + if now.duration_since(self.last_triggered) >= self.interval { + self.last_triggered = now; + return true; + } + false + } +} + +impl Default for Timer { + fn default() -> Self { + Self::new(1) + } +}