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

ILinearQuantity, ILogarithmicQuantity and IAffineQuantity interfaces #1461

Open
lipchev opened this issue Dec 16, 2024 · 10 comments
Open

ILinearQuantity, ILogarithmicQuantity and IAffineQuantity interfaces #1461

lipchev opened this issue Dec 16, 2024 · 10 comments

Comments

@lipchev
Copy link
Collaborator

lipchev commented Dec 16, 2024

Following my comments on the subject in #1200 here is the proposed modification to the IQuantity interfaces:

  1. ILinearQuantity - in v6 Wishlist #1200 I used the name IVectorQuantity but after some reflection I decided that the word Linear is a better fit (in fact, if we look at the conversion expressions- they are all of the form ax, without an offset)
/// <summary>
///     Represents a quantity that has both magnitude and direction, supporting various arithmetic operations and
///     comparisons.
/// </summary>
/// <remarks>
///     This interface defines standard linear arithmetic operations such as addition, subtraction, multiplication, and
///     division.
///     These types of quantities naturally support comparison operations with either absolute or relative tolerance, which
///     is useful for determining equality within a certain margin of error.
///     <para>
///         For more information, see the Wikipedia page on
///         <a href="https://en.wikipedia.org/wiki/Dimensional_analysis#Geometry:_position_vs._displacement">
///             Dimensional
///             Analysis
///         </a>
///         .
///     </para>
/// </remarks>
/// <typeparam name="TSelf">The type that implements this interface.</typeparam>
public interface ILinearQuantity<TSelf> : IQuantityInstance<TSelf>
#if NET7_0_OR_GREATER
    , IAdditiveIdentity<TSelf, TSelf>
#endif
    where TSelf : ILinearQuantity<TSelf>
{
#if NET7_0_OR_GREATER
    /// <summary>
    ///     The zero value of this quantity.
    /// </summary>
    static abstract TSelf Zero { get; }

    static TSelf IAdditiveIdentity<TSelf, TSelf>.AdditiveIdentity
    {
        get => TSelf.Zero;
    }

#endif

#if EXTENDED_EQUALS_INTERFACE
        /// <summary>
        ///     <para>
        ///     Compare equality to <paramref name="other"/> given a <paramref name="tolerance"/> for the maximum allowed +/- difference.
        ///     </para>
        ///     <example>
        ///     In this example, the two quantities will be equal if the value of b is within 0.01 of a (0.01m or 1cm).
        ///     <code>
        ///     var a = Length.FromMeters(2.0);
        ///     var b = Length.FromMeters(2.1);
        ///     var tolerance = Length.FromCentimeters(10);
        ///     a.Equals(b, tolerance); // true, 2m equals 2.1m +/- 0.1m
        ///     </code>
        ///     </example>
        ///     <para>
        ///     It is generally advised against specifying "zero" tolerance, due to the nature of floating-point operations.
        ///     </para>
        /// </summary>
        /// <param name="other">The other quantity to compare to.</param>
        /// <param name="tolerance">The absolute tolerance value. Must be greater than or equal to zero.</param>
        /// <returns>True if the absolute difference between the two values is not greater than the specified tolerance.</returns>
        bool Equals(TSelf? other, TSelf tolerance);
#endif
}

/// <summary>
///     An <see cref="IQuantity{TSelf, TUnitType}" /> that (in .NET 7+) implements generic math interfaces for arithmetic
///     operations.
/// </summary>
/// <typeparam name="TSelf">The type itself, for the CRT pattern.</typeparam>
/// <typeparam name="TUnitType">The underlying unit enum type.</typeparam>
public interface IArithmeticQuantity<TSelf, TUnitType> : IQuantity<TSelf, TUnitType>, ILinearQuantity<TSelf>
#if NET7_0_OR_GREATER
    , IAdditionOperators<TSelf, TSelf, TSelf>
    , ISubtractionOperators<TSelf, TSelf, TSelf>
    , IMultiplyOperators<TSelf, QuantityValue, TSelf>
    , IDivisionOperators<TSelf, QuantityValue, TSelf>
    , IUnaryNegationOperators<TSelf, TSelf>
#endif
    where TSelf : IArithmeticQuantity<TSelf, TUnitType>
    where TUnitType : struct, Enum
{
}
  1. ILogarithmicQuantity - in NET7_OR_GREATER I introduce the static LogarithmicScalingFactor, however in netstandard this needs to either be an instance member ( 😞 ) or alternatively I could introduce an extra interface for it's QuantityInfo, overriding the property to something of the sort new ILogarithmicQuantityInfo<TQuantity> QuantityInfo { get; }
/// <summary>
///     Represents a logarithmic quantity that supports arithmetic operations and implements generic math interfaces 
///     (in .NET 7+). This interface is designed for quantities that are logarithmic in nature, such as decibels.
/// </summary>
/// <typeparam name="TSelf">The type itself, for the CRT pattern.</typeparam>
/// <remarks>
///     Logarithmic quantities are different from linear quantities in that they represent values on a logarithmic scale.
///     This interface extends <see cref="IQuantity{TSelf, TUnitType}" /> and provides additional functionality specific 
///     to logarithmic quantities, including arithmetic operations and a logarithmic scaling factor.
///     The logarithmic scale assumed here is base-10.
/// </remarks>
public interface ILogarithmicQuantity<TSelf> : IQuantityInstance<TSelf>
#if NET7_0_OR_GREATER
    , IMultiplicativeIdentity<TSelf, TSelf>
#endif
    where TSelf : ILogarithmicQuantity<TSelf>
{
#if NET7_0_OR_GREATER
    /// <summary>
    ///     Gets the logarithmic scaling factor used to convert between linear and logarithmic units.
    ///     This factor is typically 10, but there are exceptions such as the PowerRatio, which uses 20.
    /// </summary>
    /// <value>
    ///     The logarithmic scaling factor.
    /// </value>
    static abstract QuantityValue LogarithmicScalingFactor { get; }
    
    /// <summary>
    ///     The zero value of this quantity.
    /// </summary>
    static abstract TSelf Zero { get; }
    
    static TSelf IMultiplicativeIdentity<TSelf, TSelf>.MultiplicativeIdentity => TSelf.Zero;
#else
    /// <summary>
    ///     Gets the logarithmic scaling factor used to convert between linear and logarithmic units.
    ///     This factor is typically 10, but there are exceptions such as the PowerRatio, which uses 20.
    /// </summary>
    /// <value>
    ///     The logarithmic scaling factor.
    /// </value>
    QuantityValue LogarithmicScalingFactor { get; }
#endif
}

/// <inheritdoc cref="ILogarithmicQuantity{TSelf}"/>
/// <typeparam name="TSelf">The type itself, for the CRT pattern.</typeparam>
/// <typeparam name="TUnitType">The underlying unit enum type.</typeparam>
public interface ILogarithmicQuantity<TSelf, TUnitType> : IQuantity<TSelf, TUnitType>, ILogarithmicQuantity<TSelf>
#if NET7_0_OR_GREATER
    , IAdditionOperators<TSelf, TSelf, TSelf>
    , ISubtractionOperators<TSelf, TSelf, TSelf>
    , IMultiplyOperators<TSelf, QuantityValue, TSelf>
    , IDivisionOperators<TSelf, QuantityValue, TSelf>
    , IUnaryNegationOperators<TSelf, TSelf>
#endif
    where TSelf : ILogarithmicQuantity<TSelf, TUnitType>
    where TUnitType : struct, Enum
{
}
  1. IAffineQuantity: there is only one of these- the Temperature with it's TOffset being the TemperatureDelta (also note that the conversion functions here are of the form ax + b)
/// <summary>
///     An <see cref="IQuantity{TSelf}"/> that (in .NET 7+) implements generic math interfaces for arithmetic operations.
/// </summary>
/// <typeparam name="TSelf">The type itself, for the CRT pattern.</typeparam>
/// <typeparam name="TOffset"></typeparam>
public interface IAffineQuantity<TSelf, TOffset> : IQuantityInstance<TSelf>
#if NET7_0_OR_GREATER
    , IAdditiveIdentity<TSelf, TOffset>
    where TOffset : IAdditiveIdentity<TOffset, TOffset>
#endif
    where TSelf : IAffineQuantity<TSelf, TOffset>
{
#if NET7_0_OR_GREATER
    /// <summary>
    ///     The zero value of this quantity.
    /// </summary>
    static abstract TSelf Zero { get; }

    static TOffset IAdditiveIdentity<TSelf, TOffset>.AdditiveIdentity => TOffset.AdditiveIdentity;
#endif
        
#if EXTENDED_EQUALS_INTERFACE
    /// <summary>
    ///     <para>
    ///     Compare equality to <paramref name="other"/> given a <paramref name="tolerance"/> for the maximum allowed +/- difference.
    ///     </para>
    ///     <example>
    ///     In this example, the two quantities will be equal if the value of b is within 0.01 of a (0.01m or 1cm).
    ///     <code>
    ///     var a = Length.FromMeters(2.0);
    ///     var b = Length.FromMeters(2.1);
    ///     var tolerance = Length.FromCentimeters(10);
    ///     a.Equals(b, tolerance); // true, 2m equals 2.1m +/- 0.1m
    ///     </code>
    ///     </example>
    ///     <para>
    ///     It is generally advised against specifying "zero" tolerance, due to the nature of floating-point operations.
    ///     </para>
    /// </summary>
    /// <param name="other">The other quantity to compare to.</param>
    /// <param name="tolerance">The absolute tolerance value. Must be greater than or equal to zero.</param>
    /// <returns>True if the absolute difference between the two values is not greater than the specified tolerance.</returns>
    bool Equals(TSelf? other, TOffset tolerance);
#endif
}

/// <summary>
///     An <see cref="IQuantity{TSelf, TUnitType}"/> that (in .NET 7+) implements generic math interfaces for arithmetic operations.
/// </summary>
/// <typeparam name="TSelf">The type itself, for the CRT pattern.</typeparam>
/// <typeparam name="TUnitType">The underlying unit enum type.</typeparam>
/// <typeparam name="TOffset"></typeparam>
public interface IAffineQuantity<TSelf, TUnitType, TOffset> : IQuantity<TSelf, TUnitType>, IAffineQuantity<TSelf, TOffset>
#if NET7_0_OR_GREATER
    , IAdditionOperators<TSelf, TOffset, TSelf>
    , ISubtractionOperators<TSelf, TSelf, TOffset>
    where TOffset : IAdditiveIdentity<TOffset, TOffset>
#endif
    where TSelf : IAffineQuantity<TSelf, TUnitType, TOffset>
    where TUnitType : struct, Enum
{
}
@lipchev
Copy link
Collaborator Author

lipchev commented Dec 16, 2024

You'd probably wonder about the IQuantityInstance<TSelf> interface - but that's a subject for another time, for now you can think of it as an interface that allows us to constrain a collection of TQuantity to be of the same type without having to introduce a second generic parameter, representing the TUnit:

/// <inheritdoc cref="IQuantity" />
/// <remarks>
///     This is a specialization of <see cref="IQuantity" /> that is used (internally) for constraining certain
///     methods, without having to include the unit type as additional generic parameter.
/// </remarks>
/// <typeparam name="TQuantity"></typeparam>
public interface IQuantityInstance<out TQuantity> : IQuantity
    where TQuantity : IQuantity

It currently sits between the IQuantity<TQuantity, TUnit> and IQuantity (in the same file - please tell me if you think this should go into it's own .cs):

public interface IQuantity<TSelf, TUnitType> : IQuantityInstance<TSelf>, IQuantity<TUnitType>
    where TSelf : IQuantity<TSelf, TUnitType>
    where TUnitType : struct, Enum

@lipchev
Copy link
Collaborator Author

lipchev commented Dec 16, 2024

Before I get to the EXTENDED_EQUALS_INTERFACE (and why I don't have it enabled on the interface) - let me give you the prototypes of the Sum/Average extensions (they work in both netstandard2.0 and NET8):

  1. LinearQuantityExtensions:
    /// <summary>
    ///     Sums a sequence of linear quantities, such as Mass and Length.
    /// </summary>
    /// <typeparam name="TQuantity">The type of the linear quantity.</typeparam>
    /// <param name="quantities">The collection of linear quantities to sum.</param>
    /// <returns>
    ///     The sum of the linear quantities. If the sequence is empty, returns zero in the base unit.
    ///     In all other cases, the result will have the unit of the first element in the collection.
    /// </returns>
    public static TQuantity Sum<TQuantity>(this IEnumerable<TQuantity> quantities)
        where TQuantity : ILinearQuantity<TQuantity>
    /// <summary>
    ///     Computes the sum of a sequence of quantities by applying a specified selector function to each element of the
    ///     sequence.
    /// </summary>
    /// <typeparam name="TSource">The type of the elements of the source sequence.</typeparam>
    /// <typeparam name="TQuantity">The type of the quantity elements in the source sequence.</typeparam>
    /// <param name="source">A sequence of quantities to calculate the sum of.</param>
    /// <param name="selector">A function to transform each element of the source sequence into a quantity.</param>
    /// <returns>
    ///     The sum of the projected quantities. If the sequence is empty, returns zero in the base unit; otherwise,
    ///     returns the sum in the unit of the first element.
    /// </returns>
    /// <exception cref="ArgumentNullException">Thrown if the source or selector is null.</exception>
    public static TQuantity Sum<TSource, TQuantity>(this IEnumerable<TSource> source, Func<TSource, TQuantity> selector)
        where TQuantity : ILinearQuantity<TQuantity>
    {
        return source.Select(selector).Sum();
    }
    /// <summary>
    ///     Sums a sequence of linear quantities, such as Mass and Length.
    /// </summary>
    /// <typeparam name="TQuantity">The type of the linear quantity.</typeparam>
    /// <typeparam name="TUnit">The unit type of the linear quantity.</typeparam>
    /// <param name="quantities">The collection of linear quantities to sum.</param>
    /// <param name="unit">The unit in which to express the sum.</param>
    /// <returns>The sum of the linear quantities in the specified unit.</returns>
    /// <exception cref="ArgumentNullException">Thrown when the sequence is null.</exception>
    /// <remarks>
    ///     This method is slightly more performant than the alternative <see cref="Sum{TQuantity}(IEnumerable{TQuantity})" />
    ///     when most of the quantities in the sequence are expected to be in the target unit.
    /// </remarks>
    public static TQuantity Sum<TQuantity, TUnit>(this IEnumerable<TQuantity> quantities, TUnit unit)
        where TQuantity : ILinearQuantity<TQuantity>, IQuantity<TQuantity, TUnit>
        where TUnit : struct, Enum
    /// <summary>
    ///     Computes the sum of the sequence of <typeparamref name="TQuantity" /> values that are obtained by invoking a
    ///     transform function on each element of the input sequence.
    /// </summary>
    /// <typeparam name="TSource">The type of the elements of the source sequence.</typeparam>
    /// <typeparam name="TQuantity">The type of the quantity elements in the source sequence.</typeparam>
    /// <typeparam name="TUnit">The type of the unit of the quantities.</typeparam>
    /// <param name="source">A sequence of quantities to calculate the sum of.</param>
    /// <param name="selector">A function to transform each element of the source sequence into a quantity.</param>
    /// <param name="targetUnit">The desired unit type for the resulting quantity</param>
    /// <returns>The sum of the projected quantities in the specified unit.</returns>
    /// <exception cref="ArgumentNullException">Thrown if the source or selector is null.</exception>
    public static TQuantity Sum<TSource, TQuantity, TUnit>(this IEnumerable<TSource> source, Func<TSource, TQuantity> selector, TUnit targetUnit)
        where TQuantity : ILinearQuantity<TQuantity>, IQuantity<TQuantity, TUnit>
        where TUnit : struct, Enum
    {
        return source.Select(selector).Sum(targetUnit);
    }

It's basically the same thing for the Average only here we're also throwing an InvalidOperationException when the sequence is empty:

    /// <summary>
    ///     Calculates the arithmetic average of a sequence of linear quantities, such as Mass and Length.
    /// </summary>
    /// <typeparam name="TQuantity">The type of the linear quantity.</typeparam>
    /// <param name="quantities">The sequence of linear quantities to average.</param>
    /// <returns>The average of the linear quantities, using the unit of the first element in the sequence.</returns>
    /// <exception cref="ArgumentNullException">Thrown when the sequence is null.</exception>
    /// <exception cref="InvalidOperationException">Thrown when the sequence is empty.</exception>
    public static TQuantity Average<TQuantity>(this IEnumerable<TQuantity> quantities)
        where TQuantity : ILinearQuantity<TQuantity>
    {
        return quantities.ArithmeticMean();
    }

    /// <summary>
    ///     Computes the arithmetic average of a sequence of quantities, such as Mass and Length, by applying a specified
    ///     selector
    ///     function to each element of the elements in the sequence.
    /// </summary>
    /// <typeparam name="TSource">The type of the elements of the source sequence.</typeparam>
    /// <typeparam name="TQuantity">The type of the quantity elements in the source sequence.</typeparam>
    /// <param name="source">A sequence of quantities to calculate the average of.</param>
    /// <param name="selector">A function to transform each element of the source sequence into a quantity.</param>
    /// <returns>The average of the projected quantities in the unit of the first element in the sequence.</returns>
    /// <exception cref="ArgumentNullException">Thrown when the sequence is null.</exception>
    /// <exception cref="InvalidOperationException">Thrown when the sequence is empty.</exception>
    public static TQuantity Average<TSource, TQuantity>(this IEnumerable<TSource> source, Func<TSource, TQuantity> selector)
        where TQuantity : ILinearQuantity<TQuantity>
    {
        return source.Select(selector).Average();
    }

    /// <summary>
    ///     Calculates the average of a sequence of linear quantities, such as Mass and Length.
    /// </summary>
    /// <typeparam name="TQuantity">The type of the linear quantity.</typeparam>
    /// <typeparam name="TUnit">The unit type of the linear quantity.</typeparam>
    /// <param name="quantities">The sequence of linear quantities to average.</param>
    /// <param name="targetUnit">The unit in which to express the average.</param>
    /// <returns>The average of the linear quantities in the specified unit.</returns>
    /// <exception cref="InvalidOperationException">Thrown when the sequence is empty.</exception>
    /// <remarks>
    ///     This method is slightly more performant than the alternative
    ///     <see cref="Average{TQuantity}(IEnumerable{TQuantity})" />
    ///     when most of the quantities in the sequence are expected to be in the target unit.
    /// </remarks>
    public static TQuantity Average<TQuantity, TUnit>(this IEnumerable<TQuantity> quantities, TUnit targetUnit)
        where TQuantity : ILinearQuantity<TQuantity>, IQuantity<TQuantity, TUnit>
        where TUnit : struct, Enum
    {
        return quantities.ArithmeticMean(targetUnit);
    }

    /// <summary>
    ///     Computes the average of the sequence of <typeparamref name="TQuantity" /> values, such as Mass and Length, that are
    ///     obtained by invoking a transform function on each element of the input sequence.
    /// </summary>
    /// <typeparam name="TSource">The type of the elements of the source sequence.</typeparam>
    /// <typeparam name="TQuantity">The type of the quantity elements in the source sequence.</typeparam>
    /// <typeparam name="TUnit">The type of the unit of the quantities.</typeparam>
    /// <param name="source">A sequence of quantities to calculate the average of.</param>
    /// <param name="selector">A function to transform each element of the source sequence into a quantity.</param>
    /// <param name="targetUnit">The desired unit type for the resulting quantity.</param>
    /// <returns>The average of the projected quantities in the specified unit.</returns>
    /// <exception cref="InvalidOperationException">Thrown when the sequence is empty.</exception>
    public static TQuantity Average<TSource, TQuantity, TUnit>(this IEnumerable<TSource> source, Func<TSource, TQuantity> selector, TUnit targetUnit)
        where TQuantity : ILinearQuantity<TQuantity>, IQuantity<TQuantity, TUnit>
        where TUnit : struct, Enum
    {
        return source.Select(selector).Average(targetUnit);
    }

All of these extensions take precedence over the extensions in the GenericMathExtensions (and they are also faster, as they just sum up the Value instead of invoking the + operator on the TQuantity).

The other extensions would apply for "custom quantities that implement the IAdditiveIdentity and IAdditiveOperators but don't explicitly implement our ILinearQuantity interface"- think "very custom" quantities 😄

PS They also replace the same extensions from the UnitMath - which should become mostly empty of extensions (see Clamp and the Min / Max - which are not really extensions, and ideally should be moved to the Quantity class, deprecating the old file completely).

@lipchev
Copy link
Collaborator Author

lipchev commented Dec 16, 2024

  1. LogarithmicQuantityExtensions:

I've also got the rest of the overloads but in order to reduce the clutter, I'm going to just focus on the main ones:

    /// <summary>
    ///     Sums a sequence of logarithmic quantities, such as PowerRatio and AmplitudeRatio.
    /// </summary>
    /// <typeparam name="TQuantity">The type of the logarithmic quantity.</typeparam>
    /// <param name="quantities">The sequence of logarithmic quantities to sum.</param>
    /// <param name="significantDigits">The number of significant digits to retain in the result. Default is 15.</param>
    /// <returns>The sum of the logarithmic quantities in the unit of the first element in the sequence.</returns>
    /// <exception cref="InvalidOperationException">Thrown when the sequence is empty.</exception>
    /// <remarks>
    ///     When the sequence is not empty, each quantity is converted to linear space (in the unit of the first element),
    ///     summed, and then the result is converted back to logarithmic space using the same unit.
    /// </remarks>
    public static TQuantity Sum<TQuantity>(this IEnumerable<TQuantity> quantities, int significantDigits = 15)
        where TQuantity : ILogarithmicQuantity<TQuantity>

Notice that we're throwing an InvalidOperationException when the collection is empty- as there isn't a valid AdditiveIdentity that we could return.

Another thing to pay attention to is the use of the significantDigits parameter- since the additions need to be performed in linear space there is no way around the possible rounding errors involved with the Pow (non integer) and the Log functions. I'm very strict about it- every operation that is not exact has this parameter (there aren't many)..

Frankly, I wasn't even sure if it makes sense to have this one - but the AI had some convincing arguments (which I no longer recall) - so I kept it.

    /// <summary>
    ///     Computes the arithmetic mean of a sequence of logarithmic quantities, such as PowerRatio and AmplitudeRatio.
    /// </summary>
    /// <typeparam name="TQuantity">The type of the logarithmic quantity.</typeparam>
    /// <param name="quantities">The sequence of logarithmic quantities to average.</param>
    /// <param name="significantDigits">The number of significant digits to retain in the result. Default is 15.</param>
    /// <returns>The average of the logarithmic quantities in the unit of the first element in the sequence.</returns>
    /// <exception cref="InvalidOperationException">Thrown when the sequence is empty.</exception>
    /// <remarks>
    ///     When the sequence is not empty, each quantity is converted to linear space (in the unit of the first element),
    ///     averaged, and then the result is converted back to logarithmic space using the same unit.
    /// </remarks>
    public static TQuantity ArithmeticMean<TQuantity>(this IEnumerable<TQuantity> quantities, int significantDigits = 15)
        where TQuantity : ILogarithmicQuantity<TQuantity>
    /// <summary>
    ///     Computes the geometric mean of a sequence of logarithmic quantities, such as PowerRatio and AmplitudeRatio.
    /// </summary>
    /// <typeparam name="TQuantity">The type of the logarithmic quantity.</typeparam>
    /// <param name="quantities">The sequence of logarithmic quantities to average.</param>
    /// <param name="accuracy">The number of decimal places of accuracy for the square root calculation. Default is 15.</param>
    /// <returns>The geometric mean of the logarithmic quantities in the unit of the first element in the sequence.</returns>
    /// <exception cref="InvalidOperationException">Thrown when the sequence is empty.</exception>
    /// <remarks>
    ///     When the sequence is not empty, calculates the n-th root of the product of the quantities, which for the
    ///     logarithmic quantities is equal to the sum the values, converted in unit of the first element.
    /// </remarks>
    public static TQuantity GeometricMean<TQuantity>(this IEnumerable<TQuantity> quantities, int accuracy = 15)
        where TQuantity : ILogarithmicQuantity<TQuantity>

Again, the AI insisted that both the arithmetic and the geometric means make sense in certain situations, but I cannot confirm or dispute that claim..

@lipchev
Copy link
Collaborator Author

lipchev commented Dec 16, 2024

  1. The AffineQuantityExtensions only has the Average for the Temperature - I'm not sure if the operation is applicable to the all affine quantities (whoever comes up with another Affine Quantity would have to tell us):
    /// <summary>
    ///     Calculates the average of a collection of <see cref="Temperature" /> values.
    /// </summary>
    /// <param name="temperatures">The collection of <see cref="Temperature" /> values to average.</param>
    /// <returns>The average <see cref="Temperature" />, in the unit of the first element in the sequence.</returns>
    /// <exception cref="ArgumentNullException">Thrown if the <paramref name="temperatures" /> collection is null.</exception>
    /// <exception cref="InvalidOperationException">Thrown if the <paramref name="temperatures" /> collection is empty.</exception>
    public static Temperature Average(this IEnumerable<Temperature> temperatures)
    {
        return temperatures.ArithmeticMean();
    }

    /// <summary>
    ///     Calculates the average of a collection of <see cref="Temperature" /> values.
    /// </summary>
    /// <param name="temperatures">The collection of <see cref="Temperature" /> values to average.</param>
    /// <param name="unit">The unit in which to express the average.</param>
    /// <returns>The average <see cref="Temperature" />, in the specified unit.</returns>
    /// <exception cref="ArgumentNullException">Thrown if the <paramref name="temperatures" /> collection is null.</exception>
    /// <exception cref="InvalidOperationException">Thrown if the <paramref name="temperatures" /> collection is empty.</exception>
    /// <remarks>
    ///     This method is slightly more performant than the alternative
    ///     <see cref="Average(System.Collections.Generic.IEnumerable{UnitsNet.Temperature})" />
    ///     when most of the quantities in the collection are expected to be in the target unit.
    /// </remarks>
    public static Temperature Average(this IEnumerable<Temperature> temperatures, TemperatureUnit unit)
    {
        return temperatures.ArithmeticMean(unit);
    }

@lipchev
Copy link
Collaborator Author

lipchev commented Dec 16, 2024

Ok lastly, I'm going to try to be short w.r.t. the "Equals with tolerance" - as we know, this is currently the recommended way of comparing all quantities due to the possible rounding error incurred by the unit conversions etc.
Although this particular issue is no longer present in my Fraction-based implementation, I think the method is still somewhat useful provided we:

  • deprecate the public bool Equals(Temperature other, Temperature tolerance) method, replacing the type of the tolerance with a TemperatureDelta.
  • move the Equals (with the correct tolerance type) out of the interface altogether (same argument as in Remove IComparisonOperators<TSelf, TSelf, bool> and IParsable<TSelf> from the IQuantity interface #1454) and turn it into an extension method in one of the previously mentioned extension classes (LinearQuantityExtensions, LogarithmicQuantityExtensions or the AffineQuantityExtensions). Here is what I've got down for the ILinearQuantity:
    /// <inheritdoc cref="EqualsAbsolute{TQuantity,TOther,TTolerance}" />
    public static bool Equals<TQuantity, TOther, TTolerance>(this TQuantity quantity, TOther? other, TTolerance tolerance)
        where TQuantity : ILinearQuantity<TQuantity>
        where TOther : IQuantityInstance<TQuantity>
        where TTolerance : IQuantityInstance<TQuantity>
    {
        return other != null && quantity.EqualsAbsolute(other, tolerance);
    }

    /// <inheritdoc cref="EqualsAbsolute{TQuantity,TOther,TTolerance}" />
    /// <exception cref="ArgumentException">
    ///     Thrown when the <paramref name="tolerance" /> is not of the same type as the
    ///     <paramref name="quantity" />.
    /// </exception>
    public static bool Equals<TQuantity>(this TQuantity quantity, IQuantity? other, IQuantity tolerance)
        where TQuantity : ILinearQuantity<TQuantity>

Ok, so I'm going to stop here for now, if you agree with the overall picture so far- I can create a separate issue regarding the modifications to the Equals (with tolerance) part of the interfaces.. (or of course I can keep going here if you prefer 😆 )..

@angularsen
Copy link
Owner

A lot to unwrap here.

  1. Name ILinearQuantity instead of IVectorQuantity - OK

  2. LogarithmicScalingFactor, must we not be backwards compatible with netstandard2.0? Meaning, if we need instance member there, we also need it for net7? I think it makes sense to push this up to QuantityInfo, which is shared information for all quantities that don't need to follow each quantity instance/value. We could maybe introduce a derived LogarithmicQuantityInfo class that extends QuantityInfo, we might not need to sprinkle yet more interfaces over this.

  3. IAffineQuantity, looks good

  4. Performance improvement of Sum/Average by passing in a unit that the quantities mostly have, sounds good. Feels a bit niche, but probably helps in some cases.

  5. The other extensions would apply for "custom quantities that implement the IAdditiveIdentity and IAdditiveOperators but don't explicitly implement our ILinearQuantity interface"- think "very custom" quantities

    I don't think we should cater to this use case, unless it costs us nothing to add and maintain this. I have no problem requiring custom quantities to implement either ILinearQuantity or ILogarithmicQuantity to benefit from our generic math, and avoid pitfalls like Temperature with generic math.

  6. significantDigits - I don't quite understand this one. I get there will be rounding errors, but why do we require/need to specify the significant digits to retain, can't we just retain the max and not perform any rounding?

  7. Frankly, I wasn't even sure if it makes sense to have this one - but the AI had some convincing arguments (which I no longer recall) - so I kept it.
    Again, the AI insisted that both the arithmetic and the geometric means make sense in certain situations, but I cannot confirm or dispute that claim

    This is a bit of red flag for me, okay orange. I think it's very important that we only add stuff that will actually be used, and ideally will be used by the implementer so we have a good chance of ensuring it is correct and makes sense in whatever domain. I really want to avoid adding stuff just because we can or hoping someone might find it useful. Similar to the IConvertible discussion earlier, and the initial generic math interface implementation although that was intentionally experimental and for exploration. If you are unsure about whether we should add something, then I think we should generally favor not doing it.

  8. Temperature equals changes - agreed

  9. The AffineQuantityExtensions only has the Average for the Temperature

    Not sure about this one, can't we do it generically with IAffineQuantity without limiting to just Temperature?

That's all I got time for right now. Generally though, I think the changes look good 👍

@angularsen
Copy link
Owner

Better to continue discussion for each change's pull request, instead of in a mega thread.

@lipchev
Copy link
Collaborator Author

lipchev commented Dec 21, 2024

A lot to unwrap here.

2. `LogarithmicScalingFactor`, must we not be backwards compatible with netstandard2.0? Meaning, if we need instance member there, we also need it for net7? I think it makes sense to push this up to `QuantityInfo`, which is shared information for all quantities that don't need to follow each quantity instance/value. We could maybe introduce a derived `LogarithmicQuantityInfo` class that extends `QuantityInfo`, we might not need to sprinkle yet more interfaces over this.

The downside here is that the LogarithmicQuantityInfo would have to be an ILogarithmicQuantityInfo - as we cannot extend the base class QuantityInfo<Unit> (I don't remember the exact reason anymore but I'm pretty sure it couldn't be a class).

3. `IAffineQuantity`, looks good

4. Performance improvement of Sum/Average by passing in a unit that the quantities mostly have, sounds good. Feels a bit niche, but probably helps in some cases.

That's just what the comment says- in reality I'm keeping it mostly in order to avoid the breaking change.

5. > The other extensions would apply for "custom quantities that implement the IAdditiveIdentity and IAdditiveOperators but don't explicitly implement our ILinearQuantity interface"- think "very custom" quantities
   I don't think we should cater to this use case, unless it costs us nothing to add and maintain this. I have no problem requiring custom quantities to implement either `ILinearQuantity` or `ILogarithmicQuantity` to benefit from our generic math, and avoid pitfalls like Temperature with generic math.

I think we should keep it for now, even if only to avoid the breaking change (for the extended quantities).

6. `significantDigits` - I don't quite understand this one. I get there will be rounding errors, but why do we require/need to specify the significant digits to retain, can't we just retain the max and not perform any rounding?

The default is 15 significant digits - which is the safe maximum precision that avoids the rounding error (when converting a double to QuantityValue). If you specify a lower number of significant digits (e.g. 4), the performance is better (depends on the numbers of course).

7. > Frankly, I wasn't even sure if it makes sense to have this one - but the AI had some convincing arguments (which I no longer recall) - so I kept it.
   > Again, the AI insisted that both the arithmetic and the geometric means make sense in certain situations, but I cannot confirm or dispute that claim
   
   
   This is a bit of red flag for me, okay orange. I think it's very important that we only add stuff that will actually be used, and ideally will be used by the implementer so we have a good chance of ensuring it is correct and makes sense in whatever domain. I really want to avoid adding stuff just because we can or hoping someone might find it useful. Similar to the `IConvertible` discussion earlier, and the initial generic math interface implementation although that was intentionally experimental and for exploration. If you are unsure about whether we should add something, then I think we should generally favor not doing it.

I'm pretty sure that the calculations for the LogarithmicSum, ArithmeticMean / GeometricMean are correct - I'm mostly not (how / if) they would be useful. I initially thought of just "not breaking" the existing usages, but then wasn't sure which average should be used by default.
In terms of code- it's not insignificant (I just finished covering the tests for the RootN ) and frankly some of it is probably not that fast- I don't care much about the LogQuantities myself, I just figured if anyone does use such extensions, they might as well use the more optimized version..

Technically there is also the possibility to calculate the RootN with 500 digits of precision but that's probably a more extreme scenario

8. Temperature equals changes - agreed

9. > The AffineQuantityExtensions only has the Average for the Temperature
   
   
   Not sure about this one, can't we do it generically with `IAffineQuantity` without limiting to just Temperature?

Technically we can but:

  • the Sum<TAffineQuantity, TOffset> would require 2 parameters (e.g. 'affineQuantities.Average<MyAffineQuantity, MyAffineOffset>()`)
  • I'm not sure that the operator makes sense for other types of affine quantities

That's all I got time for right now. Generally though, I think the changes look good 👍

Note that unfortunately I won't be able to bring these changes in without introducing the Unsafe and all the rest of it..

What do you think about the Equals with tolerance- do you agree about turning it into an extension method (removing it from the interface)? Should I open another issue / discussion about it?

@lipchev
Copy link
Collaborator Author

lipchev commented Dec 21, 2024

Note that unfortunately I won't be able to bring these changes in without introducing the Unsafe and all the rest of it..

Ok, that's not 100% accurate - I think I could introduce them with as a standalone PR, however the standard extensions (without the Unit) are going to be much slower than before (we'd have to be using the untyped As(Enum)).

@angularsen
Copy link
Owner

Equals as extension method is OK I think, as long is it's in the UnitsNet namespace and easily discoverable.

Regarding Unsafe, I don't recall the details there but this was a separate nuget and primarily a performance benefit? Let's discuss that more in a PR where we have some context.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants