reefer 0.3.0

Optimizing proc-macro for geometric algebra
Documentation
// Direct port of example_timespace_lorentz.html using Reefer's spacetime algebra.
// This example demonstrates Lorentz transformations in a 1,3 metric algebra.
//
// The original example and explanation can be found at:
// https://enkimute.github.io/ganja.js/examples/coffeeshop.html#timespace_lorentz

// Create a timespace Algebra with 1,3 metric.
#[allow(unused_macros)]
#[reefer::algebra(f64, 1, 3)]
mod spacetime {
    // First we define our basis vectors.  With reefer's basis grammar we could
    // name them as t,x,y,z and end up with objects like tx=-xt instead of e01=-e10
    // but we'll stick to the original naming for clarity. See tests/lorentz.rs for
    // an example using custom basis names.

    /// Time basis vector.
    basis!(e0 = P0); // t
    /// Space basis vector x.
    basis!(e1 = N0); // x
    /// Space basis vector y.
    basis!(e2 = N1); // y
    /// Space basis vector z.
    basis!(e3 = N2); // z

    /// Next we define the shapes we will use in this algebra. For this example
    /// we only need scalars and events. We can easily extend this with more shapes
    /// as needed.
    /// Note that we can derive common traits for convenience. These are passed through
    /// by reefer to the generated shape structs `Scalar` and `Event`.

    /// Shape representing a scalar quantity. The basis alias `1` maps to the scalar basis
    /// and will be stored in the `_1` field.
    #[derive(Debug, Clone, Copy)]
    shape!(Scalar { 1 });

    /// Shape representing an event in spacetime. The basis aliases `e0`, `e1`, `e2`, and `e3`
    /// map to the corresponding basis vectors and will be stored in the respective fields.
    #[derive(Debug, Clone, Copy)]
    shape!(Event { e0, e1, e2, e3 });

    // The rest of this `spacetime` module contains convenience functions and constants
    // that are just regular Rust code passed through by reefer as-is.
    // They do not interact with the algebra generation process in any way.

    /// Speed of light in kilometres per second. Just a plain old rust constant that will be passed through reefer as-is.
    pub const LIGHT_SPEED_KM_PER_S: f64 = 299_792.458;

    /// Convenience helpers to mirror the JS example.
    impl Event {
        /// Create a new Event from components in lightseconds
        pub fn new(e0: f64, e1: f64, e2: f64, e3: f64) -> Self {
            Self { e0, e1, e2, e3 }
        }
        /// Create an Event from microseconds and kilometres.
        pub fn from_us_km(micros: f64, km_x: f64, km_y: f64, km_z: f64) -> Self {
            Self {
                e0: micros * 1.0e-6,
                e1: km_x / LIGHT_SPEED_KM_PER_S,
                e2: km_y / LIGHT_SPEED_KM_PER_S,
                e3: km_z / LIGHT_SPEED_KM_PER_S,
            }
        }
        /// Get the components of the Event in microseconds and kilometres.
        pub fn to_us_km(&self) -> (f64, f64, f64, f64) {
            (
                self.e0 * 1.0e6,
                self.e1 * LIGHT_SPEED_KM_PER_S,
                self.e2 * LIGHT_SPEED_KM_PER_S,
                self.e3 * LIGHT_SPEED_KM_PER_S,
            )
        }
        /// Get the components of the Event in lightseconds
        pub fn components(&self) -> (f64, f64, f64, f64) {
            (self.e0, self.e1, self.e2, self.e3)
        }
        /// Get the time component of the Event in lightseconds
        pub fn time(&self) -> f64 {
            self.e0
        }
    }
}

// We can now treat our `spacetime` module as a regular Rust module. The `shape!`
// macro has generated the `Scalar` and `Event` structs for us. The module also
// has the optimizing `expr!` macro for creating expressions (see `boost` below).
// Other than that, everything else is just normal Rust code at this point.

impl From<f64> for spacetime::Scalar {
    /// Create a Scalar from a f64 value.
    fn from(value: f64) -> Self {
        Self { _1: value }
    }
}

impl core::fmt::Display for spacetime::Event {
    /// Format the Event for display in microseconds and kilometres
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        let (t, x, y, z) = self.to_us_km();
        write!(
            f,
            "Event {{ t: {:.9} μs, x: {:.9} km, y: {:.9} km, z: {:.9} km }}",
            t, x, y, z,
        )
    }
}

fn main() {
    // Define two events in our own reference frame. (the earth's)
    println!("Earth frame observations:");

    let strike_tree = { spacetime::Event::from_us_km(10.0, 0.0, 0.0, 0.0) };
    println!("  Tree: {}", strike_tree);

    let strike_pole = spacetime::Event::from_us_km(10.0, 20.0, 0.0, 0.0);
    println!("  Pole: {}", strike_pole);

    // Calculate the Lorentz transform to go to the rocket's frame via a rotor.
    let rocket_speed: f64 = 0.5; // Half the speed of light.
    let phi = rocket_speed.atanh();
    let a = -0.5 * phi;
    // Use the `expr!` macro to build an optimized boost function. This will
    // be specialized for our algebra and the types we use. Long term, the
    // goal will be to not need an intermediate closure here, but for now you
    // cannot access external scope items from inside `expr!` macros without
    // passing them in as parameters (due to macro hygiene constraints).
    let boost_calc = spacetime::expr! {
        |ev: spacetime::Event, a: spacetime::Scalar| (a * e10).exp().sandwich(ev)
    };
    // Wrap with a regular rust closure to bind the boost parameter.
    let boost = |ev: spacetime::Event| boost_calc(ev, a.into());

    // Transform the events to the rocket's frame.
    let strike_tree_r = boost(strike_tree);
    let strike_pole_r = boost(strike_pole);

    // Output our results.
    println!("Rocket frame observations at β = {} c:", rocket_speed);
    println!("  Tree: {:?}", strike_tree_r);
    println!("  Pole: {:?}", strike_pole_r);
    let simultaneous = (strike_tree_r.time() - strike_pole_r.time()).abs() < 1.0e-12;
    println!(
        "  {}",
        if simultaneous {
            "Hey that's at the same time !"
        } else {
            "Wow! That's not at the same time!"
        }
    );

    // Second question from the example: what does the timespace event at (1,1)
    // to a static observer look like for an observer going at 0.5c?
    let event = spacetime::Event::new(1.0, 1.0, 0.0, 0.0);
    let event_in_frame = boost(event);
    let (t_ls, x_ls, _, _) = event_in_frame.components();
    println!(
        "\nEvent at (t=1, x=1) as seen at 0.5c: {:?}",
        event_in_frame
    );
    println!("  time  component (lightseconds) = {:.12}", t_ls);
    println!("  space component (lightseconds) = {:.12}", x_ls);

    // Output:

    // Earth frame observations:
    //   Tree: Event { t: 10.000000000 μs, x: 0.000000000 km, y: 0.000000000 km, z: 0.000000000 km }
    //   Pole: Event { t: 10.000000000 μs, x: 20.000000000 km, y: 0.000000000 km, z: 0.000000000 km }
    // Rocket frame observations at β = 0.5 c:
    //   Tree: Event { e0: 1.1547005383792512e-5, e1: -5.773502691896257e-6, e2: 0.0, e3: 0.0 }
    //   Pole: Event { e0: -2.6969658647136894e-5, e1: 7.125982536996255e-5, e2: 0.0, e3: 0.0 }
    //   Wow! That's not at the same time!

    // Event at (t=1, x=1) as seen at 0.5c: Event { e0: 0.5773502691896257, e1: 0.5773502691896257, e2: 0.0, e3: 0.0 }
    //   time  component (lightseconds) = 0.577350269190
    //   space component (lightseconds) = 0.577350269190
}