diff --git a/src/lib.rs b/src/lib.rs index ddfa819825c97c7e7058069615a104d3e06368d5..d989e4f81c07e7da3b55d0c3e8117325bbbf424a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,12 @@ #![warn(clippy::all, rust_2018_idioms)] +mod transfer_functions; + #[allow(unused_imports)] use basic_print::basic_print; // basic print for print-debugging -use pole_position::PolePos; -use frequency_response::FreqResp; +use frequency_response_app::FreqResp; +use pole_position_app::PolePos; pub struct ControlApp { cur_app_idx: Option<usize>, @@ -12,8 +14,32 @@ pub struct ControlApp { } trait CentralApp { - fn draw_app(&mut self, ui: &mut egui::Ui); fn get_label(&self) -> &str; + fn draw_app(&mut self, ui: &mut egui::Ui); +} + +impl eframe::App for ControlApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::TopBottomPanel::top("app_selection_panel").show(ctx, |ui| { + if self.top_bar(ui) { + #[cfg(not(target_arch = "wasm32"))] // no quit on web pages! + _frame.close(); + } + }); + + egui::CentralPanel::default().show(ctx, |ui| { + ui.centered_and_justified(|ui| { + match self.cur_app_idx { + None => { + ui.label("Select an application in the bar above."); + } + Some(idx) => { + self.apps[idx].draw_app(ui); + } + }; + }); + }); + } } impl ControlApp { @@ -22,15 +48,23 @@ impl ControlApp { // `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`. let apps: Vec<Box<dyn CentralApp>> = vec![ - Box::new(PolePos::new("Pole Positioning".to_string() )), - Box::new(FreqResp::new("Frequency Response".to_string() )), + Box::new(PolePos::new("Pole Positioning".to_string())), + Box::new(FreqResp::new("Frequency Response".to_string())), ]; - // ControlApp{cur_app_idx: None, apps } - ControlApp{cur_app_idx: Some(0), apps } + if cfg!(debug_assertions) { + ControlApp { + cur_app_idx: Some(0), + apps, + } + } else { + ControlApp { + cur_app_idx: None, + apps, + } + } } - fn top_bar(&mut self, ui: &mut egui::Ui) -> bool { #[allow(unused_mut)] let mut quit = false; @@ -63,68 +97,37 @@ impl ControlApp { } egui::warn_if_debug_build(ui); }); - - }); - - return quit - } - -} - -impl eframe::App for ControlApp { - - fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - egui::TopBottomPanel::top("app_selection_panel").show(ctx, |ui| { - if self.top_bar(ui) { - #[cfg(not(target_arch = "wasm32"))] // no quit on web pages! - _frame.close(); - } }); - egui::CentralPanel::default().show(ctx, |ui| { - ui.centered_and_justified(|ui| { - match self.cur_app_idx { - None => { - ui.label("Select an application in the bar above."); - }, - Some(idx) => {self.apps[idx].draw_app(ui);}, - }; - - }); - }); + return quit; } } - -mod pole_position { +mod pole_position_app { #[allow(unused_imports)] use basic_print::basic_print; // basic print for print-debugging - use egui::plot::{ Line, LineStyle, Plot, PlotPoint, PlotPoints, PlotUi, Points, MarkerShape, }; - use egui::{ Ui, Color32, Vec2, Layout, Align, InnerResponse, Response, }; + use egui::{Ui, Vec2}; - use crate::LinearSystems::*; + use crate::transfer_functions::*; use crate::CentralApp; - use std::f64::consts::PI; - use std::ops::Range; - use std::rc::Rc; + use super::tf_plots; - - #[derive(PartialEq,Debug,Clone,Copy)] + #[derive(PartialEq, Debug, Clone, Copy)] enum Order { First, Second, } - #[derive(PartialEq,Debug,Clone,Copy)] + #[derive(PartialEq, Debug, Clone, Copy)] enum Display { StepResponse, BodeDiagram, } #[derive(Debug)] - pub struct PolePos{ + pub struct PolePos { label: String, order: Order, @@ -133,72 +136,49 @@ mod pole_position { fo: FirstOrderSystem, so: SecondOrderSystem, - pole_drag_offset: Option<(f64,f64)>, + pole_drag_offset: Option<(f64, f64)>, } - - impl PolePos { pub fn new(label: String) -> PolePos { - PolePos{ + PolePos { label, order: Order::First, display: Display::StepResponse, - fo: FirstOrderSystem{T: 1.0}, - so: SecondOrderSystem{d: 0.5, w: 0.75}, + fo: FirstOrderSystem { T: 1.0 }, + so: SecondOrderSystem { d: 0.5, w: 0.75 }, pole_drag_offset: None, } } - fn pole_plot(&mut self, ui: &mut Ui, width: f32, height: f32) { - // Plot params - let cross_radius = 10.0; - let re_bounds = -3.0..1.0; - let im_bounds = -1.0..1.0; - - - // Plot data - let p = match self.order { - Order::First => {self.fo.poles()}, - Order::Second => {self.so.poles()}, + let (dragged, pointer_coordinate) = match self.order { + Order::First => + tf_plots::pole_plot(&self.fo, ui, width, height), + Order::Second => + tf_plots::pole_plot(&self.so, ui, width, height), }; - let data = Points::new(p); - let unit_circle = Line::new(PlotPoints::from_parametric_callback(|t| (t.sin(), t.cos()), 0.0..(2.0*PI), 100)); - - // Plot - let resp = plot_show( - ui, "Pole Placement", - width, height, re_bounds, im_bounds, - |plot| {plot.data_aspect(1.0)}, - |plot_ui| { - plot_ui.line( unit_circle.color(Color32::GRAY) ); - plot_ui.points( data.shape(MarkerShape::Cross).color(Color32::BLACK).radius(cross_radius) ); - plot_ui.pointer_coordinate() - }); - let InnerResponse{ response: _, inner: (show_response, pointer) } = resp; - // Handle dragging - if show_response.dragged() { + if dragged { let (re_off, im_off) = match self.pole_drag_offset { - Some((x,y)) => (x,y), + Some((x, y)) => (x, y), None => { // We have just started to drag, find offset to closest pole so we don't jump // // For now, just assume we want to place the pole where we click self.pole_drag_offset = Some((0.0, 0.0)); - (0.0,0.0) - }, + (0.0, 0.0) + } }; - - if let Some(PlotPoint{x:re,y:im}) = pointer { // This should never fail - let (re, im) = (re+re_off, im+im_off); + if let Some((re,im)) = pointer_coordinate { + // This should never fail + let (re, im) = (re + re_off, im + im_off); match self.order { - Order::First => self.fo.adjust_poles_to(re,im), - Order::Second => self.so.adjust_poles_to(re,im), + Order::First => self.fo.adjust_poles_to(re, im), + Order::Second => self.so.adjust_poles_to(re, im), }; } } else { @@ -207,71 +187,21 @@ mod pole_position { } fn step_response_plot(&mut self, ui: &mut Ui, width: f32, height: f32) { - // Plot params - let n_samples = 100; - let t_end = 10.0; - let pad_ratio = 0.1; - - // Calculate plot bounds - let t_bounds = (0.0 - t_end*pad_ratio)..(t_end + t_end*pad_ratio); - let y_bounds = (0.0-pad_ratio)..(1.0+pad_ratio); - - // Plot data - let sys: Box<dyn LinearSystem> = match self.order { - Order::First => Box::new(self.fo), - Order::Second => Box::new(self.so), + let (_dragged, _pointer_coordinate) = match self.order { + Order::First => + tf_plots::step_response_plot(&self.fo, ui, width, height), + Order::Second => + tf_plots::step_response_plot(&self.so, ui, width, height), }; - let data = Line::new(PlotPoints::from_explicit_callback(move |t| sys.step_response(t), t_bounds.clone(), n_samples)); - - // Plot - plot_show( - ui, "Step Response", - width, height, t_bounds, y_bounds, - |plot| {plot}, - |plot_ui| { - plot_ui.line( data.color(Color32::RED).style(LineStyle::Solid) ); - }); } fn bode_plot(&mut self, ui: &mut Ui, width: f32, height: f32) { - // Plot params - let n_samples = 100; - let w_bounds_exp = -4.0..2.0; - - // Plot data - let sys: Rc<dyn LinearSystem> = match self.order { - Order::First => Rc::new(self.fo), - Order::Second => Rc::new(self.so), + let (_amp_dragged, _amp_pointer, _ph_dragged, _ph_pointer) = match self.order { + Order::First => tf_plots::bode_plot(&self.fo, ui, width, height), + Order::Second => tf_plots::bode_plot(&self.so, ui, width, height), }; - let sys_cb = sys.clone(); - let amp_data = Line::new(PlotPoints::from_explicit_callback(move |we| sys_cb.bode_amplitude(10f64.powf(we)).log10(), w_bounds_exp.clone(), n_samples)); - - let sys_cb = sys.clone(); - let phase_data = Line::new(PlotPoints::from_explicit_callback(move |we| sys_cb.bode_phase(10f64.powf(we)), w_bounds_exp.clone(), n_samples)); - - // Plot - ui.allocate_ui_with_layout( - Vec2{x: width, y: height}, - Layout::top_down(Align::LEFT), - |ui| { - let height = (height - ui.spacing().item_spacing.y)/2.0; - plot_show( - ui, "Bode Plote - Amplitude", width, height, w_bounds_exp.clone(), -4.0..5f64.log10(), - |plot| {plot}, - |plot_ui| { - plot_ui.line( amp_data.color(Color32::RED).style(LineStyle::Solid) ); - }); - plot_show( - ui, "Bode Plote - Phase", width, height, w_bounds_exp.clone(), -PI/2.0..PI/2.0, - |plot| {plot}, - |plot_ui| { - plot_ui.line( phase_data.color(Color32::RED).style(LineStyle::Solid) ); - }); - }); } - - fn order_selection(&mut self, ui: &mut Ui) { ui.heading("Select System Order"); ui.horizontal(|ui| { @@ -292,117 +222,130 @@ mod pole_position { match self.order { Order::First => { ui.heading("G(s) = 1/(sT - 1)"); - ui.add(egui::Slider::new(&mut self.fo.T, 0.2..=100.0) - .text("T") - .logarithmic(true) - ); - }, + ui.add( + egui::Slider::new(&mut self.fo.T, 0.2..=100.0) + .text("T") + .logarithmic(true), + ); + } Order::Second => { ui.heading("G(s) = ω^2/(s^2 + 2δωs+ ω^2)"); ui.add(egui::Slider::new(&mut self.so.d, 0.0..=1.5).text("δ")); ui.add(egui::Slider::new(&mut self.so.w, 0.0..=2.0).text("ω")); - }, + } }; } } - impl CentralApp for PolePos { fn draw_app(&mut self, ui: &mut Ui) { // ui.spacing_mut().item_spacing.x = 10.0; let max_width = 550.0; - let Vec2{x,y} = ui.available_size(); + let Vec2 { x, y } = ui.available_size(); let is_vertical = x < max_width; - if is_vertical { - - egui::Grid::new("app_grid") - .num_columns(1) - .show(ui, |ui| { - - ui.vertical(|ui| { - self.order_selection(ui); - ui.separator(); - self.display_selection(ui); - ui.separator(); - self.parameter_sliders(ui); - ui.separator(); - }); - ui.end_row(); - - let Vec2{x,y} = ui.available_size(); - let mut width = x; - let mut height = y/2.0; - - if width >= height*1.75 { - width = height*1.75 - } else { - height = width/1.75 - } - - self.pole_plot(ui, width, height); - ui.end_row(); - - match self.display { - Display::StepResponse => self.step_response_plot(ui, width, height), - Display::BodeDiagram => self.bode_plot(ui, width, height), - }; + egui::Grid::new("app_grid").num_columns(1).show(ui, |ui| { + ui.vertical(|ui| { + self.order_selection(ui); + ui.separator(); + self.display_selection(ui); + ui.separator(); + self.parameter_sliders(ui); + ui.separator(); }); + ui.end_row(); - } else { - let mut width = (x/2.0).min(max_width); - let mut height = y/2.0; + let Vec2 { x, y } = ui.available_size(); + let mut width = x; + let mut height = y / 2.0; - if width >= height*1.75 { - width = height*1.75 - } else { - height = width/1.75 - } + if width >= height * 1.75 { + width = height * 1.75 + } else { + height = width / 1.75 + } - egui::Grid::new("app_grid") - .num_columns(2) - .show(ui, |ui| { + self.pole_plot(ui, width, height); + ui.end_row(); - ui.vertical(|ui| { - self.order_selection(ui); - ui.add_space(20.0); - self.parameter_sliders(ui); - }); + match self.display { + Display::StepResponse => self.step_response_plot(ui, width, height), + Display::BodeDiagram => self.bode_plot(ui, width, height), + }; + }); + } else { + let mut width = (x / 2.0).min(max_width); + let mut height = y / 2.0; - self.pole_plot(ui, width, height); + if width >= height * 1.75 { + width = height * 1.75 + } else { + height = width / 1.75 + } - ui.end_row(); - self.step_response_plot(ui, width, height); - self.bode_plot(ui, width, height); + egui::Grid::new("app_grid").num_columns(2).show(ui, |ui| { + ui.vertical(|ui| { + self.order_selection(ui); + ui.add_space(20.0); + self.parameter_sliders(ui); }); - } + self.pole_plot(ui, width, height); + ui.end_row(); + self.step_response_plot(ui, width, height); + self.bode_plot(ui, width, height); + }); + } } fn get_label(&self) -> &str { &self.label } } +} + + - fn plot_show<R>( - ui: &mut Ui, title: &str, - width: f32, height: f32, x_bounds: Range<f64>, y_bounds: Range<f64>, +mod tf_plots { + use egui::plot::{ Line, LineStyle, MarkerShape, Plot, PlotPoints, PlotUi, Points, }; + use egui::{ Align, Color32, InnerResponse, Layout, Ui, Vec2, }; + + use std::f64::consts::PI; + use std::ops::Range; + + use crate::transfer_functions::*; + + // Helper that give a sane default plot window. Looks can be modified with the second to last + // argument and what is plotted is given by the last. Returns whether the plot is dragged by + // the mouse and the plot coordinate of the mouse. + fn plot_show( + ui: &mut Ui, + title: &str, + width: f32, + height: f32, + x_bounds: Range<f64>, + y_bounds: Range<f64>, plot_mod_fn: impl FnOnce(Plot) -> Plot, - build_fn: impl FnOnce(&mut PlotUi) -> R, - ) -> InnerResponse<(Response,R)> + build_fn: impl FnOnce(&mut PlotUi), + ) -> (bool, Option<(f64, f64)>) { - ui.allocate_ui_with_layout( - Vec2{x: width, y: height}, + let InnerResponse { + response: _, + inner: (dragged, pointer_coordinate), + } = ui.allocate_ui_with_layout( + Vec2 { + x: width, + y: height, + }, Layout::top_down(Align::LEFT), |ui| { ui.heading(title); - let mut plot = - Plot::new(title) + let mut plot = Plot::new(title) .allow_scroll(false) .allow_zoom(false) .allow_boxed_zoom(false) @@ -413,143 +356,170 @@ mod pole_position { .include_x(x_bounds.end) .include_y(y_bounds.start) .include_y(y_bounds.end) - .set_margin_fraction(Vec2{x:0.0, y:0.0}) - ; + .set_margin_fraction(Vec2 { x: 0.0, y: 0.0 }); plot = plot_mod_fn(plot); - let InnerResponse{response: show_response, inner: build_resp} = plot.show(ui, build_fn ); - (show_response, build_resp) - }) - } - -} - - - - + let InnerResponse { + response: show_response, + inner: pointer_coordinate, + } = plot.show(ui, |plot_ui| { + build_fn(plot_ui); + plot_ui.pointer_coordinate().map(|pp| (pp.x, pp.y)) + }); -pub mod LinearSystems { - #![allow(non_snake_case)] + (show_response.dragged(), pointer_coordinate) + }, + ); - pub trait LinearSystem { - fn step_response(&self, t: f64) -> f64; - fn bode_amplitude(&self, w: f64) -> f64; - fn bode_phase(&self, w: f64) -> f64; - fn poles(&self) -> Vec<[f64; 2]>; - fn adjust_poles_to(&mut self, re: f64, im: f64); + (dragged, pointer_coordinate) } - #[derive(Debug,Clone,Copy)] - pub struct FirstOrderSystem { - // first order system 1/(sT + 1) - // pole = -1/T - // https://www.tutorialspoint.com/control_systems/control_systems_response_first_order.htm - pub T: f64, + pub fn pole_plot( + tf: &impl TransferFunction, + ui: &mut Ui, + width: f32, + height: f32, + ) -> (bool, Option<(f64, f64)>) + { + // Plot params + let cross_radius = 10.0; + let re_bounds = -3.0..1.0; + let im_bounds = -1.0..1.0; + + // Plot data + let data = Points::new(tf.poles()); + let unit_circle = Line::new(PlotPoints::from_parametric_callback( + |t| (t.sin(), t.cos()), + 0.0..(2.0 * PI), + 100, + )); + + // Plot + plot_show( + ui, + "Pole Placement", + width, + height, + re_bounds, + im_bounds, + |plot| plot.data_aspect(1.0), + |plot_ui| { + plot_ui.line(unit_circle.color(Color32::GRAY)); + plot_ui.points( + data.shape(MarkerShape::Cross) + .color(Color32::BLACK) + .radius(cross_radius), + ); + }, + ) } - impl LinearSystem for FirstOrderSystem { - fn poles(&self) -> Vec<[f64; 2]> { - vec![[-1.0/self.T, 0.0]] - } - - fn step_response(&self, t: f64) -> f64 { - if t >= 0.0 { - 1.0 - (-t/self.T).exp() - } else { - 0.0 - } - } - - fn bode_amplitude(&self, w: f64) -> f64 { - 1.0 / ( ((w*self.T).powi(2) + 1.0).sqrt() ) - } - - fn bode_phase(&self, w: f64) -> f64 { - -(w*self.T).atan() - } - - fn adjust_poles_to(&mut self, re: f64, _im: f64) { - let pole_bound = -0.01; - - - if re >= pole_bound { - self.T = -1.0/pole_bound; - } else { - self.T = -1.0/re; - } + pub fn step_response_plot( + tf: &impl TransferFunction, + ui: &mut Ui, + width: f32, + height: f32, + ) -> (bool, Option<(f64, f64)>) + { + // Plot params + let n_samples = 100; + let t_end = 10.0; + let pad_ratio = 0.1; + + // Calculate plot bounds + let t_bounds = (0.0 - t_end * pad_ratio)..(t_end + t_end * pad_ratio); + let y_bounds = (0.0 - pad_ratio)..(1.0 + pad_ratio); + + // Calc plot data + let step = (t_bounds.end - t_bounds.start) / ((n_samples - 1) as f64); + let mut points: Vec<[f64; 2]> = Vec::new(); + for i in 0..n_samples { + let t = t_bounds.start + step * (i as f64); + points.push([t, tf.step_response(t)]); } + let data = Line::new(points); + + // Plot + plot_show( + ui, + "Step Response", + width, + height, + t_bounds, + y_bounds, + |plot| plot, + |plot_ui| { + plot_ui.line(data.color(Color32::RED).style(LineStyle::Solid)); + }, + ) } - #[derive(Debug,Clone,Copy)] - pub struct SecondOrderSystem { // TODO fix this, the step response is not corerct - // second order system w^2/(s^2 + 2dw s + w^2) - // poles = -dw +- w sqrt(d^2 - 1) - // https://www.tutorialspoint.com/control_systems/control_systems_response_second_order.htm - pub d: f64, - pub w: f64, - } - - impl LinearSystem for SecondOrderSystem { - fn poles(&self) -> Vec<[f64; 2]> { - let (d,w) = (self.d, self.w); - - if d == 0.0 { - vec![ - [0.0, w], - [0.0, -w], - ] - } else if (0.0 < d) && (d < 1.0) { - vec![ - [-d*w, (1.0-d.powi(2)).sqrt()*w], - [-d*w, -(1.0-d.powi(2)).sqrt()*w], - ] - } else if d == 1.0 { - vec![ - [-w, 0.0], - [-w, 0.0], - ] - } else { - vec![ - [-d*w + (d.powi(2) -1.0).sqrt()*w, 0.0], - [-d*w - (d.powi(2) -1.0).sqrt()*w, 0.0], - ] - } - } - - fn step_response(&self, t: f64) -> f64 { - let (d,w) = (self.d, self.w); - - if t < 0.0 { - return 0.0 - } - - if d == 0.0 { - 1.0 - (w*t).cos() - } else if (0.0 < d) && (d < 1.0) { - let d_1_sqrt = (1.0 - d.powi(2)).sqrt(); - let th = d_1_sqrt.asin(); - 1.0 - ( (-w*t).exp() )*( (w*t + th).sin() )/ d_1_sqrt - } else if d == 1.0 { - 1.0 - ( (-w*t).exp() )*(1.0 + w*t) - } else { - let d_1_sqrt = (d.powi(2)-1.0).sqrt(); - 1.0 - + (-t*w*(d + d_1_sqrt)).exp() / (2.0*(d+d_1_sqrt)*d_1_sqrt) - - (-t*w*(d - d_1_sqrt)).exp() / (2.0*(d-d_1_sqrt)*d_1_sqrt) - } - } - - fn bode_amplitude(&self, _w: f64) -> f64 { - 1.0 - } - - fn bode_phase(&self, _w: f64) -> f64 { - 0.0 + pub fn bode_plot( + tf: &impl TransferFunction, + ui: &mut Ui, + width: f32, + height: f32, + ) -> (bool, Option<(f64, f64)>, bool, Option<(f64, f64)>) + { + // Plot params + let n_samples = 100; + let w_bounds_exp = -4.0..2.0; + + // Calc plot data + let step = (w_bounds_exp.end - w_bounds_exp.start) / ((n_samples - 1) as f64); + let mut amp_points: Vec<[f64; 2]> = Vec::new(); + let mut phase_points: Vec<[f64; 2]> = Vec::new(); + for i in 0..n_samples { + let we = w_bounds_exp.start + step * (i as f64); + let w = 10f64.powf(we); + amp_points.push([we, tf.bode_amplitude(w).log10()]); + phase_points.push([we, tf.bode_phase(w)]); } + let amp_data = Line::new(amp_points); + let phase_data = Line::new(phase_points); + + // Plot + let InnerResponse { + response: _, + inner: (amp_dragged, amp_pointer, ph_dragged, ph_pointer), + } = ui.allocate_ui_with_layout( + Vec2 { + x: width, + y: height, + }, + Layout::top_down(Align::LEFT), + |ui| { + let height = (height - ui.spacing().item_spacing.y) / 2.0; + let (amp_dragged, amp_pointer) = plot_show( + ui, + "Bode Plote - Amplitude", + width, + height, + w_bounds_exp.clone(), + -4.0..5f64.log10(), + |plot| plot, + |plot_ui| { + plot_ui.line(amp_data.color(Color32::RED).style(LineStyle::Solid)); + }, + ); + let (ph_dragged, ph_pointer) = plot_show( + ui, + "Bode Plote - Phase", + width, + height, + w_bounds_exp.clone(), + -PI / 2.0..PI / 2.0, + |plot| plot, + |plot_ui| { + plot_ui.line(phase_data.color(Color32::RED).style(LineStyle::Solid)); + }, + ); + (amp_dragged, amp_pointer, ph_dragged, ph_pointer) + }, + ); - fn adjust_poles_to(&mut self, _re: f64, _im: f64) { - } + (amp_dragged, amp_pointer, ph_dragged, ph_pointer) } } @@ -557,29 +527,16 @@ pub mod LinearSystems { - - - - - - - - - - - -mod frequency_response { +mod frequency_response_app { use crate::CentralApp; - pub struct FreqResp{ + pub struct FreqResp { label: String, } impl FreqResp { pub fn new(label: String) -> FreqResp { - FreqResp{ - label, - } + FreqResp { label } } } diff --git a/src/transfer_functions.rs b/src/transfer_functions.rs new file mode 100644 index 0000000000000000000000000000000000000000..caaad8e1783f703bccdcc3688b0393f5e44680ce --- /dev/null +++ b/src/transfer_functions.rs @@ -0,0 +1,115 @@ +#![allow(non_snake_case)] + +pub trait TransferFunction { + fn step_response(&self, t: f64) -> f64; + fn bode_amplitude(&self, w: f64) -> f64; + fn bode_phase(&self, w: f64) -> f64; + fn poles(&self) -> Vec<[f64; 2]>; + fn adjust_poles_to(&mut self, re: f64, im: f64); +} + +#[derive(Debug, Clone, Copy)] +pub struct FirstOrderSystem { + // first order system 1/(sT + 1) + // pole = -1/T + // https://www.tutorialspoint.com/control_systems/control_systems_response_first_order.htm + pub T: f64, +} + +impl TransferFunction for FirstOrderSystem { + fn poles(&self) -> Vec<[f64; 2]> { + vec![[-1.0 / self.T, 0.0]] + } + + fn step_response(&self, t: f64) -> f64 { + if t >= 0.0 { + 1.0 - (-t / self.T).exp() + } else { + 0.0 + } + } + + fn bode_amplitude(&self, w: f64) -> f64 { + 1.0 / (((w * self.T).powi(2) + 1.0).sqrt()) + } + + fn bode_phase(&self, w: f64) -> f64 { + -(w * self.T).atan() + } + + fn adjust_poles_to(&mut self, re: f64, _im: f64) { + let pole_bound = -0.01; + + if re >= pole_bound { + self.T = -1.0 / pole_bound; + } else { + self.T = -1.0 / re; + } + } +} + + + +#[derive(Debug, Clone, Copy)] +pub struct SecondOrderSystem { + // TODO fix this, the step response is not corerct + // second order system w^2/(s^2 + 2dw s + w^2) + // poles = -dw +- w sqrt(d^2 - 1) + // https://www.tutorialspoint.com/control_systems/control_systems_response_second_order.htm + pub d: f64, + pub w: f64, +} + +impl TransferFunction for SecondOrderSystem { + fn poles(&self) -> Vec<[f64; 2]> { + let (d, w) = (self.d, self.w); + + if d == 0.0 { + vec![[0.0, w], [0.0, -w]] + } else if (0.0 < d) && (d < 1.0) { + vec![ + [-d * w, (1.0 - d.powi(2)).sqrt() * w], + [-d * w, -(1.0 - d.powi(2)).sqrt() * w], + ] + } else if d == 1.0 { + vec![[-w, 0.0], [-w, 0.0]] + } else { + vec![ + [-d * w + (d.powi(2) - 1.0).sqrt() * w, 0.0], + [-d * w - (d.powi(2) - 1.0).sqrt() * w, 0.0], + ] + } + } + + fn step_response(&self, t: f64) -> f64 { + let (d, w) = (self.d, self.w); + + if t < 0.0 { + return 0.0; + } + + if d == 0.0 { + 1.0 - (w * t).cos() + } else if (0.0 < d) && (d < 1.0) { + let d_1_sqrt = (1.0 - d.powi(2)).sqrt(); + let th = d_1_sqrt.asin(); + 1.0 - ((-w * t).exp()) * ((w * t + th).sin()) / d_1_sqrt + } else if d == 1.0 { + 1.0 - ((-w * t).exp()) * (1.0 + w * t) + } else { + let d_1_sqrt = (d.powi(2) - 1.0).sqrt(); + 1.0 + (-t * w * (d + d_1_sqrt)).exp() / (2.0 * (d + d_1_sqrt) * d_1_sqrt) + - (-t * w * (d - d_1_sqrt)).exp() / (2.0 * (d - d_1_sqrt) * d_1_sqrt) + } + } + + fn bode_amplitude(&self, _w: f64) -> f64 { + 1.0 + } + + fn bode_phase(&self, _w: f64) -> f64 { + 0.0 + } + + fn adjust_poles_to(&mut self, _re: f64, _im: f64) {} +}