Skip to content
Snippets Groups Projects
newton.rs 13.2 KiB
Newer Older
//! Provides definitions related to a Newtonian dynamics.

whooie's avatar
whooie committed
use std::{
    ops::{
        Neg,
        Add,
        AddAssign,
        Sub,
        SubAssign,
        Mul,
        MulAssign,
        Div,
        DivAssign,
    },
};
use ndarray::{
    self as nd,
    array,
};
use thiserror::Error;

#[derive(Error, Debug)]
pub enum NewtonError {
    #[error("rka: error bound could not be satisfied")]
    RKAErrorBound,
}
pub type NewtonResult<T> = Result<T, NewtonError>;

/// A vector in $`\mathbb{R}^3`$. Cartesian coordinates are assumed.
whooie's avatar
whooie committed
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct ThreeVector(pub f64, pub f64, pub f64);

impl ThreeVector {
    /// Return a new vector with zero magnitude.
    pub fn zero() -> Self { Self(0.0, 0.0, 0.0) }

    /// Generate from spherical coordinates $`(r, \theta, \phi)`$ where $`\phi`$
    /// is the azimuthal angle.
whooie's avatar
whooie committed
    pub fn from_angles(r: f64, theta: f64, phi: f64) -> Self {
        return r * Self(
            phi.cos() * theta.sin(),
            phi.sin() * theta.sin(),
            theta.cos(),
        );
    }

    /// Generate from cylindrical coordinates $`(r, \phi, z)`$.
whooie's avatar
whooie committed
    pub fn from_angles_cylindrical(r: f64, phi: f64, z: f64) -> Self {
        return Self(
            r * phi.cos(),
            r * phi.sin(),
            z
        );
    }

    /// Generate along a Cartesian axis.
    pub fn from_axis(r: f64, axis: Axis) -> Self {
        return match axis {
            Axis::X => Self(r, 0.0, 0.0),
            Axis::Y => Self(0.0, r, 0.0),
            Axis::Z => Self(0.0, 0.0, r),
        };
    }

    /// Magnitude of the vector.
whooie's avatar
whooie committed
    pub fn norm(&self) -> f64 {
        return (self.0.powi(2) + self.1.powi(2) + self.2.powi(2)).sqrt();
    }

    /// Create a new, normalized version of `self`.
whooie's avatar
whooie committed
    pub fn normalized(&self) -> Self {
        return *self / self.norm();
    }

    /// Apply the absolute value function to each component.
whooie's avatar
whooie committed
    pub fn abs(&self) -> Self {
        return Self(self.0.abs(), self.1.abs(), self.2.abs());
    }

    /// Convert to a 3-element array.
whooie's avatar
whooie committed
    pub fn to_array(&self) -> nd::Array1<f64> {
        return array![self.0, self.1, self.2];
    }

    /// Convert to a 3-element array, consuming `self`.
whooie's avatar
whooie committed
    pub fn into_array(self) -> nd::Array1<f64> {
        return array![self.0, self.1, self.2];
    }

    /// Get the component corresponding to an axis.
whooie's avatar
whooie committed
    pub fn get_component<A>(&self, axis: A) -> f64
    where A: Into<Axis>
    {
        return match axis.into() {
            Axis::X => self.0,
            Axis::Y => self.1,
            Axis::Z => self.2,
        };
    }

    /// Get all components except for that corresponding to an axis. Returned
    /// components are in $`x \rightarrow y \rightarrow z`$ order.
whooie's avatar
whooie committed
    pub fn get_components_except<A>(&self, axis: A) -> (f64, f64)
    where A: Into<Axis>
    {
        return match axis.into() {
            Axis::X => (self.1, self.2),
            Axis::Y => (self.0, self.2),
            Axis::Z => (self.0, self.1),
        };
    }
}

impl Neg for ThreeVector {
    type Output = Self;
    fn neg(self) -> Self { Self(-self.0, -self.1, -self.2) }
}

impl Add<ThreeVector> for ThreeVector {
    type Output = Self;
    fn add(self, rhs: Self) -> Self {
        return Self(self.0 + rhs.0, self.1 + rhs.1, self.2 + rhs.2);
    }
}

impl AddAssign<ThreeVector> for ThreeVector {
    fn add_assign(&mut self, rhs: Self) {
        self.0 += rhs.0;
        self.1 += rhs.1;
        self.2 += rhs.2;
    }
}

impl Sub<ThreeVector> for ThreeVector {
    type Output = Self;
    fn sub(self, rhs: Self) -> Self {
        return Self(self.0 - rhs.0, self.1 - rhs.1, self.2 - rhs.2);
    }
}

impl SubAssign<ThreeVector> for ThreeVector {
    fn sub_assign(&mut self, rhs: Self) {
        self.0 -= rhs.0;
        self.1 -= rhs.1;
        self.2 -= rhs.2;
    }
}

impl Mul<f64> for ThreeVector {
    type Output = Self;
    fn mul(self, rhs: f64) -> Self {
        return Self(self.0 * rhs, self.1 * rhs, self.2 * rhs);
    }
}

impl MulAssign<f64> for ThreeVector {
    fn mul_assign(&mut self, rhs: f64) {
        self.0 *= rhs;
        self.1 *= rhs;
        self.2 *= rhs;
    }
}

impl Mul<ThreeVector> for f64 {
    type Output = ThreeVector;
    fn mul(self, rhs: ThreeVector) -> ThreeVector {
        return ThreeVector(self * rhs.0, self * rhs.1, self * rhs.2);
    }
}

impl Div<f64> for ThreeVector {
    type Output = Self;
    fn div(self, rhs: f64) -> Self {
        return ThreeVector(self.0 / rhs, self.1 / rhs, self.2 / rhs);
    }
}

impl DivAssign<f64> for ThreeVector {
    fn div_assign(&mut self, rhs: f64) {
        self.0 /= rhs;
        self.1 /= rhs;
        self.2 /= rhs;
    }
}

/// A vector in $`\mathbb{R}^3 \times \mathbb{R}^3`$ phase space. Position- and
/// momentum-space vectors are separated.
whooie's avatar
whooie committed
#[derive(Copy, Clone, Debug)]
pub struct PhaseSpace {
    pub pos: ThreeVector,
    pub mom: ThreeVector,
}

impl PhaseSpace {
    /// Calculate the magnitude of the total vector (i.e. all six components).
whooie's avatar
whooie committed
    pub fn norm(&self) -> f64 {
        return (self.pos.norm().powi(2) + self.mom.norm().powi(2)).sqrt();
    }

    /// Create a new version of `self` where the position- and momentum-space
    /// vectors have been normalized independently, giving a 6-component norm of
    /// $`\sqrt{2}`$.
whooie's avatar
whooie committed
    pub fn normalized(&self) -> Self {
        return Self { pos: self.pos.normalized(), mom: self.mom.normalized() };
    }

    /// Create a new version of `self` where the position- and momentum-space
    /// vectors have been normalized together, giving a 6-component norm of
    /// $`1`$.
    pub fn normalized6(&self) -> Self {
        return *self / self.norm();
    }

    /// Apply the absolute value function to each component.
whooie's avatar
whooie committed
    pub fn abs(&self) -> Self {
        return Self { pos: self.pos.abs(), mom: self.mom.abs() };
    }

    /// Convert to a 6-element array.
whooie's avatar
whooie committed
    pub fn to_array(&self) -> nd::Array1<f64> {
        return array![
            self.pos.0, self.pos.1, self.pos.2,
            self.mom.0, self.mom.1, self.mom.2,
        ];
    }

    /// Convert to a 6-element array, consuming `self`.
whooie's avatar
whooie committed
    pub fn into_array(self) -> nd::Array1<f64> {
        return array![
            self.pos.0, self.pos.1, self.pos.2,
            self.mom.0, self.mom.1, self.mom.2,
        ];
    }

    /// Get the components of both position- and momentum-space vectors
    /// corresponding to an axis. The position-space component is listed first.
whooie's avatar
whooie committed
    pub fn get_component<A>(&self, axis: A) -> (f64, f64)
    where A: Into<Axis>
    {
        return match axis.into() {
            Axis::X => (self.pos.0, self.mom.0),
            Axis::Y => (self.pos.1, self.mom.1),
            Axis::Z => (self.pos.2, self.mom.2),
        };
    }

    /// Get all components of both position- and momentum-space vectors
    /// except those corresponding to an axis. Returned components are in $`x
    /// \rightarrow y \rightarrow z`$ order. The position-space components are
    /// listed first.
whooie's avatar
whooie committed
    pub fn get_components_except<A>(&self, axis: A) -> ((f64, f64), (f64, f64))
    where A: Into<Axis>
    {
        return match axis.into() {
            Axis::X => ((self.pos.1, self.pos.2), (self.mom.1, self.mom.2)),
            Axis::Y => ((self.pos.0, self.pos.2), (self.mom.0, self.mom.2)),
            Axis::Z => ((self.pos.0, self.pos.1), (self.mom.0, self.mom.1)),
        };
    }
}

impl Neg for PhaseSpace {
    type Output = Self;
    fn neg(self) -> Self { Self { pos: -self.pos, mom: -self.mom } }
}

impl Add<PhaseSpace> for PhaseSpace {
    type Output = Self;
    fn add(self, rhs: Self) -> Self {
        return Self { pos: self.pos + rhs.pos, mom: self.mom + rhs.mom };
    }
}

impl Sub<PhaseSpace> for PhaseSpace {
    type Output = Self;
    fn sub(self, rhs: Self) -> Self {
        return Self { pos: self.pos - rhs.pos, mom: self.pos - rhs.mom };
    }
}

impl Mul<f64> for PhaseSpace {
    type Output = Self;
    fn mul(self, rhs: f64) -> Self {
        return Self { pos: self.pos * rhs, mom: self.mom * rhs };
    }
}

impl Mul<PhaseSpace> for f64 {
    type Output = PhaseSpace;
    fn mul(self, rhs: PhaseSpace) -> PhaseSpace {
        return PhaseSpace { pos: self * rhs.pos, mom: self * rhs.mom };
    }
}

impl Div<f64> for PhaseSpace {
    type Output = Self;
    fn div(self, rhs: f64) -> Self {
        return PhaseSpace { pos: self.pos / rhs, mom: self.mom / rhs };
    }
}

/// Convert a slice of `ThreeVector`s to a single two-dimensional array of shape
/// `(3, n)` where `n` is the length of the slice.
whooie's avatar
whooie committed
pub fn traj_as_array(traj: &[ThreeVector]) -> nd::Array2<f64> {
    let arrays: Vec<nd::Array1<f64>>
        = traj.iter().map(|vk| vk.to_array()).collect();
    return nd::stack(
        nd::Axis(1),
        &arrays.iter()
            .map(|ak| ak.view())
            .collect::<Vec<nd::ArrayView1<f64>>>(),
    ).unwrap();
}

/// Convert a slice of `PhaseSpace`s to a single two-dimensional array of shape
/// `(6, n)` where `n` is the length of the slice.
whooie's avatar
whooie committed
pub fn phasespace_traj_as_array(traj: &[PhaseSpace]) -> nd::Array2<f64> {
    let arrays: Vec<nd::Array1<f64>>
        = traj.iter().map(|qk| qk.to_array()).collect();
    return nd::stack(
        nd::Axis(1),
        &arrays.iter()
            .map(|ak| ak.view())
            .collect::<Vec<nd::ArrayView1<f64>>>(),
    ).unwrap();
}

/// A single Cartesian axis.
whooie's avatar
whooie committed
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Axis {
    X = 0,
    Y = 1,
    Z = 2,
}

impl From<usize> for Axis {
    fn from(axis: usize) -> Self {
        return match axis {
            0 => Axis::X,
            1 => Axis::Y,
            _ => Axis::Z,
        };
    }
}

impl From<Axis> for usize {
    fn from(axis: Axis) -> Self { axis as usize }
}

/// Perform a single fourth-order Runge-Kutta step.
whooie's avatar
whooie committed
fn rk4_step<F>(x: f64, y: PhaseSpace, dx: f64, rhs: &F) -> PhaseSpace
where F: Fn(f64, PhaseSpace) -> PhaseSpace
{
    let x_half: f64 = x + dx / 2.0;
    let k1: PhaseSpace = rhs(x, y);
    let k2: PhaseSpace = rhs(x_half, y + dx / 2.0 * k1);
    let k3: PhaseSpace = rhs(x_half, y + dx / 2.0 * k2);
    let k4: PhaseSpace = rhs(x + dx, y + dx * k3);
    return y + dx / 6.0 * (k1 + 2.0 * (k2 + k3) + k4);
}

/// Perform a single fourth-order Runge-Kutta step with error estimation,
/// returning the value of the independent variable after the step and a
/// recommended size for the next step along with the `PhaseSpace` vector.
whooie's avatar
whooie committed
pub fn rka_step<F>(x: f64, y: PhaseSpace, dx: f64, rhs: &F, err: f64)
    -> NewtonResult<(f64, PhaseSpace, f64)>
where F: Fn(f64, PhaseSpace) -> PhaseSpace
{
    // define safety numbers for choosing next step size -- particular to rk4
    let safe1: f64 = 0.9;
    let safe2: f64 = 4.0;

    let mut dx_old: f64;
    let mut dx_new: f64 = dx;
    let (mut dx_cond1, mut dx_cond2): (f64, f64);
    let (mut dx_half, mut x_half, mut x_full): (f64, f64, f64);
    let (mut y_half, mut y_half2, mut y_full):
        (PhaseSpace, PhaseSpace, PhaseSpace);
    let (mut scale, mut diff, mut error_ratio): (PhaseSpace, PhaseSpace, f64);
    for _ in 0..100 {
        // take two half-sized steps
        dx_half = dx / 2.0;
        x_half = x + dx_half;
        y_half = rk4_step(x, y, dx_half, rhs);
        y_half2 = rk4_step(x_half, y_half, dx_half, rhs);

        // take one full-sized step
        x_full = x + dx;
        y_full = rk4_step(x, y, dx, rhs);

        // compute the estimated local truncation error
        scale = err * (y_half2.abs() + y_full.abs()) / 2.0;
        diff = (y_half2 - y_full).abs();
        error_ratio
            = scale.into_array().into_iter().zip(diff.into_array().into_iter())
            .map(|(s, d)| d / (s + f64::EPSILON))
            .max_by(|l, r| {
                match l.partial_cmp(r) {
                    Some(ord) => ord,
                    None => std::cmp::Ordering::Less,
                }
            }).unwrap();

        // estimate new step size (with safety factors)
        dx_old = dx_new;
        if error_ratio == 0.0 {
            dx_new = dx_old / safe2;
            continue;
        }
        dx_new = dx_old * error_ratio.powf(-0.2) * safe1;
        dx_cond1 = dx_old / safe2;
        dx_cond2 = dx_old * safe2;
        dx_new = if dx_cond1 > dx_new { dx_cond1 } else { dx_new };
        dx_new = if dx_cond2 < dx_new { dx_cond2 } else { dx_new };

        if error_ratio < 1.0 {
            return Ok((x_full, y_half2, dx_new));
        }
    }
    return Err(NewtonError::RKAErrorBound);
}

/// Perform fourth-order Runge-Kutta numerical integration with adaptive
/// stepsize via truncation error estimation.
whooie's avatar
whooie committed
pub fn rka<F>(
    x_bounds: (f64, f64),
    y0: PhaseSpace,
    dx0: f64,
    rhs: F,
    epsilon: f64
) -> NewtonResult<(Vec<f64>, Vec<PhaseSpace>)>
where F: Fn(f64, PhaseSpace) -> PhaseSpace
{
    let mut x: Vec<f64> = vec![x_bounds.0];
    let mut x_prev: f64 = x_bounds.0;
    let mut x_next: f64;
    let mut dx: f64 = dx0;
    let mut y: Vec<PhaseSpace> = vec![y0];
    let mut y_prev: &PhaseSpace = y.last().unwrap();
    let mut y_next: PhaseSpace;
    let mut step: (f64, PhaseSpace, f64);
    while x_prev < x_bounds.1 {
        dx = dx.min(x_bounds.1 - x_prev);
        
        step = rka_step(x_prev, *y_prev, dx, &rhs, epsilon)?;
        x_next = step.0;
        y_next = step.1;
        dx = step.2;

        x.push(x_next);
        y.push(y_next);

        x_prev = x_next;
        y_prev = y.last().unwrap();
    }
    return Ok((x, y));
}