use scirs2_core::ndarray::{Array1, Array2};
use scirs2_core::num_complex::Complex64;
use std::fmt;
pub mod pid;
pub mod stability;
pub mod state_space;
pub mod transfer_function;
pub use pid::{PIDController, TuningMethod};
pub use stability::{BodePlotData, NyquistPlotData, RouthHurwitz, StabilityMargins};
pub use state_space::StateSpace;
pub use transfer_function::TransferFunction;
pub type ControlResult<T> = Result<T, ControlError>;
#[derive(Debug, Clone)]
pub enum ControlError {
DimensionMismatch { expected: String, actual: String },
InvalidPolynomial(String),
NotControllable,
NotObservable,
Unstable(String),
InvalidParameters(String),
NumericalError(String),
ConversionError(String),
InvalidSystem(String),
LinAlgError(String),
General(String),
}
impl fmt::Display for ControlError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ControlError::DimensionMismatch { expected, actual } => {
write!(
f,
"Dimension mismatch: expected {}, got {}",
expected, actual
)
}
ControlError::InvalidPolynomial(msg) => {
write!(f, "Invalid polynomial: {}", msg)
}
ControlError::NotControllable => {
write!(f, "System is not controllable")
}
ControlError::NotObservable => {
write!(f, "System is not observable")
}
ControlError::Unstable(msg) => {
write!(f, "System is unstable: {}", msg)
}
ControlError::InvalidParameters(msg) => {
write!(f, "Invalid parameters: {}", msg)
}
ControlError::NumericalError(msg) => {
write!(f, "Numerical error: {}", msg)
}
ControlError::ConversionError(msg) => {
write!(f, "Conversion error: {}", msg)
}
ControlError::InvalidSystem(msg) => {
write!(f, "Invalid system: {}", msg)
}
ControlError::LinAlgError(msg) => {
write!(f, "Linear algebra error: {}", msg)
}
ControlError::General(msg) => {
write!(f, "Control error: {}", msg)
}
}
}
}
impl std::error::Error for ControlError {}
impl From<ControlError> for crate::error::NumRs2Error {
fn from(err: ControlError) -> Self {
crate::error::NumRs2Error::ControlError(err.to_string())
}
}
#[derive(Debug, Clone)]
pub struct FrequencyResponse {
pub frequencies: Array1<f64>,
pub magnitude: Array1<f64>,
pub phase: Array1<f64>,
}
impl FrequencyResponse {
pub fn new(
frequencies: Array1<f64>,
magnitude: Array1<f64>,
phase: Array1<f64>,
) -> ControlResult<Self> {
if frequencies.len() != magnitude.len() || frequencies.len() != phase.len() {
return Err(ControlError::DimensionMismatch {
expected: format!(
"all arrays same length as frequencies ({})",
frequencies.len()
),
actual: format!("magnitude: {}, phase: {}", magnitude.len(), phase.len()),
});
}
Ok(Self {
frequencies,
magnitude,
phase,
})
}
pub fn magnitude_db(&self) -> Array1<f64> {
self.magnitude.mapv(|m| 20.0 * m.log10())
}
pub fn phase_deg(&self) -> Array1<f64> {
self.phase.mapv(|p| p.to_degrees())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SystemType {
Continuous,
Discrete { sample_time: u64 }, }
impl SystemType {
pub fn is_continuous(&self) -> bool {
matches!(self, SystemType::Continuous)
}
pub fn is_discrete(&self) -> bool {
matches!(self, SystemType::Discrete { .. })
}
pub fn sample_time(&self) -> Option<f64> {
match self {
SystemType::Discrete { sample_time } => Some(*sample_time as f64 / 1_000_000.0),
SystemType::Continuous => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_frequency_response_creation() {
let freqs = Array1::from_vec(vec![1.0, 10.0, 100.0]);
let mags = Array1::from_vec(vec![1.0, 0.5, 0.1]);
let phases = Array1::from_vec(vec![0.0, -0.5, -1.0]);
let fr = FrequencyResponse::new(freqs.clone(), mags.clone(), phases.clone());
assert!(fr.is_ok());
let fr = fr.expect("test: valid frequency response creation");
assert_eq!(fr.frequencies, freqs);
assert_eq!(fr.magnitude, mags);
assert_eq!(fr.phase, phases);
}
#[test]
fn test_frequency_response_dimension_mismatch() {
let freqs = Array1::from_vec(vec![1.0, 10.0, 100.0]);
let mags = Array1::from_vec(vec![1.0, 0.5]);
let phases = Array1::from_vec(vec![0.0, -0.5, -1.0]);
let fr = FrequencyResponse::new(freqs, mags, phases);
assert!(fr.is_err());
}
#[test]
fn test_magnitude_db_conversion() {
let freqs = Array1::from_vec(vec![1.0]);
let mags = Array1::from_vec(vec![10.0]);
let phases = Array1::from_vec(vec![0.0]);
let fr =
FrequencyResponse::new(freqs, mags, phases).expect("test: valid frequency response");
let mag_db = fr.magnitude_db();
assert!((mag_db[0] - 20.0).abs() < 1e-10);
}
#[test]
fn test_phase_deg_conversion() {
let freqs = Array1::from_vec(vec![1.0]);
let mags = Array1::from_vec(vec![1.0]);
let phases = Array1::from_vec(vec![std::f64::consts::PI]);
let fr =
FrequencyResponse::new(freqs, mags, phases).expect("test: valid frequency response");
let phase_deg = fr.phase_deg();
assert!((phase_deg[0] - 180.0).abs() < 1e-10);
}
#[test]
fn test_system_type() {
let cont = SystemType::Continuous;
assert!(cont.is_continuous());
assert!(!cont.is_discrete());
assert_eq!(cont.sample_time(), None);
let disc = SystemType::Discrete {
sample_time: 100_000,
}; assert!(!disc.is_continuous());
assert!(disc.is_discrete());
assert!(
(disc
.sample_time()
.expect("test: discrete system has sample time")
- 0.1)
.abs()
< 1e-10
);
}
#[test]
fn test_error_conversion() {
let err = ControlError::InvalidPolynomial("test".to_string());
let numrs_err: crate::error::NumRs2Error = err.into();
match numrs_err {
crate::error::NumRs2Error::ControlError(msg) => {
assert!(msg.contains("Invalid polynomial"));
}
_ => panic!("Expected ControlError"),
}
}
}