diff --git a/src/Base.zig b/src/Base.zig index 4bde68b..4a475a7 100644 --- a/src/Base.zig +++ b/src/Base.zig @@ -3,34 +3,39 @@ 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("Quantity.zig").Scalar; +const Tensor = @import("Tensor.zig").Tensor; fn PhysicalConstant(comptime d: Dimensions.ArgOpts, comptime val: f64, comptime s: Scales.ArgOpts) type { return struct { - const dims = Dimensions.init(d); - const scales = Scales.init(s); + pub const dims = Dimensions.init(d); + pub const scales = Scales.init(s); /// Instantiates the constant into a specific numeric type. - pub fn Of(comptime T: type) Scalar(T, d, s) { - return .{ .data = @splat(@as(T, @floatCast(val))) }; + pub fn Of(comptime T: type) Tensor(T, d, s, &.{1}) { + const casted_val: T = switch (@typeInfo(T)) { + .float => @floatCast(val), + .int => @intFromFloat(val), + else => @compileError("Unsupported type for PhysicalConstant"), + }; + return Tensor(T, d, s, &.{1}).splat(casted_val); } }; } fn BaseScalar(comptime d: Dimensions.ArgOpts) type { return struct { - const dims = Dimensions.init(d); + pub const dims = Dimensions.init(d); /// Creates a Scalar of this dimension using default scales. - /// Example: const V = Quantities.Velocity.Base(f32); + /// Example: const V = Quantities.Velocity.Of(f32); pub fn Of(comptime T: type) type { - return Scalar(T, d, .{}); + return Tensor(T, d, .{}, &.{1}); } /// Creates a Scalar of this dimension using custom scales. - /// Example: const Kmh = Quantities.Velocity.Scaled(f32, Scales.init(.{ .L = .k, .T = .hour })); + /// Example: const Kmh = Quantities.Velocity.Scaled(f32, .{ .L = .k, .T = .hour }); pub fn Scaled(comptime T: type, comptime s: Scales.ArgOpts) type { - return Scalar(T, d, s); + return Tensor(T, d, s, &.{1}); } }; } @@ -107,7 +112,7 @@ pub const ElectricCapacitance = BaseScalar(.{ .T = 4, .L = -2, .M = -1, .I = 2 } pub const ElectricImpedance = ElectricResistance; pub const MagneticFlux = BaseScalar(.{ .M = 1, .L = 2, .T = -2, .I = -1 }); pub const MagneticDensity = BaseScalar(.{ .M = 1, .T = -2, .I = -1 }); -pub const MagneticStrength = BaseScalar(.{ .L = -1, .I = 1 }); // Fixed typo from MagneticStrengh +pub const MagneticStrength = BaseScalar(.{ .L = -1, .I = 1 }); pub const MagneticMoment = BaseScalar(.{ .L = 2, .I = 1 }); // ========================================== @@ -140,7 +145,7 @@ pub const ThermalHeat = Energy; pub const ThermalWork = Energy; pub const ThermalCapacity = BaseScalar(.{ .M = 1, .L = 2, .T = -2, .Tr = -1 }); pub const ThermalCapacityPerMass = BaseScalar(.{ .L = 2, .T = -2, .Tr = -1 }); -pub const ThermalFluxDensity = BaseScalar(.{ .M = 1, .T = -3 }); // Fixed typo from ThermalluxDensity +pub const ThermalFluxDensity = BaseScalar(.{ .M = 1, .T = -3 }); pub const ThermalConductance = BaseScalar(.{ .M = 1, .L = 2, .T = -3, .Tr = -1 }); pub const ThermalConductivity = BaseScalar(.{ .M = 1, .L = 1, .T = -3, .Tr = -1 }); pub const ThermalResistance = BaseScalar(.{ .M = -1, .L = -2, .T = 3, .Tr = 1 }); @@ -152,20 +157,24 @@ pub const ThermalEntropy = BaseScalar(.{ .M = 1, .L = 2, .T = -2, .Tr = -1 }); // ========================================== pub const Frequency = BaseScalar(.{ .T = -1 }); pub const Viscosity = BaseScalar(.{ .M = 1, .L = -1, .T = -1 }); -pub const SurfaceTension = BaseScalar(.{ .M = 1, .T = -2 }); // Corrected from MT-2a +pub const SurfaceTension = BaseScalar(.{ .M = 1, .T = -2 }); + +// ========================================== +// Tests +// ========================================== test "BaseQuantities - Core dimensions instantiation" { // Basic types via generic wrappers const M = Meter.Of(f32); const distance = M.splat(100); - try std.testing.expectEqual(100.0, distance.value()); + try std.testing.expectEqual(100.0, distance.data[0]); 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.splat(120); - try std.testing.expectEqual(120.0, speed.value()); + try std.testing.expectEqual(120.0, speed.data[0]); try std.testing.expectEqual(.k, @TypeOf(speed).scales.get(.L)); try std.testing.expectEqual(.hour, @TypeOf(speed).scales.get(.T)); } @@ -176,12 +185,12 @@ test "BaseQuantities - Kinematics equations" { // Velocity = Distance / Time const v = d.div(t); - try std.testing.expectEqual(25.0, v.value()); + try std.testing.expectEqual(25.0, v.data[0]); 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.data[0]); try std.testing.expect(Acceleration.dims.eql(@TypeOf(a).dims)); } @@ -193,13 +202,13 @@ test "BaseQuantities - Dynamics (Force and Work)" { // Force = mass * acceleration const f = m.mul(a); - try std.testing.expectEqual(98, f.value()); + try std.testing.expectEqual(98, f.data[0]); try std.testing.expect(Force.dims.eql(@TypeOf(f).dims)); // Energy (Work) = Force * distance 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.data[0]); try std.testing.expect(Energy.dims.eql(@TypeOf(energy).dims)); } @@ -209,26 +218,26 @@ test "BaseQuantities - Electric combinations" { // Charge = Current * time const charge = current.mul(time); - try std.testing.expectEqual(6.0, charge.value()); + try std.testing.expectEqual(6.0, charge.data[0]); 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.data[0]); 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.data[0]); 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.data[0]); 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 +246,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.data[0]); 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 +254,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.data[0]); try std.testing.expectEqual(0, @TypeOf(alpha).dims.get(.M)); try std.testing.expectEqual(0, @TypeOf(alpha).dims.get(.L)); } diff --git a/src/Scales.zig b/src/Scales.zig index 246565f..06031fb 100644 --- a/src/Scales.zig +++ b/src/Scales.zig @@ -1,5 +1,4 @@ const std = @import("std"); -const hlp = @import("helper.zig"); const Dimensions = @import("Dimensions.zig"); const Dimension = @import("Dimensions.zig").Dimension; @@ -99,7 +98,7 @@ data: std.EnumArray(Dimension, UnitScale), pub fn init(comptime init_val: ArgOpts) Self { comptime var s = Self{ .data = std.EnumArray(Dimension, UnitScale).initFill(.none) }; inline for (std.meta.fields(@TypeOf(init_val))) |f| { - if (comptime hlp.isInt(@TypeOf(@field(init_val, f.name)))) + if (comptime @typeInfo(@TypeOf(@field(init_val, f.name))) == .comptime_int) s.data.set(@field(Dimension, f.name), @enumFromInt(@field(init_val, f.name))) else s.data.set(@field(Dimension, f.name), @field(init_val, f.name)); diff --git a/src/Quantity.zig b/src/Tensor.zig similarity index 73% rename from src/Quantity.zig rename to src/Tensor.zig index f0edea6..7cb8a3c 100644 --- a/src/Quantity.zig +++ b/src/Tensor.zig @@ -1,12 +1,11 @@ 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; // ───────────────────────────────────────────────────────────────────────────── -// Comptime shape utilities +// Comptime utilities // ───────────────────────────────────────────────────────────────────────────── pub fn shapeTotal(comptime shape: []const usize) usize { @@ -34,7 +33,10 @@ pub fn shapeRemoveAxis(comptime shape: []const usize, comptime axis: usize) [sha var out: [shape.len - 1]usize = undefined; var j: usize = 0; for (shape, 0..) |v, i| { - if (i != axis) { out[j] = v; j += 1; } + if (i != axis) { + out[j] = v; + j += 1; + } } return out; } @@ -96,6 +98,32 @@ pub fn insertAxis( return out; } +fn isInt(comptime T: type) bool { + return @typeInfo(T) == .int or @typeInfo(T) == .comptime_int; +} + +fn finerScales(comptime T1: type, comptime T2: type) Scales { + const d1: Dimensions = T1.dims; + const d2: Dimensions = T2.dims; + const s1: Scales = T1.scales; + const s2: Scales = T2.scales; + comptime var out = Scales.initFill(.none); + inline for (std.enums.values(Dimension)) |dim| { + const scale1 = comptime s1.get(dim); + const scale2 = comptime s2.get(dim); + out.set(dim, if (comptime d1.get(dim) == 0 and d2.get(dim) == 0) + .none + else if (comptime d1.get(dim) == 0) + scale2 + else if (comptime d2.get(dim) == 0) + scale1 + else if (comptime scale1.getFactor() > scale2.getFactor()) + scale2 + else + scale1); + } + comptime return out; +} // ───────────────────────────────────────────────────────────────────────────── // File-scope RHS normalisation helpers // @@ -104,76 +132,93 @@ pub fn insertAxis( // Actual Tensor types are passed through unchanged. // ───────────────────────────────────────────────────────────────────────────── +fn isTensor(comptime Rhs: type) bool { + return comptime @typeInfo(Rhs) == .@"struct" and @hasDecl(Rhs, "ISTENSOR"); +} + fn RhsTensorType(comptime T: type, comptime Rhs: type) type { - if (@hasDecl(Rhs, "ISTENSOR")) return Rhs; + if (comptime isTensor(Rhs)) return Rhs; return Tensor(T, .{}, .{}, &.{1}); } fn toRhsTensor(comptime T: type, r: anytype) RhsTensorType(T, @TypeOf(r)) { const Rhs = @TypeOf(r); - if (comptime @hasDecl(Rhs, "ISTENSOR")) return r; + if (comptime isTensor(Rhs)) return r; const scalar: T = switch (comptime @typeInfo(Rhs)) { .comptime_int => switch (comptime @typeInfo(T)) { .float => @as(T, @floatFromInt(r)), - else => @as(T, r), + else => @as(T, r), }, .comptime_float => switch (comptime @typeInfo(T)) { - .int => @as(T, @intFromFloat(r)), - else => @as(T, r), + .int => @as(T, @intFromFloat(r)), + else => @as(T, r), }, - .int => switch (comptime @typeInfo(T)) { + .int => switch (comptime @typeInfo(T)) { .float => @floatFromInt(r), - else => @intCast(r), + else => @intCast(r), }, .float => switch (comptime @typeInfo(T)) { - .int => @intFromFloat(r), - else => @floatCast(r), + .int => @intFromFloat(r), + else => @floatCast(r), }, else => @compileError("Unsupported RHS type: " ++ @typeName(Rhs)), }; return Tensor(T, .{}, .{}, &.{1}){ .data = .{scalar} }; } -// ───────────────────────────────────────────────────────────────────────────── -// Tensor — unified dimensioned ND type. -// -// T : element numeric type (f32, f64, i32, i128, …) -// d_opt : SI dimension exponents -// s_opt : unit scales -// shape_ : compile-time shape -// &.{1} → scalar -// &.{3} → 3-vector -// &.{4, 4} → 4×4 matrix -// &.{3, 3, 3} → 3D field -// -// Storage: flat @Vector(total, T) where total = product(shape_). -// All arithmetic operates on the flat vector directly → SIMD wherever possible. -// -// Shape-related comptime constants exposed on every Tensor type: -// dims : Dimensions — SI exponent struct -// scales : Scales — unit scale struct -// shape : []const usize -// rank : usize = shape.len -// total : usize = product(shape) -// strides_arr : [rank]usize — row-major strides -// -// Index helper: -// Tensor.idx(.{row, col}) → flat index (comptime, no runtime cost) -// -// GPU readiness: -// tensor.asSlice() → []T (zero-copy pointer to the flat @Vector storage) -// -// Contraction (replaces dot / cross / matmul): -// a.contract(b, axis_a, axis_b) -// For rank-1 × rank-1 this is the dot product. -// For rank-2 × rank-2 with axis_a=1, axis_b=0 this is matrix multiply. -// -// Removed from Quantity: -// Scalar / Vector aliases, Vec3 / ScalarType, .value(), .vec(), .vec3(), -// dot(), cross(), mulScalar(), divScalar(), eqScalar() and friends. -// Use Tensor(..., &.{1}), .data[0], mul(), div(), eq() respectively. -// ───────────────────────────────────────────────────────────────────────────── +pub fn printSuperscript(writer: *std.Io.Writer, n: i32) !void { + if (n == 0) return; + var val = n; + if (val < 0) { + try writer.writeAll("\u{207B}"); + val = -val; + } + var buf: [12]u8 = undefined; + const str = std.fmt.bufPrint(&buf, "{d}", .{val}) catch return; + for (str) |c| { + const s = switch (c) { + '0' => "\u{2070}", + '1' => "\u{00B9}", + '2' => "\u{00B2}", + '3' => "\u{00B3}", + '4' => "\u{2074}", + '5' => "\u{2075}", + '6' => "\u{2076}", + '7' => "\u{2077}", + '8' => "\u{2078}", + '9' => "\u{2079}", + else => unreachable, + }; + try writer.writeAll(s); + } +} +/// ───────────────────────────────────────────────────────────────────────────── +/// Tensor — unified dimensioned ND type. +/// +/// T : element numeric type (f32, f64, i32, i128, …) +/// d_opt : SI dimension exponents +/// s_opt : unit scales +/// shape_ : compile-time shape +/// &.{1} → scalar +/// &.{3} → 3-vector +/// &.{4, 4} → 4×4 matrix +/// &.{3, 3, 3} → 3D field +/// +/// Storage: flat @Vector(total, T) where total = product(shape_). +/// All arithmetic operates on the flat vector directly → SIMD wherever possible. +/// +/// Shape-related comptime constants exposed on every Tensor type: +/// dims : Dimensions — SI exponent struct +/// scales : Scales — unit scale struct +/// shape : []const usize +/// rank : usize = shape.len +/// total : usize = product(shape) +/// strides_arr : [rank]usize — row-major strides +/// +/// Index helper: +/// Tensor.idx(.{row, col}) → flat index (comptime, no runtime cost) +/// ───────────────────────────────────────────────────────────────────────────── pub fn Tensor( comptime T: type, comptime d_opt: Dimensions.ArgOpts, @@ -186,9 +231,9 @@ pub fn Tensor( } @setEvalBranchQuota(10_000_000); - const _total: usize = comptime shapeTotal(shape_); - const _strides = comptime shapeStrides(shape_); - const Vec = @Vector(_total, T); + const _total: usize = comptime shapeTotal(shape_); + const _strides = comptime shapeStrides(shape_); + const Vec = @Vector(_total, T); return struct { /// Flat SIMD storage. All arithmetic operates here directly. @@ -196,14 +241,14 @@ pub fn Tensor( const Self = @This(); - pub const ValueType : type = T; - pub const dims : Dimensions = Dimensions.init(d_opt); - pub const scales : Scales = Scales.init(s_opt); - pub const shape : []const usize = shape_; - pub const rank : usize = shape_.len; - pub const total : usize = _total; + pub const ValueType: type = T; + pub const dims: Dimensions = Dimensions.init(d_opt); + pub const scales: Scales = Scales.init(s_opt); + pub const shape: []const usize = shape_; + pub const rank: usize = shape_.len; + pub const total: usize = _total; pub const strides_arr: [shape_.len]usize = _strides; - pub const ISTENSOR = true; + pub const ISTENSOR = true; // ─────────────────────────────────────────────────────────────── // Index helper @@ -211,7 +256,7 @@ pub fn Tensor( /// Convert N-D coords (row-major) to flat index — fully comptime. /// Usage: Tensor.idx(.{row, col}) - pub fn idx(comptime coords: [rank]usize) usize { + pub inline fn idx(comptime coords: [rank]usize) usize { comptime { var flat: usize = 0; for (0..rank) |i| { @@ -232,7 +277,7 @@ pub fn Tensor( } pub const zero: Self = splat(0); - pub const one: Self = splat(1); + pub const one: Self = splat(1); // ─────────────────────────────────────────────────────────────── // GPU readiness @@ -247,8 +292,12 @@ pub fn Tensor( // Internal: RHS normalisation // ─────────────────────────────────────────────────────────────── - inline fn RhsT(comptime Rhs: type) type { return RhsTensorType(T, Rhs); } - inline fn rhs(r: anytype) RhsT(@TypeOf(r)) { return toRhsTensor(T, r); } + inline fn RhsT(comptime Rhs: type) type { + return RhsTensorType(T, Rhs); + } + inline fn rhs(r: anytype) RhsT(@TypeOf(r)) { + return toRhsTensor(T, r); + } // ─────────────────────────────────────────────────────────────── // Internal: scalar broadcast (shape {1} → full Vec) @@ -270,48 +319,48 @@ pub fn Tensor( pub inline fn add(self: Self, r: anytype) Tensor( T, dims.argsOpt(), - hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(), + finerScales(Self, RhsT(@TypeOf(r))).argsOpt(), shape_, ) { - const rhs_q = rhs(r); + 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 RhsType.total != total and RhsType.total != 1) @compileError("Shape mismatch in add: element counts must match or RHS must be scalar (total=1)."); - const TargetType = Tensor(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt(), shape_); + const TargetType = Tensor(T, dims.argsOpt(), finerScales(Self, RhsType).argsOpt(), shape_); const l: Vec = if (comptime Self == TargetType) self.data else self.to(TargetType).data; const rr: Vec = blk: { - const RhsNorm = Tensor(T, RhsType.dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt(), RhsType.shape); + const RhsNorm = Tensor(T, RhsType.dims.argsOpt(), finerScales(Self, RhsType).argsOpt(), RhsType.shape); const rn = if (comptime RhsType == RhsNorm) rhs_q else rhs_q.to(RhsNorm); break :blk broadcastToVec(RhsNorm, rn); }; - return .{ .data = if (comptime hlp.isInt(T)) l +| rr else l + rr }; + return .{ .data = if (comptime 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) Tensor( T, dims.argsOpt(), - hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(), + finerScales(Self, RhsT(@TypeOf(r))).argsOpt(), shape_, ) { - const rhs_q = rhs(r); + 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 RhsType.total != total and RhsType.total != 1) @compileError("Shape mismatch in sub."); - const TargetType = Tensor(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt(), shape_); + const TargetType = Tensor(T, dims.argsOpt(), finerScales(Self, RhsType).argsOpt(), shape_); const l: Vec = if (comptime Self == TargetType) self.data else self.to(TargetType).data; const rr: Vec = blk: { - const RhsNorm = Tensor(T, RhsType.dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt(), RhsType.shape); + const RhsNorm = Tensor(T, RhsType.dims.argsOpt(), finerScales(Self, RhsType).argsOpt(), RhsType.shape); const rn = if (comptime RhsType == RhsNorm) rhs_q else rhs_q.to(RhsNorm); break :blk broadcastToVec(RhsNorm, rn); }; - return .{ .data = if (comptime hlp.isInt(T)) l -| rr else l - rr }; + return .{ .data = if (comptime isInt(T)) l -| rr else l - rr }; } /// Element-wise multiply. Dimension exponents summed. @@ -319,20 +368,20 @@ pub fn Tensor( pub inline fn mul(self: Self, r: anytype) Tensor( T, dims.add(RhsT(@TypeOf(r)).dims).argsOpt(), - hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(), + finerScales(Self, RhsT(@TypeOf(r))).argsOpt(), shape_, ) { - const rhs_q = rhs(r); + const rhs_q = rhs(r); const RhsType = @TypeOf(rhs_q); if (comptime RhsType.total != total and RhsType.total != 1) @compileError("Shape mismatch in mul."); - const SelfNorm = Tensor(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt(), shape_); - const RhsNorm = Tensor(T, RhsType.dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt(), RhsType.shape); - 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 }; + const SelfNorm = Tensor(T, dims.argsOpt(), finerScales(Self, RhsType).argsOpt(), shape_); + const RhsNorm = Tensor(T, RhsType.dims.argsOpt(), finerScales(Self, RhsType).argsOpt(), RhsType.shape); + 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 isInt(T)) l *| rr else l * rr }; } /// Element-wise divide. Dimension exponents subtracted. @@ -340,20 +389,20 @@ pub fn Tensor( pub inline fn div(self: Self, r: anytype) Tensor( T, dims.sub(RhsT(@TypeOf(r)).dims).argsOpt(), - hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(), + finerScales(Self, RhsT(@TypeOf(r))).argsOpt(), shape_, ) { - const rhs_q = rhs(r); + const rhs_q = rhs(r); const RhsType = @TypeOf(rhs_q); if (comptime RhsType.total != total and RhsType.total != 1) @compileError("Shape mismatch in div."); - const SelfNorm = Tensor(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt(), shape_); - const RhsNorm = Tensor(T, RhsType.dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt(), RhsType.shape); - 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)) { + const SelfNorm = Tensor(T, dims.argsOpt(), finerScales(Self, RhsType).argsOpt(), shape_); + const RhsNorm = Tensor(T, RhsType.dims.argsOpt(), finerScales(Self, RhsType).argsOpt(), RhsType.shape); + 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 isInt(T)) { var result: Vec = undefined; inline for (0..total) |i| result[i] = @divTrunc(l[i], rr[i]); return .{ .data = result }; @@ -378,7 +427,7 @@ pub fn Tensor( scales.argsOpt(), shape_, ) { - if (comptime hlp.isInt(T)) { + if (comptime isInt(T)) { var result: Vec = undefined; inline for (0..total) |i| result[i] = std.math.powi(T, self.data[i], exp) catch std.math.maxInt(T); @@ -440,8 +489,8 @@ pub fn Tensor( comptime std.debug.assert(Dest.total == total or Dest.total == 1); - const DestT = ActualDest.ValueType; - const ratio = comptime (scales.getFactor(dims) / ActualDest.scales.getFactor(ActualDest.dims)); + const DestT = ActualDest.ValueType; + const ratio = comptime (scales.getFactor(dims) / ActualDest.scales.getFactor(ActualDest.dims)); const DestVec = @Vector(total, DestT); // ── Same numeric type ────────────────────────────────────── @@ -455,7 +504,7 @@ pub fn Tensor( return .{ .data = self.data *| @as(Vec, @splat(mult)) }; } else { const div_val: T = comptime @intFromFloat(@round(1.0 / ratio)); - const half: T = comptime @divTrunc(div_val, 2); + const half: T = comptime @divTrunc(div_val, 2); var result: DestVec = undefined; inline for (0..total) |i| { const val = self.data[i]; @@ -473,14 +522,14 @@ pub fn Tensor( inline for (0..total) |i| { const float_val: f64 = switch (comptime @typeInfo(T)) { .float => @floatCast(self.data[i]), - .int => @floatFromInt(self.data[i]), - else => unreachable, + .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, + .int => @intFromFloat(@round(scaled)), + else => unreachable, }; } return .{ .data = result }; @@ -505,11 +554,11 @@ pub fn Tensor( /// Resolve both sides to the finer scale, broadcasting shape {1} RHS if needed. inline fn resolveScalePair(self: Self, rhs_q: anytype) struct { l: Vec, r: Vec } { - const RhsType = @TypeOf(rhs_q); - const TargetType = Tensor(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt(), shape_); + const RhsType = @TypeOf(rhs_q); + const TargetType = Tensor(T, dims.argsOpt(), finerScales(Self, RhsType).argsOpt(), shape_); const l: Vec = if (comptime Self == TargetType) self.data else self.to(TargetType).data; const rr: Vec = blk: { - const RhsNorm = Tensor(T, RhsType.dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt(), RhsType.shape); + const RhsNorm = Tensor(T, RhsType.dims.argsOpt(), finerScales(Self, RhsType).argsOpt(), RhsType.shape); const rn = if (comptime RhsType == RhsNorm) rhs_q else rhs_q.to(RhsNorm); break :blk broadcastToVec(RhsNorm, rn); }; @@ -604,39 +653,39 @@ pub fn Tensor( comptime axis_b: usize, ) blk: { const OT = @TypeOf(other); - comptime std.debug.assert(axis_a < rank); - comptime std.debug.assert(axis_b < OT.rank); - comptime std.debug.assert(shape_[axis_a] == OT.shape[axis_b]); + std.debug.assert(axis_a < rank); + std.debug.assert(axis_b < OT.rank); + std.debug.assert(shape_[axis_a] == OT.shape[axis_b]); // Contracted-away free axes; empty joint → scalar shape {1} - const sa = shapeRemoveAxis(shape_, axis_a); - const sb = shapeRemoveAxis(OT.shape, axis_b); + const sa = shapeRemoveAxis(shape_, axis_a); + const sb = shapeRemoveAxis(OT.shape, axis_b); const rs_raw = shapeCat(&sa, &sb); const rs: []const usize = if (rs_raw.len == 0) &.{1} else &rs_raw; break :blk Tensor( T, dims.add(OT.dims).argsOpt(), - hlp.finerScales(Self, OT).argsOpt(), + finerScales(Self, OT).argsOpt(), rs, ); } { const OT = @TypeOf(other); const k: usize = comptime shape_[axis_a]; // contraction dimension - const sa = comptime shapeRemoveAxis(shape_, axis_a); - const sb = comptime shapeRemoveAxis(OT.shape, axis_b); + const sa = comptime shapeRemoveAxis(shape_, axis_a); + const sb = comptime shapeRemoveAxis(OT.shape, axis_b); const rs_raw = comptime shapeCat(&sa, &sb); const rs: []const usize = comptime if (rs_raw.len == 0) &.{1} else &rs_raw; const ResultType = Tensor( T, dims.add(OT.dims).argsOpt(), - hlp.finerScales(Self, OT).argsOpt(), + finerScales(Self, OT).argsOpt(), rs, ); // Normalise scales before accumulation - const SelfNorm = Tensor(T, dims.argsOpt(), hlp.finerScales(Self, OT).argsOpt(), shape_); - const OtherNorm = Tensor(T, OT.dims.argsOpt(), hlp.finerScales(Self, OT).argsOpt(), OT.shape); + const SelfNorm = Tensor(T, dims.argsOpt(), finerScales(Self, OT).argsOpt(), shape_); + const OtherNorm = Tensor(T, OT.dims.argsOpt(), finerScales(Self, OT).argsOpt(), OT.shape); const a_data = if (comptime Self == SelfNorm) self.data else self.to(SelfNorm).data; const b_data = if (comptime OT == OtherNorm) other.data else other.to(OtherNorm).data; @@ -658,10 +707,10 @@ pub fn Tensor( // Reinsert the contracted index into free coords → full coord arrays const a_coords = comptime insertAxis(rank, axis_a, ki, &a_free); const b_coords = comptime insertAxis(OT.rank, axis_b, ki, &b_free); - const a_flat = comptime encodeFlatCoords(&a_coords, rank, _strides); - const b_flat = comptime encodeFlatCoords(&b_coords, OT.rank, OT.strides_arr); + const a_flat = comptime encodeFlatCoords(&a_coords, rank, _strides); + const b_flat = comptime encodeFlatCoords(&b_coords, OT.rank, OT.strides_arr); - if (comptime hlp.isInt(T)) + if (comptime isInt(T)) acc +|= a_data[a_flat] *| b_data[b_flat] else acc += a_data[a_flat] * b_data[b_flat]; @@ -712,10 +761,10 @@ pub fn Tensor( if (comptime total == 1) { 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, + .int, .comptime_int => try writer.printInt(self.data[0], 10, .lower, .{ + .width = options.width, .alignment = options.alignment, - .fill = options.fill, + .fill = options.fill, .precision = options.precision, }), else => unreachable, @@ -726,10 +775,10 @@ pub fn Tensor( 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, + .int, .comptime_int => try writer.printInt(self.data[i], 10, .lower, .{ + .width = options.width, .alignment = options.alignment, - .fill = options.fill, + .fill = options.fill, .precision = options.precision, }), else => unreachable, @@ -751,7 +800,7 @@ pub fn Tensor( else try writer.print("{s}{s}", .{ uscale.str(), bu.unit() }); - if (v != 1) try hlp.printSuperscript(writer, v); + if (v != 1) try printSuperscript(writer, v); } } }; @@ -773,23 +822,23 @@ pub fn Tensor( // ─── Scalar tests ───────────────────────────────────────────────────────── test "Scalar initiat" { - const Meter = Tensor(i128, .{ .L = 1 }, .{ .L = @enumFromInt(-3) }, &.{1}); - const Second = Tensor(f32, .{ .T = 1 }, .{ .T = .n }, &.{1}); + const Meter = Tensor(i128, .{ .L = 1 }, .{ .L = @enumFromInt(-3) }, &.{1}); + const Second = Tensor(f32, .{ .T = 1 }, .{ .T = .n }, &.{1}); const distance = Meter.splat(10); - const time = Second.splat(2); + const time = Second.splat(2); try std.testing.expectEqual(10, distance.data[0]); - try std.testing.expectEqual(2, time.data[0]); + try std.testing.expectEqual(2, time.data[0]); } test "Scalar comparisons (eq, ne, gt, gte, lt, lte)" { - const Meter = Tensor(i128, .{ .L = 1 }, .{}, &.{1}); + const Meter = Tensor(i128, .{ .L = 1 }, .{}, &.{1}); const KiloMeter = Tensor(i128, .{ .L = 1 }, .{ .L = .k }, &.{1}); const m1000 = Meter.splat(1000); - const km1 = KiloMeter.splat(1); - const km2 = KiloMeter.splat(2); + const km1 = KiloMeter.splat(1); + const km2 = KiloMeter.splat(2); try std.testing.expect(m1000.eq(km1)); try std.testing.expect(km1.eq(m1000)); @@ -807,65 +856,65 @@ test "Scalar comparisons (eq, ne, gt, gte, lt, lte)" { } test "Scalar Add" { - const Meter = Tensor(i128, .{ .L = 1 }, .{}, &.{1}); - const KiloMeter = Tensor(i128, .{ .L = 1 }, .{ .L = .k }, &.{1}); + const Meter = Tensor(i128, .{ .L = 1 }, .{}, &.{1}); + const KiloMeter = Tensor(i128, .{ .L = 1 }, .{ .L = .k }, &.{1}); const KiloMeter_f = Tensor(f64, .{ .L = 1 }, .{ .L = .k }, &.{1}); - const distance = Meter.splat(10); + const distance = Meter.splat(10); const distance2 = Meter.splat(20); - const added = distance.add(distance2); + const added = distance.add(distance2); try std.testing.expectEqual(30, added.data[0]); - try std.testing.expectEqual(1, @TypeOf(added).dims.get(.L)); + try std.testing.expectEqual(1, @TypeOf(added).dims.get(.L)); const distance3 = KiloMeter.splat(2); - const added2 = distance.add(distance3); + const added2 = distance.add(distance3); try std.testing.expectEqual(2010, added2.data[0]); const added3 = distance3.add(distance).to(KiloMeter); try std.testing.expectEqual(2, added3.data[0]); const distance4 = KiloMeter_f.splat(2); - const added4 = distance4.add(distance).to(KiloMeter_f); + const added4 = distance4.add(distance).to(KiloMeter_f); try std.testing.expectApproxEqAbs(2.01, added4.data[0], 0.000001); } test "Scalar Sub" { - const Meter = Tensor(i128, .{ .L = 1 }, .{}, &.{1}); - const KiloMeter_f = Tensor(f64, .{ .L = 1 }, .{ .L = .k }, &.{1}); + const Meter = Tensor(i128, .{ .L = 1 }, .{}, &.{1}); + const KiloMeter_f = Tensor(f64, .{ .L = 1 }, .{ .L = .k }, &.{1}); - const a = Meter.splat(500); - const b = Meter.splat(200); + const a = Meter.splat(500); + const b = Meter.splat(200); const diff = a.sub(b); - try std.testing.expectEqual(300, diff.data[0]); + try std.testing.expectEqual(300, diff.data[0]); const diff2 = b.sub(a); try std.testing.expectEqual(-300, diff2.data[0]); - const km_f = KiloMeter_f.splat(2.5); - const m_f = Meter.splat(500); + 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.data[0], 1e-4); } test "Scalar MulBy" { - const Meter = Tensor(i128, .{ .L = 1 }, .{}, &.{1}); - const Second = Tensor(f32, .{ .T = 1 }, .{}, &.{1}); + const Meter = Tensor(i128, .{ .L = 1 }, .{}, &.{1}); + const Second = Tensor(f32, .{ .T = 1 }, .{}, &.{1}); - const d = Meter.splat(3); - const t = Second.splat(4); + const d = Meter.splat(3); + const t = Second.splat(4); const at = d.mul(t); try std.testing.expectEqual(12, at.data[0]); - try std.testing.expectEqual(1, @TypeOf(at).dims.get(.L)); - try std.testing.expectEqual(1, @TypeOf(at).dims.get(.T)); + try std.testing.expectEqual(1, @TypeOf(at).dims.get(.L)); + try std.testing.expectEqual(1, @TypeOf(at).dims.get(.T)); - const d2 = Meter.splat(5); + const d2 = Meter.splat(5); const area = d.mul(d2); try std.testing.expectEqual(15, area.data[0]); - try std.testing.expectEqual(2, @TypeOf(area).dims.get(.L)); + try std.testing.expectEqual(2, @TypeOf(area).dims.get(.L)); } test "Scalar MulBy with scale" { const KiloMeter = Tensor(f32, .{ .L = 1 }, .{ .L = .k }, &.{1}); - const KiloGram = Tensor(f32, .{ .M = 1 }, .{ .M = .k }, &.{1}); + const KiloGram = Tensor(f32, .{ .M = 1 }, .{ .M = .k }, &.{1}); const dist = KiloMeter.splat(2.0); const mass = KiloGram.splat(3.0); @@ -875,9 +924,9 @@ test "Scalar MulBy with scale" { } test "Scalar MulBy with type change" { - const Meter = Tensor(i128, .{ .L = 1 }, .{ .L = .k }, &.{1}); - const Second = Tensor(f64, .{ .T = 1 }, .{}, &.{1}); - const KmSec = Tensor(i64, .{ .L = 1, .T = 1 }, .{ .L = .k }, &.{1}); + const Meter = Tensor(i128, .{ .L = 1 }, .{ .L = .k }, &.{1}); + const Second = Tensor(f64, .{ .T = 1 }, .{}, &.{1}); + const KmSec = Tensor(i64, .{ .L = 1, .T = 1 }, .{ .L = .k }, &.{1}); const KmSec_f = Tensor(f32, .{ .L = 1, .T = 1 }, .{ .L = .k }, &.{1}); const d = Meter.splat(3); @@ -888,103 +937,103 @@ test "Scalar MulBy with type change" { } test "Scalar MulBy small" { - const Meter = Tensor(i128, .{ .L = 1 }, .{ .L = .n }, &.{1}); - const Second = Tensor(f32, .{ .T = 1 }, .{}, &.{1}); + const Meter = Tensor(i128, .{ .L = 1 }, .{ .L = .n }, &.{1}); + const Second = Tensor(f32, .{ .T = 1 }, .{}, &.{1}); const d = Meter.splat(3); const t = Second.splat(4); try std.testing.expectEqual(12, d.mul(t).data[0]); } test "Scalar MulBy dimensionless" { - const DimLess = Tensor(i128, .{}, .{}, &.{1}); - const Meter = Tensor(i128, .{ .L = 1 }, .{}, &.{1}); - const d = Meter.splat(7); + const DimLess = Tensor(i128, .{}, .{}, &.{1}); + const Meter = Tensor(i128, .{ .L = 1 }, .{}, &.{1}); + const d = Meter.splat(7); const scaled = d.mul(DimLess.splat(3)); try std.testing.expectEqual(21, scaled.data[0]); } test "Scalar Sqrt" { - const MeterSquare = Tensor(i128, .{ .L = 2 }, .{}, &.{1}); - const MeterSquare_f = Tensor(f64, .{ .L = 2 }, .{}, &.{1}); + const MeterSquare = Tensor(i128, .{ .L = 2 }, .{}, &.{1}); + const MeterSquare_f = Tensor(f64, .{ .L = 2 }, .{}, &.{1}); - var d = MeterSquare.splat(9); + var d = MeterSquare.splat(9); var scaled = d.sqrt(); try std.testing.expectEqual(3, scaled.data[0]); try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L)); - d = MeterSquare.splat(-5); + d = MeterSquare.splat(-5); scaled = d.sqrt(); try std.testing.expectEqual(0, scaled.data[0]); - const d2 = MeterSquare_f.splat(20); + const d2 = MeterSquare_f.splat(20); const scaled2 = d2.sqrt(); try std.testing.expectApproxEqAbs(4.472135955, scaled2.data[0], 1e-4); } test "Scalar Chained: velocity and acceleration" { - const Meter = Tensor(i128, .{ .L = 1 }, .{}, &.{1}); - const Second = Tensor(f32, .{ .T = 1 }, .{}, &.{1}); + const Meter = Tensor(i128, .{ .L = 1 }, .{}, &.{1}); + const Second = Tensor(f32, .{ .T = 1 }, .{}, &.{1}); - const dist = Meter.splat(100); - const t1 = Second.splat(5); + const dist = Meter.splat(100); + const t1 = Second.splat(5); const velocity = dist.div(t1); try std.testing.expectEqual(20, velocity.data[0]); - const t2 = Second.splat(4); + const t2 = Second.splat(4); const accel = velocity.div(t2); try std.testing.expectEqual(5, accel.data[0]); } test "Scalar DivBy integer exact" { - const Meter = Tensor(i128, .{ .L = 1 }, .{}, &.{1}); - const Second = Tensor(f32, .{ .T = 1 }, .{}, &.{1}); + const Meter = Tensor(i128, .{ .L = 1 }, .{}, &.{1}); + const Second = Tensor(f32, .{ .T = 1 }, .{}, &.{1}); const dist = Meter.splat(120); const time = Second.splat(4); - const vel = dist.div(time); + const vel = dist.div(time); try std.testing.expectEqual(30, vel.data[0]); } test "Scalar Finer scales skip dim 0" { - const Dimless = Tensor(i128, .{}, .{}, &.{1}); - const KiloMetre = Tensor(i128, .{ .L = 1 }, .{ .L = .k }, &.{1}); + const Dimless = Tensor(i128, .{}, .{}, &.{1}); + const KiloMetre = Tensor(i128, .{ .L = 1 }, .{ .L = .k }, &.{1}); - const r = Dimless.splat(30); - const km = KiloMetre.splat(4); - const vel = r.mul(km); + const r = Dimless.splat(30); + const km = KiloMetre.splat(4); + const vel = r.mul(km); try std.testing.expectEqual(120, vel.data[0]); try std.testing.expectEqual(Scales.UnitScale.k, @TypeOf(vel).scales.get(.L)); } test "Scalar Conversion chain: km -> m -> cm" { - const KiloMeter = Tensor(i128, .{ .L = 1 }, .{ .L = .k }, &.{1}); - const Meter = Tensor(i128, .{ .L = 1 }, .{}, &.{1}); - const CentiMeter = Tensor(i128, .{ .L = 1 }, .{ .L = .c }, &.{1}); + const KiloMeter = Tensor(i128, .{ .L = 1 }, .{ .L = .k }, &.{1}); + const Meter = Tensor(i128, .{ .L = 1 }, .{}, &.{1}); + const CentiMeter = Tensor(i128, .{ .L = 1 }, .{ .L = .c }, &.{1}); const km = KiloMeter.splat(15); - const m = km.to(Meter); + const m = km.to(Meter); const cm = m.to(CentiMeter); - try std.testing.expectEqual(15_000, m.data[0]); + try std.testing.expectEqual(15_000, m.data[0]); try std.testing.expectEqual(1_500_000, cm.data[0]); } test "Scalar Conversion: hours -> minutes -> seconds" { - const Hour = Tensor(i128, .{ .T = 1 }, .{ .T = .hour }, &.{1}); - const Minute = Tensor(i128, .{ .T = 1 }, .{ .T = .min }, &.{1}); - const Second = Tensor(i128, .{ .T = 1 }, .{}, &.{1}); + const Hour = Tensor(i128, .{ .T = 1 }, .{ .T = .hour }, &.{1}); + const Minute = Tensor(i128, .{ .T = 1 }, .{ .T = .min }, &.{1}); + const Second = Tensor(i128, .{ .T = 1 }, .{}, &.{1}); - const h = Hour.splat(1); + const h = Hour.splat(1); const min = h.to(Minute); const sec = min.to(Second); - try std.testing.expectEqual(60, min.data[0]); + try std.testing.expectEqual(60, min.data[0]); try std.testing.expectEqual(3600, sec.data[0]); } test "Scalar Format" { const MeterPerSecondSq = Tensor(f32, .{ .L = 1, .T = -2 }, .{ .T = .n }, &.{1}); - const Meter = Tensor(f32, .{ .L = 1 }, .{}, &.{1}); + const Meter = Tensor(f32, .{ .L = 1 }, .{}, &.{1}); - const m = Meter.splat(1.23456); + const m = Meter.splat(1.23456); const accel = MeterPerSecondSq.splat(9.81); var buf: [64]u8 = undefined; @@ -996,23 +1045,23 @@ test "Scalar Format" { } test "Scalar Abs" { - const Meter = Tensor(i128, .{ .L = 1 }, .{}, &.{1}); - const MeterF = Tensor(f32, .{ .L = 1 }, .{}, &.{1}); + const Meter = Tensor(i128, .{ .L = 1 }, .{}, &.{1}); + const MeterF = Tensor(f32, .{ .L = 1 }, .{}, &.{1}); - try std.testing.expectEqual(50, Meter.splat(-50).abs().data[0]); + try std.testing.expectEqual(50, Meter.splat(-50).abs().data[0]); try std.testing.expectEqual(42.5, MeterF.splat(-42.5).abs().data[0]); } test "Scalar Pow" { const Meter = Tensor(i128, .{ .L = 1 }, .{}, &.{1}); - const d = Meter.splat(4); + const d = Meter.splat(4); try std.testing.expectEqual(16, d.pow(2).data[0]); try std.testing.expectEqual(64, d.pow(3).data[0]); } test "Scalar mul comptime_int" { const Meter = Tensor(i128, .{ .L = 1 }, .{}, &.{1}); - const d = Meter.splat(7); + const d = Meter.splat(7); try std.testing.expectEqual(21, d.mul(3).data[0]); } @@ -1020,16 +1069,16 @@ test "Scalar add/sub bare number on dimensionless scalar" { const DimLess = Tensor(i128, .{}, .{}, &.{1}); const a = DimLess.splat(10); try std.testing.expectEqual(15, a.add(5).data[0]); - try std.testing.expectEqual(7, a.sub(3).data[0]); + try std.testing.expectEqual(7, a.sub(3).data[0]); } test "Scalar Imperial length scales" { - const Foot = Tensor(f64, .{ .L = 1 }, .{ .L = .ft }, &.{1}); - const Meter = Tensor(f64, .{ .L = 1 }, .{}, &.{1}); - const Inch = Tensor(f64, .{ .L = 1 }, .{ .L = .inch }, &.{1}); + const Foot = Tensor(f64, .{ .L = 1 }, .{ .L = .ft }, &.{1}); + const Meter = Tensor(f64, .{ .L = 1 }, .{}, &.{1}); + const Inch = Tensor(f64, .{ .L = 1 }, .{ .L = .inch }, &.{1}); try std.testing.expectApproxEqAbs(0.3048, Foot.splat(1.0).to(Meter).data[0], 1e-9); - try std.testing.expectApproxEqAbs(1.0, Inch.splat(12.0).to(Foot).data[0], 1e-9); + try std.testing.expectApproxEqAbs(1.0, Inch.splat(12.0).to(Foot).data[0], 1e-9); } test "Scalar Imperial mass scales" { @@ -1057,10 +1106,10 @@ test "Vector initiate" { } test "Vector format" { - const MeterPerSecondSq = Tensor(f32, .{ .L = 1, .T = -2 }, .{ .T = .n }, &.{3}); - const KgMeterPerSecond = Tensor(f32, .{ .M = 1, .L = 1, .T = -1 }, .{ .M = .k }, &.{3}); + const MeterPerSecondSq = Tensor(f32, .{ .L = 1, .T = -2 }, .{ .T = .n }, &.{3}); + const KgMeterPerSecond = Tensor(f32, .{ .M = 1, .L = 1, .T = -1 }, .{ .M = .k }, &.{3}); - const accel = MeterPerSecondSq.splat(9.81); + const accel = MeterPerSecondSq.splat(9.81); const momentum = KgMeterPerSecond{ .data = .{ 43, 0, 11 } }; var buf: [64]u8 = undefined; @@ -1085,7 +1134,7 @@ test "Vector Vec3 Init and Basic Arithmetic" { try std.testing.expectEqual(5, v_def.data[2]); const v1 = Meter3{ .data = .{ 10, 20, 30 } }; - const v2 = Meter3{ .data = .{ 2, 4, 6 } }; + const v2 = Meter3{ .data = .{ 2, 4, 6 } }; const added = v1.add(v2); try std.testing.expectEqual(12, added.data[0]); @@ -1093,7 +1142,7 @@ test "Vector Vec3 Init and Basic Arithmetic" { 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(8, subbed.data[0]); try std.testing.expectEqual(16, subbed.data[1]); try std.testing.expectEqual(24, subbed.data[2]); @@ -1104,30 +1153,30 @@ test "Vector Vec3 Init and Basic Arithmetic" { } test "Vector Kinematics (scalar mul/div broadcast)" { - const Meter3 = Tensor(i32, .{ .L = 1 }, .{}, &.{3}); + const Meter3 = Tensor(i32, .{ .L = 1 }, .{}, &.{3}); const Second1 = Tensor(i32, .{ .T = 1 }, .{}, &.{1}); - const pos = Meter3{ .data = .{ 100, 200, 300 } }; + const pos = Meter3{ .data = .{ 100, 200, 300 } }; const time = Second1.splat(10); const vel = pos.div(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)); + 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)); const new_pos = vel.mul(time); try std.testing.expectEqual(100, new_pos.data[0]); - try std.testing.expectEqual(0, @TypeOf(new_pos).dims.get(.T)); + try std.testing.expectEqual(0, @TypeOf(new_pos).dims.get(.T)); } test "Vector Element-wise Math and Scaling" { const Meter3 = Tensor(i32, .{ .L = 1 }, .{}, &.{3}); - const v1 = Meter3{ .data = .{ 10, 20, 30 } }; - const v2 = Meter3{ .data = .{ 2, 5, 10 } }; - const dv = v1.div(v2); + const v1 = Meter3{ .data = .{ 10, 20, 30 } }; + const v2 = Meter3{ .data = .{ 2, 5, 10 } }; + const dv = v1.div(v2); try std.testing.expectEqual(5, dv.data[0]); try std.testing.expectEqual(4, dv.data[1]); try std.testing.expectEqual(3, dv.data[2]); @@ -1136,10 +1185,10 @@ test "Vector Element-wise Math and Scaling" { test "Vector Conversions" { const KiloMeter3 = Tensor(i32, .{ .L = 1 }, .{ .L = .k }, &.{3}); - const Meter3 = Tensor(i32, .{ .L = 1 }, .{}, &.{3}); + const Meter3 = Tensor(i32, .{ .L = 1 }, .{}, &.{3}); const v_km = KiloMeter3{ .data = .{ 1, 2, 3 } }; - const v_m = v_km.to(Meter3); + const v_m = v_km.to(Meter3); 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]); @@ -1147,38 +1196,38 @@ test "Vector Conversions" { } test "Vector Length" { - const MeterInt3 = Tensor(i32, .{ .L = 1 }, .{}, &.{3}); + const MeterInt3 = Tensor(i32, .{ .L = 1 }, .{}, &.{3}); const MeterFloat3 = Tensor(f32, .{ .L = 1 }, .{}, &.{3}); const v_int = MeterInt3{ .data = .{ 3, 4, 0 } }; try std.testing.expectEqual(25, v_int.lengthSqr()); - try std.testing.expectEqual(5, v_int.length()); + try std.testing.expectEqual(5, v_int.length()); const v_float = MeterFloat3{ .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); + try std.testing.expectApproxEqAbs(@as(f32, 5.0), v_float.length(), 1e-4); } test "Vector Comparisons" { - const Meter3 = Tensor(f32, .{ .L = 1 }, .{}, &.{3}); - const KiloMeter3 = Tensor(f32, .{ .L = 1 }, .{ .L = .k }, &.{3}); + const Meter3 = Tensor(f32, .{ .L = 1 }, .{}, &.{3}); + const KiloMeter3 = Tensor(f32, .{ .L = 1 }, .{ .L = .k }, &.{3}); - const v1 = Meter3{ .data = .{ 1000.0, 500.0, 0.0 } }; - const v2 = KiloMeter3{ .data = .{ 1.0, 0.5, 0.0 } }; - const v3 = KiloMeter3{ .data = .{ 1.0, 0.6, 0.0 } }; + const v1 = Meter3{ .data = .{ 1000.0, 500.0, 0.0 } }; + const v2 = KiloMeter3{ .data = .{ 1.0, 0.5, 0.0 } }; + const v3 = KiloMeter3{ .data = .{ 1.0, 0.6, 0.0 } }; try std.testing.expect(v1.eqAll(v2)); try std.testing.expect(v1.neAll(v3)); const higher = v3.gt(v1); try std.testing.expectEqual(false, higher[0]); - try std.testing.expectEqual(true, higher[1]); + try std.testing.expectEqual(true, higher[1]); try std.testing.expectEqual(false, higher[2]); const equal = v3.eq(v1); - try std.testing.expectEqual(true, equal[0]); + try std.testing.expectEqual(true, equal[0]); try std.testing.expectEqual(false, equal[1]); - try std.testing.expectEqual(true, equal[2]); + try std.testing.expectEqual(true, equal[2]); const low_eq = v1.lte(v3); try std.testing.expect(low_eq[0] and low_eq[1] and low_eq[2]); @@ -1186,7 +1235,7 @@ test "Vector Comparisons" { test "Vector vs Scalar broadcast comparison" { // Replaces the old eqScalar / gtScalar — now just eq / gt with a shape-{1} rhs. - const Meter3 = Tensor(f32, .{ .L = 1 }, .{}, &.{3}); + const Meter3 = Tensor(f32, .{ .L = 1 }, .{}, &.{3}); const KiloMeter1 = Tensor(f32, .{ .L = 1 }, .{ .L = .k }, &.{1}); const positions = Meter3{ .data = .{ 500.0, 1200.0, 3000.0 } }; @@ -1194,21 +1243,21 @@ test "Vector vs Scalar broadcast comparison" { const exceeded = positions.gt(threshold); try std.testing.expectEqual(false, exceeded[0]); - try std.testing.expectEqual(true, exceeded[1]); - try std.testing.expectEqual(true, exceeded[2]); + try std.testing.expectEqual(true, exceeded[1]); + try std.testing.expectEqual(true, exceeded[2]); - const Meter1 = Tensor(f32, .{ .L = 1 }, .{}, &.{1}); - const exact = positions.eq(Meter1.splat(500)); + const Meter1 = Tensor(f32, .{ .L = 1 }, .{}, &.{1}); + const exact = positions.eq(Meter1.splat(500)); try std.testing.expect(exact[0] == true); try std.testing.expect(exact[1] == false); } test "Vector contract — dot product (rank-1 × rank-1)" { - const Meter3 = Tensor(f32, .{ .L = 1 }, .{}, &.{3}); - const Newton3 = Tensor(f32, .{ .M = 1, .L = 1, .T = -2 }, .{}, &.{3}); + const Meter3 = Tensor(f32, .{ .L = 1 }, .{}, &.{3}); + const Newton3 = Tensor(f32, .{ .M = 1, .L = 1, .T = -2 }, .{}, &.{3}); - const pos = Meter3{ .data = .{ 10.0, 0.0, 0.0 } }; - const force = Newton3{ .data = .{ 5.0, 5.0, 0.0 } }; + const pos = Meter3{ .data = .{ 10.0, 0.0, 0.0 } }; + const force = Newton3{ .data = .{ 5.0, 5.0, 0.0 } }; // work = force · pos const work = force.contract(pos, 0, 0); @@ -1220,8 +1269,8 @@ test "Vector contract — dot product (rank-1 × rank-1)" { test "Vector contract — matrix multiply (rank-2 × rank-2)" { // 2×3 matrix multiplied by 3×2 matrix → 2×2 result - const A = Tensor(f32, .{}, .{}, &.{2, 3}); - const B = Tensor(f32, .{}, .{}, &.{3, 2}); + const A = Tensor(f32, .{}, .{}, &.{ 2, 3 }); + const B = Tensor(f32, .{}, .{}, &.{ 3, 2 }); // A = [[1, 2, 3], // [4, 5, 6]] @@ -1237,16 +1286,16 @@ test "Vector contract — matrix multiply (rank-2 × rank-2)" { // C[1][0] = 4*7 + 5*9 + 6*11 = 28 + 45 + 66 = 139 // C[1][1] = 4*8 + 5*10 + 6*12 = 32 + 50 + 72 = 154 const c = a.contract(b, 1, 0); - try std.testing.expectEqual(58, c.data[Tensor(f32, .{}, .{}, &.{2,2}).idx(.{0, 0})]); - try std.testing.expectEqual(64, c.data[Tensor(f32, .{}, .{}, &.{2,2}).idx(.{0, 1})]); - try std.testing.expectEqual(139, c.data[Tensor(f32, .{}, .{}, &.{2,2}).idx(.{1, 0})]); - try std.testing.expectEqual(154, c.data[Tensor(f32, .{}, .{}, &.{2,2}).idx(.{1, 1})]); + try std.testing.expectEqual(58, c.data[Tensor(f32, .{}, .{}, &.{ 2, 2 }).idx(.{ 0, 0 })]); + try std.testing.expectEqual(64, c.data[Tensor(f32, .{}, .{}, &.{ 2, 2 }).idx(.{ 0, 1 })]); + try std.testing.expectEqual(139, c.data[Tensor(f32, .{}, .{}, &.{ 2, 2 }).idx(.{ 1, 0 })]); + try std.testing.expectEqual(154, c.data[Tensor(f32, .{}, .{}, &.{ 2, 2 }).idx(.{ 1, 1 })]); } test "Vector Abs, Pow, Sqrt and Product" { const Meter3 = Tensor(f32, .{ .L = 1 }, .{}, &.{3}); - const v1 = Meter3{ .data = .{ -2.0, 3.0, -4.0 } }; + const v1 = Meter3{ .data = .{ -2.0, 3.0, -4.0 } }; 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]); @@ -1256,7 +1305,7 @@ test "Vector Abs, Pow, Sqrt and Product" { try std.testing.expectEqual(3, @TypeOf(vol).dims.get(.L)); const area_vec = v_abs.pow(2); - try std.testing.expectEqual(4.0, area_vec.data[0]); + 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)); @@ -1268,7 +1317,7 @@ test "Vector Abs, Pow, Sqrt and Product" { test "Vector mul comptime_int broadcast" { const Meter3 = Tensor(i32, .{ .L = 1 }, .{}, &.{3}); - const v = Meter3{ .data = .{ 1, 2, 3 } }; + const v = Meter3{ .data = .{ 1, 2, 3 } }; const scaled = v.mul(10); try std.testing.expectEqual(10, scaled.data[0]); try std.testing.expectEqual(20, scaled.data[1]); @@ -1278,8 +1327,8 @@ test "Vector mul comptime_int broadcast" { test "Vector mul comptime_float broadcast" { const MeterF3 = Tensor(f32, .{ .L = 1 }, .{}, &.{3}); - const v = MeterF3{ .data = .{ 1.0, 2.0, 4.0 } }; - const scaled = v.mul(0.5); + const v = MeterF3{ .data = .{ 1.0, 2.0, 4.0 } }; + const scaled = v.mul(0.5); 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); @@ -1288,9 +1337,9 @@ test "Vector mul comptime_float broadcast" { test "Vector div comptime_int broadcast" { const Meter3 = Tensor(i32, .{ .L = 1 }, .{}, &.{3}); - const v = Meter3{ .data = .{ 10, 20, 30 } }; + const v = Meter3{ .data = .{ 10, 20, 30 } }; const halved = v.div(2); - try std.testing.expectEqual(5, halved.data[0]); + 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)); @@ -1298,8 +1347,8 @@ test "Vector div comptime_int broadcast" { test "Vector div comptime_float broadcast" { const MeterF3 = Tensor(f64, .{ .L = 1 }, .{}, &.{3}); - const v = MeterF3{ .data = .{ 9.0, 6.0, 3.0 } }; - const r = v.div(3.0); + const v = MeterF3{ .data = .{ 9.0, 6.0, 3.0 } }; + const r = v.div(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); @@ -1311,22 +1360,22 @@ test "Vector eq broadcast on dimensionless" { const eq_res = v.eq(2); try std.testing.expectEqual(false, eq_res[0]); - try std.testing.expectEqual(true, eq_res[1]); + try std.testing.expectEqual(true, eq_res[1]); try std.testing.expectEqual(false, eq_res[2]); const gt_res = v.gt(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]); + try std.testing.expectEqual(true, gt_res[1]); + try std.testing.expectEqual(true, gt_res[2]); } test "Tensor idx helper and matrix access" { - const Mat3x3 = Tensor(f32, .{}, .{}, &.{3, 3}); + const Mat3x3 = Tensor(f32, .{}, .{}, &.{ 3, 3 }); // Identity-like: set [0][0]=1, [1][1]=2, [2][2]=3 var m: Mat3x3 = Mat3x3.zero; - m.data[Mat3x3.idx(.{0, 0})] = 1.0; - m.data[Mat3x3.idx(.{1, 1})] = 2.0; - m.data[Mat3x3.idx(.{2, 2})] = 3.0; + m.data[Mat3x3.idx(.{ 0, 0 })] = 1.0; + m.data[Mat3x3.idx(.{ 1, 1 })] = 2.0; + m.data[Mat3x3.idx(.{ 2, 2 })] = 3.0; try std.testing.expectEqual(1.0, m.data[0]); // [0][0] try std.testing.expectEqual(2.0, m.data[4]); // [1][1] (stride 3 → 1*3+1=4) @@ -1336,13 +1385,13 @@ test "Tensor idx helper and matrix access" { test "Tensor strides_arr correctness" { const T1 = Tensor(f32, .{}, .{}, &.{3}); - const T2 = Tensor(f32, .{}, .{}, &.{3, 4}); - const T3 = Tensor(f32, .{}, .{}, &.{2, 3, 4}); + const T2 = Tensor(f32, .{}, .{}, &.{ 3, 4 }); + const T3 = Tensor(f32, .{}, .{}, &.{ 2, 3, 4 }); - try std.testing.expectEqual(1, T1.strides_arr[0]); - try std.testing.expectEqual(4, T2.strides_arr[0]); - try std.testing.expectEqual(1, T2.strides_arr[1]); + try std.testing.expectEqual(1, T1.strides_arr[0]); + try std.testing.expectEqual(4, T2.strides_arr[0]); + try std.testing.expectEqual(1, T2.strides_arr[1]); try std.testing.expectEqual(12, T3.strides_arr[0]); - try std.testing.expectEqual(4, T3.strides_arr[1]); - try std.testing.expectEqual(1, T3.strides_arr[2]); + try std.testing.expectEqual(4, T3.strides_arr[1]); + try std.testing.expectEqual(1, T3.strides_arr[2]); } diff --git a/src/helper.zig b/src/helper.zig deleted file mode 100644 index fb104e1..0000000 --- a/src/helper.zig +++ /dev/null @@ -1,97 +0,0 @@ -const std = @import("std"); - -pub fn isInt(comptime T: type) bool { - return @typeInfo(T) == .int or @typeInfo(T) == .comptime_int; -} - -pub fn printSuperscript(writer: *std.Io.Writer, n: i32) !void { - if (n == 0) return; - var val = n; - if (val < 0) { - try writer.writeAll("\u{207B}"); - val = -val; - } - var buf: [12]u8 = undefined; - const str = std.fmt.bufPrint(&buf, "{d}", .{val}) catch return; - for (str) |c| { - const s = switch (c) { - '0' => "\u{2070}", - '1' => "\u{00B9}", - '2' => "\u{00B2}", - '3' => "\u{00B3}", - '4' => "\u{2074}", - '5' => "\u{2075}", - '6' => "\u{2076}", - '7' => "\u{2077}", - '8' => "\u{2078}", - '9' => "\u{2079}", - else => unreachable, - }; - try writer.writeAll(s); - } -} - -const Scales = @import("Scales.zig"); -const Dimensions = @import("Dimensions.zig"); -const Dimension = @import("Dimensions.zig").Dimension; - -pub fn finerScales(comptime T1: type, comptime T2: type) Scales { - const d1: Dimensions = T1.dims; - const d2: Dimensions = T2.dims; - const s1: Scales = T1.scales; - const s2: Scales = T2.scales; - comptime var out = Scales.initFill(.none); - inline for (std.enums.values(Dimension)) |dim| { - const scale1 = comptime s1.get(dim); - const scale2 = comptime s2.get(dim); - out.set(dim, if (comptime d1.get(dim) == 0 and d2.get(dim) == 0) - .none - else if (comptime d1.get(dim) == 0) - scale2 - else if (comptime d2.get(dim) == 0) - scale1 - else if (comptime scale1.getFactor() > scale2.getFactor()) - scale2 - else - scale1); - } - comptime return out; -} - -// --------------------------------------------------------------------------- -// RHS normalisation helpers -// --------------------------------------------------------------------------- - -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, "ISQUANTITY") and - @field(T, "ISQUANTITY"); -} - -/// Resolve the Scalar type that `rhs` will be treated as. -/// -/// Accepted rhs types: -/// - Any `Scalar_` type → returned as-is -/// - `comptime_int` / `comptime_float` → dimensionless `Scalar_(BaseT, {}, {})` -/// - `BaseT` (the scalar's value type) → dimensionless `Scalar_(BaseT, {}, {})` -/// -/// Everything else is a compile error, including other int/float types. -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 == ValueType) - return Quantity(ValueType, N, .{}, .{}); - @compileError( - "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 toRhsQuantity(comptime BaseT: type, N: usize, rhs: anytype) rhsQuantityType(BaseT, N, @TypeOf(rhs)) { - if (comptime isScalarType(@TypeOf(rhs))) return rhs; - const DimLess = Quantity(BaseT, N, .{}, .{}); - return DimLess{ .data = @splat(@as(BaseT, rhs)) }; -} diff --git a/src/main.zig b/src/main.zig index 4d61ee6..33ba626 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,15 +1,13 @@ const std = @import("std"); -pub const Vector = @import("Quantity.zig").Vector; -pub const Scalar = @import("Quantity.zig").Scalar; +pub const Tensor = @import("Tensor.zig").Tensor; pub const Dimensions = @import("Dimensions.zig"); pub const Scales = @import("Scales.zig"); pub const Base = @import("Base.zig"); test { - _ = @import("Quantity.zig"); + _ = @import("Tensor.zig"); _ = @import("Dimensions.zig"); _ = @import("Scales.zig"); _ = @import("Base.zig"); - _ = @import("helper.zig"); }