rssn 0.2.9

A comprehensive scientific computing library for Rust, aiming for feature parity with NumPy and SymPy.
Documentation
//! # Electromagnetism Module
//!
//! This module provides symbolic representations of fundamental concepts and equations
//! in classical electromagnetism.
//!
//! ## Key Concepts Covered:
//! - **Maxwell's Equations**: Gauss's laws, Faraday's law, and the Ampère-Maxwell law.
//! - **Lorentz Force**: Force on a charged particle in electromagnetic fields.
//! - **Potentials**: Electric scalar potential $V$ and magnetic vector potential $\mathbf{A}$.
//! - **Energy & Power**: Energy density and the Poynting vector.
//! - **Electrostatics & Magnetostatics**: Coulomb's Law and Biot-Savart Law.

use serde::Deserialize;
use serde::Serialize;

use crate::symbolic::calculus::differentiate;
use crate::symbolic::core::Expr;
use crate::symbolic::simplify_dag::simplify;
use crate::symbolic::vector::Vector;
use crate::symbolic::vector::curl;
use crate::symbolic::vector::divergence;
use crate::symbolic::vector::gradient;

/// Represents Maxwell's equations in their differential form.
///
/// These equations describe how electric ($\mathbf{E}$) and magnetic ($\mathbf{B}$) fields
/// are generated by charges ($\rho$) and currents ($\mathbf{J}$).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MaxwellEquations {
    /// Gauss's Law for Electricity: $\nabla \cdot \mathbf{E} = \`frac{\rho}{\epsilon_0`}$
    pub gauss_law_electric: Expr,
    /// Gauss's Law for Magnetism: $\nabla \cdot \mathbf{B} = 0$
    pub gauss_law_magnetic: Expr,
    /// Faraday's Law: $\nabla \times \mathbf{E} = -\frac{\partial \mathbf{B}}{\partial t}$
    pub faradays_law: Expr,
    /// Ampère-Maxwell Law: $\nabla \times \mathbf{B} = \`mu_0` \left( \mathbf{J} + \`epsilon_0` \frac{\partial \mathbf{E}}{\partial t} \right)$
    pub amperes_law: Expr,
}

impl MaxwellEquations {
    /// Constructs Maxwell's equations from the given fields and sources.
    ///
    /// The constants $\`epsilon_0`$ and $\`mu_0`$ are treated as symbolic variables.
    #[must_use]
    pub fn new(
        e_field: &Vector,
        b_field: &Vector,
        rho: &Expr,
        j_field: &Vector,
        vars: (&str, &str, &str),
        t_var: &str,
    ) -> Self {
        let epsilon_0 = Expr::new_variable("epsilon_0");

        let mu_0 = Expr::new_variable("mu_0");

        let gauss_law_electric = simplify(&Expr::new_sub(
            divergence(e_field, vars),
            Expr::new_div(rho.clone(), epsilon_0.clone()),
        ));

        let gauss_law_magnetic = simplify(&divergence(b_field, vars));

        let faradays_law = {
            let lhs = curl(e_field, vars);

            let rhs = Vector::new(
                differentiate(&b_field.x, t_var),
                differentiate(&b_field.y, t_var),
                differentiate(&b_field.z, t_var),
            )
            .scalar_mul(&Expr::Constant(-1.0));

            (lhs - rhs).to_expr()
        };

        let amperes_law = {
            let lhs = curl(b_field, vars);

            let de_dt = Vector::new(
                differentiate(&e_field.x, t_var),
                differentiate(&e_field.y, t_var),
                differentiate(&e_field.z, t_var),
            );

            // mu_0 * (J + epsilon_0 * dE/dt)
            let rhs = (j_field.clone() + de_dt.scalar_mul(&epsilon_0)).scalar_mul(&mu_0);

            (lhs - rhs).to_expr()
        };

        Self {
            gauss_law_electric,
            gauss_law_magnetic,
            faradays_law,
            amperes_law,
        }
    }
}

/// Calculates the Lorentz force on a particle with charge $q$.
///
/// Formula: $\mathbf{F} = q(\mathbf{E} + \mathbf{v} \times \mathbf{B})$
#[must_use]
pub fn lorentz_force(
    charge: &Expr,
    e_field: &Vector,
    velocity: &Vector,
    b_field: &Vector,
) -> Vector {
    let v_cross_b = velocity.cross(b_field);

    let term = e_field.clone() + v_cross_b;

    term.scalar_mul(charge)
}

/// Calculates the electric field $\mathbf{E}$ from scalar potential $V$ and vector potential $\mathbf{A}$.
///
/// Formula: $\mathbf{E} = -\nabla V - \frac{\partial \mathbf{A}}{\partial t}$
#[must_use]
pub fn electric_field_from_potentials(
    v: &Expr,
    a: &Vector,
    vars: (&str, &str, &str),
    t_var: &str,
) -> Vector {
    let grad_v = gradient(v, vars);

    let da_dt = Vector::new(
        differentiate(&a.x, t_var),
        differentiate(&a.y, t_var),
        differentiate(&a.z, t_var),
    );

    // -(grad V + dA/dt)
    (grad_v + da_dt).scalar_mul(&Expr::Constant(-1.0))
}

/// Calculates the electric field $\mathbf{E}$ from the scalar electric potential $V$.
///
/// This is valid for electrostatics. Formula: $\mathbf{E} = -\nabla V$
#[must_use]
pub fn electric_field_from_potential(
    v: &Expr,
    vars: (&str, &str, &str),
) -> Vector {
    gradient(v, vars).scalar_mul(&Expr::Constant(-1.0))
}

/// Calculates the magnetic field $\mathbf{B}$ from the vector potential $\mathbf{A}$.
///
/// Formula: $\mathbf{B} = \nabla \times \mathbf{A}$
#[must_use]
pub fn magnetic_field_from_vector_potential(
    a: &Vector,
    vars: (&str, &str, &str),
) -> Vector {
    curl(a, vars)
}

/// Calculates the Poynting vector, representing the directional energy flux density.
///
/// Formula: $\mathbf{S} = \`frac{1}{\mu_0`} (\mathbf{E} \times \mathbf{B})$
#[must_use]
pub fn poynting_vector(
    e_field: &Vector,
    b_field: &Vector,
) -> Vector {
    let mu_0 = Expr::new_variable("mu_0");

    e_field
        .cross(b_field)
        .scalar_mul(&Expr::new_div(Expr::Constant(1.0), mu_0))
}

/// Calculates the electromagnetic energy density $u$.
///
/// Formula: $u = \frac{1}{2} \left( \`epsilon_0` |\mathbf{E}|^2 + \`frac{1}{\mu_0`} |\mathbf{B}|^2 \right)$
#[must_use]
pub fn energy_density(
    e_field: &Vector,
    b_field: &Vector,
) -> Expr {
    let epsilon_0 = Expr::new_variable("epsilon_0");

    let mu_0 = Expr::new_variable("mu_0");

    let term_e = Expr::new_mul(
        epsilon_0,
        Expr::new_pow(e_field.magnitude(), Expr::Constant(2.0)),
    );

    let term_b = Expr::new_div(
        Expr::new_pow(b_field.magnitude(), Expr::Constant(2.0)),
        mu_0,
    );

    simplify(&Expr::new_mul(
        Expr::Constant(0.5),
        Expr::new_add(term_e, term_b),
    ))
}

/// Represents Coulomb's Law for the electric field produced by a point charge.
///
/// Formula: $\mathbf{E} = \`frac{1}{4\pi\epsilon_0`} \frac{q}{|\mathbf{r}|^3} \mathbf{r}$
#[must_use]
pub fn coulombs_law(
    charge: &Expr,
    r_vector: &Vector,
) -> Vector {
    let epsilon_0 = Expr::new_variable("epsilon_0");

    let pi = Expr::Pi;

    let denominator = simplify(&Expr::new_mul(
        Expr::new_mul(Expr::Constant(4.0), pi),
        epsilon_0,
    ));

    let mag_r = r_vector.magnitude();

    let mag_r_cubed = Expr::new_pow(mag_r, Expr::Constant(3.0));

    let scalar = simplify(&Expr::new_div(
        charge.clone(),
        Expr::new_mul(denominator, mag_r_cubed),
    ));

    r_vector.scalar_mul(&scalar)
}