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

Inferred division from defined multiplication relations #1354

Merged
merged 5 commits into from
Mar 1, 2024

Conversation

Muximize
Copy link
Contributor

@Muximize Muximize commented Jan 7, 2024

In #1329 this proposal came up:

Another idea: generate division operators based on multiplication. Right now we define:

ElectricPotential.Volt = ElectricCurrent.Ampere * ElectricResistance.Ohm (and generate the reverse)
ElectricCurrent.Ampere = ElectricPotential.Volt / ElectricResistance.Ohm
ElectricResistance.Ohm = ElectricPotential.Volt / ElectricCurrent.Ampere

But those last two could also be generated based on the first.

This PR is an experiment implementing this.

Breaking changes:

  • TimeSpan = Volume / VolumeFlow => Duration = Volume / VolumeFlow

@Muximize
Copy link
Contributor Author

Muximize commented Jan 25, 2024

I rebased this on the latest changes in #1329 which makes it much cleaner.

Example of an interesting situation that occurs here, we previously defined:

MassFlow.GramPerSecond = Area.SquareMeter * MassFlux.GramPerSecondPerSquareMeter
MassFlux.KilogramPerSecondPerSquareMeter = MassFlow.KilogramPerSecond / Area.SquareMeter

If we infer division from multiplication, one of those has to add or lose a kilo 😅

@Muximize Muximize changed the base branch from master to release/v6 February 4, 2024 19:54
@angularsen
Copy link
Owner

Will review this next, hopefully in the next few days.

If we infer division from multiplication, one of those has to add or lose a kilo 😅

Haha 😆 But it will still work, right?
The inferred division will just use grams instead?

@Muximize
Copy link
Contributor Author

Muximize commented Feb 5, 2024

Yes, it will work, except we're not sure why the original contributor chose a particular unit for an operator and if it matters, with precision for example.

Also, this PR is more of an experiment than a serious proposal. There's some questions about the "magic" of it, and I took some liberties with implicit/explicit conversion between Density and MassConcentration that I'm not quite sure about. Maybe @lipchev can chime in on this?

@lipchev
Copy link
Collaborator

lipchev commented Feb 5, 2024

Yes, it will work, except we're not sure why the original contributor chose a particular unit for an operator and if it matters, with precision for example.

Also, this PR is more of an experiment than a serious proposal. There's some questions about the "magic" of it, and I took some liberties with implicit/explicit conversion between Density and MassConcentration that I'm not quite sure about. Maybe @lipchev can chime in on this?

Although the Density and Mass Concentration share the same units, they are not interchangeable- they describe different types of ratios. Casting between them (either implicitly or explicitly) makes no sense..

@Muximize
Copy link
Contributor Author

Muximize commented Feb 6, 2024

Thanks for the explanation! That makes the concept of inferred division a bit harder, given that we currently have:

Mass.Kilogram = Density.KilogramPerCubicMeter * Volume.CubicMeter
Mass.Kilogram = MassConcentration.KilogramPerCubicMeter * Volume.CubicMeter

which leads to these inferred ambiguous operators, which is what I tried to solve with my casting shenanigans:

Density.KilogramPerCubicMeter           = Mass.Kilogram / Volume.CubicMeter
MassConcentration.KilogramPerCubicMeter = Mass.Kilogram / Volume.CubicMeter

The latter is currently not defined, so skipping it would not be a breaking change. I've reworked this PR to do that and removed the casting stuff.

Copy link
Owner

@angularsen angularsen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like where this is going and would like to get it merged.
Some comments so far.

UnitsNet.Tests/CustomCode/VolumeTests.cs Show resolved Hide resolved
@@ -630,11 +638,10 @@ public static bool TryParseUnit(string str, IFormatProvider? provider, out Recip
return ReciprocalArea.FromInverseSquareMeters(left.InverseMeters * right.InverseMeters);
}

/// <summary>Calculates the inverse of this quantity.</summary>
/// <returns>The corresponding inverse quantity, <see cref="Length"/>.</returns>
public Length Inverse()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move Inverse() back to its original position? It'll reduce the diff in PR.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this was just this one file, you can disregard this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not just this one, this happens in Area, ReciprocalArea, Length and ReciprocalLength.

But I think it's an improvement, Inverse is now always the first operator in the #region because it's now defined as

1 = Area.SquareMeter * ReciprocalArea.InverseSquareMeter

instead of

ReciprocalArea.InverseSquareMeter = 1 / Area.SquareMeter

and sorted on that string alphanumerically.

.ToList());

// Remove inferred relation "MassConcentration = Mass / Volume" because it duplicates "Density = Mass / Volume"
relations.RemoveAll(r => r is { Operator: "/", ResultQuantity.Name: "MassConcentration", LeftQuantity.Name: "Mass", RightQuantity.Name: "Volume" });
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can solve this in a way that makes it more discoverable by moving this into the JSON file, how about this:

Option 1) Skip division if multiple competing definitions

If there more than one inferred division definition QuantityA / QuantityB with different resulting quantity type, then skip all of them. They will have to be manually defined in the JSON file.

Option 2) Allow defining opt-out in the JSON file

We could move the array to a structure like this:

{
  "skipInferredDivisionForDefinitions": [
    "Mass.Kilogram = MassConcentration.KilogramPerCubicMeter * Volume.CubicMeter"
  ],
  "definitions": [
    "1 = Area.SquareMeter * ReciprocalArea.InverseSquareMeter",
    "1 = ElectricResistivity.OhmMeter * ElectricConductivity.SiemensPerMeter",
    "1 = Length.Meter * ReciprocalLength.InverseMeter",
    "Acceleration.MeterPerSecondSquared = Jerk.MeterPerSecondCubed * Duration.Second",
    "AmountOfSubstance.Kilomole = MolarFlow.KilomolePerSecond * Duration.Second",
    "AmountOfSubstance.Mole = Molarity.MolePerCubicMeter * Volume.CubicMeter",
    "Mass.Kilogram = MassConcentration.KilogramPerCubicMeter * Volume.CubicMeter"
  ]
}

Or add a structure for each definition, allowing us to configure stuff for each entry:

{
  "definitions": [
    { "d": "1 = Area.SquareMeter * ReciprocalArea.InverseSquareMeter" },
    { "d": "1 = ElectricResistivity.OhmMeter * ElectricConductivity.SiemensPerMeter" },
    { "d": "1 = Length.Meter * ReciprocalLength.InverseMeter" },
    { "d": "Acceleration.MeterPerSecondSquared = Jerk.MeterPerSecondCubed * Duration.Second" },
    { "d": "AmountOfSubstance.Kilomole = MolarFlow.KilomolePerSecond * Duration.Second" },
    { "d": "AmountOfSubstance.Mole = Molarity.MolePerCubicMeter * Volume.CubicMeter" },
    { "d": "Mass.Kilogram = MassConcentration.KilogramPerCubicMeter * Volume.CubicMeter", "skipInferredDivision": true }
  ]
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like option 2.2, but I also like the simple json structure we have now, so I propose a string-based solution of just appending NoInferredDivision to the relation, see last commits.

UnitsNet/GeneratedCode/Quantities/Force.g.cs Outdated Show resolved Hide resolved
@Muximize Muximize marked this pull request as ready for review February 24, 2024 12:56
@Muximize Muximize force-pushed the inferred-division branch 2 times, most recently from 44bd139 to 1950b51 Compare February 24, 2024 13:08
angularsen added a commit that referenced this pull request Feb 26, 2024
### Changes
- Change `Duration` from explicit to implicit cast to/from `TimeSpan`
- Remove operator overloads for `TimeSpan` now covered by implicit cast for all but left operands

### Background
See #1354 (comment)

One issue is that the operator overloads only work when `TimeSpan` is the right operand.

I changed the code generation to take this into account, but another option would be to make a breaking change where we just don't support `TimeSpan` as the left operand at all.

Then users would have to cast explicitly, or for multiplication just reverse the operands.

This would affect 13 operators:

```
TimeSpan * Acceleration
TimeSpan * ElectricCurrent
TimeSpan * ElectricCurrentGradient
TimeSpan * ForceChangeRate
TimeSpan * KinematicViscosity
TimeSpan * MassFlow
TimeSpan * MolarFlow
TimeSpan * Power
TimeSpan * PressureChangeRate
TimeSpan * RotationalSpeed
TimeSpan * Speed
TimeSpan * TemperatureChangeRate
TimeSpan * VolumeFlow
```

Of which only 6 are used in tests so I assume are supported in v5:
```
TimeSpan * KinematicViscosity
TimeSpan * MassFlow
TimeSpan * Power
TimeSpan * RotationalSpeed
TimeSpan * TemperatureChangeRate
TimeSpan * Speed
```

---------

Co-authored-by: Andreas Gullberg Larsen <[email protected]>
Copy link
Owner

@angularsen angularsen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, just one minor comment we can address later.

CodeGen/Generators/QuantityRelationsParser.cs Show resolved Hide resolved
@@ -576,7 +590,7 @@ public static bool TryParseUnit(string str, IFormatProvider? provider, out Tempe
/// <summary>Get <see cref="Length"/> from <see cref="TemperatureDelta"/> / <see cref="TemperatureGradient"/>.</summary>
public static Length operator /(TemperatureDelta temperatureDelta, TemperatureGradient temperatureGradient)
{
return Length.FromKilometers(temperatureDelta.Kelvins / temperatureGradient.DegreesCelsiusPerKilometer);
return Length.FromKilometers(temperatureDelta.DegreesCelsius / temperatureGradient.DegreesCelsiusPerKilometer);
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is from before, but isn't PerKilometer an odd choice here?
I'd expect DegreesCelciusPerMeter.
Same below.

Can fix in separate PR.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Muximize Just an extra ping to make sure you saw this:

This is from before, but isn't PerKilometer an odd choice here?
I'd expect DegreesCelciusPerMeter.
Same below.

Can fix in separate PR.

Copy link
Contributor Author

@Muximize Muximize Mar 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree it seems odd, but it was added this way in #991. I guess this is used in meteorology, where kilometers make more sense?

The Kelvin part is weird tho. Previously we had:

TemperatureDelta.DegreeCelsius = TemperatureGradient.DegreeCelsiusPerKilometer * Length.Kilometer

TemperatureGradient.KelvinPerMeter = TemperatureDelta.Kelvin / Length.Meter
Length.Kilometer = TemperatureDelta.Kelvin / TemperatureGradient.DegreeCelsiusPerKilometer

We dropped the last two because they now get inferred, but with Celsius instead of Kelvin. I guess we need a domain expert to answer both these concerns?

Edit: just checked TemperatureDelta.json, Kelvin and DegreeCelsius both have {x} so they're basically aliases.

@angularsen angularsen merged commit f7ce00b into angularsen:release/v6 Mar 1, 2024
1 check passed
angularsen added a commit to Tim-Borcherding/UnitsNet that referenced this pull request Mar 1, 2024
Division now inferred by f7ce00b - Inferred division from defined multiplication relations (angularsen#1354)
@Muximize Muximize deleted the inferred-division branch March 2, 2024 17:48
@angularsen
Copy link
Owner

Nuget on the way

Release UnitsNet/6.0.0-pre003 · angularsen/UnitsNet

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

Successfully merging this pull request may close these issues.

3 participants