Skip to content

Commit

Permalink
Inferred division from defined multiplication relations (angularsen#1354
Browse files Browse the repository at this point in the history
)

In
[angularsen#1329](angularsen#1329 (comment))
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`
  • Loading branch information
Muximize committed Mar 1, 2024
1 parent 667eab1 commit f7ce00b
Show file tree
Hide file tree
Showing 41 changed files with 551 additions and 159 deletions.
47 changes: 38 additions & 9 deletions CodeGen/Generators/QuantityRelationsParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@ internal static class QuantityRelationsParser
///
/// The format of a relation definition is "Quantity.Unit operator Quantity.Unit = Quantity.Unit" (See examples below).
/// "double" can be used as a unitless operand.
/// "1" can be used as the left operand to define inverse relations.
/// "1" can be used as the result operand to define inverse relations.
///
/// Division relations are inferred from multiplication relations,
/// but this can be skipped if the string ends with "NoInferredDivision".
/// </summary>
/// <example>
/// [
/// "Power.Watt = ElectricPotential.Volt * ElectricCurrent.Ampere",
/// "Speed.MeterPerSecond = Length.Meter / Duration.Second",
/// "ReciprocalLength.InverseMeter = 1 / Length.Meter"
/// "1 = Length.Meter * ReciprocalLength.InverseMeter"
/// "Power.Watt = ElectricPotential.Volt * ElectricCurrent.Ampere",
/// "Mass.Kilogram = MassConcentration.KilogramPerCubicMeter * Volume.CubicMeter -- NoInferredDivision",
/// ]
/// </example>
/// <param name="rootDir">Repository root directory.</param>
Expand All @@ -58,10 +61,25 @@ public static void ParseAndApplyRelations(string rootDir, Quantity[] quantities)
RightUnit = r.LeftUnit,
})
.ToList());

// We can infer division relations from multiplication relations.
relations.AddRange(relations
.Where(r => r is { Operator: "*", NoInferredDivision: false })
.Select(r => r with
{
Operator = "/",
LeftQuantity = r.ResultQuantity,
LeftUnit = r.ResultUnit,
ResultQuantity = r.LeftQuantity,
ResultUnit = r.LeftUnit,
})
// Skip division between equal quantities because the ratio is already generated as part of the Arithmetic Operators.
.Where(r => r.LeftQuantity != r.RightQuantity)
.ToList());

// Sort all relations to keep generated operators in a consistent order.
relations.Sort();

var duplicates = relations
.GroupBy(r => r.SortString)
.Where(g => g.Count() > 1)
Expand All @@ -73,6 +91,18 @@ public static void ParseAndApplyRelations(string rootDir, Quantity[] quantities)
var list = string.Join("\n ", duplicates);
throw new UnitsNetCodeGenException($"Duplicate inferred relations:\n {list}");
}

var ambiguous = relations
.GroupBy(r => $"{r.LeftQuantity.Name} {r.Operator} {r.RightQuantity.Name}")
.Where(g => g.Count() > 1)
.Select(g => g.Key)
.ToList();

if (ambiguous.Any())
{
var list = string.Join("\n ", ambiguous);
throw new UnitsNetCodeGenException($"Ambiguous inferred relations:\n {list}\n\nHint: you could use NoInferredDivision in the definition file.");
}

foreach (var quantity in quantities)
{
Expand Down Expand Up @@ -122,7 +152,7 @@ private static QuantityRelation ParseRelation(string relationString, IReadOnlyDi
{
var segments = relationString.Split(' ');

if (segments is not [_, "=", _, "*" or "/", _])
if (segments is not [_, "=", _, "*", _, ..])
{
throw new Exception($"Invalid relation string: {relationString}");
}
Expand All @@ -140,15 +170,14 @@ private static QuantityRelation ParseRelation(string relationString, IReadOnlyDi
var rightUnit = GetUnit(rightQuantity, right.ElementAtOrDefault(1));
var resultUnit = GetUnit(resultQuantity, result.ElementAtOrDefault(1));

if (leftQuantity.Name == "1")
if (resultQuantity.Name == "1")
{
@operator = "inverse";
leftQuantity = resultQuantity;
leftUnit = resultUnit;
}

return new QuantityRelation
{
NoInferredDivision = segments.Contains("NoInferredDivision"),
Operator = @operator,
LeftQuantity = leftQuantity,
LeftUnit = leftUnit,
Expand Down
1 change: 1 addition & 0 deletions CodeGen/JsonTypes/QuantityRelation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace CodeGen.JsonTypes
{
internal record QuantityRelation : IComparable<QuantityRelation>
{
public bool NoInferredDivision = false;
public string Operator = null!;

public Quantity LeftQuantity = null!;
Expand Down
115 changes: 15 additions & 100 deletions Common/UnitRelations.json
Original file line number Diff line number Diff line change
@@ -1,164 +1,79 @@
[
"Acceleration.MeterPerSecondSquared = Force.Newton / Mass.Kilogram",
"Acceleration.MeterPerSecondSquared = SpecificWeight.NewtonPerCubicMeter / Density.KilogramPerCubicMeter",
"Acceleration.MeterPerSecondSquared = Speed.MeterPerSecond / Duration.Second",
"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 = Mass.Kilogram / MolarMass.KilogramPerMole",
"AmountOfSubstance.Mole = Molarity.MolePerCubicMeter * Volume.CubicMeter",
"Angle.Radian = RotationalSpeed.RadianPerSecond * Duration.Second",
"Angle.Radian = Torque.NewtonMeter / RotationalStiffness.NewtonMeterPerRadian",
"Area.SquareMeter = KinematicViscosity.SquareMeterPerSecond * Duration.Second",
"Area.SquareMeter = Length.Meter * Length.Meter",
"Area.SquareMeter = LinearDensity.KilogramPerMeter / Density.KilogramPerCubicMeter",
"Area.SquareMeter = LuminousIntensity.Candela / Luminance.CandelaPerSquareMeter",
"Area.SquareMeter = Mass.Kilogram / AreaDensity.KilogramPerSquareMeter",
"Area.SquareMeter = MassFlow.KilogramPerSecond / MassFlux.KilogramPerSecondPerSquareMeter",
"Area.SquareMeter = Power.Watt / HeatFlux.WattPerSquareMeter",
"Area.SquareMeter = Volume.CubicMeter / Length.Meter",
"Area.SquareMeter = VolumeFlow.CubicMeterPerSecond / Speed.MeterPerSecond",
"AreaDensity.KilogramPerSquareMeter = Mass.Kilogram / Area.SquareMeter",
"BrakeSpecificFuelConsumption.KilogramPerJoule = double / SpecificEnergy.JoulePerKilogram",
"BrakeSpecificFuelConsumption.KilogramPerJoule = MassFlow.KilogramPerSecond / Power.Watt",
"Density.KilogramPerCubicMeter = double / SpecificVolume.CubicMeterPerKilogram",
"Density.KilogramPerCubicMeter = LinearDensity.KilogramPerMeter / Area.SquareMeter",
"Density.KilogramPerCubicMeter = Mass.Kilogram / Volume.CubicMeter",
"Density.KilogramPerCubicMeter = MassFlow.KilogramPerSecond / VolumeFlow.CubicMeterPerSecond",
"Density.KilogramPerCubicMeter = MassFlux.KilogramPerSecondPerSquareMeter / Speed.MeterPerSecond",
"Density.KilogramPerCubicMeter = SpecificWeight.NewtonPerCubicMeter / Acceleration.MeterPerSecondSquared",
"AreaMomentOfInertia.MeterToTheFourth = Volume.CubicMeter * Length.Meter",
"double = Density.KilogramPerCubicMeter * SpecificVolume.CubicMeterPerKilogram",
"double = SpecificEnergy.JoulePerKilogram * BrakeSpecificFuelConsumption.KilogramPerJoule",
"double = TemperatureDelta.Kelvin * CoefficientOfThermalExpansion.PerKelvin",
"Duration.Hour = ElectricCharge.AmpereHour / ElectricCurrent.Ampere",
"Duration.Second = Energy.Joule / Power.Watt",
"Duration.Second = Force.Newton / ForceChangeRate.NewtonPerSecond",
"Duration.Second = Length.Meter / Speed.MeterPerSecond",
"Duration.Second = Speed.MeterPerSecond / Acceleration.MeterPerSecondSquared",
"DynamicViscosity.NewtonSecondPerMeterSquared = Density.KilogramPerCubicMeter * KinematicViscosity.SquareMeterPerSecond",
"ElectricCharge.AmpereHour = ElectricCurrent.Ampere * Duration.Hour",
"ElectricCharge.Coulomb = Energy.Joule / ElectricPotential.Volt",
"ElectricConductivity.SiemensPerMeter = 1 / ElectricResistivity.OhmMeter",
"ElectricCurrent.Ampere = ElectricCharge.AmpereHour / Duration.Hour",
"ElectricCurrent.Ampere = ElectricCurrentGradient.AmperePerSecond * Duration.Second",
"ElectricCurrent.Ampere = ElectricPotential.Volt / ElectricResistance.Ohm",
"ElectricCurrent.Ampere = Power.Watt / ElectricPotential.Volt",
"ElectricCurrentGradient.AmperePerSecond = ElectricCurrent.Ampere / Duration.Second",
"ElectricPotential.Volt = ElectricCurrent.Ampere * ElectricResistance.Ohm",
"ElectricPotential.Volt = Energy.Joule / ElectricCharge.Coulomb",
"ElectricPotential.Volt = Power.Watt / ElectricCurrent.Ampere",
"ElectricResistance.Ohm = ElectricPotential.Volt / ElectricCurrent.Ampere",
"Energy.Joule = ElectricPotential.Volt * ElectricCharge.Coulomb",
"Energy.Joule = EnergyDensity.JoulePerCubicMeter * Volume.CubicMeter",
"Energy.Joule = Power.Watt * Duration.Second",
"Energy.Joule = SpecificEnergy.JoulePerKilogram * Mass.Kilogram",
"Energy.Joule = TemperatureDelta.Kelvin * Entropy.JoulePerKelvin",
"Entropy.JoulePerKelvin = Energy.Joule / TemperatureDelta.Kelvin",
"Entropy.JoulePerKelvin = SpecificEntropy.JoulePerKilogramKelvin * Mass.Kilogram",
"Force.Newton = ForceChangeRate.NewtonPerSecond * Duration.Second",
"Force.Newton = ForcePerLength.NewtonPerMeter * Length.Meter",
"Force.Newton = ForcePerLength.NewtonPerMeter / ReciprocalLength.InverseMeter",
"Force.Newton = Mass.Kilogram * Acceleration.MeterPerSecondSquared",
"Force.Newton = Power.Watt / Speed.MeterPerSecond",
"Force.Newton = Pressure.Pascal * Area.SquareMeter",
"Force.Newton = Pressure.Pascal / ReciprocalArea.InverseSquareMeter",
"Force.Newton = Torque.NewtonMeter / Length.Meter",
"ForcePerLength.NewtonPerMeter = Force.Newton * ReciprocalLength.InverseMeter",
"ForcePerLength.NewtonPerMeter = Force.Newton / Length.Meter",
"ForcePerLength.NewtonPerMeter = Pressure.Pascal / ReciprocalLength.InverseMeter",
"ForcePerLength.NewtonPerMeter = Pressure.NewtonPerSquareMeter * Length.Meter",
"ForcePerLength.NewtonPerMeter = SpecificWeight.NewtonPerCubicMeter * Area.SquareMeter",
"HeatFlux.WattPerSquareMeter = Power.Watt / Area.SquareMeter",
"Jerk.MeterPerSecondCubed = Acceleration.MeterPerSecondSquared / Duration.Second",
"KinematicViscosity.SquareMeterPerSecond = DynamicViscosity.NewtonSecondPerMeterSquared / Density.KilogramPerCubicMeter",
"KinematicViscosity.SquareMeterPerSecond = Length.Meter * Speed.MeterPerSecond",
"Length.Kilometer = TemperatureDelta.Kelvin / TemperatureGradient.DegreeCelsiusPerKilometer",
"Length.Meter = Area.SquareMeter / Length.Meter",
"Length.Meter = Force.Newton / ForcePerLength.NewtonPerMeter",
"Length.Meter = Mass.Kilogram / LinearDensity.KilogramPerMeter",
"Length.Meter = Pressure.Pascal / SpecificWeight.NewtonPerCubicMeter",
"Length.Meter = ReciprocalLength.InverseMeter / ReciprocalArea.InverseSquareMeter",
"Length.Meter = RotationalStiffness.NewtonMeterPerRadian / RotationalStiffnessPerLength.NewtonMeterPerRadianPerMeter",
"Length.Meter = Speed.MeterPerSecond * Duration.Second",
"Length.Meter = Torque.NewtonMeter / Force.Newton",
"Length.Meter = Volume.CubicMeter / Area.SquareMeter",
"LinearDensity.KilogramPerMeter = Area.SquareMeter * Density.KilogramPerCubicMeter",
"LinearDensity.KilogramPerMeter = Mass.Kilogram / Length.Meter",
"Luminance.CandelaPerSquareMeter = LuminousIntensity.Candela / Area.SquareMeter",
"LuminousIntensity.Candela = Luminance.CandelaPerSquareMeter * Area.SquareMeter",
"Mass.Gram = AmountOfSubstance.Mole * MolarMass.GramPerMole",
"Mass.Kilogram = AreaDensity.KilogramPerSquareMeter * Area.SquareMeter",
"Mass.Kilogram = Density.KilogramPerCubicMeter * Volume.CubicMeter",
"Mass.Kilogram = Energy.Joule / SpecificEnergy.JoulePerKilogram",
"Mass.Kilogram = Force.Newton / Acceleration.MeterPerSecondSquared",
"Mass.Kilogram = LinearDensity.KilogramPerMeter * Length.Meter",
"Mass.Kilogram = Mass.Kilogram / MassFraction.DecimalFraction",
"Mass.Kilogram = MassConcentration.KilogramPerCubicMeter * Volume.CubicMeter",
"Mass.Kilogram = MassConcentration.KilogramPerCubicMeter * Volume.CubicMeter -- NoInferredDivision",
"Mass.Kilogram = MassFlow.KilogramPerSecond * Duration.Second",
"Mass.Kilogram = MassFraction.DecimalFraction * Mass.Kilogram",
"MassConcentration.GramPerCubicMeter = Molarity.MolePerCubicMeter * MolarMass.GramPerMole",
"MassConcentration.KilogramPerCubicMeter = Molarity.MolePerCubicMeter * MolarMass.KilogramPerMole",
"MassConcentration.KilogramPerCubicMeter = VolumeConcentration.DecimalFraction * Density.KilogramPerCubicMeter",
"MassFlow.GramPerSecond = Area.SquareMeter * MassFlux.GramPerSecondPerSquareMeter",
"MassFlow.KilogramPerSecond = Mass.Kilogram / Duration.Second",
"MassFlow.KilogramPerSecond = Area.SquareMeter * MassFlux.KilogramPerSecondPerSquareMeter",
"MassFlow.KilogramPerSecond = MolarFlow.KilomolePerSecond * MolarMass.KilogramPerKilomole",
"MassFlow.KilogramPerSecond = Power.Watt * BrakeSpecificFuelConsumption.KilogramPerJoule",
"MassFlow.KilogramPerSecond = Power.Watt / SpecificEnergy.JoulePerKilogram",
"MassFlow.KilogramPerSecond = VolumeFlow.CubicMeterPerSecond * Density.KilogramPerCubicMeter",
"MassFlux.KilogramPerSecondPerSquareMeter = MassFlow.KilogramPerSecond / Area.SquareMeter",
"MassFlux.KilogramPerSecondPerSquareMeter = Speed.MeterPerSecond * Density.KilogramPerCubicMeter",
"Molarity.MolePerCubicMeter = AmountOfSubstance.Mole / Volume.CubicMeter",
"Molarity.MolePerCubicMeter = MassConcentration.GramPerCubicMeter / MolarMass.GramPerMole",
"MolarFlow.MolePerSecond = VolumeFlow.CubicMeterPerSecond * Molarity.MolePerCubicMeter",
"Molarity.MolePerCubicMeter = Molarity.MolePerCubicMeter * VolumeConcentration.DecimalFraction",
"Power.Watt = ElectricPotential.Volt * ElectricCurrent.Ampere",
"Power.Watt = Energy.Joule * Frequency.PerSecond",
"Power.Watt = Energy.Joule / Duration.Second",
"Power.Watt = Force.Newton * Speed.MeterPerSecond",
"Power.Watt = HeatFlux.WattPerSquareMeter * Area.SquareMeter",
"Power.Watt = MassFlow.KilogramPerSecond / BrakeSpecificFuelConsumption.KilogramPerJoule",
"Power.Watt = SpecificEnergy.JoulePerKilogram * MassFlow.KilogramPerSecond",
"Power.Watt = Torque.NewtonMeter * RotationalSpeed.RadianPerSecond",
"Pressure.NewtonPerSquareMeter = Force.Newton * ReciprocalArea.InverseSquareMeter",
"Pressure.NewtonPerSquareMeter = ForcePerLength.NewtonPerMeter * ReciprocalLength.InverseMeter",
"Pressure.NewtonPerSquareMeter = ForcePerLength.NewtonPerMeter / Length.Meter",
"Pressure.Pascal = Force.Newton / Area.SquareMeter",
"Pressure.Pascal = PressureChangeRate.PascalPerSecond * Duration.Second",
"Pressure.Pascal = SpecificWeight.NewtonPerCubicMeter * Length.Meter",
"PressureChangeRate.PascalPerSecond = Pressure.Pascal / Duration.Second",
"Ratio.DecimalFraction = Area.SquareMeter * ReciprocalArea.InverseSquareMeter",
"ReciprocalArea.InverseSquareMeter = 1 / Area.SquareMeter",
"ReciprocalArea.InverseSquareMeter = ReciprocalLength.InverseMeter * ReciprocalLength.InverseMeter",
"ReciprocalLength.InverseMeter = 1 / Length.Meter",
"ReciprocalLength.InverseMeter = ReciprocalArea.InverseSquareMeter / ReciprocalLength.InverseMeter",
"RotationalSpeed.RadianPerSecond = Angle.Radian / Duration.Second",
"RotationalSpeed.RadianPerSecond = Power.Watt / Torque.NewtonMeter",
"ReciprocalLength.InverseMeter = Length.Meter * ReciprocalArea.InverseSquareMeter",
"RotationalStiffness.NewtonMeterPerRadian = RotationalStiffnessPerLength.NewtonMeterPerRadianPerMeter * Length.Meter",
"RotationalStiffness.NewtonMeterPerRadian = Torque.NewtonMeter / Angle.Radian",
"RotationalStiffnessPerLength.NewtonMeterPerRadianPerMeter = RotationalStiffness.NewtonMeterPerRadian / Length.Meter",
"SpecificEnergy.JoulePerKilogram = double / BrakeSpecificFuelConsumption.KilogramPerJoule",
"SpecificEnergy.JoulePerKilogram = Energy.Joule / Mass.Kilogram",
"SpecificEnergy.JoulePerKilogram = Power.Watt / MassFlow.KilogramPerSecond",
"SpecificEnergy.JoulePerKilogram = SpecificEntropy.JoulePerKilogramKelvin * TemperatureDelta.Kelvin",
"SpecificEnergy.JoulePerKilogram = Speed.MeterPerSecond * Speed.MeterPerSecond",
"SpecificEntropy.JoulePerKilogramKelvin = Entropy.JoulePerKelvin / Mass.Kilogram",
"SpecificEntropy.JoulePerKilogramKelvin = SpecificEnergy.JoulePerKilogram / TemperatureDelta.Kelvin",
"SpecificWeight.NewtonPerCubicMeter = Acceleration.MeterPerSecondSquared * Density.KilogramPerCubicMeter",
"SpecificWeight.NewtonPerCubicMeter = Pressure.Pascal / Length.Meter",
"Speed.MeterPerSecond = Acceleration.MeterPerSecondSquared * Duration.Second",
"Speed.MeterPerSecond = KinematicViscosity.SquareMeterPerSecond / Length.Meter",
"Speed.MeterPerSecond = Length.Meter / Duration.Second",
"Speed.MeterPerSecond = MassFlux.KilogramPerSecondPerSquareMeter / Density.KilogramPerCubicMeter",
"Speed.MeterPerSecond = VolumeFlow.CubicMeterPerSecond / Area.SquareMeter",
"TemperatureDelta.DegreeCelsius = TemperatureChangeRate.DegreeCelsiusPerSecond * Duration.Second",
"TemperatureDelta.DegreeCelsius = TemperatureGradient.DegreeCelsiusPerKilometer * Length.Kilometer",
"TemperatureDelta.Kelvin = Energy.Joule / Entropy.JoulePerKelvin",
"TemperatureGradient.KelvinPerMeter = TemperatureDelta.Kelvin / Length.Meter",
"Torque.NewtonMeter = ForcePerLength.NewtonPerMeter * Area.SquareMeter",
"Torque.NewtonMeter = Length.Meter * Force.Newton",
"Torque.NewtonMeter = Power.Watt / RotationalSpeed.RadianPerSecond",
"Torque.NewtonMeter = RotationalStiffness.NewtonMeterPerRadian * Angle.Radian",
"Volume.CubicMeter = AmountOfSubstance.Mole / Molarity.MolePerCubicMeter",
"Volume.CubicMeter = AreaMomentOfInertia.MeterToTheFourth / Length.Meter",
"Volume.CubicMeter = Length.Meter * Area.SquareMeter",
"Volume.CubicMeter = Mass.Kilogram / Density.KilogramPerCubicMeter",
"Volume.CubicMeter = SpecificVolume.CubicMeterPerKilogram * Mass.Kilogram",
"Volume.CubicMeter = VolumeFlow.CubicMeterPerSecond * Duration.Second",
"VolumeConcentration.DecimalFraction = MassConcentration.KilogramPerCubicMeter / Density.KilogramPerCubicMeter",
"VolumeFlow.CubicMeterPerSecond = Area.SquareMeter * Speed.MeterPerSecond",
"VolumeFlow.CubicMeterPerSecond = MassFlow.KilogramPerSecond / Density.KilogramPerCubicMeter",
"VolumeFlow.CubicMeterPerSecond = MolarFlow.MolePerSecond / Molarity.MolePerCubicMeter",
"VolumeFlow.CubicMeterPerSecond = Volume.CubicMeter / Duration.Second"
"VolumeFlow.CubicMeterPerSecond = Area.SquareMeter * Speed.MeterPerSecond"
]
4 changes: 2 additions & 2 deletions UnitsNet.Tests/CustomCode/VolumeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ public void VolumeDividedByDurationEqualsVolumeFlow()
[Fact]
public void VolumeDividedByVolumeFlowEqualsTimeSpan()
{
TimeSpan timeSpan = Volume.FromCubicMeters(20) / VolumeFlow.FromCubicMetersPerSecond(2);
Assert.Equal(TimeSpan.FromSeconds(10), timeSpan);
Duration duration = Volume.FromCubicMeters(20) / VolumeFlow.FromCubicMetersPerSecond(2);
Assert.Equal(Duration.FromSeconds(10), duration);
}
}
}
16 changes: 0 additions & 16 deletions UnitsNet/CustomCode/Quantities/Volume.extra.cs

This file was deleted.

Loading

0 comments on commit f7ce00b

Please sign in to comment.