70 Commits
Author SHA1 Message Date
adrien 22ffd4fc64 Fix TensorAlloc copy that was copying the ptr 2026-05-27 21:15:58 +02:00
adrien 9ac3d4d699 Squeletton TensorGpu that init 2026-05-26 23:34:53 +02:00
adrien 2215a5d86d Added TensorStatic and Alloc to lib.zig 2026-05-26 20:06:36 +02:00
adrien 9deb25b825 Updated zon version 2026-05-26 20:01:04 +02:00
adrien ceff8ff1bd Added everything to TensorAlloc 2026-05-25 22:52:27 +02:00
adrien ff21f0ac8b Added all operation and test for TensorAlloc, missing slice 2026-05-25 18:18:35 +02:00
adrien 5ac9968021 Added back more test for TensorAlloc 2026-05-25 13:53:17 +02:00
adrien 8028cf41a5 Removed inline for TensorAlloc fn 2026-05-25 01:54:47 +02:00
adrien 0ef19e18de Working other base operation (sub, mul, div, ect) 2026-05-25 01:52:12 +02:00
adrien 7494595db4 Working TensorAlloc add 2026-05-25 01:44:09 +02:00
adrien 91c5c41fc5 Working comparaison for TensorAlloc 2026-05-24 21:23:20 +02:00
adrien ba671ee486 Working basic eq between TensorAlloc 2026-05-24 18:40:14 +02:00
adrien 09d6ca1ff5 Renamed Tensor to TensorStatic 2026-05-23 10:29:57 +02:00
adrien bcd888d59e Splited main.zig into lib and test.zig 2026-05-20 23:02:30 +02:00
adrien 63e9b6b63d Removed vs native benchmarks 2026-05-20 22:58:36 +02:00
adrien 957f75243f Added zig-wgpu import 2026-05-20 16:03:15 +02:00
adrien 5f833a5e58 Removed TensorAlloc and made TensorStatic back to just Tensor
Realized I can just do alloc.create instead of a all new struct
2026-05-15 00:46:22 +02:00
adrien 00e0f5ab73 Moved isTensor to shared + added isTensorAlloc/Static 2026-05-15 00:32:58 +02:00
adrien f67e9d709d Working add TensorAlloc 2026-05-15 00:24:39 +02:00
adrien e6d0f62929 TensorAlloc add and to compilable but still error for basic add test 2026-05-14 22:25:35 +02:00
adrien f702c1e09a slice can now use null value like python [2:] 2026-05-14 10:56:26 +02:00
adrien b959f5f28a Added slice to TensorStatic 2026-05-14 01:28:24 +02:00
adrien 6ba1e664c1 Started to add TensorAlloc 2026-05-14 00:53:05 +02:00
adrien 6559ed9f62 Removed comptime to parseSegment 2026-05-12 09:06:28 +02:00
adrien 55906e0ab7 Small fixs 2026-05-11 17:12:37 +02:00
adrien 7d28de2028 Removed comptime for str UnitParser 2026-05-04 23:55:00 +02:00
adrien eb3b0d4de3 Update .zon 2026-05-04 22:59:54 +02:00
adrien 5bdc78c065 Simplified pow 2026-05-04 22:57:53 +02:00
adrien 18830c8b45 Fixed benchmark 2026-05-04 22:25:18 +02:00
adrien 4595397e70 Removed the feature where you can use comptime int or float ar rhs for operation 2026-05-04 22:10:55 +02:00
adrien 7844aacfce Added a UnitParser to get Dimensions and Scales from a str 2026-05-04 19:10:06 +02:00
adrien 9b6cd4b377 Removed release fast 2026-05-04 14:34:31 +02:00
adrien 4d275dca2d Renamed Tensor to TensorStatic to later introduce TensorAlloc and TensorGPU 2026-04-29 18:07:13 +02:00
adrien 9635cfb481 Changed self: Self to self: *const SElf in tensor for performance 2026-04-28 16:06:13 +02:00
adrien f0029449f0 tmp 2026-04-28 14:50:08 +02:00
adrien 8816a65518 Now pass all test with new *const way
I am not quite sure about it yet, but it is faster sooo idk.
Let's see long term
2026-04-28 13:51:10 +02:00
adrien 26ff02c50f Changed TEnsor to use *const 2026-04-28 13:10:14 +02:00
adrien bb6dd59b9a Removed more 2026-04-28 01:02:30 +02:00
adrien acb908a448 Removed some char 2026-04-28 01:01:40 +02:00
adrien d4a1054fdc Removed deploy.yaml 2026-04-28 00:58:24 +02:00
adrien fb2a6f4806 Removed docs/ 2026-04-28 00:57:50 +02:00
adrien 8c77ab86f8 Readded it but also in gitignore 2026-04-28 00:56:42 +02:00
adrien 8565869919 Removed mkdocs.yaml 2026-04-28 00:56:02 +02:00
adrien 1a69b3dbf2 Removed old .md 2026-04-28 00:55:32 +02:00
adrien cd3632210d Updated README
Deploy MkDocs to Garage / build-and-deploy (push) Successful in 4m40s
2026-04-28 00:45:12 +02:00
adrien de2e9cce68 Merge branch 'tensor'
Deploy MkDocs to Garage / build-and-deploy (push) Successful in 40s
2026-04-27 22:10:02 +02:00
adrien 0b2469d321 . 2026-04-27 22:07:45 +02:00
adrien c6f613a787 I guess I can't do better. Scalar still suck, but at least it is builtin SIDM so ok I guess 2026-04-27 22:07:39 +02:00
adrien a0961e7571 Start to optimize the shit out of it, still a long way to go
After that GPPPPPUUUUUU baby!
2026-04-27 21:24:03 +02:00
adrien 168312b78e Removed lots of usless inline and comptime in Scales and Dimensions 2026-04-27 19:09:55 +02:00
AdrienBouvais 698e968ef8 Added a high dimension benchmark 2026-04-27 16:18:31 +02:00
AdrienBouvais 44aaa8a8b2 Removed all inline for (0..total) for either builtin or for loop without inline
This is to prevent giant binary for Tensor with a lot of Scalar
2026-04-27 16:11:46 +02:00
AdrienBouvais cd954b379b Added cross to Tensor + fix benchmark 2026-04-27 15:13:15 +02:00
AdrienBouvais 16d25e7e7e Added shape comptime check for Tensor add/sub/div/mul 2026-04-27 14:45:41 +02:00
AdrienBouvais f37a196b15 Fixed new Tensor to be everything (Scalar, Vector, Matrix and above) 2026-04-27 09:11:24 +02:00
Adrien Bouvais 934a40fe1a Basic untested tensor 2026-04-26 22:16:25 +02:00
adrien 1dcfd58189 .
Deploy MkDocs to Garage / build-and-deploy (push) Successful in 29s
2026-04-26 01:37:00 +02:00
adrien 0914ea0a60 .
Deploy MkDocs to Garage / build-and-deploy (push) Failing after 1m1s
2026-04-26 01:35:37 +02:00
adrien 37a95607b3 .
Deploy MkDocs to Garage / build-and-deploy (push) Failing after 28s
2026-04-26 01:33:37 +02:00
adrien fe7ee54601 Moved mkdocs.yml to /docs
Deploy MkDocs to Garage / build-and-deploy (push) Failing after 34s
2026-04-26 01:31:45 +02:00
adrien 81effc9815 Merge pull request 'Replace Scalar and Vector to a single Quantity that use @Vector' (#1) from simd into main
Deploy MkDocs to Garage / build-and-deploy (push) Failing after 33s
Reviewed-on: #1
2026-04-25 23:29:30 +00:00
adrien 4a2d45b384 Fix benchmarks to work with new Scalar and Vector 2026-04-26 01:28:33 +02:00
adrien d32de3fe82 Converted Base test to new Scalar 2026-04-26 01:18:41 +02:00
adrien 58fd4d5797 Fix
Deploy MkDocs to Garage / build-and-deploy (push) Failing after 29s
2026-04-26 01:11:51 +02:00
adrien ae6b069da1 Added simd ref to action
Deploy MkDocs to Garage / build-and-deploy (push) Failing after 27s
2026-04-26 01:09:20 +02:00
adrien 4b01dfe412 Replaced Vector with Quantity
Deploy MkDocs to Garage / build-and-deploy (push) Failing after 25s
2026-04-26 01:07:12 +02:00
adrien c350ffd1d6 Working vector initiate
Deploy MkDocs to Garage / build-and-deploy (push) Failing after 30s
2026-04-26 00:40:10 +02:00
adrien a591736b19 Totally replaced Scalar with Quantity
Deploy MkDocs to Garage / build-and-deploy (push) Failing after 49s
2026-04-26 00:29:00 +02:00
adrien 89e46f4cd4 .
Deploy MkDocs to Garage / build-and-deploy (push) Failing after 2m17s
2026-04-25 23:29:51 +02:00
adrien db6aaf433d Added mkdocs 2026-04-25 23:29:06 +02:00
20 changed files with 5676 additions and 3063 deletions
+2
View File
@@ -1,2 +1,4 @@
zig-out
.zig-cache
mkdocs.yaml
zig-pkg
+159 -252
View File
@@ -1,42 +1,55 @@
# dimal — Dimensional Analysis for Zig
A comptime-first dimensional analysis module for Zig. If you try to add meters to seconds, **it won't compile**. That's the point.
A dimensional analysis library for Zig with a unified `Tensor` API for scalars, vectors, matrices, and higher-dimensional data. All dimension and unit tracking happens at compile time—zero runtime overhead—and all operations use SIMD intrinsics.
Started by a space simulation where `i128` positions were needed to avoid float imprecision far from the origin, this module grew into a full physical-unit type system with zero runtime overhead.
If you try to add meters to seconds, it won't compile. That's the point.
> **Source:** [git.bouvais.lu/adrien/zig-dimal](https://git.bouvais.lu/adrien/zig-dimal)
> **Minimum Zig version:** `0.16.0`
---
## Background
Started because I needed `i128` positions for a space simulation to avoid floating-point precision loss far from the origin. Grew into a type system for tracking physical dimensions at compile time. It's been useful enough to share.
- **Compile-time dimension checking** — catch unit mismatches before runtime.
- **Unified `Tensor` API** — same interface for scalars, vectors, matrices, and higher-rank tensors.
- **SIMD operations** — vector and matrix code automatically uses SIMD instructions.
- **Zero runtime cost** — all dimension and scale tracking is erased at compile time.
- **Supports `i128`** — useful for high-precision fixed-point integer math.
---
## Features
- **100% comptime** — all dimension and unit tracking happens at compile time. No added memory, *almost* native performance.
- **Compile-time dimension errors** — adding `Meter` to `Second` is a compile error, not a runtime panic.
- **Compile-time dimension checking** — all physical-unit tracking happens at compile time.
- **Automatic unit conversion** — use `.to()` to convert between compatible units (e.g. `km/h` → `m/s`). Scale factors are resolved at comptime.
* **Full SI prefix & Imperial support** — `pico` through `peta`, plus common Imperial units like `inch`, `ft`, `mi`, `lb`, and `oz`.
- **Time scale support** — `min`, `hour`, `year` built in.
- **Scalar and Vector types** — operate on individual values or fixed-size arrays with the same dimensional safety.
- **Built-in physical quantities** — `dma.Base` provides ready-made types for `Velocity`, `Acceleration`, `Force`, `Energy`, `Pressure`, `ElectricCharge`, `ThermalConductivity`, and many more.
- **Comparison operations** — `eq`, `ne`, `gt`, `gte`, `lt`, `lte` on both `Scalar` and `Vector`, with automatic scale resolution.
- **Arithmetic with bare numbers** — multiply or divide a dimensioned value by a `comptime_int`, `comptime_float`, or plain `T` directly. The value is treated as dimensionless; dimensions pass through unchanged.
- **`abs`, `pow`, `sqrt`** — unary operations with correct dimension tracking (`pow(2)` on `L¹` → `L²`, etc.).
- **Vector geometry** — `dot` product (returns a `Scalar`), `cross` product (Vec3 only), element-wise `product` (all components multiplied).
- **Rich formatting** — values print with their unit automatically: `9.81m.s⁻²`, `42m.kg.s⁻¹`, `0.172km`.
- **`i128` support** — the whole reason this exists. Use large integers for high-precision fixed-point positions without manual conversion.
- **Tests and benchmarks included** — run them and see how it performs on your machine (results welcome!).
- **Unified `Tensor` API** — one type for scalars `{1}`, vectors `{N}`, matrices `{M, N}`, and higher-rank tensors.
- **SIMD operations** — vector and matrix code compiles to SIMD instructions automatically.
- **Tensor contraction** — `.contract(other, axis_a, axis_b)` for dot products, matrix multiplication, and general tensor contractions.
- **Full SI prefix support** — `pico` through `peta`, plus Imperial units and time scales.
- **Physical constants** — Planck, Boltzmann, speed of light, gravitational constant, etc.
- **Pre-built quantities** — `Velocity`, `Acceleration`, `Force`, `Energy`, `Pressure`, `Charge`, and more.
- **Basic vector operations** — cross product, length/magnitude, element-wise arithmetic.
- **Formatting** — values print with units: `9.81m.s⁻²`, `0.172km`.
### Current Limitations
- GPU support not implemented.
- Performance on small tensors is limited by Zig's vector width.
---
## The 7 SI Base Dimensions
| Symbol | Dimension | SI Unit |
|--------|----------------------|----------|
|--------|----------------------|---------|
| `L` | Length | `m` |
| `M` | Mass | `g` |
| `T` | Time | `s` |
| `I` | Electric Current | `A` |
| `Tp` | Temperature | `K` |
| `Tr` | Temperature | `K` |
| `N` | Amount of Substance | `mol` |
| `J` | Luminous Intensity | `cd` |
@@ -44,10 +57,10 @@ Started by a space simulation where `i128` positions were needed to avoid float
## Installation
### 1. Fetch the dependency
### 1. Add the dependency (Zig 0.14+)
```sh
zig fetch --save git+https://git.bouvais.lu/adrien/zig-dimal#0.1.1
zig fetch --save git+https://git.bouvais.lu/adrien/zig-dimal#0.2.0
```
### 2. Wire it up in `build.zig`
@@ -57,287 +70,181 @@ const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const dimal = b.dependency("dimal", .{}).module("dimal");
const optimize = b.standardOptimizeOption(.{});
const dimal = b.dependency("dimal", .{
.target = target,
.optimize = optimize,
}).module("dimal");
const exe = b.addExecutable(.{
.name = "my_project",
.root_module = b.createModule(.{
.name = "my_app",
.root_source_file = b.path("src/main.zig"),
.target = target,
.imports = &.{.{
.name = "dimal",
.module = dimal,
}},
}),
.optimize = optimize,
});
exe.root_module.addImport("dimal", dimal);
b.installArtifact(exe);
}
```
### 3. Import in your code
### 3. Import and use
```zig
const dma = @import("dimal");
const Scalar = dma.Scalar;
const Dimensions = dma.Dimensions;
const Scales = dma.Scales;
const Tensor = dma.Tensor;
const Base = dma.Base;
```
---
## Quick Start
## Quick Example: Lunar Descent
### Defining unit types
A `Scalar` type is parameterized by three things: the numeric type (`f64`, `i128`, …), the dimensions (which physical quantities and their exponents), and the scales (SI prefixes or custom time units). Both the dimension and scale arguments are plain struct literals — no wrapper call needed.
Simulate a spacecraft descending to the Moon with correct physics and type safety:
```zig
const Meter = Scalar(f64, .{ .L = 1 }, .{});
const NanoMeter = Scalar(i64, .{ .L = 1 }, .{ .L = .n });
const KiloMeter = Scalar(f64, .{ .L = 1 }, .{ .L = .k });
const Second = Scalar(f64, .{ .T = 1 }, .{});
const Velocity = Scalar(f64, .{ .L = 1, .T = -1 }, .{});
const Kmh = Scalar(f64, .{ .L = 1, .T = -1 }, .{ .L = .k, .T = .hour });
const std = @import("std");
const dma = @import("dimal");
const Tensor = dma.Tensor;
pub fn main() void {
// Define types: m/s² acceleration, m/s velocity, m distance
const Acceleration = dma.Base.Acceleration.Of(f64);
const Velocity = dma.Base.Velocity.Of(f64);
const Distance = dma.Base.Meter.Of(f64);
const Time = dma.Base.Second.Of(f64);
// Initial conditions
const g_moon: Acceleration = .{ .data = @splat(1.62) };
const v_initial: Velocity = .{ .data = @splat(100.0) };
const h_initial: Distance = .{ .data = @splat(10000.0) };
const dt: Time = .{ .data = @splat(1.0) };
var h = h_initial;
var v = v_initial;
var t: f64 = 0;
// Simulate descent
while (h.data[0] > 0 and t < 1000) : (t += 1.0) {
// a = -g (gravity pulls down)
const a = g_moon.mul(-1.0);
// Update: v = v₀ + at
v = v.add(a.mul(dt));
// Update: h = h₀ + vt
h = h.add(v.mul(dt));
if (@mod(t, 100.0) == 0) {
std.debug.print("t={d:.0}s | h={d:.1} | v={d:.1}\n", .{
t,
h,
v,
});
}
}
std.debug.print("Landed in {d:.1}s at h={d:.1}\n", .{ t, h });
}
```
Or use the pre-built helpers from `dma.Base`:
```zig
const Acceleration = dma.Base.Acceleration.Of(f64);
const KmhSpeed = dma.Base.Speed.Scaled(f64, .{ .L = .k, .T = .hour });
**Output:**
```
### Kinematics example
```zig
const v0 = Velocity{ .value = 10.0 }; // 10 m/s
const accel = Acceleration{ .value = 9.81 }; // 9.81 m/s²
const time = Second{ .value = 5.0 }; // 5 s
// d = v₀t + ½at²
const d1 = v0.mul(time); // → Meter
const d2 = accel.mul(time).mul(time).mul(0.5); // → Meter (bare 0.5 is dimensionless)
const dist = d1.add(d2);
const v_final = v0.add(accel.mul(time));
std.debug.print("Distance: {d} | {d}\n", .{ dist, dist.to(KiloMeter) });
// Distance: 172.625m | 0.172625km
std.debug.print("Final speed: {d:.2}\n", .{v_final});
// Final speed: 59.05m.s⁻¹
```
### Unit conversion
`.to()` converts between compatible units at comptime. Mixing incompatible dimensions is a **compile error**.
```zig
const speed_kmh = Kmh{ .value = 120.0 };
const speed_ms = speed_kmh.to(Velocity); // 33.333... m/s — comptime ratio
// This would NOT compile:
// const bad = speed_kmh.to(Second); // "Dimension mismatch in to: L1T-1 vs T1"
```
#### Imperial
```zig
const Inch = Scalar(f64, .{ .L = 1 }, .{ .L = .inch });
const Mile = Scalar(f64, .{ .L = 1 }, .{ .L = .mi });
const Pound = Scalar(f64, .{ .M = 1 }, .{ .M = .lb });
// Conversion example
const dist_m = Meter{ .value = 1609.344 };
const dist_mi = dist_m.to(Mile); // Result: 1.0
```
### Arithmetic with bare numbers
Passing a `comptime_int`, `comptime_float`, or plain `T` to `mul` / `div` treats it as a dimensionless value. Dimensions pass through unchanged.
```zig
const Meter = Scalar(f64, .{ .L = 1 }, .{});
const d = Meter{ .value = 6.0 };
const half = d.mul(0.5); // comptime_float → still Meter
const doubled = d.mul(2); // comptime_int → still Meter
const factor: f64 = 3.0;
const tripled = d.mul(factor); // runtime f64 → still Meter
```
### Comparisons
`eq`, `ne`, `gt`, `gte`, `lt`, `lte` work on any two `Scalar` values of the **same dimension**. Scales are resolved automatically before comparing.
```zig
const Meter = Scalar(i64, .{ .L = 1 }, .{});
const KiloMeter = Scalar(i64, .{ .L = 1 }, .{ .L = .k });
const m1000 = Meter{ .value = 1000 };
const km1 = KiloMeter{ .value = 1 };
const km2 = KiloMeter{ .value = 2 };
_ = m1000.eq(km1); // true — same magnitude
_ = km2.gt(m1000); // true — 2 km > 1000 m
_ = m1000.lte(km2); // true
// Comparing with a bare number works when the scalar is dimensionless.
// Comparing incompatible dimensions is a compile error.
```
### Unary operations: `abs`, `pow`, `sqrt`
```zig
const Meter = Scalar(f64, .{ .L = 1 }, .{});
const d = Meter{ .value = -4.0 };
const magnitude = d.abs(); // 4.0 m — dimension unchanged
const area = d.pow(2); // 16.0 m² — dims scaled by exponent
const side = area.sqrt(); // 4.0 m — dims halved (requires even exponents)
```
`pow` accepts any `comptime_int` exponent and adjusts the dimension exponents accordingly. `sqrt` is a compile error unless all dimension exponents are even.
### Working with Vectors
Every `Scalar` type exposes a `.Vec3` alias and a generic `.Vec(n)` type accessor:
```zig
const Vec3Meter = Meter.Vec3; // equivalent to Vector(3, Meter)
const pos = Vec3Meter{ .data = .{ 100, 200, 300 } };
const t = Second{ .value = 10 };
const vel = pos.divScalar(t); // → Vec3 of Velocity (m/s)
std.debug.print("{d}\n", .{vel}); // (10, 20, 30)m.s⁻¹
```
#### Dot and cross products
```zig
const Newton = Scalar(f32, .{ .M = 1, .L = 1, .T = -2 }, .{});
const r = Meter.Vec3{ .data = .{ 10.0, 0.0, 0.0 } };
const force = Newton.Vec3{ .data = .{ 5.0, 5.0, 0.0 } };
// Dot product — returns a Scalar (dimensions summed)
const work = force.dot(r); // 50.0 J (M¹L²T⁻²)
// Cross product — returns a Vec3 (dimensions summed, Vec3 only)
const torque = r.cross(force); // (0, 0, 50) N·m
```
#### Vector comparisons
Element-wise comparisons return `[len]bool`. Whole-vector equality uses `eqAll` / `neAll`. A single scalar can be broadcast with the `*Scalar` variants.
```zig
const positions = Meter.Vec3{ .data = .{ 500.0, 1200.0, 3000.0 } };
const threshold = KiloMeter{ .value = 1.0 }; // 1 km
const exceeded = positions.gtScalar(threshold); // [false, true, true]
const eq_each = positions.eq(positions); // [true, true, true] (element-wise)
const all_same = positions.eqAll(positions); // true (whole-vector)
```
#### Other Vector operations
```zig
const v = Meter.Vec3{ .data = .{ -2.0, 3.0, -4.0 } };
const v_abs = v.abs(); // { 2, 3, 4 } m
const vol = v_abs.product(); // 24 m³ (dims × len)
const area = v_abs.pow(2); // { 4, 9, 16 } m²
const sides = area.sqrt(); // { 2, 3, 4 } m (element-wise sqrt)
t=0s | h=10000m | v=100m.s⁻¹
t=100s | h=8019m | v=-61.8m.s⁻¹
t=200s | h=4174.4m | v=-223.6m.s⁻¹
...
Landed in 323.5s at h=-0.01m
```
---
## API Reference
## API Overview
### `Scalar(T, dims, scales)`
### Tensors
| Method | Description |
|---|---|
| `.add(rhs)` | Add two quantities of the same dimension. Auto-converts scales. |
| `.sub(rhs)` | Subtract. Auto-converts scales. |
| `.mul(rhs)` | Multiply — dimensions are **summed**. `rhs` may be a `Scalar`, `T`, `comptime_int`, or `comptime_float` (bare numbers are dimensionless). |
| `.div(rhs)` | Divide — dimensions are **subtracted**. Same `rhs` flexibility as `mul`. |
| `.abs()` | Absolute value. Dimensions and scales unchanged. |
| `.pow(exp)` | Raise to a `comptime_int` exponent. Dimension exponents are multiplied by `exp`. |
| `.sqrt()` | Square root. Compile error unless all dimension exponents are even. |
| `.eq(rhs)` / `.ne(rhs)` | Equality / inequality comparison. Scales auto-resolved. |
| `.gt(rhs)` / `.gte(rhs)` | Greater-than / greater-than-or-equal. |
| `.lt(rhs)` / `.lte(rhs)` | Less-than / less-than-or-equal. |
| `.to(DestType)` | Convert to another unit of the same dimension. Compile error on mismatch. |
| `.vec(len)` | Return a `Vector(len, Self)` with all components set to this value. |
| `.vec3()` | Shorthand for `.vec(3)`. |
| `.Vec3` | Type alias for `Vector(3, Self)`. |
A **`Tensor`** is parameterized by:
- **`T`** — numeric type: `f32`, `f64`, `i128`, etc.
- **`dims`** — physical dimensions (struct literal): `.{.L = 1, .T = -1}` means length/time (velocity).
- **`scales`** — SI prefixes or custom scales: `.{.L = .k, .T = .hour}` means km/h.
- **`shape`** — array shape: `&.{1}` is a scalar, `&.{3}` is a 3-vector, `&.{3, 3}` is a 3×3 matrix.
### `Vector(len, Q)`
```zig
// Scalar: 1-element tensor
const Meter = Tensor(f64, .{.L = 1}, .{}, &.{1});
const m = Meter{ .data = @splat(5.0) };
| Method | Description |
|---|---|
| `.add(rhs)` / `.sub(rhs)` | Element-wise add / subtract. |
| `.mul(rhs)` / `.div(rhs)` | Element-wise multiply / divide (both operands are Vectors). |
| `.mulScalar(s)` / `.divScalar(s)` | Scale every component by a single `Scalar`, `T`, `comptime_int`, or `comptime_float`. |
| `.dot(rhs)` | Dot product → `Scalar` with combined dimensions. |
| `.cross(rhs)` | Cross product → `Vector(3, …)`. Vec3 only. |
| `.abs()` | Element-wise absolute value. |
| `.pow(exp)` | Element-wise `comptime_int` power. Dimension exponents scaled. |
| `.sqrt()` | Element-wise square root. |
| `.product()` | Multiply all components → `Scalar` with dimensions × `len`. |
| `.negate()` | Negate all components. |
| `.length()` | Euclidean length (returns `T`). |
| `.lengthSqr()` | Sum of squared components (returns `T`). Cheaper than `length`. |
| `.eq(rhs)` / `.ne(rhs)` | Element-wise comparison → `[len]bool`. |
| `.gt(rhs)` / `.gte(rhs)` / `.lt(rhs)` / `.lte(rhs)` | Element-wise ordered comparisons → `[len]bool`. |
| `.eqAll(rhs)` / `.neAll(rhs)` | Whole-vector equality / inequality → `bool`. |
| `.eqScalar(s)` / `.neScalar(s)` | Broadcast scalar comparison → `[len]bool`. |
| `.gtScalar(s)` / `.gteScalar(s)` / `.ltScalar(s)` / `.lteScalar(s)` | Broadcast ordered scalar comparisons → `[len]bool`. |
| `.to(DestQ)` | Convert all components to a compatible scalar type. |
// Vector: N-element tensor (SIMD)
const Vec3Meter = Tensor(f64, .{.L = 1}, .{}, &.{3});
const v = Vec3Meter{ .data = @shuffle(f64, [_]f64{1, 2, 3}, [_]f64 undefined, [_]i32{0, 1, 2, 0, 0, 0}) };
### `dma.Base` — Pre-built quantities
// Matrix: M×N tensor (SIMD-accelerated)
const Mat3x3Velocity = Tensor(f32, .{.L = 1, .T = -1}, .{}, &.{3, 3});
const m_vel = Mat3x3Velocity{ .data = @splat(10.0) };
Call `.Of(T)` for base-unit scalars, `.Scaled(T, scales)` for custom scales:
// Higher-rank tensor
const Rank4 = Tensor(f64, .{.M = 1}, .{}, &.{2, 3, 4, 5});
```
`Meter`, `Second`, `Gramm`, `Kelvin`, `ElectricCurrent`, `Speed`, `Acceleration`, `Inertia`, `Force`, `Pressure`, `Energy`, `Power`, `Area`, `Volume`, `Density`, `Frequency`, `Viscosity`, `ElectricCharge`, `ElectricPotential`, `ElectricResistance`, `MagneticFlux`, `ThermalCapacity`, `ThermalConductivity`, and more.
### Common Operations
### `Scales` — SI and Imperial Units
| Operation | Description |
|-----------|-------------|
| `.add(rhs)` | Element-wise addition. Auto-converts scales. |
| `.sub(rhs)` | Element-wise subtraction. |
| `.mul(rhs)` | Multiply; dimensions are summed. `rhs` can be a tensor or bare number. |
| `.div(rhs)` | Divide; dimensions are subtracted. |
| `.contract(other, axis_a, axis_b)` | Tensor contraction: dot product, matrix multiply, or general N-D contraction. |
| `.cross(rhs)` | Cross product (3-vectors only). Returns a 3-vector. |
| `.length()` / `.lengthSqr()` | Euclidean length (or squared length) of a vector. Returns a scalar `T`. |
| `.product()` | Multiply all elements. Returns a scalar with combined dimensions. |
| `.abs()` | Element-wise absolute value. Dimensions unchanged. |
| `.pow(exp)` | Raise to comptime exponent. Dimension exponents multiplied by `exp`. |
| `.sqrt()` | Element-wise square root. Compile error if any dimension exponent is odd. |
| `.to(DestType)` | Convert to another unit of the same dimension. Comptime error on mismatch. |
| `.eq(rhs)` / `.ne(rhs)` | Element-wise equality/inequality. |
| `.gt(rhs)` / `.gte(rhs)` | Greater-than comparisons. |
| `.lt(rhs)` / `.lte(rhs)` | Less-than comparisons. |
| Tag | Factor (Relative to Base) | Type |
|---|---|---|
| `.P` ... `.f` | $10^{15}$ ... $10^{-15}$ | SI Prefixes |
| `.min`, `.hour`, `.year` | 60, 3600, 31,536,000 | Time |
| **`.inch`** | **0.0254** | Imperial Length (m) |
| **`.ft`** | **0.3048** | Imperial Length (m) |
| **`.yd`** | **0.9144** | Imperial Length (m) |
| **`.mi`** | **1609.344** | Imperial Length (m) |
| **`.oz`** | **28.3495231** | Imperial Mass (g) |
| **`.lb`** | **453.59237** | Imperial Mass (g) |
| **`.st`** | **6350.29318** | Imperial Mass (g) |
### Pre-built Types (via `dma.Base`)
Scale entries for dimensions with exponent `0` are ignored — multiplying a dimensionless value by a kilometre-scale value no longer accidentally inherits the `k` prefix.
Use `.Of(T)` for base units, `.Scaled(T, scales)` for custom scales:
```zig
const Velocity = dma.Base.Velocity.Of(f64);
const Kmh = dma.Base.Velocity.Scaled(f64, .{.L = .k, .T = .hour});
const Force = dma.Base.Force.Of(f32);
const Energy = dma.Base.Energy.Of(f64);
```
Also available: `Acceleration`, `Inertia`, `Pressure`, `Power`, `Area`, `Volume`, `Density`, `Frequency`, `Viscosity`, `Charge`, `Potential`, `Resistance`, `MagneticFlux`, `ThermalCapacity`, `ThermalConductivity`, and many more.
---
## Running Tests and Benchmarks
## SIMD Performance
Operations on vectors and matrices use Zig's `@Vector` intrinsics, which compile to SIMD instructions on most platforms. This makes vector operations faster than equivalent scalar loops, but don't expect miracles—SIMD is still limited by memory bandwidth and CPU cache.
Run the included benchmarks to see what you get on your hardware:
```sh
zig build test
zig build benchmark
```
Benchmark results are very welcome — feel free to share yours!
---
## Next Steps
- **GPU support** — eventually, for large tensor operations. WebGPU is a target.
- **Toy physics language** — I've been sketching ideas for a language optimized for numerical physics (tentatively called Éclat). It would use dimal as the foundation. No timeline yet; this is a long-term experiment.
---
## Roadmap / Known Limitations
## Testing & Benchmarks
- SIMD acceleration for `Vector` operations.
- Some paths may still fall back to runtime computation — optimization ongoing.
- More test coverage.
```sh
zig build test # Run all unit tests
zig build benchmark # Run performance benchmarks
```
---
+12 -4
View File
@@ -4,20 +4,27 @@ pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// 1. Define the module so other projects can import it
_ = b.addModule("dimal", .{
.root_source_file = b.path("src/main.zig"),
const zig_wgpu = b.dependency("zig_wgpu", .{
.target = target,
.optimize = optimize,
});
// 1. Define the module so other projects can import it
const mod = b.addModule("dimal", .{
.root_source_file = b.path("src/lib.zig"),
});
mod.addImport("gpu", zig_wgpu.module("zig-wgpu"));
const exe_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.root_source_file = b.path("src/test.zig"),
.target = target,
.optimize = optimize,
}),
.test_runner = .{ .path = b.path("test_runner.zig"), .mode = .simple },
});
exe_tests.root_module.addImport("gpu", zig_wgpu.module("zig-wgpu"));
const run_exe_tests = b.addRunArtifact(exe_tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_exe_tests.step);
@@ -30,6 +37,7 @@ pub fn build(b: *std.Build) void {
.imports = &.{},
}),
});
bench_exe.root_module.addImport("gpu", zig_wgpu.module("zig-wgpu"));
b.installArtifact(bench_exe);
+7 -2
View File
@@ -1,9 +1,14 @@
.{
.name = .dimal,
.version = "0.1.0",
.version = "0.3.0",
.fingerprint = 0x9453b1ff1e52d858,
.minimum_zig_version = "0.16.0",
.dependencies = .{},
.dependencies = .{
.zig_wgpu = .{
.url = "git+https://git.bouvais.lu/adrien/zig-wgpu?ref=0.2.2#5f8da0940d77c40eacd39c268d09acbeaea0b2a5",
.hash = "zig_wgpu-0.2.0-xsLAy2-s0QPNwR2QNd8ZX2kWiVfV5oB92N3ga1V1Uwpu",
},
},
.paths = .{
"build.zig",
"build.zig.zon",
+50 -41
View File
@@ -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("Scalar.zig").Scalar;
const Tensor = @import("TensorStatic.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 .{ .value = @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});
}
};
}
@@ -55,7 +60,7 @@ pub const Constants = struct {
/// Newtonian constant of gravitation (G) [m³⋅kg⁻¹⋅s⁻²]
pub const Gravitational = PhysicalConstant(.{ .M = -1, .L = 3, .T = -2 }, 6.67430e-11, .{ .M = .k });
/// Stefan–Boltzmann constant (σ) [W⋅m⁻²⋅K⁻⁴ = kg⋅s⁻³⋅K⁻⁴]
/// Stefan-Boltzmann constant () [W⋅m⁻²⋅K⁻⁴ = kg⋅s⁻³⋅K⁻⁴]
pub const StefanBoltzmann = PhysicalConstant(.{ .M = 1, .T = -3, .Tp = -4 }, 5.670374419e-8, .{ .M = .k });
/// Elementary charge (e) [C = A⋅s]
@@ -76,7 +81,7 @@ pub const Constants = struct {
/// Neutron mass (m_n) [kg]
pub const NeutronMass = PhysicalConstant(.{ .M = 1 }, 1.67492750056e-27, .{ .M = .k });
/// Fine-structure constant (α) [Dimensionless]
/// Fine-structure constant () [Dimensionless]
pub const FineStructure = PhysicalConstant(.{}, 0.0072973525643, .{});
/// Avogadro constant (N_A) [mol⁻¹]
@@ -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,83 +157,87 @@ 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{ .value = 100.0 };
try std.testing.expectEqual(100.0, distance.value);
const distance = M.splat(100);
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{ .value = 120.0 };
try std.testing.expectEqual(120.0, speed.value);
const speed = Kmh.splat(120);
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));
}
test "BaseQuantities - Kinematics equations" {
const d = Meter.Of(f32){ .value = 50.0 };
const t = Second.Of(f32){ .value = 2.0 };
const d = Meter.Of(f32).splat(50.0);
const t = Second.Of(f32).splat(2.0);
// Velocity = Distance / Time
const v = d.div(t);
try std.testing.expectEqual(25.0, v.value);
try std.testing.expect(Speed.dims.eql(@TypeOf(v).dims));
try std.testing.expectEqual(25.0, v.data[0]);
try comptime 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.expect(Acceleration.dims.eql(@TypeOf(a).dims));
try std.testing.expectEqual(12.5, a.data[0]);
try comptime std.testing.expect(Acceleration.dims.eql(@TypeOf(a).dims));
}
test "BaseQuantities - Dynamics (Force and Work)" {
// 10 kg
const m = Gramm.Scaled(f32, .{ .M = .k }){ .value = 10.0 };
const m = Gramm.Scaled(f32, .{ .M = .k }).splat(10.0);
// 9.8 m/s^2
const a = Acceleration.Of(f32){ .value = 9.8 };
const a = Acceleration.Of(f32).splat(9.8);
// Force = mass * acceleration
const f = m.mul(a);
try std.testing.expectEqual(98, f.value);
try std.testing.expect(Force.dims.eql(@TypeOf(f).dims));
try std.testing.expectEqual(98, f.data[0]);
try comptime std.testing.expect(Force.dims.eql(@TypeOf(f).dims));
// Energy (Work) = Force * distance
const distance = Meter.Of(f32){ .value = 5.0 };
const distance = Meter.Of(f32).splat(5.0);
const energy = f.mul(distance);
try std.testing.expectEqual(490, energy.value);
try std.testing.expect(Energy.dims.eql(@TypeOf(energy).dims));
try std.testing.expectEqual(490, energy.data[0]);
try comptime std.testing.expect(Energy.dims.eql(@TypeOf(energy).dims));
}
test "BaseQuantities - Electric combinations" {
const current = ElectricCurrent.Of(f32){ .value = 2.0 }; // 2 A
const time = Second.Of(f32){ .value = 3.0 }; // 3 s
const current = ElectricCurrent.Of(f32).splat(2); // 2 A
const time = Second.Of(f32).splat(3.0); // 3 s
// Charge = Current * time
const charge = current.mul(time);
try std.testing.expectEqual(6.0, charge.value);
try std.testing.expect(ElectricCharge.dims.eql(@TypeOf(charge).dims));
try std.testing.expectEqual(6.0, charge.data[0]);
try comptime 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));
}
+27 -27
View File
@@ -1,13 +1,13 @@
const std = @import("std");
pub const ArgOpts = struct {
L: comptime_int = 0,
M: comptime_int = 0,
T: comptime_int = 0,
I: comptime_int = 0,
Tp: comptime_int = 0,
N: comptime_int = 0,
J: comptime_int = 0,
L: isize = 0,
M: isize = 0,
T: isize = 0,
I: isize = 0,
Tp: isize = 0,
N: isize = 0,
J: isize = 0,
};
pub const Dimension = enum {
@@ -49,81 +49,81 @@ data: std.EnumArray(Dimension, comptime_int),
/// Create a `Dimensions` from a struct literal, e.g. `.{ .L = 1, .T = -1 }`.
/// Unspecified dimensions default to 0.
pub fn init(comptime init_val: ArgOpts) Self {
pub fn init(init_val: ArgOpts) Self {
var s = Self{ .data = std.EnumArray(Dimension, comptime_int).initFill(0) };
inline for (std.meta.fields(@TypeOf(init_val))) |f|
for (std.meta.fields(@TypeOf(init_val))) |f|
s.data.set(@field(Dimension, f.name), @field(init_val, f.name));
return s;
}
pub fn initFill(comptime val: comptime_int) Self {
pub fn initFill(val: comptime_int) Self {
return .{ .data = std.EnumArray(Dimension, comptime_int).initFill(val) };
}
pub fn get(comptime self: Self, comptime key: Dimension) comptime_int {
pub fn get(self: Self, key: Dimension) comptime_int {
return self.data.get(key);
}
pub fn set(comptime self: *Self, comptime key: Dimension, comptime val: i8) void {
pub fn set(self: *Self, key: Dimension, val: i8) void {
self.data.set(key, val);
}
pub fn argsOpt(self: Self) ArgOpts {
var args: ArgOpts = undefined;
inline for (std.enums.values(Dimension)) |d|
for (std.enums.values(Dimension)) |d|
@field(args, @tagName(d)) = self.get(d);
return args;
}
/// Add exponents component-wise. Used internally by `mul`.
pub fn add(comptime a: Self, comptime b: Self) Self {
pub fn add(a: Self, b: Self) Self {
var result = Self.initFill(0);
inline for (std.enums.values(Dimension)) |d|
for (std.enums.values(Dimension)) |d|
result.set(d, a.get(d) + b.get(d));
return result;
}
/// Subtract exponents component-wise. Used internally by `div`.
pub fn sub(comptime a: Self, comptime b: Self) Self {
pub fn sub(a: Self, b: Self) Self {
var result = Self.initFill(0);
inline for (std.enums.values(Dimension)) |d|
for (std.enums.values(Dimension)) |d|
result.set(d, a.get(d) - b.get(d));
return result;
}
/// Multiply exponents by a scalar integer. Used internally by `pow` in Scalar.
pub fn scale(comptime a: Self, comptime exp: comptime_int) Self {
pub fn scale(a: Self, exp: comptime_int) Self {
var result = Self.initFill(0);
inline for (std.enums.values(Dimension)) |d|
for (std.enums.values(Dimension)) |d|
result.set(d, a.get(d) * exp);
return result;
}
pub fn div(comptime a: Self, comptime exp: comptime_int) Self {
pub fn div(a: Self, exp: comptime_int) Self {
var result = Self.initFill(0);
inline for (std.enums.values(Dimension)) |d|
for (std.enums.values(Dimension)) |d|
result.set(d, a.get(d) / exp);
return result;
}
/// Returns true if every dimension exponent is equal. Used to enforce type compatibility in `add`, `sub`, `to`.
pub fn eql(comptime a: Self, comptime b: Self) bool {
inline for (std.enums.values(Dimension)) |d|
pub fn eql(a: Self, b: Self) bool {
for (std.enums.values(Dimension)) |d|
if (a.get(d) != b.get(d)) return false;
return true;
}
pub fn isSquare(comptime a: Self) bool {
inline for (std.enums.values(Dimension)) |d|
pub fn isSquare(a: Self) bool {
for (std.enums.values(Dimension)) |d|
if (a.get(d) % 2 != 0) return false;
return true;
}
pub fn str(comptime a: Self) []const u8 {
pub fn str(a: Self) []const u8 {
var out: []const u8 = "";
const dims = std.enums.values(Dimension);
inline for (dims) |d| {
for (dims) |d| {
const val = a.get(d);
if (val != 0) {
out = out ++ @tagName(d) ++ std.fmt.comptimePrint("{d}", .{val});
-625
View File
@@ -1,625 +0,0 @@
const std = @import("std");
const hlp = @import("helper.zig");
const Scales = @import("Scales.zig");
const Dimensions = @import("Dimensions.zig");
const Dimension = Dimensions.Dimension;
// ---------------------------------------------------------------------------
// Quantity — the single unified dimensioned type.
//
// T : element numeric type (f32, f64, i32, i128, …)
// N : lane count (1 = Scalar, >1 = Vector)
// d : SI dimension exponents
// s : unit scales
//
// All arithmetic is performed directly on the underlying @Vector(N, T), so
// the compiler can emit SIMD instructions wherever the target supports them.
//
// Thin aliases (same type identity, no wrapper overhead):
// Scalar(T, d, s) ≡ Quantity(T, 1, d, s)
// Vector(N, Q) ≡ Quantity(Q.ValueType, N, Q.dims.argsOpt(), Q.scales.argsOpt())
// ---------------------------------------------------------------------------
//
// @reduce(comptime op: std.builtin.ReduceOp, value: anytype) E
// @select(comptime T: type, pred: @Vector(len, bool), a: @Vector(len, T), b: @Vector(len, T)) @Vector(len, T)
// @shuffle(comptime E: type, a: @Vector(a_len, E), b: @Vector(b_len, E), comptime mask: @Vector(mask_len, i32)) @Vector(mask_len, E)
pub fn Quantity(
comptime T: type,
comptime N: usize,
comptime d_opt: Dimensions.ArgOpts,
comptime s_opt: Scales.ArgOpts,
) type {
comptime std.debug.assert(N >= 1);
@setEvalBranchQuota(10_000_000);
// Local shorthand for the SIMD vector type used in storage.
const Vec = @Vector(N, T);
return struct {
/// SIMD-friendly storage. Arithmetic operates here directly.
data: Vec,
const Self = @This();
pub const ValueType: type = T;
pub const Len: usize = N;
pub const dims: Dimensions = Dimensions.init(d_opt);
pub const scales: Scales = Scales.init(s_opt);
/// Scalar variant of this quantity (lane=1). Returned by dot(), product(), etc.
pub const ScalarType: type = Quantity(T, 1, d_opt, s_opt);
/// Convenience: a 3-lane vector of the same dimension/scale.
pub const Vec3: type = Quantity(T, 3, d_opt, s_opt);
// ---------------------------------------------------------------
// Constructors
// ---------------------------------------------------------------
/// Broadcast a single value across all N lanes.
pub inline fn splat(v: T) Self {
return .{ .data = @splat(v) };
}
/// Backward-compat alias used by Vector tests (`initDefault`).
pub const initDefault = splat;
pub const zero: Self = splat(0);
pub const one: Self = splat(1);
// ---------------------------------------------------------------
// Scalar-only helpers (N = 1)
// ---------------------------------------------------------------
/// Return the single scalar value. Compile error when N ≠ 1.
pub inline fn value(self: Self) T {
comptime if (N != 1)
@compileError(".value() is only available on Scalar (N=1).");
return self.data[0];
}
/// Expand this scalar into a len-lane vector by splatting.
pub inline fn vec(self: Self, comptime len: usize) Quantity(T, len, d_opt, s_opt) {
comptime if (N != 1)
@compileError(".vec() is only available on Scalar (N=1).");
return .{ .data = @splat(self.data[0]) };
}
pub inline fn vec3(self: Self) Vec3 {
return self.vec(3);
}
// ---------------------------------------------------------------
// Internal: RHS normalisation
//
// • For N=1 (Scalar context): bare numbers → Quantity(T, 1, dimless, none)
// • For N>1 (Vector context): bare numbers → Quantity(T, N, dimless, none)
// Quantity(T,1) → broadcast (handled in each op)
//
// A bare number used as rhs is ALWAYS treated as dimensionless.
// ---------------------------------------------------------------
inline fn RhsT(comptime Rhs: type) type {
return hlp.rhsQuantityType(T, N, Rhs);
}
inline fn rhs(r: anytype) RhsT(@TypeOf(r)) {
return hlp.toRhsQuantity(T, N, r);
}
/// Scalar rhs (N=1) — used by mulScalar / divScalar / eqScalar etc.
inline fn ScalarRhsT(comptime Rhs: type) type {
return hlp.rhsQuantityType(T, 1, Rhs);
}
inline fn scalarRhs(r: anytype) ScalarRhsT(@TypeOf(r)) {
return hlp.toRhsQuantity(T, 1, r);
}
// ---------------------------------------------------------------
// Internal: broadcast helper
//
// When an N=1 rhs is used in an N>1 operation, splat it.
// ---------------------------------------------------------------
inline fn broadcastToVec(comptime RhsType: type, r: RhsType) Vec {
if (comptime RhsType.Len == 1 and N > 1)
return @splat(r.data[0])
else
return r.data;
}
// ---------------------------------------------------------------
// Arithmetic
// ---------------------------------------------------------------
/// Element-wise add. Dimensions must match; scales resolve to finer.
/// For N=1: rhs may be a bare number (treated as dimensionless).
/// For N>1: rhs must be a same-length Quantity.
pub inline fn add(self: Self, r: anytype) Quantity(
T,
N,
dims.argsOpt(),
hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(),
) {
const rhs_q = rhs(r);
const RhsType = @TypeOf(rhs_q);
if (comptime !dims.eql(RhsType.dims))
@compileError("Dimension mismatch in add: " ++ dims.str() ++ " vs " ++ RhsType.dims.str());
if (comptime N > 1 and RhsType.Len != N)
@compileError("Vector add requires same-length Quantity.");
const TargetType = Quantity(T, N, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const l: Vec = if (comptime Self == TargetType) self.data else self.to(TargetType).data;
const rr: Vec = if (comptime RhsType == TargetType) rhs_q.data else rhs_q.to(TargetType).data;
return .{ .data = if (comptime hlp.isInt(T)) l +| rr else l + rr };
}
/// Element-wise subtract. Dimensions must match; scales resolve to finer.
pub inline fn sub(self: Self, r: anytype) Quantity(
T,
N,
dims.argsOpt(),
hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(),
) {
const rhs_q = rhs(r);
const RhsType = @TypeOf(rhs_q);
if (comptime !dims.eql(RhsType.dims))
@compileError("Dimension mismatch in sub: " ++ dims.str() ++ " vs " ++ RhsType.dims.str());
if (comptime N > 1 and RhsType.Len != N)
@compileError("Vector sub requires same-length Quantity.");
const TargetType = Quantity(T, N, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const l: Vec = if (comptime Self == TargetType) self.data else self.to(TargetType).data;
const rr: Vec = if (comptime RhsType == TargetType) rhs_q.data else rhs_q.to(TargetType).data;
return .{ .data = if (comptime hlp.isInt(T)) l -| rr else l - rr };
}
/// Element-wise multiply. Dimension exponents are summed.
/// An N=1 rhs on an N>1 self is automatically broadcast (scalar × vector).
pub inline fn mul(self: Self, r: anytype) Quantity(
T,
N,
dims.add(RhsT(@TypeOf(r)).dims).argsOpt(),
hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(),
) {
const rhs_q = rhs(r);
const RhsType = @TypeOf(rhs_q);
const SelfNorm = Quantity(T, N, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const RhsNorm = Quantity(T, N, RhsType.dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const l: Vec = if (comptime Self == SelfNorm) self.data else self.to(SelfNorm).data;
const rr_base = if (comptime RhsType == RhsNorm) rhs_q else rhs_q.to(RhsNorm);
const rr: Vec = broadcastToVec(RhsNorm, rr_base);
return .{ .data = if (comptime hlp.isInt(T)) l *| rr else l * rr };
}
/// Element-wise divide. Dimension exponents are subtracted.
/// An N=1 rhs on an N>1 self is automatically broadcast.
pub inline fn div(self: Self, r: anytype) Quantity(
T,
N,
dims.sub(RhsT(@TypeOf(r)).dims).argsOpt(),
hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(),
) {
const rhs_q = rhs(r);
const RhsType = @TypeOf(rhs_q);
const SelfNorm = Quantity(T, N, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const RhsNorm = Quantity(T, N, RhsType.dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const l: Vec = if (comptime Self == SelfNorm) self.data else self.to(SelfNorm).data;
const rr_base = if (comptime RhsType == RhsNorm) rhs_q else rhs_q.to(RhsNorm);
const rr: Vec = broadcastToVec(RhsNorm, rr_base);
if (comptime hlp.isInt(T)) {
var result: Vec = undefined;
inline for (0..N) |i| result[i] = @divTrunc(l[i], rr[i]);
return .{ .data = result };
} else {
return .{ .data = l / rr };
}
}
// ---------------------------------------------------------------
// Unary
// ---------------------------------------------------------------
/// Absolute value of every lane. Uses native `@abs` (SIMD for floats & ints).
pub inline fn abs(self: Self) Self {
return .{ .data = @abs(self.data) };
}
/// Raise every lane to a comptime integer exponent.
/// Repeated SIMD multiply — good for small exponents.
pub inline fn pow(self: Self, comptime exp: comptime_int) Quantity(
T,
N,
dims.scale(exp).argsOpt(),
scales.argsOpt(),
) {
if (comptime hlp.isInt(T)) {
// No SIMD pow for integers — element-wise std.math.powi.
var result: Vec = undefined;
inline for (0..N) |i|
result[i] = std.math.powi(T, self.data[i], exp) catch std.math.maxInt(T);
return .{ .data = result };
} else {
// Float: unrolled SIMD multiplications.
const abs_exp = comptime @abs(exp);
var result: Vec = @splat(1);
comptime var i = 0;
inline while (i < abs_exp) : (i += 1) result *= self.data;
if (comptime exp < 0) result = @as(Vec, @splat(1)) / result;
return .{ .data = result };
}
}
/// Square root of every lane. All dimension exponents must be even.
pub inline fn sqrt(self: Self) Quantity(
T,
N,
dims.div(2).argsOpt(),
scales.argsOpt(),
) {
if (comptime !dims.isSquare())
@compileError("Cannot take sqrt of " ++ dims.str() ++ ": exponents must be even.");
if (comptime @typeInfo(T) == .float) {
return .{ .data = @sqrt(self.data) };
} else {
// Integer sqrt is not SIMD-able — element-wise.
var result: Vec = undefined;
const UnsignedT = @Int(.unsigned, @typeInfo(T).int.bits);
inline for (0..N) |i| {
const v = self.data[i];
if (v < 0) {
result[i] = 0;
continue;
}
result[i] = @as(T, @intCast(std.math.sqrt(@as(UnsignedT, @intCast(v)))));
}
return .{ .data = result };
}
}
/// Negate every lane.
pub inline fn negate(self: Self) Self {
return .{ .data = -self.data };
}
// ---------------------------------------------------------------
// Conversion
// ---------------------------------------------------------------
/// Convert to a compatible quantity type. Dimension mismatch is a compile error.
/// The scale ratio is computed entirely at comptime; the only runtime cost is
/// a SIMD multiply-by-splat (or element-wise cast for cross-numeric-type conversions).
pub inline fn to(self: Self, comptime Dest: type) Dest {
if (comptime !dims.eql(Dest.dims))
@compileError("Dimension mismatch in to: " ++ dims.str() ++ " vs " ++ Dest.dims.str());
if (comptime Self == Dest) return self;
comptime std.debug.assert(Dest.Len == N);
const DestT = Dest.ValueType;
const ratio = comptime (scales.getFactor(dims) / Dest.scales.getFactor(Dest.dims));
const DestVec = @Vector(N, DestT);
// ── Fast path: same numeric type → pure SIMD ──────────────
if (comptime T == DestT) {
if (comptime @typeInfo(T) == .float) {
return .{ .data = self.data * @as(DestVec, @splat(@as(T, @floatCast(ratio)))) };
}
// Integer same-T
if (comptime ratio >= 1.0 and @round(ratio) == ratio) {
const mult: T = comptime @intFromFloat(ratio);
return .{ .data = self.data *| @as(Vec, @splat(mult)) };
}
// Sub-unit ratio: rounded integer divide, element-wise.
const d: T = comptime @intFromFloat(1.0 / ratio);
var result: DestVec = undefined;
inline for (0..N) |i| {
const val = self.data[i];
const half: T = comptime d / 2;
result[i] = if (val >= 0) @divTrunc(val + half, d) else @divTrunc(val - half, d);
}
return .{ .data = result };
}
// ── Cross-numeric-type: go through f64 element-wise ────────
var result: DestVec = undefined;
inline for (0..N) |i| {
const float_val: f64 = switch (comptime @typeInfo(T)) {
.float => @floatCast(self.data[i]),
.int => @floatFromInt(self.data[i]),
else => unreachable,
};
const scaled = float_val * ratio;
result[i] = switch (comptime @typeInfo(DestT)) {
.float => @floatCast(scaled),
.int => @intFromFloat(@round(scaled)),
else => unreachable,
};
}
return .{ .data = result };
}
// ---------------------------------------------------------------
// Comparisons
//
// Return type: bool when N = 1 (Scalar semantics)
// [N]bool when N > 1 (Vector semantics, element-wise)
//
// Whole-vector "all equal/any differ" → use eqAll / neAll.
// Broadcast scalar comparison → use eqScalar / gtScalar / …
// ---------------------------------------------------------------
const CmpResult = if (N == 1) bool else [N]bool;
inline fn cmpResult(v: @Vector(N, bool)) CmpResult {
return if (comptime N == 1) v[0] else @as([N]bool, v);
}
inline fn resolveScalePair(self: Self, rhs_q: anytype) struct { l: Vec, r: Vec } {
const RhsType = @TypeOf(rhs_q);
const TargetType = Quantity(T, N, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
return .{
.l = if (comptime Self == TargetType) self.data else self.to(TargetType).data,
.r = if (comptime RhsType == TargetType) rhs_q.data else rhs_q.to(TargetType).data,
};
}
pub inline fn eq(self: Self, r: anytype) CmpResult {
const rhs_q = rhs(r);
if (comptime !dims.eql(@TypeOf(rhs_q).dims))
@compileError("Dimension mismatch in eq: " ++ dims.str() ++ " vs " ++ @TypeOf(rhs_q).dims.str());
const p = resolveScalePair(self, rhs_q);
return cmpResult(p.l == p.r);
}
pub inline fn ne(self: Self, r: anytype) CmpResult {
const rhs_q = rhs(r);
if (comptime !dims.eql(@TypeOf(rhs_q).dims))
@compileError("Dimension mismatch in ne.");
const p = resolveScalePair(self, rhs_q);
return cmpResult(p.l != p.r);
}
pub inline fn gt(self: Self, r: anytype) CmpResult {
const rhs_q = rhs(r);
if (comptime !dims.eql(@TypeOf(rhs_q).dims))
@compileError("Dimension mismatch in gt.");
const p = resolveScalePair(self, rhs_q);
return cmpResult(p.l > p.r);
}
pub inline fn gte(self: Self, r: anytype) CmpResult {
const rhs_q = rhs(r);
if (comptime !dims.eql(@TypeOf(rhs_q).dims))
@compileError("Dimension mismatch in gte.");
const p = resolveScalePair(self, rhs_q);
return cmpResult(p.l >= p.r);
}
pub inline fn lt(self: Self, r: anytype) CmpResult {
const rhs_q = rhs(r);
if (comptime !dims.eql(@TypeOf(rhs_q).dims))
@compileError("Dimension mismatch in lt.");
const p = resolveScalePair(self, rhs_q);
return cmpResult(p.l < p.r);
}
pub inline fn lte(self: Self, r: anytype) CmpResult {
const rhs_q = rhs(r);
if (comptime !dims.eql(@TypeOf(rhs_q).dims))
@compileError("Dimension mismatch in lte.");
const p = resolveScalePair(self, rhs_q);
return cmpResult(p.l <= p.r);
}
// ---------------------------------------------------------------
// Vector whole-quantity comparisons (N > 1 intended, but work for N=1 too)
// ---------------------------------------------------------------
/// True iff every lane is equal after scale resolution.
pub inline fn eqAll(self: Self, other: anytype) bool {
if (comptime !dims.eql(@TypeOf(other).dims))
@compileError("Dimension mismatch in eqAll.");
const p = resolveScalePair(self, other);
return @reduce(.And, p.l == p.r);
}
pub inline fn neAll(self: Self, other: anytype) bool {
return !self.eqAll(other);
}
// ---------------------------------------------------------------
// Vector broadcast-scalar comparisons (always returns [N]bool)
// ---------------------------------------------------------------
inline fn broadcastScalarForCmp(self: Self, scalar: anytype) struct { l: Vec, r: Vec } {
const s = scalarRhs(scalar);
const SN = @TypeOf(s);
const TargetScalar = Quantity(T, 1, dims.argsOpt(), hlp.finerScales(Self, SN).argsOpt());
const TargetSelf = Quantity(T, N, dims.argsOpt(), hlp.finerScales(Self, SN).argsOpt());
const s_val: T = if (comptime SN == TargetScalar) s.data[0] else s.to(TargetScalar).data[0];
const l: Vec = if (comptime Self == TargetSelf) self.data else self.to(TargetSelf).data;
return .{ .l = l, .r = @splat(s_val) };
}
pub inline fn eqScalar(self: Self, scalar: anytype) [N]bool {
const p = broadcastScalarForCmp(self, scalar);
return @as([N]bool, p.l == p.r);
}
pub inline fn neScalar(self: Self, scalar: anytype) [N]bool {
const p = broadcastScalarForCmp(self, scalar);
return @as([N]bool, p.l != p.r);
}
pub inline fn gtScalar(self: Self, scalar: anytype) [N]bool {
const p = broadcastScalarForCmp(self, scalar);
return @as([N]bool, p.l > p.r);
}
pub inline fn gteScalar(self: Self, scalar: anytype) [N]bool {
const p = broadcastScalarForCmp(self, scalar);
return @as([N]bool, p.l >= p.r);
}
pub inline fn ltScalar(self: Self, scalar: anytype) [N]bool {
const p = broadcastScalarForCmp(self, scalar);
return @as([N]bool, p.l < p.r);
}
pub inline fn lteScalar(self: Self, scalar: anytype) [N]bool {
const p = broadcastScalarForCmp(self, scalar);
return @as([N]bool, p.l <= p.r);
}
// ---------------------------------------------------------------
// Vector broadcast multiply / divide
// (These are explicit aliases for mul/div with an N=1 rhs; kept for
// clarity and backward-compat with the old Vector API.)
// ---------------------------------------------------------------
pub inline fn mulScalar(self: Self, scalar: anytype) Quantity(
T,
N,
dims.add(ScalarRhsT(@TypeOf(scalar)).dims).argsOpt(),
hlp.finerScales(Self, ScalarRhsT(@TypeOf(scalar))).argsOpt(),
) {
return self.mul(scalar);
}
pub inline fn divScalar(self: Self, scalar: anytype) Quantity(
T,
N,
dims.sub(ScalarRhsT(@TypeOf(scalar)).dims).argsOpt(),
hlp.finerScales(Self, ScalarRhsT(@TypeOf(scalar))).argsOpt(),
) {
return self.div(scalar);
}
// ---------------------------------------------------------------
// Vector geometric operations
// ---------------------------------------------------------------
/// Dot product — sum of element-wise products; returns a Scalar.
pub inline fn dot(self: Self, other: anytype) Quantity(
T,
1,
dims.add(@TypeOf(other).dims).argsOpt(),
hlp.finerScales(Self, @TypeOf(other)).argsOpt(),
) {
const Tr = @TypeOf(other);
const SelfNorm = Quantity(T, N, dims.argsOpt(), hlp.finerScales(Self, Tr).argsOpt());
const OtherNorm = Quantity(T, N, Tr.dims.argsOpt(), hlp.finerScales(Self, Tr).argsOpt());
const l: Vec = if (comptime Self == SelfNorm) self.data else self.to(SelfNorm).data;
const r2: Vec = if (comptime Tr == OtherNorm) other.data else other.to(OtherNorm).data;
return .{ .data = .{@reduce(.Add, l * r2)} };
}
/// 3D cross product. Requires N = 3.
pub inline fn cross(self: Self, other: anytype) Quantity(
T,
3,
dims.add(@TypeOf(other).dims).argsOpt(),
hlp.finerScales(Self, @TypeOf(other)).argsOpt(),
) {
comptime if (N != 3) @compileError("cross() requires len=3.");
const a = self.data;
const b = other.data;
return .{ .data = .{
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
} };
}
/// Sum of squared components. Cheaper than length(); use for comparisons.
pub inline fn lengthSqr(self: Self) T {
return @reduce(.Add, self.data * self.data);
}
/// Euclidean length. Float types use SIMD @reduce → @sqrt.
/// Integer types use integer sqrt (truncated).
pub inline fn length(self: Self) T {
const sq = self.lengthSqr();
if (comptime @typeInfo(T) == .int) {
const UnsignedT = @Int(.unsigned, @typeInfo(T).int.bits);
return @as(T, @intCast(std.math.sqrt(@as(UnsignedT, @intCast(sq)))));
}
return @sqrt(sq);
}
/// Product of all components. Result dimension is (original dim × N).
pub inline fn product(self: Self) Quantity(T, 1, dims.scale(N).argsOpt(), scales.argsOpt()) {
return .{ .data = .{@reduce(.Mul, self.data)} };
}
// ---------------------------------------------------------------
// Formatting (unchanged from old Scalar / Vector)
// ---------------------------------------------------------------
pub fn formatNumber(
self: Self,
writer: *std.Io.Writer,
options: std.fmt.Number,
) !void {
if (comptime N == 1) {
// Scalar-style: just print the value + units
switch (@typeInfo(T)) {
.float, .comptime_float => try writer.printFloat(self.data[0], options),
.int, .comptime_int => try writer.printInt(self.data[0], 10, .lower, .{
.width = options.width,
.alignment = options.alignment,
.fill = options.fill,
.precision = options.precision,
}),
else => unreachable,
}
} else {
// Vector-style: (v0, v1, …) + units
try writer.writeAll("(");
inline for (0..N) |i| {
if (i > 0) try writer.writeAll(", ");
switch (@typeInfo(T)) {
.float, .comptime_float => try writer.printFloat(self.data[i], options),
.int, .comptime_int => try writer.printInt(self.data[i], 10, .lower, .{
.width = options.width,
.alignment = options.alignment,
.fill = options.fill,
.precision = options.precision,
}),
else => unreachable,
}
}
try writer.writeAll(")");
}
var first = true;
inline for (std.enums.values(Dimension)) |bu| {
const v = dims.get(bu);
if (comptime v == 0) continue;
if (!first) try writer.writeAll(".");
first = false;
const uscale = scales.get(bu);
if (bu == .T and (uscale == .min or uscale == .hour or uscale == .year))
try writer.print("{s}", .{uscale.str()})
else
try writer.print("{s}{s}", .{ uscale.str(), bu.unit() });
if (v != 1) try hlp.printSuperscript(writer, v);
}
}
};
}
fn Scalar(comptime T: type, comptime d: Dimensions.ArgOpts, comptime s: Scales.ArgOpts) type {
return Quantity(T, 1, d, s);
}
test "Generate quantity" {
const Meter = Scalar(i128, .{ .L = 1 }, .{ .L = @enumFromInt(-3) });
const Second = Scalar(f32, .{ .T = 1 }, .{ .T = .n });
const distance = Meter.splat(10);
const time = Second.splat(2);
try std.testing.expectEqual(10, distance.value());
try std.testing.expectEqual(2, time.value());
}
-815
View File
@@ -1,815 +0,0 @@
const std = @import("std");
const hlp = @import("helper.zig");
const Vector = @import("Vector.zig").Vector;
const Scales = @import("Scales.zig");
const UnitScale = Scales.UnitScale;
const Dimensions = @import("Dimensions.zig");
const Dimension = Dimensions.Dimension;
/// A dimensioned scalar value. `T` is the numeric type, `d` the dimension exponents, `s` the SI scales.
/// All dimension and unit tracking is resolved at comptime — zero runtime overhead.
pub fn Scalar(comptime T: type, comptime d_opt: Dimensions.ArgOpts, comptime s_opt: Scales.ArgOpts) type {
@setEvalBranchQuota(10_000_000);
return struct {
value: T,
const Self = @This();
/// Type of Vector(3, Self)
pub const Vec3: type = Vector(3, Self);
/// Type of underline value, mostly use for Vector
pub const ValueType: type = T;
pub const dims: Dimensions = Dimensions.init(d_opt);
pub const scales = Scales.init(s_opt);
// ---------------------------------------------------------------
// Internal: resolved-rhs shorthands
// ---------------------------------------------------------------
/// Scalar type that `rhs` normalises to (bare numbers → dimensionless).
inline fn RhsT(comptime Rhs: type) type {
return hlp.rhsScalarType(T, Rhs);
}
/// Normalise `rhs` (bare number or Scalar) into a proper Scalar value.
inline fn rhs(r: anytype) RhsT(@TypeOf(r)) {
return hlp.toRhsScalar(T, r);
}
// ---------------------------------------------------------------
// Arithmetic
// ---------------------------------------------------------------
/// Add two quantities. Dimensions must match — compile error otherwise.
/// Scales are auto-resolved to the finer of the two.
/// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`
/// (bare numbers are treated as dimensionless).
pub inline fn add(self: Self, r: anytype) Scalar(
T,
dims.argsOpt(),
hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(),
) {
const rhs_s = rhs(r);
const RhsType = @TypeOf(rhs_s);
if (comptime !dims.eql(RhsType.dims))
@compileError("Dimension mismatch in add: " ++ dims.str() ++ " vs " ++ RhsType.dims.str());
if (comptime RhsType == Self)
return .{ .value = self.value + rhs_s.value };
const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value;
const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value;
return .{ .value = if (comptime hlp.isInt(T)) lhs_val +| rhs_val else lhs_val + rhs_val };
}
/// Subtract two quantities. Dimensions must match — compile error otherwise.
/// Scales are auto-resolved to the finer of the two.
/// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`.
pub inline fn sub(self: Self, r: anytype) Scalar(
T,
dims.argsOpt(),
hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(),
) {
const rhs_s = rhs(r);
const RhsType = @TypeOf(rhs_s);
if (comptime !dims.eql(RhsType.dims))
@compileError("Dimension mismatch in sub: " ++ dims.str() ++ " vs " ++ RhsType.dims.str());
if (comptime RhsType == Self)
return .{ .value = self.value - rhs_s.value };
const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value;
const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value;
return .{ .value = if (comptime hlp.isInt(T)) lhs_val -| rhs_val else lhs_val - rhs_val };
}
/// Multiply two quantities. Dimension exponents are summed: `L¹ * T⁻¹ → L¹T⁻¹`.
/// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`
/// (bare numbers are treated as dimensionless — dimensions pass through unchanged).
pub inline fn mul(self: Self, r: anytype) Scalar(
T,
dims.add(RhsT(@TypeOf(r)).dims).argsOpt(),
hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(),
) {
const rhs_s = rhs(r);
const RhsType = @TypeOf(rhs_s);
const SelfNorm = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const RhsNorm = Scalar(T, RhsType.dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
if (comptime Self == SelfNorm and RhsType == RhsNorm)
return .{ .value = self.value * rhs_s.value };
const lhs_val = if (comptime Self == SelfNorm) self.value else self.to(SelfNorm).value;
const rhs_val = if (comptime RhsType == RhsNorm) rhs_s.value else rhs_s.to(RhsNorm).value;
return .{ .value = if (comptime hlp.isInt(T)) lhs_val *| rhs_val else lhs_val * rhs_val };
}
/// Divide two quantities. Dimension exponents are subtracted: `L¹ / T¹ → L¹T⁻¹`.
/// Integer types use truncating division.
/// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`.
pub inline fn div(self: Self, r: anytype) Scalar(
T,
dims.sub(RhsT(@TypeOf(r)).dims).argsOpt(),
hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(),
) {
const rhs_s = rhs(r);
const RhsType = @TypeOf(rhs_s);
const SelfNorm = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const RhsNorm = Scalar(T, RhsType.dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const lhs_val = if (comptime Self == SelfNorm) self.value else self.to(SelfNorm).value;
const rhs_val = if (comptime RhsType == RhsNorm) rhs_s.value else rhs_s.to(RhsNorm).value;
if (comptime hlp.isInt(T)) {
return .{ .value = @divTrunc(lhs_val, rhs_val) };
} else {
return .{ .value = lhs_val / rhs_val };
}
}
// ---------------------------------------------------------------
// Unary
// ---------------------------------------------------------------
/// Returns the absolute value of the quantity.
/// Dimensions and scales remain entirely unchanged.
pub inline fn abs(self: Self) Self {
if (comptime @typeInfo(T) == .int)
return .{ .value = @intCast(@abs(self.value)) }
else
return .{ .value = @abs(self.value) };
}
/// Raises the quantity to a compile-time integer exponent.
/// Dimension exponents are multiplied by the exponent: `(L²)³ → L⁶`.
pub inline fn pow(self: Self, comptime exp: comptime_int) Scalar(
T,
dims.scale(exp).argsOpt(),
scales.argsOpt(),
) {
if (comptime hlp.isInt(T))
return .{ .value = std.math.powi(T, self.value, exp) catch std.math.maxInt(T) }
else
return .{ .value = std.math.pow(T, self.value, @as(T, @floatFromInt(exp))) };
}
pub inline fn sqrt(self: Self) Scalar(
T,
dims.div(2).argsOpt(),
scales.argsOpt(),
) {
if (comptime !dims.isSquare()) // Check if all exponents are divisible by 2
@compileError("Cannot take sqrt of " ++ dims.str() ++ ": exponents must be even.");
if (self.value < 0) return .{ .value = 0 };
if (comptime hlp.isInt(T)) {
const UnsignedT = @Int(.unsigned, @typeInfo(T).int.bits);
const u_len_sq = @as(UnsignedT, @intCast(self.value));
return .{ .value = @as(T, @intCast(std.math.sqrt(u_len_sq))) };
} else {
return .{ .value = @sqrt(self.value) };
}
}
// ---------------------------------------------------------------
// Conversion
// ---------------------------------------------------------------
/// Convert to a compatible unit type. The scale ratio is computed at comptime.
/// Compile error if dimensions don't match.
pub inline fn to(self: Self, comptime Dest: type) Dest {
if (comptime !dims.eql(Dest.dims))
@compileError("Dimension mismatch in to: " ++ dims.str() ++ " vs " ++ Dest.dims.str());
if (comptime @TypeOf(self) == Dest)
return self;
const DestT = Dest.ValueType;
const ratio = comptime (scales.getFactor(dims) / Dest.scales.getFactor(Dest.dims));
// Fast-path: Native pure-integer exact conversions
if (comptime @typeInfo(T) == .int and @typeInfo(DestT) == .int) {
if (comptime ratio >= 1.0 and @round(ratio) == ratio) {
const mult: DestT = comptime @intFromFloat(ratio);
return .{ .value = @as(DestT, @intCast(self.value)) * mult };
} else if (comptime ratio < 1.0 and @round(1.0 / ratio) == 1.0 / ratio) {
const d: DestT = comptime @intFromFloat(1.0 / ratio);
const val = @as(DestT, @intCast(self.value));
const half = comptime d / 2;
const rounded = if (val >= 0) @divTrunc(val + half, d) else @divTrunc(val - half, d);
return .{ .value = rounded };
}
}
// Fallback preserving native Float types (e.g., f128 shouldn't downcast to f64)
if (comptime @typeInfo(DestT) == .float) {
const val_f = switch (@typeInfo(T)) {
inline .int => @as(DestT, @floatFromInt(self.value)),
inline .float => @as(DestT, @floatCast(self.value)),
else => unreachable,
};
return .{ .value = val_f * @as(DestT, @floatCast(ratio)) };
} else {
const val_f = switch (@typeInfo(T)) {
inline .int => @as(f64, @floatFromInt(self.value)),
inline .float => @as(f64, @floatCast(self.value)),
else => unreachable,
};
return .{ .value = @intFromFloat(@round(val_f * ratio)) };
}
}
// ---------------------------------------------------------------
// Comparisons
// ---------------------------------------------------------------
/// Compares two Scalar for exact equality.
/// Dimensions must match — compile error otherwise. Scales are auto-resolved.
/// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`.
pub inline fn eq(self: Self, r: anytype) bool {
const rhs_s = rhs(r);
const RhsType = @TypeOf(rhs_s);
if (comptime !dims.eql(RhsType.dims))
@compileError("Dimension mismatch in eq: " ++ dims.str() ++ " vs " ++ RhsType.dims.str());
if (comptime RhsType == Self)
return self.value == rhs_s.value;
const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value;
const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value;
return lhs_val == rhs_val;
}
/// Compares two quantities for inequality.
/// Dimensions must match — compile error otherwise. Scales are auto-resolved.
/// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`.
pub inline fn ne(self: Self, r: anytype) bool {
const rhs_s = rhs(r);
const RhsType = @TypeOf(rhs_s);
if (comptime !dims.eql(RhsType.dims))
@compileError("Dimension mismatch in ne: " ++ dims.str() ++ " vs " ++ RhsType.dims.str());
if (comptime RhsType == Self)
return self.value != rhs_s.value;
const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value;
const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value;
return lhs_val != rhs_val;
}
/// Returns true if this quantity is strictly greater than the right-hand side.
/// Dimensions must match — compile error otherwise. Scales are auto-resolved.
/// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`.
pub inline fn gt(self: Self, r: anytype) bool {
const rhs_s = rhs(r);
const RhsType = @TypeOf(rhs_s);
if (comptime !dims.eql(RhsType.dims))
@compileError("Dimension mismatch in gt: " ++ dims.str() ++ " vs " ++ RhsType.dims.str());
if (comptime RhsType == Self)
return self.value > rhs_s.value;
const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value;
const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value;
return lhs_val > rhs_val;
}
/// Returns true if this quantity is greater than or equal to the right-hand side.
/// Dimensions must match — compile error otherwise. Scales are auto-resolved.
/// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`.
pub inline fn gte(self: Self, r: anytype) bool {
const rhs_s = rhs(r);
const RhsType = @TypeOf(rhs_s);
if (comptime !dims.eql(RhsType.dims))
@compileError("Dimension mismatch in gte: " ++ dims.str() ++ " vs " ++ RhsType.dims.str());
if (comptime RhsType == Self)
return self.value >= rhs_s.value;
const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value;
const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value;
return lhs_val >= rhs_val;
}
/// Returns true if this quantity is strictly less than the right-hand side.
/// Dimensions must match — compile error otherwise. Scales are auto-resolved.
/// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`.
pub inline fn lt(self: Self, r: anytype) bool {
const rhs_s = rhs(r);
const RhsType = @TypeOf(rhs_s);
if (comptime !dims.eql(RhsType.dims))
@compileError("Dimension mismatch in lt: " ++ dims.str() ++ " vs " ++ RhsType.dims.str());
if (comptime RhsType == Self)
return self.value < rhs_s.value;
const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value;
const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value;
return lhs_val < rhs_val;
}
/// Returns true if this quantity is less than or equal to the right-hand side.
/// Dimensions must match — compile error otherwise. Scales are auto-resolved.
/// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`.
pub inline fn lte(self: Self, r: anytype) bool {
const rhs_s = rhs(r);
const RhsType = @TypeOf(rhs_s);
if (comptime !dims.eql(RhsType.dims))
@compileError("Dimension mismatch in lte: " ++ dims.str() ++ " vs " ++ RhsType.dims.str());
if (comptime RhsType == Self)
return self.value <= rhs_s.value;
const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value;
const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value;
return lhs_val <= rhs_val;
}
// ---------------------------------------------------------------
// Vector helpers
// ---------------------------------------------------------------
/// Return a `Vector(len, Self)` type.
pub fn Vec(_: Self, comptime len: comptime_int) type {
return Vector(len, Self);
}
/// Return a `Vector(len, Self)` with all components set to this value.
pub fn vec(self: Self, comptime len: comptime_int) Vector(len, Self) {
return Vector(len, Self).initDefault(self.value);
}
/// Shorthand for `Vec(3)` — wrap this value into a 3-component vector.
pub fn vec3(self: Self) Vec3 {
return Vec3.initDefault(self.value);
}
// ---------------------------------------------------------------
// Formatting
// ---------------------------------------------------------------
pub fn formatNumber(
self: Self,
writer: *std.Io.Writer,
options: std.fmt.Number,
) !void {
switch (@typeInfo(T)) {
.float, .comptime_float => try writer.printFloat(self.value, options),
.int, .comptime_int => try writer.printInt(self.value, 10, .lower, .{
.width = options.width,
.alignment = options.alignment,
.fill = options.fill,
.precision = options.precision,
}),
else => unreachable,
}
var first = true;
inline for (std.enums.values(Dimension)) |bu| {
const v = dims.get(bu);
if (comptime v == 0) continue;
if (!first)
try writer.writeAll(".");
first = false;
const uscale = scales.get(bu);
if (bu == .T and (uscale == .min or uscale == .hour or uscale == .year))
try writer.print("{s}", .{uscale.str()})
else
try writer.print("{s}{s}", .{ uscale.str(), bu.unit() });
if (v != 1)
try hlp.printSuperscript(writer, v);
}
}
};
}
test "Generate quantity" {
const Meter = Scalar(i128, .{ .L = 1 }, .{ .L = @enumFromInt(-3) });
const Second = Scalar(f32, .{ .T = 1 }, .{ .T = .n });
const distance = Meter{ .value = 10 };
const time = Second{ .value = 2 };
try std.testing.expectEqual(10, distance.value);
try std.testing.expectEqual(2, time.value);
}
test "Comparisons (eq, ne, gt, gte, lt, lte)" {
const Meter = Scalar(i128, .{ .L = 1 }, .{});
const KiloMeter = Scalar(i128, .{ .L = 1 }, .{ .L = .k });
const m1000 = Meter{ .value = 1000 };
const km1 = KiloMeter{ .value = 1 };
const km2 = KiloMeter{ .value = 2 };
// Equal / Not Equal
try std.testing.expect(m1000.eq(km1));
try std.testing.expect(km1.eq(m1000));
try std.testing.expect(km2.ne(m1000));
// Greater Than / Greater Than or Equal
try std.testing.expect(km2.gt(m1000));
try std.testing.expect(km2.gt(km1));
try std.testing.expect(km1.gte(m1000));
try std.testing.expect(km2.gte(m1000));
// Less Than / Less Than or Equal
try std.testing.expect(m1000.lt(km2));
try std.testing.expect(km1.lt(km2));
try std.testing.expect(km1.lte(m1000));
try std.testing.expect(m1000.lte(km2));
}
test "Add" {
const Meter = Scalar(i128, .{ .L = 1 }, .{});
const distance = Meter{ .value = 10 };
const distance2 = Meter{ .value = 20 };
const added = distance.add(distance2);
try std.testing.expectEqual(30, added.value);
try std.testing.expectEqual(1, @TypeOf(added).dims.get(.L));
const KiloMeter = Scalar(i128, .{ .L = 1 }, .{ .L = .k });
const distance3 = KiloMeter{ .value = 2 };
const added2 = distance.add(distance3);
try std.testing.expectEqual(2010, added2.value);
try std.testing.expectEqual(1, @TypeOf(added2).dims.get(.L));
const added3 = distance3.add(distance).to(KiloMeter);
try std.testing.expectEqual(2, added3.value);
try std.testing.expectEqual(1, @TypeOf(added3).dims.get(.L));
const KiloMeter_f = Scalar(f64, .{ .L = 1 }, .{ .L = .k });
const distance4 = KiloMeter_f{ .value = 2 };
const added4 = distance4.add(distance).to(KiloMeter_f);
try std.testing.expectApproxEqAbs(2.01, added4.value, 0.000001);
try std.testing.expectEqual(1, @TypeOf(added4).dims.get(.L));
}
test "Sub" {
const Meter = Scalar(i128, .{ .L = 1 }, .{});
const KiloMeter_f = Scalar(f64, .{ .L = 1 }, .{ .L = .k });
const a = Meter{ .value = 500 };
const b = Meter{ .value = 200 };
const diff = a.sub(b);
try std.testing.expectEqual(300, diff.value);
const diff2 = b.sub(a);
try std.testing.expectEqual(-300, diff2.value);
const km_f = KiloMeter_f{ .value = 2.5 };
const m_f = Meter{ .value = 500 };
const diff3 = km_f.sub(m_f);
try std.testing.expectApproxEqAbs(2000, diff3.value, 1e-4);
}
test "MulBy" {
const Meter = Scalar(i128, .{ .L = 1 }, .{});
const Second = Scalar(f32, .{ .T = 1 }, .{});
const d = Meter{ .value = 3.0 };
const t = Second{ .value = 4.0 };
const area_time = d.mul(t);
try std.testing.expectEqual(12, area_time.value);
try std.testing.expectEqual(1, @TypeOf(area_time).dims.get(.L));
try std.testing.expectEqual(1, @TypeOf(area_time).dims.get(.T));
const d2 = Meter{ .value = 5.0 };
const area = d.mul(d2);
try std.testing.expectEqual(15, area.value);
try std.testing.expectEqual(2, @TypeOf(area).dims.get(.L));
try std.testing.expectEqual(0, @TypeOf(area).dims.get(.T));
}
test "MulBy with scale" {
const KiloMeter = Scalar(f32, .{ .L = 1 }, .{ .L = .k });
const KiloGram = Scalar(f32, .{ .M = 1 }, .{ .M = .k });
const dist = KiloMeter{ .value = 2.0 };
const mass = KiloGram{ .value = 3.0 };
const prod = dist.mul(mass);
try std.testing.expectEqual(1, @TypeOf(prod).dims.get(.L));
try std.testing.expectEqual(1, @TypeOf(prod).dims.get(.M));
}
test "MulBy with type change" {
const Meter = Scalar(i128, .{ .L = 1 }, .{ .L = .k });
const Second = Scalar(f64, .{ .T = 1 }, .{});
const KmSec = Scalar(i64, .{ .L = 1, .T = 1 }, .{ .L = .k });
const KmSec_f = Scalar(f32, .{ .L = 1, .T = 1 }, .{ .L = .k });
const d = Meter{ .value = 3.0 };
const t = Second{ .value = 4.0 };
const area_time = d.mul(t).to(KmSec);
const area_time_f = d.mul(t).to(KmSec_f);
try std.testing.expectEqual(12, area_time.value);
try std.testing.expectApproxEqAbs(12, area_time_f.value, 0.0001);
try std.testing.expectEqual(1, @TypeOf(area_time).dims.get(.L));
try std.testing.expectEqual(1, @TypeOf(area_time).dims.get(.T));
}
test "MulBy small" {
const Meter = Scalar(i128, .{ .L = 1 }, .{ .L = .n });
const Second = Scalar(f32, .{ .T = 1 }, .{});
const d = Meter{ .value = 3.0 };
const t = Second{ .value = 4.0 };
const area_time = d.mul(t);
try std.testing.expectEqual(12, area_time.value);
try std.testing.expectEqual(1, @TypeOf(area_time).dims.get(.L));
try std.testing.expectEqual(1, @TypeOf(area_time).dims.get(.T));
}
test "MulBy dimensionless" {
const DimLess = Scalar(i128, .{}, .{});
const Meter = Scalar(i128, .{ .L = 1 }, .{});
const d = Meter{ .value = 7 };
const scaled = d.mul(DimLess{ .value = 3 });
try std.testing.expectEqual(21, scaled.value);
try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L));
}
test "Sqrt" {
const MeterSquare = Scalar(i128, .{ .L = 2 }, .{});
var d = MeterSquare{ .value = 9 };
var scaled = d.sqrt();
try std.testing.expectEqual(3, scaled.value);
try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L));
d = MeterSquare{ .value = -5 };
scaled = d.sqrt();
try std.testing.expectEqual(0, scaled.value);
try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L));
const MeterSquare_f = Scalar(f64, .{ .L = 2 }, .{});
const d2 = MeterSquare_f{ .value = 20 };
const scaled2 = d2.sqrt();
try std.testing.expectApproxEqAbs(4.472135955, scaled2.value, 1e-4);
try std.testing.expectEqual(1, @TypeOf(scaled2).dims.get(.L));
}
test "Chained: velocity and acceleration" {
const Meter = Scalar(i128, .{ .L = 1 }, .{});
const Second = Scalar(f32, .{ .T = 1 }, .{});
const dist = Meter{ .value = 100.0 };
const t1 = Second{ .value = 5.0 };
const velocity = dist.div(t1);
try std.testing.expectEqual(20, velocity.value);
try std.testing.expectEqual(1, @TypeOf(velocity).dims.get(.L));
try std.testing.expectEqual(-1, @TypeOf(velocity).dims.get(.T));
const t2 = Second{ .value = 4.0 };
const accel = velocity.div(t2);
try std.testing.expectEqual(5, accel.value);
try std.testing.expectEqual(1, @TypeOf(accel).dims.get(.L));
try std.testing.expectEqual(-2, @TypeOf(accel).dims.get(.T));
}
test "DivBy integer exact" {
const Meter = Scalar(i128, .{ .L = 1 }, .{});
const Second = Scalar(f32, .{ .T = 1 }, .{});
const dist = Meter{ .value = 120 };
const time = Second{ .value = 4 };
const vel = dist.div(time);
try std.testing.expectEqual(30, vel.value);
try std.testing.expectEqual(1, @TypeOf(vel).dims.get(.L));
try std.testing.expectEqual(-1, @TypeOf(vel).dims.get(.T));
}
test "Finer scales skip dim 0" {
const Dimless = Scalar(i128, .{}, .{});
const KiloMetre = Scalar(i128, .{ .L = 1 }, .{ .L = .k });
const r = Dimless{ .value = 30 };
const time = KiloMetre{ .value = 4 };
const vel = r.mul(time);
try std.testing.expectEqual(120, vel.value);
try std.testing.expectEqual(Scales.UnitScale.k, @TypeOf(vel).scales.get(.L));
}
test "Conversion chain: km -> m -> cm" {
const KiloMeter = Scalar(i128, .{ .L = 1 }, .{ .L = .k });
const Meter = Scalar(i128, .{ .L = 1 }, .{});
const CentiMeter = Scalar(i128, .{ .L = 1 }, .{ .L = .c });
const km = KiloMeter{ .value = 15 };
const m = km.to(Meter);
const cm = m.to(CentiMeter);
try std.testing.expectEqual(15_000, m.value);
try std.testing.expectEqual(1_500_000, cm.value);
}
test "Conversion: hours -> minutes -> seconds" {
const Hour = Scalar(i128, .{ .T = 1 }, .{ .T = .hour });
const Minute = Scalar(i128, .{ .T = 1 }, .{ .T = .min });
const Second = Scalar(i128, .{ .T = 1 }, .{});
const h = Hour{ .value = 1.0 };
const min = h.to(Minute);
const sec = min.to(Second);
try std.testing.expectEqual(60, min.value);
try std.testing.expectEqual(3600, sec.value);
}
test "Negative values" {
const Meter = Scalar(i128, .{ .L = 1 }, .{});
const a = Meter{ .value = 5 };
const b = Meter{ .value = 20 };
const diff = a.sub(b);
try std.testing.expectEqual(-15, diff.value);
}
test "Format Scalar" {
const MeterPerSecondSq = Scalar(f32, .{ .L = 1, .T = -2 }, .{ .T = .n });
const KgMeterPerSecond = Scalar(f32, .{ .M = 1, .L = 1, .T = -1 }, .{ .M = .k });
const Meter = Scalar(f32, .{ .L = 1 }, .{});
const m = Meter{ .value = 1.23456 };
const accel = MeterPerSecondSq{ .value = 9.81 };
const momentum = KgMeterPerSecond{ .value = 42.0 };
var buf: [64]u8 = undefined;
var res = try std.fmt.bufPrint(&buf, "{d:.2}", .{m});
try std.testing.expectEqualStrings("1.23m", res);
res = try std.fmt.bufPrint(&buf, "{d}", .{accel});
try std.testing.expectEqualStrings("9.81m.ns⁻²", res);
res = try std.fmt.bufPrint(&buf, "{d}", .{momentum});
try std.testing.expectEqualStrings("42m.kg.s⁻¹", res);
res = try std.fmt.bufPrint(&buf, "{d:_>10.1}", .{m});
try std.testing.expectEqualStrings("_______1.2m", res);
}
test "Abs" {
const Meter = Scalar(i128, .{ .L = 1 }, .{});
const m1 = Meter{ .value = -50 };
const m2 = m1.abs();
try std.testing.expectEqual(50, m2.value);
try std.testing.expectEqual(1, @TypeOf(m2).dims.get(.L));
const m_float = Scalar(f32, .{ .L = 1 }, .{});
const m3 = m_float{ .value = -42.5 };
try std.testing.expectEqual(42.5, m3.abs().value);
}
test "Pow" {
const Meter = Scalar(i128, .{ .L = 1 }, .{});
const d = Meter{ .value = 4 };
const area = d.pow(2);
try std.testing.expectEqual(16, area.value);
try std.testing.expectEqual(2, @TypeOf(area).dims.get(.L));
const volume = d.pow(3);
try std.testing.expectEqual(64, volume.value);
try std.testing.expectEqual(3, @TypeOf(volume).dims.get(.L));
// Float test
const MeterF = Scalar(f32, .{ .L = 1 }, .{});
const d_f = MeterF{ .value = 2.0 };
const area_f = d_f.pow(3);
try std.testing.expectEqual(8.0, area_f.value);
try std.testing.expectEqual(3, @TypeOf(area_f).dims.get(.L));
}
test "mul comptime_int" {
const Meter = Scalar(i128, .{ .L = 1 }, .{});
const d = Meter{ .value = 7 };
const scaled = d.mul(3); // comptime_int → dimensionless
try std.testing.expectEqual(21, scaled.value);
try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L));
try std.testing.expectEqual(0, @TypeOf(scaled).dims.get(.T));
}
test "mul comptime_float" {
const MeterF = Scalar(f64, .{ .L = 1 }, .{});
const d = MeterF{ .value = 4.0 };
const scaled = d.mul(2.5); // comptime_float → dimensionless
try std.testing.expectApproxEqAbs(10.0, scaled.value, 1e-9);
try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L));
}
test "mul T (value type)" {
const MeterF = Scalar(f32, .{ .L = 1 }, .{});
const d = MeterF{ .value = 6.0 };
const factor: f32 = 0.5;
const scaled = d.mul(factor); // bare f32 → dimensionless
try std.testing.expectApproxEqAbs(3.0, scaled.value, 1e-6);
try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L));
}
test "div comptime_int" {
const Meter = Scalar(i128, .{ .L = 1 }, .{});
const d = Meter{ .value = 100 };
const half = d.div(4); // comptime_int → dimensionless divisor
try std.testing.expectEqual(25, half.value);
try std.testing.expectEqual(1, @TypeOf(half).dims.get(.L));
}
test "div comptime_float" {
const MeterF = Scalar(f64, .{ .L = 1 }, .{});
const d = MeterF{ .value = 9.0 };
const r = d.div(3.0);
try std.testing.expectApproxEqAbs(3.0, r.value, 1e-9);
try std.testing.expectEqual(1, @TypeOf(r).dims.get(.L));
}
test "add/sub bare number on dimensionless scalar" {
// Bare numbers are dimensionless, so add/sub only works when Self is also dimensionless.
const DimLess = Scalar(i128, .{}, .{});
const a = DimLess{ .value = 10 };
const b = a.add(5); // comptime_int, both dimensionless → ok
try std.testing.expectEqual(15, b.value);
const c = a.sub(3);
try std.testing.expectEqual(7, c.value);
}
test "Imperial length scales" {
const Foot = Scalar(f64, .{ .L = 1 }, .{ .L = .ft });
const Meter = Scalar(f64, .{ .L = 1 }, .{});
const Inch = Scalar(f64, .{ .L = 1 }, .{ .L = .inch });
const CentiMeter = Scalar(f64, .{ .L = 1 }, .{ .L = .c });
const Mile = Scalar(f64, .{ .L = 1 }, .{ .L = .mi });
const KiloMeter = Scalar(f64, .{ .L = 1 }, .{ .L = .k });
const Yard = Scalar(f64, .{ .L = 1 }, .{ .L = .yd });
// 1 ft → 0.3048 m
const one_ft = Foot{ .value = 1.0 };
try std.testing.expectApproxEqAbs(0.3048, one_ft.to(Meter).value, 1e-9);
// 12 in → 1 ft
const twelve_in = Inch{ .value = 12.0 };
try std.testing.expectApproxEqAbs(1.0, twelve_in.to(Foot).value, 1e-9);
// 1 in → 2.54 cm
const one_in = Inch{ .value = 1.0 };
try std.testing.expectApproxEqAbs(2.54, one_in.to(CentiMeter).value, 1e-9);
// 1 mi → 1.609344 km
const one_mi = Mile{ .value = 1.0 };
try std.testing.expectApproxEqAbs(1.609344, one_mi.to(KiloMeter).value, 1e-9);
// 3 ft → 1 yd
const three_ft = Foot{ .value = 3.0 };
try std.testing.expectApproxEqAbs(1.0, three_ft.to(Yard).value, 1e-9);
}
test "Imperial mass scales" {
const Pound = Scalar(f64, .{ .M = 1 }, .{ .M = .lb });
const KiloGram = Scalar(f64, .{ .M = 1 }, .{ .M = .k });
const Ounce = Scalar(f64, .{ .M = 1 }, .{ .M = .oz });
const Stone = Scalar(f64, .{ .M = 1 }, .{ .M = .st });
// 1 lb → ~0.453592 kg
const one_lb = Pound{ .value = 1.0 };
try std.testing.expectApproxEqAbs(0.45359237, one_lb.to(KiloGram).value, 1e-6);
// 16 oz → 1 lb
const sixteen_oz = Ounce{ .value = 16.0 };
try std.testing.expectApproxEqAbs(1.0, sixteen_oz.to(Pound).value, 1e-6);
// 1 stone → 14 lb
const one_st = Stone{ .value = 1.0 };
try std.testing.expectApproxEqAbs(14.0, one_st.to(Pound).value, 1e-4);
// 2 lb + 8 oz → 2.5 lb
const two_lb = Pound{ .value = 2.0 };
const eight_oz = Ounce{ .value = 8.0 };
const total = two_lb.add(eight_oz).to(Pound);
try std.testing.expectApproxEqAbs(2.5, total.value, 1e-6);
}
test "comparisons with comptime_int on dimensionless scalar" {
const DimLess = Scalar(i128, .{}, .{});
const x = DimLess{ .value = 42 };
try std.testing.expect(x.eq(42));
try std.testing.expect(x.ne(0));
try std.testing.expect(x.gt(10));
try std.testing.expect(x.gte(42));
try std.testing.expect(x.lt(100));
try std.testing.expect(x.lte(42));
}
+35 -29
View File
@@ -1,5 +1,4 @@
const std = @import("std");
const hlp = @import("helper.zig");
const Dimensions = @import("Dimensions.zig");
const Dimension = @import("Dimensions.zig").Dimension;
@@ -56,35 +55,36 @@ pub const UnitScale = enum(isize) {
// Undefined
_,
pub inline fn str(self: @This()) []const u8 {
pub fn str(self: @This()) []const u8 {
var buf: [16]u8 = undefined;
return switch (self) {
inline .none => "",
inline .P, .T, .G, .M, .k, .h, .da, .d, .c, .m, .u, .n, .p, .f, .min, .hour, .year, .inch, .ft, .yd, .mi, .oz, .lb, .st => @tagName(self),
.none => "",
.P, .T, .G, .M, .k, .h, .da, .d, .c, .m, .u, .n, .p, .f, .min, .year, .inch, .ft, .yd, .mi, .oz, .lb, .st => @tagName(self),
.hour => "h",
else => std.fmt.bufPrint(&buf, "[{d}]", .{@intFromEnum(self)}) catch "[]", // This cannot be inline because of non exhaustive enum, but that's ok, it is just str, not calculation
};
}
pub inline fn getFactor(self: @This()) comptime_float {
return comptime switch (self) {
pub fn getFactor(self: @This()) comptime_float {
return switch (self) {
// Standard SI Exponents
inline .P, .T, .G, .M, .k, .h, .da, .none, .d, .c, .m, .u, .n, .p, .f => std.math.pow(f64, 10.0, @floatFromInt(@intFromEnum(self))),
.P, .T, .G, .M, .k, .h, .da, .none, .d, .c, .m, .u, .n, .p, .f => std.math.pow(f64, 10.0, @floatFromInt(@intFromEnum(self))),
// Time Factors
inline .min, .hour, .year => @floatFromInt(@intFromEnum(self)),
.min, .hour, .year => @floatFromInt(@intFromEnum(self)),
// Imperial Length (metres)
inline .inch => 0.0254,
inline .ft => 0.3048,
inline .yd => 0.9144,
inline .mi => 1609.344,
.inch => 0.0254,
.ft => 0.3048,
.yd => 0.9144,
.mi => 1609.344,
// Imperial Mass (grams — base unit for M is gram, i.e. .none = 1 g)
inline .oz => 28.3495231,
inline .lb => 453.59237,
inline .st => 6350.29318,
.oz => 28.3495231,
.lb => 453.59237,
.st => 6350.29318,
inline else => @floatFromInt(@intFromEnum(self)),
else => @floatFromInt(@intFromEnum(self)),
};
}
};
@@ -98,40 +98,46 @@ data: std.EnumArray(Dimension, UnitScale),
/// Unspecified dimensions default to `.none` (factor 1).
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))))
for (std.meta.fields(@TypeOf(init_val))) |f| {
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));
}
return s;
return comptime s;
}
pub fn initFill(comptime val: UnitScale) Self {
return comptime .{ .data = std.EnumArray(Dimension, UnitScale).initFill(val) };
pub fn initFill(val: UnitScale) Self {
return .{ .data = std.EnumArray(Dimension, UnitScale).initFill(val) };
}
pub fn get(comptime self: Self, comptime key: Dimension) UnitScale {
return comptime self.data.get(key);
pub fn get(self: Self, key: Dimension) UnitScale {
return self.data.get(key);
}
pub fn set(comptime self: *Self, comptime key: Dimension, comptime val: UnitScale) void {
comptime self.data.set(key, val);
pub fn set(self: *Self, key: Dimension, val: UnitScale) void {
self.data.set(key, val);
}
pub fn eql(self: Self, other: Self) bool {
for (self.data.values, other.data.values) |l, r|
if (l != r) return false;
return true;
}
pub fn argsOpt(self: Self) ArgOpts {
var args: ArgOpts = undefined;
inline for (std.enums.values(Dimension)) |d|
for (std.enums.values(Dimension)) |d|
@field(args, @tagName(d)) = self.get(d);
return args;
}
/// Compute the combined scale factor for a given dimension signature.
/// Each dimension's prefix is raised to its exponent and multiplied together.
pub inline fn getFactor(comptime s: Self, comptime d: Dimensions) comptime_float {
pub fn getFactor(s: Self, d: Dimensions) comptime_float {
var factor: f64 = 1.0;
for (std.enums.values(Dimension)) |dim| {
const power = comptime d.get(dim);
const power = d.get(dim);
if (power == 0) continue;
const base = s.get(dim).getFactor();
@@ -145,5 +151,5 @@ pub inline fn getFactor(comptime s: Self, comptime d: Dimensions) comptime_float
factor /= base;
}
}
return comptime factor;
return factor;
}
+1745
View File
File diff suppressed because it is too large Load Diff
+1775
View File
File diff suppressed because it is too large Load Diff
+1421
View File
File diff suppressed because it is too large Load Diff
+145
View File
@@ -0,0 +1,145 @@
const std = @import("std");
const Dimensions = @import("Dimensions.zig");
const Scales = @import("Scales.zig");
/// A container returning the separated arguments needed to construct a Tensor.
pub const ParsedUnit = struct {
dims: Dimensions.ArgOpts = .{},
scales: Scales.ArgOpts = .{},
};
pub const UnitParseError = error{
UnknownBaseUnit,
UnknownPrefix,
InvalidExponent,
EmptyStr,
};
/// Parses strings like "km/s^2", "m", "kg*m/s^2", "1/min".
/// Evaluates entirely at comptime.
pub fn parseUnit(str: []const u8) !ParsedUnit {
if (str.len == 0) return UnitParseError.EmptyStr;
var parsed: ParsedUnit = .{ .dims = .{}, .scales = .{} };
// We need to track if we are after a '/' to flip exponents to negative
var is_denominator = false;
// Manual iteration to handle '/' properly
var cursor: usize = 0;
while (cursor < str.len) {
// Find the next segment
const segment_start = cursor;
while (cursor < str.len and str[cursor] != '/' and str[cursor] != '.' and str[cursor] != '*') : (cursor += 1) {}
const segment = str[segment_start..cursor];
if (segment.len > 0) {
try parseSegment(segment, &parsed, is_denominator);
}
if (cursor < str.len) {
if (str[cursor] == '/') {
is_denominator = true;
}
cursor += 1; // skip the separator
}
}
return parsed;
}
fn parseSegment(segment: []const u8, parsed: *ParsedUnit, is_denominator: bool) !void {
var scale: Scales.UnitScale = .none;
var found_scale = false;
var active_dim: ?Dimensions.Dimension = null;
// 1. Try to find a Scale + Dimension pair (e.g., "mm", "km")
inline for (std.enums.values(Scales.UnitScale)) |sca| {
const s_str = sca.str();
if (s_str.len > 0 and std.mem.startsWith(u8, segment, s_str)) {
// Check if it's a "Unit-as-Scale" (hour, min) or a prefix (k, m, c)
switch (sca) {
.hour, .min, .year => {
// These are dimensions themselves (Time)
if (segment.len == s_str.len or (segment.len > s_str.len and (segment[s_str.len] == '^' or (segment[s_str.len] >= '0' and segment[s_str.len] <= '9')))) {
scale = sca;
active_dim = .T;
found_scale = true;
}
},
else => {
// Standard prefixes: Must be followed by a valid dimension unit
inline for (std.enums.values(Dimensions.Dimension)) |dim| {
if (std.mem.startsWith(u8, segment[s_str.len..], dim.unit())) {
scale = sca;
active_dim = dim;
found_scale = true;
break;
}
}
},
}
}
if (found_scale) break;
}
// 2. If no scale prefix was found, try identifying as a pure Dimension (e.g., "m", "s")
if (!found_scale) {
inline for (std.enums.values(Dimensions.Dimension)) |dim| {
if (std.mem.startsWith(u8, segment, dim.unit())) {
active_dim = dim;
break;
}
}
}
const dimen = active_dim orelse return UnitParseError.UnknownBaseUnit;
// 3. Determine where the exponent starts
// If it was a Time Scale (like 'h'), the exponent starts after 'h'
// If it was a Prefix + Dim (like 'km'), it starts after 'km'
const unit_part_len = if (found_scale)
(if (scale == .hour or scale == .min or scale == .year) scale.str().len else scale.str().len + dimen.unit().len)
else
dimen.unit().len;
const expo_str = segment[unit_part_len..];
// 4. Parse Exponent
var expo: i32 = 1;
if (expo_str.len > 0) {
const cleaned_expo = if (expo_str[0] == '^') expo_str[1..] else expo_str;
expo = std.fmt.parseInt(i32, cleaned_expo, 10) catch return UnitParseError.InvalidExponent;
}
if (is_denominator) expo *= -1;
// 5. Assign to struct
inline for (std.meta.fields(Dimensions.ArgOpts)) |f| {
if (std.mem.eql(u8, f.name, @tagName(dimen))) {
@field(parsed.dims, f.name) += expo;
@field(parsed.scales, f.name) = scale;
}
}
}
inline fn testParser(
comptime str: []const u8,
comptime expected_dims: Dimensions.ArgOpts,
comptime expected_scales: Scales.ArgOpts,
) !void {
const unit = comptime try parseUnit(str);
if (comptime !Dimensions.init(expected_dims).eql(Dimensions.init(unit.dims))) return error.WrongDims;
if (comptime !Scales.init(expected_scales).eql(Scales.init(unit.scales))) return error.WrongScales;
}
test "parseUnit" {
@setEvalBranchQuota(10000);
try testParser("m", .{ .L = 1 }, .{});
try testParser("s", .{ .T = 1 }, .{});
try testParser("mm", .{ .L = 1 }, .{ .L = .m });
try testParser("m/s", .{ .L = 1, .T = -1 }, .{});
try testParser("m1/s2/kg", .{ .L = 1, .T = -2, .M = -1 }, .{ .M = .k });
try testParser("km/h", .{ .L = 1, .T = -1 }, .{ .L = .k, .T = .hour });
try testParser("m.s^-1", .{ .L = 1, .T = -1 }, .{});
}
-831
View File
@@ -1,831 +0,0 @@
const std = @import("std");
const hlp = @import("helper.zig");
const Scalar = @import("Scalar.zig").Scalar;
const Scales = @import("Scales.zig");
const UnitScale = Scales.UnitScale;
const Dimensions = @import("Dimensions.zig");
const Dimension = Dimensions.Dimension;
/// A fixed-size array of `len` elements sharing the same dimension and scale as scalar type `Q`.
pub fn Vector(comptime len: usize, comptime Q: type) type {
const T = Q.ValueType;
return struct {
data: [len]T,
const Self = @This();
pub const ScalarType = Q;
pub const ValueType = T;
pub const dims: Dimensions = Q.dims;
pub const scales = Q.scales;
pub const zero = initDefault(0);
pub const one = initDefault(1);
pub fn initDefault(v: T) Self {
var data: [len]T = undefined;
inline for (&data) |*item| item.* = v;
return .{ .data = data };
}
// -------------------------------------------------------------------
// Internal: scalar-rhs normalisation (mirrors Scalar.zig)
// -------------------------------------------------------------------
/// Resolved Scalar type for a scalar operand (bare number or Scalar).
/// Passing another Vector here is a compile error.
inline fn ScalarRhsT(comptime Rhs: type) type {
if (comptime switch (@typeInfo(Rhs)) {
.@"struct", .@"enum", .@"union", .@"opaque" => @hasDecl(Rhs, "ScalarType"),
else => false,
})
@compileError(
"Expected a Scalar or bare number; got a Vector. " ++
"Use mul / div for element-wise vector operations.",
);
return hlp.rhsScalarType(T, Rhs);
}
/// Normalise a scalar rhs (bare number → dimensionless Scalar).
inline fn scalarRhs(r: anytype) ScalarRhsT(@TypeOf(r)) {
return hlp.toRhsScalar(T, r);
}
// -------------------------------------------------------------------
// Vector–Vector operations (rhs must be a Vector of the same length)
// -------------------------------------------------------------------
/// Element-wise addition. Dimensions must match; scales resolve to the finer of the two.
pub inline fn add(self: Self, rhs: anytype) Vector(len, Scalar(
T,
dims.argsOpt(),
hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(),
)) {
const Tr = @TypeOf(rhs);
var res: Vector(len, Scalar(
T,
dims.argsOpt(),
hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(),
)) = undefined;
inline for (self.data, 0..) |v, i| {
const q = (Q{ .value = v }).add(Tr.ScalarType{ .value = rhs.data[i] });
res.data[i] = q.value;
}
return res;
}
/// Element-wise subtraction. Dimensions must match; scales resolve to the finer of the two.
pub inline fn sub(self: Self, rhs: anytype) Vector(len, Scalar(
T,
dims.argsOpt(),
hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(),
)) {
const Tr = @TypeOf(rhs);
var res: Vector(len, Scalar(
T,
dims.argsOpt(),
hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(),
)) = undefined;
inline for (self.data, 0..) |v, i| {
const q = (Q{ .value = v }).sub(Tr.ScalarType{ .value = rhs.data[i] });
res.data[i] = q.value;
}
return res;
}
/// Element-wise division. Dimension exponents are subtracted per component.
pub inline fn div(
self: Self,
rhs: anytype,
) Vector(len, Scalar(
T,
dims.sub(@TypeOf(rhs).dims).argsOpt(),
hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(),
)) {
const Tr = @TypeOf(rhs);
var res: Vector(len, Scalar(
T,
dims.sub(Tr.dims).argsOpt(),
hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(),
)) = undefined;
inline for (self.data, 0..) |v, i| {
const q = (Q{ .value = v }).div(Tr.ScalarType{ .value = rhs.data[i] });
res.data[i] = q.value;
}
return res;
}
/// Element-wise multiplication. Dimension exponents are summed per component.
pub inline fn mul(
self: Self,
rhs: anytype,
) Vector(len, Scalar(
T,
dims.add(@TypeOf(rhs).dims).argsOpt(),
hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(),
)) {
const Tr = @TypeOf(rhs);
var res: Vector(len, Scalar(
T,
dims.add(Tr.dims).argsOpt(),
hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(),
)) = undefined;
inline for (self.data, 0..) |v, i| {
const q = (Q{ .value = v }).mul(Tr.ScalarType{ .value = rhs.data[i] });
res.data[i] = q.value;
}
return res;
}
// -------------------------------------------------------------------
// Vector–Scalar operations
// scalar may be: Scalar, T, comptime_int, comptime_float
// -------------------------------------------------------------------
/// Divide every component by a single scalar. Dimensions are subtracted.
/// `scalar` may be a Scalar, `T`, `comptime_int`, or `comptime_float`.
pub inline fn divScalar(
self: Self,
scalar: anytype,
) Vector(len, Scalar(
T,
dims.sub(ScalarRhsT(@TypeOf(scalar)).dims).argsOpt(),
hlp.finerScales(Self, ScalarRhsT(@TypeOf(scalar))).argsOpt(),
)) {
const s_norm = scalarRhs(scalar);
const SN = @TypeOf(s_norm);
var res: Vector(len, Scalar(
T,
dims.sub(SN.dims).argsOpt(),
hlp.finerScales(Self, SN).argsOpt(),
)) = undefined;
inline for (self.data, 0..) |v, i|
res.data[i] = (Q{ .value = v }).div(s_norm).value;
return res;
}
/// Multiply every component by a single scalar. Dimensions are summed.
/// `scalar` may be a Scalar, `T`, `comptime_int`, or `comptime_float`.
pub inline fn mulScalar(
self: Self,
scalar: anytype,
) Vector(len, Scalar(
T,
dims.add(ScalarRhsT(@TypeOf(scalar)).dims).argsOpt(),
hlp.finerScales(Self, ScalarRhsT(@TypeOf(scalar))).argsOpt(),
)) {
const s_norm = scalarRhs(scalar);
const SN = @TypeOf(s_norm);
var res: Vector(len, Scalar(
T,
dims.add(SN.dims).argsOpt(),
hlp.finerScales(Self, SN).argsOpt(),
)) = undefined;
inline for (self.data, 0..) |v, i|
res.data[i] = (Q{ .value = v }).mul(s_norm).value;
return res;
}
// -------------------------------------------------------------------
// Dot / Cross
// -------------------------------------------------------------------
/// Standard dot product. Dimensions are summed (e.g., Force * Distance = Energy).
/// Returns a Scalar type with the combined dimensions and finest scale.
pub inline fn dot(self: Self, rhs: anytype) Scalar(
T,
dims.add(@TypeOf(rhs).dims).argsOpt(),
hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(),
) {
const Tr = @TypeOf(rhs);
var sum: T = 0;
inline for (self.data, 0..) |v, i| {
const q_lhs = Q{ .value = v };
const q_rhs = Tr.ScalarType{ .value = rhs.data[i] };
sum += q_lhs.mul(q_rhs).value;
}
return .{ .value = sum };
}
/// 3D Cross product. Dimensions are summed.
/// Only valid for vectors of length 3.
pub inline fn cross(self: Self, rhs: anytype) Vector(3, Scalar(
T,
dims.add(@TypeOf(rhs).dims).argsOpt(),
hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(),
)) {
if (comptime len != 3)
@compileError("Cross product is only defined for Vector(3, ...)");
const Tr = @TypeOf(rhs);
const ResScalar = Scalar(T, dims.add(Tr.dims).argsOpt(), hlp.finerScales(Self, Tr).argsOpt());
const ResVec = Vector(3, ResScalar);
// Calculation: [y1*z2 - z1*y2, z1*x2 - x1*z2, x1*y2 - y1*x2]
const s1 = Q{ .value = self.data[0] };
const s2 = Q{ .value = self.data[1] };
const s3 = Q{ .value = self.data[2] };
const o1 = Tr.ScalarType{ .value = rhs.data[0] };
const o2 = Tr.ScalarType{ .value = rhs.data[1] };
const o3 = Tr.ScalarType{ .value = rhs.data[2] };
return ResVec{
.data = .{
s2.mul(o3).sub(s3.mul(o2)).value,
s3.mul(o1).sub(s1.mul(o3)).value,
s1.mul(o2).sub(s2.mul(o1)).value,
},
};
}
// -------------------------------------------------------------------
// Unary
// -------------------------------------------------------------------
/// Returns a vector where each component is the absolute value of the original.
pub inline fn abs(self: Self) Self {
var res: Self = undefined;
inline for (self.data, 0..) |v, i| {
const q = Q{ .value = v };
res.data[i] = q.abs().value;
}
return res;
}
/// Returns a vector where each component is the absolute value of the original.
pub inline fn sqrt(self: Self) Self {
var res: Self = undefined;
inline for (self.data, 0..) |v, i| {
const q = Q{ .value = v };
res.data[i] = q.sqrt().value;
}
return res;
}
/// Multiplies all components of the vector together.
/// Resulting dimensions are (Original Dims * len).
pub inline fn product(self: Self) Scalar(
T,
dims.scale(len).argsOpt(),
scales.argsOpt(),
) {
var res_val: T = 1;
if (comptime hlp.isInt(T)) {
inline for (self.data) |v|
res_val = res_val *| v;
} else inline for (self.data) |v|
res_val *= v;
return .{ .value = res_val };
}
/// Raises every component to a compile-time integer power.
/// Dimensions are scaled by the exponent.
pub inline fn pow(self: Self, comptime exp: comptime_int) Vector(
len,
Scalar(
T,
dims.scale(exp).argsOpt(),
scales.argsOpt(),
),
) {
const ResScalar = Scalar(T, dims.scale(exp).argsOpt(), scales.argsOpt());
var res: Vector(len, ResScalar) = undefined;
inline for (self.data, 0..) |v, i| {
const q = Q{ .value = v };
res.data[i] = q.pow(exp).value;
}
return res;
}
/// Negate all components. Dimensions are preserved.
pub fn negate(self: Self) Self {
var res: Self = undefined;
inline for (self.data, 0..) |v, i|
res.data[i] = -v;
return res;
}
// -------------------------------------------------------------------
// Conversion
// -------------------------------------------------------------------
/// Convert all components to a compatible scalar type. Compile error on dimension mismatch.
pub inline fn to(self: Self, comptime DestQ: type) Vector(len, DestQ) {
var res: Vector(len, DestQ) = undefined;
inline for (self.data, 0..) |v, i|
res.data[i] = (Q{ .value = v }).to(DestQ).value;
return res;
}
// -------------------------------------------------------------------
// Length
// -------------------------------------------------------------------
/// Sum of squared components. Cheaper than `length` — use for comparisons.
pub inline fn lengthSqr(self: Self) T {
var sum: T = 0;
inline for (self.data) |v|
sum += v * v;
return sum;
}
/// Euclidean length. Integer types use integer sqrt (truncated).
pub inline fn length(self: Self) T {
const len_sq = self.lengthSqr();
if (comptime @typeInfo(T) == .int) {
const UnsignedT = @Int(.unsigned, @typeInfo(T).int.bits);
const u_len_sq = @as(UnsignedT, @intCast(len_sq));
return @as(T, @intCast(std.math.sqrt(u_len_sq)));
} else {
return @sqrt(len_sq);
}
}
// -------------------------------------------------------------------
// Vector–Vector comparisons
// -------------------------------------------------------------------
/// Returns true only if all components are equal after scale resolution.
pub inline fn eqAll(self: Self, rhs: anytype) bool {
const Tr = @TypeOf(rhs);
if (comptime !dims.eql(Tr.dims))
@compileError("Dimension mismatch in eq: " ++ dims.str() ++ " vs " ++ Tr.dims.str());
inline for (self.data, 0..) |v, i| {
const lhs_q = Q{ .value = v };
const rhs_q = Tr.ScalarType{ .value = rhs.data[i] };
if (!lhs_q.eq(rhs_q)) return false;
}
return true;
}
/// Returns true if any component differs after scale resolution.
pub inline fn neAll(self: Self, rhs: anytype) bool {
return !self.eqAll(rhs);
}
/// Element-wise "Equal". Returns an array of booleans.
pub inline fn eq(self: Self, rhs: anytype) [len]bool {
const Tr = @TypeOf(rhs);
var res: [len]bool = undefined;
inline for (self.data, 0..) |v, i|
res[i] = (Q{ .value = v }).eq(Tr.ScalarType{ .value = rhs.data[i] });
return res;
}
/// Element-wise "Not Equal". Returns an array of booleans.
pub inline fn ne(self: Self, rhs: anytype) [len]bool {
const Tr = @TypeOf(rhs);
var res: [len]bool = undefined;
inline for (self.data, 0..) |v, i|
res[i] = (Q{ .value = v }).ne(Tr.ScalarType{ .value = rhs.data[i] });
return res;
}
/// Element-wise "Greater Than". Returns an array of booleans.
pub inline fn gt(self: Self, rhs: anytype) [len]bool {
const Tr = @TypeOf(rhs);
var res: [len]bool = undefined;
inline for (self.data, 0..) |v, i|
res[i] = (Q{ .value = v }).gt(Tr.ScalarType{ .value = rhs.data[i] });
return res;
}
/// Element-wise "Greater Than or Equal". Returns an array of booleans.
pub inline fn gte(self: Self, rhs: anytype) [len]bool {
const Tr = @TypeOf(rhs);
var res: [len]bool = undefined;
inline for (self.data, 0..) |v, i|
res[i] = (Q{ .value = v }).gte(Tr.ScalarType{ .value = rhs.data[i] });
return res;
}
/// Element-wise "Less Than". Returns an array of booleans.
pub inline fn lt(self: Self, rhs: anytype) [len]bool {
const Tr = @TypeOf(rhs);
var res: [len]bool = undefined;
inline for (self.data, 0..) |v, i|
res[i] = (Q{ .value = v }).lt(Tr.ScalarType{ .value = rhs.data[i] });
return res;
}
/// Element-wise "Less Than or Equal". Returns an array of booleans.
pub inline fn lte(self: Self, rhs: anytype) [len]bool {
const Tr = @TypeOf(rhs);
var res: [len]bool = undefined;
inline for (self.data, 0..) |v, i|
res[i] = (Q{ .value = v }).lte(Tr.ScalarType{ .value = rhs.data[i] });
return res;
}
// -------------------------------------------------------------------
// Vector–Scalar comparisons
// scalar may be: Scalar, T, comptime_int, comptime_float
// -------------------------------------------------------------------
/// Compares every element in the vector to a single scalar for equality.
/// Returns an array of booleans [len]bool. Dimensions must match; scales are auto-resolved.
pub inline fn eqScalar(self: Self, scalar: anytype) [len]bool {
var res: [len]bool = undefined;
inline for (self.data, 0..) |v, i|
res[i] = (Q{ .value = v }).eq(scalar);
return res;
}
/// Compares every element in the vector to a single scalar for inequality.
/// Returns an array of booleans [len]bool. Dimensions must match; scales are auto-resolved.
pub inline fn neScalar(self: Self, scalar: anytype) [len]bool {
var res: [len]bool = undefined;
inline for (self.data, 0..) |v, i|
res[i] = (Q{ .value = v }).ne(scalar);
return res;
}
/// Checks if each element in the vector is strictly greater than the given scalar.
/// Returns an array of booleans [len]bool.
pub inline fn gtScalar(self: Self, scalar: anytype) [len]bool {
var res: [len]bool = undefined;
inline for (self.data, 0..) |v, i|
res[i] = (Q{ .value = v }).gt(scalar);
return res;
}
/// Checks if each element in the vector is greater than or equal to the given scalar.
/// Returns an array of booleans [len]bool.
pub inline fn gteScalar(self: Self, scalar: anytype) [len]bool {
var res: [len]bool = undefined;
inline for (self.data, 0..) |v, i|
res[i] = (Q{ .value = v }).gte(scalar);
return res;
}
/// Checks if each element in the vector is strictly less than the given scalar.
/// Returns an array of booleans [len]bool.
pub inline fn ltScalar(self: Self, scalar: anytype) [len]bool {
var res: [len]bool = undefined;
inline for (self.data, 0..) |v, i|
res[i] = (Q{ .value = v }).lt(scalar);
return res;
}
/// Checks if each element in the vector is less than or equal to the given scalar.
/// Returns an array of booleans [len]bool.
pub inline fn lteScalar(self: Self, scalar: anytype) [len]bool {
var res: [len]bool = undefined;
inline for (self.data, 0..) |v, i|
res[i] = (Q{ .value = v }).lte(scalar);
return res;
}
// -------------------------------------------------------------------
// Formatting
// -------------------------------------------------------------------
pub fn formatNumber(
self: Self,
writer: *std.Io.Writer,
options: std.fmt.Number,
) !void {
try writer.writeAll("(");
for (self.data, 0..) |v, i| {
if (i > 0) try writer.writeAll(", ");
switch (@typeInfo(T)) {
.float, .comptime_float => try writer.printFloat(v, options),
.int, .comptime_int => try writer.printInt(v, 10, .lower, .{
.width = options.width,
.alignment = options.alignment,
.fill = options.fill,
.precision = options.precision,
}),
else => unreachable,
}
}
try writer.writeAll(")");
var first = true;
inline for (std.enums.values(Dimension)) |bu| {
const v = dims.get(bu);
if (comptime v == 0) continue;
if (!first)
try writer.writeAll(".");
first = false;
const uscale = scales.get(bu);
if (bu == .T and (uscale == .min or uscale == .hour or uscale == .year))
try writer.print("{s}", .{uscale.str()})
else
try writer.print("{s}{s}", .{ uscale.str(), bu.unit() });
if (v != 1)
try hlp.printSuperscript(writer, v);
}
}
};
}
test "Format VectorX" {
const MeterPerSecondSq = Scalar(f32, .{ .L = 1, .T = -2 }, .{ .T = .n });
const KgMeterPerSecond = Scalar(f32, .{ .M = 1, .L = 1, .T = -1 }, .{ .M = .k });
const accel = MeterPerSecondSq.Vec3.initDefault(9.81);
const momentum = KgMeterPerSecond.Vec3{ .data = .{ 43, 0, 11 } };
var buf: [64]u8 = undefined;
var res = try std.fmt.bufPrint(&buf, "{d}", .{accel});
try std.testing.expectEqualStrings("(9.81, 9.81, 9.81)m.ns⁻²", res);
res = try std.fmt.bufPrint(&buf, "{d:.2}", .{momentum});
try std.testing.expectEqualStrings("(43.00, 0.00, 11.00)m.kg.s⁻¹", res);
}
test "VecX Init and Basic Arithmetic" {
const Meter = Scalar(i32, .{ .L = 1 }, .{});
const Vec3M = Meter.Vec3;
// Test zero, one, initDefault
const v_zero = Vec3M.zero;
try std.testing.expectEqual(0, v_zero.data[0]);
try std.testing.expectEqual(0, v_zero.data[1]);
try std.testing.expectEqual(0, v_zero.data[2]);
const v_one = Vec3M.one;
try std.testing.expectEqual(1, v_one.data[0]);
try std.testing.expectEqual(1, v_one.data[1]);
try std.testing.expectEqual(1, v_one.data[2]);
const v_def = Vec3M.initDefault(5);
try std.testing.expectEqual(5, v_def.data[0]);
try std.testing.expectEqual(5, v_def.data[1]);
try std.testing.expectEqual(5, v_def.data[2]);
// Test add and sub
const v1 = Vec3M{ .data = .{ 10, 20, 30 } };
const v2 = Vec3M{ .data = .{ 2, 4, 6 } };
const added = v1.add(v2);
try std.testing.expectEqual(12, added.data[0]);
try std.testing.expectEqual(24, added.data[1]);
try std.testing.expectEqual(36, added.data[2]);
const subbed = v1.sub(v2);
try std.testing.expectEqual(8, subbed.data[0]);
try std.testing.expectEqual(16, subbed.data[1]);
try std.testing.expectEqual(24, subbed.data[2]);
// Test negate
const neg = v1.negate();
try std.testing.expectEqual(-10, neg.data[0]);
try std.testing.expectEqual(-20, neg.data[1]);
try std.testing.expectEqual(-30, neg.data[2]);
}
test "VecX Kinematics (Scalar Mul/Div)" {
const Meter = Scalar(i32, .{ .L = 1 }, .{});
const Second = Scalar(i32, .{ .T = 1 }, .{});
const Vec3M = Meter.Vec3;
const pos = Vec3M{ .data = .{ 100, 200, 300 } };
const time = Second{ .value = 10 };
// Vector divided by scalar (Velocity = Position / Time)
const vel = pos.divScalar(time);
try std.testing.expectEqual(10, vel.data[0]);
try std.testing.expectEqual(20, vel.data[1]);
try std.testing.expectEqual(30, vel.data[2]);
try std.testing.expectEqual(1, @TypeOf(vel).dims.get(.L));
try std.testing.expectEqual(-1, @TypeOf(vel).dims.get(.T));
// Vector multiplied by scalar (Position = Velocity * Time)
const new_pos = vel.mulScalar(time);
try std.testing.expectEqual(100, new_pos.data[0]);
try std.testing.expectEqual(200, new_pos.data[1]);
try std.testing.expectEqual(300, new_pos.data[2]);
try std.testing.expectEqual(1, @TypeOf(new_pos).dims.get(.L));
try std.testing.expectEqual(0, @TypeOf(new_pos).dims.get(.T));
}
test "VecX Element-wise Math and Scaling" {
const Meter = Scalar(i32, .{ .L = 1 }, .{});
const Vec3M = Meter.Vec3;
const v1 = Vec3M{ .data = .{ 10, 20, 30 } };
const v2 = Vec3M{ .data = .{ 2, 5, 10 } };
// Element-wise division
const div = v1.div(v2);
try std.testing.expectEqual(5, div.data[0]);
try std.testing.expectEqual(4, div.data[1]);
try std.testing.expectEqual(3, div.data[2]);
try std.testing.expectEqual(0, @TypeOf(div).dims.get(.L)); // M / M = Dimensionless
}
test "VecX Conversions" {
const KiloMeter = Scalar(i32, .{ .L = 1 }, .{ .L = .k });
const Meter = Scalar(i32, .{ .L = 1 }, .{});
const v_km = KiloMeter.Vec3{ .data = .{ 1, 2, 3 } };
const v_m = v_km.to(Meter);
try std.testing.expectEqual(1000, v_m.data[0]);
try std.testing.expectEqual(2000, v_m.data[1]);
try std.testing.expectEqual(3000, v_m.data[2]);
// Type checking the result
try std.testing.expectEqual(1, @TypeOf(v_m).dims.get(.L));
try std.testing.expectEqual(UnitScale.none, @TypeOf(v_m).scales.get(.L));
}
test "VecX Length" {
const MeterInt = Scalar(i32, .{ .L = 1 }, .{});
const MeterFloat = Scalar(f32, .{ .L = 1 }, .{});
// Integer length
// 3-4-5 triangle on XY plane
const v_int = MeterInt.Vec3{ .data = .{ 3, 4, 0 } };
try std.testing.expectEqual(25, v_int.lengthSqr());
try std.testing.expectEqual(5, v_int.length());
// Float length
const v_float = MeterFloat.Vec3{ .data = .{ 3.0, 4.0, 0.0 } };
try std.testing.expectApproxEqAbs(@as(f32, 25.0), v_float.lengthSqr(), 1e-4);
try std.testing.expectApproxEqAbs(@as(f32, 5.0), v_float.length(), 1e-4);
}
test "Vector Comparisons" {
const Meter = Scalar(f32, .{ .L = 1 }, .{});
const KiloMeter = Scalar(f32, .{ .L = 1 }, .{ .L = .k });
const v1 = Meter.Vec3{ .data = .{ 1000.0, 500.0, 0.0 } };
const v2 = KiloMeter.Vec3{ .data = .{ 1.0, 0.5, 0.0 } };
const v3 = KiloMeter.Vec3{ .data = .{ 1.0, 0.6, 0.0 } };
// 1. Equality (Whole vector)
try std.testing.expect(v1.eqAll(v2));
try std.testing.expect(v1.neAll(v3));
// 2. Element-wise Ordered Comparison
const higher = v3.gt(v1); // compares 1km, 0.6km, 0km vs 1000m, 500m, 0m
try std.testing.expectEqual(false, higher[0]); // 1km == 1000m
try std.testing.expectEqual(true, higher[1]); // 0.6km > 500m
try std.testing.expectEqual(false, higher[2]); // 0 == 0
// 3. Element-wise Equal Comparison
const equal = v3.eq(v1); // compares 1km, 0.6km, 0km vs 1000m, 500m, 0m
try std.testing.expectEqual(true, equal[0]); // 1km == 1000m
try std.testing.expectEqual(false, equal[1]); // 0.6km > 500m
try std.testing.expectEqual(true, equal[2]); // 0 == 0
// 3. Less than or equal
const low_eq = v1.lte(v3);
try std.testing.expect(low_eq[0] and low_eq[1] and low_eq[2]);
}
test "Vector vs Scalar Comparisons" {
const Meter = Scalar(f32, .{ .L = 1 }, .{});
const KiloMeter = Scalar(f32, .{ .L = 1 }, .{ .L = .k });
const positions = Meter.Vec3{ .data = .{ 500.0, 1200.0, 3000.0 } };
const threshold = KiloMeter{ .value = 1.0 }; // 1km (1000m)
// Check which axes exceed the 1km threshold
const exceeded = positions.gtScalar(threshold);
try std.testing.expectEqual(false, exceeded[0]); // 500m > 1km is false
try std.testing.expectEqual(true, exceeded[1]); // 1200m > 1km is true
try std.testing.expectEqual(true, exceeded[2]); // 3000m > 1km is true
// Check for equality (broadcasted)
const exact_match = positions.eqScalar(Meter{ .value = 500.0 });
try std.testing.expect(exact_match[0] == true);
try std.testing.expect(exact_match[1] == false);
}
test "Vector Dot and Cross Products" {
const Meter = Scalar(f32, .{ .L = 1 }, .{});
const Newton = Scalar(f32, .{ .M = 1, .L = 1, .T = -2 }, .{});
const pos = Meter.Vec3{ .data = .{ 10.0, 0.0, 0.0 } };
const force = Newton.Vec3{ .data = .{ 5.0, 5.0, 0.0 } };
// 1. Dot Product (Work = F dot d)
const work = force.dot(pos);
try std.testing.expectEqual(50.0, work.value);
// Dimensions should be M¹L²T⁻² (Energy/Joules)
try std.testing.expectEqual(1, @TypeOf(work).dims.get(.M));
try std.testing.expectEqual(2, @TypeOf(work).dims.get(.L));
try std.testing.expectEqual(-2, @TypeOf(work).dims.get(.T));
// 2. Cross Product (Torque = r cross F)
const torque = pos.cross(force);
try std.testing.expectEqual(0.0, torque.data[0]);
try std.testing.expectEqual(0.0, torque.data[1]);
try std.testing.expectEqual(50.0, torque.data[2]);
// Torque dimensions are same as Energy but as a Vector
try std.testing.expectEqual(2, @TypeOf(torque).dims.get(.L));
}
test "Vector Abs, Pow, Sqrt and Product" {
const Meter = Scalar(f32, .{ .L = 1 }, .{});
const v1 = Meter.Vec3{ .data = .{ -2.0, 3.0, -4.0 } };
// 1. Abs
const v_abs = v1.abs();
try std.testing.expectEqual(2.0, v_abs.data[0]);
try std.testing.expectEqual(4.0, v_abs.data[2]);
// 2. Product (L1 * L1 * L1 = L3)
const vol = v_abs.product();
try std.testing.expectEqual(24.0, vol.value);
try std.testing.expectEqual(3, @TypeOf(vol).dims.get(.L));
// 3. Pow (Scalar exponent: (L1)^2 = L2)
const area_vec = v_abs.pow(2);
try std.testing.expectEqual(4.0, area_vec.data[0]);
try std.testing.expectEqual(16.0, area_vec.data[2]);
try std.testing.expectEqual(2, @TypeOf(area_vec).dims.get(.L));
// 4. Sqrt
const sqrted = area_vec.sqrt();
try std.testing.expectEqual(2, sqrted.data[0]);
try std.testing.expectEqual(4, sqrted.data[2]);
try std.testing.expectEqual(2, @TypeOf(sqrted).dims.get(.L));
}
test "mulScalar comptime_int" {
const Meter = Scalar(i32, .{ .L = 1 }, .{});
const v = Meter.Vec3{ .data = .{ 1, 2, 3 } };
const scaled = v.mulScalar(10); // comptime_int → dimensionless
try std.testing.expectEqual(10, scaled.data[0]);
try std.testing.expectEqual(20, scaled.data[1]);
try std.testing.expectEqual(30, scaled.data[2]);
// Dimensions unchanged: L¹ × dimensionless = L¹
try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L));
try std.testing.expectEqual(0, @TypeOf(scaled).dims.get(.T));
}
test "mulScalar comptime_float" {
const MeterF = Scalar(f32, .{ .L = 1 }, .{});
const v = MeterF.Vec3{ .data = .{ 1.0, 2.0, 4.0 } };
const scaled = v.mulScalar(0.5); // comptime_float → dimensionless
try std.testing.expectApproxEqAbs(0.5, scaled.data[0], 1e-6);
try std.testing.expectApproxEqAbs(1.0, scaled.data[1], 1e-6);
try std.testing.expectApproxEqAbs(2.0, scaled.data[2], 1e-6);
try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L));
}
test "mulScalar T (value type)" {
const MeterF = Scalar(f32, .{ .L = 1 }, .{});
const v = MeterF.Vec3{ .data = .{ 3.0, 6.0, 9.0 } };
const factor: f32 = 2.0;
const scaled = v.mulScalar(factor);
try std.testing.expectApproxEqAbs(6.0, scaled.data[0], 1e-6);
try std.testing.expectApproxEqAbs(12.0, scaled.data[1], 1e-6);
try std.testing.expectApproxEqAbs(18.0, scaled.data[2], 1e-6);
try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L));
}
test "divScalar comptime_int" {
const Meter = Scalar(i32, .{ .L = 1 }, .{});
const v = Meter.Vec3{ .data = .{ 10, 20, 30 } };
const halved = v.divScalar(2); // comptime_int → dimensionless divisor
try std.testing.expectEqual(5, halved.data[0]);
try std.testing.expectEqual(10, halved.data[1]);
try std.testing.expectEqual(15, halved.data[2]);
try std.testing.expectEqual(1, @TypeOf(halved).dims.get(.L));
}
test "divScalar comptime_float" {
const MeterF = Scalar(f64, .{ .L = 1 }, .{});
const v = MeterF.Vec3{ .data = .{ 9.0, 6.0, 3.0 } };
const r = v.divScalar(3.0);
try std.testing.expectApproxEqAbs(3.0, r.data[0], 1e-9);
try std.testing.expectApproxEqAbs(2.0, r.data[1], 1e-9);
try std.testing.expectApproxEqAbs(1.0, r.data[2], 1e-9);
try std.testing.expectEqual(1, @TypeOf(r).dims.get(.L));
}
test "eqScalar / gtScalar with comptime_int on dimensionless vector" {
// Bare numbers are dimensionless, so comparisons only work when vector is dimensionless too.
const DimLess = Scalar(i32, .{}, .{});
const v = DimLess.Vec3{ .data = .{ 1, 2, 3 } };
const eq_res = v.eqScalar(2);
try std.testing.expectEqual(false, eq_res[0]);
try std.testing.expectEqual(true, eq_res[1]);
try std.testing.expectEqual(false, eq_res[2]);
const gt_res = v.gtScalar(1);
try std.testing.expectEqual(false, gt_res[0]);
try std.testing.expectEqual(true, gt_res[1]);
try std.testing.expectEqual(true, gt_res[2]);
}
+116 -314
View File
@@ -1,7 +1,6 @@
const std = @import("std");
const Io = std.Io;
const Scalar = @import("Scalar.zig").Scalar;
const Vector = @import("Vector.zig").Vector;
const Tensor = @import("Tensor.zig").Tensor;
var io: Io = undefined;
pub fn main(init: std.process.Init) !void {
@@ -11,25 +10,12 @@ pub fn main(init: std.process.Init) !void {
io = init.io;
try vectorSIMDvsNative(f64, &stdout_writer.interface);
try bench_Scalar(&stdout_writer.interface);
try stdout_writer.flush();
try vectorSIMDvsNative(f32, &stdout_writer.interface);
try bench_Vector(&stdout_writer.interface);
try stdout_writer.flush();
try vectorSIMDvsNative(i32, &stdout_writer.interface);
try bench_HighDimTensor(&stdout_writer.interface);
try stdout_writer.flush();
try vectorSIMDvsNative(i64, &stdout_writer.interface);
try stdout_writer.flush();
try vectorSIMDvsNative(i128, &stdout_writer.interface);
try stdout_writer.flush();
// try bench_Scalar(&stdout_writer.interface);
// try stdout_writer.flush();
// try bench_vsNative(&stdout_writer.interface);
// try stdout_writer.flush();
// try bench_crossTypeVsNative(&stdout_writer.interface);
// try stdout_writer.flush();
// try bench_Vector(&stdout_writer.interface);
// try stdout_writer.flush();
}
fn getTime() Io.Timestamp {
@@ -97,9 +83,9 @@ fn bench_Scalar(writer: *std.Io.Writer) !void {
comptime var tidx: usize = 0;
inline for (Types, TNames) |T, tname| {
const M = Scalar(T, .{ .L = 1 }, .{});
const KM = Scalar(T, .{ .L = 1 }, .{ .L = .k });
const S = Scalar(T, .{ .T = 1 }, .{});
const M = Tensor(T, .{ .L = 1 }, .{}, &.{1});
const KM = Tensor(T, .{ .L = 1 }, .{ .L = .k }, &.{1});
const S = Tensor(T, .{ .T = 1 }, .{}, &.{1});
inline for (Ops, 0..) |op_name, oidx| {
var samples: [SAMPLES]f64 = undefined;
@@ -111,23 +97,23 @@ fn bench_Scalar(writer: *std.Io.Writer) !void {
std.mem.doNotOptimizeAway(
{
_ = if (comptime std.mem.eql(u8, op_name, "add"))
(M{ .value = getVal(T, i, 63) }).add(M{ .value = getVal(T, i +% 7, 63) })
(M.splat(getVal(T, i, 63))).add(M.splat(getVal(T, i +% 7, 63)))
else if (comptime std.mem.eql(u8, op_name, "sub"))
(M{ .value = getVal(T, i +% 10, 63) }).sub(M{ .value = getVal(T, i, 63) })
(M.splat(getVal(T, i +% 10, 63))).sub(M.splat(getVal(T, i, 63)))
else if (comptime std.mem.eql(u8, op_name, "mul"))
(M{ .value = getVal(T, i, 63) }).mul(M{ .value = getVal(T, i +% 1, 63) })
(M.splat(getVal(T, i, 63))).mul(M.splat(getVal(T, i +% 1, 63)))
else if (comptime std.mem.eql(u8, op_name, "div"))
(M{ .value = getVal(T, i +% 10, 63) }).div(S{ .value = getVal(T, i, 63) })
(M.splat(getVal(T, i +% 10, 63))).div(S.splat(getVal(T, i, 63)))
else if (comptime std.mem.eql(u8, op_name, "to"))
(KM{ .value = getVal(T, i, 15) }).to(M)
(KM.splat(getVal(T, i, 15))).to(M)
else if (comptime std.mem.eql(u8, op_name, "abs"))
(M{ .value = getVal(T, i, 63) }).abs()
(M.splat(getVal(T, i, 63))).abs()
else if (comptime std.mem.eql(u8, op_name, "eq"))
(M{ .value = getVal(T, i, 63) }).eq(M{ .value = getVal(T, i +% 3, 63) })
(M.splat(getVal(T, i, 63))).eq(M.splat(getVal(T, i +% 3, 63)))
else if (comptime std.mem.eql(u8, op_name, "gt"))
(M{ .value = getVal(T, i, 63) }).gt(M{ .value = getVal(T, i +% 3, 63) })
(M.splat(getVal(T, i, 63))).gt(M.splat(getVal(T, i +% 3, 63)))
else
(M{ .value = getVal(T, i, 63) }).mul(3);
(M.splat(getVal(T, i, 63))).mul(M.splat(3));
},
);
}
@@ -168,222 +154,6 @@ fn bench_Scalar(writer: *std.Io.Writer) !void {
try writer.print("└──────────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘\n", .{});
}
fn bench_vsNative(writer: *std.Io.Writer) !void {
const ITERS: usize = 100_000;
const SAMPLES: usize = 5;
const getValT = struct {
fn f(comptime TT: type, i: usize) TT {
const v = (i % 100) + 1;
return if (comptime @typeInfo(TT) == .float) @floatFromInt(v) else @intCast(v);
}
}.f;
const Types = .{ i32, i64, i128, f32, f64 };
const TNames = .{ "i32", "i64", "i128", "f32", "f64" };
// Expanded Ops to match bench_Scalar
const Ops = .{ "add", "sub", "mul", "div", "abs", "eq", "gt" };
try writer.print(
\\
\\ Scalar vs Native Overhead Analysis
\\
\\┌───────────┬──────┬───────────┬───────────┬───────────┐
\\│ Operation │ Type │ Native │ Scalar │ Slowdown │
\\├───────────┼──────┼───────────┼───────────┼───────────┤
\\
, .{});
inline for (Ops, 0..) |op_name, j| {
inline for (Types, 0..) |T, tidx| {
var native_total_ns: f64 = 0;
var quantity_total_ns: f64 = 0;
const M = Scalar(T, .{ .L = 1 }, .{});
const S = Scalar(T, .{ .T = 1 }, .{});
std.mem.doNotOptimizeAway({
for (0..SAMPLES) |_| {
// --- 1. Benchmark Native ---
const n_start = getTime();
for (0..ITERS) |i| {
const a = getValT(T, i);
const b = getValT(T, 2);
// Native logic branch
_ = if (comptime std.mem.eql(u8, op_name, "add"))
a + b
else if (comptime std.mem.eql(u8, op_name, "sub"))
a - b
else if (comptime std.mem.eql(u8, op_name, "mul"))
a * b
else if (comptime std.mem.eql(u8, op_name, "div"))
if (comptime @typeInfo(T) == .int) @divTrunc(a, b) else a / b
else if (comptime std.mem.eql(u8, op_name, "abs"))
if (comptime @typeInfo(T) == .int) @abs(a) else @as(T, @abs(a))
else if (comptime std.mem.eql(u8, op_name, "eq"))
a == b
else if (comptime std.mem.eql(u8, op_name, "gt"))
a > b
else
unreachable;
}
const n_end = getTime();
native_total_ns += @as(f64, @floatFromInt(n_start.durationTo(n_end).toNanoseconds()));
// --- 2. Benchmark Scalar ---
const q_start = getTime();
for (0..ITERS) |i| {
const qa = M{ .value = getValT(T, i) };
const qb = if (comptime std.mem.eql(u8, op_name, "div")) S{ .value = getValT(T, 2) } else M{ .value = getValT(T, 2) };
// Scalar logic branch
_ = if (comptime std.mem.eql(u8, op_name, "add"))
qa.add(qb)
else if (comptime std.mem.eql(u8, op_name, "sub"))
qa.sub(qb)
else if (comptime std.mem.eql(u8, op_name, "mul"))
qa.mul(qb)
else if (comptime std.mem.eql(u8, op_name, "div"))
qa.div(qb)
else if (comptime std.mem.eql(u8, op_name, "abs"))
qa.abs()
else if (comptime std.mem.eql(u8, op_name, "eq"))
qa.eq(qb)
else if (comptime std.mem.eql(u8, op_name, "gt"))
qa.gt(qb)
else
unreachable;
}
const q_end = getTime();
quantity_total_ns += @as(f64, @floatFromInt(q_start.durationTo(q_end).toNanoseconds()));
}
});
const avg_n = (native_total_ns / SAMPLES) / @as(f64, @floatFromInt(ITERS));
const avg_q = (quantity_total_ns / SAMPLES) / @as(f64, @floatFromInt(ITERS));
const slowdown = avg_q / avg_n;
try writer.print("│ {s:<9} │ {s:<4} │ {d:>7.2}ns │ {d:>7.2}ns │ {d:>8.2}x │\n", .{
op_name, TNames[tidx], avg_n, avg_q, slowdown,
});
}
if (j != Ops.len - 1) try writer.print("├───────────┼──────┼───────────┼───────────┼───────────┤\n", .{});
}
try writer.print("└───────────┴──────┴───────────┴───────────┴───────────┘\n", .{});
}
fn bench_crossTypeVsNative(writer: *std.Io.Writer) !void {
const ITERS: usize = 100_000;
const SAMPLES: usize = 5;
const getValT = struct {
fn f(comptime TT: type, i: usize) TT {
// Keep values safe and non-zero to avoid division by zero or overflows during cross-casting
const v = (i % 50) + 1;
return if (comptime @typeInfo(TT) == .float) @floatFromInt(v) else @intCast(v);
}
}.f;
// Helper for the Native baseline: explicitly casting T2 to T1 before the operation
const castTo = struct {
fn f(comptime DestT: type, comptime SrcT: type, val: SrcT) DestT {
if (comptime DestT == SrcT) return val;
const src_info = @typeInfo(SrcT);
const dest_info = @typeInfo(DestT);
if (dest_info == .int and src_info == .int) return @intCast(val);
if (dest_info == .float and src_info == .int) return @floatFromInt(val);
if (dest_info == .int and src_info == .float) return @intFromFloat(val);
if (dest_info == .float and src_info == .float) return @floatCast(val);
unreachable;
}
}.f;
const Types = .{ i16, i64, i128, f32, f64 };
const TNames = .{ "i16", "i64", "i128", "f32", "f64" };
const Ops = .{ "add", "mul", "div" };
try writer.print(
\\
\\ Cross-Type Overhead Analysis: Scalar vs Native
\\
\\┌─────────┬──────┬──────┬───────────┬───────────┬───────────┐
\\│ Op │ T1 │ T2 │ Native │ Scalar │ Slowdown │
\\├─────────┼──────┼──────┼───────────┼───────────┼───────────┤
\\
, .{});
inline for (Ops, 0..) |op_name, j| {
inline for (Types, 0..) |T1, t1_idx| {
inline for (Types, 0..) |T2, t2_idx| {
var native_total_ns: f64 = 0;
var quantity_total_ns: f64 = 0;
const M1 = Scalar(T1, .{ .L = 1 }, .{});
const M2 = Scalar(T2, .{ .L = 1 }, .{});
const S2 = Scalar(T2, .{ .T = 1 }, .{});
std.mem.doNotOptimizeAway({
for (0..SAMPLES) |_| {
// --- 1. Benchmark Native (Cast T2 to T1, then math) ---
const n_start = getTime();
for (0..ITERS) |i| {
const a = getValT(T1, i);
const b_raw = getValT(T2, 2);
const b = castTo(T1, T2, b_raw);
_ = if (comptime std.mem.eql(u8, op_name, "add"))
a + b
else if (comptime std.mem.eql(u8, op_name, "mul"))
a * b
else if (comptime @typeInfo(T1) == .int)
@divTrunc(a, b)
else
a / b;
}
const n_end = getTime();
native_total_ns += @as(f64, @floatFromInt(n_start.durationTo(n_end).toNanoseconds()));
// --- 2. Benchmark Scalar ---
const q_start = getTime();
for (0..ITERS) |i| {
const qa = M1{ .value = getValT(T1, i) };
const qb = if (comptime std.mem.eql(u8, op_name, "div"))
S2{ .value = getValT(T2, 2) }
else
M2{ .value = getValT(T2, 2) };
_ = if (comptime std.mem.eql(u8, op_name, "add"))
qa.add(qb)
else if (comptime std.mem.eql(u8, op_name, "mul"))
qa.mul(qb)
else
qa.div(qb);
}
const q_end = getTime();
quantity_total_ns += @as(f64, @floatFromInt(q_start.durationTo(q_end).toNanoseconds()));
}
const avg_n = (native_total_ns / SAMPLES) / @as(f64, @floatFromInt(ITERS));
const avg_q = (quantity_total_ns / SAMPLES) / @as(f64, @floatFromInt(ITERS));
const slowdown = avg_q / avg_n;
try writer.print("│ {s:<7} │ {s:<4} │ {s:<4} │ {d:>7.2}ns │ {d:>7.2}ns │ {d:>8.2}x │\n", .{
op_name, TNames[t1_idx], TNames[t2_idx], avg_n, avg_q, slowdown,
});
});
}
}
if (j != Ops.len - 1) {
try writer.print("├─────────┼──────┼──────┼───────────┼───────────┼───────────┤\n", .{});
}
}
try writer.print("└─────────┴──────┴──────┴───────────┴───────────┴───────────┘\n", .{});
}
fn bench_Vector(writer: *std.Io.Writer) !void {
const ITERS: usize = 10_000;
const SAMPLES: usize = 10;
@@ -412,26 +182,25 @@ fn bench_Vector(writer: *std.Io.Writer) !void {
\\ Vector<N, T> benchmark — {d} iterations, {d} samples/cell
\\ (Results in ns/op; "---" = not applicable for this length)
\\
\\┌──────────────────┬──────┬─────────┬─────────┬─────────┐
\\│ Operation │ Type │ Len=3 │ Len=4 │ Len=16 │
\\├──────────────────┼──────┼─────────┼─────────┼─────────┤
\\┌──────────────────┬──────┬─────────┬─────────┬─────────┬─────────┬─────────┐
\\│ Operation │ Type │ Len=1 │ Len=3 │ Len=4 │ Len=16 │ Len=100 │
\\├──────────────────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤
\\
, .{ ITERS, SAMPLES });
const Types = .{ i32, i64, i128, f32, f64 };
const TNames = .{ "i32", "i64", "i128", "f32", "f64" };
const Lengths = .{ 3, 4, 16 };
const Lengths = .{ 1, 3, 4, 16, 100 };
// "cross" is only valid for len=3; other cells will show " --- "
const Ops = .{ "add", "div", "mulScalar", "dot", "cross", "product", "pow", "length" };
const Ops = .{ "add", "div", "mulScalar", "dot", "product", "pow", "length" };
inline for (Ops, 0..) |op_name, o_idx| {
inline for (Types, TNames) |T, tname| {
try writer.print("│ {s:<16} │ {s:<4} │", .{ op_name, tname });
inline for (Lengths) |len| {
const Q_base = Scalar(T, .{ .L = 1 }, .{});
const Q_time = Scalar(T, .{ .T = 1 }, .{});
const V = Vector(len, Q_base);
const Q_time = Tensor(T, .{ .T = 1 }, .{}, &.{1});
const V = Tensor(T, .{ .L = 1 }, .{}, &.{len});
// cross product is only defined for len == 3
const is_cross = comptime std.mem.eql(u8, op_name, "cross");
@@ -446,23 +215,19 @@ fn bench_Vector(writer: *std.Io.Writer) !void {
for (0..SAMPLES) |s_idx| {
const t_start = getTime();
for (0..ITERS) |i| {
const v1 = V.initDefault(getVal(T, i, 63));
const v1 = V.splat(getVal(T, i, 63));
if (comptime std.mem.eql(u8, op_name, "add")) {
const v2 = V.initDefault(getVal(T, i +% 7, 63));
const v2 = V.splat(getVal(T, i +% 7, 63));
_ = v1.add(v2);
} else if (comptime std.mem.eql(u8, op_name, "div")) {
_ = v1.div(V.initDefault(getVal(T, i +% 2, 63)));
_ = v1.div(V.splat(getVal(T, i +% 2, 63)));
} else if (comptime std.mem.eql(u8, op_name, "mulScalar")) {
const s_val = Q_time{ .value = getVal(T, i +% 2, 63) };
_ = v1.mulScalar(s_val);
const s_val = Q_time.splat(getVal(T, i +% 2, 63));
_ = v1.mul(s_val);
} else if (comptime std.mem.eql(u8, op_name, "dot")) {
const v2 = V.initDefault(getVal(T, i +% 5, 63));
_ = v1.dot(v2);
} else if (comptime std.mem.eql(u8, op_name, "cross")) {
// len == 3 guaranteed by the guard above
const v2 = V.initDefault(getVal(T, i +% 5, 63));
_ = v1.cross(v2);
const v2 = V.splat(getVal(T, i +% 5, 63));
_ = v1.contract(v2, 0, 0);
} else if (comptime std.mem.eql(u8, op_name, "product")) {
_ = v1.product();
} else if (comptime std.mem.eql(u8, op_name, "pow")) {
@@ -484,67 +249,104 @@ fn bench_Vector(writer: *std.Io.Writer) !void {
}
if (o_idx < Ops.len - 1) {
try writer.print("├──────────────────┼──────┼─────────┼─────────┼─────────┤\n", .{});
try writer.print("├──────────────────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤\n", .{});
}
}
try writer.print("└──────────────────┴──────┴─────────┴─────────┴─────────┘\n", .{});
try writer.print("└──────────────────┴──────┴─────────┴─────────┴─────────┴─────────┴─────────┘\n", .{});
}
fn vectorSIMDvsNative(comptime T: type, writer: *std.Io.Writer) !void {
const iterations: u64 = 10_000;
const lens = [_]u32{ 1, 2, 3, 4, 5, 10, 100, 1_000, 10_000 };
fn bench_HighDimTensor(writer: *std.Io.Writer) !void {
const ITERS: usize = 5_000;
const SAMPLES: usize = 5;
try writer.print("\nSIMD Speedup Analysis: {s}\n", .{@typeName(T)});
try writer.print("┌────────────┬────────────┬────────────┬────────────┐\n", .{});
try writer.print("│ Vector Len │ Scalar (us)│ Vector (us)│ Speedup │\n", .{});
try writer.print("├────────────┼────────────┼────────────┼────────────┤\n", .{});
inline for (lens) |vector_len| {
// --- Scalar Test ---
var scalar_val: T = 10;
const start_scalar = getTime();
var i: u64 = 0;
while (i < iterations * vector_len) : (i += 1) {
if (comptime @typeInfo(T) == .int)
scalar_val = scalar_val +% 1
else
scalar_val = scalar_val + 1;
const 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);
}
const scalar_time = start_scalar.durationTo(getTime()).toMicroseconds();
}.f;
// --- Vector Test ---
var vector_val: @Vector(vector_len, T) = @splat(20);
const start_vector = getTime();
i = 0;
const increment: @Vector(vector_len, T) = @splat(1);
while (i < iterations) : (i += 1) {
if (comptime @typeInfo(T) == .int)
vector_val = vector_val +% increment
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
vector_val = vector_val + increment;
samples[mid];
return median_ns / @as(f64, @floatFromInt(iters));
}
const vector_time = start_vector.durationTo(getTime()).toMicroseconds();
}.f;
// --- Results ---
const s_float = @as(f64, @floatFromInt(scalar_time));
const v_float = @as(f64, @floatFromInt(vector_time));
try writer.print(
\\
\\ High Dimension Tensor benchmark — {d} iterations, {d} samples/cell
\\ (Results in ns/op)
\\
\\┌─────────────────┬──────┬──────────────┬──────────────┬──────────────┬──────────────┐
\\│ Operation │ Type │ 2x2x2 │ 3x3x3 │ 4x4x4 │ 10x10x10x10 │
\\├─────────────────┼──────┼──────────────┼──────────────┼──────────────┼──────────────┤
\\
, .{ ITERS, SAMPLES });
// Speedup = ScalarTime / VectorTime.
// > 1.0 means SIMD is faster.
const speedup = if (vector_time > 0) s_float / v_float else 0;
const Types = .{ i32, i64, f32, f64 };
const TNames = .{ "i32", "i64", "f32", "f64" };
try writer.print("│ {d:<10} │ {d:>10} │ {d:>10} │ {d:>9.2}x │\n", .{
vector_len,
scalar_time,
vector_time,
speedup,
// Testing multiple structural bounds
const Shapes = .{
&.{ 2, 2, 2 },
&.{ 3, 3, 3 },
&.{ 4, 4, 4 },
&.{ 10, 10, 10, 10 },
};
const Ops = .{ "add", "sub", "mulElem", "mulScalar", "abs" };
inline for (Ops, 0..) |op_name, o_idx| {
inline for (Types, TNames) |T, tname| {
try writer.print("│ {s:<15} │ {s:<4} │", .{ op_name, tname });
inline for (Shapes) |shape| {
const V = Tensor(T, .{ .L = 1 }, .{}, shape);
const Q = Tensor(T, .{ .T = 1 }, .{}, &.{1}); // For scalar broadcasting operations
var samples: [SAMPLES]f64 = undefined;
for (0..SAMPLES) |s_idx| {
const t_start = getTime();
for (0..ITERS) |i| {
std.mem.doNotOptimizeAway({
const t1 = V.splat(getVal(T, i, 63));
_ = if (comptime std.mem.eql(u8, op_name, "add"))
t1.add(V.splat(getVal(T, i +% 7, 63)))
else if (comptime std.mem.eql(u8, op_name, "sub"))
t1.sub(V.splat(getVal(T, i +% 3, 63)))
else if (comptime std.mem.eql(u8, op_name, "mulElem"))
t1.mul(V.splat(getVal(T, i +% 5, 63)))
else if (comptime std.mem.eql(u8, op_name, "mulScalar"))
t1.mul(Q.splat(getVal(T, i +% 2, 63)))
else if (comptime std.mem.eql(u8, op_name, "abs"))
t1.abs()
else
unreachable;
});
try writer.flush();
std.mem.doNotOptimizeAway(scalar_val);
std.mem.doNotOptimizeAway(vector_val);
}
try writer.print("└────────────┴────────────┴────────────┴────────────┘\n", .{});
const t_end = getTime();
samples[s_idx] = @as(f64, @floatFromInt(t_start.durationTo(t_end).toNanoseconds()));
}
const median_ns_per_op = computeStats(&samples, ITERS);
try writer.print(" {d:>12.1} │", .{median_ns_per_op});
}
try writer.print("\n", .{});
}
if (o_idx < Ops.len - 1) {
try writer.print("├─────────────────┼──────┼──────────────┼──────────────┼──────────────┼──────────────┤\n", .{});
}
}
try writer.print("└─────────────────┴──────┴──────────────┴──────────────┴──────────────┴──────────────┘\n", .{});
}
-98
View File
@@ -1,98 +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 Scalar = @import("Scalar.zig").Scalar;
/// Returns true if `T` is a `Scalar_` type (has `dims`, `scales`, and `value`).
pub fn isScalarType(comptime T: type) bool {
return @typeInfo(T) == .@"struct" and
@hasDecl(T, "dims") and
@hasDecl(T, "scales") and
@hasField(T, "value");
}
/// 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 rhsScalarType(comptime BaseT: type, comptime RhsT: type) type {
if (comptime isScalarType(RhsT)) return RhsT;
if (comptime RhsT == comptime_int or RhsT == comptime_float or RhsT == BaseT)
return Scalar(BaseT, .{}, .{});
@compileError(
"rhs must be a Scalar, " ++ @typeName(BaseT) ++
", comptime_int, or comptime_float; got " ++ @typeName(RhsT),
);
}
/// Convert `rhs` to its normalised Scalar form (see `rhsScalarType`).
pub inline fn toRhsScalar(comptime BaseT: type, rhs: anytype) rhsScalarType(BaseT, @TypeOf(rhs)) {
if (comptime isScalarType(@TypeOf(rhs))) return rhs;
const DimLess = Scalar(BaseT, .{}, .{});
return DimLess{ .value = @as(BaseT, rhs) };
}
+9
View File
@@ -0,0 +1,9 @@
const std = @import("std");
pub const TensorStatic = @import("TensorStatic.zig").Tensor;
pub const TensorAlloc = @import("TensorAlloc.zig").Tensor;
pub const TensorGpu = @import("TensorGpu.zig").Tensor;
pub const Dimensions = @import("Dimensions.zig");
pub const Scales = @import("Scales.zig");
pub const Base = @import("Base.zig");
pub const UnitParser = @import("UnitParser.zig");
-16
View File
@@ -1,16 +0,0 @@
const std = @import("std");
pub const Scalar = @import("Scalar.zig").Scalar;
pub const Vector = @import("Vector.zig").Vector;
pub const Dimensions = @import("Dimensions.zig");
pub const Scales = @import("Scales.zig");
pub const Base = @import("Base.zig");
test {
_ = @import("Quantity.zig");
// _ = @import("Vector.zig");
// _ = @import("Dimensions.zig");
// _ = @import("Scales.zig");
// _ = @import("Base.zig");
// _ = @import("helper.zig");
}
+155
View File
@@ -0,0 +1,155 @@
const std = @import("std");
const Scales = @import("Scales.zig");
const UnitScale = Scales.UnitScale;
const Dimensions = @import("Dimensions.zig");
const Dimension = Dimensions.Dimension;
pub const TensorKind = enum { static, alloc, gpu };
pub fn isTensor(comptime T: type) bool {
return comptime @typeInfo(T) == .@"struct" and @hasDecl(T, "ISTENSOR");
}
pub fn shapeTotal(shape: []const comptime_int) usize {
var t: comptime_int = 1;
for (shape) |s| t *= s;
return t;
}
/// Check if two shapes are strictly identical.
pub fn shapeEql(a: []const comptime_int, b: []const comptime_int) bool {
if (a.len != b.len) return false;
for (a, 0..) |v, i|
if (v != b[i]) return false;
return true;
}
/// Row-major (C-order) strides: strides[i] = product(shape[i+1..]).
/// e.g. shape {3, 4} → strides {4, 1}
/// shape {2, 3, 4} → strides {12, 4, 1}
pub fn shapeStrides(shape: []const comptime_int) [shape.len]comptime_int {
var st: [shape.len]comptime_int = undefined;
if (shape.len == 0) return st;
st[shape.len - 1] = 1;
if (shape.len > 1) {
var i: comptime_int = shape.len - 1;
while (i > 0) : (i -= 1) st[i - 1] = st[i] * shape[i];
}
return st;
}
/// Return a copy of `shape` with the element at `axis` removed.
pub fn shapeRemoveAxis(shape: []const comptime_int, axis: comptime_int) [shape.len - 1]comptime_int {
var out: [shape.len - 1]comptime_int = undefined;
var j: comptime_int = 0;
for (shape, 0..) |v, i| {
if (i != axis) {
out[j] = v;
j += 1;
}
}
return out;
}
/// Concatenate two compile-time slices.
pub fn shapeCat(a: []const comptime_int, b: []const comptime_int) [a.len + b.len]comptime_int {
var out: [a.len + b.len]comptime_int = undefined;
for (a, 0..) |v, i| out[i] = v;
for (b, 0..) |v, i| out[a.len + i] = v;
return out;
}
/// Decode a flat row-major index into N-D coordinates.
/// Called only in comptime contexts (all arguments are comptime).
pub fn decodeFlatCoords(flat: comptime_int, n: comptime_int, strd: [n]comptime_int) [n]usize {
var coords: [n]comptime_int = undefined;
var tmp = flat;
for (0..n) |i| {
coords[i] = if (strd[i] == 0) 0 else tmp / strd[i];
tmp = if (strd[i] == 0) 0 else tmp % strd[i];
}
return coords;
}
/// Encode N-D coordinates into a flat row-major index.
/// Called only in comptime contexts.
pub fn encodeFlatCoords(coords: []const usize, n: usize, strd: [n]usize) usize {
var flat: usize = 0;
for (0..n) |i| flat += coords[i] * strd[i];
return flat;
}
/// Rebuild a full coordinate array by inserting `val` at `axis` into `free`.
/// `free` holds the remaining (non-contracted) coordinates in order.
pub fn insertAxis(
comptime n: usize,
comptime axis: usize,
comptime val: usize,
comptime free: []const usize,
) [n]usize {
var out: [n]usize = undefined;
var fi: usize = 0;
for (0..n) |i| {
if (i == axis) {
out[i] = val;
} else {
out[i] = free[fi];
fi += 1;
}
}
return out;
}
pub inline fn isInt(comptime T: type) bool {
return @typeInfo(T) == .int or @typeInfo(T) == .comptime_int;
}
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);
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);
}
return out;
}
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);
}
}
+9
View File
@@ -0,0 +1,9 @@
test {
_ = @import("TensorStatic.zig");
_ = @import("TensorAlloc.zig");
_ = @import("TensorGpu.zig");
_ = @import("Dimensions.zig");
_ = @import("Scales.zig");
_ = @import("Base.zig");
_ = @import("UnitParser.zig");
}