233 lines
8.2 KiB
Python
233 lines
8.2 KiB
Python
import numpy as np
|
|
import pytest
|
|
from tests.node_tests._shared import make_field
|
|
|
|
|
|
# All 28 patterns supported by SyntheticSurface
|
|
ALL_PATTERNS = [
|
|
"fbm", "white_noise", "lattice", "steps", "particles", "flat",
|
|
"columnar", "objects", "fibres", "waves", "dunes",
|
|
"domains", "ballistic", "deposition", "rods", "dla",
|
|
"discs", "plateaus", "pileups", "annealing", "voronoi",
|
|
"spinodal", "pde", "spectral", "residues",
|
|
"noise", "periodic", "wfr",
|
|
]
|
|
|
|
# Iterative patterns that need n_iterations kept low for speed
|
|
ITERATIVE_PATTERNS = {"domains", "ballistic", "annealing", "spinodal", "pde", "dla"}
|
|
|
|
|
|
def _make_node():
|
|
from backend.nodes.synthetic_surface import SyntheticSurface
|
|
return SyntheticSurface()
|
|
|
|
|
|
def _common_kwargs(pattern, xres=64, yres=64):
|
|
"""Build kwargs dict with low iteration count for iterative patterns."""
|
|
kwargs = dict(
|
|
pattern=pattern,
|
|
xres=xres,
|
|
yres=yres,
|
|
xreal=1e-6,
|
|
yreal=1e-6,
|
|
amplitude=1e-9,
|
|
seed=42,
|
|
)
|
|
if pattern in ITERATIVE_PATTERNS:
|
|
kwargs["n_iterations"] = 20
|
|
return kwargs
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. Every pattern produces the correct output shape
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.mark.parametrize("pattern", ALL_PATTERNS)
|
|
def test_all_patterns_produce_correct_shape(pattern):
|
|
node = _make_node()
|
|
kwargs = _common_kwargs(pattern, xres=64, yres=64)
|
|
(field,) = node.process(**kwargs)
|
|
assert field.data.shape == (64, 64), (
|
|
f"Pattern {pattern!r} produced shape {field.data.shape}, expected (64, 64)"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2. Seed reproducibility
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_seed_reproducibility():
|
|
node = _make_node()
|
|
kwargs = dict(
|
|
pattern="fbm", xres=64, yres=64,
|
|
xreal=1e-6, yreal=1e-6, amplitude=1e-9, seed=123,
|
|
)
|
|
(r1,) = node.process(**kwargs)
|
|
(r2,) = node.process(**kwargs)
|
|
assert np.array_equal(r1.data, r2.data), "Same seed must produce identical output"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. Amplitude scaling
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_amplitude_scaling():
|
|
node = _make_node()
|
|
amp = 5e-9
|
|
(field,) = node.process(
|
|
pattern="fbm", xres=64, yres=64,
|
|
xreal=1e-6, yreal=1e-6, amplitude=amp, seed=42,
|
|
)
|
|
# After normalisation the range should be [0, amplitude]
|
|
assert field.data.min() >= -1e-15, "Min value should be >= 0 (within tolerance)"
|
|
assert field.data.max() <= amp + 1e-15, (
|
|
f"Max value {field.data.max()} exceeds amplitude {amp}"
|
|
)
|
|
# The range should actually span close to the full amplitude
|
|
assert field.data.max() - field.data.min() == pytest.approx(amp, rel=1e-6)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 4. Flat pattern produces all zeros
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_flat_is_zero():
|
|
node = _make_node()
|
|
(field,) = node.process(
|
|
pattern="flat", xres=64, yres=64,
|
|
xreal=1e-6, yreal=1e-6, amplitude=1e-9, seed=0,
|
|
)
|
|
assert np.allclose(field.data, 0.0), "Flat pattern must produce all zeros"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 5. Object shapes
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.mark.parametrize("obj_shape", ["sphere", "pyramid", "box", "cylinder", "cone"])
|
|
def test_objects_shapes(obj_shape):
|
|
node = _make_node()
|
|
(field,) = node.process(
|
|
pattern="objects", xres=64, yres=64,
|
|
xreal=1e-6, yreal=1e-6, amplitude=1.0, seed=42,
|
|
n_particles=5, particle_radius_px=5, object_shape=obj_shape,
|
|
)
|
|
assert field.data.shape == (64, 64), (
|
|
f"Object shape {obj_shape!r} produced wrong output shape"
|
|
)
|
|
assert np.any(field.data != 0.0), (
|
|
f"Object shape {obj_shape!r} should not be all zeros"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 6. Noise types
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.mark.parametrize("noise_type", [
|
|
"gaussian", "poisson", "exponential", "uniform", "salt_pepper",
|
|
])
|
|
def test_noise_types(noise_type):
|
|
node = _make_node()
|
|
(field,) = node.process(
|
|
pattern="noise", xres=64, yres=64,
|
|
xreal=1e-6, yreal=1e-6, amplitude=1e-9, seed=42,
|
|
noise_type=noise_type,
|
|
)
|
|
assert field.data.shape == (64, 64), (
|
|
f"Noise type {noise_type!r} produced wrong shape"
|
|
)
|
|
assert np.all(np.isfinite(field.data)), (
|
|
f"Noise type {noise_type!r} produced non-finite values"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 7. Periodic types
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.mark.parametrize("periodic_type", [
|
|
"checker", "hex", "stripe", "diamond", "staircase", "rings",
|
|
])
|
|
def test_periodic_types(periodic_type):
|
|
node = _make_node()
|
|
(field,) = node.process(
|
|
pattern="periodic", xres=64, yres=64,
|
|
xreal=1e-6, yreal=1e-6, amplitude=1e-9, seed=42,
|
|
periodic_type=periodic_type,
|
|
)
|
|
assert field.data.shape == (64, 64), (
|
|
f"Periodic type {periodic_type!r} produced wrong shape"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 8. Spinodal converges to bimodal distribution
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_spinodal_converges():
|
|
node = _make_node()
|
|
(field,) = node.process(
|
|
pattern="spinodal", xres=64, yres=64,
|
|
xreal=1e-6, yreal=1e-6, amplitude=1e-9, seed=42,
|
|
n_iterations=50,
|
|
)
|
|
data = field.data
|
|
mid = (data.min() + data.max()) / 2.0
|
|
span = data.max() - data.min()
|
|
# In a bimodal distribution most values cluster near min or max,
|
|
# so few values should be in the middle third.
|
|
middle_band = np.abs(data - mid) < span / 6.0
|
|
fraction_in_middle = middle_band.sum() / data.size
|
|
assert fraction_in_middle < 0.5, (
|
|
f"Expected bimodal distribution but {fraction_in_middle:.1%} of values "
|
|
f"are in the middle band"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 9. Voronoi produces discrete height levels
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_voronoi_discrete_heights():
|
|
node = _make_node()
|
|
n_sites = 5
|
|
(field,) = node.process(
|
|
pattern="voronoi", xres=64, yres=64,
|
|
xreal=1e-6, yreal=1e-6, amplitude=1e-9, seed=42,
|
|
n_particles=n_sites,
|
|
)
|
|
# Round to avoid floating-point noise and count unique levels
|
|
unique_levels = np.unique(np.round(field.data, decimals=12))
|
|
assert len(unique_levels) <= n_sites, (
|
|
f"Voronoi with {n_sites} sites produced {len(unique_levels)} distinct "
|
|
f"height levels, expected at most {n_sites}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 10. Steps count matches n_steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_steps_count():
|
|
node = _make_node()
|
|
n_steps = 3
|
|
(field,) = node.process(
|
|
pattern="steps", xres=64, yres=64,
|
|
xreal=1e-6, yreal=1e-6, amplitude=1e-9, seed=42,
|
|
n_steps=n_steps,
|
|
)
|
|
# The steps generator creates floor(linspace(0, n_steps, xres)) values.
|
|
# After normalisation, there should be approximately n_steps distinct
|
|
# non-zero step levels (plus possibly zero).
|
|
unique_levels = np.unique(np.round(field.data, decimals=12))
|
|
# Allow for the zero level plus n_steps levels
|
|
assert len(unique_levels) <= n_steps + 1, (
|
|
f"Steps with n_steps={n_steps} produced {len(unique_levels)} distinct "
|
|
f"levels, expected at most {n_steps + 1}"
|
|
)
|
|
# Should have at least 2 distinct levels (not a flat surface)
|
|
assert len(unique_levels) >= 2, (
|
|
f"Steps pattern should produce multiple distinct levels, got {len(unique_levels)}"
|
|
)
|