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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.");
angularsen marked this conversation as resolved.
Show resolved Hide resolved
}

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);
angularsen marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
16 changes: 0 additions & 16 deletions UnitsNet/CustomCode/Quantities/Volume.extra.cs

This file was deleted.

Loading