Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is it time for Moments? #118

Closed
dunmatt opened this issue Mar 25, 2019 · 22 comments
Closed

Is it time for Moments? #118

dunmatt opened this issue Mar 25, 2019 · 22 comments

Comments

@dunmatt
Copy link

dunmatt commented Mar 25, 2019

In pursuit of #117 I've been self educating on moments, what they are and what they mean, and I don't think that Kinds are the best way to implement them (although I'll be the first to admit that I'm new to this, so feel free to stomp all over what I'm saying, I welcome the opportunity to learn from this).

I believe that a Moment belongs in the same category as a Quantity, or perhaps even higher than Quantity, since quantities can be thought of as zeroth moments. So a quantity has a kind, but a quantity is a moment.

So, as a straw man proposal how do you feel about

// pseudocode...
pub trait Moment<Order, Frame, Quantity> {}

Where math is permitted between moments when their orders are Z0, or when their orders and frames match (and, obviously, if the math would be otherwise allowed by the quantities). Then you could do something to the effect of

impl <FrameToken> Moment<P1, FrameToken, Force> for Torque<FrameToken> {...}
impl <FrameToken> Moment<P2, FrameToken, Mass> for RotationalInertia<FrameToken> {...}

etc. I realize this isn't totally fleshed out, sorry about that, I'm brand new to this... but hopefully there's enough here to see what I mean?

@iliekturtles
Copy link
Owner

I'm going to need to do some more research to fully understand the topic. One thing I have noticed so far is that moments are quantities. Torque is the moment of force. Volume is the moment of area.

@dunmatt
Copy link
Author

dunmatt commented Mar 26, 2019

No, volume is not the moment of area. The characteristic that makes moments what they are is that they're proportional to a power of the radius from a frame of reference. Volumes and areas don't work that way, the 2L softdrink bottle doesn't get larger as it moves away from me... but the same force exerts more torque as it moves away.

It's a subtype relation, non-moment quantities can be thought of as order zero moments (proportional to the radius from the measured thing to me, raised to the zeroth power), but moments in general cannot be thought of as quantities, since the measurement depends on the frame of reference.

To give a good example of moments not behaving like quantities, imagine you're driving a front wheel drive electric car at a constant non-zero speed (in constant conditions). The motor is applying some constant torque about the front axle to combat rolling resistance and drag. But that same force measures differently from the back axle. It doesn't torque about the back axle at all, were it not for friction against the road the back wheels wouldn't spin. Other quantities don't work this way, the front and back axles agree on how much current is flowing through the motor, for example.

Edit: Another way to see that non-zero moments aren't quantities is that they're not represented as individual numbers. More properly a torque is actually a 3-vector of quantities (since we're in 3D space and it's a first moment. For moments of inertia it takes 3^2=9 numbers since its a second moment in 3D space), so no single float can hold one (except for zeroth moments where you need 3^0=1 float). Of course, if we were to represent them that way we'd lose the "zero cost abstraction" property. For my use case (robotics) the frames of reference are actually discrete (this motor, that motor, etc), so I'd rather have the zero cost abstractions than physics-prof perfection, but I can see how others might want continuous frame representations (that said, I'm not volunteering to write the continuous frame version... and you probably wouldn't want me to anyway, I only learned this stuff yesterday).

@dunmatt
Copy link
Author

dunmatt commented Apr 3, 2019

Poke. Any thoughts on this?

@iliekturtles
Copy link
Owner

Sorry for the delays, my recent free time went into releasing v0.22.0. I'll make this more of a priority over the next few days and respond with my thoughts.

@dunmatt
Copy link
Author

dunmatt commented Apr 3, 2019

No rush, just making sure it's not forgotten

@iliekturtles
Copy link
Owner

The simplest formula to calculate the nth moment of a physical quantity Q is µ = r^n · Q. The product of any two quantities (r^n and Q in this case) is still a quantity. Based on dimensional analysis volume is the first moment of area (https://en.wikipedia.org/wiki/First_moment_of_area#Definition).

My inclination at this point is to leave moments, as an explicit type, out of uom. Moments as quantities exist, but I'm not sure of the exact case to use them as moments. If you have some example use or want to put an implementation together I can review more.

Regarding your original proposal I believe the first two generic arguments are redundant. The Order generic is already captured in the quantity's length dimension. For FrameToken it seems odd to keep a reference to one of the inputs in the result. e.g. when talking about a + b the result is c, not (c, b).

@dunmatt
Copy link
Author

dunmatt commented Apr 7, 2019

I mean, if you want to just base it on dimensional analysis then Torque is the same thing as Energy, as are Durations and SystemTimes, angular velocity is a frequency, and your two Kinds of temperature are the same thing. (Edit: and also, based on dimensional analysis you could even say volume is the third moment of an angle). Clearly there's more to it than mere dimensional analysis.

On the topic of my original proposal (which I would like to continue discussing with you (if you're up for it) since I now plan to roll it into its own crate (I need it at work)), I think I did a bad job explaining it from the get go. I'm not proposing to keep the length dimension(s) in a separate collection of bits than the quantity dimension. It's a true zero cost abstraction, so if the representation is f64 then a moment will only occupy those 64 bits. Because of that there needs to be some other way to prevent adding first moments with second moments (etc).

Similarly, I think I botched the explanation of the FrameToken. The frame token is size zero and its only role is preventing nonsensical math between otherwise compatible types. So if I have a robot arm with 6 DOFs it's perfectly sensible to ask what the total dissipated power of the system is, and to compute it by summing the power of each DOF (and another term for the electronics), however it's not sensible to do that to get the net torque at the end effector. The torques cannot be added because they're not about the same axis. If, however, I wanted to compute the net torque about a single DOF, I could do that by computing the downstream center of mass and total mass, using the center's position relative to the motor in question's axis to compute the torque from downstream gravity, and then adding that torque to the applied torque reported by the motor. So I need something to allow additions between torques, but only when they're in the same frame of reference, and ideally I'd like that enforced at compile time rather than run time. Does that make sense?

If you'd rather hold off on adding moments that's fine, I'm perfectly happy to scratch my own itch. Could I persuade you to be a code reviewer for the new crate I'm going to start?

@iliekturtles
Copy link
Owner

I think it is more that I don't fully understand the usage of moments vs. what can already be done with quantities. Perhaps you could come up with some example code showing usage with what you expect things to look like and what compile-time checks would happen. Also if you have any thoughts about when you would want to use a moment vs. a quantity.

If you want to create a branch in your fork of uom for create a separate crate I'd be happy to review.

@dunmatt
Copy link
Author

dunmatt commented Apr 8, 2019

Sure, I plan to use moments for a couple of different projects, but the most obvious one is a driver for an ATI Axia Force/Torque sensor. Currently, it streams the following to users:

/// One reading of the sensor.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FtReading {
    /// This is the time that the UDP packet containing this reading arrived via
    /// the network connection.
    pub arrival_time: SystemTime,
    /// This is incremented for each reading.  The readings happen at ~7KHz, so
    /// be ready for this to wrap around back to zero every so often.
    pub sequence_number: u32,
    /// The X component of the force applied at the current coordinate frame.
    pub fx: Force,
    /// The Y component of the force applied at the current coordinate frame.
    pub fy: Force,
    /// The Z component of the force applied at the current coordinate frame.
    pub fz: Force,
    // TODO: make a better type for these torques to be
    tx: i32, // currently a raw reading...
    ty: i32, // currently a raw reading...
    tz: i32, // currently a raw reading...
}

There are a few problems with this. First, the lack of units on the torques. Second, with the API designed this way it doesn't expose the frame of reference that the sensor is currently using (it can report in either the frame centered on its face, or at a configured Tool Center Point (which the industry confusingly shortens to TCP despite most of these robots being network accessible), and the frame its reporting in can be switched at will during operation). I easily could add a field for it, but that seems easy miss compared to being forced to take the frame into account.

Alternatively, I could add an enum for the various frames, and then use something like a TorqueMagnitude quantity, but that still has the problems I ran into in that other issue where it's not clear how a Length and a Force know what Kind to multiply into.

@dunmatt
Copy link
Author

dunmatt commented Apr 8, 2019

As to why I'm leaning towards moments instead of quantities, partially it's to prohibit nonsense math (adding between frames), and partially it's to avoid having to create a new unit for each of the moment types I'm going to need for a different project (an RTDE driver for a UR5 robot arm) that exposes not just torques, but also rotational inertias and possibly one other moment that isn't coming to mind off the top of my head.

@Aehmlo
Copy link
Contributor

Aehmlo commented Apr 8, 2019

It sounds like, at a more pragmatic level, you want to embed additional information (e.g. reference frames) as (type) parameters. I can certainly see how this is useful (especially for your use case), but I’m not certain whether it makes sense as part of the base uom crate. I’m sure there’s a way to do some type-level hacks to encode the information “this moment is about this origin in this vector space,” but uom advertises itself as a dimensional analysis crate, and the information that would be encoded for moments goes beyond dimension or even kind and into something more complex.

I think this is a really cool idea and I’d love to see it happen (and it’d be interesting to see how it could work technically), but I foresee this being a great deal of complexity that I’m not sure belongs in the base uom crate (maybe a uom-moments or a separate spinoff that just depends on uom?), especially given the already-high compile times.

Just my unsolicited thoughts.

@iliekturtles
Copy link
Owner

Stubbed out a bit of implementation before running out of time. Thoughts? Anything crucial missing? Re-doing all the impls seems like a lot of busy work. Maybe one of the auto-derive crates can help with this?

// Order (rank?) is D::L.
struct Moment<F, D, U, V> {
    frame: PhantomData<F>,
    quantity: Quantity<D, U, V>
}

mod torque {
    type Dimension = super::Dimension<...>;
    type Torque<Frame, U, V> = Moment<Frame, Dimension, U, V>;
}

// Add, Sub, Mul, Div, One, Zero... all need impls.

As a slight aside do you intend/desire to turn fx, fy, fz into Vec3<Force> or Force<V = Vec3f32>?

@dunmatt
Copy link
Author

dunmatt commented Apr 11, 2019

You might be overestimating my current Rust skill level. What is D::L?

In that example, why does Torque have the U parameter?

@Aehmlo
Copy link
Contributor

Aehmlo commented Apr 11, 2019

D implements Dimension, so D::L is the power of length in the dimension. For instance, area has dimensions of length squared, so for area's dimension, L == P2. (See typenum if you're weirded out by P2; basically, it's a hack to work around the current lack of const generics.)

Similar to how D encodes information about the quantity's dimension, U encodes the quantity's units as specified (see Units).

Basically, you're going to need D (dimension), U (units) and V (value/storage type) to work with quantities as they exist now. The wrapper type @iliekturtles gave above (Moment) wraps Quantity and adds another type parameter F to encode the frame, which can then be constrained using the type system when implementing algebraic operations (so you can e.g. only implement Add for two moments where One::F == Other::F).

I'm starting to think this doesn't need to be as complex as I had feared!

@iliekturtles
Copy link
Owner

Thanks for the details @Aehmlo. One thing to add is that right now all quantities have the D, U, and V parameters but these are generally hidden from end users through type aliases.

One thing I started thinking about is that using a type parameter for F means changing the frame of reference changes the type. What is the best way to write a method where the returned type can change and is controlled by the method and not the caller? e.g. a theoretical method call let t: Torque<F> = sensor.read_torque(). The caller doesn't know what F which would be invalid Rust. Will need to think on this more.

@Aehmlo
Copy link
Contributor

Aehmlo commented Apr 11, 2019

I'm not sure if it works in this case, but the whole use case for impl Trait is loosely "letting the method decide what to return instead of the caller." I suspect it may not quite be flexible enough, however, as we'd probably want to have multiple (infinite?) possible Fs for a single method.

Problem with that is that it requires bumping the minimum Rust version (I believe impl Trait was Rust 1.26?).

@iliekturtles
Copy link
Owner

(Thought I posted this yesterday, came back today and saw it was still a draft!)

impl Trait does solve this issue. It allows you to not name a type, but the method must still return a single type. e.g. the following doesn't compile: note: expected type f32 found type f64.

trait T {}

impl T for f32 {}
impl T for f64 {}

fn f(x: i32) -> impl T {
    if x > 0 { 0.0_f32 } else { 0.0_f64 }
}

fn main() {
}

@Aehmlo
Copy link
Contributor

Aehmlo commented Apr 12, 2019

Yeah, I was thinking that we might have branching within the method body that would prevent impl Trait from being sufficient, but I suppose it would probably just be several monomorphized methods without branching, with the type system determining the path. I'm just not sure how you get the information about the origin into the measurement, unless it's just supplied by the caller.

@dunmatt
Copy link
Author

dunmatt commented Apr 13, 2019

I don't have a good answer for how to make the type runtime switchable (without resorting to an enum). If I stop trying to enforce that does it simplify this into the realm of the doable?

I like @iliekturtles' implementation suggestion. I assume we can constrain it such that D::L is is non-negative?

@iliekturtles
Copy link
Owner

An enum doesn't solve the problem at the type level because enum variants are not types and cannot be used as generic parameters. That leaves us stuck with a generic frame parameter that is static at compile time or the need for a non-zero-sized field and using run-time checks to ensure that two moments have the same frame.

See the following example proving that a Moment could constrain the underlying Quantity to have a positive length dimension. Note that invalid Moment type aliases (M0, M2) can be defined, but cause a compile error if referenced.

#![allow(dead_code)]

use std::marker::PhantomData;
use typenum::{Cmp, Integer, IsGreater, Same, B1, N1, P1, Z0};

trait Dimension {
    type L: Integer;
    type M: Integer;
}

struct Quantity<D>
where
    D: Dimension + ?Sized,
{
    dimension: PhantomData<D>,
}

struct Moment<D>
where
    D: Dimension + ?Sized,
    D::L: Cmp<Z0> + IsGreater<Z0>,
    <D::L as IsGreater<Z0>>::Output: Same<B1>,
{
    quantity: Quantity<D>,
}

type M0 = Moment<Dimension<L = Z0, M = Z0>>;
type M1 = Moment<Dimension<L = P1, M = Z0>>;
type M2 = Moment<Dimension<L = N1, M = Z0>>;

fn main() {
    // error: the trait bound `Same<B1>` is not satisfied.
    //let _m0 = M0 { quantity: Quantity { dimension: PhantomData } };
    let _m1 = M1 { quantity: Quantity { dimension: PhantomData, }, };
    // let _m2 = M2 { quantity: Quantity { dimension: PhantomData } };
}

@dunmatt
Copy link
Author

dunmatt commented Apr 13, 2019

Yeah, that looks great, way better than my initial straw man. Is the generic frame parameter really all that bad of a thing?

@iliekturtles
Copy link
Owner

A frame parameter vs a sized frame field have trade-offs. See #91 where the same essential issue exists for methods that have a generic parameter N: Unit. For a general implementation of moments by inclination would be to lean towards using a sized field because it allows for different frames at run-time. I still don't fully understand moments and the requirements so experimentation is probably the next step. Implement something like what I stubbed out above and see how it works out.

// Function requires run-time check to ensure that the sensor is set to the desired explicit
// frame. All uses of the torque after reading are checked at compile time. The frame can't
// be changed at run-time.
fn read_torque() -> Torque<ExplicitFrame> { ... }
// Function requires run-time check to ensure that the sensor is set to the caller-given
// frame. All uses of torque after reading are checked at compile time. The `F` parameter
// will essentially infect all methods in the call-stack. There are limited options to change
// the frame at run-time.
fn read_torque<F>() -> Torque<F> { ... }
// Torque needs a sized frame field to hold the sensor's frame. Provides the most flexibility
// at run-time but also requires run-time checks whenever the torque is used and the
// frame is relevant.
fn read_torque() -> Torque { ... }

@dunmatt dunmatt closed this as completed May 10, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants