20 Commits
Author SHA1 Message Date
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
16 changed files with 843 additions and 1039 deletions
-34
View File
@@ -1,34 +0,0 @@
name: Deploy MkDocs to Garage
on:
push:
branches:
- main # Adjust to your branch name
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Install dependencies
run: pip install mkdocs-material
- name: Build
run: mkdocs build
- name: Sync to Garage S3
uses: https://github.com/jakejarvis/s3-sync-action@master
with:
args: --endpoint-url https://s3.garage.bouvais.lu --acl public-read --delete
env:
AWS_S3_BUCKET: 'zig-dimal.bouvais.lu'
AWS_ACCESS_KEY_ID: ${{ secrets.GARAGE_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.GARAGE_SECRET_KEY }}
AWS_REGION: 'garage'
SOURCE_DIR: 'site' # MkDocs defaults to 'site' folder for output
+1
View File
@@ -1,2 +1,3 @@
zig-out
.zig-cache
mkdocs.yaml
+157 -250
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;
Or use the pre-built helpers from `dma.Base`:
```zig
pub fn main() void {
// Define types: m/s² acceleration, m/s velocity, m distance
const Acceleration = dma.Base.Acceleration.Of(f64);
const KmhSpeed = dma.Base.Speed.Scaled(f64, .{ .L = .k, .T = .hour });
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 });
}
```
### 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⁻¹
**Output:**
```
### 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
```
---
+1 -1
View File
@@ -2,7 +2,7 @@ const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseFast });
const optimize = b.standardOptimizeOption(.{});
// 1. Define the module so other projects can import it
_ = b.addModule("dimal", .{
+1 -1
View File
@@ -1,6 +1,6 @@
.{
.name = .dimal,
.version = "0.1.0",
.version = "0.2.1",
.fingerprint = 0x9453b1ff1e52d858,
.minimum_zig_version = "0.16.0",
.dependencies = .{},
-11
View File
@@ -1,11 +0,0 @@
- Changed Quantity to Tensor that can use any shape and is a single @Vector.
Point being to add WebGPU easily from this.
Scalr suffer in performance tho, I will work on that
Maybe I can do a jupiter like web interface with cells to make Dim analysis
I could:
- Use cells with a toy language
- A nice debugger to display current variables with dimensions, type and value
- Realtime error (I try to compile at change, display error on the cell)
- Integrate a small graphic API that use Raylib canvas
- COuld generate template at comptime =o
-26
View File
@@ -1,26 +0,0 @@
# Welcome to My Project
This is a static site hosted via **Gitea Actions** and **Garage S3 Storage**.
!!! info "Status"
The deployment pipeline is currently **Active**.
Updates to the `main` branch are pushed automatically.
## Quick Start
To replicate this setup, you need:
1. **Traefik** as the reverse proxy.
2. **Garage** for S3-compatible web hosting.
3. **Gitea** for version control and CI.
### Deployment Details
| Component | Technology |
| :--- | :--- |
| **Engine** | MkDocs Material |
| **Hosting** | Garage S3 |
| **Routing** | Traefik |
---
## Contact
If you have questions, reach out via the Gitea instance.
-3
View File
@@ -1,3 +0,0 @@
## GPU support with WebGPU
Example: https://github.com/seyhajin/webgpu-wasm-zig
-48
View File
@@ -1,48 +0,0 @@
site_name: Bouvais Docs
site_url: https://zig-dimal.bouvais.lu
site_description: A minimal technical documentation site.
site_author: Adrien Bouvais
theme:
name: material
language: en
# Color palette with auto light/dark mode
palette:
- media: "(prefers-color-scheme: light)"
scheme: default
primary: indigo
accent: indigo
toggle:
icon: material/brightness-7
name: Switch to dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: indigo
accent: indigo
toggle:
icon: material/brightness-4
name: Switch to light mode
features:
- navigation.sections
- navigation.top
- content.code.copy
- content.code.annotate
# Minimal plugins
plugins:
- search
# Your single page
nav:
- Home: index.md
# Extensions to make your markdown look better
markdown_extensions:
- admonition
- pymdownx.details
- pymdownx.superfences
- pymdownx.highlight:
anchor_linenums: true
- pymdownx.inlinehilite
- attr_list
+3 -3
View File
@@ -3,7 +3,7 @@ const std = @import("std");
// Adjust these imports to match your actual file names
const Dimensions = @import("Dimensions.zig");
const Scales = @import("Scales.zig");
const Tensor = @import("Tensor.zig").Tensor;
const Tensor = @import("Tensor.zig").TensorStatic;
fn PhysicalConstant(comptime d: Dimensions.ArgOpts, comptime val: f64, comptime s: Scales.ArgOpts) type {
return struct {
@@ -60,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]
@@ -81,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⁻¹]
+2 -1
View File
@@ -59,7 +59,8 @@ pub const UnitScale = enum(isize) {
var buf: [16]u8 = undefined;
return switch (self) {
.none => "",
.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),
.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
};
}
+269 -546
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(comptime 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 }, .{});
}
+21 -23
View File
@@ -1,6 +1,6 @@
const std = @import("std");
const Io = std.Io;
const Tensor = @import("Tensor.zig").Tensor;
const Tensor = @import("Tensor.zig").TensorStatic;
var io: Io = undefined;
pub fn main(init: std.process.Init) !void {
@@ -10,22 +10,22 @@ pub fn main(init: std.process.Init) !void {
io = init.io;
// try vectorSIMDvsNative(f64, &stdout_writer.interface);
// try stdout_writer.flush();
// try vectorSIMDvsNative(f32, &stdout_writer.interface);
// try stdout_writer.flush();
// try vectorSIMDvsNative(i32, &stdout_writer.interface);
// try stdout_writer.flush();
// try vectorSIMDvsNative(i64, &stdout_writer.interface);
// try stdout_writer.flush();
// try vectorSIMDvsNative(i128, &stdout_writer.interface);
// try stdout_writer.flush();
//
// try bench_Scalar(&stdout_writer.interface);
// try stdout_writer.flush();
try vectorSIMDvsNative(f64, &stdout_writer.interface);
try stdout_writer.flush();
try vectorSIMDvsNative(f32, &stdout_writer.interface);
try stdout_writer.flush();
try vectorSIMDvsNative(i32, &stdout_writer.interface);
try stdout_writer.flush();
try vectorSIMDvsNative(i64, &stdout_writer.interface);
try stdout_writer.flush();
try vectorSIMDvsNative(i128, &stdout_writer.interface);
try stdout_writer.flush();
try bench_Scalar(&stdout_writer.interface);
try stdout_writer.flush();
try bench_vsNative(&stdout_writer.interface);
try stdout_writer.flush();
// try bench_crossTypeVsNative(&stdout_writer.interface);
try bench_crossTypeVsNative(&stdout_writer.interface);
try stdout_writer.flush();
try bench_Vector(&stdout_writer.interface);
try stdout_writer.flush();
@@ -128,7 +128,7 @@ fn bench_Scalar(writer: *std.Io.Writer) !void {
else if (comptime std.mem.eql(u8, op_name, "gt"))
(M.splat(getVal(T, i, 63))).gt(M.splat(getVal(T, i +% 3, 63)))
else
(M.splat(getVal(T, i, 63))).mul(3);
(M.splat(getVal(T, i, 63))).mul(M.splat(3));
},
);
}
@@ -180,8 +180,8 @@ fn bench_vsNative(writer: *std.Io.Writer) !void {
}
}.f;
const Types = .{ f64, i64, i128, f32, f64 };
const TNames = .{ "f64", "i64", "i128", "f32", "f64" };
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" };
@@ -203,7 +203,6 @@ fn bench_vsNative(writer: *std.Io.Writer) !void {
const M = Tensor(T, .{}, .{}, &.{1});
std.mem.doNotOptimizeAway({
for (0..SAMPLES) |_| {
// --- 1. Benchmark Native ---
const n_start = getTime();
@@ -232,8 +231,8 @@ fn bench_vsNative(writer: *std.Io.Writer) !void {
native_total_ns += @as(f64, @floatFromInt(n_start.durationTo(n_end).toNanoseconds()));
const v_start = getTime();
const va = getValT(T, 10);
const vb = getValT(T, 2);
const va = @Vector(1, T){getValT(T, 10)};
const vb = @Vector(1, T){getValT(T, 2)};
for (0..ITERS) |_| {
// Native logic branch
_ = if (comptime std.mem.eql(u8, op_name, "add"))
@@ -245,7 +244,7 @@ fn bench_vsNative(writer: *std.Io.Writer) !void {
else if (comptime std.mem.eql(u8, op_name, "div"))
if (comptime @typeInfo(T) == .int) @divTrunc(va, vb) else va / vb
else if (comptime std.mem.eql(u8, op_name, "abs"))
if (comptime @typeInfo(T) == .int) @abs(va) else @as(T, @abs(va))
if (comptime @typeInfo(T) == .int) @as(T, @intCast(@abs(va[0]))) else @abs(va)
else if (comptime std.mem.eql(u8, op_name, "eq"))
va == vb
else if (comptime std.mem.eql(u8, op_name, "gt"))
@@ -282,7 +281,6 @@ fn bench_vsNative(writer: *std.Io.Writer) !void {
const q_end = getTime();
tensor_total_ns += @as(f64, @floatFromInt(q_start.durationTo(q_end).toNanoseconds()));
}
});
const avg_n = (native_total_ns / SAMPLES) / @as(f64, @floatFromInt(ITERS));
const avg_v = (vector_total_ns / SAMPLES) / @as(f64, @floatFromInt(ITERS));
+2
View File
@@ -4,10 +4,12 @@ pub const Tensor = @import("Tensor.zig").Tensor;
pub const Dimensions = @import("Dimensions.zig");
pub const Scales = @import("Scales.zig");
pub const Base = @import("Base.zig");
pub const UnitParser = @import("UnitParser.zig");
test {
_ = @import("Tensor.zig");
_ = @import("Dimensions.zig");
_ = @import("Scales.zig");
_ = @import("Base.zig");
_ = @import("UnitParser.zig");
}
+149
View File
@@ -0,0 +1,149 @@
const std = @import("std");
const Scales = @import("Scales.zig");
const UnitScale = Scales.UnitScale;
const Dimensions = @import("Dimensions.zig");
const Dimension = Dimensions.Dimension;
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);
}
}