Two related defects, found while testing whether the strength ceiling scales with
latent size and step count. It does not -- SD 1.5 at a 64x64 latent and 20 steps
breaks down harder and earlier than MiniMax H3 at 38x22 and 8 steps, which
inverts the premise. The cause is the channel axis.
shaders/base.py::_expand_channels builds every extra channel as a pointwise
function (sin, abs) of a mixture of the FIRST TWO channels, so the draw spans
almost nothing however many channels are asked for. Effective rank, measured as
the participation ratio of the channel covariance spectrum:
4ch 16ch 24ch 128ch
gaussian 4.00 16.00 23.90 120.30
domain_warp 1.00 2.22 2.06 2.37
temporal_coherent 1.00 1.00 1.00 1.00
curl_noise 3.58 10.88 22.11 25.22
tensor_field 3.81 14.31 23.54 90.78
domain_warp -- the default -- is a two-dimensional signal copied across up to 128
channels, and temporal_coherent is literally one channel repeated. Samplers
expect i.i.d. noise; that collapse is why the shader's own pattern surfaces so
readily. It also explains the ordering already observed on H3, where
tensor_field (near full rank) tolerated the most strength.
`decorrelate_channels` fills the channel axis from DECORRELATION_BASIS
independent renders mixed through a seeded random matrix, so the rank becomes
min(channels, basis) while every basis element is still that shader. On SD 1.5
it moves the usable ceiling from below 0.25 -- where stock is already an
abstract green smear -- to a clean photoreal variation at 0.25 that stays partly
readable at 0.5.
Guarded twice, because neither guess held on its own: generators already wider
than the basis are skipped (curl_noise spans 25 of 128 channels despite looking
correlated, so a correlation threshold was the wrong test), and the remix is
kept only when it actually widens the draw (curl_noise at four channels remixes
narrower than it started). Turning it on can never narrow the noise.
Separately, shaders/temporal_coherent_noise.py read params["base_seed"]
unconditionally where domain_warp gates it on use_temporal_coherence. The node
always sets base_seed, so that generator ignored its seed argument entirely:
every stage and every decorrelation draw produced the identical field, and only
`time` still varied. It now matches domain_warp. Nothing could select
temporal_coherent from a workflow until the previous commit, so no saved
workflow changes; the 11 legacy goldens still pass.
Exposed as an optional `decorrelate_channels` input, default off. 23 new tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>