Skip to content

Calculation of some Units #515

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

Closed
arn9000 opened this issue Oct 13, 2018 · 20 comments
Closed

Calculation of some Units #515

arn9000 opened this issue Oct 13, 2018 · 20 comments

Comments

@arn9000
Copy link

arn9000 commented Oct 13, 2018

Hi! I am trying to calculate area of inertia using area ang Length like this:

            private Length dimAlongForce;
            private Length dimAcrossForce;
            private Area area;
            private AreaMomentOfInertia areaOfInertia;

            area = dimAlongForce * dimAcrossForce;
            massOMoment = area * 5.Meters() * 5.Meters();

but the error "operator * cannot be applied to operands fo type Volume and Length" happens.
Is there any way to do such calculation?

@angularsen
Copy link
Owner

Hi, this calculation is not yet defined. I'm not very familiar with AreaMomentOfInertia myself, but generally speaking I think it's maybe ambiguous what quantity to expect for 4-dimensional length?
I don't know any other 4-dimensional ones though, and if there truly are none, we could add a * multiplication overload for Volume and Length as you have here.

If, however, there are other quantities of the same dimensions, then the better way would be to construct it something like this:
AreaMomentOfInertia.FromAreaByLength2(Area a, Length along, Length across)

What would work better, do you think?

@tmilnthorp
Copy link
Collaborator

I think the operator is the way to go.

Even if there were other types with the same dimensions, you'd just have to specify the left hand side to resolve the ambiguity (could not use var).

@tmilnthorp
Copy link
Collaborator

tmilnthorp commented Oct 19, 2018

I misspoke: we need to use out parameters to distinguish multiply/divide operations rather than overloads, because the return type can not be the only difference.

For example, using the BaseDimensions to calculate the operations, Length gives the following:

    public partial struct Length
    {
        public void Multiply( Acceleration right, out SpecificEnergy result ) { result = SpecificEnergy.Zero; }
        public void Multiply( Area right, out Volume result ) { result = Volume.Zero; }
        public void Multiply( AreaDensity right, out LinearDensity result ) { result = LinearDensity.Zero; }
        public void Divide( AreaDensity right, out SpecificVolume result ) { result = SpecificVolume.Zero; }
        public void Multiply( Density right, out AreaDensity result ) { result = AreaDensity.Zero; }
        public void Divide( Duration right, out Speed result ) { result = Speed.Zero; }
        public void Multiply( DynamicViscosity right, out MassFlow result ) { result = MassFlow.Zero; }
        public void Divide( ElectricAdmittance right, out ElectricResistivity result ) { result = ElectricResistivity.Zero; }
        public void Divide( ElectricConductance right, out ElectricResistivity result ) { result = ElectricResistivity.Zero; }
        public void Multiply( ElectricConductivity right, out ElectricAdmittance result ) { result = ElectricAdmittance.Zero; }
        public void Multiply( ElectricConductivity right, out ElectricConductance result ) { result = ElectricConductance.Zero; }
        public void Multiply( ElectricCurrentDensity right, out Magnetization result ) { result = Magnetization.Zero; }
        public void Multiply( ElectricField right, out ElectricPotential result ) { result = ElectricPotential.Zero; }
        public void Multiply( ElectricResistance right, out ElectricResistivity result ) { result = ElectricResistivity.Zero; }
        public void Divide( ElectricResistivity right, out ElectricAdmittance result ) { result = ElectricAdmittance.Zero; }
        public void Divide( ElectricResistivity right, out ElectricConductance result ) { result = ElectricConductance.Zero; }
        public void Multiply( Force right, out ApparentEnergy result ) { result = ApparentEnergy.Zero; }
        public void Multiply( Force right, out Energy result ) { result = Energy.Zero; }
        public void Multiply( Force right, out RotationalStiffness result ) { result = RotationalStiffness.Zero; }
        public void Multiply( Force right, out Torque result ) { result = Torque.Zero; }
        public void Multiply( ForceChangeRate right, out ApparentPower result ) { result = ApparentPower.Zero; }
        public void Multiply( ForceChangeRate right, out Power result ) { result = Power.Zero; }
        public void Multiply( ForceChangeRate right, out ReactivePower result ) { result = ReactivePower.Zero; }
        public void Multiply( ForcePerLength right, out Force result ) { result = Force.Zero; }
        public void Multiply( ForcePerLength right, out RotationalStiffnessPerLength result ) { result = RotationalStiffnessPerLength.Zero; }
        public void Multiply( Frequency right, out Speed result ) { result = Speed.Zero; }
        public void Multiply( HeatFlux right, out ForceChangeRate result ) { result = ForceChangeRate.Zero; }
        public void Multiply( HeatTransferCoefficient right, out ThermalConductivity result ) { result = ThermalConductivity.Zero; }
        public void Multiply( Irradiance right, out ForceChangeRate result ) { result = ForceChangeRate.Zero; }
        public void Multiply( Irradiation right, out Force result ) { result = Force.Zero; }
        public void Multiply( Irradiation right, out RotationalStiffnessPerLength result ) { result = RotationalStiffnessPerLength.Zero; }
        public void Multiply( KinematicViscosity right, out VolumeFlow result ) { result = VolumeFlow.Zero; }
        public void Multiply( LapseRate right, out Temperature result ) { result = Temperature.Zero; }
        public void Multiply( Length right, out Area result ) { result = Area.Zero; }
        public void Multiply( LinearDensity right, out Mass result ) { result = Mass.Zero; }
        public void Multiply( Magnetization right, out ElectricCurrent result ) { result = ElectricCurrent.Zero; }
        public void Multiply( MassFlux right, out DynamicViscosity result ) { result = DynamicViscosity.Zero; }
        public void Multiply( Permeability right, out ElectricInductance result ) { result = ElectricInductance.Zero; }
        public void Multiply( Permittivity right, out Capacitance result ) { result = Capacitance.Zero; }
        public void Multiply( PowerDensity right, out HeatFlux result ) { result = HeatFlux.Zero; }
        public void Multiply( PowerDensity right, out Irradiance result ) { result = Irradiance.Zero; }
        public void Multiply( Pressure right, out ForcePerLength result ) { result = ForcePerLength.Zero; }
        public void Multiply( Pressure right, out Irradiation result ) { result = Irradiation.Zero; }
        public void Multiply( PressureChangeRate right, out HeatFlux result ) { result = HeatFlux.Zero; }
        public void Multiply( PressureChangeRate right, out Irradiance result ) { result = Irradiance.Zero; }
        public void Multiply( RotationalAcceleration right, out Acceleration result ) { result = Acceleration.Zero; }
        public void Multiply( RotationalSpeed right, out Speed result ) { result = Speed.Zero; }
        public void Multiply( RotationalStiffnessPerLength right, out ApparentEnergy result ) { result = ApparentEnergy.Zero; }
        public void Multiply( RotationalStiffnessPerLength right, out Energy result ) { result = Energy.Zero; }
        public void Multiply( RotationalStiffnessPerLength right, out RotationalStiffness result ) { result = RotationalStiffness.Zero; }
        public void Multiply( RotationalStiffnessPerLength right, out Torque result ) { result = Torque.Zero; }
        public void Divide( SpecificVolume right, out AreaDensity result ) { result = AreaDensity.Zero; }
        public void Multiply( SpecificWeight right, out Pressure result ) { result = Pressure.Zero; }
        public void Multiply( Speed right, out KinematicViscosity result ) { result = KinematicViscosity.Zero; }
        public void Divide( Speed right, out Duration result ) { result = Duration.Zero; }
        public void Divide( ThermalConductivity right, out ThermalResistance result ) { result = ThermalResistance.Zero; }
        public void Divide( ThermalResistance right, out ThermalConductivity result ) { result = ThermalConductivity.Zero; }
        public void Multiply( Volume right, out AreaMomentOfInertia result ) { result = AreaMomentOfInertia.Zero; }
    }

Since there's only one Multiply that takes a Length parameter, you can use var since Area is the only valid option:

            var length = Length.FromMeters( 1.0 );
            var length2 = Length.FromMeters( 2.0 );

            length.Multiply( length2, out var area );
            Console.WriteLine( area.SquareCentimeters );

However for Multiply( Force ) it could be ApparentEnergy, Energy, RotationalStiffness, or Torque. In this case you'll need to specify the out variable type to distinguish:

            var length = Length.FromMeters( 1.0 );
            var force = Force.FromNewtons( 1.0 );

            length.Multiply( force, out Torque torque );
            Console.WriteLine( torque.NewtonMeters );

Does this seem acceptable?

@angularsen
Copy link
Owner

I have a feeling the Multiply method approach is perhaps too complicated?

First, there is different behavior/possibilities when using * and when using Multiply() that consumers need to discover and understand the difference of.

Second, having a Multiply method that specifies different results using different out params also feels weird/uncommon to me.

I think AreaMomentOfInertia.FromAreaByLength2(Area a, Length along, Length across) is more explicit and simpler, but maybe not more discoverable.

On second thought, how about we add a Length4 quantity that has explicit and implicit cast to AreaMomentOfInertia? This way, the result is really a 4-dimensional length, but if you assign it a variable of type AreaMomentOfInertia it will happily convert itself to that type.

// Instead of this
public void Multiply( Force right, out ApparentEnergy result ) { result = ApparentEnergy.Zero; }
public void Multiply( Force right, out Energy result ) { result = Energy.Zero; }
public void Multiply( Force right, out RotationalStiffness result ) { result = RotationalStiffness.Zero; }
public void Multiply( Force right, out Torque result ) { result = Torque.Zero; }

// How about this?
public static class Test
{
    public static void TestIt()
    {
        AreaMomentOfInertia ami = Area.FromSquareMeters(4) * Length.FromMeters(3) * Length.FromMeters(4);
    }
}

public struct Length4
{
    public double TesseractMeters { get; }
    public Length4(double tesseractMeters)
    {
        TesseractMeters  = tesseractMeters;
    }
}

public partial struct Volume
{
    public static Length4 operator *(Volume volume, Length length)
    {
        return new Length4(volume.CubicMeters * length.Meters);
    }
}

public partial struct AreaMomentOfInertia
{
    public static implicit operator AreaMomentOfInertia(Length4 length4)
    {
        return AreaMomentOfInertia.FromMetersToTheFourth(length4.TesseractMeters);
    }
}

I guess the same approach could really be used for all our quantities, now that we have the base dimensions mapped out. This way, arithmetic given similar input quantity types could still result in different output quantity types.

Length meter = Length.FromMeters(1);
Force newton = Force.FromNewtons(1);

ApparentEnergy ae = meter * newton;
Energy e = meter * newton;
RotationalStiffness rs = meter * newton;
Torque t = meter * newton;

What do you guys think?

@tmilnthorp
Copy link
Collaborator

I don't like the multiply either. But there isn't any obvious solutions.

I think your example would fall apart for derived units but I haven't tried.

@SGStino
Copy link

SGStino commented Jan 31, 2019

I'm not sure if you need to be able to know the desired result of the output inside the multiplication.
If you would return a generic output like IUnit<ILength,ILength, IMass, IInverseUnit<ITime>, IInverseUnit<ITime>> and provide that with an implicit cast to IApparentEnergy or ITorque or any other (which would inherit from IUnit<ILength,ILength, IMass, IInverseUnit<ITime>, IInverseUnit<ITime>>. Then both issues can be solved.

However, operator overloads for interfaces are (not yet) supported by the language. 😭

But if they would, I'd provide the unit combinations through generics to keep it somewhat static typed.

But then add implicit casts (IApparentEnergy energy = length * force) or via extension methods (var energy = (length * force).AsKnownUnit<IApparentUnit>()) depending on how far the language features come along (as these aren't mutually exclusive and could become both available)

But untill the operator overloads are available, i think we'd be stuck with .Multiply(), .Divide(), .Add(), .Substract() extension methods for the combinations.

We'd have to find a way to generate those smartly though. IUnit<IMass, IInverse> should be automatically become a plain scalar etc.

An alternative would be just IUnit (like mathjs) without generics, would simplify it a lot, but we'd loose the compiler checking.

And after a given amount of levels we'd have to fall back to dynamic types anyway since the combinations would just explode.

@Mobz87
Copy link

Mobz87 commented Jan 31, 2019

I have also wanted this for a long time and In my mind if UnitsNet can do math with any kind of temporary quantities it would be a huge milestone to reach!

I dont have a solution but I would love to brainstorm to reach one!

Ex
We try to do this:
Temperature = (Temperature * Power) / (Power)

This math is fine but we Inside the equation get a temporary unit Temperature * Power that we right now cant do because we are missing the operator for Temperature * Power

what if we did something like this in the Temperature class:

//Temperature class
public static dynamic operator *(Temperature x, dynamic  y)
 {
        dynamic temporaryUnit;
        //Logic that i am yet to find out!
        return temporaryUnit;
}

This will return the unit Temperature * Power to -->

//Power class
public static dynamic operator / (dynamic  y, Power x)
 {
        dynamic temporaryUnit;
        //Logic that i am yet to find out!
       
        //Using a dynamic way to typecast it to the correct unit
        return (Temperature)temporaryUnit;
}

This will return Temperature unit

My got-feeling is telling me that this can be done

@Mobz87
Copy link

Mobz87 commented Jan 31, 2019

Just brainstorming with myself:

Define a unit for TemporaryUnit.
Which holds the information when an unknown unit has been created by operator ( * or / )
ex (Temperature * Power)
it holds the value of Temperature(In SI).Value * Power(In SI).Value
and the new temporary unit maybe as a string: "Temperature * Power"

//Temperature class:

//This covers all cases of unknown temporary Units
public static TemporaryUnit operator *(Temperature x, dynamic y)
{
TemporaryUnit MyUnit = new TemporaryUnit();
MyUnit.Value = x.value * y.value;
MyUnit.Unit = x.unit + " * " + y.unit; //String or something better

    return MyUnit;

}
//Same just the other way

public static TemporaryUnit operator *(dynamic  y, Temperature x)
{
        return x * y;
}
public static dynamic operator * (Temperature x, TemporaryUnit  y)
 {
        dynamic MyDynamicUnit;

        //Logic to test if we can typecast it to a known unit
        //Else typecast to TemporaryUnit

        //Math logic

        return MyDynamicUnit; //Typecasted to either a TemporaryUnit or a known unit
}
//Same just the other way
public static dynamic operator * (TemporaryUnit  y, Temperature x)
 {

        return x*y;
}

@tmilnthorp
Copy link
Collaborator

You don't need a temporary unit. We can multiply or divide the base dimensions to find out what the resultant quantity is (or quantities in some cases). When #576 is merged, you could look up what that quantity is from the BaseDimensions. Otherwise throw an exception. And you can always return IQuantity. Then you just have to figure out what to do with it. Strong typing would be nice, but not sure we can for the reasons above (more than one resultant unit).

@SGStino
Copy link

SGStino commented Feb 1, 2019

Thats where the genrics could come in i'd guess, just not sure how feasible that is yet with the current state of c#. Would need to do some experimenting first.

@angularsen
Copy link
Owner

I'd be very interested to see what you can come up with in some proof of concept PR.

With current C# and possibly with a future C#. I think you can even build your own compiler and pull in experimental features from Roslyn pull requests, and use it with a nuget or something. Haven't tried it, but sounds awesome.

@Mobz87
Copy link

Mobz87 commented Feb 1, 2019

@tmilnthorp
I cant see a way around using some kind of a temporary unit when you want to pass around units that is not known.
but feel free to show me the way :)
That could simplify it for sure!

Lets say you try to do:
Temperature = (Temperature*Temperature*Temperature)/(Temperature*Temperature)

First it will go into:
public static Dynamic operator * (Temperature x, dynamic y) //In the TemporaryUnit the (Temperature*Temperature) is stored

And it will go into it again:
public static Dynamic operator * (Temperature x, dynamic y) //In the TemporaryUnit the (Temperature*Temperature) * Temperature is stored

Then two times into into
public static Dynamic operator / (Temperature x, dynamic y)

Where it will finally return a Temperature

@angularsen
Are you looking for a proof of concept of using Dynamic in operator * and /
or a proof of concept of the logic inside the operator * and / ?

@angularsen
Copy link
Owner

angularsen commented Feb 1, 2019

@Mobz87 If I understand your suggestion right, and I only skimmed it once, you are not really getting static type safety.

Where it will finally return a Temperature

I think this is the crux in your example. How can you with type safety ensure that this is correct? You have lost the information you/the compiler needs to figure this out.

I don't see the benefit here over simply this:

// Example: if we add Multiply(), Divide() and ToQuantity<T> to IQuantity
IQuantity t2 = Temperature.FromDegreesCelsius(20);
IQuantity t3 = Temperature.FromDegreesCelsius(20);
IQuantity t4 = Temperature.FromDegreesCelsius(20);
IQuantity t1 = t2.Multiply(t3).Divide(t4); // 20 Celsius
double t1Celsius = t1.As(TemperatureUnit.DegreeCelsius); // 20, throws if not compatible
Temperature t1_ = t1.ToQuantity<Temperature>(); // 20 Celsius, throws if not compatible

Edit:
Okay, operator overloads and syntactic sugar is one benefit, but not a big win compared to how many methods we would need to add. If we change from struct to class and introduce a base class, or if newer c# versions enable operator overloads on interfaces, then we could get operator overloads in my example.

@Mobz87
Copy link

Mobz87 commented Feb 1, 2019

Personally I would love to be able to use the operators instead of Multiply() and Divide(). They make equations harder and less 'math like' to read.

In my example there is no type safety for all unknown units - with logic and typecast we will insure no errors.

@SGStino
Copy link

SGStino commented Feb 2, 2019

Updated to C# vNext:

IQuantity t2 = Temperature.FromDegreesCelsius(20);
IQuantity t3 = Temperature.FromDegreesCelsius(20);
IQuantity t4 = Temperature.FromDegreesCelsius(20);
IQuantity t1 = t2 * t3 / t4; // 20 Celsius
double t1Celsius = t1.As(TemperatureUnit.DegreeCelsius); // 20, throws if not compatible
Temperature t1_ = t1.ToQuantity<Temperature>(); // 20 Celsius, throws if not compatible

Updated to what I hope to achieve:

IQuantity<Temperature> t2 = Temperature.FromDegreesCelsius(20);
IQuantity<Temperature> t3 = Temperature.FromDegreesCelsius(20);
IQuantity<Temperature> t4 = Temperature.FromDegreesCelsius(20);
IQuantity<Temperature, Temperature> t23 = t2 * t3;
IQuantity<Temperature> t1 = t23 / t4; // 20 Celsius
double t1Celsius = t1.As(TemperatureUnit.DegreeCelsius); // 20, no exceptions required
Temperature t1_ = t1; // 20 Celsius, implicit cast, no exceptions required

Ofc, you can then replace all the IQuantity<>'s with var

var t2 = Temperature.FromDegreesCelsius(20);
var t3 = Temperature.FromDegreesCelsius(20);
var t4 = Temperature.FromDegreesCelsius(20);
var t23 = t2 * t3;
var t1 = t23 / t4; // 20 Celsius
double t1Celsius = t1.As(TemperatureUnit.DegreeCelsius); // 20, no exceptions required
Temperature t1_ = t1; // 20 Celsius, implicit cast, no exceptions required

So far, this is only wishfull thinking, and is entirely up to the C# language team to provide us with the operator overloads for interfaces.

But I'm afraid the amount of method combinations this would yield would be impossible to handle.
Templating instead of Generics would be more suited for this task. However templates are not supported by the CLI.

Just a quick example to try: Coulomb's Law.

F = k*Q1*Q2 / r^2

Would result in an intermediate unit of IQuantity<IMass, ILength, ILength, ILength, IInverseUnit<ITime>, IInverseUnit<ITime>, IInverseUnit<ITime>, IInverseUnit<ITime>, IInverseUnit<ICurrent>, IInverseUnit<ICurrent>>
just for the constant k alone..

Now imagine all the methods that need to be generated to support reducing that back into IQuantity<IMass, ILength, IInverseUnit<ITime>, IInverseUnit<ITime>> without any duplicates like ITime, IInverseUnit<ITime>

For now I would indeed settle with just IQuantity and runtime casts that throw exceptions if there is something wrong.

@tmilnthorp
Copy link
Collaborator

Today we can do this:

Length length1 = Length.FromInches( 2.0 );
Length length2 = Length.FromMeters( 2.0 );

double multipliedValue = length1.Value * length2.As( length1.Unit );
BaseDimensions multipliedBaseDimensions = length1.Dimensions * length2.Dimensions;

QuantityInfo multipliedQuantityInfo = Quantity.Infos.Where( info => info.BaseDimensions == multipliedBaseDimensions ).First();

Here multipliedValue is 157.48031496062993. This is in inches (square inches really).
multipliedBaseDimensions is [Length]^2, which corresponds to Area.

And indeed multipliedQuantityInfo is a UnitsNet.QuantityInfo<UnitsNet.Units.AreaUnit>

The remaining piece needed is the BaseUnits for each unit. This is also necessary for #547.

In this case, we need to add something per-unit as follows for this example:

      "SingularName": "Inch",
      "PluralName": "Inches",
      "BaseUnits": {
        "L": "Inch"
      },

Then we can get the base units for the left-hand side of inches, and combine it with the BaseDimensions.

Area would look as follows:

      "SingularName": "SquareInch",
      "PluralName": "SquareInches",
      "BaseUnits": {
        "L": "Inch"
      },

We would use multipliedBaseDimensions as it exists today, plus the BaseUnits on each unit to find we need Area in SquareInches.

@angularsen
Copy link
Owner

Thanks for the detailed thought experiment @SGStino , it helped me grasp the concept and the challenges.

For now I would indeed settle with just IQuantity and runtime casts that throw exceptions if there is something wrong.

I agree, for now I don't see a practical way to achieve type safety of quantity calculations. It sure is enticing and I'd love to get there, but I think C# would need something like templating or a more powerful type inference system to make it happen.

Just FYI, but F# has its own native support for measures that I've heard works pretty well for these things. I have not tried it myself, but here are some examples:
https://fsharpforfunandprofit.com/posts/units-of-measure/

F# runs on .NET too, so it could possibly be a way to achieve what we describe here. Not sure what the limitations are, but I would be very interested to hear about it if anyone knows or finds out.

@SGStino
Copy link

SGStino commented Feb 18, 2019

What kind of sorcery is this! Attributes inside generics ... Or more accuratly, what rock have I been living under?
I'd love to peak into the generated IL from those examples ...
I'm guessing they have some sort of custom compiler checks on the attributes?

But this is a big turn-off for what I am concerned at the moment:

This means that there is no (easy) way at runtime to determine what unit of measure a value has, nor any way to dynamically assign a unit of measure at runtime.

Although I'm not entirely sure how correct that statement is given that it uses attributes, and that attributes are available at runtime. Perhaps no support in the library, but not that difficult.

And they treat all units as separate entities, no relation between feet and meters. Conversion would require a constant in the unit meter/feet. Not that elegant 😄

Meanwhile, I'm also going to dig a little into when I find some time:
https://github.com/unitsofmeasurement/unit-api/tree/master/src/main/java/javax/measure
See if they came up with some intresting insights. At first glance it looks very similar, implemented through generic interfaces.

And then extra libraries for the SI/non-SI implementations.

@angularsen
Copy link
Owner

Cool, thanks for reporting back on F#.
Look forward to hear what you find on that java lib, too!

@angularsen
Copy link
Owner

Closing this due to inactivity.

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

5 participants