Fidelity to the Julia implementation
These tools are Rust ports of the OptArt modules in the Julia DigitalArt
package. Some outputs match Julia exactly, some to floating-point tolerance, and
some not at all — by design. This page says which is which, so that a difference
you notice can be classified as expected or as a bug.
What matches
| Layer | Matches Julia |
|---|---|
| Pattern matrices, tile geometry | Exactly |
Channel matrices (mod1(i+j,3), variant map) |
Exactly |
| Hilbert indices | Exactly |
| Alias table construction | Exactly |
Density field (density_field), including exact zeros |
Bit-exact |
Color grid t = 1 - value, tone clamping |
To ~1e-12 |
| sRGB transfer functions, luminance | To ~1e-12 |
| Voronoi centroids and weights, tone banding | To ~1e-12 |
| Downsampled intensities, grayscale source | To ~1e-12 |
| Downsampled intensities, color source | To ~7e-7 on the mean |
| Seeded stochastic stages (stipple, relax, spatial partition) | No |
Seeded random variant grids |
No |
| Rendered pixels | No (antialiasing differs) |
truchet -r N output size |
No, intentionally |
The density field was verified pixel-for-pixel on the cameraman image at three different gamma and floor settings. The color-source downsampling tolerance of ~7e-7 is bounded by 8-bit source depth, not by the arithmetic.
What does not match, and why
Random stages
--seed N is reproducible within each implementation but does not reproduce
the other’s output. Julia’s Truchet path uses dSFMT MersenneTwister; Ariadne
uses StableRNGs. Matching either draw-for-draw was out of scope.
Instead of matching draws, the stochastic stages are verified by statistical invariants: nearest-neighbor coefficient of variation falling toward blue noise, mass conservation, tour validity, and cap adherence.
That the two agree in character is checked by running both. On the cameraman
image at 20,000 points, Julia reports cv_nn=0.44, reseeded=612 and Rust
cv_nn=0.442, reseeded=602 — two independent generators converging on the same
behavior.
Rendered pixels
Antialiasing differs between Makie and tiny-skia. The geometry is the same;
the pixels are not identical.
truchet -r means different things
Rust’s -r N produces an image exactly N pixels wide. Julia’s -r N produces
2N pixels: Makie’s Figure(size=...) is specified in points and saved at
pt_per_unit=2.
This is a leak of a Makie implementation detail rather than a designed behavior,
so it was deliberately not reproduced. Double Julia’s -r value, or halve
Rust’s, when comparing outputs.
What is not ported
Julia’s Segment module (felzenszwalb, seeded_region_growing) needs
ImageSegmentation.jl and has no Rust equivalent.
Its purpose is to produce the integer label matrix that Partition’s :labels
mode consumes — and ariadne --partition labels --mask PNG consumes exactly
that, reading regions from a painted or Julia-exported PNG. So only the
automatic derivation of labels is missing, which Julia’s own documentation
records as the weaker path anyway: 28–46 connected components per group, against
1 for seeded.
In practice, painting a mask by hand gives better results than automatic segmentation did.
How this is enforced
Both core crates carry golden tests against fixtures generated by Julia
(truchet-core/tests/golden.rs, ariadne-core/tests/golden.rs).
The two *-core crates have no runtime dependencies at all — deliberately,
so the golden tests exercise pure arithmetic with no image decoder or rasterizer
in the dependency graph. The RNG is injected through a trait rather than pulled
in as a crate.
Regenerate the fixtures after any change to Julia’s patterns, downsampler, tile geometry, color assignment, or Ariadne’s deterministic stages:
cd DigitalArt/Julia
julia --project=. scripts/export_fixtures.jl
A diff in testdata/ is then a reviewable signal that behavior changed.
Performance
Measured on the cameraman image (512×512), Apple Silicon:
| Command | Julia | Rust |
|---|---|---|
ariadne -n 20000 -e 60 |
10.3 s | 0.34 s (~30×) |
truchet --color -n 360 |
21.9 s | 0.52 s (~42×) |
For Ariadne the gap is dominated by rendering: Julia spends 9.5 s of its 10.3 s in Makie’s SVG writer, against 0.06 s here. The arithmetic stages are much closer — relaxation is 0.31 s in Julia against 0.22 s in Rust.
The Julia figures predate the TSP solver; a current --solver tsp run in Rust
spends about 2.5 s in tour construction, which is a different algorithm rather
than a slowdown.