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.