vdc1;
auto pop() { return std::array{vdc0.pop(), vdc1.pop()}; }
};
```
```rust
pub struct Halton { // ๐ฆ Rust โ bases at runtime
vdc0: VdCorput, vdc1: VdCorput,
}
```
]
---
### ๐ Circle, Disk, Sphere: Geometric Mappings
All use the same mathematical transformations:
.font-sm[
| Generator | Formula | Mapping |
|-----------|---------|---------|
| **Circle** $S^1$ | $\theta = 2\pi \cdot v$ | $(\cos\theta, \sin\theta)$ |
| **Disk** $D^2$ | $\theta = 2\pi \cdot v_\theta, r^2 = v_r$ | $(r\cos\theta, r\sin\theta)$ |
| **Sphere** $S^2$ | $z = 2v_z - 1, \theta = 2\pi \cdot v_\theta$ | $((1-z^2)^{0.5}\cos\theta, (1-z^2)^{0.5}\sin\theta, z)$ |
]
**Key difference in Sphere mapping:**
.font-sm[
```python
# ๐ Python / ๐ฆ Rust: Sphere stores VdCorput + Circle internally
cosphi = 2.0 * vdc.pop() - 1.0
sinphi = sqrt(1.0 - cosphi * cosphi)
[cos, sin] = circle_gen.pop()
return [sinphi * cos, sinphi * sin, cosphi]
```
```cpp
// โก C++: Sphere stores VdCorput + Circle as template members
auto cosphi = 2.0 * vdcgen.pop() - 1.0;
auto sinphi = sqrt(1.0 - cosphi * cosphi);
auto [cos, sin] = cirgen.pop();
```
]
---
### ๐ฎ Sphere3Hopf: Hopf Fibration
Generates points on **3-sphere** $S^3$ (4D hypersphere) using Hopf coordinates:
$$ (\cos\eta \cdot e^{i\psi}, \; \sin\eta \cdot e^{i(\phi+\psi)}) $$
$$ \eta = \sqrt{v},\quad \psi = 2\pi v_\psi,\quad \phi = 2\pi v_\phi $$
```python
# ๐ Python: 3 VdCorput generators
phi = self.vdc0.pop() * TWO_PI
psy = self.vdc1.pop() * TWO_PI
eta = self.vdc2.pop()
cos_eta = sqrt(eta)
sin_eta = sqrt(1.0 - eta)
# Returns [cos_eta*cos(psy), cos_eta*sin(psy), \
# sin_eta*cos(phi+psy), sin_eta*sin(phi+psy)]
```
**All four projects use identical Hopf fibration math** โ only the language syntax differs.
---
### ๐ฎ N-Dimensional Spheres: Missing Piece
**Sphere3** ($S^3$) and **SphereN** ($S^{n-1}$) exist in:
- โ
Rust (`sphere_n.rs` โ recursive `SphereN` with table interpolation)
- โ
Python (`sphere_n.py` โ same recursive approach)
- โ
C++ lds-gen-cpp (`sphere_n.hpp` โ `SphereGen` trait, cached tables)
- โ **C++ lds-cpp** โ **NOT implemented!**
**Algorithm:** Uses inverse transform sampling with precomputed marginal CDF tables:
.mermaid[
graph LR
A["VdC value v โ [0,1]"] --> B["Map via Tโโปยน table\n(lookup + interpolate)"]
B --> C["Angle ฯ = Tโโปยน(v)"]
C --> D["(sin ฯ ยท sโโโ, cos ฯ)"]
D --> E["sโโโ is recursively\ngenerated from\nlower sphere"]
]
> **The gap:** lds-cpp is the only project missing n-dimensional sphere generation.
> **Fix:** Port `sphere_n` from lds-gen-cpp to lds-cpp.
---
### โ
Sphere-N: Output Verification
All three projects with sphere_n produce **identical results** (verified to 15+ decimal places):
**Sphere3 [2,3,5] (4D)** โ first point:
.font-sm[
| Coordinate | Python ๐ | Rust ๐ฆ | C++ โก (lds-gen-cpp) |
|-----------|-----------|---------|---------------------|
| 0 | 0.2913440162992141 | โ
match | โ
match |
| 1 | 0.8966646826186098 | โ
match | โ
match |
| 2 | -0.3333333333333334 | โ
match | โ
match |
| 3 | 0.0000000000000001 | โ
match | โ
match |
]
**SphereN [2,3,5,7] (5D) and [2,3,5,7,11] (6D)** โ all coordinates match identically โ
> **The only mismatch:** 1 out of 45 coordinates differed at the 16th decimal place โ **machine epsilon** for double precision, from different `libm` implementations (MSVC vs CPython vs Rust).
---
### โก Sphere-N: Performance Benchmarks
**Generating points on n-dimensional spheres (ns per point):**
| Generator | Rust ๐ฆ | C++ โก | Python ๐ | Winner |
|-----------|---------|-------|-----------|--------|
| **Sphere3 4D** [2,3,5] | 1035 ns | **657 ns** | 7749 ns | โก C++ |
| **SphereN 5D** [2,3,5,7] | **1607 ns** | 1665 ns | 12153 ns | ๐ฆ Rust |
| **SphereN 6D** [2,3,5,7,11] | **2020 ns** | 2312 ns | 48443 ns | ๐ฆ Rust |
---
**Why the crossover?**
.mermaid[
graph TD
subgraph "4D: C++ wins"
A["C++ Sphere3\n1 mutex lock\n+ template VdC"] --> B["657 ns\nfastest"]
end
subgraph "5D/6D: Rust wins"
C["Rust SphereN\nrecursive: 0 locks\nAtomicU64 at every level"] --> D["2020 ns\nfastest"]
E["C++ SphereN\nrecursive: N mutex locks\n1 per sub-generator"] --> F["2312 ns\nslower with depth"]
end
]
---
### โก Sphere-N: Key Observations
**C++ wins for 4D** โ Simple single-level generator. The `std::mutex` is acquired once per point. Template-optimized `VdCorput` modulo operations compensate for the lock overhead.
**Rust catches up for 5D/6D** โ SphereN is **recursive**: 6D wraps a 5D sphere, which wraps a 4D sphere. C++ acquires a `std::mutex` at **every level** (3 locks per 6D point). Rust's `AtomicU64` is **lock-free** โ cost doesn't scale with depth.
**Python is 7โ24ร slower** โ Pure interpreter overhead for the interpolation table lookup (binary search in `simple_interp`), trigonometric functions, and dynamic dispatch dominates.
**Regression: Locks vs Atomics**
```
Operation C++ locks Rust atomics
Sphere3 (4D) 1 lock 1 atomic โ C++ 1.60ร faster
SphereN (5D) 2 locks 2 atomics โ Rust 1.04ร faster
SphereN (6D) 3 locks 3 atomics โ Rust 1.14ร faster
```
> **Bottom line:** For shallow hierarchies, C++ templates beat atomics. For deep recursion, lock-free wins.
---
### ๐ Thread Safety Approaches
.font-sm[
| Language | Mechanism | Overhead | API Impact |
|----------|-----------|----------|------------|
| ๐ Python | `threading.Lock` in `pop()` | High (GIL + lock) | `pop()` and `reseed()` are locked |
| ๐ฆ Rust | `AtomicU64` for `count` | **Minimal** (lock-free) | `pop()` uses `fetch_add` |
| โก C++ (both) | **None** (not thread-safe) | Zero | No atomics needed |
]
```rust
// ๐ฆ Rust โ lock-free atomic increment
pub fn pop(&mut self) -> f64 {
let count = self.count.fetch_add(1, Ordering::Relaxed) + 1;
// ... compute vdc(count, self.base) ...
}
```
```python
# ๐ Python โ mutex-protected increment
def pop(self):
with self._count_lock:
self._count += 1
count = self._count
# ... compute vdc(count, self.base) ...
```
---
### ๐ Cross-Language Comparison
.mermaid[
graph LR
subgraph "Generator Style"
A["C++ templates\ncompile-time Base"] --> D["constexpr VdCorput\nzero-cost abstraction"]
B["Rust runtime Base\nAtomicU64"] --> E["heap-allocated VdCorput\nthread-safe by default"]
C["Python runtime Base\nthreading.Lock"] --> F["object-oriented LDS\nwith iterators"]
end
]
---
**Key design differences:**
.font-sm[
| Aspect | Python ๐ | Rust ๐ฆ | C++ โก |
|--------|-----------|---------|--------|
| **Base parameter** | runtime `int` | runtime `u64` | compile-time `template` |
| **State mutation** | explicit lock | `AtomicU64` | plain `unsigned long` |
| **API extras** | `pop_batch()`, `__iter__` | `Iterator` impl | STL `begin()/end()` iterators |
| **Precomputed tables** | no | `rev_lst` array | `rev_lst` array + static precomputed VdC tables |
| **Sphere N** | recursive + interp | recursive + interp + cache | recursive + interp (lds-gen-cpp only) |
]
---
### โ
Output Verification
All four projects produce **identical sequences** (verified for small n):
.font-sm[
| Generator | First value (base 2, seed 0) |
|-----------|------------------------------|
| **VdCorput** | `0.5` โ all match โ
|
| **Halton [2,3]** | `[0.5, 0.333...]` โ all match โ
|
| **Circle** | `[-1.0, 0.0]` โ all match โ
|
| **Sphere [2,3]** | `[-0.5, 0.866, 0.0]` โ all match โ
|
| **Sphere3Hopf [2,3,5]** | `[-0.224, 0.387, 0.447, -0.775]` โ Rust/Python match โ
|
]
**VdCorput(2) first 10 values:**
```
0.5, 0.25, 0.75, 0.125, 0.625, 0.375, 0.875, 0.0625, 0.5625, 0.3125
```
All languages produce **bit-identical output** for the same seed ๐ฏ
---
### ๐ฏ Key Takeaways
1. **Same math, same sequences** โ The radical inverse algorithm is identical across all four projects โ
2. **C++ wins with compile-time templates** โ `VdCorput` with `Base` as template parameter lets the compiler optimize the modulo to multiplication. **Zero-cost abstraction** at its best ๐
3. **Rust's AtomicU64 is elegant** โ Lock-free thread safety with minimal overhead. But runtime `base` means the modulo can't be optimized ๐ฆ
4. **Python's threading.Lock has real cost** โ GIL contention + lock acquisition makes concurrent `pop()` expensive. Fine for single-threaded use ๐
5. **lds-cpp is missing sphere_n** โ The only project without n-dimensional sphere generation. `Sphere3`/`SphereN` exist in Rust, Python, and lds-gen-cpp โ
6. **C++ has TWO projects** โ `lds-cpp` (core LDS) and `lds-gen-cpp` (core + sphere_n). Consider merging or porting sphere_n to lds-cpp ๐๏ธ
---
### ๐ Scorecard
.font-sm[
| Metric | ๐ Python | ๐ฆ Rust | โก C++ (lds-cpp) | โก C++ (lds-gen-cpp) |
|--------|-----------|---------|-----------------|---------------------|
| **Core LDS completeness** | โ
Full | โ
Full | โ
Full | โ
Full |
| **Sphere3/SphereN** | โ
| โ
| โ Missing | โ
|
| **Sphere3 4D perf** | 7749 ns | 1035 ns | โ | **657 ns** ๐ |
| **SphereN 6D perf** | 48443 ns | **2020 ns** ๐ | โ | 2312 ns |
| **Thread safety** | โ
Locks | โ
Atomic | โ None | โ None |
| **Compile-time optimization** | โ | โ | โ
Template Base | โ
Template Base |
| **Precomputed tables** | โ | โ
rev_lst | โ
VdC table + rev_lst | โ
VdC table + rev_lst |
| **Iterator support** | โ
`__iter__` | โ
`Iterator` | โ
STL iterators | โ
STL iterators |
]
**Bottom line:** All four projects implement the same low-discrepancy sequences faithfully. C++ is fastest for single-level generators (template-optimized VdC). Rust wins for deep recursive generators (lock-free atomics scale better). Python is most flexible (runtime dispatch). The main gap is `sphere_n` in `lds-cpp` โ a natural next step for alignment.
---
count: false
class: nord-dark, middle, center
# ๐ Thank You!
### *Questions?*
**Repositories:**
- https://github.com/luk036/lds-rs ๐ฆ
- https://github.com/luk036/lds-cpp โก
- https://github.com/luk036/lds-gen-cpp โก
- https://github.com/luk036/lds-gen ๐
**Slides:** https://luk036.github.io/cqs/py-rs-cpp-lds-remark.html
> ๐ก **"Make it work, make it right, make it fast โ in your language of choice."**