Compare commits

...

9 Commits

Author SHA1 Message Date
AdrienBouvais
52e58829eb Added test to BaseQuantity and a benchmark for Vectors 2026-04-21 14:19:22 +02:00
AdrienBouvais
f2e18da797 Forgot to save this from previous commit 2026-04-21 13:33:27 +02:00
AdrienBouvais
8f589d3818 Created a BaseQuantity and movec Quantity and QuantityVec to seperat files 2026-04-21 13:18:53 +02:00
AdrienBouvais
fd423f2bf6 Changed QuantityVec3 to QuantityVecX for vectors of any size 2026-04-21 13:08:37 +02:00
AdrienBouvais
de210588ee Moved some fn 2026-04-21 11:44:48 +02:00
AdrienBouvais
a484e2e7da Optimized length to be optimized for int 2026-04-21 11:43:29 +02:00
AdrienBouvais
38bf79e741 Removed old function usless after optimization 2026-04-21 11:26:39 +02:00
AdrienBouvais
d9ab2f304a Optimized divBy to prevent type conversion 2026-04-21 11:23:55 +02:00
AdrienBouvais
19de4e1dd2 Optimized Quantity.to 2026-04-21 11:02:10 +02:00
5 changed files with 1123 additions and 822 deletions

153
src/BaseQuantities.zig Normal file
View File

@ -0,0 +1,153 @@
const std = @import("std");
// Adjust these imports to match your actual file names
const Dimensions = @import("Dimensions.zig");
const Scales = @import("Scales.zig");
const Quantity = @import("Quantity.zig").Quantity;
/// Helper function to create a clean namespace for each physical dimension.
/// It exposes the raw dimensions, and easy type-creators for Base or Scaled variants.
pub fn QtyNamespace(comptime d: anytype) type {
return struct {
pub const dims = Dimensions.init(d);
/// Creates a Quantity of this dimension using default scales.
/// Example: const V = Quantities.Velocity.Base(f32);
pub fn Base(comptime T: type) type {
return Quantity(T, dims, Scales.init(.{}));
}
/// Creates a Quantity of this dimension using custom scales.
/// Example: const Kmh = Quantities.Velocity.Scaled(f32, Scales.init(.{ .L = .k, .T = .hour }));
pub fn Scaled(comptime T: type, comptime s: Scales) type {
return Quantity(T, dims, s);
}
};
}
// ==========================================
// Base Quantities
// ==========================================
pub const Meter = QtyNamespace(.{ .L = 1 });
pub const Second = QtyNamespace(.{ .T = 1 });
pub const Gramm = QtyNamespace(.{ .M = 1 });
pub const Kelvin = QtyNamespace(.{ .Tr = 1 });
pub const ElectricCurrent = QtyNamespace(.{ .I = 1 });
// ==========================================
// Electric
// ==========================================
pub const ElectricConductivity = QtyNamespace(.{ .M = -1, .L = -3, .T = 3, .I = 2 });
pub const ElectricCharge = QtyNamespace(.{ .T = 1, .I = 1 });
pub const ElectricPotential = QtyNamespace(.{ .T = -3, .L = 2, .M = 1, .I = -1 });
pub const ElectricResistance = QtyNamespace(.{ .M = 1, .L = 2, .T = -3, .I = -2 });
pub const ElectricResistivity = QtyNamespace(.{ .M = 1, .L = 3, .T = -3, .I = -2 });
pub const ElectricCapacitance = QtyNamespace(.{ .T = 4, .L = -2, .M = -1, .I = 2 });
pub const ElectricImpedance = ElectricResistance;
pub const MagneticFlux = QtyNamespace(.{ .M = 1, .L = 2, .T = -2, .I = -1 });
pub const MagneticDensity = QtyNamespace(.{ .M = 1, .T = -2, .I = -1 });
pub const MagneticStrength = QtyNamespace(.{ .L = -1, .I = 1 }); // Fixed typo from MagneticStrengh
pub const MagneticMoment = QtyNamespace(.{ .L = 2, .I = 1 });
// ==========================================
// Movement
// ==========================================
pub const Velocity = QtyNamespace(.{ .L = 1, .T = -1 });
pub const Acceleration = QtyNamespace(.{ .L = 1, .T = -2 });
pub const Inertia = QtyNamespace(.{ .M = 1, .L = 2 });
// ==========================================
// Forces / Energy
// ==========================================
pub const Force = QtyNamespace(.{ .T = -2, .M = 1, .L = 1 });
pub const Pressure = QtyNamespace(.{ .T = -2, .L = -1, .M = 1 });
pub const Energy = QtyNamespace(.{ .T = -2, .L = 2, .M = 1 });
pub const Power = QtyNamespace(.{ .T = -3, .L = 2, .M = 1 });
// ==========================================
// Dimension
// ==========================================
pub const Area = QtyNamespace(.{ .L = 2 });
pub const Volume = QtyNamespace(.{ .L = 3 });
pub const AreaDensity = QtyNamespace(.{ .M = 1, .L = -2 });
pub const Density = QtyNamespace(.{ .M = 1, .L = -3 });
// ==========================================
// Thermal
// ==========================================
pub const ThermalHeat = Energy;
pub const ThermalWork = Energy;
pub const ThermalCapacity = QtyNamespace(.{ .M = 1, .L = 2, .T = -2, .Tr = -1 });
pub const ThermalCapacityPerMass = QtyNamespace(.{ .L = 2, .T = -2, .Tr = -1 });
pub const ThermalFluxDensity = QtyNamespace(.{ .M = 1, .T = -3 }); // Fixed typo from ThermalluxDensity
pub const ThermalConductance = QtyNamespace(.{ .M = 1, .L = 2, .T = -3, .Tr = -1 });
pub const ThermalConductivity = QtyNamespace(.{ .M = 1, .L = 1, .T = -3, .Tr = -1 });
pub const ThermalResistance = QtyNamespace(.{ .M = -1, .L = -2, .T = 3, .Tr = 1 });
pub const ThermalResistivity = QtyNamespace(.{ .M = -1, .L = -1, .T = 3, .Tr = 1 });
pub const ThermalEntropy = QtyNamespace(.{ .M = 1, .L = 2, .T = -2, .Tr = -1 });
// ==========================================
// Others
// ==========================================
pub const Frequency = QtyNamespace(.{ .T = -1 });
pub const Viscosity = QtyNamespace(.{ .M = 1, .L = -1, .T = -1 });
pub const SurfaceTension = QtyNamespace(.{ .M = 1, .T = -2 }); // Corrected from MT-2a
test "BaseQuantities - Core dimensions instantiation" {
// Basic types via generic wrappers
const M = Meter.Base(f32);
const distance = M{ .value = 100.0 };
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 = Velocity.Scaled(f32, Scales.init(.{ .L = .k, .T = .hour }));
const speed = Kmh{ .value = 120.0 };
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.Base(f32){ .value = 50.0 };
const t = Second.Base(f32){ .value = 2.0 };
// Velocity = Distance / Time
const v = d.divBy(t);
try std.testing.expectEqual(25.0, v.value);
try std.testing.expect(Velocity.dims.eql(@TypeOf(v).dims));
// Acceleration = Velocity / Time
const a = v.divBy(t);
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, Scales.init(.{ .M = .k })){ .value = 10.0 };
// 9.8 m/s^2
const a = Acceleration.Base(f32){ .value = 9.8 };
// Force = mass * acceleration
const f = m.mulBy(a);
try std.testing.expectEqual(98000, f.value);
try std.testing.expect(Force.dims.eql(@TypeOf(f).dims));
// Energy (Work) = Force * distance
const distance = Meter.Base(f32){ .value = 5.0 };
const energy = f.mulBy(distance);
try std.testing.expectEqual(490000, energy.value);
try std.testing.expect(Energy.dims.eql(@TypeOf(energy).dims));
}
test "BaseQuantities - Electric combinations" {
const current = ElectricCurrent.Base(f32){ .value = 2.0 }; // 2 A
const time = Second.Base(f32){ .value = 3.0 }; // 3 s
// Charge = Current * time
const charge = current.mulBy(time);
try std.testing.expectEqual(6.0, charge.value);
try std.testing.expect(ElectricCharge.dims.eql(@TypeOf(charge).dims));
}

529
src/Quantity.zig Normal file
View File

@ -0,0 +1,529 @@
const std = @import("std");
const hlp = @import("helper.zig");
const QuantityVec = @import("QuantityVec.zig").QuantityVec;
const Scales = @import("Scales.zig");
const UnitScale = Scales.UnitScale;
const Dimensions = @import("Dimensions.zig");
const Dimension = Dimensions.Dimension;
pub fn Quantity(T: type, d: Dimensions, s: Scales) type {
return struct {
value: T,
const Self = @This();
pub const Vec3: type = QuantityVec(3, Self);
pub const ValueType: type = T;
pub const dims: Dimensions = d;
pub const scales = s;
pub fn add(self: Self, rhs: anytype) Quantity(
T,
dims,
scales.min(@TypeOf(rhs).scales),
) {
if (comptime !dims.eql(@TypeOf(rhs).dims))
@compileError("Dimension mismatch in add: " ++ dims.str() ++ " vs " ++ @TypeOf(rhs).dims.str());
const TargetType = Quantity(T, dims, scales.min(@TypeOf(rhs).scales));
const lhs_converted = self.to(TargetType);
const rhs_converted = rhs.to(TargetType);
return .{ .value = lhs_converted.value + rhs_converted.value };
}
pub fn sub(self: Self, rhs: anytype) Quantity(
T,
dims,
scales.min(@TypeOf(rhs).scales),
) {
if (comptime !dims.eql(@TypeOf(rhs).dims))
@compileError("Dimension mismatch in sub: " ++ dims.str() ++ " vs " ++ @TypeOf(rhs).dims.str());
const TargetType = Quantity(T, dims, scales.min(@TypeOf(rhs).scales));
const lhs_converted = self.to(TargetType);
const rhs_converted = rhs.to(TargetType);
return .{ .value = lhs_converted.value - rhs_converted.value };
}
pub fn mulBy(self: Self, rhs: anytype) Quantity(
T,
dims.add(@TypeOf(rhs).dims),
scales.min(@TypeOf(rhs).scales),
) {
const self_ = self.to(Quantity(T, dims, scales.min(@TypeOf(rhs).scales)));
const rhs_ = rhs.to(Quantity(T, @TypeOf(rhs).dims, scales.min(@TypeOf(rhs).scales)));
return .{ .value = self_.value * rhs_.value };
}
pub fn divBy(self: Self, rhs: anytype) Quantity(T, dims.sub(@TypeOf(rhs).dims), scales.min(@TypeOf(rhs).scales)) {
const self_ = self.to(Quantity(T, dims, scales.min(@TypeOf(rhs).scales)));
const rhs_ = rhs.to(Quantity(T, @TypeOf(rhs).dims, scales.min(@TypeOf(rhs).scales)));
if (comptime @typeInfo(T) == .int) {
return .{ .value = @divTrunc(self_.value, rhs_.value) };
} else {
return .{ .value = self_.value / rhs_.value };
}
}
pub fn scale(self: Self, sc: T) Self {
return .{ .value = self.value * sc };
}
pub 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 (ratio >= 1.0 and @round(ratio) == ratio) {
const mult: DestT = @intFromFloat(ratio);
return .{ .value = @as(DestT, @intCast(self.value)) * mult };
} else if (ratio < 1.0 and @round(1.0 / ratio) == 1.0 / ratio) {
const div: DestT = @intFromFloat(1.0 / ratio);
const val = @as(DestT, @intCast(self.value));
const half = div / 2;
// Native round-to-nearest
const rounded = if (val >= 0) @divTrunc(val + half, div) else @divTrunc(val - half, div);
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)) {
.int => @as(DestT, @floatFromInt(self.value)),
.float => @as(DestT, @floatCast(self.value)),
else => unreachable,
};
return .{ .value = val_f * @as(DestT, @floatCast(ratio)) };
} else {
const val_f = switch (@typeInfo(T)) {
.int => @as(f64, @floatFromInt(self.value)),
.float => @as(f64, @floatCast(self.value)),
else => unreachable,
};
return .{ .value = @intFromFloat(@round(val_f * ratio)) };
}
}
pub fn Vec(self: Self, comptime len: usize) QuantityVec(len, Self) {
return QuantityVec(len, Self).initDefault(self.value);
}
pub fn vec3(self: Self) Vec3 {
return Vec3.initDefault(self.value);
}
pub fn format(
self: Self,
writer: *std.Io.Writer,
) !void {
try writer.print("{d}", .{self.value});
var iter = std.EnumSet(Dimension).initFull().iterator();
var first = true;
while (iter.next()) |bu| {
const v = dims.get(bu);
if (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 = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = -3 }));
const Second = Quantity(f32, Dimensions.init(.{ .T = 1 }), Scales.init(.{ .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 "Add" {
const Meter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
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));
std.debug.print("KiloMeter {f} + {f} = {f} OK\n", .{ distance, distance2, added });
const KiloMeter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .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));
std.debug.print("KiloMeter {f} + {f} = {f} OK\n", .{ distance, distance3, added2 });
const added3 = distance3.add(distance).to(KiloMeter);
try std.testing.expectEqual(2, added3.value);
try std.testing.expectEqual(1, @TypeOf(added3).dims.get(.L));
std.debug.print("KiloMeter {f} + {f} = {f} OK\n", .{ distance3, distance, added3 });
const KiloMeter_f = Quantity(f64, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .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));
std.debug.print("KiloMeter_f {f} + {f} = {f} OK\n", .{ distance4, distance, added4 });
}
test "Sub" {
const Meter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const KiloMeter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .k }));
const KiloMeter_f = Quantity(f64, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .k }));
const a = Meter{ .value = 500 };
const b = Meter{ .value = 200 };
const diff = a.sub(b);
try std.testing.expectEqual(300, diff.value);
std.debug.print("Sub: {f} - {f} = {f} OK\n", .{ a, b, diff });
const km = KiloMeter{ .value = 1 };
const diff2 = a.sub(km);
std.debug.print("Sub cross-scale: {f} - {f} = {f}\n", .{ a, km, diff2 });
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);
std.debug.print("Sub float cross-scale: {f} - {f} = {f} OK\n", .{ km_f, m_f, diff3 });
}
test "MulBy" {
const Meter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const Second = Quantity(f32, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
const d = Meter{ .value = 3.0 };
const t = Second{ .value = 4.0 };
const area_time = d.mulBy(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));
std.debug.print("MulBy: {f} * {f} = {f} OK\n", .{ d, t, area_time });
const d2 = Meter{ .value = 5.0 };
const area = d.mulBy(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));
std.debug.print("MulBy: {f} * {f} = {f} OK\n", .{ d, d2, area });
}
test "MulBy with scale" {
const KiloMeter = Quantity(f32, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .k }));
const KiloGram = Quantity(f32, Dimensions.init(.{ .M = 1 }), Scales.init(.{ .M = .k }));
const dist = KiloMeter{ .value = 2.0 };
const mass = KiloGram{ .value = 3.0 };
const prod = dist.mulBy(mass);
try std.testing.expectEqual(1, @TypeOf(prod).dims.get(.L));
try std.testing.expectEqual(1, @TypeOf(prod).dims.get(.M));
std.debug.print("MulBy scaled: {f} * {f} = {f} OK\n", .{ dist, mass, prod });
}
test "MulBy with type change" {
const Meter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .k }));
const Second = Quantity(f64, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
const KmSec = Quantity(i64, Dimensions.init(.{ .L = 1, .T = 1 }), Scales.init(.{ .L = .k }));
const KmSec_f = Quantity(f32, Dimensions.init(.{ .L = 1, .T = 1 }), Scales.init(.{ .L = .k }));
const d = Meter{ .value = 3.0 };
const t = Second{ .value = 4.0 };
const area_time = d.mulBy(t).to(KmSec);
const area_time_f = d.mulBy(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));
std.debug.print("MulBy: {f} * {f} = {f} OK\n", .{ d, t, area_time });
}
test "MulBy small" {
const Meter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .n }));
const Second = Quantity(f32, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
const d = Meter{ .value = 3.0 };
const t = Second{ .value = 4.0 };
const area_time = d.mulBy(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));
std.debug.print("MulBy: {f} * {f} = {f} OK\n", .{ d, t, area_time });
}
test "Scale" {
const Meter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const Second = Quantity(f32, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
const d = Meter{ .value = 7 };
const scaled = d.scale(3);
try std.testing.expectEqual(21, scaled.value);
try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L));
std.debug.print("Scale int: {f} * 3 = {f} OK\n", .{ d, scaled });
const t = Second{ .value = 1.5 };
const scaled_f = t.scale(4.0);
try std.testing.expectApproxEqAbs(@as(f32, 6.0), scaled_f.value, 1e-4);
std.debug.print("Scale float: {f} * 4 = {f} OK\n", .{ t, scaled_f });
}
test "Chained: velocity and acceleration" {
const Meter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const Second = Quantity(f32, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
const dist = Meter{ .value = 100.0 };
const t1 = Second{ .value = 5.0 };
const velocity = dist.divBy(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.divBy(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));
std.debug.print("Velocity: {f}, Acceleration: {f} OK\n", .{ velocity, accel });
}
test "DivBy integer exact" {
const Meter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const Second = Quantity(f32, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
const dist = Meter{ .value = 120 };
const time = Second{ .value = 4 };
const vel = dist.divBy(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));
std.debug.print("DivBy int: {f} / {f} = {f} OK\n", .{ dist, time, vel });
}
test "Conversion chain: km -> m -> cm" {
const KiloMeter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .k }));
const Meter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const CentiMeter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .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);
std.debug.print("Chain: {f} -> {f} -> {f} OK\n", .{ km, m, cm });
}
test "Conversion: hours -> minutes -> seconds" {
const Hour = Quantity(i128, Dimensions.init(.{ .T = 1 }), Scales.init(.{ .T = .hour }));
const Minute = Quantity(i128, Dimensions.init(.{ .T = 1 }), Scales.init(.{ .T = .min }));
const Second = Quantity(i128, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
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);
std.debug.print("Time chain: {f} -> {f} -> {f} OK\n", .{ h, min, sec });
}
test "Negative values" {
const Meter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const a = Meter{ .value = 5 };
const b = Meter{ .value = 20 };
const diff = a.sub(b);
try std.testing.expectEqual(-15, diff.value);
std.debug.print("Negative sub: {f} - {f} = {f} OK\n", .{ a, b, diff });
}
test "Format Quantity" {
const MeterPerSecondSq = Quantity(
f32,
Dimensions.init(.{ .L = 1, .T = -2 }),
Scales.init(.{ .T = .n }),
);
const KgMeterPerSecond = Quantity(
f32,
Dimensions.init(.{ .M = 1, .L = 1, .T = -1 }),
Scales.init(.{ .M = .k }),
);
const accel = MeterPerSecondSq{ .value = 9.81 };
const momentum = KgMeterPerSecond{ .value = 42.0 };
std.debug.print("Acceleration: {f}\n", .{accel});
std.debug.print("Momentum: {f}\n", .{momentum});
}
test "Benchmark" {
const Io = std.Io;
const ITERS: usize = 100_000;
const SAMPLES: usize = 10; // Number of samples for stats
var gsink: f64 = 0;
const io = std.testing.io;
// Standard Zig 0.16 timestamp retrieval
const getTime = struct {
fn f(i: Io) Io.Timestamp {
return Io.Clock.awake.now(i);
}
}.f;
const fold = struct {
fn f(comptime TT: type, s: *f64, v: TT) void {
s.* += if (comptime @typeInfo(TT) == .float)
@as(f64, @floatCast(v))
else
@as(f64, @floatFromInt(v));
}
}.f;
const getVal = struct {
fn f(comptime TT: type, i: usize, comptime mask: u7) TT {
const v: u8 = @as(u8, @truncate(i & @as(usize, mask))) + 1;
return if (comptime @typeInfo(TT) == .float) @floatFromInt(v) else @intCast(v);
}
}.f;
const Stats = struct {
median: f64,
delta: f64,
ops_per_sec: f64,
};
const computeStats = struct {
fn f(samples: []f64, iters: usize) Stats {
std.mem.sort(f64, samples, {}, std.sort.asc(f64));
const mid = samples.len / 2;
const median_ns = if (samples.len % 2 == 0) (samples[mid - 1] + samples[mid]) / 2.0 else samples[mid];
const low = samples[0];
const high = samples[samples.len - 1];
const delta_ns = (high - low) / 2.0;
const ns_per_op = median_ns / @as(f64, @floatFromInt(iters));
return .{
.median = ns_per_op,
.delta = (delta_ns / @as(f64, @floatFromInt(iters))),
.ops_per_sec = 1_000_000_000.0 / ns_per_op,
};
}
}.f;
std.debug.print(
\\
\\ Quantity<T> benchmark — {d} iterations, {d} samples/cell
\\
\\┌───────────────────┬──────┬─────────────────────┬─────────────────────┐
\\│ Operation │ Type │ ns / op (± delta) │ Throughput (ops/s) │
\\├───────────────────┼──────┼─────────────────────┼─────────────────────┤
\\
, .{ ITERS, SAMPLES });
const Types = .{ i16, i32, i64, i128, i256, f32, f64, f128 };
const TNames = .{ "i16", "i32", "i64", "i128", "i256", "f32", "f64", "f128" };
const Ops = .{ "add", "sub", "mulBy", "divBy", "scale", "to" };
var results_matrix: [Ops.len][Types.len]f64 = undefined;
comptime var tidx: usize = 0;
inline for (Types, TNames) |T, tname| {
const M = Quantity(T, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const KM = Quantity(T, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .k }));
const S = Quantity(T, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
inline for (Ops, 0..) |op_name, oidx| {
var samples: [SAMPLES]f64 = undefined;
for (0..SAMPLES) |s_idx| {
var sink: T = 0;
const t_start = getTime(io);
for (0..ITERS) |i| {
const r = if (comptime std.mem.eql(u8, op_name, "add"))
(M{ .value = getVal(T, i, 63) }).add(M{ .value = 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) })
else if (comptime std.mem.eql(u8, op_name, "mulBy"))
(M{ .value = getVal(T, i, 63) }).mulBy(M{ .value = getVal(T, i +% 1, 63) })
else if (comptime std.mem.eql(u8, op_name, "divBy"))
(M{ .value = getVal(T, i +% 10, 63) }).divBy(S{ .value = getVal(T, i, 63) })
else if (comptime std.mem.eql(u8, op_name, "scale"))
(M{ .value = getVal(T, i, 63) }).scale(getVal(T, i +% 2, 63))
else
(KM{ .value = getVal(T, i, 15) }).to(M);
if (comptime @typeInfo(T) == .float) sink += r.value else sink ^= r.value;
}
const t_end = getTime(io);
samples[s_idx] = @as(f64, @floatFromInt(t_start.durationTo(t_end).toNanoseconds()));
fold(T, &gsink, sink);
}
const stats = computeStats(&samples, ITERS);
results_matrix[oidx][tidx] = stats.median;
std.debug.print("│ {s:<17} │ {s:<4} │ {d:>8.2} ns ±{d:<6.2} │ {d:>19.0} │\n", .{ op_name, tname, stats.median, stats.delta, stats.ops_per_sec });
}
if (comptime tidx < Types.len - 1) {
std.debug.print("├───────────────────┼──────┼─────────────────────┼─────────────────────┤\n", .{});
}
tidx += 1;
}
// Median Summary Table
std.debug.print("└───────────────────┴──────┴─────────────────────┴─────────────────────┘\n\n", .{});
std.debug.print("Median Summary (ns/op):\n", .{});
std.debug.print("┌──────────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┐\n", .{});
std.debug.print("│ Operation │ i16 │ i32 │ i64 │ i128 │ i256 │ f32 │ f64 │ f128 │\n", .{});
std.debug.print("├──────────────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┤\n", .{});
inline for (Ops, 0..) |op_name, oidx| {
std.debug.print("│ {s:<11} │", .{op_name});
var i: usize = 0;
while (i < Types.len) : (i += 1) {
std.debug.print("{d:>6.1} │", .{results_matrix[oidx][i]});
}
std.debug.print("\n", .{});
}
std.debug.print("└──────────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘\n", .{});
std.debug.print("\nAnti-optimisation sink: {d:.4}\n", .{gsink});
try std.testing.expect(gsink != 0);
}

428
src/QuantityVec.zig Normal file
View File

@ -0,0 +1,428 @@
const std = @import("std");
const hlp = @import("helper.zig");
const Quantity = @import("Quantity.zig").Quantity;
const Scales = @import("Scales.zig");
const UnitScale = Scales.UnitScale;
const Dimensions = @import("Dimensions.zig");
const Dimension = Dimensions.Dimension;
pub fn QuantityVec(comptime len: usize, comptime Q: type) type {
const T = Q.ValueType;
const d: Dimensions = Q.dims;
const s: Scales = Q.scales;
return struct {
data: [len]T,
const Self = @This();
pub const QuantityType = Q;
pub const ValueType = T;
pub const dims: Dimensions = d;
pub const scales = s;
pub const zero = initDefault(0);
pub const one = initDefault(1);
pub fn initDefault(v: T) Self {
var data: [len]T = undefined;
for (&data) |*item| item.* = v;
return .{ .data = data };
}
pub fn add(self: Self, rhs: anytype) QuantityVec(len, Quantity(T, d, s.min(@TypeOf(rhs).scales))) {
const Tr = @TypeOf(rhs);
var res: QuantityVec(len, Quantity(T, d, s.min(Tr.scales))) = undefined;
for (self.data, 0..) |v, i| {
const q = (Q{ .value = v }).add(Tr.QuantityType{ .value = rhs.data[i] });
res.data[i] = q.value;
}
return res;
}
pub fn sub(self: Self, rhs: anytype) QuantityVec(len, Quantity(T, d, s.min(@TypeOf(rhs).scales))) {
const Tr = @TypeOf(rhs);
var res: QuantityVec(len, Quantity(T, d, s.min(Tr.scales))) = undefined;
for (self.data, 0..) |v, i| {
const q = (Q{ .value = v }).sub(Tr.QuantityType{ .value = rhs.data[i] });
res.data[i] = q.value;
}
return res;
}
pub fn divBy(
self: Self,
rhs: anytype,
) QuantityVec(len, Quantity(T, d.sub(@TypeOf(rhs).dims), s.min(@TypeOf(rhs).scales))) {
const Tr = @TypeOf(rhs);
var res: QuantityVec(len, Quantity(T, d.sub(Tr.dims), s.min(Tr.scales))) = undefined;
for (self.data, 0..) |v, i| {
const q = (Q{ .value = v }).divBy(Tr.QuantityType{ .value = rhs.data[i] });
res.data[i] = q.value;
}
return res;
}
pub fn mulBy(
self: Self,
rhs: anytype,
) QuantityVec(len, Quantity(T, d.add(@TypeOf(rhs).dims), s.min(@TypeOf(rhs).scales))) {
const Tr = @TypeOf(rhs);
var res: QuantityVec(len, Quantity(T, d.add(Tr.dims), s.min(Tr.scales))) = undefined;
for (self.data, 0..) |v, i| {
const q = (Q{ .value = v }).mulBy(Tr.QuantityType{ .value = rhs.data[i] });
res.data[i] = q.value;
}
return res;
}
pub fn divByScalar(
self: Self,
scalar: anytype,
) QuantityVec(len, Quantity(T, d.sub(@TypeOf(scalar).dims), s.min(@TypeOf(scalar).scales))) {
var res: QuantityVec(len, Quantity(T, d.sub(@TypeOf(scalar).dims), s.min(@TypeOf(scalar).scales))) = undefined;
for (self.data, 0..) |v, i| {
const q = Q{ .value = v };
res.data[i] = q.divBy(scalar).value;
}
return res;
}
pub fn mulByScalar(
self: Self,
scalar: anytype,
) QuantityVec(len, Quantity(T, d.add(@TypeOf(scalar).dims), s.min(@TypeOf(scalar).scales))) {
var res: QuantityVec(len, Quantity(T, d.add(@TypeOf(scalar).dims), s.min(@TypeOf(scalar).scales))) = undefined;
for (self.data, 0..) |v, i| {
const q = Q{ .value = v };
res.data[i] = q.mulBy(scalar).value;
}
return res;
}
pub fn negate(self: Self) Self {
var res: Self = undefined;
for (self.data, 0..) |v, i| {
res.data[i] = -v;
}
return res;
}
pub fn scale(self: Self, rhs: T) Self {
var res: Self = undefined;
for (self.data, 0..) |v, i| {
res.data[i] = (Q{ .value = v }).scale(rhs).value;
}
return res;
}
pub fn to(self: Self, comptime DestQ: type) QuantityVec(len, DestQ) {
var res: QuantityVec(len, DestQ) = undefined;
for (self.data, 0..) |v, i| {
res.data[i] = (Q{ .value = v }).to(DestQ).value;
}
return res;
}
pub fn lengthSqr(self: Self) T {
var sum: T = 0;
for (self.data) |v| {
sum += v * v;
}
return sum;
}
pub fn length(self: Self) T {
const len_sq = self.lengthSqr();
if (comptime @typeInfo(T) == .int) {
// Construct the unsigned equivalent of T at comptime (e.g., i32 -> u32)
const UnsignedT = @Int(.unsigned, @typeInfo(T).int.bits);
// len_sq is always positive, so @intCast is perfectly safe
const u_len_sq = @as(UnsignedT, @intCast(len_sq));
return @as(T, @intCast(std.math.sqrt(u_len_sq)));
} else {
return @sqrt(len_sq);
}
}
pub fn format(self: Self, writer: *std.Io.Writer) !void {
try writer.writeAll("(");
for (self.data, 0..) |v, i| {
if (i > 0) try writer.writeAll(", ");
try writer.print("{d:.2}", .{v});
}
try writer.writeAll(")");
var iter = std.EnumSet(Dimension).initFull().iterator();
var first = true;
while (iter.next()) |bu| {
const v = dims.get(bu);
if (v == 0) continue;
if (!first) try writer.writeAll(".");
first = false;
try writer.print("{s}{s}", .{ scales.get(bu).str(), bu.unit() });
if (v != 1) try hlp.printSuperscript(writer, v);
}
}
};
}
test "Format VectorX" {
const MeterPerSecondSq = Quantity(
f32,
Dimensions.init(.{ .L = 1, .T = -2 }),
Scales.init(.{ .T = .n }),
);
const KgMeterPerSecond = Quantity(
f32,
Dimensions.init(.{ .M = 1, .L = 1, .T = -1 }),
Scales.init(.{ .M = .k }),
);
const accel = MeterPerSecondSq.Vec3.initDefault(9.81);
const momentum = KgMeterPerSecond.Vec3{ .data = .{ 43, 0, 11 } };
std.debug.print("Acceleration: {f}\n", .{accel});
std.debug.print("Momentum: {f}\n", .{momentum});
}
test "VecX Init and Basic Arithmetic" {
const Meter = Quantity(i32, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
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 = Quantity(i32, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const Second = Quantity(i32, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
const Vec3M = Meter.Vec3;
const pos = Vec3M{ .data = .{ 100, 200, 300 } };
const time = Second{ .value = 10 };
// Vector divided by scalar Quantity (Velocity = Position / Time)
const vel = pos.divByScalar(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 Quantity (Position = Velocity * Time)
const new_pos = vel.mulByScalar(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 = Quantity(i32, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const Vec3M = Meter.Vec3;
const v1 = Vec3M{ .data = .{ 10, 20, 30 } };
const v2 = Vec3M{ .data = .{ 2, 5, 10 } };
// Element-wise division
const div = v1.divBy(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
// Scale by primitive
const scaled = v1.scale(2);
try std.testing.expectEqual(20, scaled.data[0]);
try std.testing.expectEqual(40, scaled.data[1]);
try std.testing.expectEqual(60, scaled.data[2]);
}
test "VecX Conversions" {
const KiloMeter = Quantity(i32, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .k }));
const Meter = Quantity(i32, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
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 = Quantity(i32, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const MeterFloat = Quantity(f32, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
// Integer length (using your custom isqrt)
// 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 "Benchmark QuantityVec ops" {
const Io = std.Io;
const ITERS: usize = 10_000;
const SAMPLES: usize = 10;
var gsink: f64 = 0;
// In Zig 0.14+, we use the testing IO for clock access in tests
const io = std.testing.io;
const getTime = struct {
fn f(i: Io) Io.Timestamp {
return Io.Clock.awake.now(i);
}
}.f;
const getVal = struct {
fn f(comptime TT: type, i: usize, comptime mask: u7) TT {
const v: u8 = @as(u8, @truncate(i & @as(usize, mask))) + 1;
return if (comptime @typeInfo(TT) == .float) @floatFromInt(v) else @intCast(v);
}
}.f;
const fold = struct {
fn f(comptime TT: type, s: *f64, v: TT) void {
s.* += if (comptime @typeInfo(TT) == .float)
@as(f64, @floatCast(v))
else
@as(f64, @floatFromInt(v));
}
}.f;
const computeStats = struct {
fn f(samples: []f64, iters: usize) f64 {
std.mem.sort(f64, samples, {}, std.sort.asc(f64));
const mid = samples.len / 2;
const median_ns = if (samples.len % 2 == 0)
(samples[mid - 1] + samples[mid]) / 2.0
else
samples[mid];
return median_ns / @as(f64, @floatFromInt(iters));
}
}.f;
std.debug.print(
\\
\\ QuantityVec<N, T> benchmark — {d} iterations, {d} samples/cell
\\ (Results in ns/op)
\\
\\┌─────────────┬──────┬─────────┬─────────┬─────────┐
\\│ Operation │ Type │ Len=3 │ Len=4 │ Len=16 │
\\├─────────────┼──────┼─────────┼─────────┼─────────┤
\\
, .{ ITERS, SAMPLES });
const Types = .{ i32, i64, i128, f32, f64 };
const TNames = .{ "i32", "i64", "i128", "f32", "f64" };
const Lengths = .{ 3, 4, 16 };
const Ops = .{ "add", "scale", "mulByScalar", "length" };
inline for (Ops, 0..) |op_name, o_idx| {
inline for (Types, TNames) |T, tname| {
std.debug.print("│ {s:<11} │ {s:<4} │", .{ op_name, tname });
inline for (Lengths) |len| {
const Q_base = Quantity(T, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const Q_time = Quantity(T, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
const V = QuantityVec(len, Q_base);
var samples: [SAMPLES]f64 = undefined;
for (0..SAMPLES) |s_idx| {
var sink: T = 0;
const t_start = getTime(io);
for (0..ITERS) |i| {
const v1 = V.initDefault(getVal(T, i, 63));
if (comptime std.mem.eql(u8, op_name, "add")) {
const v2 = V.initDefault(getVal(T, i +% 7, 63));
const res = v1.add(v2);
for (res.data) |val| {
if (comptime @typeInfo(T) == .float) sink += val else sink ^= val;
}
} else if (comptime std.mem.eql(u8, op_name, "scale")) {
const sc = getVal(T, i +% 2, 63);
const res = v1.scale(sc);
for (res.data) |val| {
if (comptime @typeInfo(T) == .float) sink += val else sink ^= val;
}
} else if (comptime std.mem.eql(u8, op_name, "mulByScalar")) {
const s_val = Q_time{ .value = getVal(T, i +% 2, 63) };
const res = v1.mulByScalar(s_val);
for (res.data) |val| {
if (comptime @typeInfo(T) == .float) sink += val else sink ^= val;
}
} else if (comptime std.mem.eql(u8, op_name, "length")) {
const r = v1.length();
if (comptime @typeInfo(T) == .float) sink += r else sink ^= r;
}
}
const t_end = getTime(io);
samples[s_idx] = @as(f64, @floatFromInt(t_start.durationTo(t_end).toNanoseconds()));
fold(T, &gsink, sink);
}
const median_ns_per_op = computeStats(&samples, ITERS);
std.debug.print(" {d:>7.1} │", .{median_ns_per_op});
}
std.debug.print("\n", .{});
}
if (o_idx < Ops.len - 1) {
std.debug.print("├─────────────┼──────┼─────────┼─────────┼─────────┤\n", .{});
}
}
std.debug.print("└─────────────┴──────┴─────────┴─────────┴─────────┘\n", .{});
std.debug.print("\nAnti-optimisation sink: {d:.4}\n", .{gsink});
try std.testing.expect(gsink != 0);
}

View File

@ -82,6 +82,7 @@ pub fn set(self: *Scales, key: Dimension, val: UnitScale) void {
} }
pub fn min(comptime s1: Scales, comptime s2: Scales) Scales { pub fn min(comptime s1: Scales, comptime s2: Scales) Scales {
@setEvalBranchQuota(10_000);
var out = Scales.initFill(.none); var out = Scales.initFill(.none);
for (std.enums.values(Dimension)) |dim| for (std.enums.values(Dimension)) |dim|
out.set(dim, if (s1.get(dim).getFactorInt() > s2.get(dim).getFactorInt()) s2.get(dim) else s1.get(dim)); out.set(dim, if (s1.get(dim).getFactorInt() > s2.get(dim).getFactorInt()) s2.get(dim) else s1.get(dim));

View File

@ -1,826 +1,16 @@
const std = @import("std"); const std = @import("std");
const hlp = @import("helper.zig");
const Scales = @import("Scales.zig"); pub const Quantity = @import("Quantity.zig").Quantity;
const UnitScale = Scales.UnitScale; pub const QuantityVec = @import("QuantityVec.zig").QuantityVec;
const Dimensions = @import("Dimensions.zig"); pub const Dimensions = @import("Dimensions.zig");
const Dimension = Dimensions.Dimension; pub const Scales = @import("Scales.zig");
pub const Base = @import("BaseQuantities.zig");
pub fn Quantity(T: type, d: Dimensions, s: Scales) type { test {
return struct { _ = @import("Quantity.zig");
value: T, _ = @import("QuantityVec.zig");
_ = @import("Dimensions.zig");
const Self = @This(); _ = @import("Scales.zig");
pub const Vec3: type = QuantityVec3(Self); _ = @import("BaseQuantities.zig");
pub const ValueType: type = T; _ = @import("helper.zig");
pub const dims: Dimensions = d;
pub const scales = s;
/// Internal helper to convert any supported T to f64 for math
fn toF64(val: anytype) f64 {
const TIn = @TypeOf(val);
return switch (@typeInfo(TIn)) {
.int => @floatFromInt(val),
.float => @floatCast(val),
else => @compileError("Unsupported type for Quantity"),
};
}
/// Internal helper to convert f64 back to the target T
fn fromF64(val: f64) T {
return switch (@typeInfo(T)) {
.int => @intFromFloat(@round(val)),
.float => @floatCast(val),
else => unreachable,
};
}
/// Helper for integer power of 10 at comptime
fn pow10(comptime exp: i32) T {
var res: T = 1;
var i: i32 = 0;
const abs_exp = if (exp < 0) -exp else exp;
while (i < abs_exp) : (i += 1) res *= 10;
return res;
}
pub fn add(self: Self, rhs: anytype) Quantity(
T,
dims,
scales.min(@TypeOf(rhs).scales),
) {
if (comptime !dims.eql(@TypeOf(rhs).dims))
@compileError("Dimension mismatch in add");
const TargetType = Quantity(T, dims, scales.min(@TypeOf(rhs).scales));
const lhs_converted = self.to(TargetType);
const rhs_converted = rhs.to(TargetType);
return .{ .value = lhs_converted.value + rhs_converted.value };
}
pub fn sub(self: Self, rhs: anytype) Quantity(
T,
dims,
scales.min(@TypeOf(rhs).scales),
) {
if (comptime !dims.eql(@TypeOf(rhs).dims))
@compileError("Dimension mismatch in sub");
const TargetType = Quantity(T, dims, scales.min(@TypeOf(rhs).scales));
const lhs_converted = self.to(TargetType);
const rhs_converted = rhs.to(TargetType);
return .{ .value = lhs_converted.value - rhs_converted.value };
}
pub fn mulBy(self: Self, rhs: anytype) Quantity(
T,
dims.add(@TypeOf(rhs).dims),
scales.min(@TypeOf(rhs).scales),
) {
const self_ = self.to(Quantity(T, dims, scales.min(@TypeOf(rhs).scales)));
const rhs_ = rhs.to(Quantity(T, @TypeOf(rhs).dims, scales.min(@TypeOf(rhs).scales)));
return .{ .value = self_.value * rhs_.value };
}
pub fn divBy(self: Self, rhs: anytype) Quantity(
T,
dims.sub(@TypeOf(rhs).dims),
scales.min(@TypeOf(rhs).scales),
) {
const self_ = self.to(Quantity(T, dims, scales.min(@TypeOf(rhs).scales)));
const rhs_ = rhs.to(Quantity(T, @TypeOf(rhs).dims, scales.min(@TypeOf(rhs).scales)));
return .{ .value = fromF64((toF64(self_.value) / toF64(rhs_.value))) };
}
pub fn scale(self: Self, sc: T) Self {
return .{ .value = self.value * sc };
}
pub fn to(self: Self, comptime Dest: type) Dest {
if (comptime !dims.eql(Dest.dims))
@compileError("Dimension mismatch: " ++ dims.str() ++ " vs " ++ Dest.dims.str());
if (comptime @TypeOf(self) == Dest)
return self;
const source_factor = scales.getFactor(dims);
const dest_factor = Dest.scales.getFactor(Dest.dims);
const ratio = source_factor / dest_factor;
const result_f = toF64(self.value) * ratio;
const DestT = Dest.ValueType;
return .{ .value = switch (@typeInfo(DestT)) {
.int => @intFromFloat(@round(result_f)),
.float => @floatCast(result_f),
else => unreachable,
} };
}
pub fn vec3(self: Self) Vec3 {
return .{ .x = self.value, .y = self.value, .z = self.value };
}
pub fn format(
self: Self,
writer: *std.Io.Writer,
) !void {
try writer.print("{d}", .{self.value});
var iter = std.EnumSet(Dimension).initFull().iterator();
var first = true;
while (iter.next()) |bu| {
const v = dims.get(bu);
if (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);
}
}
};
}
pub fn QuantityVec3(Q: type) type {
const T = Q.ValueType;
const d: Dimensions = Q.dims;
const s: Scales = Q.scales;
return struct {
x: T,
y: T,
z: T,
const Self = @This();
pub const QuantityType = Q;
pub const ValueType = T;
pub const dims: Dimensions = d;
pub const scales = s;
pub const zero = Self{ .x = 0, .y = 0, .z = 0 };
pub const one = Self{ .x = 1, .y = 1, .z = 1 };
pub fn initDefault(v: T) Self {
return .{ .x = v, .y = v, .z = v };
}
pub fn add(self: Self, rhs: anytype) QuantityVec3(Quantity(T, d, s.min(@TypeOf(rhs).scales))) {
const Tr = @TypeOf(rhs);
// We leverage the logic in the scalar Quantity.add
const qx = (Q{ .value = self.x }).add(Tr.QuantityType{ .value = rhs.x });
const qy = (Q{ .value = self.y }).add(Tr.QuantityType{ .value = rhs.y });
const qz = (Q{ .value = self.z }).add(Tr.QuantityType{ .value = rhs.z });
return .{
.x = qx.value,
.y = qy.value,
.z = qz.value,
};
}
pub fn sub(self: Self, rhs: anytype) QuantityVec3(Quantity(T, d, s.min(@TypeOf(rhs).scales))) {
const Tr = @TypeOf(rhs);
const qx = (Q{ .value = self.x }).sub(Tr.QuantityType{ .value = rhs.x });
const qy = (Q{ .value = self.y }).sub(Tr.QuantityType{ .value = rhs.y });
const qz = (Q{ .value = self.z }).sub(Tr.QuantityType{ .value = rhs.z });
return .{
.x = qx.value,
.y = qy.value,
.z = qz.value,
};
}
pub fn divBy(
self: Self,
rhs: anytype,
) QuantityVec3(Quantity(T, d.sub(@TypeOf(rhs).dims), s.min(@TypeOf(rhs).scales))) {
const Tr = @TypeOf(rhs);
return .{
.x = (Q{ .value = self.x }).divBy(Tr.QuantityType{ .value = rhs.x }).value,
.y = (Q{ .value = self.y }).divBy(Tr.QuantityType{ .value = rhs.y }).value,
.z = (Q{ .value = self.z }).divBy(Tr.QuantityType{ .value = rhs.z }).value,
};
}
pub fn mulBy(
self: Self,
rhs: anytype,
) QuantityVec3(Quantity(T, d.sub(@TypeOf(rhs).dims), s.min(@TypeOf(rhs).scales))) {
const Tr = @TypeOf(rhs);
return .{
.x = (Q{ .value = self.x }).mulBy(Tr.QuantityType{ .value = rhs.x }).value,
.y = (Q{ .value = self.y }).mulBy(Tr.QuantityType{ .value = rhs.y }).value,
.z = (Q{ .value = self.z }).mulBy(Tr.QuantityType{ .value = rhs.z }).value,
};
}
pub fn divByScalar(
self: Self,
scalar: anytype,
) QuantityVec3(Quantity(T, d.sub(@TypeOf(scalar).dims), s.min(@TypeOf(scalar).scales))) {
const q_x = Q{ .value = self.x };
const q_y = Q{ .value = self.y };
const q_z = Q{ .value = self.z };
return .{
.x = q_x.divBy(scalar).value,
.y = q_y.divBy(scalar).value,
.z = q_z.divBy(scalar).value,
};
}
pub fn mulByScalar(
self: Self,
scalar: anytype,
) QuantityVec3(Quantity(T, d.add(@TypeOf(scalar).dims), s.min(@TypeOf(scalar).scales))) {
const q_x = Q{ .value = self.x };
const q_y = Q{ .value = self.y };
const q_z = Q{ .value = self.z };
return .{
.x = q_x.mulBy(scalar).value,
.y = q_y.mulBy(scalar).value,
.z = q_z.mulBy(scalar).value,
};
}
pub fn negate(self: Self) Self {
return .{ .x = -self.x, .y = -self.y, .z = -self.z };
}
pub fn scale(self: Self, rhs: T) Self {
return .{
.x = (Q{ .value = self.x }).scale(rhs).value,
.y = (Q{ .value = self.y }).scale(rhs).value,
.z = (Q{ .value = self.z }).scale(rhs).value,
};
}
pub fn to(self: Self, comptime DestQ: type) QuantityVec3(DestQ) {
return .{
.x = (Q{ .value = self.x }).to(DestQ).value,
.y = (Q{ .value = self.y }).to(DestQ).value,
.z = (Q{ .value = self.z }).to(DestQ).value,
};
}
pub fn format(self: Self, writer: *std.Io.Writer) !void {
try writer.print("({d:.2}, {d:.2}, {d:.2})", .{ self.x, self.y, self.z });
var iter = std.EnumSet(Dimension).initFull().iterator();
var first = true;
while (iter.next()) |bu| {
const v = dims.get(bu);
if (v == 0) continue;
if (!first) try writer.writeAll(".");
first = false;
try writer.print("{s}{s}", .{ scales.get(bu).str(), bu.unit() });
if (v != 1) try hlp.printSuperscript(writer, v);
}
}
pub fn lengthSqr(self: Self) T {
return self.x * self.x + self.y * self.y + self.z * self.z;
}
pub fn length(self: Self) T {
if (comptime hlp.isInt(T))
return self.isqrt()
else
return @sqrt(self.x * self.x + self.y * self.y + self.z * self.z);
}
fn isqrt(self: Self) T {
const squared_sum = (self.x * self.x) + (self.y * self.y) + (self.z * self.z);
if (squared_sum <= 0) return 0;
var x = squared_sum;
var y = @divTrunc(x + 1, 2);
while (y < x) {
x = y;
y = @divTrunc(x + @divTrunc(squared_sum, x), 2);
}
return x;
}
};
}
pub fn main(_: std.process.Init) void {}
test "Generate quantity" {
const Meter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = -3 }));
const Second = Quantity(f32, Dimensions.init(.{ .T = 1 }), Scales.init(.{ .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 "Add" {
const Meter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
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));
std.debug.print("KiloMeter {f} + {f} = {f} OK\n", .{ distance, distance2, added });
const KiloMeter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .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));
std.debug.print("KiloMeter {f} + {f} = {f} OK\n", .{ distance, distance3, added2 });
const added3 = distance3.add(distance).to(KiloMeter);
try std.testing.expectEqual(2, added3.value);
try std.testing.expectEqual(1, @TypeOf(added3).dims.get(.L));
std.debug.print("KiloMeter {f} + {f} = {f} OK\n", .{ distance3, distance, added3 });
const KiloMeter_f = Quantity(f64, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .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));
std.debug.print("KiloMeter_f {f} + {f} = {f} OK\n", .{ distance4, distance, added4 });
}
test "Sub" {
const Meter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const KiloMeter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .k }));
const KiloMeter_f = Quantity(f64, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .k }));
const a = Meter{ .value = 500 };
const b = Meter{ .value = 200 };
const diff = a.sub(b);
try std.testing.expectEqual(300, diff.value);
std.debug.print("Sub: {f} - {f} = {f} OK\n", .{ a, b, diff });
const km = KiloMeter{ .value = 1 };
const diff2 = a.sub(km);
std.debug.print("Sub cross-scale: {f} - {f} = {f}\n", .{ a, km, diff2 });
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);
std.debug.print("Sub float cross-scale: {f} - {f} = {f} OK\n", .{ km_f, m_f, diff3 });
}
test "MulBy" {
const Meter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const Second = Quantity(f32, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
const d = Meter{ .value = 3.0 };
const t = Second{ .value = 4.0 };
const area_time = d.mulBy(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));
std.debug.print("MulBy: {f} * {f} = {f} OK\n", .{ d, t, area_time });
const d2 = Meter{ .value = 5.0 };
const area = d.mulBy(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));
std.debug.print("MulBy: {f} * {f} = {f} OK\n", .{ d, d2, area });
}
test "MulBy with scale" {
const KiloMeter = Quantity(f32, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .k }));
const KiloGram = Quantity(f32, Dimensions.init(.{ .M = 1 }), Scales.init(.{ .M = .k }));
const dist = KiloMeter{ .value = 2.0 };
const mass = KiloGram{ .value = 3.0 };
const prod = dist.mulBy(mass);
try std.testing.expectEqual(1, @TypeOf(prod).dims.get(.L));
try std.testing.expectEqual(1, @TypeOf(prod).dims.get(.M));
std.debug.print("MulBy scaled: {f} * {f} = {f} OK\n", .{ dist, mass, prod });
}
test "MulBy with type change" {
const Meter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .k }));
const Second = Quantity(f32, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
const KmSec = Quantity(f32, Dimensions.init(.{ .L = 1, .T = 1 }), Scales.init(.{ .L = .k }));
const d = Meter{ .value = 3.0 };
const t = Second{ .value = 4.0 };
const area_time = d.mulBy(t).to(KmSec);
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));
std.debug.print("MulBy: {f} * {f} = {f} OK\n", .{ d, t, area_time });
}
test "MulBy small" {
const Meter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .n }));
const Second = Quantity(f32, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
const d = Meter{ .value = 3.0 };
const t = Second{ .value = 4.0 };
const area_time = d.mulBy(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));
std.debug.print("MulBy: {f} * {f} = {f} OK\n", .{ d, t, area_time });
}
test "Scale" {
const Meter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const Second = Quantity(f32, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
const d = Meter{ .value = 7 };
const scaled = d.scale(3);
try std.testing.expectEqual(21, scaled.value);
try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L));
std.debug.print("Scale int: {f} * 3 = {f} OK\n", .{ d, scaled });
const t = Second{ .value = 1.5 };
const scaled_f = t.scale(4.0);
try std.testing.expectApproxEqAbs(@as(f32, 6.0), scaled_f.value, 1e-4);
std.debug.print("Scale float: {f} * 4 = {f} OK\n", .{ t, scaled_f });
}
test "Chained: velocity and acceleration" {
const Meter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const Second = Quantity(f32, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
const dist = Meter{ .value = 100.0 };
const t1 = Second{ .value = 5.0 };
const velocity = dist.divBy(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.divBy(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));
std.debug.print("Velocity: {f}, Acceleration: {f} OK\n", .{ velocity, accel });
}
test "DivBy integer exact" {
const Meter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const Second = Quantity(f32, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
const dist = Meter{ .value = 120 };
const time = Second{ .value = 4 };
const vel = dist.divBy(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));
std.debug.print("DivBy int: {f} / {f} = {f} OK\n", .{ dist, time, vel });
}
test "Conversion chain: km -> m -> cm" {
const KiloMeter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .k }));
const Meter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const CentiMeter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .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);
std.debug.print("Chain: {f} -> {f} -> {f} OK\n", .{ km, m, cm });
}
test "Conversion: hours -> minutes -> seconds" {
const Hour = Quantity(i128, Dimensions.init(.{ .T = 1 }), Scales.init(.{ .T = .hour }));
const Minute = Quantity(i128, Dimensions.init(.{ .T = 1 }), Scales.init(.{ .T = .min }));
const Second = Quantity(i128, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
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);
std.debug.print("Time chain: {f} -> {f} -> {f} OK\n", .{ h, min, sec });
}
test "Negative values" {
const Meter = Quantity(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const a = Meter{ .value = 5 };
const b = Meter{ .value = 20 };
const diff = a.sub(b);
try std.testing.expectEqual(-15, diff.value);
std.debug.print("Negative sub: {f} - {f} = {f} OK\n", .{ a, b, diff });
}
test "Format Quantity" {
const MeterPerSecondSq = Quantity(
f32,
Dimensions.init(.{ .L = 1, .T = -2 }),
Scales.init(.{ .T = .n }),
);
const KgMeterPerSecond = Quantity(
f32,
Dimensions.init(.{ .M = 1, .L = 1, .T = -1 }),
Scales.init(.{ .M = .k }),
);
const accel = MeterPerSecondSq{ .value = 9.81 };
const momentum = KgMeterPerSecond{ .value = 42.0 };
std.debug.print("Acceleration: {f}\n", .{accel});
std.debug.print("Momentum: {f}\n", .{momentum});
}
test "Format Vector3" {
const MeterPerSecondSq = Quantity(
f32,
Dimensions.init(.{ .L = 1, .T = -2 }),
Scales.init(.{ .T = .n }),
);
const KgMeterPerSecond = Quantity(
f32,
Dimensions.init(.{ .M = 1, .L = 1, .T = -1 }),
Scales.init(.{ .M = .k }),
);
const accel = MeterPerSecondSq.Vec3.initDefault(9.81);
const momentum = KgMeterPerSecond.Vec3{ .x = 43, .y = 0, .z = 11 };
std.debug.print("Acceleration: {f}\n", .{accel});
std.debug.print("Momentum: {f}\n", .{momentum});
}
test "Vec3 Init and Basic Arithmetic" {
const Meter = Quantity(i32, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const Vec3M = Meter.Vec3;
// Test zero, one, initDefault
const v_zero = Vec3M.zero;
try std.testing.expectEqual(0, v_zero.x);
const v_one = Vec3M.one;
try std.testing.expectEqual(1, v_one.x);
const v_def = Vec3M.initDefault(5);
try std.testing.expectEqual(5, v_def.x);
try std.testing.expectEqual(5, v_def.y);
try std.testing.expectEqual(5, v_def.z);
// Test add and sub
const v1 = Vec3M{ .x = 10, .y = 20, .z = 30 };
const v2 = Vec3M{ .x = 2, .y = 4, .z = 6 };
const added = v1.add(v2);
try std.testing.expectEqual(12, added.x);
try std.testing.expectEqual(24, added.y);
try std.testing.expectEqual(36, added.z);
const subbed = v1.sub(v2);
try std.testing.expectEqual(8, subbed.x);
try std.testing.expectEqual(16, subbed.y);
try std.testing.expectEqual(24, subbed.z);
// Test negate
const neg = v1.negate();
try std.testing.expectEqual(-10, neg.x);
try std.testing.expectEqual(-20, neg.y);
try std.testing.expectEqual(-30, neg.z);
}
test "Vec3 Kinematics (Scalar Mul/Div)" {
const Meter = Quantity(i32, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const Second = Quantity(i32, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
const Vec3M = Meter.Vec3;
const pos = Vec3M{ .x = 100, .y = 200, .z = 300 };
const time = Second{ .value = 10 };
// Vector divided by scalar Quantity (Velocity = Position / Time)
const vel = pos.divByScalar(time);
try std.testing.expectEqual(10, vel.x);
try std.testing.expectEqual(20, vel.y);
try std.testing.expectEqual(30, vel.z);
try std.testing.expectEqual(1, @TypeOf(vel).dims.get(.L));
try std.testing.expectEqual(-1, @TypeOf(vel).dims.get(.T));
// Vector multiplied by scalar Quantity (Position = Velocity * Time)
const new_pos = vel.mulByScalar(time);
try std.testing.expectEqual(100, new_pos.x);
try std.testing.expectEqual(200, new_pos.y);
try std.testing.expectEqual(300, new_pos.z);
try std.testing.expectEqual(1, @TypeOf(new_pos).dims.get(.L));
try std.testing.expectEqual(0, @TypeOf(new_pos).dims.get(.T));
}
test "Vec3 Element-wise Math and Scaling" {
const Meter = Quantity(i32, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const Vec3M = Meter.Vec3;
const v1 = Vec3M{ .x = 10, .y = 20, .z = 30 };
const v2 = Vec3M{ .x = 2, .y = 5, .z = 10 };
// Element-wise division
const div = v1.divBy(v2);
try std.testing.expectEqual(5, div.x);
try std.testing.expectEqual(4, div.y);
try std.testing.expectEqual(3, div.z);
try std.testing.expectEqual(0, @TypeOf(div).dims.get(.L)); // M / M = Dimensionless
// Scale by primitive
const scaled = v1.scale(2);
try std.testing.expectEqual(20, scaled.x);
try std.testing.expectEqual(40, scaled.y);
try std.testing.expectEqual(60, scaled.z);
}
test "Vec3 Conversions" {
const KiloMeter = Quantity(i32, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .k }));
const Meter = Quantity(i32, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const v_km = KiloMeter.Vec3{ .x = 1, .y = 2, .z = 3 };
const v_m = v_km.to(Meter);
try std.testing.expectEqual(1000, v_m.x);
try std.testing.expectEqual(2000, v_m.y);
try std.testing.expectEqual(3000, v_m.z);
// 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 "Vec3 Length" {
const MeterInt = Quantity(i32, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const MeterFloat = Quantity(f32, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
// Integer length (using your custom isqrt)
// 3-4-5 triangle on XY plane
const v_int = MeterInt.Vec3{ .x = 3, .y = 4, .z = 0 };
try std.testing.expectEqual(25, v_int.lengthSqr());
try std.testing.expectEqual(5, v_int.length());
// Float length
const v_float = MeterFloat.Vec3{ .x = 3.0, .y = 4.0, .z = 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 "Comprehensive Benchmark: All Ops × All Types" {
const Io = std.Io;
const ITERS: usize = 100_000;
const SAMPLES: usize = 10; // Number of samples for stats
var gsink: f64 = 0;
const io = std.testing.io;
// Standard Zig 0.16 timestamp retrieval
const getTime = struct {
fn f(i: Io) Io.Timestamp {
return Io.Clock.awake.now(i);
}
}.f;
const fold = struct {
fn f(comptime TT: type, s: *f64, v: TT) void {
s.* += if (comptime @typeInfo(TT) == .float)
@as(f64, @floatCast(v))
else
@as(f64, @floatFromInt(v));
}
}.f;
const getVal = struct {
fn f(comptime TT: type, i: usize, comptime mask: u7) TT {
const v: u8 = @as(u8, @truncate(i & @as(usize, mask))) + 1;
return if (comptime @typeInfo(TT) == .float) @floatFromInt(v) else @intCast(v);
}
}.f;
const Stats = struct {
median: f64,
delta: f64,
ops_per_sec: f64,
};
const computeStats = struct {
fn f(samples: []f64, iters: usize) Stats {
std.mem.sort(f64, samples, {}, std.sort.asc(f64));
const mid = samples.len / 2;
const median_ns = if (samples.len % 2 == 0) (samples[mid - 1] + samples[mid]) / 2.0 else samples[mid];
const low = samples[0];
const high = samples[samples.len - 1];
const delta_ns = (high - low) / 2.0;
const ns_per_op = median_ns / @as(f64, @floatFromInt(iters));
return .{
.median = ns_per_op,
.delta = (delta_ns / @as(f64, @floatFromInt(iters))),
.ops_per_sec = 1_000_000_000.0 / ns_per_op,
};
}
}.f;
std.debug.print(
\\
\\ Quantity<T> benchmark — {d} iterations, {d} samples/cell
\\
\\┌───────────────────┬──────┬─────────────────────┬─────────────────────┐
\\│ Operation │ Type │ ns / op (± delta) │ Throughput (ops/s) │
\\├───────────────────┼──────┼─────────────────────┼─────────────────────┤
\\
, .{ ITERS, SAMPLES });
const Types = .{ i16, i32, i64, i128, i256, f32, f64, f128 };
const TNames = .{ "i16", "i32", "i64", "i128", "i256", "f32", "f64", "f128" };
const Ops = .{ "add", "sub", "mulBy", "divBy", "scale", "to" };
var results_matrix: [Ops.len][Types.len]f64 = undefined;
comptime var tidx: usize = 0;
inline for (Types, TNames) |T, tname| {
const M = Quantity(T, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const KM = Quantity(T, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .k }));
const S = Quantity(T, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
inline for (Ops, 0..) |op_name, oidx| {
var samples: [SAMPLES]f64 = undefined;
for (0..SAMPLES) |s_idx| {
var sink: T = 0;
const t_start = getTime(io);
for (0..ITERS) |i| {
const r = if (comptime std.mem.eql(u8, op_name, "add"))
(M{ .value = getVal(T, i, 63) }).add(M{ .value = 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) })
else if (comptime std.mem.eql(u8, op_name, "mulBy"))
(M{ .value = getVal(T, i, 63) }).mulBy(M{ .value = getVal(T, i +% 1, 63) })
else if (comptime std.mem.eql(u8, op_name, "divBy"))
(M{ .value = getVal(T, i +% 10, 63) }).divBy(S{ .value = getVal(T, i, 63) })
else if (comptime std.mem.eql(u8, op_name, "scale"))
(M{ .value = getVal(T, i, 63) }).scale(getVal(T, i +% 2, 63))
else
(KM{ .value = getVal(T, i, 15) }).to(M);
if (comptime @typeInfo(T) == .float) sink += r.value else sink ^= r.value;
}
const t_end = getTime(io);
samples[s_idx] = @as(f64, @floatFromInt(t_start.durationTo(t_end).toNanoseconds()));
fold(T, &gsink, sink);
}
const stats = computeStats(&samples, ITERS);
results_matrix[oidx][tidx] = stats.median;
std.debug.print("│ {s:<17} │ {s:<4} │ {d:>8.2} ns ±{d:<6.2} │ {d:>19.0} │\n", .{ op_name, tname, stats.median, stats.delta, stats.ops_per_sec });
}
if (comptime tidx < Types.len - 1) {
std.debug.print("├───────────────────┼──────┼─────────────────────┼─────────────────────┤\n", .{});
}
tidx += 1;
}
// Median Summary Table
std.debug.print("└───────────────────┴──────┴─────────────────────┴─────────────────────┘\n\n", .{});
std.debug.print("Median Summary (ns/op):\n", .{});
std.debug.print("┌──────────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┐\n", .{});
std.debug.print("│ Operation │ i16 │ i32 │ i64 │ i128 │ i256 │ f32 │ f64 │ f128 │\n", .{});
std.debug.print("├──────────────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┤\n", .{});
inline for (Ops, 0..) |op_name, oidx| {
std.debug.print("│ {s:<11} │", .{op_name});
var i: usize = 0;
while (i < Types.len) : (i += 1) {
std.debug.print("{d:>6.1} │", .{results_matrix[oidx][i]});
}
std.debug.print("\n", .{});
}
std.debug.print("└──────────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘\n", .{});
std.debug.print("\nAnti-optimisation sink: {d:.4}\n", .{gsink});
try std.testing.expect(gsink != 0);
} }