sanzo-sampler · a case study

The colours were easy. Teaching the metric how Wada combines them took five tries.

I set out to generate palettes in the spirit of A Dictionary of Color Combinations. The colours were easy. Learning how Wada combines them took five tries — and each failure taught me I was measuring the wrong thing.

The real story is the metric

Every fix was a better answer to one question: what makes a Wada combination Wada?

The palettes only improved because my yardstick did. Here's how it evolved.

metric v1

Harmony score

Density of each pair's relationships among his combos.

✗ a coin flip
metric v2

Diversity volume

Spread = the volume the colours span.

✗ collapsed to the muted core
metric v3

Contrast number

Hit Wada's measured ΔE, ≈0.28.

✗ bought with saturation
metric v4

Value vs chroma

Split contrast into value and saturation.

◑ the diagnosis
metric v5

Learned quality vector

A tuned vector of Wada-ness; sample, don't maximise.

✓ matches his eye
ACT I
I trusted a harmony score I'd never tested.

The first sampler ranked colours by a learned harmony and chose with MMR. Before leaning on it, I ran a simple test: show the score one real Wada combination and one random pairing of his colours — can it tell which is real? A perfect judge is right 100% of the time; a useless one, 50% — a coin flip.

coin flip50%75%certain100% HARMONY · a coin flip GAMUT · a good guess

Harmony scored a coin flip. Another way to see it: plot the scores it gives to real combinations versus random pairings. A good metric pulls them apart; harmony piles them on top of each other. The gamut (which colours, not how they're combined) is the signal that actually separates.

score →
Harmony score
real Wada random
the two piles overlap → it can't tell them apart
score →
Gamut score
real Wada random
the piles separate → this one can
what that blind score actually picks · roll the dice

MMR is sometimes fine. But it's only as reliable as the score steering it — and that score is a coin flip, so good rolls are luck, not skill.

The flaw: a harmony score that can't tell Wada from random can't reliably steer toward Wada.
ACT II
So I reached for the "optimal" algorithm — and it went timid.

If I can't score harmony, lean on structure: a Determinantal Point Process, the canonical diverse-subset picker. But my kernel weighted "quality" so hard that the DPP huddled in the dense, muted core of the gamut. Every palette became three mid-tones at the same lightness — flatter than random.

Wada · same chroma budget
#253122Deep Slate Olive
#837e31Olive
#eeb480Pinkish Cinnamon
#f5ecc2Sulpher Yellow
Ranges all the way from near-black to pale, at low chroma.
ΔL0.36chroma max0.10ΔE0.37
mine · quality-weighted DPP
#864b4dRed Maple Leaf
#c65841Summer Fig
#978466Antique Royal Gold
#d2ae89Cheddar Biscuit
Stranded in the muted middle — no light, no dark.
ΔL0.16chroma max0.14ΔE0.18
The flaw: the metric had no notion that Wada spreads. Same mutedness; he ranges light→dark, I didn't.
ACT III
I forced contrast to a number — and got garish primaries.

I measured Wada's real contrast (mean ΔE ≈ 0.28) and tuned the kernel to hit it. The number matched; the palettes turned into loud primaries — because a single contrast number doesn't care how you reach it, and the cheapest contrast to buy is raw saturation.

Wada · more contrast, half the chroma
#111314Black
#1c4286Deep Lyons Blue
#a36aa5Aconite Violet
#fdc57eCinnamon Buff
ΔE 0.41 at chroma 0.12 — punch from value; colours stay muted.
ΔL0.37chroma max0.12ΔE0.41
mine · contrast-by-number
#6d4145Red Rumour
#e31f26Brutal Doom
#00918eSupermint
#d4aa83Maple Tan
Same ΔE, chroma 0.22: loud, not Wada.
ΔL0.17chroma max0.22ΔE0.27
The flaw: matching a scalar was necessary but not sufficient. It was blind to which axis the contrast lived on.

Act IV · the diagnosis

Wada's contrast is anisotropic — it lives on one axis.

I stopped tuning and looked. Take every pair of colours inside a combination and ask how one turns into the other: does it change in value (lighter/darker) or in saturation? Plotted, the answer is a tall, thin cloud — Wada moves up and down, almost never sideways. His contrast is ~13× more value than chroma.

← change in saturation → change in value (ΔL) ↑
Wada offsets — tall & thin: he moves in value, not saturation   our garish attempt — round: it chased saturation

My isotropic kernel treated both directions the same, so it happily traded value for the saturation it should have left alone. The fix had to make the algorithm see colour the way the cloud is shaped.

The gamut, and why it matters

Every Wada colour, in one picture.

Plot all 159 colours by lightness and saturation. They hug the left — low chroma — at every lightness. That muted band is the gamut. The whole job is: stay in the band, but travel up and down it.

muted band (Wada lives here) chroma → saturation lightness (dark → light)
muted band (Wada lives here) chroma → saturation lightness (dark → light)
× garish picks — shoved right, into saturation
muted band (Wada lives here) chroma → saturation lightness (dark → light)
◯ final picks — stay in the muted band, spread top-to-bottom (value)

Left, the garish attempt breaks right out of the band into saturation. Right, the final sampler stays in the band and spreads top-to-bottom — contrast from value, exactly like Wada.

ACT V
The fix: measure quality as a vector, and let sampling do the rest.

The anisotropy is real — but baking it into a bespoke distance metric had a side-effect the two-sample test caught: it skewed palettes warm and clustered their hues. The cleaner fix puts the intelligence in the quality: a multidimensional, Optuna-tuned vector of Wada-ness — manifold proximity, value-conditional typicality (so dark colours aren't punished for being dark), mutedness, hue-fit. With that, diversity can be measured plainly (isotropic), and drawing a k-DPP sample instead of the single greedy-best yields the dark tail, the muted chroma, and a naturally graded saturated accent — no hand-tuned metric required.

Wada · a real combination
#40456aViolet Blue
#064f6eVandar Poel's Blue
#f68c50Apricot Orange
#f3a257Golden Yellow
Violet-blue to golden yellow: value-led, one warm accent.
ΔL0.24chroma max0.15ΔE0.29
mine · k-DPP, learned quality
#7a433cSweet Spiceberry
#c85444Chilli Cashew
#c2ae93Brown Bunny
#fbe6a0Rich Glow
The same character: value contrast, muted, one accent.
ΔL0.28chroma max0.11ΔE0.30
Solved: value contrast and muted chroma at once — from a learned quality vector + sampling, not a bespoke metric.

The whole journey, on one map

Four attempts circling one target.

Each attempt placed by what it actually made: saturation across, value contrast up. Timid sinks to the floor; the contrast-by-number fix overshoots into saturation; only the final recipe — isotropic diversity + the learned quality vector — lands on Wada: high value contrast at low chroma.

too saturated too flat chroma → saturation ΔL → value contrast WADA — the target I · MMRII · timidIII · garishV · final recipe

One more fix underneath

I was even sampling colours wrong.

My candidate colours had been blobs jittered off the 159 named points — which sprayed a third of them out past the muted band, into saturation. I replaced it with sampling the gamut region directly.

chroma →
jitter off the 159 points
only 65% land in the muted band — the rest leak right, into saturation
chroma →
sample the gamut directly
100% in-band, by construction — and more varied

Alternatives considered & discarded

Roads I didn't take.

More harmony features

Three different feature sets all scored ≈ a coin flip. Convergence, not a gap — more would just overfit 348 combos.

A Gaussian-process surrogate

No scalar function to regress — the dependence is a covariance, which is a copula's job, not a GP's. I did try the copula; see the honest-ceiling section.

A hard chroma cap

Clipping saturation works but throws away Wada's value–chroma correlation. The learned metric keeps it.

Pure diversity

Drop quality entirely and the picker grabs garish gamut-edge outliers. Quality still has to gate what's allowed in.

Just widen the jitter

Bigger blobs clump harder around the 159 points and leak further out. The gamut region is the principled fix.

Did I actually become Wada?

The honest test: can a classifier tell my palettes apart?

Matching a few averages is not the same as matching the distribution. The real check: train a classifier to tell generated palettes from real Wada combinations on perceptual (OKLab) features. If it can't — AUC near 0.5 — they're indistinguishable; near 1.0 it has found an obvious tell.

At first it was trivial (AUC ≈ 0.95): my palettes ran too warm, their hues clustered, their colours washed. The fixes — make colour quality a learned vector of Wada-ness (Optuna-weighted, with a chroma term), measure diversity isotropically, and sample the dark tail — pulled it to 0.71, with every marginal landing on Wada:

two-sample AUC — lower is more like Wada target score algo 0.50.60.70.80.91.0 0.991density score0.928+ value targets0.89+ vector score0.869+ isotropic algo0.643+ chroma term

AUC is just this: line every palette up by the classifier's score and ask how often a real Wada outranks one of mine. At 0.71 the two populations overlap heavily — and the paired examples below, which the classifier scored the same, are genuinely hard to call (top of each pair = the book, bottom = mine).

the book (real Wada) ↑ my palettes (generated) ↓ ← looks like my palettes classifier score looks like the book →
book
mine
book
mine
book
mine
book
mine
book
mine
book
mine
book
mine
featureWadabeforeafter
warm balance (frac warm)0.520.650.57
hue spread0.400.150.40
chroma (saturation)0.100.080.10
saturated accent (chroma range)0.100.070.10
dark reach (min L)0.450.510.44

Not a perfect 0.5 — a from-scratch generator still leaves a faint fingerprint (a slightly narrower chroma range) — but mine now sits inside Wada's distribution rather than beside it.

How close can I get? The honest ceiling

I tried to close the last gap — and learned why I can't, yet.

Every marginal matches, but AUC ~0.70 means a joint-structure residual remains: which lightness pairs with which chroma and hue. To close it I modelled the joint directly — a Gaussian copula (Sklar's theorem: keep the exact marginals, add only the dependence — the covariance idea a Gaussian process is built on, aimed at generation) and a GMM — and validated honestly with a train/test holdout.

approachAUCnoveltyverdict
perturb real combos (jitter)0.450.03matches — but only by copying (no novelty)
k-DPP sampler (shipped)0.700.09novel + controllable — the honest best
Gaussian copula0.61*0.09*in-sample only; held-out it overfits to ~0.71
Gaussian mixture (GMM)0.740.12worse — re-models the marginals I'd matched

The trap is the last two columns. Perturbing real combos hits AUC 0.45 — but novelty 0.03 means it's just copying. The copula dazzles in-sample (0.61), but held-out it overfits: a 9–12-dimensional joint estimated from ~160 training combinations memorises their quirks. A held-out global Optuna found no real gain over the defaults either.

So the honest verdict: with 348 curated combinations, AUC→0.5 is reachable only by memorising — ~0.70 is the ceiling for genuinely novel generation. The one lever that moves it is more data (Wada's Volume 2 and the original six), at which point the copula and a global tune stop overfitting and become real wins. Until then, the controllable sampler is the right answer.

Where we landed

Pick a colour. Watch it become Wada.

Choose a hue from the wheel. The sampler builds a palette centred on it — your colour forced in, surrounded by its Wada-fit companions. Beside it, the closest real combination from the book: the one whose nearest colour matches your pick.

pick
a hue
mine · centred on
closest real Wada combination

manifold KDE · gamut SMOTE sampling · multidimensional Optuna-tuned quality · isotropic k-DPP sampling · validated by replay + held-out two-sample test

Colour data: Matt DesLauriers' digitisation of Sanzo Wada's A Dictionary of Color Combinations (Seigensha). The lesson that paid for the detour: when a metric won't separate signal from noise, the fix is rarely more features — it's asking whether you're measuring the right thing at all.

Teaching an algorithm Sanzo Wada's eye