CLAUDE.md states: "IVectorN.Magnitude() (for N >= 1) returns the corresponding IVector0." The C++ projection does exactly that — each form answers magnitude() with the magnitude form of the same dimension, and the doc makes a point of how nicely the dimension works out through sqrt.
The C# side does not. IVector3<TVector, T> declares:
public T Length();
public T Distance(TVector other);
So position.Length() on a Position3D<T> hands back a bare T. The dimension is discarded precisely where the caller wanted Length<T> — or Distance<T> — and has to be re-wrapped by hand, which is the untyped step the library exists to remove. The same applies to IVector2 and IVector4.
Checked across all 72 entries in dimensions.json: every dimension that declares a vector form also declares a vector0, so there is no case where the magnitude type is unavailable.
Design
Add alongside the existing members without changing them — Length() and Distance() stay, since generated code and consumers use them:
/// <summary>Gets the magnitude of this vector as its dimension's magnitude quantity.</summary>
public global::ktsu.Semantics.Quantities.Length<T> Magnitude()
=> global::ktsu.Semantics.Quantities.Length<T>.Create(StorageMath.Sqrt(LengthSquared()));
/// <summary>Gets the distance to another vector as its dimension's magnitude quantity.</summary>
public global::ktsu.Semantics.Quantities.Length<T> DistanceTo(Position3D<T> other)
=> global::ktsu.Semantics.Quantities.Length<T>.Create(StorageMath.Sqrt(DistanceSquared(other)));
Four things to settle
1. Return the V0 base, not an overload. Displacement3D<T>.Magnitude() returns Length<T>, not Distance<T>, because the base is what every overload widens from (resolved decision 3 in CLAUDE.md). A caller who wants Distance narrows explicitly. That also keeps the generator rule purely mechanical: read quantities.vector0.base.
2. A name collision the generator has to handle. Displacement3D<T> already has a method called Length(), so writing Length<T> as a return type inside that same class puts a method group and a generic type under one identifier. It parses — the type argument list disambiguates — but it is fragile and unreadable, and it will confuse anyone reading the generated source. Emit the fully qualified name, as above. This is the one non-obvious implementation detail in the issue.
3. Do not emit MagnitudeSquared(). C++ answers it with a bare Quantity<D> for the honest reason that the square of a dimension usually has no name, and where it has one it is not unique. C# has no structural layer to fall back on, and Velocity² has no declared name at all — so there is nothing to return. Leave LengthSquared() returning T, which is the correct answer rather than a missing feature.
4. Magnitude() uses Create, not a From{Unit} factory, so it bypasses Vector0Guards. That is correct — a magnitude is non-negative by construction — and it avoids paying for a guard on every call in a hot loop.
Tests
- For each vector form,
Magnitude() equals the V0 constructed from Length()
- The return type is the dimension's V0 base — a compile-time check, so an explicitly typed local is enough
Magnitude() of a zero vector equals the V0's Zero
DistanceTo agrees with Magnitude() of the difference, for the signed forms where subtraction is defined
- Cover at least one dimension whose V0 base name differs from the dimension name (
Velocity3D → Speed, Acceleration3D → AccelerationMagnitude), since that is where a naive generator rule using the dimension name instead of the V0 base would break
CLAUDE.mdstates: "IVectorN.Magnitude()(for N >= 1) returns the correspondingIVector0." The C++ projection does exactly that — each form answersmagnitude()with the magnitude form of the same dimension, and the doc makes a point of how nicely the dimension works out throughsqrt.The C# side does not.
IVector3<TVector, T>declares:So
position.Length()on aPosition3D<T>hands back a bareT. The dimension is discarded precisely where the caller wantedLength<T>— orDistance<T>— and has to be re-wrapped by hand, which is the untyped step the library exists to remove. The same applies toIVector2andIVector4.Checked across all 72 entries in
dimensions.json: every dimension that declares a vector form also declares avector0, so there is no case where the magnitude type is unavailable.Design
Add alongside the existing members without changing them —
Length()andDistance()stay, since generated code and consumers use them:Four things to settle
1. Return the V0 base, not an overload.
Displacement3D<T>.Magnitude()returnsLength<T>, notDistance<T>, because the base is what every overload widens from (resolved decision 3 inCLAUDE.md). A caller who wantsDistancenarrows explicitly. That also keeps the generator rule purely mechanical: readquantities.vector0.base.2. A name collision the generator has to handle.
Displacement3D<T>already has a method calledLength(), so writingLength<T>as a return type inside that same class puts a method group and a generic type under one identifier. It parses — the type argument list disambiguates — but it is fragile and unreadable, and it will confuse anyone reading the generated source. Emit the fully qualified name, as above. This is the one non-obvious implementation detail in the issue.3. Do not emit
MagnitudeSquared(). C++ answers it with a bareQuantity<D>for the honest reason that the square of a dimension usually has no name, and where it has one it is not unique. C# has no structural layer to fall back on, andVelocity²has no declared name at all — so there is nothing to return. LeaveLengthSquared()returningT, which is the correct answer rather than a missing feature.4.
Magnitude()usesCreate, not aFrom{Unit}factory, so it bypassesVector0Guards. That is correct — a magnitude is non-negative by construction — and it avoids paying for a guard on every call in a hot loop.Tests
Magnitude()equals the V0 constructed fromLength()Magnitude()of a zero vector equals the V0'sZeroDistanceToagrees withMagnitude()of the difference, for the signed forms where subtraction is definedVelocity3D→Speed,Acceleration3D→AccelerationMagnitude), since that is where a naive generator rule using the dimension name instead of the V0 base would break