diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..150c668 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,29 @@ +name: Deploy MkDocs to Garage +on: + push: + branches: + - main # Adjust to your branch name + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build MkDocs Material + # We use the official image to build the site into the 'site' folder + run: | + docker run --rm -v "${{ github.workspace }}:/docs" \ + squidfunk/mkdocs-material build + + - name: Sync to Garage S3 + uses: https://github.com/jakejarvis/s3-sync-action@master + with: + args: --endpoint-url https://s3.garage.bouvais.lu --acl public-read --delete + env: + AWS_S3_BUCKET: 'zig-dimal.bouvais.lu' + AWS_ACCESS_KEY_ID: ${{ secrets.GARAGE_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.GARAGE_SECRET_KEY }} + AWS_REGION: 'garage' + SOURCE_DIR: 'site' # MkDocs defaults to 'site' folder for output diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..3aeb3d3 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,26 @@ +# Welcome to My Project + +This is a static site hosted via **Gitea Actions** and **Garage S3 Storage**. + +!!! info "Status" + The deployment pipeline is currently **Active**. + Updates to the `main` branch are pushed automatically. + +## Quick Start + +To replicate this setup, you need: +1. **Traefik** as the reverse proxy. +2. **Garage** for S3-compatible web hosting. +3. **Gitea** for version control and CI. + +### Deployment Details +| Component | Technology | +| :--- | :--- | +| **Engine** | MkDocs Material | +| **Hosting** | Garage S3 | +| **Routing** | Traefik | + +--- + +## Contact +If you have questions, reach out via the Gitea instance. diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..48fc8ad --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,48 @@ +site_name: Bouvais Docs +site_url: https://zig-dimal.bouvais.lu +site_description: A minimal technical documentation site. +site_author: Adrien Bouvais + +theme: + name: material + language: en + # Color palette with auto light/dark mode + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: indigo + accent: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: indigo + accent: indigo + toggle: + icon: material/brightness-4 + name: Switch to light mode + + features: + - navigation.sections + - navigation.top + - content.code.copy + - content.code.annotate + +# Minimal plugins +plugins: + - search + +# Your single page +nav: + - Home: index.md + +# Extensions to make your markdown look better +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - attr_list diff --git a/src/Base.zig b/src/Base.zig index c5f4448..4bde68b 100644 --- a/src/Base.zig +++ b/src/Base.zig @@ -3,7 +3,7 @@ const std = @import("std"); // Adjust these imports to match your actual file names const Dimensions = @import("Dimensions.zig"); const Scales = @import("Scales.zig"); -const Scalar = @import("Scalar.zig").Scalar; +const Scalar = @import("Quantity.zig").Scalar; fn PhysicalConstant(comptime d: Dimensions.ArgOpts, comptime val: f64, comptime s: Scales.ArgOpts) type { return struct { @@ -12,7 +12,7 @@ fn PhysicalConstant(comptime d: Dimensions.ArgOpts, comptime val: f64, comptime /// Instantiates the constant into a specific numeric type. pub fn Of(comptime T: type) Scalar(T, d, s) { - return .{ .value = @as(T, @floatCast(val)) }; + return .{ .data = @splat(@as(T, @floatCast(val))) }; } }; } @@ -157,78 +157,78 @@ pub const SurfaceTension = BaseScalar(.{ .M = 1, .T = -2 }); // Corrected from M test "BaseQuantities - Core dimensions instantiation" { // Basic types via generic wrappers const M = Meter.Of(f32); - const distance = M{ .value = 100.0 }; - try std.testing.expectEqual(100.0, distance.value); + const distance = M.splat(100); + try std.testing.expectEqual(100.0, distance.value()); try std.testing.expectEqual(1, M.dims.get(.L)); try std.testing.expectEqual(0, M.dims.get(.T)); // Test specific scale variants const Kmh = Speed.Scaled(f32, .{ .L = .k, .T = .hour }); - const speed = Kmh{ .value = 120.0 }; - try std.testing.expectEqual(120.0, speed.value); + const speed = Kmh.splat(120); + try std.testing.expectEqual(120.0, speed.value()); try std.testing.expectEqual(.k, @TypeOf(speed).scales.get(.L)); try std.testing.expectEqual(.hour, @TypeOf(speed).scales.get(.T)); } test "BaseQuantities - Kinematics equations" { - const d = Meter.Of(f32){ .value = 50.0 }; - const t = Second.Of(f32){ .value = 2.0 }; + const d = Meter.Of(f32).splat(50.0); + const t = Second.Of(f32).splat(2.0); // Velocity = Distance / Time const v = d.div(t); - try std.testing.expectEqual(25.0, v.value); + try std.testing.expectEqual(25.0, v.value()); try std.testing.expect(Speed.dims.eql(@TypeOf(v).dims)); // Acceleration = Velocity / Time const a = v.div(t); - try std.testing.expectEqual(12.5, a.value); + try std.testing.expectEqual(12.5, a.value()); try std.testing.expect(Acceleration.dims.eql(@TypeOf(a).dims)); } test "BaseQuantities - Dynamics (Force and Work)" { // 10 kg - const m = Gramm.Scaled(f32, .{ .M = .k }){ .value = 10.0 }; + const m = Gramm.Scaled(f32, .{ .M = .k }).splat(10.0); // 9.8 m/s^2 - const a = Acceleration.Of(f32){ .value = 9.8 }; + const a = Acceleration.Of(f32).splat(9.8); // Force = mass * acceleration const f = m.mul(a); - try std.testing.expectEqual(98, f.value); + try std.testing.expectEqual(98, f.value()); try std.testing.expect(Force.dims.eql(@TypeOf(f).dims)); // Energy (Work) = Force * distance - const distance = Meter.Of(f32){ .value = 5.0 }; + const distance = Meter.Of(f32).splat(5.0); const energy = f.mul(distance); - try std.testing.expectEqual(490, energy.value); + try std.testing.expectEqual(490, energy.value()); try std.testing.expect(Energy.dims.eql(@TypeOf(energy).dims)); } test "BaseQuantities - Electric combinations" { - const current = ElectricCurrent.Of(f32){ .value = 2.0 }; // 2 A - const time = Second.Of(f32){ .value = 3.0 }; // 3 s + const current = ElectricCurrent.Of(f32).splat(2); // 2 A + const time = Second.Of(f32).splat(3.0); // 3 s // Charge = Current * time const charge = current.mul(time); - try std.testing.expectEqual(6.0, charge.value); + try std.testing.expectEqual(6.0, charge.value()); try std.testing.expect(ElectricCharge.dims.eql(@TypeOf(charge).dims)); } test "Constants - Initialization and dimension checks" { // Speed of Light const c = Constants.SpeedOfLight.Of(f64); - try std.testing.expectEqual(299792458.0, c.value); + try std.testing.expectEqual(299792458.0, c.value()); try std.testing.expectEqual(1, @TypeOf(c).dims.get(.L)); try std.testing.expectEqual(-1, @TypeOf(c).dims.get(.T)); // Electron Mass (verifying scale as well) const me = Constants.ElectronMass.Of(f64); - try std.testing.expectEqual(9.1093837139e-31, me.value); + try std.testing.expectEqual(9.1093837139e-31, me.value()); try std.testing.expectEqual(1, @TypeOf(me).dims.get(.M)); try std.testing.expectEqual(.k, @TypeOf(me).scales.get(.M)); // Should be scaled to kg // Boltzmann Constant (Complex derived dimensions) const kb = Constants.Boltzmann.Of(f64); - try std.testing.expectEqual(1.380649e-23, kb.value); + try std.testing.expectEqual(1.380649e-23, kb.value()); try std.testing.expectEqual(1, @TypeOf(kb).dims.get(.M)); try std.testing.expectEqual(2, @TypeOf(kb).dims.get(.L)); try std.testing.expectEqual(-2, @TypeOf(kb).dims.get(.T)); @@ -237,7 +237,7 @@ test "Constants - Initialization and dimension checks" { // Vacuum Permittivity const eps0 = Constants.VacuumPermittivity.Of(f64); - try std.testing.expectEqual(8.8541878188e-12, eps0.value); + try std.testing.expectEqual(8.8541878188e-12, eps0.value()); try std.testing.expectEqual(-1, @TypeOf(eps0).dims.get(.M)); try std.testing.expectEqual(-3, @TypeOf(eps0).dims.get(.L)); try std.testing.expectEqual(4, @TypeOf(eps0).dims.get(.T)); @@ -245,7 +245,7 @@ test "Constants - Initialization and dimension checks" { // Fine Structure Constant (Dimensionless) const alpha = Constants.FineStructure.Of(f64); - try std.testing.expectEqual(0.0072973525643, alpha.value); + try std.testing.expectEqual(0.0072973525643, alpha.value()); try std.testing.expectEqual(0, @TypeOf(alpha).dims.get(.M)); try std.testing.expectEqual(0, @TypeOf(alpha).dims.get(.L)); } diff --git a/src/Quantity.zig b/src/Quantity.zig new file mode 100644 index 0000000..4e30ec9 --- /dev/null +++ b/src/Quantity.zig @@ -0,0 +1,1259 @@ +const std = @import("std"); +const hlp = @import("helper.zig"); +const Scales = @import("Scales.zig"); +const UnitScale = Scales.UnitScale; +const Dimensions = @import("Dimensions.zig"); +const Dimension = Dimensions.Dimension; + +// --------------------------------------------------------------------------- +// Quantity — single unified dimensioned type. +// +// T : element numeric type (f32, f64, i32, i128, …) +// N : lane count (1 = Scalar, >1 = Vector) +// d : SI dimension exponents +// s : unit scales +// +// All arithmetic is performed directly on the underlying @Vector(N, T), so +// the compiler can emit SIMD instructions wherever the target supports them. +// +// Thin aliases (same type identity, no wrapper overhead): +// Scalar(T, d, s) ≡ Quantity(T, 1, d, s) +// Vector(N, Q) ≡ Quantity(Q.ValueType, N, Q.dims.argsOpt(), Q.scales.argsOpt()) +// --------------------------------------------------------------------------- +// +// To investigate: +// @reduce(comptime op: std.builtin.ReduceOp, value: anytype) E +// @select(comptime T: type, pred: @Vector(len, bool), a: @Vector(len, T), b: @Vector(len, T)) @Vector(len, T) +// @shuffle(comptime E: type, a: @Vector(a_len, E), b: @Vector(b_len, E), comptime mask: @Vector(mask_len, i32)) @Vector(mask_len, E) + +pub fn Quantity( + comptime T: type, + comptime N: usize, + comptime d_opt: Dimensions.ArgOpts, + comptime s_opt: Scales.ArgOpts, +) type { + comptime std.debug.assert(N >= 1); + @setEvalBranchQuota(10_000_000); + + // Local shorthand for the SIMD vector type used in storage. + const Vec = @Vector(N, T); + + return struct { + /// SIMD-friendly storage. Arithmetic operates here directly. + data: Vec, + + const Self = @This(); + + pub const ValueType: type = T; + pub const Len: usize = N; + pub const dims: Dimensions = Dimensions.init(d_opt); + pub const scales: Scales = Scales.init(s_opt); + pub const ISQUANTITY = true; + + /// Scalar variant of this quantity (lane=1). Returned by dot(), product(), etc. + pub const ScalarType: type = Quantity(T, 1, d_opt, s_opt); + /// Convenience: a 3-lane vector of the same dimension/scale. + pub const Vec3: type = Quantity(T, 3, d_opt, s_opt); + + // --------------------------------------------------------------- + // Constructors + // --------------------------------------------------------------- + + /// Broadcast a single value across all N lanes. + pub inline fn splat(v: T) Self { + return .{ .data = @splat(v) }; + } + + pub const zero: Self = splat(0); + pub const one: Self = splat(1); + + // --------------------------------------------------------------- + // Scalar-only helpers (N = 1) + // --------------------------------------------------------------- + + /// Return the single scalar value. Compile error when N ≠ 1. + pub inline fn value(self: Self) T { + comptime if (N != 1) + @compileError(".value() is only available on Scalar (N=1)."); + return self.data[0]; + } + + /// Expand this scalar into a len-lane vector by splatting. + pub inline fn vec(self: Self, comptime len: usize) Quantity(T, len, d_opt, s_opt) { + comptime if (N != 1) + @compileError(".vec() is only available on Scalar (N=1)."); + return .{ .data = @splat(self.data[0]) }; + } + + pub inline fn vec3(self: Self) Vec3 { + return self.vec(3); + } + + // --------------------------------------------------------------- + // Internal: RHS normalisation + // + // • For N=1 (Scalar context): bare numbers → Quantity(T, 1, dimless, none) + // • For N>1 (Vector context): bare numbers → Quantity(T, N, dimless, none) + // Quantity(T,1) → broadcast (handled in each op) + // + // A bare number used as rhs is ALWAYS treated as dimensionless. + // --------------------------------------------------------------- + + inline fn RhsT(comptime Rhs: type) type { + return hlp.rhsQuantityType(T, N, Rhs); + } + inline fn rhs(r: anytype) RhsT(@TypeOf(r)) { + return hlp.toRhsQuantity(T, N, r); + } + + /// Scalar rhs (N=1) — used by mulScalar / divScalar / eqScalar etc. + inline fn ScalarRhsT(comptime Rhs: type) type { + return hlp.rhsQuantityType(T, 1, Rhs); + } + inline fn scalarRhs(r: anytype) ScalarRhsT(@TypeOf(r)) { + return hlp.toRhsQuantity(T, 1, r); + } + + // --------------------------------------------------------------- + // Internal: broadcast helper + // + // When an N=1 rhs is used in an N>1 operation, splat it. + // --------------------------------------------------------------- + inline fn broadcastToVec(comptime RhsType: type, r: RhsType) Vec { + if (comptime RhsType.Len == 1 and N > 1) + return @splat(r.data[0]) + else + return r.data; + } + + // --------------------------------------------------------------- + // Arithmetic + // --------------------------------------------------------------- + + /// Element-wise add. Dimensions must match; scales resolve to finer. + /// For N=1: rhs may be a bare number (treated as dimensionless). + /// For N>1: rhs must be a same-length Quantity. + pub inline fn add(self: Self, r: anytype) Quantity( + T, + N, + dims.argsOpt(), + hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(), + ) { + const rhs_q = rhs(r); + const RhsType = @TypeOf(rhs_q); + if (comptime !dims.eql(RhsType.dims)) + @compileError("Dimension mismatch in add: " ++ dims.str() ++ " vs " ++ RhsType.dims.str()); + if (comptime N > 1 and RhsType.Len != N) + @compileError("Vector add requires same-length Quantity."); + + const TargetType = Quantity(T, N, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); + const l: Vec = if (comptime Self == TargetType) self.data else self.to(TargetType).data; + const rr: Vec = if (comptime RhsType == TargetType) rhs_q.data else rhs_q.to(TargetType).data; + return .{ .data = if (comptime hlp.isInt(T)) l +| rr else l + rr }; + } + + /// Element-wise subtract. Dimensions must match; scales resolve to finer. + pub inline fn sub(self: Self, r: anytype) Quantity( + T, + N, + dims.argsOpt(), + hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(), + ) { + const rhs_q = rhs(r); + const RhsType = @TypeOf(rhs_q); + if (comptime !dims.eql(RhsType.dims)) + @compileError("Dimension mismatch in sub: " ++ dims.str() ++ " vs " ++ RhsType.dims.str()); + if (comptime N > 1 and RhsType.Len != N) + @compileError("Vector sub requires same-length Quantity."); + + const TargetType = Quantity(T, N, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); + const l: Vec = if (comptime Self == TargetType) self.data else self.to(TargetType).data; + const rr: Vec = if (comptime RhsType == TargetType) rhs_q.data else rhs_q.to(TargetType).data; + return .{ .data = if (comptime hlp.isInt(T)) l -| rr else l - rr }; + } + + /// Element-wise multiply. Dimension exponents are summed. + /// An N=1 rhs on an N>1 self is automatically broadcast (scalar × vector). + pub inline fn mul(self: Self, r: anytype) Quantity( + T, + N, + dims.add(RhsT(@TypeOf(r)).dims).argsOpt(), + hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(), + ) { + const rhs_q = rhs(r); + const RhsType = @TypeOf(rhs_q); + const SelfNorm = Quantity(T, N, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); + const RhsNorm = Quantity(T, RhsType.Len, RhsType.dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); + const l: Vec = if (comptime Self == SelfNorm) self.data else self.to(SelfNorm).data; + const rr_base = if (comptime RhsType == RhsNorm) rhs_q else rhs_q.to(RhsNorm); + const rr: Vec = broadcastToVec(RhsNorm, rr_base); + return .{ .data = if (comptime hlp.isInt(T)) l *| rr else l * rr }; + } + + /// Element-wise divide. Dimension exponents are subtracted. + /// An N=1 rhs on an N>1 self is automatically broadcast. + pub inline fn div(self: Self, r: anytype) Quantity( + T, + N, + dims.sub(RhsT(@TypeOf(r)).dims).argsOpt(), + hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(), + ) { + const rhs_q = rhs(r); + const RhsType = @TypeOf(rhs_q); + const SelfNorm = Quantity(T, N, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); + const RhsNorm = Quantity(T, RhsType.Len, RhsType.dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); + const l: Vec = if (comptime Self == SelfNorm) self.data else self.to(SelfNorm).data; + const rr_base = if (comptime RhsType == RhsNorm) rhs_q else rhs_q.to(RhsNorm); + const rr: Vec = broadcastToVec(RhsNorm, rr_base); + if (comptime hlp.isInt(T)) { + var result: Vec = undefined; + inline for (0..N) |i| result[i] = @divTrunc(l[i], rr[i]); + return .{ .data = result }; + } else { + return .{ .data = l / rr }; + } + } + + // --------------------------------------------------------------- + // Unary + // --------------------------------------------------------------- + + /// Absolute value of every lane. Uses native `@abs` (SIMD for floats & ints). + pub inline fn abs(self: Self) Self { + return .{ .data = @bitCast(@abs(self.data)) }; + } + + /// Raise every lane to a comptime integer exponent. + /// Repeated SIMD multiply — good for small exponents. + pub inline fn pow(self: Self, comptime exp: comptime_int) Quantity( + T, + N, + dims.scale(exp).argsOpt(), + scales.argsOpt(), + ) { + if (comptime hlp.isInt(T)) { + // No SIMD pow for integers — element-wise std.math.powi. + var result: Vec = undefined; + inline for (0..N) |i| + result[i] = std.math.powi(T, self.data[i], exp) catch std.math.maxInt(T); + return .{ .data = result }; + } else { + // Float: unrolled SIMD multiplications. + const abs_exp = comptime @abs(exp); + var result: Vec = @splat(1); + comptime var i = 0; + inline while (i < abs_exp) : (i += 1) result *= self.data; + if (comptime exp < 0) result = @as(Vec, @splat(1)) / result; + return .{ .data = result }; + } + } + + /// Square root of every lane. All dimension exponents must be even. + pub inline fn sqrt(self: Self) Quantity( + T, + N, + dims.div(2).argsOpt(), + scales.argsOpt(), + ) { + if (comptime !dims.isSquare()) + @compileError("Cannot take sqrt of " ++ dims.str() ++ ": exponents must be even."); + if (comptime @typeInfo(T) == .float) { + return .{ .data = @sqrt(self.data) }; + } else { + // Integer sqrt is not SIMD-able — element-wise. + var result: Vec = undefined; + const UnsignedT = @Int(.unsigned, @typeInfo(T).int.bits); + inline for (0..N) |i| { + const v = self.data[i]; + if (v < 0) + result[i] = 0 + else + result[i] = @as(T, @intCast(std.math.sqrt(@as(UnsignedT, @intCast(v))))); + } + return .{ .data = result }; + } + } + + /// Negate every lane. + pub inline fn negate(self: Self) Self { + return .{ .data = -self.data }; + } + + // --------------------------------------------------------------- + // Conversion + // --------------------------------------------------------------- + + /// Convert to a compatible quantity type. Dimension mismatch is a compile error. + /// Dest can have the same Len as this quantity, or Len == 1 (in which case it + /// will be automatically recast to this quantity's Len). + /// The scale ratio is computed entirely at comptime; the only runtime cost is + /// a SIMD multiply-by-splat (or element-wise cast for cross-numeric-type conversions). + pub inline fn to( + self: Self, + comptime Dest: type, + ) Quantity(Dest.ValueType, N, Dest.dims.argsOpt(), Dest.scales.argsOpt()) { + const ActualDest = Quantity(Dest.ValueType, N, Dest.dims.argsOpt(), Dest.scales.argsOpt()); + + if (comptime !dims.eql(ActualDest.dims)) + @compileError("Dimension mismatch in to: " ++ dims.str() ++ " vs " ++ ActualDest.dims.str()); + + if (comptime Self == ActualDest) return self; + + // Allow Dest to be exactly matching Len or a Scalar (Len == 1) + comptime std.debug.assert(Dest.Len == N or Dest.Len == 1); + + const DestT = ActualDest.ValueType; + const ratio = comptime (scales.getFactor(dims) / ActualDest.scales.getFactor(ActualDest.dims)); + const DestVec = @Vector(N, DestT); + + // ── Same numeric type path ── + if (comptime T == DestT) { + if (comptime @typeInfo(T) == .float) + return .{ .data = self.data * @as(DestVec, @splat(@as(T, @floatCast(ratio)))) }; + + // Integer logic: Branching prevents division by zero errors + if (comptime ratio >= 1.0) { + // Upscaling (e.g., km -> m, ratio = 1000) + const mult: T = comptime @intFromFloat(@round(ratio)); + return .{ .data = self.data *| @as(Vec, @splat(mult)) }; + } else { + // Downscaling (e.g., m -> km, ratio = 0.001) + const div_val: T = comptime @intFromFloat(@round(1.0 / ratio)); + var result: DestVec = undefined; + const half: T = comptime @divTrunc(div_val, 2); + + inline for (0..N) |i| { + const val = self.data[i]; + // Rounding division for integers + result[i] = if (val >= 0) @divTrunc(val + half, div_val) else @divTrunc(val - half, div_val); + } + return .{ .data = result }; + } + } + + // ── Cross-numeric-type (unchanged) ── + var result: DestVec = undefined; + inline for (0..N) |i| { + const float_val: f64 = switch (comptime @typeInfo(T)) { + .float => @floatCast(self.data[i]), + .int => @floatFromInt(self.data[i]), + else => unreachable, + }; + const scaled = float_val * ratio; + result[i] = switch (comptime @typeInfo(DestT)) { + .float => @floatCast(scaled), + .int => @intFromFloat(@round(scaled)), + else => unreachable, + }; + } + return .{ .data = result }; + } + + // --------------------------------------------------------------- + // Comparisons + // + // Return type: bool when N = 1 (Scalar semantics) + // [N]bool when N > 1 (Vector semantics, element-wise) + // + // Whole-vector "all equal/any differ" → use eqAll / neAll. + // Broadcast scalar comparison → use eqScalar / gtScalar / … + // --------------------------------------------------------------- + + const CmpResult = if (N == 1) bool else [N]bool; + + inline fn cmpResult(v: @Vector(N, bool)) CmpResult { + return if (comptime N == 1) v[0] else @as([N]bool, v); + } + + inline fn resolveScalePair(self: Self, rhs_q: anytype) struct { l: Vec, r: Vec } { + const RhsType = @TypeOf(rhs_q); + const TargetType = Quantity(T, N, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); + return .{ + .l = if (comptime Self == TargetType) self.data else self.to(TargetType).data, + .r = if (comptime RhsType == TargetType) rhs_q.data else rhs_q.to(TargetType).data, + }; + } + + pub inline fn eq(self: Self, r: anytype) CmpResult { + const rhs_q = rhs(r); + if (comptime !dims.eql(@TypeOf(rhs_q).dims)) + @compileError("Dimension mismatch in eq: " ++ dims.str() ++ " vs " ++ @TypeOf(rhs_q).dims.str()); + const p = resolveScalePair(self, rhs_q); + return cmpResult(p.l == p.r); + } + + pub inline fn ne(self: Self, r: anytype) CmpResult { + const rhs_q = rhs(r); + if (comptime !dims.eql(@TypeOf(rhs_q).dims)) + @compileError("Dimension mismatch in ne."); + const p = resolveScalePair(self, rhs_q); + return cmpResult(p.l != p.r); + } + + pub inline fn gt(self: Self, r: anytype) CmpResult { + const rhs_q = rhs(r); + if (comptime !dims.eql(@TypeOf(rhs_q).dims)) + @compileError("Dimension mismatch in gt."); + const p = resolveScalePair(self, rhs_q); + return cmpResult(p.l > p.r); + } + + pub inline fn gte(self: Self, r: anytype) CmpResult { + const rhs_q = rhs(r); + if (comptime !dims.eql(@TypeOf(rhs_q).dims)) + @compileError("Dimension mismatch in gte."); + const p = resolveScalePair(self, rhs_q); + return cmpResult(p.l >= p.r); + } + + pub inline fn lt(self: Self, r: anytype) CmpResult { + const rhs_q = rhs(r); + if (comptime !dims.eql(@TypeOf(rhs_q).dims)) + @compileError("Dimension mismatch in lt."); + const p = resolveScalePair(self, rhs_q); + return cmpResult(p.l < p.r); + } + + pub inline fn lte(self: Self, r: anytype) CmpResult { + const rhs_q = rhs(r); + if (comptime !dims.eql(@TypeOf(rhs_q).dims)) + @compileError("Dimension mismatch in lte."); + const p = resolveScalePair(self, rhs_q); + return cmpResult(p.l <= p.r); + } + + // --------------------------------------------------------------- + // Vector whole-quantity comparisons (N > 1 intended, but work for N=1 too) + // --------------------------------------------------------------- + + /// True iff every lane is equal after scale resolution. + pub inline fn eqAll(self: Self, other: anytype) bool { + if (comptime !dims.eql(@TypeOf(other).dims)) + @compileError("Dimension mismatch in eqAll."); + const p = resolveScalePair(self, other); + return @reduce(.And, p.l == p.r); + } + + pub inline fn neAll(self: Self, other: anytype) bool { + return !self.eqAll(other); + } + + // --------------------------------------------------------------- + // Vector broadcast-scalar comparisons (always returns [N]bool) + // --------------------------------------------------------------- + + inline fn broadcastScalarForCmp(self: Self, scalar: anytype) struct { l: Vec, r: Vec } { + const s = scalarRhs(scalar); + const SN = @TypeOf(s); + const TargetScalar = Quantity(T, 1, dims.argsOpt(), hlp.finerScales(Self, SN).argsOpt()); + const TargetSelf = Quantity(T, N, dims.argsOpt(), hlp.finerScales(Self, SN).argsOpt()); + const s_val: T = if (comptime SN == TargetScalar) s.data[0] else s.to(TargetScalar).data[0]; + const l: Vec = if (comptime Self == TargetSelf) self.data else self.to(TargetSelf).data; + return .{ .l = l, .r = @splat(s_val) }; + } + + pub inline fn eqScalar(self: Self, scalar: anytype) [N]bool { + const p = broadcastScalarForCmp(self, scalar); + return @as([N]bool, p.l == p.r); + } + + pub inline fn neScalar(self: Self, scalar: anytype) [N]bool { + const p = broadcastScalarForCmp(self, scalar); + return @as([N]bool, p.l != p.r); + } + + pub inline fn gtScalar(self: Self, scalar: anytype) [N]bool { + const p = broadcastScalarForCmp(self, scalar); + return @as([N]bool, p.l > p.r); + } + + pub inline fn gteScalar(self: Self, scalar: anytype) [N]bool { + const p = broadcastScalarForCmp(self, scalar); + return @as([N]bool, p.l >= p.r); + } + + pub inline fn ltScalar(self: Self, scalar: anytype) [N]bool { + const p = broadcastScalarForCmp(self, scalar); + return @as([N]bool, p.l < p.r); + } + + pub inline fn lteScalar(self: Self, scalar: anytype) [N]bool { + const p = broadcastScalarForCmp(self, scalar); + return @as([N]bool, p.l <= p.r); + } + + // --------------------------------------------------------------- + // Vector broadcast multiply / divide + // (These are explicit aliases for mul/div with an N=1 rhs; kept for + // clarity and backward-compat with the old Vector API.) + // --------------------------------------------------------------- + + pub inline fn mulScalar(self: Self, scalar: anytype) Quantity( + T, + N, + dims.add(ScalarRhsT(@TypeOf(scalar)).dims).argsOpt(), + hlp.finerScales(Self, ScalarRhsT(@TypeOf(scalar))).argsOpt(), + ) { + return self.mul(scalar); + } + + pub inline fn divScalar(self: Self, scalar: anytype) Quantity( + T, + N, + dims.sub(ScalarRhsT(@TypeOf(scalar)).dims).argsOpt(), + hlp.finerScales(Self, ScalarRhsT(@TypeOf(scalar))).argsOpt(), + ) { + return self.div(scalar); + } + + // --------------------------------------------------------------- + // Vector geometric operations + // --------------------------------------------------------------- + + /// Dot product — sum of element-wise products; returns a Scalar. + pub inline fn dot(self: Self, other: anytype) Quantity( + T, + 1, + dims.add(@TypeOf(other).dims).argsOpt(), + hlp.finerScales(Self, @TypeOf(other)).argsOpt(), + ) { + const Tr = @TypeOf(other); + const SelfNorm = Quantity(T, N, dims.argsOpt(), hlp.finerScales(Self, Tr).argsOpt()); + const OtherNorm = Quantity(T, N, Tr.dims.argsOpt(), hlp.finerScales(Self, Tr).argsOpt()); + const l: Vec = if (comptime Self == SelfNorm) self.data else self.to(SelfNorm).data; + const r2: Vec = if (comptime Tr == OtherNorm) other.data else other.to(OtherNorm).data; + return .{ .data = .{@reduce(.Add, l * r2)} }; + } + + /// 3D cross product. Requires N = 3. + pub inline fn cross(self: Self, other: anytype) Quantity( + T, + 3, + dims.add(@TypeOf(other).dims).argsOpt(), + hlp.finerScales(Self, @TypeOf(other)).argsOpt(), + ) { + comptime if (N != 3) @compileError("cross() requires len=3."); + const a = self.data; + const b = other.data; + return .{ .data = .{ + a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0], + } }; + } + + /// Sum of squared components. Cheaper than length(); use for comparisons. + pub inline fn lengthSqr(self: Self) T { + return @reduce(.Add, self.data * self.data); + } + + /// Euclidean length. Float types use SIMD @reduce → @sqrt. + /// Integer types use integer sqrt (truncated). + pub inline fn length(self: Self) T { + const sq = self.lengthSqr(); + if (comptime @typeInfo(T) == .int) { + const UnsignedT = @Int(.unsigned, @typeInfo(T).int.bits); + return @as(T, @intCast(std.math.sqrt(@as(UnsignedT, @intCast(sq))))); + } + return @sqrt(sq); + } + + /// Product of all components. Result dimension is (original dim × N). + pub inline fn product(self: Self) Quantity(T, 1, dims.scale(N).argsOpt(), scales.argsOpt()) { + return .{ .data = .{@reduce(.Mul, self.data)} }; + } + + // --------------------------------------------------------------- + // Formatting (unchanged from old Scalar / Vector) + // --------------------------------------------------------------- + + pub fn formatNumber( + self: Self, + writer: *std.Io.Writer, + options: std.fmt.Number, + ) !void { + if (comptime N == 1) { + // Scalar-style: just print the value + units + switch (@typeInfo(T)) { + .float, .comptime_float => try writer.printFloat(self.data[0], options), + .int, .comptime_int => try writer.printInt(self.data[0], 10, .lower, .{ + .width = options.width, + .alignment = options.alignment, + .fill = options.fill, + .precision = options.precision, + }), + else => unreachable, + } + } else { + // Vector-style: (v0, v1, …) + units + try writer.writeAll("("); + inline for (0..N) |i| { + if (i > 0) try writer.writeAll(", "); + switch (@typeInfo(T)) { + .float, .comptime_float => try writer.printFloat(self.data[i], options), + .int, .comptime_int => try writer.printInt(self.data[i], 10, .lower, .{ + .width = options.width, + .alignment = options.alignment, + .fill = options.fill, + .precision = options.precision, + }), + else => unreachable, + } + } + try writer.writeAll(")"); + } + + var first = true; + inline for (std.enums.values(Dimension)) |bu| { + const v = dims.get(bu); + if (comptime v == 0) continue; + if (!first) try writer.writeAll("."); + first = false; + + const uscale = scales.get(bu); + if (bu == .T and (uscale == .min or uscale == .hour or uscale == .year)) + try writer.print("{s}", .{uscale.str()}) + else + try writer.print("{s}{s}", .{ uscale.str(), bu.unit() }); + + if (v != 1) try hlp.printSuperscript(writer, v); + } + } + }; +} + +// Scalar tests + +pub fn Scalar(comptime T: type, comptime d: Dimensions.ArgOpts, comptime s: Scales.ArgOpts) type { + return Quantity(T, 1, d, s); +} + +test "Scalar initiat" { + const Meter = Scalar(i128, .{ .L = 1 }, .{ .L = @enumFromInt(-3) }); + const Second = Scalar(f32, .{ .T = 1 }, .{ .T = .n }); + + const distance = Meter.splat(10); + const time = Second.splat(2); + + try std.testing.expectEqual(10, distance.value()); + try std.testing.expectEqual(2, time.value()); +} + +test "Scalar comparisons (eq, ne, gt, gte, lt, lte)" { + const Meter = Scalar(i128, .{ .L = 1 }, .{}); + const KiloMeter = Scalar(i128, .{ .L = 1 }, .{ .L = .k }); + + const m1000 = Meter.splat(1000); + const km1 = KiloMeter.splat(1); + const km2 = KiloMeter.splat(2); + + // Equal / Not Equal + try std.testing.expect(m1000.eq(km1)); + try std.testing.expect(km1.eq(m1000)); + try std.testing.expect(km2.ne(m1000)); + + // Greater Than / Greater Than or Equal + try std.testing.expect(km2.gt(m1000)); + try std.testing.expect(km2.gt(km1)); + try std.testing.expect(km1.gte(m1000)); + try std.testing.expect(km2.gte(m1000)); + + // Less Than / Less Than or Equal + try std.testing.expect(m1000.lt(km2)); + try std.testing.expect(km1.lt(km2)); + try std.testing.expect(km1.lte(m1000)); + try std.testing.expect(m1000.lte(km2)); +} + +test "Scalar Add" { + const Meter = Scalar(i128, .{ .L = 1 }, .{}); + + const distance = Meter.splat(10); + const distance2 = Meter.splat(20); + + const added = distance.add(distance2); + try std.testing.expectEqual(30, added.value()); + try std.testing.expectEqual(1, @TypeOf(added).dims.get(.L)); + + const KiloMeter = Scalar(i128, .{ .L = 1 }, .{ .L = .k }); + const distance3 = KiloMeter.splat(2); + const added2 = distance.add(distance3); + try std.testing.expectEqual(2010, added2.value()); + try std.testing.expectEqual(1, @TypeOf(added2).dims.get(.L)); + + const added3 = distance3.add(distance).to(KiloMeter); + try std.testing.expectEqual(2, added3.value()); + try std.testing.expectEqual(1, @TypeOf(added3).dims.get(.L)); + + const KiloMeter_f = Scalar(f64, .{ .L = 1 }, .{ .L = .k }); + const distance4 = KiloMeter_f.splat(2); + const added4 = distance4.add(distance).to(KiloMeter_f); + try std.testing.expectApproxEqAbs(2.01, added4.value(), 0.000001); + try std.testing.expectEqual(1, @TypeOf(added4).dims.get(.L)); +} + +test "Scalar Sub" { + const Meter = Scalar(i128, .{ .L = 1 }, .{}); + const KiloMeter_f = Scalar(f64, .{ .L = 1 }, .{ .L = .k }); + + const a = Meter.splat(500); + const b = Meter.splat(200); + + const diff = a.sub(b); + try std.testing.expectEqual(300, diff.value()); + const diff2 = b.sub(a); + try std.testing.expectEqual(-300, diff2.value()); + + const km_f = KiloMeter_f.splat(2.5); + const m_f = Meter.splat(500); + const diff3 = km_f.sub(m_f); + try std.testing.expectApproxEqAbs(2000, diff3.value(), 1e-4); +} + +test "Scalar MulBy" { + const Meter = Scalar(i128, .{ .L = 1 }, .{}); + const Second = Scalar(f32, .{ .T = 1 }, .{}); + + const d = Meter.splat(3.0); + const t = Second.splat(4.0); + + const area_time = d.mul(t); + try std.testing.expectEqual(12, area_time.value()); + try std.testing.expectEqual(1, @TypeOf(area_time).dims.get(.L)); + try std.testing.expectEqual(1, @TypeOf(area_time).dims.get(.T)); + + const d2 = Meter.splat(5.0); + const area = d.mul(d2); + try std.testing.expectEqual(15, area.value()); + try std.testing.expectEqual(2, @TypeOf(area).dims.get(.L)); +} + +test "Scalar MulBy with scale" { + const KiloMeter = Scalar(f32, .{ .L = 1 }, .{ .L = .k }); + const KiloGram = Scalar(f32, .{ .M = 1 }, .{ .M = .k }); + + const dist = KiloMeter.splat(2.0); + const mass = KiloGram.splat(3.0); + const prod = dist.mul(mass); + try std.testing.expectEqual(1, @TypeOf(prod).dims.get(.L)); + try std.testing.expectEqual(1, @TypeOf(prod).dims.get(.M)); +} + +test "Scalar MulBy with type change" { + const Meter = Scalar(i128, .{ .L = 1 }, .{ .L = .k }); + const Second = Scalar(f64, .{ .T = 1 }, .{}); + const KmSec = Scalar(i64, .{ .L = 1, .T = 1 }, .{ .L = .k }); + const KmSec_f = Scalar(f32, .{ .L = 1, .T = 1 }, .{ .L = .k }); + + const d = Meter.splat(3.0); + const t = Second.splat(4.0); + + const area_time = d.mul(t).to(KmSec); + const area_time_f = d.mul(t).to(KmSec_f); + try std.testing.expectEqual(12, area_time.value()); + try std.testing.expectApproxEqAbs(12.0, area_time_f.value(), 0.0001); +} + +test "Scalar MulBy small" { + const Meter = Scalar(i128, .{ .L = 1 }, .{ .L = .n }); + const Second = Scalar(f32, .{ .T = 1 }, .{}); + + const d = Meter.splat(3.0); + const t = Second.splat(4.0); + + const area_time = d.mul(t); + try std.testing.expectEqual(12, area_time.value()); +} + +test "Scalar MulBy dimensionless" { + const DimLess = Scalar(i128, .{}, .{}); + const Meter = Scalar(i128, .{ .L = 1 }, .{}); + + const d = Meter.splat(7); + const scaled = d.mul(DimLess.splat(3)); + try std.testing.expectEqual(21, scaled.value()); +} + +test "Scalar Sqrt" { + const MeterSquare = Scalar(i128, .{ .L = 2 }, .{}); + + var d = MeterSquare.splat(9); + var scaled = d.sqrt(); + try std.testing.expectEqual(3, scaled.value()); + try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L)); + + d = MeterSquare.splat(-5); + scaled = d.sqrt(); + try std.testing.expectEqual(0, scaled.value()); + + const MeterSquare_f = Scalar(f64, .{ .L = 2 }, .{}); + const d2 = MeterSquare_f.splat(20); + const scaled2 = d2.sqrt(); + try std.testing.expectApproxEqAbs(4.472135955, scaled2.value(), 1e-4); +} + +test "Scalar Chained: velocity and acceleration" { + const Meter = Scalar(i128, .{ .L = 1 }, .{}); + const Second = Scalar(f32, .{ .T = 1 }, .{}); + + const dist = Meter.splat(100.0); + const t1 = Second.splat(5.0); + const velocity = dist.div(t1); + try std.testing.expectEqual(20, velocity.value()); + + const t2 = Second.splat(4.0); + const accel = velocity.div(t2); + try std.testing.expectEqual(5, accel.value()); +} + +test "Scalar DivBy integer exact" { + const Meter = Scalar(i128, .{ .L = 1 }, .{}); + const Second = Scalar(f32, .{ .T = 1 }, .{}); + + const dist = Meter.splat(120); + const time = Second.splat(4); + const vel = dist.div(time); + + try std.testing.expectEqual(30, vel.value()); +} + +test "Scalar Finer scales skip dim 0" { + const Dimless = Scalar(i128, .{}, .{}); + const KiloMetre = Scalar(i128, .{ .L = 1 }, .{ .L = .k }); + + const r = Dimless.splat(30); + const time = KiloMetre.splat(4); + const vel = r.mul(time); + + try std.testing.expectEqual(120, vel.value()); + try std.testing.expectEqual(Scales.UnitScale.k, @TypeOf(vel).scales.get(.L)); +} + +test "Scalar Conversion chain: km -> m -> cm" { + const KiloMeter = Scalar(i128, .{ .L = 1 }, .{ .L = .k }); + const Meter = Scalar(i128, .{ .L = 1 }, .{}); + const CentiMeter = Scalar(i128, .{ .L = 1 }, .{ .L = .c }); + + const km = KiloMeter.splat(15); + const m = km.to(Meter); + const cm = m.to(CentiMeter); + + try std.testing.expectEqual(15_000, m.value()); + try std.testing.expectEqual(1_500_000, cm.value()); +} + +test "Scalar Conversion: hours -> minutes -> seconds" { + const Hour = Scalar(i128, .{ .T = 1 }, .{ .T = .hour }); + const Minute = Scalar(i128, .{ .T = 1 }, .{ .T = .min }); + const Second = Scalar(i128, .{ .T = 1 }, .{}); + + const h = Hour.splat(1.0); + const min = h.to(Minute); + const sec = min.to(Second); + + try std.testing.expectEqual(60, min.value()); + try std.testing.expectEqual(3600, sec.value()); +} + +test "Scalar Format Scalar" { + const MeterPerSecondSq = Scalar(f32, .{ .L = 1, .T = -2 }, .{ .T = .n }); + const Meter = Scalar(f32, .{ .L = 1 }, .{}); + + const m = Meter.splat(1.23456); + const accel = MeterPerSecondSq.splat(9.81); + + var buf: [64]u8 = undefined; + var res = try std.fmt.bufPrint(&buf, "{d:.2}", .{m}); + try std.testing.expectEqualStrings("1.23m", res); + + res = try std.fmt.bufPrint(&buf, "{d}", .{accel}); + try std.testing.expectEqualStrings("9.81m.ns⁻²", res); +} + +test "Scalar Abs" { + const Meter = Scalar(i128, .{ .L = 1 }, .{}); + const m1 = Meter.splat(-50); + const m2 = m1.abs(); + + try std.testing.expectEqual(50, m2.value()); + + const m_float = Scalar(f32, .{ .L = 1 }, .{}); + const m3 = m_float.splat(-42.5); + try std.testing.expectEqual(42.5, m3.abs().value()); +} + +test "Scalar Pow" { + const Meter = Scalar(i128, .{ .L = 1 }, .{}); + const d = Meter.splat(4); + + const area = d.pow(2); + try std.testing.expectEqual(16, area.value()); + + const volume = d.pow(3); + try std.testing.expectEqual(64, volume.value()); +} + +test "Scalar mul comptime_int" { + const Meter = Scalar(i128, .{ .L = 1 }, .{}); + const d = Meter.splat(7); + + const scaled = d.mul(3); + try std.testing.expectEqual(21, scaled.value()); +} + +test "Scalar add/sub bare number on dimensionless scalar" { + const DimLess = Scalar(i128, .{}, .{}); + const a = DimLess.splat(10); + + const b = a.add(5); + try std.testing.expectEqual(15, b.value()); + + const c = a.sub(3); + try std.testing.expectEqual(7, c.value()); +} + +test "Scalar Imperial length scales" { + const Foot = Scalar(f64, .{ .L = 1 }, .{ .L = .ft }); + const Meter = Scalar(f64, .{ .L = 1 }, .{}); + const Inch = Scalar(f64, .{ .L = 1 }, .{ .L = .inch }); + + const one_ft = Foot.splat(1.0); + try std.testing.expectApproxEqAbs(0.3048, one_ft.to(Meter).value(), 1e-9); + + const twelve_in = Inch.splat(12.0); + try std.testing.expectApproxEqAbs(1.0, twelve_in.to(Foot).value(), 1e-9); +} + +test "Scalar Imperial mass scales" { + const Pound = Scalar(f64, .{ .M = 1 }, .{ .M = .lb }); + const Ounce = Scalar(f64, .{ .M = 1 }, .{ .M = .oz }); + + const two_lb = Pound.splat(2.0); + const eight_oz = Ounce.splat(8.0); + const total = two_lb.add(eight_oz).to(Pound); + try std.testing.expectApproxEqAbs(2.5, total.value(), 1e-6); +} + +test "Scalar comparisons with comptime_int on dimensionless scalar" { + const DimLess = Scalar(i128, .{}, .{}); + const x = DimLess.splat(42); + + try std.testing.expect(x.eq(42)); + try std.testing.expect(x.gt(10)); +} + +// Vector tests + +pub fn Vector(N: comptime_int, Q: type) type { + return Quantity(Q.ValueType, N, Q.dims.argsOpt(), Q.scales.argsOpt()); +} + +test "Vector initiate" { + const Meter = Vector(4, Scalar(f32, .{ .L = 1 }, .{})); + const m = Meter.splat(1); + + try std.testing.expect(m.data[0] == 1); +} + +test "Vector format" { + const MeterPerSecondSq = Scalar(f32, .{ .L = 1, .T = -2 }, .{ .T = .n }); + const KgMeterPerSecond = Scalar(f32, .{ .M = 1, .L = 1, .T = -1 }, .{ .M = .k }); + + const accel = MeterPerSecondSq.Vec3.splat(9.81); + const momentum = KgMeterPerSecond.Vec3{ .data = .{ 43, 0, 11 } }; + + var buf: [64]u8 = undefined; + var res = try std.fmt.bufPrint(&buf, "{d}", .{accel}); + try std.testing.expectEqualStrings("(9.81, 9.81, 9.81)m.ns⁻²", res); + + res = try std.fmt.bufPrint(&buf, "{d:.2}", .{momentum}); + try std.testing.expectEqualStrings("(43.00, 0.00, 11.00)m.kg.s⁻¹", res); +} + +test "Vector Vec3 Init and Basic Arithmetic" { + const Meter = Scalar(i32, .{ .L = 1 }, .{}); + const Vec3M = Meter.Vec3; + + // Test zero, one, initDefault + const v_zero = Vec3M.zero; + try std.testing.expectEqual(0, v_zero.data[0]); + try std.testing.expectEqual(0, v_zero.data[1]); + try std.testing.expectEqual(0, v_zero.data[2]); + + const v_one = Vec3M.one; + try std.testing.expectEqual(1, v_one.data[0]); + try std.testing.expectEqual(1, v_one.data[1]); + try std.testing.expectEqual(1, v_one.data[2]); + + const v_def = Vec3M.splat(5); + try std.testing.expectEqual(5, v_def.data[0]); + try std.testing.expectEqual(5, v_def.data[1]); + try std.testing.expectEqual(5, v_def.data[2]); + + // Test add and sub + const v1 = Vec3M{ .data = .{ 10, 20, 30 } }; + const v2 = Vec3M{ .data = .{ 2, 4, 6 } }; + + const added = v1.add(v2); + try std.testing.expectEqual(12, added.data[0]); + try std.testing.expectEqual(24, added.data[1]); + try std.testing.expectEqual(36, added.data[2]); + + const subbed = v1.sub(v2); + try std.testing.expectEqual(8, subbed.data[0]); + try std.testing.expectEqual(16, subbed.data[1]); + try std.testing.expectEqual(24, subbed.data[2]); + + // Test negate + const neg = v1.negate(); + try std.testing.expectEqual(-10, neg.data[0]); + try std.testing.expectEqual(-20, neg.data[1]); + try std.testing.expectEqual(-30, neg.data[2]); +} + +test "Vector Kinematics (Scalar Mul/Div)" { + const Meter = Scalar(i32, .{ .L = 1 }, .{}); + const Second = Scalar(i32, .{ .T = 1 }, .{}); + const Vec3M = Meter.Vec3; + + const pos = Vec3M{ .data = .{ 100, 200, 300 } }; + const time = Second.splat(10); + + // Vector divided by scalar (Velocity = Position / Time) + const vel = pos.divScalar(time); + try std.testing.expectEqual(10, vel.data[0]); + try std.testing.expectEqual(20, vel.data[1]); + try std.testing.expectEqual(30, vel.data[2]); + try std.testing.expectEqual(1, @TypeOf(vel).dims.get(.L)); + try std.testing.expectEqual(-1, @TypeOf(vel).dims.get(.T)); + + // Vector multiplied by scalar (Position = Velocity * Time) + const new_pos = vel.mulScalar(time); + try std.testing.expectEqual(100, new_pos.data[0]); + try std.testing.expectEqual(200, new_pos.data[1]); + try std.testing.expectEqual(300, new_pos.data[2]); + try std.testing.expectEqual(1, @TypeOf(new_pos).dims.get(.L)); + try std.testing.expectEqual(0, @TypeOf(new_pos).dims.get(.T)); +} + +test "Vector Element-wise Math and Scaling" { + const Meter = Scalar(i32, .{ .L = 1 }, .{}); + const Vec3M = Meter.Vec3; + + const v1 = Vec3M{ .data = .{ 10, 20, 30 } }; + const v2 = Vec3M{ .data = .{ 2, 5, 10 } }; + + // Element-wise division + const div = v1.div(v2); + try std.testing.expectEqual(5, div.data[0]); + try std.testing.expectEqual(4, div.data[1]); + try std.testing.expectEqual(3, div.data[2]); + try std.testing.expectEqual(0, @TypeOf(div).dims.get(.L)); // M / M = Dimensionless +} + +test "Vector Conversions" { + const KiloMeter = Scalar(i32, .{ .L = 1 }, .{ .L = .k }); + const Meter = Scalar(i32, .{ .L = 1 }, .{}); + + const v_km = KiloMeter.Vec3{ .data = .{ 1, 2, 3 } }; + const v_m = v_km.to(Meter); + + try std.testing.expectEqual(1000, v_m.data[0]); + try std.testing.expectEqual(2000, v_m.data[1]); + try std.testing.expectEqual(3000, v_m.data[2]); + + // Type checking the result + try std.testing.expectEqual(1, @TypeOf(v_m).dims.get(.L)); + try std.testing.expectEqual(UnitScale.none, @TypeOf(v_m).scales.get(.L)); +} + +test "Vector Length" { + const MeterInt = Scalar(i32, .{ .L = 1 }, .{}); + const MeterFloat = Scalar(f32, .{ .L = 1 }, .{}); + + // Integer length + // 3-4-5 triangle on XY plane + const v_int = MeterInt.Vec3{ .data = .{ 3, 4, 0 } }; + try std.testing.expectEqual(25, v_int.lengthSqr()); + try std.testing.expectEqual(5, v_int.length()); + + // Float length + const v_float = MeterFloat.Vec3{ .data = .{ 3.0, 4.0, 0.0 } }; + try std.testing.expectApproxEqAbs(@as(f32, 25.0), v_float.lengthSqr(), 1e-4); + try std.testing.expectApproxEqAbs(@as(f32, 5.0), v_float.length(), 1e-4); +} + +test "Vector Comparisons" { + const Meter = Scalar(f32, .{ .L = 1 }, .{}); + const KiloMeter = Scalar(f32, .{ .L = 1 }, .{ .L = .k }); + + const v1 = Meter.Vec3{ .data = .{ 1000.0, 500.0, 0.0 } }; + const v2 = KiloMeter.Vec3{ .data = .{ 1.0, 0.5, 0.0 } }; + const v3 = KiloMeter.Vec3{ .data = .{ 1.0, 0.6, 0.0 } }; + + // 1. Equality (Whole vector) + try std.testing.expect(v1.eqAll(v2)); + try std.testing.expect(v1.neAll(v3)); + + // 2. Element-wise Ordered Comparison + const higher = v3.gt(v1); // compares 1km, 0.6km, 0km vs 1000m, 500m, 0m + try std.testing.expectEqual(false, higher[0]); // 1km == 1000m + try std.testing.expectEqual(true, higher[1]); // 0.6km > 500m + try std.testing.expectEqual(false, higher[2]); // 0 == 0 + + // 3. Element-wise Equal Comparison + const equal = v3.eq(v1); // compares 1km, 0.6km, 0km vs 1000m, 500m, 0m + try std.testing.expectEqual(true, equal[0]); // 1km == 1000m + try std.testing.expectEqual(false, equal[1]); // 0.6km > 500m + try std.testing.expectEqual(true, equal[2]); // 0 == 0 + + // 3. Less than or equal + const low_eq = v1.lte(v3); + try std.testing.expect(low_eq[0] and low_eq[1] and low_eq[2]); +} + +test "Vector vs Scalar Comparisons" { + const Meter = Scalar(f32, .{ .L = 1 }, .{}); + const KiloMeter = Scalar(f32, .{ .L = 1 }, .{ .L = .k }); + + const positions = Meter.Vec3{ .data = .{ 500.0, 1200.0, 3000.0 } }; + const threshold = KiloMeter.splat(1); // 1km (1000m) + + // Check which axes exceed the 1km threshold + const exceeded = positions.gtScalar(threshold); + + try std.testing.expectEqual(false, exceeded[0]); // 500m > 1km is false + try std.testing.expectEqual(true, exceeded[1]); // 1200m > 1km is true + try std.testing.expectEqual(true, exceeded[2]); // 3000m > 1km is true + + // Check for equality (broadcasted) + const exact_match = positions.eqScalar(Meter.splat(500)); + try std.testing.expect(exact_match[0] == true); + try std.testing.expect(exact_match[1] == false); +} + +test "Vector Dot and Cross Products" { + const Meter = Scalar(f32, .{ .L = 1 }, .{}); + const Newton = Scalar(f32, .{ .M = 1, .L = 1, .T = -2 }, .{}); + + const pos = Meter.Vec3{ .data = .{ 10.0, 0.0, 0.0 } }; + const force = Newton.Vec3{ .data = .{ 5.0, 5.0, 0.0 } }; + + // 1. Dot Product (Work = F dot d) + const work = force.dot(pos); + try std.testing.expectEqual(50.0, work.value()); + // Dimensions should be M¹L²T⁻² (Energy/Joules) + try std.testing.expectEqual(1, @TypeOf(work).dims.get(.M)); + try std.testing.expectEqual(2, @TypeOf(work).dims.get(.L)); + try std.testing.expectEqual(-2, @TypeOf(work).dims.get(.T)); + + // 2. Cross Product (Torque = r cross F) + const torque = pos.cross(force); + try std.testing.expectEqual(0.0, torque.data[0]); + try std.testing.expectEqual(0.0, torque.data[1]); + try std.testing.expectEqual(50.0, torque.data[2]); + // Torque dimensions are same as Energy but as a Vector + try std.testing.expectEqual(2, @TypeOf(torque).dims.get(.L)); +} + +test "Vector Abs, Pow, Sqrt and Product" { + const Meter = Scalar(f32, .{ .L = 1 }, .{}); + + const v1 = Meter.Vec3{ .data = .{ -2.0, 3.0, -4.0 } }; + + // 1. Abs + const v_abs = v1.abs(); + try std.testing.expectEqual(2.0, v_abs.data[0]); + try std.testing.expectEqual(4.0, v_abs.data[2]); + + // 2. Product (L1 * L1 * L1 = L3) + const vol = v_abs.product(); + try std.testing.expectEqual(24.0, vol.value()); + try std.testing.expectEqual(3, @TypeOf(vol).dims.get(.L)); + + // 3. Pow (Scalar exponent: (L1)^2 = L2) + const area_vec = v_abs.pow(2); + try std.testing.expectEqual(4.0, area_vec.data[0]); + try std.testing.expectEqual(16.0, area_vec.data[2]); + try std.testing.expectEqual(2, @TypeOf(area_vec).dims.get(.L)); + + // 4. Sqrt + const sqrted = area_vec.sqrt(); + try std.testing.expectEqual(2, sqrted.data[0]); + try std.testing.expectEqual(4, sqrted.data[2]); + try std.testing.expectEqual(1, @TypeOf(sqrted).dims.get(.L)); +} + +test "Vector mulScalar comptime_int" { + const Meter = Scalar(i32, .{ .L = 1 }, .{}); + const v = Meter.Vec3{ .data = .{ 1, 2, 3 } }; + + const scaled = v.mulScalar(10); // comptime_int → dimensionless + try std.testing.expectEqual(10, scaled.data[0]); + try std.testing.expectEqual(20, scaled.data[1]); + try std.testing.expectEqual(30, scaled.data[2]); + // Dimensions unchanged: L¹ × dimensionless = L¹ + try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L)); + try std.testing.expectEqual(0, @TypeOf(scaled).dims.get(.T)); +} + +test "Vector mulScalar comptime_float" { + const MeterF = Scalar(f32, .{ .L = 1 }, .{}); + const v = MeterF.Vec3{ .data = .{ 1.0, 2.0, 4.0 } }; + + const scaled = v.mulScalar(0.5); // comptime_float → dimensionless + try std.testing.expectApproxEqAbs(0.5, scaled.data[0], 1e-6); + try std.testing.expectApproxEqAbs(1.0, scaled.data[1], 1e-6); + try std.testing.expectApproxEqAbs(2.0, scaled.data[2], 1e-6); + try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L)); +} + +test "Vector mulScalar T (value type)" { + const MeterF = Scalar(f32, .{ .L = 1 }, .{}); + const v = MeterF.Vec3{ .data = .{ 3.0, 6.0, 9.0 } }; + const factor: f32 = 2.0; + + const scaled = v.mulScalar(factor); + try std.testing.expectApproxEqAbs(6.0, scaled.data[0], 1e-6); + try std.testing.expectApproxEqAbs(12.0, scaled.data[1], 1e-6); + try std.testing.expectApproxEqAbs(18.0, scaled.data[2], 1e-6); + try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L)); +} + +test "Vector divScalar comptime_int" { + const Meter = Scalar(i32, .{ .L = 1 }, .{}); + const v = Meter.Vec3{ .data = .{ 10, 20, 30 } }; + + const halved = v.divScalar(2); // comptime_int → dimensionless divisor + try std.testing.expectEqual(5, halved.data[0]); + try std.testing.expectEqual(10, halved.data[1]); + try std.testing.expectEqual(15, halved.data[2]); + try std.testing.expectEqual(1, @TypeOf(halved).dims.get(.L)); +} + +test "Vector divScalar comptime_float" { + const MeterF = Scalar(f64, .{ .L = 1 }, .{}); + const v = MeterF.Vec3{ .data = .{ 9.0, 6.0, 3.0 } }; + + const r = v.divScalar(3.0); + try std.testing.expectApproxEqAbs(3.0, r.data[0], 1e-9); + try std.testing.expectApproxEqAbs(2.0, r.data[1], 1e-9); + try std.testing.expectApproxEqAbs(1.0, r.data[2], 1e-9); + try std.testing.expectEqual(1, @TypeOf(r).dims.get(.L)); +} + +test "Vector eqScalar / gtScalar with comptime_int on dimensionless vector" { + // Bare numbers are dimensionless, so comparisons only work when vector is dimensionless too. + const DimLess = Scalar(i32, .{}, .{}); + const v = DimLess.Vec3{ .data = .{ 1, 2, 3 } }; + + const eq_res = v.eqScalar(2); + try std.testing.expectEqual(false, eq_res[0]); + try std.testing.expectEqual(true, eq_res[1]); + try std.testing.expectEqual(false, eq_res[2]); + + const gt_res = v.gtScalar(1); + try std.testing.expectEqual(false, gt_res[0]); + try std.testing.expectEqual(true, gt_res[1]); + try std.testing.expectEqual(true, gt_res[2]); +} diff --git a/src/Scalar.zig b/src/Scalar.zig deleted file mode 100644 index ecc35f6..0000000 --- a/src/Scalar.zig +++ /dev/null @@ -1,817 +0,0 @@ -const std = @import("std"); -const hlp = @import("helper.zig"); - -const Vector = @import("Vector.zig").Vector; -const Scales = @import("Scales.zig"); -const UnitScale = Scales.UnitScale; -const Dimensions = @import("Dimensions.zig"); -const Dimension = Dimensions.Dimension; - -// --------------------------------------------------------------------------- - -/// A dimensioned scalar value. `T` is the numeric type, `d` the dimension exponents, `s` the SI scales. -/// All dimension and unit tracking is resolved at comptime — zero runtime overhead. -pub fn Scalar(comptime T: type, comptime d_opt: Dimensions.ArgOpts, comptime s_opt: Scales.ArgOpts) type { - @setEvalBranchQuota(10_000_000); - return struct { - value: T, - - const Self = @This(); - - /// Type of Vector(3, Self) - pub const Vec3: type = Vector(3, Self); - - /// Type of underline value, mostly use for Vector - pub const ValueType: type = T; - pub const dims: Dimensions = Dimensions.init(d_opt); - pub const scales = Scales.init(s_opt); - - // --------------------------------------------------------------- - // Internal: resolved-rhs shorthands - // --------------------------------------------------------------- - - /// Scalar type that `rhs` normalises to (bare numbers → dimensionless). - inline fn RhsT(comptime Rhs: type) type { - return hlp.rhsScalarType(T, Rhs); - } - - /// Normalise `rhs` (bare number or Scalar) into a proper Scalar value. - inline fn rhs(r: anytype) RhsT(@TypeOf(r)) { - return hlp.toRhsScalar(T, r); - } - - // --------------------------------------------------------------- - // Arithmetic - // --------------------------------------------------------------- - - /// Add two quantities. Dimensions must match — compile error otherwise. - /// Scales are auto-resolved to the finer of the two. - /// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float` - /// (bare numbers are treated as dimensionless). - pub inline fn add(self: Self, r: anytype) Scalar( - T, - dims.argsOpt(), - hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(), - ) { - const rhs_s = rhs(r); - const RhsType = @TypeOf(rhs_s); - if (comptime !dims.eql(RhsType.dims)) - @compileError("Dimension mismatch in add: " ++ dims.str() ++ " vs " ++ RhsType.dims.str()); - if (comptime RhsType == Self) - return .{ .value = self.value + rhs_s.value }; - - const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); - const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value; - const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value; - return .{ .value = if (comptime hlp.isInt(T)) lhs_val +| rhs_val else lhs_val + rhs_val }; - } - - /// Subtract two quantities. Dimensions must match — compile error otherwise. - /// Scales are auto-resolved to the finer of the two. - /// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`. - pub inline fn sub(self: Self, r: anytype) Scalar( - T, - dims.argsOpt(), - hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(), - ) { - const rhs_s = rhs(r); - const RhsType = @TypeOf(rhs_s); - if (comptime !dims.eql(RhsType.dims)) - @compileError("Dimension mismatch in sub: " ++ dims.str() ++ " vs " ++ RhsType.dims.str()); - if (comptime RhsType == Self) - return .{ .value = self.value - rhs_s.value }; - - const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); - const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value; - const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value; - return .{ .value = if (comptime hlp.isInt(T)) lhs_val -| rhs_val else lhs_val - rhs_val }; - } - - /// Multiply two quantities. Dimension exponents are summed: `L¹ * T⁻¹ → L¹T⁻¹`. - /// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float` - /// (bare numbers are treated as dimensionless — dimensions pass through unchanged). - pub inline fn mul(self: Self, r: anytype) Scalar( - T, - dims.add(RhsT(@TypeOf(r)).dims).argsOpt(), - hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(), - ) { - const rhs_s = rhs(r); - const RhsType = @TypeOf(rhs_s); - const SelfNorm = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); - const RhsNorm = Scalar(T, RhsType.dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); - if (comptime Self == SelfNorm and RhsType == RhsNorm) - return .{ .value = self.value * rhs_s.value }; - - const lhs_val = if (comptime Self == SelfNorm) self.value else self.to(SelfNorm).value; - const rhs_val = if (comptime RhsType == RhsNorm) rhs_s.value else rhs_s.to(RhsNorm).value; - return .{ .value = if (comptime hlp.isInt(T)) lhs_val *| rhs_val else lhs_val * rhs_val }; - } - - /// Divide two quantities. Dimension exponents are subtracted: `L¹ / T¹ → L¹T⁻¹`. - /// Integer types use truncating division. - /// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`. - pub inline fn div(self: Self, r: anytype) Scalar( - T, - dims.sub(RhsT(@TypeOf(r)).dims).argsOpt(), - hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(), - ) { - const rhs_s = rhs(r); - const RhsType = @TypeOf(rhs_s); - const SelfNorm = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); - const RhsNorm = Scalar(T, RhsType.dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); - const lhs_val = if (comptime Self == SelfNorm) self.value else self.to(SelfNorm).value; - const rhs_val = if (comptime RhsType == RhsNorm) rhs_s.value else rhs_s.to(RhsNorm).value; - if (comptime hlp.isInt(T)) { - return .{ .value = @divTrunc(lhs_val, rhs_val) }; - } else { - return .{ .value = lhs_val / rhs_val }; - } - } - - // --------------------------------------------------------------- - // Unary - // --------------------------------------------------------------- - - /// Returns the absolute value of the quantity. - /// Dimensions and scales remain entirely unchanged. - pub inline fn abs(self: Self) Self { - if (comptime @typeInfo(T) == .int) - return .{ .value = @intCast(@abs(self.value)) } - else - return .{ .value = @abs(self.value) }; - } - - /// Raises the quantity to a compile-time integer exponent. - /// Dimension exponents are multiplied by the exponent: `(L²)³ → L⁶`. - pub inline fn pow(self: Self, comptime exp: comptime_int) Scalar( - T, - dims.scale(exp).argsOpt(), - scales.argsOpt(), - ) { - if (comptime hlp.isInt(T)) - return .{ .value = std.math.powi(T, self.value, exp) catch std.math.maxInt(T) } - else - return .{ .value = std.math.pow(T, self.value, @as(T, @floatFromInt(exp))) }; - } - - pub inline fn sqrt(self: Self) Scalar( - T, - dims.div(2).argsOpt(), - scales.argsOpt(), - ) { - if (comptime !dims.isSquare()) // Check if all exponents are divisible by 2 - @compileError("Cannot take sqrt of " ++ dims.str() ++ ": exponents must be even."); - if (self.value < 0) return .{ .value = 0 }; - - if (comptime hlp.isInt(T)) { - const UnsignedT = @Int(.unsigned, @typeInfo(T).int.bits); - const u_len_sq = @as(UnsignedT, @intCast(self.value)); - return .{ .value = @as(T, @intCast(std.math.sqrt(u_len_sq))) }; - } else { - return .{ .value = @sqrt(self.value) }; - } - } - - // --------------------------------------------------------------- - // Conversion - // --------------------------------------------------------------- - - /// Convert to a compatible unit type. The scale ratio is computed at comptime. - /// Compile error if dimensions don't match. - pub inline fn to(self: Self, comptime Dest: type) Dest { - if (comptime !dims.eql(Dest.dims)) - @compileError("Dimension mismatch in to: " ++ dims.str() ++ " vs " ++ Dest.dims.str()); - if (comptime @TypeOf(self) == Dest) - return self; - - const DestT = Dest.ValueType; - const ratio = comptime (scales.getFactor(dims) / Dest.scales.getFactor(Dest.dims)); - - // Fast-path: Native pure-integer exact conversions - if (comptime @typeInfo(T) == .int and @typeInfo(DestT) == .int) { - if (comptime ratio >= 1.0 and @round(ratio) == ratio) { - const mult: DestT = comptime @intFromFloat(ratio); - return .{ .value = @as(DestT, @intCast(self.value)) * mult }; - } else if (comptime ratio < 1.0 and @round(1.0 / ratio) == 1.0 / ratio) { - const d: DestT = comptime @intFromFloat(1.0 / ratio); - const val = @as(DestT, @intCast(self.value)); - const half = comptime d / 2; - const rounded = if (val >= 0) @divTrunc(val + half, d) else @divTrunc(val - half, d); - return .{ .value = rounded }; - } - } - - // Fallback preserving native Float types (e.g., f128 shouldn't downcast to f64) - if (comptime @typeInfo(DestT) == .float) { - const val_f = switch (@typeInfo(T)) { - inline .int => @as(DestT, @floatFromInt(self.value)), - inline .float => @as(DestT, @floatCast(self.value)), - else => unreachable, - }; - return .{ .value = val_f * @as(DestT, @floatCast(ratio)) }; - } else { - const val_f = switch (@typeInfo(T)) { - inline .int => @as(f64, @floatFromInt(self.value)), - inline .float => @as(f64, @floatCast(self.value)), - else => unreachable, - }; - return .{ .value = @intFromFloat(@round(val_f * ratio)) }; - } - } - - // --------------------------------------------------------------- - // Comparisons - // --------------------------------------------------------------- - - /// Compares two Scalar for exact equality. - /// Dimensions must match — compile error otherwise. Scales are auto-resolved. - /// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`. - pub inline fn eq(self: Self, r: anytype) bool { - const rhs_s = rhs(r); - const RhsType = @TypeOf(rhs_s); - if (comptime !dims.eql(RhsType.dims)) - @compileError("Dimension mismatch in eq: " ++ dims.str() ++ " vs " ++ RhsType.dims.str()); - if (comptime RhsType == Self) - return self.value == rhs_s.value; - - const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); - const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value; - const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value; - return lhs_val == rhs_val; - } - - /// Compares two quantities for inequality. - /// Dimensions must match — compile error otherwise. Scales are auto-resolved. - /// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`. - pub inline fn ne(self: Self, r: anytype) bool { - const rhs_s = rhs(r); - const RhsType = @TypeOf(rhs_s); - if (comptime !dims.eql(RhsType.dims)) - @compileError("Dimension mismatch in ne: " ++ dims.str() ++ " vs " ++ RhsType.dims.str()); - if (comptime RhsType == Self) - return self.value != rhs_s.value; - - const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); - const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value; - const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value; - return lhs_val != rhs_val; - } - - /// Returns true if this quantity is strictly greater than the right-hand side. - /// Dimensions must match — compile error otherwise. Scales are auto-resolved. - /// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`. - pub inline fn gt(self: Self, r: anytype) bool { - const rhs_s = rhs(r); - const RhsType = @TypeOf(rhs_s); - if (comptime !dims.eql(RhsType.dims)) - @compileError("Dimension mismatch in gt: " ++ dims.str() ++ " vs " ++ RhsType.dims.str()); - if (comptime RhsType == Self) - return self.value > rhs_s.value; - - const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); - const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value; - const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value; - return lhs_val > rhs_val; - } - - /// Returns true if this quantity is greater than or equal to the right-hand side. - /// Dimensions must match — compile error otherwise. Scales are auto-resolved. - /// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`. - pub inline fn gte(self: Self, r: anytype) bool { - const rhs_s = rhs(r); - const RhsType = @TypeOf(rhs_s); - if (comptime !dims.eql(RhsType.dims)) - @compileError("Dimension mismatch in gte: " ++ dims.str() ++ " vs " ++ RhsType.dims.str()); - if (comptime RhsType == Self) - return self.value >= rhs_s.value; - - const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); - const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value; - const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value; - return lhs_val >= rhs_val; - } - - /// Returns true if this quantity is strictly less than the right-hand side. - /// Dimensions must match — compile error otherwise. Scales are auto-resolved. - /// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`. - pub inline fn lt(self: Self, r: anytype) bool { - const rhs_s = rhs(r); - const RhsType = @TypeOf(rhs_s); - if (comptime !dims.eql(RhsType.dims)) - @compileError("Dimension mismatch in lt: " ++ dims.str() ++ " vs " ++ RhsType.dims.str()); - if (comptime RhsType == Self) - return self.value < rhs_s.value; - - const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); - const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value; - const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value; - return lhs_val < rhs_val; - } - - /// Returns true if this quantity is less than or equal to the right-hand side. - /// Dimensions must match — compile error otherwise. Scales are auto-resolved. - /// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`. - pub inline fn lte(self: Self, r: anytype) bool { - const rhs_s = rhs(r); - const RhsType = @TypeOf(rhs_s); - if (comptime !dims.eql(RhsType.dims)) - @compileError("Dimension mismatch in lte: " ++ dims.str() ++ " vs " ++ RhsType.dims.str()); - if (comptime RhsType == Self) - return self.value <= rhs_s.value; - - const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); - const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value; - const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value; - return lhs_val <= rhs_val; - } - - // --------------------------------------------------------------- - // Vector helpers - // --------------------------------------------------------------- - - /// Return a `Vector(len, Self)` type. - pub fn Vec(_: Self, comptime len: comptime_int) type { - return Vector(len, Self); - } - - /// Return a `Vector(len, Self)` with all components set to this value. - pub fn vec(self: Self, comptime len: comptime_int) Vector(len, Self) { - return Vector(len, Self).initDefault(self.value); - } - - /// Shorthand for `Vec(3)` — wrap this value into a 3-component vector. - pub fn vec3(self: Self) Vec3 { - return Vec3.initDefault(self.value); - } - - // --------------------------------------------------------------- - // Formatting - // --------------------------------------------------------------- - - pub fn formatNumber( - self: Self, - writer: *std.Io.Writer, - options: std.fmt.Number, - ) !void { - switch (@typeInfo(T)) { - .float, .comptime_float => try writer.printFloat(self.value, options), - .int, .comptime_int => try writer.printInt(self.value, 10, .lower, .{ - .width = options.width, - .alignment = options.alignment, - .fill = options.fill, - .precision = options.precision, - }), - else => unreachable, - } - var first = true; - inline for (std.enums.values(Dimension)) |bu| { - const v = dims.get(bu); - if (comptime v == 0) continue; - if (!first) - try writer.writeAll("."); - - first = false; - - const uscale = scales.get(bu); - if (bu == .T and (uscale == .min or uscale == .hour or uscale == .year)) - try writer.print("{s}", .{uscale.str()}) - else - try writer.print("{s}{s}", .{ uscale.str(), bu.unit() }); - - if (v != 1) - try hlp.printSuperscript(writer, v); - } - } - }; -} - -test "Generate quantity" { - const Meter = Scalar(i128, .{ .L = 1 }, .{ .L = @enumFromInt(-3) }); - const Second = Scalar(f32, .{ .T = 1 }, .{ .T = .n }); - - const distance = Meter{ .value = 10 }; - const time = Second{ .value = 2 }; - - try std.testing.expectEqual(10, distance.value); - try std.testing.expectEqual(2, time.value); -} - -test "Comparisons (eq, ne, gt, gte, lt, lte)" { - const Meter = Scalar(i128, .{ .L = 1 }, .{}); - const KiloMeter = Scalar(i128, .{ .L = 1 }, .{ .L = .k }); - - const m1000 = Meter{ .value = 1000 }; - const km1 = KiloMeter{ .value = 1 }; - const km2 = KiloMeter{ .value = 2 }; - - // Equal / Not Equal - try std.testing.expect(m1000.eq(km1)); - try std.testing.expect(km1.eq(m1000)); - try std.testing.expect(km2.ne(m1000)); - - // Greater Than / Greater Than or Equal - try std.testing.expect(km2.gt(m1000)); - try std.testing.expect(km2.gt(km1)); - try std.testing.expect(km1.gte(m1000)); - try std.testing.expect(km2.gte(m1000)); - - // Less Than / Less Than or Equal - try std.testing.expect(m1000.lt(km2)); - try std.testing.expect(km1.lt(km2)); - try std.testing.expect(km1.lte(m1000)); - try std.testing.expect(m1000.lte(km2)); -} - -test "Add" { - const Meter = Scalar(i128, .{ .L = 1 }, .{}); - - const distance = Meter{ .value = 10 }; - const distance2 = Meter{ .value = 20 }; - - const added = distance.add(distance2); - try std.testing.expectEqual(30, added.value); - try std.testing.expectEqual(1, @TypeOf(added).dims.get(.L)); - - const KiloMeter = Scalar(i128, .{ .L = 1 }, .{ .L = .k }); - const distance3 = KiloMeter{ .value = 2 }; - const added2 = distance.add(distance3); - try std.testing.expectEqual(2010, added2.value); - try std.testing.expectEqual(1, @TypeOf(added2).dims.get(.L)); - - const added3 = distance3.add(distance).to(KiloMeter); - try std.testing.expectEqual(2, added3.value); - try std.testing.expectEqual(1, @TypeOf(added3).dims.get(.L)); - - const KiloMeter_f = Scalar(f64, .{ .L = 1 }, .{ .L = .k }); - const distance4 = KiloMeter_f{ .value = 2 }; - const added4 = distance4.add(distance).to(KiloMeter_f); - try std.testing.expectApproxEqAbs(2.01, added4.value, 0.000001); - try std.testing.expectEqual(1, @TypeOf(added4).dims.get(.L)); -} - -test "Sub" { - const Meter = Scalar(i128, .{ .L = 1 }, .{}); - const KiloMeter_f = Scalar(f64, .{ .L = 1 }, .{ .L = .k }); - - const a = Meter{ .value = 500 }; - const b = Meter{ .value = 200 }; - const diff = a.sub(b); - try std.testing.expectEqual(300, diff.value); - const diff2 = b.sub(a); - try std.testing.expectEqual(-300, diff2.value); - - const km_f = KiloMeter_f{ .value = 2.5 }; - const m_f = Meter{ .value = 500 }; - const diff3 = km_f.sub(m_f); - try std.testing.expectApproxEqAbs(2000, diff3.value, 1e-4); -} - -test "MulBy" { - const Meter = Scalar(i128, .{ .L = 1 }, .{}); - const Second = Scalar(f32, .{ .T = 1 }, .{}); - - const d = Meter{ .value = 3.0 }; - const t = Second{ .value = 4.0 }; - - const area_time = d.mul(t); - try std.testing.expectEqual(12, area_time.value); - try std.testing.expectEqual(1, @TypeOf(area_time).dims.get(.L)); - try std.testing.expectEqual(1, @TypeOf(area_time).dims.get(.T)); - - const d2 = Meter{ .value = 5.0 }; - const area = d.mul(d2); - try std.testing.expectEqual(15, area.value); - try std.testing.expectEqual(2, @TypeOf(area).dims.get(.L)); - try std.testing.expectEqual(0, @TypeOf(area).dims.get(.T)); -} - -test "MulBy with scale" { - const KiloMeter = Scalar(f32, .{ .L = 1 }, .{ .L = .k }); - const KiloGram = Scalar(f32, .{ .M = 1 }, .{ .M = .k }); - - const dist = KiloMeter{ .value = 2.0 }; - const mass = KiloGram{ .value = 3.0 }; - const prod = dist.mul(mass); - try std.testing.expectEqual(1, @TypeOf(prod).dims.get(.L)); - try std.testing.expectEqual(1, @TypeOf(prod).dims.get(.M)); -} - -test "MulBy with type change" { - const Meter = Scalar(i128, .{ .L = 1 }, .{ .L = .k }); - const Second = Scalar(f64, .{ .T = 1 }, .{}); - const KmSec = Scalar(i64, .{ .L = 1, .T = 1 }, .{ .L = .k }); - const KmSec_f = Scalar(f32, .{ .L = 1, .T = 1 }, .{ .L = .k }); - - const d = Meter{ .value = 3.0 }; - const t = Second{ .value = 4.0 }; - - const area_time = d.mul(t).to(KmSec); - const area_time_f = d.mul(t).to(KmSec_f); - try std.testing.expectEqual(12, area_time.value); - try std.testing.expectApproxEqAbs(12, area_time_f.value, 0.0001); - try std.testing.expectEqual(1, @TypeOf(area_time).dims.get(.L)); - try std.testing.expectEqual(1, @TypeOf(area_time).dims.get(.T)); -} - -test "MulBy small" { - const Meter = Scalar(i128, .{ .L = 1 }, .{ .L = .n }); - const Second = Scalar(f32, .{ .T = 1 }, .{}); - - const d = Meter{ .value = 3.0 }; - const t = Second{ .value = 4.0 }; - - const area_time = d.mul(t); - try std.testing.expectEqual(12, area_time.value); - try std.testing.expectEqual(1, @TypeOf(area_time).dims.get(.L)); - try std.testing.expectEqual(1, @TypeOf(area_time).dims.get(.T)); -} - -test "MulBy dimensionless" { - const DimLess = Scalar(i128, .{}, .{}); - const Meter = Scalar(i128, .{ .L = 1 }, .{}); - - const d = Meter{ .value = 7 }; - const scaled = d.mul(DimLess{ .value = 3 }); - try std.testing.expectEqual(21, scaled.value); - try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L)); -} - -test "Sqrt" { - const MeterSquare = Scalar(i128, .{ .L = 2 }, .{}); - - var d = MeterSquare{ .value = 9 }; - var scaled = d.sqrt(); - try std.testing.expectEqual(3, scaled.value); - try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L)); - - d = MeterSquare{ .value = -5 }; - scaled = d.sqrt(); - try std.testing.expectEqual(0, scaled.value); - try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L)); - - const MeterSquare_f = Scalar(f64, .{ .L = 2 }, .{}); - const d2 = MeterSquare_f{ .value = 20 }; - const scaled2 = d2.sqrt(); - try std.testing.expectApproxEqAbs(4.472135955, scaled2.value, 1e-4); - try std.testing.expectEqual(1, @TypeOf(scaled2).dims.get(.L)); -} - -test "Chained: velocity and acceleration" { - const Meter = Scalar(i128, .{ .L = 1 }, .{}); - const Second = Scalar(f32, .{ .T = 1 }, .{}); - - const dist = Meter{ .value = 100.0 }; - const t1 = Second{ .value = 5.0 }; - const velocity = dist.div(t1); - try std.testing.expectEqual(20, velocity.value); - try std.testing.expectEqual(1, @TypeOf(velocity).dims.get(.L)); - try std.testing.expectEqual(-1, @TypeOf(velocity).dims.get(.T)); - - const t2 = Second{ .value = 4.0 }; - const accel = velocity.div(t2); - try std.testing.expectEqual(5, accel.value); - try std.testing.expectEqual(1, @TypeOf(accel).dims.get(.L)); - try std.testing.expectEqual(-2, @TypeOf(accel).dims.get(.T)); -} - -test "DivBy integer exact" { - const Meter = Scalar(i128, .{ .L = 1 }, .{}); - const Second = Scalar(f32, .{ .T = 1 }, .{}); - - const dist = Meter{ .value = 120 }; - const time = Second{ .value = 4 }; - const vel = dist.div(time); - - try std.testing.expectEqual(30, vel.value); - try std.testing.expectEqual(1, @TypeOf(vel).dims.get(.L)); - try std.testing.expectEqual(-1, @TypeOf(vel).dims.get(.T)); -} - -test "Finer scales skip dim 0" { - const Dimless = Scalar(i128, .{}, .{}); - const KiloMetre = Scalar(i128, .{ .L = 1 }, .{ .L = .k }); - - const r = Dimless{ .value = 30 }; - const time = KiloMetre{ .value = 4 }; - const vel = r.mul(time); - - try std.testing.expectEqual(120, vel.value); - try std.testing.expectEqual(Scales.UnitScale.k, @TypeOf(vel).scales.get(.L)); -} - -test "Conversion chain: km -> m -> cm" { - const KiloMeter = Scalar(i128, .{ .L = 1 }, .{ .L = .k }); - const Meter = Scalar(i128, .{ .L = 1 }, .{}); - const CentiMeter = Scalar(i128, .{ .L = 1 }, .{ .L = .c }); - - const km = KiloMeter{ .value = 15 }; - const m = km.to(Meter); - const cm = m.to(CentiMeter); - - try std.testing.expectEqual(15_000, m.value); - try std.testing.expectEqual(1_500_000, cm.value); -} - -test "Conversion: hours -> minutes -> seconds" { - const Hour = Scalar(i128, .{ .T = 1 }, .{ .T = .hour }); - const Minute = Scalar(i128, .{ .T = 1 }, .{ .T = .min }); - const Second = Scalar(i128, .{ .T = 1 }, .{}); - - const h = Hour{ .value = 1.0 }; - const min = h.to(Minute); - const sec = min.to(Second); - - try std.testing.expectEqual(60, min.value); - try std.testing.expectEqual(3600, sec.value); -} - -test "Negative values" { - const Meter = Scalar(i128, .{ .L = 1 }, .{}); - - const a = Meter{ .value = 5 }; - const b = Meter{ .value = 20 }; - const diff = a.sub(b); - try std.testing.expectEqual(-15, diff.value); -} - -test "Format Scalar" { - const MeterPerSecondSq = Scalar(f32, .{ .L = 1, .T = -2 }, .{ .T = .n }); - const KgMeterPerSecond = Scalar(f32, .{ .M = 1, .L = 1, .T = -1 }, .{ .M = .k }); - const Meter = Scalar(f32, .{ .L = 1 }, .{}); - - const m = Meter{ .value = 1.23456 }; - const accel = MeterPerSecondSq{ .value = 9.81 }; - const momentum = KgMeterPerSecond{ .value = 42.0 }; - - var buf: [64]u8 = undefined; - var res = try std.fmt.bufPrint(&buf, "{d:.2}", .{m}); - try std.testing.expectEqualStrings("1.23m", res); - - res = try std.fmt.bufPrint(&buf, "{d}", .{accel}); - try std.testing.expectEqualStrings("9.81m.ns⁻²", res); - - res = try std.fmt.bufPrint(&buf, "{d}", .{momentum}); - try std.testing.expectEqualStrings("42m.kg.s⁻¹", res); - - res = try std.fmt.bufPrint(&buf, "{d:_>10.1}", .{m}); - try std.testing.expectEqualStrings("_______1.2m", res); -} - -test "Abs" { - const Meter = Scalar(i128, .{ .L = 1 }, .{}); - const m1 = Meter{ .value = -50 }; - const m2 = m1.abs(); - - try std.testing.expectEqual(50, m2.value); - try std.testing.expectEqual(1, @TypeOf(m2).dims.get(.L)); - - const m_float = Scalar(f32, .{ .L = 1 }, .{}); - const m3 = m_float{ .value = -42.5 }; - try std.testing.expectEqual(42.5, m3.abs().value); -} - -test "Pow" { - const Meter = Scalar(i128, .{ .L = 1 }, .{}); - const d = Meter{ .value = 4 }; - - const area = d.pow(2); - try std.testing.expectEqual(16, area.value); - try std.testing.expectEqual(2, @TypeOf(area).dims.get(.L)); - - const volume = d.pow(3); - try std.testing.expectEqual(64, volume.value); - try std.testing.expectEqual(3, @TypeOf(volume).dims.get(.L)); - - // Float test - const MeterF = Scalar(f32, .{ .L = 1 }, .{}); - const d_f = MeterF{ .value = 2.0 }; - const area_f = d_f.pow(3); - try std.testing.expectEqual(8.0, area_f.value); - try std.testing.expectEqual(3, @TypeOf(area_f).dims.get(.L)); -} - -test "mul comptime_int" { - const Meter = Scalar(i128, .{ .L = 1 }, .{}); - const d = Meter{ .value = 7 }; - - const scaled = d.mul(3); // comptime_int → dimensionless - try std.testing.expectEqual(21, scaled.value); - try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L)); - try std.testing.expectEqual(0, @TypeOf(scaled).dims.get(.T)); -} - -test "mul comptime_float" { - const MeterF = Scalar(f64, .{ .L = 1 }, .{}); - const d = MeterF{ .value = 4.0 }; - - const scaled = d.mul(2.5); // comptime_float → dimensionless - try std.testing.expectApproxEqAbs(10.0, scaled.value, 1e-9); - try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L)); -} - -test "mul T (value type)" { - const MeterF = Scalar(f32, .{ .L = 1 }, .{}); - const d = MeterF{ .value = 6.0 }; - const factor: f32 = 0.5; - - const scaled = d.mul(factor); // bare f32 → dimensionless - try std.testing.expectApproxEqAbs(3.0, scaled.value, 1e-6); - try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L)); -} - -test "div comptime_int" { - const Meter = Scalar(i128, .{ .L = 1 }, .{}); - const d = Meter{ .value = 100 }; - - const half = d.div(4); // comptime_int → dimensionless divisor - try std.testing.expectEqual(25, half.value); - try std.testing.expectEqual(1, @TypeOf(half).dims.get(.L)); -} - -test "div comptime_float" { - const MeterF = Scalar(f64, .{ .L = 1 }, .{}); - const d = MeterF{ .value = 9.0 }; - - const r = d.div(3.0); - try std.testing.expectApproxEqAbs(3.0, r.value, 1e-9); - try std.testing.expectEqual(1, @TypeOf(r).dims.get(.L)); -} - -test "add/sub bare number on dimensionless scalar" { - // Bare numbers are dimensionless, so add/sub only works when Self is also dimensionless. - const DimLess = Scalar(i128, .{}, .{}); - const a = DimLess{ .value = 10 }; - - const b = a.add(5); // comptime_int, both dimensionless → ok - try std.testing.expectEqual(15, b.value); - - const c = a.sub(3); - try std.testing.expectEqual(7, c.value); -} - -test "Imperial length scales" { - const Foot = Scalar(f64, .{ .L = 1 }, .{ .L = .ft }); - const Meter = Scalar(f64, .{ .L = 1 }, .{}); - const Inch = Scalar(f64, .{ .L = 1 }, .{ .L = .inch }); - const CentiMeter = Scalar(f64, .{ .L = 1 }, .{ .L = .c }); - const Mile = Scalar(f64, .{ .L = 1 }, .{ .L = .mi }); - const KiloMeter = Scalar(f64, .{ .L = 1 }, .{ .L = .k }); - const Yard = Scalar(f64, .{ .L = 1 }, .{ .L = .yd }); - - // 1 ft → 0.3048 m - const one_ft = Foot{ .value = 1.0 }; - try std.testing.expectApproxEqAbs(0.3048, one_ft.to(Meter).value, 1e-9); - - // 12 in → 1 ft - const twelve_in = Inch{ .value = 12.0 }; - try std.testing.expectApproxEqAbs(1.0, twelve_in.to(Foot).value, 1e-9); - - // 1 in → 2.54 cm - const one_in = Inch{ .value = 1.0 }; - try std.testing.expectApproxEqAbs(2.54, one_in.to(CentiMeter).value, 1e-9); - - // 1 mi → 1.609344 km - const one_mi = Mile{ .value = 1.0 }; - try std.testing.expectApproxEqAbs(1.609344, one_mi.to(KiloMeter).value, 1e-9); - - // 3 ft → 1 yd - const three_ft = Foot{ .value = 3.0 }; - try std.testing.expectApproxEqAbs(1.0, three_ft.to(Yard).value, 1e-9); -} - -test "Imperial mass scales" { - const Pound = Scalar(f64, .{ .M = 1 }, .{ .M = .lb }); - const KiloGram = Scalar(f64, .{ .M = 1 }, .{ .M = .k }); - const Ounce = Scalar(f64, .{ .M = 1 }, .{ .M = .oz }); - const Stone = Scalar(f64, .{ .M = 1 }, .{ .M = .st }); - - // 1 lb → ~0.453592 kg - const one_lb = Pound{ .value = 1.0 }; - try std.testing.expectApproxEqAbs(0.45359237, one_lb.to(KiloGram).value, 1e-6); - - // 16 oz → 1 lb - const sixteen_oz = Ounce{ .value = 16.0 }; - try std.testing.expectApproxEqAbs(1.0, sixteen_oz.to(Pound).value, 1e-6); - - // 1 stone → 14 lb - const one_st = Stone{ .value = 1.0 }; - try std.testing.expectApproxEqAbs(14.0, one_st.to(Pound).value, 1e-4); - - // 2 lb + 8 oz → 2.5 lb - const two_lb = Pound{ .value = 2.0 }; - const eight_oz = Ounce{ .value = 8.0 }; - const total = two_lb.add(eight_oz).to(Pound); - try std.testing.expectApproxEqAbs(2.5, total.value, 1e-6); -} - -test "comparisons with comptime_int on dimensionless scalar" { - const DimLess = Scalar(i128, .{}, .{}); - const x = DimLess{ .value = 42 }; - - try std.testing.expect(x.eq(42)); - try std.testing.expect(x.ne(0)); - try std.testing.expect(x.gt(10)); - try std.testing.expect(x.gte(42)); - try std.testing.expect(x.lt(100)); - try std.testing.expect(x.lte(42)); -} diff --git a/src/Vector.zig b/src/Vector.zig deleted file mode 100644 index 2e2bdee..0000000 --- a/src/Vector.zig +++ /dev/null @@ -1,830 +0,0 @@ -const std = @import("std"); -const hlp = @import("helper.zig"); - -const Scalar = @import("Scalar.zig").Scalar; -const Scales = @import("Scales.zig"); -const UnitScale = Scales.UnitScale; -const Dimensions = @import("Dimensions.zig"); -const Dimension = Dimensions.Dimension; - -/// A fixed-size array of `len` elements sharing the same dimension and scale as scalar type `Q`. -pub fn Vector(comptime len: usize, comptime Q: type) type { - const T = Q.ValueType; - - return struct { - data: [len]T, - - const Self = @This(); - pub const ScalarType = Q; - pub const ValueType = T; - pub const dims: Dimensions = Q.dims; - pub const scales = Q.scales; - - pub const zero = initDefault(0); - pub const one = initDefault(1); - - pub fn initDefault(v: T) Self { - var data: [len]T = undefined; - inline for (&data) |*item| item.* = v; - return .{ .data = data }; - } - - // ------------------------------------------------------------------- - // Internal: scalar-rhs normalisation (mirrors Scalar.zig) - // ------------------------------------------------------------------- - - /// Resolved Scalar type for a scalar operand (bare number or Scalar). - /// Passing another Vector here is a compile error. - inline fn ScalarRhsT(comptime Rhs: type) type { - if (comptime switch (@typeInfo(Rhs)) { - .@"struct", .@"enum", .@"union", .@"opaque" => @hasDecl(Rhs, "ScalarType"), - else => false, - }) - @compileError( - "Expected a Scalar or bare number; got a Vector. " ++ - "Use mul / div for element-wise vector operations.", - ); - return hlp.rhsScalarType(T, Rhs); - } - - /// Normalise a scalar rhs (bare number → dimensionless Scalar). - inline fn scalarRhs(r: anytype) ScalarRhsT(@TypeOf(r)) { - return hlp.toRhsScalar(T, r); - } - - // ------------------------------------------------------------------- - // Vector–Vector operations (rhs must be a Vector of the same length) - // ------------------------------------------------------------------- - - /// Element-wise addition. Dimensions must match; scales resolve to the finer of the two. - pub inline fn add(self: Self, rhs: anytype) Vector(len, Scalar( - T, - dims.argsOpt(), - hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(), - )) { - const Tr = @TypeOf(rhs); - var res: Vector(len, Scalar( - T, - dims.argsOpt(), - hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(), - )) = undefined; - inline for (self.data, 0..) |v, i| { - const q = (Q{ .value = v }).add(Tr.ScalarType{ .value = rhs.data[i] }); - res.data[i] = q.value; - } - return res; - } - /// Element-wise subtraction. Dimensions must match; scales resolve to the finer of the two. - pub inline fn sub(self: Self, rhs: anytype) Vector(len, Scalar( - T, - dims.argsOpt(), - hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(), - )) { - const Tr = @TypeOf(rhs); - var res: Vector(len, Scalar( - T, - dims.argsOpt(), - hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(), - )) = undefined; - inline for (self.data, 0..) |v, i| { - const q = (Q{ .value = v }).sub(Tr.ScalarType{ .value = rhs.data[i] }); - res.data[i] = q.value; - } - return res; - } - - /// Element-wise division. Dimension exponents are subtracted per component. - pub inline fn div( - self: Self, - rhs: anytype, - ) Vector(len, Scalar( - T, - dims.sub(@TypeOf(rhs).dims).argsOpt(), - hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(), - )) { - const Tr = @TypeOf(rhs); - var res: Vector(len, Scalar( - T, - dims.sub(Tr.dims).argsOpt(), - hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(), - )) = undefined; - inline for (self.data, 0..) |v, i| { - const q = (Q{ .value = v }).div(Tr.ScalarType{ .value = rhs.data[i] }); - res.data[i] = q.value; - } - return res; - } - - /// Element-wise multiplication. Dimension exponents are summed per component. - pub inline fn mul( - self: Self, - rhs: anytype, - ) Vector(len, Scalar( - T, - dims.add(@TypeOf(rhs).dims).argsOpt(), - hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(), - )) { - const Tr = @TypeOf(rhs); - var res: Vector(len, Scalar( - T, - dims.add(Tr.dims).argsOpt(), - hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(), - )) = undefined; - inline for (self.data, 0..) |v, i| { - const q = (Q{ .value = v }).mul(Tr.ScalarType{ .value = rhs.data[i] }); - res.data[i] = q.value; - } - return res; - } - - // ------------------------------------------------------------------- - // Vector–Scalar operations - // scalar may be: Scalar, T, comptime_int, comptime_float - // ------------------------------------------------------------------- - - /// Divide every component by a single scalar. Dimensions are subtracted. - /// `scalar` may be a Scalar, `T`, `comptime_int`, or `comptime_float`. - pub inline fn divScalar( - self: Self, - scalar: anytype, - ) Vector(len, Scalar( - T, - dims.sub(ScalarRhsT(@TypeOf(scalar)).dims).argsOpt(), - hlp.finerScales(Self, ScalarRhsT(@TypeOf(scalar))).argsOpt(), - )) { - const s_norm = scalarRhs(scalar); - const SN = @TypeOf(s_norm); - var res: Vector(len, Scalar( - T, - dims.sub(SN.dims).argsOpt(), - hlp.finerScales(Self, SN).argsOpt(), - )) = undefined; - inline for (self.data, 0..) |v, i| - res.data[i] = (Q{ .value = v }).div(s_norm).value; - return res; - } - - /// Multiply every component by a single scalar. Dimensions are summed. - /// `scalar` may be a Scalar, `T`, `comptime_int`, or `comptime_float`. - pub inline fn mulScalar( - self: Self, - scalar: anytype, - ) Vector(len, Scalar( - T, - dims.add(ScalarRhsT(@TypeOf(scalar)).dims).argsOpt(), - hlp.finerScales(Self, ScalarRhsT(@TypeOf(scalar))).argsOpt(), - )) { - const s_norm = scalarRhs(scalar); - const SN = @TypeOf(s_norm); - var res: Vector(len, Scalar( - T, - dims.add(SN.dims).argsOpt(), - hlp.finerScales(Self, SN).argsOpt(), - )) = undefined; - inline for (self.data, 0..) |v, i| - res.data[i] = (Q{ .value = v }).mul(s_norm).value; - return res; - } - - // ------------------------------------------------------------------- - // Dot / Cross - // ------------------------------------------------------------------- - - /// Standard dot product. Dimensions are summed (e.g., Force * Distance = Energy). - /// Returns a Scalar type with the combined dimensions and finest scale. - pub inline fn dot(self: Self, rhs: anytype) Scalar( - T, - dims.add(@TypeOf(rhs).dims).argsOpt(), - hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(), - ) { - const Tr = @TypeOf(rhs); - - var sum: T = 0; - inline for (self.data, 0..) |v, i| { - const q_lhs = Q{ .value = v }; - const q_rhs = Tr.ScalarType{ .value = rhs.data[i] }; - sum += q_lhs.mul(q_rhs).value; - } - return .{ .value = sum }; - } - - /// 3D Cross product. Dimensions are summed. - /// Only valid for vectors of length 3. - pub inline fn cross(self: Self, rhs: anytype) Vector(3, Scalar( - T, - dims.add(@TypeOf(rhs).dims).argsOpt(), - hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(), - )) { - if (comptime len != 3) - @compileError("Cross product is only defined for Vector(3, ...)"); - - const Tr = @TypeOf(rhs); - const ResScalar = Scalar(T, dims.add(Tr.dims).argsOpt(), hlp.finerScales(Self, Tr).argsOpt()); - const ResVec = Vector(3, ResScalar); - - // Calculation: [y1*z2 - z1*y2, z1*x2 - x1*z2, x1*y2 - y1*x2] - const s1 = Q{ .value = self.data[0] }; - const s2 = Q{ .value = self.data[1] }; - const s3 = Q{ .value = self.data[2] }; - - const o1 = Tr.ScalarType{ .value = rhs.data[0] }; - const o2 = Tr.ScalarType{ .value = rhs.data[1] }; - const o3 = Tr.ScalarType{ .value = rhs.data[2] }; - - return ResVec{ - .data = .{ - s2.mul(o3).sub(s3.mul(o2)).value, - s3.mul(o1).sub(s1.mul(o3)).value, - s1.mul(o2).sub(s2.mul(o1)).value, - }, - }; - } - - // ------------------------------------------------------------------- - // Unary - // ------------------------------------------------------------------- - - /// Returns a vector where each component is the absolute value of the original. - pub inline fn abs(self: Self) Self { - var res: Self = undefined; - inline for (self.data, 0..) |v, i| { - const q = Q{ .value = v }; - res.data[i] = q.abs().value; - } - return res; - } - - /// Returns a vector where each component is the absolute value of the original. - pub inline fn sqrt(self: Self) Self { - var res: Self = undefined; - inline for (self.data, 0..) |v, i| { - const q = Q{ .value = v }; - res.data[i] = q.sqrt().value; - } - return res; - } - - /// Multiplies all components of the vector together. - /// Resulting dimensions are (Original Dims * len). - pub inline fn product(self: Self) Scalar( - T, - dims.scale(len).argsOpt(), - scales.argsOpt(), - ) { - var res_val: T = 1; - if (comptime hlp.isInt(T)) { - inline for (self.data) |v| - res_val = res_val *| v; - } else inline for (self.data) |v| - res_val *= v; - return .{ .value = res_val }; - } - - /// Raises every component to a compile-time integer power. - /// Dimensions are scaled by the exponent. - pub inline fn pow(self: Self, comptime exp: comptime_int) Vector( - len, - Scalar( - T, - dims.scale(exp).argsOpt(), - scales.argsOpt(), - ), - ) { - const ResScalar = Scalar(T, dims.scale(exp).argsOpt(), scales.argsOpt()); - var res: Vector(len, ResScalar) = undefined; - inline for (self.data, 0..) |v, i| { - const q = Q{ .value = v }; - res.data[i] = q.pow(exp).value; - } - return res; - } - - /// Negate all components. Dimensions are preserved. - pub fn negate(self: Self) Self { - var res: Self = undefined; - inline for (self.data, 0..) |v, i| - res.data[i] = -v; - return res; - } - - // ------------------------------------------------------------------- - // Conversion - // ------------------------------------------------------------------- - - /// Convert all components to a compatible scalar type. Compile error on dimension mismatch. - pub inline fn to(self: Self, comptime DestQ: type) Vector(len, DestQ) { - var res: Vector(len, DestQ) = undefined; - inline for (self.data, 0..) |v, i| - res.data[i] = (Q{ .value = v }).to(DestQ).value; - return res; - } - - // ------------------------------------------------------------------- - // Length - // ------------------------------------------------------------------- - - /// Sum of squared components. Cheaper than `length` — use for comparisons. - pub inline fn lengthSqr(self: Self) T { - var sum: T = 0; - inline for (self.data) |v| - sum += v * v; - return sum; - } - - /// Euclidean length. Integer types use integer sqrt (truncated). - pub inline fn length(self: Self) T { - const len_sq = self.lengthSqr(); - - if (comptime @typeInfo(T) == .int) { - const UnsignedT = @Int(.unsigned, @typeInfo(T).int.bits); - const u_len_sq = @as(UnsignedT, @intCast(len_sq)); - return @as(T, @intCast(std.math.sqrt(u_len_sq))); - } else { - return @sqrt(len_sq); - } - } - - // ------------------------------------------------------------------- - // Vector–Vector comparisons - // ------------------------------------------------------------------- - - /// Returns true only if all components are equal after scale resolution. - pub inline fn eqAll(self: Self, rhs: anytype) bool { - const Tr = @TypeOf(rhs); - if (comptime !dims.eql(Tr.dims)) - @compileError("Dimension mismatch in eq: " ++ dims.str() ++ " vs " ++ Tr.dims.str()); - - inline for (self.data, 0..) |v, i| { - const lhs_q = Q{ .value = v }; - const rhs_q = Tr.ScalarType{ .value = rhs.data[i] }; - if (!lhs_q.eq(rhs_q)) return false; - } - return true; - } - - /// Returns true if any component differs after scale resolution. - pub inline fn neAll(self: Self, rhs: anytype) bool { - return !self.eqAll(rhs); - } - - /// Element-wise "Equal". Returns an array of booleans. - pub inline fn eq(self: Self, rhs: anytype) [len]bool { - const Tr = @TypeOf(rhs); - var res: [len]bool = undefined; - inline for (self.data, 0..) |v, i| - res[i] = (Q{ .value = v }).eq(Tr.ScalarType{ .value = rhs.data[i] }); - return res; - } - - /// Element-wise "Not Equal". Returns an array of booleans. - pub inline fn ne(self: Self, rhs: anytype) [len]bool { - const Tr = @TypeOf(rhs); - var res: [len]bool = undefined; - inline for (self.data, 0..) |v, i| - res[i] = (Q{ .value = v }).ne(Tr.ScalarType{ .value = rhs.data[i] }); - return res; - } - - /// Element-wise "Greater Than". Returns an array of booleans. - pub inline fn gt(self: Self, rhs: anytype) [len]bool { - const Tr = @TypeOf(rhs); - var res: [len]bool = undefined; - inline for (self.data, 0..) |v, i| - res[i] = (Q{ .value = v }).gt(Tr.ScalarType{ .value = rhs.data[i] }); - return res; - } - - /// Element-wise "Greater Than or Equal". Returns an array of booleans. - pub inline fn gte(self: Self, rhs: anytype) [len]bool { - const Tr = @TypeOf(rhs); - var res: [len]bool = undefined; - inline for (self.data, 0..) |v, i| - res[i] = (Q{ .value = v }).gte(Tr.ScalarType{ .value = rhs.data[i] }); - return res; - } - - /// Element-wise "Less Than". Returns an array of booleans. - pub inline fn lt(self: Self, rhs: anytype) [len]bool { - const Tr = @TypeOf(rhs); - var res: [len]bool = undefined; - inline for (self.data, 0..) |v, i| - res[i] = (Q{ .value = v }).lt(Tr.ScalarType{ .value = rhs.data[i] }); - return res; - } - - /// Element-wise "Less Than or Equal". Returns an array of booleans. - pub inline fn lte(self: Self, rhs: anytype) [len]bool { - const Tr = @TypeOf(rhs); - var res: [len]bool = undefined; - inline for (self.data, 0..) |v, i| - res[i] = (Q{ .value = v }).lte(Tr.ScalarType{ .value = rhs.data[i] }); - return res; - } - - // ------------------------------------------------------------------- - // Vector–Scalar comparisons - // scalar may be: Scalar, T, comptime_int, comptime_float - // ------------------------------------------------------------------- - - /// Compares every element in the vector to a single scalar for equality. - /// Returns an array of booleans [len]bool. Dimensions must match; scales are auto-resolved. - pub inline fn eqScalar(self: Self, scalar: anytype) [len]bool { - var res: [len]bool = undefined; - inline for (self.data, 0..) |v, i| - res[i] = (Q{ .value = v }).eq(scalar); - return res; - } - - /// Compares every element in the vector to a single scalar for inequality. - /// Returns an array of booleans [len]bool. Dimensions must match; scales are auto-resolved. - pub inline fn neScalar(self: Self, scalar: anytype) [len]bool { - var res: [len]bool = undefined; - inline for (self.data, 0..) |v, i| - res[i] = (Q{ .value = v }).ne(scalar); - return res; - } - - /// Checks if each element in the vector is strictly greater than the given scalar. - /// Returns an array of booleans [len]bool. - pub inline fn gtScalar(self: Self, scalar: anytype) [len]bool { - var res: [len]bool = undefined; - inline for (self.data, 0..) |v, i| - res[i] = (Q{ .value = v }).gt(scalar); - return res; - } - - /// Checks if each element in the vector is greater than or equal to the given scalar. - /// Returns an array of booleans [len]bool. - pub inline fn gteScalar(self: Self, scalar: anytype) [len]bool { - var res: [len]bool = undefined; - inline for (self.data, 0..) |v, i| - res[i] = (Q{ .value = v }).gte(scalar); - return res; - } - - /// Checks if each element in the vector is strictly less than the given scalar. - /// Returns an array of booleans [len]bool. - pub inline fn ltScalar(self: Self, scalar: anytype) [len]bool { - var res: [len]bool = undefined; - inline for (self.data, 0..) |v, i| - res[i] = (Q{ .value = v }).lt(scalar); - return res; - } - - /// Checks if each element in the vector is less than or equal to the given scalar. - /// Returns an array of booleans [len]bool. - pub inline fn lteScalar(self: Self, scalar: anytype) [len]bool { - var res: [len]bool = undefined; - inline for (self.data, 0..) |v, i| - res[i] = (Q{ .value = v }).lte(scalar); - return res; - } - - // ------------------------------------------------------------------- - // Formatting - // ------------------------------------------------------------------- - - pub fn formatNumber( - self: Self, - writer: *std.Io.Writer, - options: std.fmt.Number, - ) !void { - try writer.writeAll("("); - for (self.data, 0..) |v, i| { - if (i > 0) try writer.writeAll(", "); - switch (@typeInfo(T)) { - .float, .comptime_float => try writer.printFloat(v, options), - .int, .comptime_int => try writer.printInt(v, 10, .lower, .{ - .width = options.width, - .alignment = options.alignment, - .fill = options.fill, - .precision = options.precision, - }), - else => unreachable, - } - } - try writer.writeAll(")"); - var first = true; - inline for (std.enums.values(Dimension)) |bu| { - const v = dims.get(bu); - if (comptime v == 0) continue; - if (!first) - try writer.writeAll("."); - - first = false; - - const uscale = scales.get(bu); - if (bu == .T and (uscale == .min or uscale == .hour or uscale == .year)) - try writer.print("{s}", .{uscale.str()}) - else - try writer.print("{s}{s}", .{ uscale.str(), bu.unit() }); - - if (v != 1) - try hlp.printSuperscript(writer, v); - } - } - }; -} - -test "Format VectorX" { - const MeterPerSecondSq = Scalar(f32, .{ .L = 1, .T = -2 }, .{ .T = .n }); - const KgMeterPerSecond = Scalar(f32, .{ .M = 1, .L = 1, .T = -1 }, .{ .M = .k }); - - const accel = MeterPerSecondSq.Vec3.initDefault(9.81); - const momentum = KgMeterPerSecond.Vec3{ .data = .{ 43, 0, 11 } }; - - var buf: [64]u8 = undefined; - var res = try std.fmt.bufPrint(&buf, "{d}", .{accel}); - try std.testing.expectEqualStrings("(9.81, 9.81, 9.81)m.ns⁻²", res); - - res = try std.fmt.bufPrint(&buf, "{d:.2}", .{momentum}); - try std.testing.expectEqualStrings("(43.00, 0.00, 11.00)m.kg.s⁻¹", res); -} - -test "VecX Init and Basic Arithmetic" { - const Meter = Scalar(i32, .{ .L = 1 }, .{}); - const Vec3M = Meter.Vec3; - - // Test zero, one, initDefault - const v_zero = Vec3M.zero; - try std.testing.expectEqual(0, v_zero.data[0]); - try std.testing.expectEqual(0, v_zero.data[1]); - try std.testing.expectEqual(0, v_zero.data[2]); - - const v_one = Vec3M.one; - try std.testing.expectEqual(1, v_one.data[0]); - try std.testing.expectEqual(1, v_one.data[1]); - try std.testing.expectEqual(1, v_one.data[2]); - - const v_def = Vec3M.initDefault(5); - try std.testing.expectEqual(5, v_def.data[0]); - try std.testing.expectEqual(5, v_def.data[1]); - try std.testing.expectEqual(5, v_def.data[2]); - - // Test add and sub - const v1 = Vec3M{ .data = .{ 10, 20, 30 } }; - const v2 = Vec3M{ .data = .{ 2, 4, 6 } }; - - const added = v1.add(v2); - try std.testing.expectEqual(12, added.data[0]); - try std.testing.expectEqual(24, added.data[1]); - try std.testing.expectEqual(36, added.data[2]); - - const subbed = v1.sub(v2); - try std.testing.expectEqual(8, subbed.data[0]); - try std.testing.expectEqual(16, subbed.data[1]); - try std.testing.expectEqual(24, subbed.data[2]); - - // Test negate - const neg = v1.negate(); - try std.testing.expectEqual(-10, neg.data[0]); - try std.testing.expectEqual(-20, neg.data[1]); - try std.testing.expectEqual(-30, neg.data[2]); -} - -test "VecX Kinematics (Scalar Mul/Div)" { - const Meter = Scalar(i32, .{ .L = 1 }, .{}); - const Second = Scalar(i32, .{ .T = 1 }, .{}); - const Vec3M = Meter.Vec3; - - const pos = Vec3M{ .data = .{ 100, 200, 300 } }; - const time = Second{ .value = 10 }; - - // Vector divided by scalar (Velocity = Position / Time) - const vel = pos.divScalar(time); - try std.testing.expectEqual(10, vel.data[0]); - try std.testing.expectEqual(20, vel.data[1]); - try std.testing.expectEqual(30, vel.data[2]); - try std.testing.expectEqual(1, @TypeOf(vel).dims.get(.L)); - try std.testing.expectEqual(-1, @TypeOf(vel).dims.get(.T)); - - // Vector multiplied by scalar (Position = Velocity * Time) - const new_pos = vel.mulScalar(time); - try std.testing.expectEqual(100, new_pos.data[0]); - try std.testing.expectEqual(200, new_pos.data[1]); - try std.testing.expectEqual(300, new_pos.data[2]); - try std.testing.expectEqual(1, @TypeOf(new_pos).dims.get(.L)); - try std.testing.expectEqual(0, @TypeOf(new_pos).dims.get(.T)); -} - -test "VecX Element-wise Math and Scaling" { - const Meter = Scalar(i32, .{ .L = 1 }, .{}); - const Vec3M = Meter.Vec3; - - const v1 = Vec3M{ .data = .{ 10, 20, 30 } }; - const v2 = Vec3M{ .data = .{ 2, 5, 10 } }; - - // Element-wise division - const div = v1.div(v2); - try std.testing.expectEqual(5, div.data[0]); - try std.testing.expectEqual(4, div.data[1]); - try std.testing.expectEqual(3, div.data[2]); - try std.testing.expectEqual(0, @TypeOf(div).dims.get(.L)); // M / M = Dimensionless -} - -test "VecX Conversions" { - const KiloMeter = Scalar(i32, .{ .L = 1 }, .{ .L = .k }); - const Meter = Scalar(i32, .{ .L = 1 }, .{}); - - const v_km = KiloMeter.Vec3{ .data = .{ 1, 2, 3 } }; - const v_m = v_km.to(Meter); - - try std.testing.expectEqual(1000, v_m.data[0]); - try std.testing.expectEqual(2000, v_m.data[1]); - try std.testing.expectEqual(3000, v_m.data[2]); - - // Type checking the result - try std.testing.expectEqual(1, @TypeOf(v_m).dims.get(.L)); - try std.testing.expectEqual(UnitScale.none, @TypeOf(v_m).scales.get(.L)); -} - -test "VecX Length" { - const MeterInt = Scalar(i32, .{ .L = 1 }, .{}); - const MeterFloat = Scalar(f32, .{ .L = 1 }, .{}); - - // Integer length - // 3-4-5 triangle on XY plane - const v_int = MeterInt.Vec3{ .data = .{ 3, 4, 0 } }; - try std.testing.expectEqual(25, v_int.lengthSqr()); - try std.testing.expectEqual(5, v_int.length()); - - // Float length - const v_float = MeterFloat.Vec3{ .data = .{ 3.0, 4.0, 0.0 } }; - try std.testing.expectApproxEqAbs(@as(f32, 25.0), v_float.lengthSqr(), 1e-4); - try std.testing.expectApproxEqAbs(@as(f32, 5.0), v_float.length(), 1e-4); -} - -test "Vector Comparisons" { - const Meter = Scalar(f32, .{ .L = 1 }, .{}); - const KiloMeter = Scalar(f32, .{ .L = 1 }, .{ .L = .k }); - - const v1 = Meter.Vec3{ .data = .{ 1000.0, 500.0, 0.0 } }; - const v2 = KiloMeter.Vec3{ .data = .{ 1.0, 0.5, 0.0 } }; - const v3 = KiloMeter.Vec3{ .data = .{ 1.0, 0.6, 0.0 } }; - - // 1. Equality (Whole vector) - try std.testing.expect(v1.eqAll(v2)); - try std.testing.expect(v1.neAll(v3)); - - // 2. Element-wise Ordered Comparison - const higher = v3.gt(v1); // compares 1km, 0.6km, 0km vs 1000m, 500m, 0m - try std.testing.expectEqual(false, higher[0]); // 1km == 1000m - try std.testing.expectEqual(true, higher[1]); // 0.6km > 500m - try std.testing.expectEqual(false, higher[2]); // 0 == 0 - - // 3. Element-wise Equal Comparison - const equal = v3.eq(v1); // compares 1km, 0.6km, 0km vs 1000m, 500m, 0m - try std.testing.expectEqual(true, equal[0]); // 1km == 1000m - try std.testing.expectEqual(false, equal[1]); // 0.6km > 500m - try std.testing.expectEqual(true, equal[2]); // 0 == 0 - - // 3. Less than or equal - const low_eq = v1.lte(v3); - try std.testing.expect(low_eq[0] and low_eq[1] and low_eq[2]); -} - -test "Vector vs Scalar Comparisons" { - const Meter = Scalar(f32, .{ .L = 1 }, .{}); - const KiloMeter = Scalar(f32, .{ .L = 1 }, .{ .L = .k }); - - const positions = Meter.Vec3{ .data = .{ 500.0, 1200.0, 3000.0 } }; - const threshold = KiloMeter{ .value = 1.0 }; // 1km (1000m) - - // Check which axes exceed the 1km threshold - const exceeded = positions.gtScalar(threshold); - - try std.testing.expectEqual(false, exceeded[0]); // 500m > 1km is false - try std.testing.expectEqual(true, exceeded[1]); // 1200m > 1km is true - try std.testing.expectEqual(true, exceeded[2]); // 3000m > 1km is true - - // Check for equality (broadcasted) - const exact_match = positions.eqScalar(Meter{ .value = 500.0 }); - try std.testing.expect(exact_match[0] == true); - try std.testing.expect(exact_match[1] == false); -} - -test "Vector Dot and Cross Products" { - const Meter = Scalar(f32, .{ .L = 1 }, .{}); - const Newton = Scalar(f32, .{ .M = 1, .L = 1, .T = -2 }, .{}); - - const pos = Meter.Vec3{ .data = .{ 10.0, 0.0, 0.0 } }; - const force = Newton.Vec3{ .data = .{ 5.0, 5.0, 0.0 } }; - - // 1. Dot Product (Work = F dot d) - const work = force.dot(pos); - try std.testing.expectEqual(50.0, work.value); - // Dimensions should be M¹L²T⁻² (Energy/Joules) - try std.testing.expectEqual(1, @TypeOf(work).dims.get(.M)); - try std.testing.expectEqual(2, @TypeOf(work).dims.get(.L)); - try std.testing.expectEqual(-2, @TypeOf(work).dims.get(.T)); - - // 2. Cross Product (Torque = r cross F) - const torque = pos.cross(force); - try std.testing.expectEqual(0.0, torque.data[0]); - try std.testing.expectEqual(0.0, torque.data[1]); - try std.testing.expectEqual(50.0, torque.data[2]); - // Torque dimensions are same as Energy but as a Vector - try std.testing.expectEqual(2, @TypeOf(torque).dims.get(.L)); -} - -test "Vector Abs, Pow, Sqrt and Product" { - const Meter = Scalar(f32, .{ .L = 1 }, .{}); - - const v1 = Meter.Vec3{ .data = .{ -2.0, 3.0, -4.0 } }; - - // 1. Abs - const v_abs = v1.abs(); - try std.testing.expectEqual(2.0, v_abs.data[0]); - try std.testing.expectEqual(4.0, v_abs.data[2]); - - // 2. Product (L1 * L1 * L1 = L3) - const vol = v_abs.product(); - try std.testing.expectEqual(24.0, vol.value); - try std.testing.expectEqual(3, @TypeOf(vol).dims.get(.L)); - - // 3. Pow (Scalar exponent: (L1)^2 = L2) - const area_vec = v_abs.pow(2); - try std.testing.expectEqual(4.0, area_vec.data[0]); - try std.testing.expectEqual(16.0, area_vec.data[2]); - try std.testing.expectEqual(2, @TypeOf(area_vec).dims.get(.L)); - - // 4. Sqrt - const sqrted = area_vec.sqrt(); - try std.testing.expectEqual(2, sqrted.data[0]); - try std.testing.expectEqual(4, sqrted.data[2]); - try std.testing.expectEqual(2, @TypeOf(sqrted).dims.get(.L)); -} - -test "mulScalar comptime_int" { - const Meter = Scalar(i32, .{ .L = 1 }, .{}); - const v = Meter.Vec3{ .data = .{ 1, 2, 3 } }; - - const scaled = v.mulScalar(10); // comptime_int → dimensionless - try std.testing.expectEqual(10, scaled.data[0]); - try std.testing.expectEqual(20, scaled.data[1]); - try std.testing.expectEqual(30, scaled.data[2]); - // Dimensions unchanged: L¹ × dimensionless = L¹ - try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L)); - try std.testing.expectEqual(0, @TypeOf(scaled).dims.get(.T)); -} - -test "mulScalar comptime_float" { - const MeterF = Scalar(f32, .{ .L = 1 }, .{}); - const v = MeterF.Vec3{ .data = .{ 1.0, 2.0, 4.0 } }; - - const scaled = v.mulScalar(0.5); // comptime_float → dimensionless - try std.testing.expectApproxEqAbs(0.5, scaled.data[0], 1e-6); - try std.testing.expectApproxEqAbs(1.0, scaled.data[1], 1e-6); - try std.testing.expectApproxEqAbs(2.0, scaled.data[2], 1e-6); - try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L)); -} - -test "mulScalar T (value type)" { - const MeterF = Scalar(f32, .{ .L = 1 }, .{}); - const v = MeterF.Vec3{ .data = .{ 3.0, 6.0, 9.0 } }; - const factor: f32 = 2.0; - - const scaled = v.mulScalar(factor); - try std.testing.expectApproxEqAbs(6.0, scaled.data[0], 1e-6); - try std.testing.expectApproxEqAbs(12.0, scaled.data[1], 1e-6); - try std.testing.expectApproxEqAbs(18.0, scaled.data[2], 1e-6); - try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L)); -} - -test "divScalar comptime_int" { - const Meter = Scalar(i32, .{ .L = 1 }, .{}); - const v = Meter.Vec3{ .data = .{ 10, 20, 30 } }; - - const halved = v.divScalar(2); // comptime_int → dimensionless divisor - try std.testing.expectEqual(5, halved.data[0]); - try std.testing.expectEqual(10, halved.data[1]); - try std.testing.expectEqual(15, halved.data[2]); - try std.testing.expectEqual(1, @TypeOf(halved).dims.get(.L)); -} - -test "divScalar comptime_float" { - const MeterF = Scalar(f64, .{ .L = 1 }, .{}); - const v = MeterF.Vec3{ .data = .{ 9.0, 6.0, 3.0 } }; - - const r = v.divScalar(3.0); - try std.testing.expectApproxEqAbs(3.0, r.data[0], 1e-9); - try std.testing.expectApproxEqAbs(2.0, r.data[1], 1e-9); - try std.testing.expectApproxEqAbs(1.0, r.data[2], 1e-9); - try std.testing.expectEqual(1, @TypeOf(r).dims.get(.L)); -} - -test "eqScalar / gtScalar with comptime_int on dimensionless vector" { - // Bare numbers are dimensionless, so comparisons only work when vector is dimensionless too. - const DimLess = Scalar(i32, .{}, .{}); - const v = DimLess.Vec3{ .data = .{ 1, 2, 3 } }; - - const eq_res = v.eqScalar(2); - try std.testing.expectEqual(false, eq_res[0]); - try std.testing.expectEqual(true, eq_res[1]); - try std.testing.expectEqual(false, eq_res[2]); - - const gt_res = v.gtScalar(1); - try std.testing.expectEqual(false, gt_res[0]); - try std.testing.expectEqual(true, gt_res[1]); - try std.testing.expectEqual(true, gt_res[2]); -} diff --git a/src/benchmark.zig b/src/benchmark.zig index 2d9d5d0..7da3e6f 100644 --- a/src/benchmark.zig +++ b/src/benchmark.zig @@ -1,7 +1,7 @@ const std = @import("std"); const Io = std.Io; -const Scalar = @import("Scalar.zig").Scalar; -const Vector = @import("Vector.zig").Vector; +const Scalar = @import("Quantity.zig").Scalar; +const Vector = @import("Quantity.zig").Vector; var io: Io = undefined; pub fn main(init: std.process.Init) !void { @@ -11,6 +11,17 @@ pub fn main(init: std.process.Init) !void { io = init.io; + // try vectorSIMDvsNative(f64, &stdout_writer.interface); + // try stdout_writer.flush(); + // try vectorSIMDvsNative(f32, &stdout_writer.interface); + // try stdout_writer.flush(); + // try vectorSIMDvsNative(i32, &stdout_writer.interface); + // try stdout_writer.flush(); + // try vectorSIMDvsNative(i64, &stdout_writer.interface); + // try stdout_writer.flush(); + // try vectorSIMDvsNative(i128, &stdout_writer.interface); + // try stdout_writer.flush(); + try bench_Scalar(&stdout_writer.interface); try stdout_writer.flush(); try bench_vsNative(&stdout_writer.interface); @@ -100,23 +111,23 @@ fn bench_Scalar(writer: *std.Io.Writer) !void { std.mem.doNotOptimizeAway( { _ = if (comptime std.mem.eql(u8, op_name, "add")) - (M{ .value = getVal(T, i, 63) }).add(M{ .value = getVal(T, i +% 7, 63) }) + (M.splat(getVal(T, i, 63))).add(M.splat(getVal(T, i +% 7, 63))) else if (comptime std.mem.eql(u8, op_name, "sub")) - (M{ .value = getVal(T, i +% 10, 63) }).sub(M{ .value = getVal(T, i, 63) }) + (M.splat(getVal(T, i +% 10, 63))).sub(M.splat(getVal(T, i, 63))) else if (comptime std.mem.eql(u8, op_name, "mul")) - (M{ .value = getVal(T, i, 63) }).mul(M{ .value = getVal(T, i +% 1, 63) }) + (M.splat(getVal(T, i, 63))).mul(M.splat(getVal(T, i +% 1, 63))) else if (comptime std.mem.eql(u8, op_name, "div")) - (M{ .value = getVal(T, i +% 10, 63) }).div(S{ .value = getVal(T, i, 63) }) + (M.splat(getVal(T, i +% 10, 63))).div(S.splat(getVal(T, i, 63))) else if (comptime std.mem.eql(u8, op_name, "to")) - (KM{ .value = getVal(T, i, 15) }).to(M) + (KM.splat(getVal(T, i, 15))).to(M) else if (comptime std.mem.eql(u8, op_name, "abs")) - (M{ .value = getVal(T, i, 63) }).abs() + (M.splat(getVal(T, i, 63))).abs() else if (comptime std.mem.eql(u8, op_name, "eq")) - (M{ .value = getVal(T, i, 63) }).eq(M{ .value = getVal(T, i +% 3, 63) }) + (M.splat(getVal(T, i, 63))).eq(M.splat(getVal(T, i +% 3, 63))) else if (comptime std.mem.eql(u8, op_name, "gt")) - (M{ .value = getVal(T, i, 63) }).gt(M{ .value = getVal(T, i +% 3, 63) }) + (M.splat(getVal(T, i, 63))).gt(M.splat(getVal(T, i +% 3, 63))) else - (M{ .value = getVal(T, i, 63) }).mul(3); + (M.splat(getVal(T, i, 63))).mul(3); }, ); } @@ -223,8 +234,8 @@ fn bench_vsNative(writer: *std.Io.Writer) !void { // --- 2. Benchmark Scalar --- const q_start = getTime(); for (0..ITERS) |i| { - const qa = M{ .value = getValT(T, i) }; - const qb = if (comptime std.mem.eql(u8, op_name, "div")) S{ .value = getValT(T, 2) } else M{ .value = getValT(T, 2) }; + const qa = M.splat(getValT(T, i)); + const qb = if (comptime std.mem.eql(u8, op_name, "div")) S.splat(getValT(T, 2)) else M.splat(getValT(T, 2)); // Scalar logic branch _ = if (comptime std.mem.eql(u8, op_name, "add")) @@ -338,11 +349,11 @@ fn bench_crossTypeVsNative(writer: *std.Io.Writer) !void { // --- 2. Benchmark Scalar --- const q_start = getTime(); for (0..ITERS) |i| { - const qa = M1{ .value = getValT(T1, i) }; + const qa = M1.splat(getValT(T1, i)); const qb = if (comptime std.mem.eql(u8, op_name, "div")) - S2{ .value = getValT(T2, 2) } + S2.splat(getValT(T2, 2)) else - M2{ .value = getValT(T2, 2) }; + M2.splat(getValT(T2, 2)); _ = if (comptime std.mem.eql(u8, op_name, "add")) qa.add(qb) @@ -401,15 +412,15 @@ fn bench_Vector(writer: *std.Io.Writer) !void { \\ Vector benchmark — {d} iterations, {d} samples/cell \\ (Results in ns/op; "---" = not applicable for this length) \\ - \\┌──────────────────┬──────┬─────────┬─────────┬─────────┐ - \\│ Operation │ Type │ Len=3 │ Len=4 │ Len=16 │ - \\├──────────────────┼──────┼─────────┼─────────┼─────────┤ + \\┌──────────────────┬──────┬─────────┬─────────┬─────────┬─────────┬─────────┐ + \\│ Operation │ Type │ Len=1 │ Len=3 │ Len=4 │ Len=16 │ Len=100 │ + \\├──────────────────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ \\ , .{ ITERS, SAMPLES }); const Types = .{ i32, i64, i128, f32, f64 }; const TNames = .{ "i32", "i64", "i128", "f32", "f64" }; - const Lengths = .{ 3, 4, 16 }; + const Lengths = .{ 1, 3, 4, 16, 100 }; // "cross" is only valid for len=3; other cells will show " --- " const Ops = .{ "add", "div", "mulScalar", "dot", "cross", "product", "pow", "length" }; @@ -435,22 +446,22 @@ fn bench_Vector(writer: *std.Io.Writer) !void { for (0..SAMPLES) |s_idx| { const t_start = getTime(); for (0..ITERS) |i| { - const v1 = V.initDefault(getVal(T, i, 63)); + const v1 = V.splat(getVal(T, i, 63)); if (comptime std.mem.eql(u8, op_name, "add")) { - const v2 = V.initDefault(getVal(T, i +% 7, 63)); + const v2 = V.splat(getVal(T, i +% 7, 63)); _ = v1.add(v2); } else if (comptime std.mem.eql(u8, op_name, "div")) { - _ = v1.div(V.initDefault(getVal(T, i +% 2, 63))); + _ = v1.div(V.splat(getVal(T, i +% 2, 63))); } else if (comptime std.mem.eql(u8, op_name, "mulScalar")) { - const s_val = Q_time{ .value = getVal(T, i +% 2, 63) }; + const s_val = Q_time.splat(getVal(T, i +% 2, 63)); _ = v1.mulScalar(s_val); } else if (comptime std.mem.eql(u8, op_name, "dot")) { - const v2 = V.initDefault(getVal(T, i +% 5, 63)); + const v2 = V.splat(getVal(T, i +% 5, 63)); _ = v1.dot(v2); } else if (comptime std.mem.eql(u8, op_name, "cross")) { // len == 3 guaranteed by the guard above - const v2 = V.initDefault(getVal(T, i +% 5, 63)); + const v2 = V.splat(getVal(T, i +% 5, 63)); _ = v1.cross(v2); } else if (comptime std.mem.eql(u8, op_name, "product")) { _ = v1.product(); @@ -473,8 +484,67 @@ fn bench_Vector(writer: *std.Io.Writer) !void { } if (o_idx < Ops.len - 1) { - try writer.print("├──────────────────┼──────┼─────────┼─────────┼─────────┤\n", .{}); + try writer.print("├──────────────────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤\n", .{}); } } - try writer.print("└──────────────────┴──────┴─────────┴─────────┴─────────┘\n", .{}); + try writer.print("└──────────────────┴──────┴─────────┴─────────┴─────────┴─────────┴─────────┘\n", .{}); +} + +fn vectorSIMDvsNative(comptime T: type, writer: *std.Io.Writer) !void { + const iterations: u64 = 10_000; + const lens = [_]u32{ 1, 2, 3, 4, 5, 10, 100, 1_000, 10_000 }; + + try writer.print("\nSIMD Speedup Analysis: {s}\n", .{@typeName(T)}); + try writer.print("┌────────────┬────────────┬────────────┬────────────┐\n", .{}); + try writer.print("│ Vector Len │ Scalar (us)│ Vector (us)│ Speedup │\n", .{}); + try writer.print("├────────────┼────────────┼────────────┼────────────┤\n", .{}); + + inline for (lens) |vector_len| { + // --- Scalar Test --- + var scalar_val: T = 10; + const start_scalar = getTime(); + + var i: u64 = 0; + while (i < iterations * vector_len) : (i += 1) { + if (comptime @typeInfo(T) == .int) + scalar_val = scalar_val +% 1 + else + scalar_val = scalar_val + 1; + } + const scalar_time = start_scalar.durationTo(getTime()).toMicroseconds(); + + // --- Vector Test --- + var vector_val: @Vector(vector_len, T) = @splat(20); + const start_vector = getTime(); + + i = 0; + const increment: @Vector(vector_len, T) = @splat(1); + while (i < iterations) : (i += 1) { + if (comptime @typeInfo(T) == .int) + vector_val = vector_val +% increment + else + vector_val = vector_val + increment; + } + const vector_time = start_vector.durationTo(getTime()).toMicroseconds(); + + // --- Results --- + const s_float = @as(f64, @floatFromInt(scalar_time)); + const v_float = @as(f64, @floatFromInt(vector_time)); + + // Speedup = ScalarTime / VectorTime. + // > 1.0 means SIMD is faster. + const speedup = if (vector_time > 0) s_float / v_float else 0; + + try writer.print("│ {d:<10} │ {d:>10} │ {d:>10} │ {d:>9.2}x │\n", .{ + vector_len, + scalar_time, + vector_time, + speedup, + }); + try writer.flush(); + + std.mem.doNotOptimizeAway(scalar_val); + std.mem.doNotOptimizeAway(vector_val); + } + try writer.print("└────────────┴────────────┴────────────┴────────────┘\n", .{}); } diff --git a/src/helper.zig b/src/helper.zig index 30c89fc..fb104e1 100644 --- a/src/helper.zig +++ b/src/helper.zig @@ -62,14 +62,13 @@ pub fn finerScales(comptime T1: type, comptime T2: type) Scales { // RHS normalisation helpers // --------------------------------------------------------------------------- -const Scalar = @import("Scalar.zig").Scalar; +const Quantity = @import("Quantity.zig").Quantity; /// Returns true if `T` is a `Scalar_` type (has `dims`, `scales`, and `value`). pub fn isScalarType(comptime T: type) bool { return @typeInfo(T) == .@"struct" and - @hasDecl(T, "dims") and - @hasDecl(T, "scales") and - @hasField(T, "value"); + @hasDecl(T, "ISQUANTITY") and + @field(T, "ISQUANTITY"); } /// Resolve the Scalar type that `rhs` will be treated as. @@ -80,19 +79,19 @@ pub fn isScalarType(comptime T: type) bool { /// - `BaseT` (the scalar's value type) → dimensionless `Scalar_(BaseT, {}, {})` /// /// Everything else is a compile error, including other int/float types. -pub fn rhsScalarType(comptime BaseT: type, comptime RhsT: type) type { +pub fn rhsQuantityType(comptime ValueType: type, N: usize, comptime RhsT: type) type { if (comptime isScalarType(RhsT)) return RhsT; - if (comptime RhsT == comptime_int or RhsT == comptime_float or RhsT == BaseT) - return Scalar(BaseT, .{}, .{}); + if (comptime RhsT == comptime_int or RhsT == comptime_float or RhsT == ValueType) + return Quantity(ValueType, N, .{}, .{}); @compileError( - "rhs must be a Scalar, " ++ @typeName(BaseT) ++ + "rhs must be a Scalar, " ++ @typeName(ValueType) ++ ", comptime_int, or comptime_float; got " ++ @typeName(RhsT), ); } /// Convert `rhs` to its normalised Scalar form (see `rhsScalarType`). -pub inline fn toRhsScalar(comptime BaseT: type, rhs: anytype) rhsScalarType(BaseT, @TypeOf(rhs)) { +pub inline fn toRhsQuantity(comptime BaseT: type, N: usize, rhs: anytype) rhsQuantityType(BaseT, N, @TypeOf(rhs)) { if (comptime isScalarType(@TypeOf(rhs))) return rhs; - const DimLess = Scalar(BaseT, .{}, .{}); - return DimLess{ .value = @as(BaseT, rhs) }; + const DimLess = Quantity(BaseT, N, .{}, .{}); + return DimLess{ .data = @splat(@as(BaseT, rhs)) }; } diff --git a/src/main.zig b/src/main.zig index 015d36f..4d61ee6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,14 +1,13 @@ const std = @import("std"); -pub const Scalar = @import("Scalar.zig").Scalar; -pub const Vector = @import("Vector.zig").Vector; +pub const Vector = @import("Quantity.zig").Vector; +pub const Scalar = @import("Quantity.zig").Scalar; pub const Dimensions = @import("Dimensions.zig"); pub const Scales = @import("Scales.zig"); pub const Base = @import("Base.zig"); test { - _ = @import("Scalar.zig"); - _ = @import("Vector.zig"); + _ = @import("Quantity.zig"); _ = @import("Dimensions.zig"); _ = @import("Scales.zig"); _ = @import("Base.zig");