From e297fa8495c2f6f2a7b05ad5eccf0b8255893472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=A4gi?= Date: Thu, 12 Feb 2026 20:16:57 +0100 Subject: [PATCH 01/72] Create AI enabled workspace --- .github/copilot-instructions.md | 448 ++++++++++++++++++++++++++++++++ .gitignore | 1 - Quantities.code-workspace | 15 ++ 3 files changed, 463 insertions(+), 1 deletion(-) create mode 100644 .github/copilot-instructions.md create mode 100644 Quantities.code-workspace diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..4349585b --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,448 @@ +# GitHub Copilot Instructions + +**When generating, editing, or suggesting code for this project, strictly adhere to the following coding conventions and style guidelines.** + +--- + +# Code Style Guidelines + +This document describes the coding conventions and style guidelines for the Atmoos.Quantities project. + +## Language and Framework + +- **Language**: C# (using latest language features including extensions, file-scoped types) +- **Framework**: .NET (modern versions) +- **Style**: Explicit, strongly-typed, performance-conscious + +## File Organization + +### Namespace Declarations + +Use **file-scoped namespaces** without braces: + +```csharp +namespace Atmoos.Quantities.Units.Si.Metric; + +public readonly struct Metre : ISiUnit, ILength +{ + // Implementation +} +``` + +### Using Directives + +- Place `using` directives at the top of the file +- Use global usings in `Globals.cs` for commonly used namespaces: + ```csharp + global using Atmoos.Quantities.Core; + ``` +- Static imports are acceptable when they improve readability: + ```csharp + using static Atmoos.Quantities.Extensions; + ``` + +## Type System + +### Type Aliases + +Always use **explicit .NET type aliases** (not C# keywords): + +✅ **Correct**: +```csharp +Double value; +Int32 count; +String name; +Boolean flag; +``` + +❌ **Incorrect**: +```csharp +double value; +int count; +string name; +bool flag; +``` + +### Struct vs Class vs Record + +- Use `readonly struct` for value types and lightweight data structures +- Use `record struct` when equality semantics and deconstruction are needed +- Use `sealed class` for reference types that shouldn't be inherited +- Use `interface` extensively for contracts and marker interfaces + +### Generic Constraints + +Apply comprehensive generic constraints with clear `where` clauses: + +```csharp +public static ref readonly Scalar Si() + where TPrefix : IMetricPrefix + where TUnit : ISiUnit, IDimension => ref Cache>.Value; +``` + +## Method Parameters + +### `in` Modifier + +Use the `in` modifier extensively for passing structs by reference: + +```csharp +public static Length Of(in Double value, in Scalar measure) + where TLength : ILength, IUnit => new(measure.Create(in value)); +``` + +### `ref readonly` Returns + +Use `ref readonly` for returning references without allowing mutation: + +```csharp +public static ref readonly Scalar Si() + where TUnit : ISiUnit, IDimension => ref Cache>.Value; +``` + +## Naming Conventions + +### Fields + +- Private fields use `this.` prefix explicitly: + ```csharp + private readonly Double nominator, denominator, offset; + + public Polynomial() => (this.nominator, this.denominator, this.offset) = (1, 1, 0); + ``` + +### Methods and Properties + +- Use PascalCase for public members +- Use camelCase for parameters and local variables +- Use descriptive names that convey intent + +### Factory Methods + +Use static factory methods with the pattern `Of()`: + +```csharp +public static Length Of(in Double value, in Scalar measure) + where TLength : ILength, IUnit => new(measure.Create(in value)); +``` + +### Constants + +Use `static readonly` fields for constants: + +```csharp +internal static String RoundTripFormat = "G17"; +``` + +## Code Structure + +### File-Scoped Types + +Use the `file` keyword for types that should only be visible within the file: + +```csharp +file static class Cache + where T : ITransform +{ + public static readonly Polynomial Polynomial = Polynomial.Of(T.ToSi(new Transformation())); +} +``` + +### Extension Methods + +Use the modern `extension()` syntax: + +```csharp +extension(TQuantity quantity) + where TQuantity : struct, IQuantity, IDimension +{ + internal static TQuantity Create(in Quantity value) => TQuantity.Create(in value); + + public void Serialize(IWriter writer) => quantity.Value.Write(writer, typeof(TQuantity).Name.ToLowerInvariant()); +} +``` + +For operators: + +```csharp +extension(TQuantity) + where TQuantity : struct, IQuantity, IDimension +{ + public static Boolean operator ==(in TQuantity left, in TQuantity right) => left.Equals(right); + public static TQuantity operator +(in TQuantity left, in TQuantity right) => TQuantity.Create(left.Value + right.Value); +} +``` + +### Access Modifiers + +- Use `public` for API surface +- Use `internal` for implementation details that cross file boundaries +- Use `private` for encapsulated implementation +- Always specify access modifiers explicitly + +## Operators + +### Operator Overloading + +Overload operators consistently for quantity types: + +```csharp +public static Boolean operator ==(in TQuantity left, in TQuantity right) => left.Equals(right); +public static Boolean operator !=(in TQuantity left, in TQuantity right) => !left.Equals(right); +public static TQuantity operator +(in TQuantity left, in TQuantity right) => TQuantity.Create(left.Value + right.Value); +public static TQuantity operator *(Double scalar, in TQuantity right) => TQuantity.Create(scalar * right.Value); +``` + +### Numeric Operators + +Implement standard .NET numeric operator interfaces: + +```csharp +public sealed class Transformation + : IAdditionOperators, + ISubtractionOperators, + IMultiplyOperators, + IDivisionOperators +``` + +## Interfaces + +### Marker Interfaces + +Use marker interfaces for categorization: + +```csharp +public interface IUnit : IRepresentable; // marker interface +``` + +### Interface Implementation + +Implement interfaces explicitly when needed: + +```csharp +internal Quantity Value => this.length; +Quantity IQuantity.Value => this.length; + +static Length IFactory.Create(in Quantity value) => new(in value); +``` + +## Comments and Documentation + +### Inline Comments + +- Use comments to explain **why**, not **what** +- Reference external sources for physical constants and formulas: + ```csharp + // See: https://en.wikipedia.org/wiki/Foot_(unit) + public readonly struct Foot : IImperialUnit, ILength + ``` + +### Format Strings + +Document format string choices: + +```csharp +internal static String RoundTripFormat = "G17"; // https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings#RFormatString +``` + +### TODOs + +Use `ToDo:` prefix for future work: + +```csharp +/* ToDo: re-introduce when C# supports roles. +, IComparisonOperators +, IEqualityOperators */ +``` + +## Testing + +### Test Structure + +- Use xUnit framework +- One test class per production class (e.g., `ExtensionsTest` for `Extensions`) +- Use `Fact` for single tests, `Theory` with `MemberData` for parameterized tests + +### Test Naming + +Use descriptive method names that explain what is being tested: + +```csharp +[Fact] +public void DeconstructionOfScalarQuantities() + +[Theory] +[MemberData(nameof(Velocities))] +public void DeconstructionOfCompoundQuotientQuantities(Velocity velocity, String expectedUnit) +``` + +### Test Data + +Use `TheoryData` for type-safe test data: + +```csharp +public static TheoryData Velocities() + => new() { + { Velocity.Of(E, Si().Per(Si())), "m/s" }, + { Velocity.Of(PI, Si().Per(Metric())), "km/h" } + }; +``` + +### Test Constants + +Use static readonly fields for test data: + +```csharp +private static readonly Length length = Length.Of(23, Si()); +private static readonly Length epsilon = Length.Of(2, Si()); +``` + +## Expression and Statement Style + +### Expression-Bodied Members + +Use expression-bodied members for simple implementations: + +```csharp +internal Quantity Value => this.length; +public override Int32 GetHashCode() => this.length.GetHashCode(); +public override String ToString() => this.length.ToString(); +``` + +### Tuple Deconstruction + +Support and use tuple deconstruction: + +```csharp +public void Deconstruct(out Double value, out String unit) => (value, unit) = (this.value, this.measure.ToString() ?? String.Empty); + +// Usage: +(Double value, String unit) = length; +``` + +### Pattern Matching + +Use modern pattern matching: + +```csharp +var offset = o switch { + < 0d => $" - {-o:g4}", + > 0d => $" + {o:g4}", + _ => String.Empty +}; +``` + +### Tuple Patterns + +Use tuple patterns in switch expressions: + +```csharp +var fraction = (n, d) switch { + (1, 1) => "x", + (1, _) => $"x/{d:g4}", + (-1, 1) => "-x", + _ => $"{n:g4}*x/{d:g4}" +}; +``` + +## Formatting + +### Indentation + +- Use **4 spaces** (not tabs) +- Brace-less namespaces (file-scoped) +- Opening braces on same line for methods when using expression bodies +- New line for opening braces on types and control flow + +### Line Length + +- Keep lines reasonably short +- Break long method signatures across multiple lines with proper indentation + +### Spacing + +- Space after keywords: `if (`, `for (`, `while (` +- No space between method name and parentheses: `ToString()` +- Space around operators: `a + b`, `x == y` + +## Performance Considerations + +### Struct Passing + +Use `in` parameters to avoid unnecessary struct copies: + +```csharp +public static Boolean operator ==(in TQuantity left, in TQuantity right) => left.Equals(right); +``` + +### Caching + +Use static caching patterns for computed values: + +```csharp +file static class Cache + where TMeasure : IMeasure + where TUnit : IUnit, IDimension +{ + private static readonly Scalar scalar = new(in Factory.Of()); + public static ref readonly Scalar Value => ref scalar; +} +``` + +### Boxing Avoidance + +Use generic constraints to avoid boxing: + +```csharp +where TQuantity : struct, IQuantity, IDimension +``` + +## Error Handling + +### Exception Factory Methods + +Use helper methods for creating exceptions: + +```csharp +public static NotImplementedException NotImplemented([CallerMemberName] String memberName = "", [CallerLineNumber] Int32 line = 0) +{ + return NotImplemented(typeof(T), memberName, line); +} +``` + +### Caller Information + +Use `CallerMemberName` and `CallerLineNumber` attributes for debugging: + +```csharp +public static NotImplementedException NotImplemented(Object self, [CallerMemberName] String memberName = "", [CallerLineNumber] Int32 line = 0) +``` + +## Culture and Formatting + +### Invariant Culture + +Use `CultureInfo.InvariantCulture` for formatting when locale-independence is required: + +```csharp +public static String ToString(this IFormattable formattable, String format) + => formattable.ToString(format, CultureInfo.InvariantCulture); +``` + +### Number Formatting + +Use standard format strings with explicit precision: + +```csharp +public override String ToString() => ToString("g5", CultureInfo.CurrentCulture); +``` + +## Summary + +This code style emphasizes: +- **Explicitness**: Explicit types, explicit modifiers, explicit intent +- **Performance**: By-reference passing, caching, struct optimization +- **Type Safety**: Strong generic constraints, marker interfaces +- **Immutability**: Readonly structs, readonly fields, in parameters +- **Modern C#**: Latest language features including extensions, file-scoped types, pattern matching +- **Consistency**: Uniform naming, formatting, and structural patterns diff --git a/.gitignore b/.gitignore index e1aa82aa..3a723589 100644 --- a/.gitignore +++ b/.gitignore @@ -63,4 +63,3 @@ publish/ # VsCode .vscode/ -*.code-workspace diff --git a/Quantities.code-workspace b/Quantities.code-workspace new file mode 100644 index 00000000..1cd909dc --- /dev/null +++ b/Quantities.code-workspace @@ -0,0 +1,15 @@ +{ + "folders": [ + { + "path": ".github" + }, + { + "path": "source" + } + ], + "settings": { + "chat.tools.terminal.autoApprove": { + "dotnet test": true + } + } +} From 962eea97b2a1634816ae719181c458f0ef016f2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=A4gi?= Date: Thu, 12 Feb 2026 20:25:48 +0100 Subject: [PATCH 02/72] Add numerical stability ai probes. --- .github/copilot-instructions.md | 12 + .../Numerics/NumericalStabilityProbe.ai.cs | 211 +++++++++++++++ .../Numerics/NumericalStabilityTest.ai.cs | 253 ++++++++++++++++++ 3 files changed, 476 insertions(+) create mode 100644 source/Atmoos.Quantities.Test/Numerics/NumericalStabilityProbe.ai.cs create mode 100644 source/Atmoos.Quantities.Test/Numerics/NumericalStabilityTest.ai.cs diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4349585b..020812fa 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -16,6 +16,18 @@ This document describes the coding conventions and style guidelines for the Atmo ## File Organization +### AI-Generated Files + +Files that are **completely AI-generated** should use the `.ai.` infix in their filename to clearly indicate their origin: + +**Pattern**: `{name}.ai.{extension}` + +**Examples**: +- `NumericalStabilityProbe.ai.cs` - AI-generated C# test file +- `Documentation.ai.md` - AI-generated markdown documentation + +This convention makes it immediately obvious which files were created entirely by AI tooling (like GitHub Copilot) versus human-authored code. Use this pattern consistently for full-file AI generations. + ### Namespace Declarations Use **file-scoped namespaces** without braces: diff --git a/source/Atmoos.Quantities.Test/Numerics/NumericalStabilityProbe.ai.cs b/source/Atmoos.Quantities.Test/Numerics/NumericalStabilityProbe.ai.cs new file mode 100644 index 00000000..7de0355c --- /dev/null +++ b/source/Atmoos.Quantities.Test/Numerics/NumericalStabilityProbe.ai.cs @@ -0,0 +1,211 @@ +using Atmoos.Quantities.Core.Numerics; +using Xunit.Abstractions; +using static System.Math; +using static Atmoos.Quantities.Test.Convenience; +using static Atmoos.Quantities.TestTools.Convenience; + +namespace Atmoos.Quantities.Test.Numerics; + +// Counterexamples: scenarios where naive double arithmetic is more +// numerically stable than the library's Polynomial layer. +// +// Root cause: Polynomial.Pow(N) with nonzero offset composes the transform +// N times symbolically. When the scale factor is > 1, each composition +// multiplies the offset by the scale, causing it to grow exponentially. +// A 64-bit double storing that huge offset loses relative precision in +// its fractional digits. When the inverse Pow(-N) is then composed, the +// offsets must cancel — but they've already been rounded, leaving a +// residual. Sequential step-by-step evaluation avoids this because the +// offset stays small (just the constant 'b') at every step. +public sealed class NumericalStabilityProbe(ITestOutputHelper output) +{ + // Counterexample 1: Upscaling power with exact integer coefficients. + // + // p(x) = 10x + 100. Sequential evaluation: each step is + // 10*x + 100 (exact for any x < 2^50) and (x-100)/10 (one rounding). + // Polynomial: Pow(N) builds offset ≈ 100*(10^N - 1)/9, which at N=15 + // is ~1.11e16 — so large that its fractional part vanishes in a Double. + // When Pow(-15) is composed, the offset residual is ~0.06, i.e. ~2% + // of the input value 1.5, which is a catastrophic error. + [Theory] + [InlineData(5, 6.5370e-11)] + [InlineData(10, 4.8668e-06)] + [InlineData(15, 6e-02)] + public void UpscalingPowerWithIntegerCoefficients(Int32 exponent, Double polyErrorBound) + { + const Double value = 1.5; + const Double n = 10d, d = 1d, offset = 100d; + + // Sequential: apply forward N times, then inverse N times. + Double sequential = value; + for (Int32 i = 0; i < exponent; i++) + sequential = n * sequential + offset; + for (Int32 i = 0; i < exponent; i++) + sequential = (sequential - offset) / n; + Double naiveError = Abs(sequential - value); + + // Polynomial: compose p^N * p^{-N}, evaluate once. + var poly = Poly(n, d, offset); + var roundTrip = poly.Pow(exponent) * poly.Pow(-exponent); + Double polyResult = roundTrip * value; + Double polyError = Abs(polyResult - value); + + output.WriteLine( + $"exp={exponent}: naive={naiveError:e4}, poly={polyError:e4}, " + + $"ratio={SafeRatio(polyError, naiveError):f0}x"); + + // Sequential achieves EXACT results (integer arithmetic is exact). + Assert.Equal(0d, naiveError); + // Polynomial has non-trivial error that grows with exponent. + Assert.True(polyError <= polyErrorBound, + $"exp={exponent}: poly error {polyError:e4} exceeds bound {polyErrorBound:e4}"); + Assert.True(polyError > naiveError, + $"exp={exponent}: expected polynomial to be less precise than naive"); + } + + // Counterexample 2: Upscaling power with non-integer ratio, large offset. + // + // p(x) = (7/3)x + 100 (scale > 1). After composing p^7, the offset + // grows to ~37,000. The offset rounding residual after composing with + // p^{-7} causes measurable error. Sequential evaluation keeps the + // per-step offset at a constant 100, accumulating only ~7 ULP. + [Theory] + [InlineData(5, 100)] + [InlineData(7, 100)] + [InlineData(5, 1000)] + [InlineData(7, 1000)] + public void UpscalingPowerWithFractionalRatioAndLargeOffset(Int32 exponent, Double offset) + { + const Double value = PI; + const Double n = 7d, d = 3d; + + Double sequential = value; + for (Int32 i = 0; i < exponent; i++) + sequential = n / d * sequential + offset; + for (Int32 i = 0; i < exponent; i++) + sequential = d / n * (sequential - offset); + Double naiveError = Abs(sequential - value); + + var poly = Poly(n, d, offset); + var roundTrip = poly.Pow(exponent) * poly.Pow(-exponent); + Double polyResult = roundTrip * value; + Double polyError = Abs(polyResult - value); + + output.WriteLine( + $"exp={exponent},o={offset}: naive={naiveError:e4}, poly={polyError:e4}, " + + $"ratio={SafeRatio(polyError, naiveError):f0}x"); + + Assert.True(polyError > naiveError, + $"exp={exponent},o={offset}: expected polynomial to be less precise"); + } + + // Counterexample 3: Downscaling power with offset. + // + // Even with scale < 1 (n/d = 3/2), a large offset still causes the + // composed polynomial to lose precision relative to sequential eval. + [Theory] + [InlineData(3, 50)] + [InlineData(5, 50)] + [InlineData(7, 50)] + [InlineData(5, 1000)] + [InlineData(7, 1000)] + public void DownscalingPowerWithOffset(Int32 exponent, Double offset) + { + const Double value = PI; + const Double n = 3d, d = 2d; + + Double sequential = value; + for (Int32 i = 0; i < exponent; i++) + sequential = n / d * sequential + offset; + for (Int32 i = 0; i < exponent; i++) + sequential = d / n * (sequential - offset); + Double naiveError = Abs(sequential - value); + + var poly = Poly(n, d, offset); + var roundTrip = poly.Pow(exponent) * poly.Pow(-exponent); + Double polyResult = roundTrip * value; + Double polyError = Abs(polyResult - value); + + output.WriteLine( + $"exp={exponent},o={offset}: naive={naiveError:e4}, poly={polyError:e4}, " + + $"ratio={SafeRatio(polyError, naiveError):f0}x"); + + Assert.True(polyError > naiveError, + $"exp={exponent},o={offset}: expected polynomial to be less precise"); + } + + // Counterexample 4: Inverse evaluation near the offset. + // + // The polynomial's inverse formula: d*(x - offset)/n. + // When x ≈ offset, the subtraction x - offset is catastrophic + // cancellation: most significant bits cancel, leaving only noise. + // A caller who has access to the small delta directly (i.e. knowing + // x = offset + delta) can compute d*delta/n without subtraction, + // getting zero error. + // + // This is a structural limitation: the Polynomial only receives x, + // not the decomposition x = offset + delta. + [Theory] + [InlineData(1e-10)] + [InlineData(1e-12)] + [InlineData(1e-14)] + public void InverseEvaluationNearOffset(Double delta) + { + const Double n = 5d, d = 3d, offset = 1000d; + Double x = offset + delta; + Double expected = d * delta / n; + + // Polynomial inverse: must subtract x - offset, losing bits. + var poly = Poly(n, d, offset); + Double polyResult = poly / x; + Double polyError = Abs(polyResult - expected); + + // "Naive" with structural knowledge: compute from delta directly. + Double naiveResult = d * delta / n; + Double naiveError = Abs(naiveResult - expected); + + output.WriteLine( + $"delta={delta:e}: naive={naiveError:e4}, poly={polyError:e4}"); + + Assert.Equal(0d, naiveError); + Assert.True(polyError > naiveError, + $"delta={delta:e}: expected polynomial inverse to lose precision"); + } + + // Counterexample 5: Irrational scale factors. + // + // For p(x) = π*x, Simplify can't reduce (π, 1) via GCD since π is + // irrational. Composing Poly(π)^13 * Poly(1/π)^13 produces + // (π^13, π^13, 0). The Simplify method attempts to scale these to + // integers, but π^13 ≈ 2.7e6 is not an exact integer after scaling. + // The GCD path thus fails to reduce, and the final division π^13/π^13 + // may not cancel to exactly 1. Naive Math.Pow benefits from the same + // FP representation and sometimes lands on exactly 1. + [Theory] + [InlineData(3)] + [InlineData(13)] + public void IrrationalScaleFactorRoundTrip(Int32 repetitions) + { + const Double value = E; + + // Naive: π^n * (1/π)^n * value + Double naiveResult = Algorithms.Pow(PI, repetitions) + * Algorithms.Pow(1d / PI, repetitions) * value; + Double naiveError = Abs(naiveResult - value); + + // Polynomial: Poly(π)^n * Poly(1/π)^n + var scaleUp = Poly(nominator: PI); + var scaleDown = Poly(denominator: PI); + var roundTrip = scaleUp.Pow(repetitions) * scaleDown.Pow(repetitions); + Double polyResult = roundTrip * value; + Double polyError = Abs(polyResult - value); + + output.WriteLine( + $"reps={repetitions}: naive={naiveError:e4}, poly={polyError:e4}"); + + Assert.True(polyError >= naiveError, + $"reps={repetitions}: expected polynomial to be no better than naive"); + } + + private static Double SafeRatio(Double a, Double b) => b == 0d ? Double.PositiveInfinity : a / b; +} diff --git a/source/Atmoos.Quantities.Test/Numerics/NumericalStabilityTest.ai.cs b/source/Atmoos.Quantities.Test/Numerics/NumericalStabilityTest.ai.cs new file mode 100644 index 00000000..425a992b --- /dev/null +++ b/source/Atmoos.Quantities.Test/Numerics/NumericalStabilityTest.ai.cs @@ -0,0 +1,253 @@ +using Atmoos.Quantities.Core.Numerics; +using static System.Math; +using static Atmoos.Quantities.Test.Convenience; +using static Atmoos.Quantities.TestTools.Convenience; + +namespace Atmoos.Quantities.Test.Numerics; + +// These tests demonstrate that the library's Polynomial layer is more +// numerically stable than "just using doubles". Each test shows a scenario +// where naive double arithmetic accumulates error, while the Polynomial +// representation (rational fraction + FMA + GCD simplification) retains +// significantly more precision. +public class NumericalStabilityTest +{ + // Scenario 1: Chained scaling + // Multiplying and dividing by a large factor (e.g. 1e18) then comparing + // to the identity. Naive doubles lose precision because intermediate + // values are rounded after each operation. + [Fact] + public void ChainedScalingPreservesMorePrecisionThanNaiveDoubles() + { + const Double value = PI; + const Double scale = 1e18; + + // Naive: apply scale then undo it manually. + Double naiveResult = value * scale / scale; + Double naiveError = Abs(naiveResult - value); + + // Polynomial: compose p(x)=scale*x with its inverse p(x)=x/scale, + // then evaluate. The rational representation cancels exactly via GCD. + var scaleUp = Poly(nominator: scale); // p(x) = scale * x + var scaleDown = Poly(denominator: scale); // q(x) = x / scale + var roundTrip = scaleUp * scaleDown; // compose symbolically + Double polyResult = roundTrip * value; + Double polyError = Abs(polyResult - value); + + // The polynomial achieves strictly less (or equal) error. + Assert.True(polyError <= naiveError, + $"Polynomial error ({polyError:e}) should be <= naive error ({naiveError:e})"); + // And (for this case) the polynomial is actually exact. + Assert.Equal(value, polyResult); + } + + // Scenario 2: Deep composition chain + // Composing many affine transformations (with offsets) sequentially + // and then composing their inverses. With naive doubles, each + // composition step introduces rounding; the Polynomial layer defers + // evaluation and simplifies symbolically. + [Fact] + public void DeepCompositionChainIsMoreStableThanNaiveDoubles() + { + const Double value = E; + const Int32 depth = 12; + // An affine transform: f(x) = 13x/7 + 3 + const Double n = 13d, d = 7d, offset = 3d; + + // --- Naive double chain --- + Double naiveForward = value; + for (Int32 i = 0; i < depth; i++) { + naiveForward = n / d * naiveForward + offset; + } + Double naiveBackward = naiveForward; + for (Int32 i = 0; i < depth; i++) { + naiveBackward = d / n * (naiveBackward - offset); + } + Double naiveError = Abs(naiveBackward - value); + + // --- Polynomial chain --- + var poly = Poly(n, d, offset); + var forward = poly; + for (Int32 i = 1; i < depth; i++) { + forward = poly * forward; // symbolic composition + } + var inverse = forward; // apply forward... + for (Int32 i = 0; i < depth; i++) { + inverse = Polynomial.One / poly * inverse; // ...then undo + } + // The inverse chain ideally collapses back to identity. + Double polyResult = inverse * value; + Double polyError = Abs(polyResult - value); + + Assert.True(polyError <= naiveError, + $"Polynomial error ({polyError:e}) should be <= naive error ({naiveError:e})"); + } + + // Scenario 3: Power round-trip (p^n * p^-n == identity) + // Raising a polynomial to a high power and back should yield identity. + // With naive doubles, exponentiation amplifies rounding error. + [Theory] + [InlineData(3)] + [InlineData(5)] + [InlineData(7)] + [InlineData(11)] + public void PowerRoundTripIsMoreStableThanNaiveDoubles(Int32 exponent) + { + const Double value = Tau / E; + const Double n = 13d, d = 14d; + + // Naive: (n/d)^exp * (d/n)^exp * value + Double naiveScale = Pow(n / d, exponent); + Double naiveInverse = Pow(d / n, exponent); + Double naiveResult = naiveScale * naiveInverse * value; + Double naiveError = Abs(naiveResult - value); + + // Polynomial: symbolic Pow then compose. + var poly = Poly(n, d); + var power = poly.Pow(exponent); + var inversePower = poly.Pow(-exponent); + var roundTrip = power * inversePower; + Double polyResult = roundTrip * value; + Double polyError = Abs(polyResult - value); + + Assert.True(polyError <= naiveError, + $"exp={exponent}: Polynomial error ({polyError:e}) should be <= naive error ({naiveError:e})"); + } + + // Scenario 4: Chaining through many scale factors + // Multiplying and dividing by 1000 twenty times each using naive + // doubles accumulates rounding at every step (~40 operations). + // The Polynomial layer composes all steps symbolically and simplifies + // the result to an exact identity before evaluating — zero drift. + [Fact] + public void ChainingThroughManyScaleFactorsAccumulatesLessDriftThanNaiveDoubles() + { + const Double value = E; + const Int32 steps = 20; + const Double factor = 1000d; + + // Naive: multiply by 1000 twenty times, then divide by 1000 twenty times. + // Each step can introduce up to 0.5 ULP of rounding error. + Double naive = value; + for (Int32 i = 0; i < steps; i++) naive *= factor; + for (Int32 i = 0; i < steps; i++) naive /= factor; + Double naiveError = Abs(naive - value); + + // Polynomial: compose the scalings symbolically, then simplify and evaluate once. + // The composition yields (1000^20, 1000^20, 0), which simplifies to (1, 1, 0). + var up = Poly(nominator: factor); + var down = Poly(denominator: factor); + var composed = Polynomial.One; + for (Int32 i = 0; i < steps; i++) composed = up * composed; + for (Int32 i = 0; i < steps; i++) composed = down * composed; + composed = composed.Simplify(); + Double polyResult = composed * value; + Double polyError = Abs(polyResult - value); + + // The polynomial layer eliminates all drift; naive accumulates ~20 ULPs. + Assert.True(polyError <= naiveError, + $"Polynomial error ({polyError:e}) should be <= naive error ({naiveError:e})"); + Assert.Equal(0d, polyError); // exact identity! + } + + // Scenario 5: GCD simplification keeps coefficients small + // When composing polynomials, nominators and denominators grow + // multiplicatively. Simplification via GCD keeps them small, + // which preserves precision in subsequent operations. + [Fact] + public void GcdSimplificationKeepsCoefficientsMaintainable() + { + Double value = Sqrt(2d); + // Poly(12, 18) stores (12, 18)—the Poly helper does not auto-simplify. + // But Simplify() reduces it to (2, 3) via GCD(12, 18) = 6. + var poly = Poly(12, 18); + var simplified = poly.Simplify(); + var (n, d, _) = simplified; + Assert.Equal(2d, n); + Assert.Equal(3d, d); + + // Demonstrate that p^n * p^{-n} yields identity on the simplified form. + // The rational representation avoids the rounding that naive doubles incur. + const Int32 exp = 10; + var power = simplified.Pow(exp); + var inversePower = simplified.Pow(-exp); + var roundTrip = power * inversePower; + var (rn, rd, ro) = roundTrip.Simplify(); + + // Polynomial round-trip is exact: n=1, d=1, offset=0. + Assert.Equal(1d, rn); + Assert.Equal(1d, rd); + Assert.Equal(0d, ro); + + // Naive: ((2/3)^10) * ((3/2)^10) * value + Double naiveScale = Pow(2d / 3d, exp); + Double naiveInverse = Pow(3d / 2d, exp); + Double naiveResult = naiveScale * naiveInverse * value; + + // The polynomial is exact; naive accumulates floating-point drift. + Double polyResult = roundTrip * value; + Assert.Equal(value, polyResult); // exact + Assert.True(Abs(naiveResult - value) >= Abs(polyResult - value), + $"Polynomial should be at least as precise as naive doubles"); + } + + // Scenario 6: FusedMultiplyAdd vs. separate multiply-then-add + // FMA computes (a * b + c) with a single rounding step, while + // separate operations round twice. This matters for unit conversions + // that involve both scaling and offset (temperature conversions). + [Fact] + public void FmaIsMorePreciseThanSeparateMultiplyAndAdd() + { + // Large nominator/denominator with non-trivial offset, evaluated + // at a value that causes the product and offset to nearly cancel. + const Double n = 1_000_000_000_000_001d; + const Double d = 1_000_000_000_000_000d; + const Double offset = -1d; + const Double x = 1d; + // Exact: (n * x + d * offset) / d = (n - d) / d = 1e-15 + const Double expected = 1e-15; + + // Naive: n/d * x + offset = 1.000000000000001 - 1.0 + // This subtraction loses most significant digits. + Double naiveResult = (n / d) * x + offset; + Double naiveError = Abs(naiveResult - expected); + + // FMA-based Polynomial evaluation: FMA(n, x, d * offset) / d + // = FMA(1e15+1, 1, -1e15) / 1e15 — the FMA keeps full precision + // for the intermediate sum before the final division. + var poly = Poly(n, d, offset); + Double polyResult = poly * x; + Double polyError = Abs(polyResult - expected); + + Assert.True(polyError <= naiveError, + $"Polynomial error ({polyError:e}) should be <= naive error ({naiveError:e})"); + } + + // Scenario 7: Extreme prefix round-trips + // Converting from Quecto (1e-30) to base and back. Even though the + // mathematical result is exact, intermediate rounding in naive doubles + // loses precision at these extreme scales. + [Theory] + [InlineData(1e-30)] // Quecto + [InlineData(1e-24)] // Yocto + [InlineData(1e18)] // Exa + [InlineData(1e30)] // Quetta + public void ExtremePrefixRoundTripsAreStable(Double prefixFactor) + { + const Double value = PI; + + // Naive: multiply by factor, then divide by factor. + Double naiveResult = (value * prefixFactor) / prefixFactor; + Double naiveError = Abs(naiveResult - value); + + // Polynomial: compose scaling with its inverse symbolically. + var forward = Poly(nominator: prefixFactor); + var backward = Poly(denominator: prefixFactor); + var roundTrip = forward * backward; + Double polyResult = roundTrip * value; + Double polyError = Abs(polyResult - value); + + Assert.True(polyError <= naiveError, + $"factor={prefixFactor:e}: Polynomial error ({polyError:e}) should be <= naive error ({naiveError:e})"); + } +} From 4e6a63853d557e0f5f0fbd6f2bcf047113611181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=A4gi?= Date: Thu, 12 Feb 2026 20:50:10 +0100 Subject: [PATCH 03/72] Add first MCP server for MS docs --- .gitignore | 3 --- .vscode/launch.json | 26 +++++++++++++++++++++++++ .vscode/tasks.json | 41 +++++++++++++++++++++++++++++++++++++++ Quantities.code-workspace | 16 ++++++++++++++- 4 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json diff --git a/.gitignore b/.gitignore index 3a723589..ade6d94a 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,3 @@ publish/ # JetBrains Rider .idea/ *.sln.iml - -# VsCode -.vscode/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..f3e0c582 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/source/Quantities.Test/bin/Debug/net7.0/Quantities.Test.dll", + "args": [], + "cwd": "${workspaceFolder}/source/Quantities.Test", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..6f3346aa --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/source/Quantities.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/source/Quantities.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/source/Quantities.sln" + ], + "problemMatcher": "$msCompile" + } + ] +} diff --git a/Quantities.code-workspace b/Quantities.code-workspace index 1cd909dc..f8e3cea4 100644 --- a/Quantities.code-workspace +++ b/Quantities.code-workspace @@ -1,5 +1,8 @@ { "folders": [ + { + "path": ".vscode" + }, { "path": ".github" }, @@ -10,6 +13,17 @@ "settings": { "chat.tools.terminal.autoApprove": { "dotnet test": true + }, + "mcp": { + "servers": { + "microsoftdocs/mcp": { + "type": "http", + "url": "https://learn.microsoft.com/api/mcp", + "gallery": "https://api.mcp.github.com", + "version": "1.0.0" + } + }, + "inputs": [] } } -} +} \ No newline at end of file From c1ba039d40ca0da934763d5f4dda798fbd951030 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=A4gi?= Date: Tue, 17 Feb 2026 17:06:53 +0100 Subject: [PATCH 04/72] Copilot: Create agent to create quantities --- .github/prompts/new-quantity.prompt.md | 478 +++++++++++++++++++++++++ 1 file changed, 478 insertions(+) create mode 100644 .github/prompts/new-quantity.prompt.md diff --git a/.github/prompts/new-quantity.prompt.md b/.github/prompts/new-quantity.prompt.md new file mode 100644 index 00000000..fc0c5e49 --- /dev/null +++ b/.github/prompts/new-quantity.prompt.md @@ -0,0 +1,478 @@ +# New Quantity Generation Agent + +Generate a new physical quantity type for the Atmoos.Quantities library. + +## Instructions + +When asked to create a new quantity, follow these steps: + +1. **Identify the quantity category** (see below) based on its SI dimensional analysis. +2. **Create or verify the dimension interface** in the appropriate dimensions file. +3. **Create the quantity struct** in `source/Atmoos.Quantities/Quantities/`. +4. **Add cross-quantity operators** in the appropriate file under `source/Atmoos.Quantities/Physics/`. + +Always adhere to the coding conventions defined in `.github/copilot-instructions.md`. + +--- + +## Quantity Categories + +Every quantity falls into one of these categories, determined by its SI dimensional formula: + +### 1. Scalar (base quantity with a single dimension) + +**When**: The quantity is an SI base quantity or is independently defined with its own linear dimension. + +**Dimension**: `ILinear, IBaseQuantity` (for base) or `ILinear, IDerivedQuantity` (for derived). + +**Struct implements**: `IQuantity, IDimension, IScalar` + +**Examples**: `Length` (ILength), `Time` (ITime), `Mass` (IMass), `ElectricCurrent`, `Power` (IPower), `ElectricPotential`, `ElectricalResistance` + +**Template** (using `Length` as canonical example): + +```csharp +using Atmoos.Quantities.Creation; +using Atmoos.Quantities.Dimensions; +using Atmoos.Quantities.Units; + +namespace Atmoos.Quantities; + +public readonly struct {Name} : IQuantity<{Name}>, I{Name}, IScalar<{Name}, I{Name}> +{ + private readonly Quantity {fieldName}; + internal Quantity Value => this.{fieldName}; + Quantity IQuantity<{Name}>.Value => this.{fieldName}; + + private {Name}(in Quantity value) => this.{fieldName} = value; + + public {Name} To(in Scalar other) + where TUnit : I{Name}, IUnit => new(other.Transform(in this.{fieldName})); + + public static {Name} Of(in Double value, in Scalar measure) + where TUnit : I{Name}, IUnit => new(measure.Create(in value)); + + static {Name} IFactory<{Name}>.Create(in Quantity value) => new(in value); + + public Boolean Equals({Name} other) => this.{fieldName}.Equals(other.{fieldName}); + + public override Boolean Equals(Object? obj) => obj is {Name} {fieldName} && Equals({fieldName}); + + public override Int32 GetHashCode() => this.{fieldName}.GetHashCode(); + + public override String ToString() => this.{fieldName}.ToString(); + + public String ToString(String? format, IFormatProvider? provider) => this.{fieldName}.ToString(format, provider); +} +``` + +**Notes**: +- The constructor is `private` for scalar quantities. +- There is no `using` for `Atmoos.Quantities.Core.Numerics` (not needed). +- `{fieldName}` is the camelCase version of `{Name}`. + +--- + +### 2. PowerOf (quantity that is a power of a linear dimension) + +**When**: The quantity's dimension is a single base dimension raised to a power > 1. + +**Dimension**: `IDimension, IDerivedQuantity` (e.g., `IDimension` for Area). + +**Struct implements**: `IQuantity, IDimension, IPowerOf` + +**Examples**: `Area` (Length²), `Volume` (Length³) + +**Template** (using `Area` as canonical example): + +```csharp +using Atmoos.Quantities.Core.Numerics; +using Atmoos.Quantities.Creation; +using Atmoos.Quantities.Dimensions; +using Atmoos.Quantities.Units; + +namespace Atmoos.Quantities; + +public readonly struct {Name} : IQuantity<{Name}>, I{Name}, IPowerOf<{Name}, I{Name}, I{Linear}, {Exponent}> +{ + private readonly Quantity {fieldName}; + internal Quantity Value => this.{fieldName}; + Quantity IQuantity<{Name}>.Value => this.{fieldName}; + + private {Name}(in Quantity value) => this.{fieldName} = value; + + public {Name} To(in Power other) + where TUnit : I{Linear}, IUnit => new(other.Transform(in this.{fieldName})); + + public readonly {Name} To(in Scalar other) + where TAlias : I{Name}, IPowerOf, IUnit => new(other.Transform(in this.{fieldName}, static f => ref f.AliasOf())); + + public static {Name} Of(in Double value, in Power measure) + where TUnit : I{Linear}, IUnit => new(measure.Create(in value)); + + public static {Name} Of(in Double value, in Scalar measure) + where TAlias : I{Name}, IPowerOf, IUnit => new(measure.Create(in value, static f => ref f.AliasOf())); + + static {Name} IFactory<{Name}>.Create(in Quantity value) => new(in value); + + public Boolean Equals({Name} other) => this.{fieldName}.Equals(other.{fieldName}); + + public override Boolean Equals(Object? obj) => obj is {Name} {localVar} && Equals({localVar}); + + public override Int32 GetHashCode() => this.{fieldName}.GetHashCode(); + + public override String ToString() => this.{fieldName}.ToString(); + + public String ToString(String? format, IFormatProvider? provider) => this.{fieldName}.ToString(format, provider); +} +``` + +**Notes**: +- Requires `using Atmoos.Quantities.Core.Numerics;` for the exponent type (`Two`, `Three`, etc.). +- Has two overloads each for `To` and `Of`: one for the power form (e.g., `m²`) and one for alias units (e.g., `Litre` for Volume). +- The constructor is `private`. + +--- + +### 3. Quotient (quantity defined as a ratio of two dimensions) + +**When**: The quantity is dimensionally a ratio of two base/derived dimensions: `Nominator / Denominator`. + +**Dimension**: `IProduct>>` (e.g., Velocity = Length / Time). + +**Struct implements**: `IQuantity, IDimension, IQuotient` + +**Examples**: `Velocity` (Length/Time), `DataRate` (AmountOfInformation/Time) + +**Template** (using `Velocity` as canonical example): + +```csharp +using Atmoos.Quantities.Creation; +using Atmoos.Quantities.Dimensions; +using Atmoos.Quantities.Units; + +namespace Atmoos.Quantities; + +public readonly struct {Name} : IQuantity<{Name}>, I{Name}, IQuotient<{Name}, I{Name}, I{Nominator}, I{Denominator}> +{ + private readonly Quantity {fieldName}; + internal Quantity Value => this.{fieldName}; + Quantity IQuantity<{Name}>.Value => this.{fieldName}; + + internal {Name}(in Quantity value) => this.{fieldName} = value; + + public {Name} To(in Scalar other) + where TUnit : I{Name}, IUnit => new(other.Transform(in this.{fieldName})); + + public {Name} To(in Quotient other) + where TNominator : I{Nominator}, IUnit + where TDenominator : I{Denominator}, IUnit => new(other.Transform(in this.{fieldName})); + + public static {Name} Of(in Double value, in Scalar measure) + where TUnit : I{Name}, IUnit => new(measure.Create(in value)); + + public static {Name} Of(in Double value, in Quotient measure) + where TNominator : IUnit, I{Nominator} + where TDenominator : IUnit, I{Denominator} => new(measure.Create(in value)); + + static {Name} IFactory<{Name}>.Create(in Quantity value) => new(in value); + + public Boolean Equals({Name} other) => this.{fieldName}.Equals(other.{fieldName}); + + public override Boolean Equals(Object? obj) => obj is {Name} {fieldName} && Equals({fieldName}); + + public override Int32 GetHashCode() => this.{fieldName}.GetHashCode(); + + public override String ToString() => this.{fieldName}.ToString(); + + public String ToString(String? format, IFormatProvider? provider) => this.{fieldName}.ToString(format, provider); +} +``` + +**Notes**: +- The constructor is `internal` (not `private`) for quotient quantities, since cross-quantity operators in the `Physics` namespace need to create instances. +- Has two overloads each for `To` and `Of`: one for a scalar alias unit and one for the quotient form. + +--- + +### 4. Quotient with powered denominator + +**When**: The quantity is a ratio where the denominator has an exponent > 1. + +**Dimension**: `IProduct>>`. + +**Struct implements**: `IQuantity, IDimension, IQuotient` + +**Examples**: `Acceleration` (Length/Time²), `Pressure` (Force/Length²) + +**Template** (using `Acceleration` as canonical example): + +```csharp +using Atmoos.Quantities.Core.Numerics; +using Atmoos.Quantities.Creation; +using Atmoos.Quantities.Dimensions; +using Atmoos.Quantities.Units; + +namespace Atmoos.Quantities; + +public readonly struct {Name} : IQuantity<{Name}>, I{Name}, IQuotient<{Name}, I{Name}, I{Nominator}, I{Denominator}, {Exponent}> +{ + private readonly Quantity {fieldName}; + internal Quantity Value => this.{fieldName}; + Quantity IQuantity<{Name}>.Value => this.{fieldName}; + + internal {Name}(in Quantity value) => this.{fieldName} = value; + + public {Name} To(in Scalar other) + where TUnit : I{Name}, IUnit => new(other.Transform(in this.{fieldName})); + + public {Name} To(in Quotient> other) + where TNominator : I{Nominator}, IUnit + where TDenominator : I{Denominator}, IUnit => new(other.Transform(in this.{fieldName})); + + public static {Name} Of(in Double value, in Scalar measure) + where TUnit : I{Name}, IUnit => new(measure.Create(in value)); + + public static {Name} Of(in Double value, in Quotient> measure) + where TNominator : IUnit, I{Nominator} + where TDenominator : IUnit, I{Denominator} => new(measure.Create(in value)); + + static {Name} IFactory<{Name}>.Create(in Quantity value) => new(in value); + + public Boolean Equals({Name} other) => this.{fieldName}.Equals(other.{fieldName}); + + public override Boolean Equals(Object? obj) => obj is {Name} {fieldName} && Equals({fieldName}); + + public override Int32 GetHashCode() => this.{fieldName}.GetHashCode(); + + public override String ToString() => this.{fieldName}.ToString(); + + public String ToString(String? format, IFormatProvider? provider) => this.{fieldName}.ToString(format, provider); +} +``` + +**Notes**: +- Requires `using Atmoos.Quantities.Core.Numerics;` for `Two`, `Three`, etc. +- The quotient form uses `Power` in both `To` and `Of`. +- The constructor is `internal`. + +--- + +### 5. Product (quantity defined as a product of two dimensions) + +**When**: The quantity is dimensionally a product of two dimensions. + +**Dimension**: `IProduct, IDerivedQuantity`. + +**Struct implements**: `IQuantity, IDimension, IProduct` + +**Examples**: `Energy` (Power × Time) + +**Template** (using `Energy` as canonical example): + +```csharp +using Atmoos.Quantities.Creation; +using Atmoos.Quantities.Dimensions; +using Atmoos.Quantities.Units; + +namespace Atmoos.Quantities; + +public readonly struct {Name} : IQuantity<{Name}>, I{Name}, IProduct<{Name}, I{Name}, I{Left}, I{Right}> +{ + private readonly Quantity {fieldName}; + internal Quantity Value => this.{fieldName}; + Quantity IQuantity<{Name}>.Value => this.{fieldName}; + + private {Name}(in Quantity value) => this.{fieldName} = value; + + public {Name} To(in Scalar other) + where TUnit : IUnit, I{Name} => new(other.Transform(in this.{fieldName})); + + public {Name} To(in Product other) + where TLeft : IUnit, I{Left} + where TRight : IUnit, I{Right} => new(other.Transform(in this.{fieldName})); + + public static {Name} Of(in Double value, in Scalar measure) + where TUnit : IUnit, I{Name} => new(measure.Create(in value)); + + public static {Name} Of(in Double value, in Product measure) + where TLeft : IUnit, I{Left} + where TRight : IUnit, I{Right} => new(measure.Create(in value)); + + static {Name} IFactory<{Name}>.Create(in Quantity value) => new(in value); + + public Boolean Equals({Name} other) => this.{fieldName}.Equals(other.{fieldName}); + + public override Boolean Equals(Object? obj) => obj is {Name} {fieldName} && Equals({fieldName}); + + public override Int32 GetHashCode() => this.{fieldName}.GetHashCode(); + + public override String ToString() => this.{fieldName}.ToString(); + + public String ToString(String? format, IFormatProvider? provider) => this.{fieldName}.ToString(format, provider); +} +``` + +**Notes**: +- The constructor is `private` for product quantities. + +--- + +### 6. Invertible (quantity that is the inverse of a base dimension) + +**When**: The quantity is dimensionally the inverse of a single base dimension (exponent = -1). + +**Dimension**: `IDimension>, ILinear, IDerivedQuantity`. + +**Struct implements**: `IQuantity, IDimension, IInvertible` + +**Examples**: `Frequency` (1/Time) + +**Template** (using `Frequency` as canonical example): + +```csharp +using Atmoos.Quantities.Dimensions; +using Atmoos.Quantities.Units; + +namespace Atmoos.Quantities; + +public readonly struct {Name} : IQuantity<{Name}>, I{Name}, IInvertible<{Name}, I{Name}, I{Inverse}> +{ + private readonly Quantity {fieldName}; + internal Quantity Value => this.{fieldName}; + Quantity IQuantity<{Name}>.Value => this.{fieldName}; + + private {Name}(in Quantity value) => this.{fieldName} = value; + + public {Name} To(in Creation.Scalar other) + where TUnit : I{Name}, IInvertible, IUnit => new(other.Transform(in this.{fieldName}, static f => ref f.InverseOf())); + + public static {Name} Of(in Double value, in Creation.Scalar measure) + where TUnit : I{Name}, IInvertible, IUnit => new(measure.Create(in value, static f => ref f.InverseOf())); + + static {Name} IFactory<{Name}>.Create(in Quantity value) => new(in value); + + public Boolean Equals({Name} other) => this.{fieldName}.Equals(other.{fieldName}); + + public override Boolean Equals(Object? obj) => obj is {Name} {fieldName} && Equals({fieldName}); + + public override Int32 GetHashCode() => this.{fieldName}.GetHashCode(); + + public override String ToString() => this.{fieldName}.ToString(); + + public String ToString(String? format, IFormatProvider? provider) => this.{fieldName}.ToString(format, provider); +} +``` + +**Notes**: +- Uses `Creation.Scalar` (qualified) to avoid conflict with the struct name. +- The `To` and `Of` methods constrain `TUnit : IInvertible` additionally. +- Uses `InverseOf` on the factory instead of `AliasOf`. +- The constructor is `private`. + +--- + +## Step-by-Step Procedure + +### Step 1: Determine the dimensional formula + +Analyse the requested quantity using SI dimensional analysis. Express it in terms of existing base dimensions: `ILength`, `ITime`, `IMass`, `IElectricCurrent`, `ITemperature`, `IAmountOfSubstance`, `ILuminousIntensity`, or existing derived dimensions: `IArea`, `IVolume`, `IVelocity`, `IAcceleration`, `IForce`, `IPower`, `IEnergy`, `IFrequency`, `IPressure`, `IElectricPotential`, `IElectricalResistance`, `IAmountOfInformation`, `IInformationRate`. + +### Step 2: Create or verify the dimension interface + +If a matching dimension interface does not already exist: + +- **Base quantities** → add to `source/Atmoos.Quantities/Dimensions/BaseDimensions.cs` +- **Derived quantities** → add to `source/Atmoos.Quantities/Dimensions/DerivedDimensions.cs` +- **Electrical quantities** → add to `source/Atmoos.Quantities/Dimensions/ElectricalDimesions.cs` + +Use the existing patterns: + +```csharp +// Base: linear, independent dimension +public interface IMyQuantity : ILinear, IBaseQuantity; + +// Derived: power of a base dimension +public interface IMyQuantity : IDimension, IDerivedQuantity; + +// Derived: product of dimensions (used for quotients too) +public interface IMyQuantity : IProduct>>, IDerivedQuantity; +``` + +### Step 3: Create the quantity struct + +Create the file `source/Atmoos.Quantities/Quantities/{Name}.cs` using the appropriate template from the categories above. + +### Step 4: Add cross-quantity operators + +Add operators that relate the new quantity to existing quantities according to SI rules. Place them in the appropriate file under `source/Atmoos.Quantities/Physics/`: + +- `MechanicalEngineering.cs` → geometry (Length, Area, Volume) and kinematics (Velocity, Acceleration, Force, Energy, Power, Pressure) +- `ElectricalEngineering.cs` → electrical quantities (Ohm's law, Power laws) +- `ComputerScience.cs` → data and information rate +- `Generic.cs` → frequency/time inversion and other dimensionless relations + +Use the extension method pattern: + +```csharp +using static Atmoos.Quantities.Extensions; + +// Inside appropriate static class: +extension({QuantityType}) +{ + public static {ResultType} operator *|/(in {QuantityType} left, in {OtherType} right) => + Create<{ResultType}>(left.Value *|/ right.Value); +} +``` + +Key rules for operator placement: +- For `A = B * C`, define `operator *` on **both** `B` and `C` (commutativity). +- For `A = B / C`, define `operator /` on `B`. +- For each product `A = B * C`, also define the inverse divisions: `B = A / C` and `C = A / B` on `A`. + +### Step 5: Verify + +Build the solution to ensure everything compiles: + +``` +dotnet build source/Atmoos.Quantities.sln +``` + +--- + +## Quick Reference: Existing Dimension-Quantity Mappings + +| Quantity | Dimension Interface | Category | SI Formula | +| --------------------- | -------------------- | -------------------------- | -------------------- | +| Length | ILength | Scalar (base) | L | +| Time | ITime | Scalar (base) | T | +| Mass | IMass | Scalar (base) | M | +| ElectricCurrent | IElectricCurrent | Scalar (base) | I | +| Temperature | ITemperature | Scalar (base) | Θ | +| Area | IArea | PowerOf(ILength, Two) | L² | +| Volume | IVolume | PowerOf(ILength, Three) | L³ | +| Velocity | IVelocity | Quotient(ILength, ITime) | L·T⁻¹ | +| Acceleration | IAcceleration | Quotient(ILength, ITime²) | L·T⁻² | +| Force | IForce | Scalar (derived) | M·L·T⁻² | +| Power | IPower | Scalar (derived) | M·L²·T⁻³ | +| Energy | IEnergy | Product(IPower, ITime) | M·L²·T⁻² | +| Pressure | IPressure | Quotient(IForce, ILength²) | M·L⁻¹·T⁻² | +| Frequency | IFrequency | Invertible(ITime) | T⁻¹ | +| ElectricPotential | IElectricPotential | Scalar (derived) | M·L²·T⁻³·I⁻¹ | +| ElectricalResistance | IElectricalResistance| Scalar (derived) | M·L²·T⁻³·I⁻² | +| Data | IAmountOfInformation | Scalar (derived) | (information) | +| DataRate | IInformationRate | Quotient(IAmountOfInformation, ITime) | information·T⁻¹ | + +--- + +## Important Conventions + +1. **Namespace**: All quantity structs live in `namespace Atmoos.Quantities;` (not a sub-namespace). +2. **File location**: All quantity struct files are in `source/Atmoos.Quantities/Quantities/`. +3. **Field naming**: The private field is always the camelCase of the quantity name (e.g., `velocity` for `Velocity`). +4. **Constructor access**: `private` for scalar, power-of, product, and invertible quantities; `internal` for quotient quantities. +5. **.NET type aliases**: Always use `Double`, `Boolean`, `Int32`, `String`, `Object` — never `double`, `bool`, `int`, `string`, `object`. +6. **`in` modifier**: Use `in` for all struct parameters consistently. +7. **File naming for AI-generated files**: Since these files follow established patterns, use the standard `{Name}.cs` naming (not `.ai.cs`), matching all existing quantities. +8. **Cross-quantity operators** use `Create(...)` from `Extensions` via `using static Atmoos.Quantities.Extensions;`. +9. **Operators class**: Do NOT modify `Operators.cs` — it automatically provides `==`, `!=`, `>`, `>=`, `<`, `<=`, `+`, `-`, `*`, `/` for all `IQuantity` types via extensions. From c6065082e6195b9a089547816a514ce984d9c605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=A4gi?= Date: Tue, 17 Feb 2026 17:09:13 +0100 Subject: [PATCH 05/72] Copilot: Move the agent into the agents folder. --- .../{prompts/new-quantity.prompt.md => agents/new-quantity.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{prompts/new-quantity.prompt.md => agents/new-quantity.md} (100%) diff --git a/.github/prompts/new-quantity.prompt.md b/.github/agents/new-quantity.md similarity index 100% rename from .github/prompts/new-quantity.prompt.md rename to .github/agents/new-quantity.md From 3f39ae98e658bb46ed0fde74c546948b4c458d42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=A4gi?= Date: Tue, 17 Feb 2026 17:17:49 +0100 Subject: [PATCH 06/72] Improve workspace --- Quantities.code-workspace | 47 ++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/Quantities.code-workspace b/Quantities.code-workspace index f8e3cea4..5279c577 100644 --- a/Quantities.code-workspace +++ b/Quantities.code-workspace @@ -1,29 +1,20 @@ { - "folders": [ - { - "path": ".vscode" - }, - { - "path": ".github" - }, - { - "path": "source" - } - ], - "settings": { - "chat.tools.terminal.autoApprove": { - "dotnet test": true - }, - "mcp": { - "servers": { - "microsoftdocs/mcp": { - "type": "http", - "url": "https://learn.microsoft.com/api/mcp", - "gallery": "https://api.mcp.github.com", - "version": "1.0.0" - } - }, - "inputs": [] - } - } -} \ No newline at end of file + "folders": [ + { + "path": "." + } + ], + "settings": { + "files.exclude": { + "assets": true, + "documentation": true, + ".gitattributes": true, + ".gitignore": true, + "LICENSE": true, + "Quantities.code-workspace": true + }, + "chat.tools.terminal.autoApprove": { + "dotnet test": true + } + } +} From 41ce84867b4c7b97a459ed4d3de923ef383c17d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=A4gi?= Date: Tue, 17 Feb 2026 17:23:01 +0100 Subject: [PATCH 07/72] allow dotnet build --- Quantities.code-workspace | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Quantities.code-workspace b/Quantities.code-workspace index 5279c577..29c93ea3 100644 --- a/Quantities.code-workspace +++ b/Quantities.code-workspace @@ -14,7 +14,8 @@ "Quantities.code-workspace": true }, "chat.tools.terminal.autoApprove": { - "dotnet test": true + "dotnet test": true, + "dotnet build": true } } } From 0ba30145ed51a2601dfa27223c130389328658c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=A4gi?= Date: Tue, 17 Feb 2026 20:44:17 +0100 Subject: [PATCH 08/72] add plan to fix consistency issues --- .../plan-quantityConsistency.prompt.md | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 .github/prompts/plan-quantityConsistency.prompt.md diff --git a/.github/prompts/plan-quantityConsistency.prompt.md b/.github/prompts/plan-quantityConsistency.prompt.md new file mode 100644 index 00000000..7514d0ce --- /dev/null +++ b/.github/prompts/plan-quantityConsistency.prompt.md @@ -0,0 +1,96 @@ +## Plan: Fix Quantity Struct Inconsistencies + +**TL;DR** — 19 quantity structs were checked against the templates in [new-quantity.md](../agents/new-quantity.md). 7 are fully consistent (Length, Velocity, Acceleration, Energy, Pressure, Data, Frequency). The remaining 12 have inconsistencies across 6 categories. The canonical reference is [Length.cs](../../source/Atmoos.Quantities/Quantities/Length.cs) for scalar quantities. + +--- + +### Issue 1: Missing `using Atmoos.Quantities.Creation;` + qualified `Creation.Scalar` (8 files) + +These scalar quantities omit the `using` directive and qualify `Creation.Scalar` instead of using unqualified `Scalar`: + +- [Time.cs](../../source/Atmoos.Quantities/Quantities/Time.cs) +- [Mass.cs](../../source/Atmoos.Quantities/Quantities/Mass.cs) +- [Temperature.cs](../../source/Atmoos.Quantities/Quantities/Temperature.cs) +- [ElectricCurrent.cs](../../source/Atmoos.Quantities/Quantities/ElectricCurrent.cs) +- [LuminousIntensity.cs](../../source/Atmoos.Quantities/Quantities/LuminousIntensity.cs) +- [Force.cs](../../source/Atmoos.Quantities/Quantities/Force.cs) +- [Power.cs](../../source/Atmoos.Quantities/Quantities/Power.cs) +- [ElectricalResistance.cs](../../source/Atmoos.Quantities/Quantities/ElectricalResistance.cs) + +The template and canonical example (Length) use `using Atmoos.Quantities.Creation;` with unqualified `Scalar`. Note: the invertible template (Frequency) intentionally uses qualified `Creation.Scalar`, so Frequency is correct as-is. + +### Issue 2: Field naming not camelCase of type name (3 files) + +| File | Actual field | Expected (camelCase of type name) | +|------|-------------|-----------------------------------| +| [ElectricCurrent.cs](../../source/Atmoos.Quantities/Quantities/ElectricCurrent.cs#L8) | `current` | `electricCurrent` | +| [ElectricPotential.cs](../../source/Atmoos.Quantities/Quantities/ElectricPotential.cs#L9) | `potential` | `electricPotential` | +| [ElectricalResistance.cs](../../source/Atmoos.Quantities/Quantities/ElectricalResistance.cs#L8) | `resistance` | `electricalResistance` | + +Every other multi-word quantity (e.g., `DataRate` → `dataRate`, `LuminousIntensity` → `luminousIntensity`, `ElectricCurrent` → should be `electricCurrent`) follows the full camelCase convention. These three use shortened names. + +### Issue 3: Missing `internal Quantity Value` property (1 file) + +[Mass.cs](../../source/Atmoos.Quantities/Quantities/Mass.cs#L9) only has the explicit interface implementation `Quantity IQuantity.Value => this.mass;` but is missing the corresponding `internal Quantity Value => this.mass;` property that all other 18 quantities have. Currently nothing accesses `Mass.Value` internally, but this breaks the pattern and would block any future cross-quantity operators involving Mass. + +### Issue 4: PascalCase / wrong variable name in `Equals(Object?)` (3 files) + +| File | Actual | Expected | +|------|--------|----------| +| [Area.cs](../../source/Atmoos.Quantities/Quantities/Area.cs#L33) | `obj is Area Area` | `obj is Area area` | +| [Volume.cs](../../source/Atmoos.Quantities/Quantities/Volume.cs#L37) | `obj is Volume Volume` | `obj is Volume volume` | +| [DataRate.cs](../../source/Atmoos.Quantities/Quantities/DataRate.cs#L32) | `obj is DataRate rate` | `obj is DataRate dataRate` | + +Area and Volume use PascalCase (shadowing the type name). DataRate uses a shortened name `rate` instead of the field name `dataRate`. + +### Issue 5: Method ordering differs from template (2 files) + +Both [ElectricCurrent.cs](../../source/Atmoos.Quantities/Quantities/ElectricCurrent.cs#L25) and [ElectricPotential.cs](../../source/Atmoos.Quantities/Quantities/ElectricPotential.cs#L25) place `ToString(String?, IFormatProvider?)` between `Equals(T)` and `Equals(Object?)`. + +**Template order**: `Equals(T)` → `Equals(Object?)` → `GetHashCode()` → `ToString()` → `ToString(String?, IFormatProvider?)` + +### Issue 6: Wrong constraint order in `Of` method (1 file) + +[DataRate.cs](../../source/Atmoos.Quantities/Quantities/DataRate.cs#L26-L27): The quotient `Of` method uses `IDimension, IUnit` constraint order instead of `IUnit, IDimension`. + +**Actual**: `TNominator : IAmountOfInformation, IUnit` / `TDenominator : ITime, IUnit` +**Expected**: `TNominator : IUnit, IAmountOfInformation` / `TDenominator : IUnit, ITime` + +All other quotient types (Velocity, Acceleration, Pressure) correctly use `IUnit, IDimension` order in `Of` while using `IDimension, IUnit` in `To`. + +### Issue 7: Missing `readonly` on alias `To` method (1 file) + +[Volume.cs](../../source/Atmoos.Quantities/Quantities/Volume.cs#L20): The alias `To` method is `public Volume To(...)` but [Area.cs](../../source/Atmoos.Quantities/Quantities/Area.cs#L20) (same category) and the template both use `public readonly Area To(...)`. + +--- + +### Steps + +1. **Fix Issue 1** — In each of the 8 scalar files: add `using Atmoos.Quantities.Creation;` and replace `Creation.Scalar` with `Scalar` in `To` and `Of` methods. + +2. **Fix Issue 2** — In [ElectricCurrent.cs](../../source/Atmoos.Quantities/Quantities/ElectricCurrent.cs), [ElectricPotential.cs](../../source/Atmoos.Quantities/Quantities/ElectricPotential.cs), and [ElectricalResistance.cs](../../source/Atmoos.Quantities/Quantities/ElectricalResistance.cs): rename the private field and update all references (field, constructor, `Value` properties, `To`, `Of`, `Equals`, `GetHashCode`, `ToString`). + +3. **Fix Issue 3** — In [Mass.cs](../../source/Atmoos.Quantities/Quantities/Mass.cs): add `internal Quantity Value => this.mass;` before the explicit interface line. + +4. **Fix Issue 4** — In [Area.cs](../../source/Atmoos.Quantities/Quantities/Area.cs), [Volume.cs](../../source/Atmoos.Quantities/Quantities/Volume.cs), and [DataRate.cs](../../source/Atmoos.Quantities/Quantities/DataRate.cs): fix the variable name in `Equals(Object?)` to use the (corrected) field name in camelCase. + +5. **Fix Issue 5** — In [ElectricCurrent.cs](../../source/Atmoos.Quantities/Quantities/ElectricCurrent.cs) and [ElectricPotential.cs](../../source/Atmoos.Quantities/Quantities/ElectricPotential.cs): reorder methods to match template order. + +6. **Fix Issue 6** — In [DataRate.cs](../../source/Atmoos.Quantities/Quantities/DataRate.cs): swap constraint order in the quotient `Of` method to `IUnit, IDimension`. + +7. **Fix Issue 7** — In [Volume.cs](../../source/Atmoos.Quantities/Quantities/Volume.cs): add `readonly` modifier to the alias `To` method. + +8. **Build** — Run `dotnet build ../../source/Atmoos.Quantities.sln` to verify all changes compile. + +9. **Run tests** — Run `dotnet test ../../source/Atmoos.Quantities.sln` to verify no regressions. + +### Verification + +- `dotnet build ../../source/Atmoos.Quantities.sln` must succeed +- `dotnet test ../../source/Atmoos.Quantities.sln` must pass all existing tests +- Visual inspection: all 19 quantity files should follow their respective category template + +### Decisions + +- Issue 1 (using/qualified): Align to the template convention (unqualified + import), matching the canonical Length example, rather than the majority pattern (qualified without import) +- Issue 2 (field naming): Use strict camelCase of the full type name per the template convention, even though shortened names are arguably more readable for long type names From d97b6c2561970f30f9f5dac2d999df2f60e509a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=A4gi?= Date: Tue, 17 Feb 2026 21:02:06 +0100 Subject: [PATCH 09/72] fix inconsistencies by executing plan --- .github/agents/new-quantity.md | 2 +- .../plan-quantityConsistency.prompt.md | 4 +-- source/Atmoos.Quantities/Quantities/Area.cs | 4 +-- .../Atmoos.Quantities/Quantities/DataRate.cs | 6 ++--- .../Quantities/ElectricCurrent.cs | 27 ++++++++++--------- .../Quantities/ElectricPotential.cs | 20 +++++++------- .../Quantities/ElectricalResistance.cs | 27 ++++++++++--------- source/Atmoos.Quantities/Quantities/Force.cs | 7 ++--- source/Atmoos.Quantities/Quantities/Mass.cs | 8 +++--- source/Atmoos.Quantities/Quantities/Power.cs | 7 ++--- .../Quantities/Temperature.cs | 7 ++--- source/Atmoos.Quantities/Quantities/Time.cs | 7 ++--- source/Atmoos.Quantities/Quantities/Volume.cs | 2 +- 13 files changed, 68 insertions(+), 60 deletions(-) diff --git a/.github/agents/new-quantity.md b/.github/agents/new-quantity.md index fc0c5e49..190472cd 100644 --- a/.github/agents/new-quantity.md +++ b/.github/agents/new-quantity.md @@ -104,7 +104,7 @@ public readonly struct {Name} : IQuantity<{Name}>, I{Name}, IPowerOf<{Name}, I{N public {Name} To(in Power other) where TUnit : I{Linear}, IUnit => new(other.Transform(in this.{fieldName})); - public readonly {Name} To(in Scalar other) + public {Name} To(in Scalar other) where TAlias : I{Name}, IPowerOf, IUnit => new(other.Transform(in this.{fieldName}, static f => ref f.AliasOf())); public static {Name} Of(in Double value, in Power measure) diff --git a/.github/prompts/plan-quantityConsistency.prompt.md b/.github/prompts/plan-quantityConsistency.prompt.md index 7514d0ce..9f92e3e9 100644 --- a/.github/prompts/plan-quantityConsistency.prompt.md +++ b/.github/prompts/plan-quantityConsistency.prompt.md @@ -58,7 +58,7 @@ Both [ElectricCurrent.cs](../../source/Atmoos.Quantities/Quantities/ElectricCurr All other quotient types (Velocity, Acceleration, Pressure) correctly use `IUnit, IDimension` order in `Of` while using `IDimension, IUnit` in `To`. -### Issue 7: Missing `readonly` on alias `To` method (1 file) +### ~~Issue 7: Missing `readonly` on alias `To` method (1 file)~~ FALSE POSITIVE [Volume.cs](../../source/Atmoos.Quantities/Quantities/Volume.cs#L20): The alias `To` method is `public Volume To(...)` but [Area.cs](../../source/Atmoos.Quantities/Quantities/Area.cs#L20) (same category) and the template both use `public readonly Area To(...)`. @@ -78,7 +78,7 @@ All other quotient types (Velocity, Acceleration, Pressure) correctly use `IUnit 6. **Fix Issue 6** — In [DataRate.cs](../../source/Atmoos.Quantities/Quantities/DataRate.cs): swap constraint order in the quotient `Of` method to `IUnit, IDimension`. -7. **Fix Issue 7** — In [Volume.cs](../../source/Atmoos.Quantities/Quantities/Volume.cs): add `readonly` modifier to the alias `To` method. +7. ~~**Fix Issue 7**~~ — In [Volume.cs](../../source/Atmoos.Quantities/Quantities/Volume.cs): add `readonly` modifier to the alias `To` method. 8. **Build** — Run `dotnet build ../../source/Atmoos.Quantities.sln` to verify all changes compile. diff --git a/source/Atmoos.Quantities/Quantities/Area.cs b/source/Atmoos.Quantities/Quantities/Area.cs index 29997dd7..995ef2f9 100644 --- a/source/Atmoos.Quantities/Quantities/Area.cs +++ b/source/Atmoos.Quantities/Quantities/Area.cs @@ -16,7 +16,7 @@ namespace Atmoos.Quantities; public Area To(in Power other) where TLength : ILength, IUnit => new(other.Transform(in this.area)); - public readonly Area To(in Scalar other) + public Area To(in Scalar other) where TArea : IArea, IPowerOf, IUnit => new(other.Transform(in this.area, static f => ref f.AliasOf())); public static Area Of(in Double value, in Power measure) @@ -29,7 +29,7 @@ public static Area Of(in Double value, in Scalar measure) public Boolean Equals(Area other) => this.area.Equals(other.area); - public override Boolean Equals(Object? obj) => obj is Area Area && Equals(Area); + public override Boolean Equals(Object? obj) => obj is Area area && Equals(area); public override Int32 GetHashCode() => this.area.GetHashCode(); diff --git a/source/Atmoos.Quantities/Quantities/DataRate.cs b/source/Atmoos.Quantities/Quantities/DataRate.cs index d8a84ec8..db214c1e 100644 --- a/source/Atmoos.Quantities/Quantities/DataRate.cs +++ b/source/Atmoos.Quantities/Quantities/DataRate.cs @@ -23,14 +23,14 @@ public static DataRate Of(in Double value, in Scalar measure) where TUnit : IInformationRate, IUnit => new(measure.Create(in value)); public static DataRate Of(in Double value, in Quotient measure) - where TNominator : IAmountOfInformation, IUnit - where TDenominator : ITime, IUnit => new(measure.Create(in value)); + where TNominator : IUnit, IAmountOfInformation + where TDenominator : IUnit, ITime => new(measure.Create(in value)); static DataRate IFactory.Create(in Quantity value) => new(in value); public Boolean Equals(DataRate other) => this.dataRate.Equals(other.dataRate); - public override Boolean Equals(Object? obj) => obj is DataRate rate && Equals(rate); + public override Boolean Equals(Object? obj) => obj is DataRate dataRate && Equals(dataRate); public override Int32 GetHashCode() => this.dataRate.GetHashCode(); diff --git a/source/Atmoos.Quantities/Quantities/ElectricCurrent.cs b/source/Atmoos.Quantities/Quantities/ElectricCurrent.cs index 46411bdd..03766f18 100644 --- a/source/Atmoos.Quantities/Quantities/ElectricCurrent.cs +++ b/source/Atmoos.Quantities/Quantities/ElectricCurrent.cs @@ -1,31 +1,32 @@ -using Atmoos.Quantities.Dimensions; +using Atmoos.Quantities.Creation; +using Atmoos.Quantities.Dimensions; using Atmoos.Quantities.Units; namespace Atmoos.Quantities; public readonly struct ElectricCurrent : IQuantity, IElectricCurrent, IScalar { - private readonly Quantity current; - internal Quantity Value => this.current; - Quantity IQuantity.Value => this.current; + private readonly Quantity electricCurrent; + internal Quantity Value => this.electricCurrent; + Quantity IQuantity.Value => this.electricCurrent; - private ElectricCurrent(in Quantity value) => this.current = value; + private ElectricCurrent(in Quantity value) => this.electricCurrent = value; - public ElectricCurrent To(in Creation.Scalar other) - where TUnit : IElectricCurrent, IUnit => new(other.Transform(in this.current)); + public ElectricCurrent To(in Scalar other) + where TUnit : IElectricCurrent, IUnit => new(other.Transform(in this.electricCurrent)); - public static ElectricCurrent Of(in Double value, in Creation.Scalar measure) + public static ElectricCurrent Of(in Double value, in Scalar measure) where TUnit : IElectricCurrent, IUnit => new(measure.Create(in value)); static ElectricCurrent IFactory.Create(in Quantity value) => new(in value); - public Boolean Equals(ElectricCurrent other) => this.current.Equals(other.current); + public Boolean Equals(ElectricCurrent other) => this.electricCurrent.Equals(other.electricCurrent); - public String ToString(String? format, IFormatProvider? provider) => this.current.ToString(format, provider); + public override Boolean Equals(Object? obj) => obj is ElectricCurrent electricCurrent && Equals(electricCurrent); - public override Boolean Equals(Object? obj) => obj is ElectricCurrent current && Equals(current); + public override Int32 GetHashCode() => this.electricCurrent.GetHashCode(); - public override Int32 GetHashCode() => this.current.GetHashCode(); + public override String ToString() => this.electricCurrent.ToString(); - public override String ToString() => this.current.ToString(); + public String ToString(String? format, IFormatProvider? provider) => this.electricCurrent.ToString(format, provider); } diff --git a/source/Atmoos.Quantities/Quantities/ElectricPotential.cs b/source/Atmoos.Quantities/Quantities/ElectricPotential.cs index d0228f01..4501670f 100644 --- a/source/Atmoos.Quantities/Quantities/ElectricPotential.cs +++ b/source/Atmoos.Quantities/Quantities/ElectricPotential.cs @@ -6,27 +6,27 @@ namespace Atmoos.Quantities; public readonly struct ElectricPotential : IQuantity, IElectricPotential, IScalar { - private readonly Quantity potential; - internal Quantity Value => this.potential; - Quantity IQuantity.Value => this.potential; + private readonly Quantity electricPotential; + internal Quantity Value => this.electricPotential; + Quantity IQuantity.Value => this.electricPotential; - private ElectricPotential(in Quantity value) => this.potential = value; + private ElectricPotential(in Quantity value) => this.electricPotential = value; public ElectricPotential To(in Scalar other) - where TUnit : IElectricPotential, IUnit => new(other.Transform(in this.potential)); + where TUnit : IElectricPotential, IUnit => new(other.Transform(in this.electricPotential)); public static ElectricPotential Of(in Double value, in Scalar measure) where TUnit : IElectricPotential, IUnit => new(measure.Create(in value)); static ElectricPotential IFactory.Create(in Quantity value) => new(in value); - public Boolean Equals(ElectricPotential other) => this.potential.Equals(other.potential); + public Boolean Equals(ElectricPotential other) => this.electricPotential.Equals(other.electricPotential); - public String ToString(String? format, IFormatProvider? provider) => this.potential.ToString(format, provider); + public override Boolean Equals(Object? obj) => obj is ElectricPotential electricPotential && Equals(electricPotential); - public override Boolean Equals(Object? obj) => obj is ElectricPotential potential && Equals(potential); + public override Int32 GetHashCode() => this.electricPotential.GetHashCode(); - public override Int32 GetHashCode() => this.potential.GetHashCode(); + public override String ToString() => this.electricPotential.ToString(); - public override String ToString() => this.potential.ToString(); + public String ToString(String? format, IFormatProvider? provider) => this.electricPotential.ToString(format, provider); } diff --git a/source/Atmoos.Quantities/Quantities/ElectricalResistance.cs b/source/Atmoos.Quantities/Quantities/ElectricalResistance.cs index deaa4e06..8020271c 100644 --- a/source/Atmoos.Quantities/Quantities/ElectricalResistance.cs +++ b/source/Atmoos.Quantities/Quantities/ElectricalResistance.cs @@ -1,31 +1,32 @@ -using Atmoos.Quantities.Dimensions; +using Atmoos.Quantities.Creation; +using Atmoos.Quantities.Dimensions; using Atmoos.Quantities.Units; namespace Atmoos.Quantities; public readonly struct ElectricalResistance : IQuantity, IElectricalResistance, IScalar { - private readonly Quantity resistance; - internal Quantity Value => this.resistance; - Quantity IQuantity.Value => this.resistance; + private readonly Quantity electricalResistance; + internal Quantity Value => this.electricalResistance; + Quantity IQuantity.Value => this.electricalResistance; - private ElectricalResistance(in Quantity value) => this.resistance = value; + private ElectricalResistance(in Quantity value) => this.electricalResistance = value; - public ElectricalResistance To(in Creation.Scalar other) - where TUnit : IElectricalResistance, IUnit => new(other.Transform(in this.resistance)); + public ElectricalResistance To(in Scalar other) + where TUnit : IElectricalResistance, IUnit => new(other.Transform(in this.electricalResistance)); - public static ElectricalResistance Of(in Double value, in Creation.Scalar measure) + public static ElectricalResistance Of(in Double value, in Scalar measure) where TUnit : IElectricalResistance, IUnit => new(measure.Create(in value)); static ElectricalResistance IFactory.Create(in Quantity value) => new(in value); - public Boolean Equals(ElectricalResistance other) => this.resistance.Equals(other.resistance); + public Boolean Equals(ElectricalResistance other) => this.electricalResistance.Equals(other.electricalResistance); - public override Boolean Equals(Object? obj) => obj is ElectricalResistance resistance && Equals(resistance); + public override Boolean Equals(Object? obj) => obj is ElectricalResistance electricalResistance && Equals(electricalResistance); - public override Int32 GetHashCode() => this.resistance.GetHashCode(); + public override Int32 GetHashCode() => this.electricalResistance.GetHashCode(); - public override String ToString() => this.resistance.ToString(); + public override String ToString() => this.electricalResistance.ToString(); - public String ToString(String? format, IFormatProvider? provider) => this.resistance.ToString(format, provider); + public String ToString(String? format, IFormatProvider? provider) => this.electricalResistance.ToString(format, provider); } diff --git a/source/Atmoos.Quantities/Quantities/Force.cs b/source/Atmoos.Quantities/Quantities/Force.cs index dfbe8438..68456a57 100644 --- a/source/Atmoos.Quantities/Quantities/Force.cs +++ b/source/Atmoos.Quantities/Quantities/Force.cs @@ -1,4 +1,5 @@ -using Atmoos.Quantities.Dimensions; +using Atmoos.Quantities.Creation; +using Atmoos.Quantities.Dimensions; using Atmoos.Quantities.Units; namespace Atmoos.Quantities; @@ -11,10 +12,10 @@ namespace Atmoos.Quantities; private Force(in Quantity value) => this.force = value; - public Force To(in Creation.Scalar other) + public Force To(in Scalar other) where TUnit : IForce, IUnit => new(other.Transform(in this.force)); - public static Force Of(in Double value, in Creation.Scalar measure) + public static Force Of(in Double value, in Scalar measure) where TUnit : IForce, IUnit => new(measure.Create(in value)); static Force IFactory.Create(in Quantity value) => new(in value); diff --git a/source/Atmoos.Quantities/Quantities/Mass.cs b/source/Atmoos.Quantities/Quantities/Mass.cs index 455b2eb4..20545f83 100644 --- a/source/Atmoos.Quantities/Quantities/Mass.cs +++ b/source/Atmoos.Quantities/Quantities/Mass.cs @@ -1,4 +1,5 @@ -using Atmoos.Quantities.Dimensions; +using Atmoos.Quantities.Creation; +using Atmoos.Quantities.Dimensions; using Atmoos.Quantities.Units; namespace Atmoos.Quantities; @@ -6,14 +7,15 @@ namespace Atmoos.Quantities; public readonly struct Mass : IQuantity, IMass, IScalar { private readonly Quantity mass; + internal Quantity Value => this.mass; Quantity IQuantity.Value => this.mass; private Mass(in Quantity value) => this.mass = value; - public Mass To(in Creation.Scalar other) + public Mass To(in Scalar other) where TUnit : IMass, IUnit => new(other.Transform(in this.mass)); - public static Mass Of(in Double value, in Creation.Scalar measure) + public static Mass Of(in Double value, in Scalar measure) where TUnit : IMass, IUnit => new(measure.Create(in value)); static Mass IFactory.Create(in Quantity value) => new(in value); diff --git a/source/Atmoos.Quantities/Quantities/Power.cs b/source/Atmoos.Quantities/Quantities/Power.cs index 7b0ec8a1..aeee0154 100644 --- a/source/Atmoos.Quantities/Quantities/Power.cs +++ b/source/Atmoos.Quantities/Quantities/Power.cs @@ -1,4 +1,5 @@ -using Atmoos.Quantities.Dimensions; +using Atmoos.Quantities.Creation; +using Atmoos.Quantities.Dimensions; using Atmoos.Quantities.Units; namespace Atmoos.Quantities; @@ -11,10 +12,10 @@ namespace Atmoos.Quantities; private Power(in Quantity value) => this.power = value; - public Power To(in Creation.Scalar other) + public Power To(in Scalar other) where TUnit : IPower, IUnit => new(other.Transform(in this.power)); - public static Power Of(in Double value, in Creation.Scalar measure) + public static Power Of(in Double value, in Scalar measure) where TUnit : IPower, IUnit => new(measure.Create(in value)); static Power IFactory.Create(in Quantity value) => new(in value); diff --git a/source/Atmoos.Quantities/Quantities/Temperature.cs b/source/Atmoos.Quantities/Quantities/Temperature.cs index c1b6df2f..2bec710e 100644 --- a/source/Atmoos.Quantities/Quantities/Temperature.cs +++ b/source/Atmoos.Quantities/Quantities/Temperature.cs @@ -1,4 +1,5 @@ -using Atmoos.Quantities.Dimensions; +using Atmoos.Quantities.Creation; +using Atmoos.Quantities.Dimensions; using Atmoos.Quantities.Units; namespace Atmoos.Quantities; @@ -11,10 +12,10 @@ namespace Atmoos.Quantities; private Temperature(in Quantity value) => this.temperature = value; - public Temperature To(in Creation.Scalar other) + public Temperature To(in Scalar other) where TUnit : ITemperature, IUnit => new(other.Transform(in this.temperature)); - public static Temperature Of(in Double value, in Creation.Scalar measure) + public static Temperature Of(in Double value, in Scalar measure) where TUnit : ITemperature, IUnit => new(measure.Create(in value)); static Temperature IFactory.Create(in Quantity value) => new(in value); diff --git a/source/Atmoos.Quantities/Quantities/Time.cs b/source/Atmoos.Quantities/Quantities/Time.cs index 85594ec1..40a3ed28 100644 --- a/source/Atmoos.Quantities/Quantities/Time.cs +++ b/source/Atmoos.Quantities/Quantities/Time.cs @@ -1,4 +1,5 @@ -using Atmoos.Quantities.Dimensions; +using Atmoos.Quantities.Creation; +using Atmoos.Quantities.Dimensions; using Atmoos.Quantities.Units; namespace Atmoos.Quantities; @@ -11,10 +12,10 @@ namespace Atmoos.Quantities; private Time(in Quantity value) => this.time = value; - public Time To(in Creation.Scalar other) + public Time To(in Scalar other) where TUnit : ITime, IUnit => new(other.Transform(in this.time)); - public static Time Of(in Double value, in Creation.Scalar measure) + public static Time Of(in Double value, in Scalar measure) where TUnit : ITime, IUnit => new(measure.Create(in value)); static Time IFactory