Files
tono/backend/nodes/synthetic_surface.py

609 lines
24 KiB
Python

"""Synthetic surface generation — create test surfaces for development and calibration."""
from __future__ import annotations
import numpy as np
from backend.node_registry import register_node
from backend.data_types import DataField
# ---------------------------------------------------------------------------
# Original generators
# ---------------------------------------------------------------------------
def _fbm_surface(shape, rng, H=0.7):
"""Fractional Brownian motion surface via spectral synthesis."""
yres, xres = shape
kx = np.fft.fftfreq(xres)
ky = np.fft.fftfreq(yres)
KX, KY = np.meshgrid(kx, ky)
K = np.sqrt(KX**2 + KY**2)
K[0, 0] = 1.0
power = K ** (-(H + 1.0))
power[0, 0] = 0.0
phases = rng.uniform(0, 2 * np.pi, shape)
amplitudes = rng.standard_normal(shape)
fft_data = amplitudes * np.sqrt(power) * np.exp(1j * phases)
return np.real(np.fft.ifft2(fft_data))
def _lattice_surface(shape, xreal, yreal, spacing, angle_deg):
"""Periodic lattice (sinusoidal grid)."""
yres, xres = shape
x = np.linspace(0, xreal, xres, endpoint=False)
y = np.linspace(0, yreal, yres, endpoint=False)
X, Y = np.meshgrid(x, y)
theta = np.radians(angle_deg)
k = 2 * np.pi / spacing
return np.cos(k * X) + np.cos(k * (X * np.cos(theta) + Y * np.sin(theta)))
def _steps_surface(shape, n_steps):
"""Terraced step structure."""
yres, xres = shape
ramp = np.linspace(0, n_steps, xres, endpoint=False)
steps = np.floor(ramp)
return np.tile(steps, (yres, 1)).astype(np.float64)
def _particles_surface(shape, rng, n_particles, radius_px):
"""Random spherical particles on a flat background."""
yres, xres = shape
surface = np.zeros(shape)
yy, xx = np.ogrid[:yres, :xres]
for _ in range(n_particles):
cy = rng.integers(0, yres)
cx = rng.integers(0, xres)
r2 = (yy - cy)**2 + (xx - cx)**2
height = np.sqrt(np.maximum(radius_px**2 - r2, 0.0))
surface = np.maximum(surface, height)
return surface
# ---------------------------------------------------------------------------
# New generators
# ---------------------------------------------------------------------------
def _columnar_surface(shape, rng, n, radius):
"""Columnar growth — Gaussian pillars at random positions."""
surface = np.zeros(shape)
yy, xx = np.ogrid[:shape[0], :shape[1]]
sigma2 = max(1.0, float(radius) ** 2)
for _ in range(n):
cy, cx = rng.integers(0, shape[0]), rng.integers(0, shape[1])
h = rng.uniform(0.3, 1.0)
r2 = (yy - cy) ** 2 + (xx - cx) ** 2
surface += h * np.exp(-r2 / (2.0 * sigma2))
return surface
def _objects_surface(shape, rng, n, size, obj_shape):
"""Random geometric objects (sphere, pyramid, box, cylinder, cone)."""
surface = np.zeros(shape)
yy, xx = np.ogrid[:shape[0], :shape[1]]
s = max(float(size), 1.0)
for _ in range(n):
cy, cx = rng.integers(0, shape[0]), rng.integers(0, shape[1])
h = rng.uniform(0.5, 1.0)
dy = (yy - cy).astype(np.float64)
dx = (xx - cx).astype(np.float64)
r = np.sqrt(dy ** 2 + dx ** 2)
if obj_shape == "pyramid":
bump = np.maximum(1.0 - np.maximum(np.abs(dy), np.abs(dx)) / s, 0.0)
elif obj_shape == "box":
bump = ((np.abs(dy) <= s) & (np.abs(dx) <= s)).astype(np.float64)
elif obj_shape == "cylinder":
bump = (r <= s).astype(np.float64)
elif obj_shape == "cone":
bump = np.maximum(1.0 - r / s, 0.0)
else: # sphere
bump = np.sqrt(np.maximum(s ** 2 - dy ** 2 - dx ** 2, 0.0)) / s
surface = np.maximum(surface, h * bump)
return surface
def _fibres_surface(shape, rng, n, length, width):
"""Randomly oriented fibre/line features."""
yres, xres = shape
surface = np.zeros(shape)
yy, xx = np.mgrid[:yres, :xres]
for _ in range(n):
cy, cx = rng.uniform(0, yres), rng.uniform(0, xres)
angle = rng.uniform(0, np.pi)
h = rng.uniform(0.5, 1.0)
cos_a, sin_a = np.cos(angle), np.sin(angle)
along = (xx - cx) * cos_a + (yy - cy) * sin_a
across = -(xx - cx) * sin_a + (yy - cy) * cos_a
mask = (np.abs(along) <= length / 2) & (np.abs(across) <= width)
surface = np.maximum(surface, h * mask.astype(np.float64))
return surface
def _waves_surface(shape, rng, n_sources, frequency):
"""Superposition of decaying circular waves from random sources."""
yres, xres = shape
xn = np.arange(xres, dtype=np.float64) / max(xres - 1, 1)
yn = np.arange(yres, dtype=np.float64) / max(yres - 1, 1)
X, Y = np.meshgrid(xn, yn)
surface = np.zeros(shape)
for _ in range(n_sources):
sx, sy = rng.random(), rng.random()
amp = rng.uniform(0.5, 1.0)
r = np.sqrt((X - sx) ** 2 + (Y - sy) ** 2)
surface += amp * np.exp(-3.0 * r) * np.cos(2 * np.pi * frequency * r)
return surface
def _dunes_surface(shape, rng, frequency, direction_deg):
"""Asymmetric dune-like rippled surface."""
yres, xres = shape
theta = np.radians(direction_deg)
xn = np.arange(xres, dtype=np.float64) / max(xres - 1, 1)
yn = np.arange(yres, dtype=np.float64) / max(yres - 1, 1)
X, Y = np.meshgrid(xn, yn)
phase = frequency * (X * np.cos(theta) + Y * np.sin(theta))
frac = phase - np.floor(phase)
profile = np.where(frac < 0.7, frac / 0.7, (1.0 - frac) / 0.3)
return profile + rng.standard_normal(shape) * 0.03
def _domains_surface(shape, rng, n_iterations):
"""Phase-separated domains via 2D Ising model (checkerboard Metropolis)."""
yres, xres = shape
spins = rng.choice([-1.0, 1.0], size=shape)
beta = 0.55
y, x = np.ogrid[:yres, :xres]
for _ in range(n_iterations):
for parity in range(2):
mask = ((y + x) % 2 == parity)
neighbors = (np.roll(spins, 1, axis=0) + np.roll(spins, -1, axis=0) +
np.roll(spins, 1, axis=1) + np.roll(spins, -1, axis=1))
dE = 2.0 * spins * neighbors
flip = (dE <= 0) | (rng.random(shape) < np.exp(np.minimum(-beta * dE, 0.0)))
spins = np.where(mask & flip, -spins, spins)
return spins
def _ballistic_surface(shape, rng, n_iterations):
"""Ballistic deposition with neighbor adhesion (vectorised)."""
heights = np.zeros(shape)
for _ in range(n_iterations):
drops = rng.random(shape) > 0.7
padded = np.pad(heights, 1, mode='wrap')
neighbor_max = np.maximum.reduce([
padded[:-2, 1:-1], padded[2:, 1:-1],
padded[1:-1, :-2], padded[1:-1, 2:],
])
heights = np.where(drops, np.maximum(heights, neighbor_max) + 1, heights)
return heights
def _deposition_surface(shape, rng, n, radius):
"""Particle stacking — spheres deposited with gravity."""
surface = np.zeros(shape)
yy, xx = np.ogrid[:shape[0], :shape[1]]
for _ in range(n):
cy, cx = rng.integers(0, shape[0]), rng.integers(0, shape[1])
r2 = ((yy - cy) ** 2 + (xx - cx) ** 2).astype(np.float64)
h_sphere = np.sqrt(np.maximum(float(radius) ** 2 - r2, 0.0))
footprint = h_sphere > 0
base = float(surface[footprint].max()) if footprint.any() else 0.0
surface = np.maximum(surface, base + h_sphere)
return surface
def _rods_surface(shape, rng, n, length, width):
"""Rod/wire features with rounded (semicircular) cross-section."""
yres, xres = shape
surface = np.zeros(shape)
yy, xx = np.mgrid[:yres, :xres]
w = max(float(width), 1.0)
for _ in range(n):
cy, cx = rng.uniform(0, yres), rng.uniform(0, xres)
angle = rng.uniform(0, np.pi)
h = rng.uniform(0.5, 1.0)
cos_a, sin_a = np.cos(angle), np.sin(angle)
along = (xx - cx) * cos_a + (yy - cy) * sin_a
across = (-(xx - cx) * sin_a + (yy - cy) * cos_a).astype(np.float64)
in_rod = (np.abs(along) <= length / 2).astype(np.float64)
profile = np.sqrt(np.maximum(w ** 2 - across ** 2, 0.0)) / w
surface = np.maximum(surface, h * profile * in_rod)
return surface
def _dla_surface(shape, rng, n_iterations):
"""Diffusion-limited aggregation via iterative boundary growth."""
from scipy.ndimage import binary_dilation
grid = np.zeros(shape)
grid[shape[0] // 2, shape[1] // 2] = 1.0
struct = np.ones((3, 3), dtype=bool)
for _ in range(n_iterations):
dilated = binary_dilation(grid > 0, structure=struct)
boundary = dilated & (grid == 0)
candidates = np.argwhere(boundary)
if len(candidates) == 0:
break
n_add = max(1, len(candidates) // 8)
chosen = rng.choice(len(candidates), size=min(n_add, len(candidates)),
replace=False)
for idx in chosen:
grid[candidates[idx][0], candidates[idx][1]] = rng.uniform(0.5, 1.0)
return grid
def _discs_surface(shape, rng, n, radius):
"""Flat-topped circular disc features."""
surface = np.zeros(shape)
yy, xx = np.ogrid[:shape[0], :shape[1]]
for _ in range(n):
cy, cx = rng.integers(0, shape[0]), rng.integers(0, shape[1])
h = rng.uniform(0.5, 1.0)
r = np.sqrt(((yy - cy) ** 2 + (xx - cx) ** 2).astype(np.float64))
surface = np.maximum(surface, h * (r <= radius).astype(np.float64))
return surface
def _plateaus_surface(shape, rng, n, radius):
"""Flat-topped features with smooth (tanh) edges."""
surface = np.zeros(shape)
yy, xx = np.ogrid[:shape[0], :shape[1]]
for _ in range(n):
cy, cx = rng.integers(0, shape[0]), rng.integers(0, shape[1])
h = rng.uniform(0.5, 1.0)
r = np.sqrt(((yy - cy) ** 2 + (xx - cx) ** 2).astype(np.float64))
edge_w = max(float(radius) * 0.2, 1.0)
bump = h * 0.5 * (1.0 - np.tanh(3.0 * (r - radius) / edge_w))
surface = np.maximum(surface, np.maximum(bump, 0.0))
return surface
def _pileups_surface(shape, rng, n, size):
"""Rounded rectangle pileup structures."""
surface = np.zeros(shape)
yy, xx = np.ogrid[:shape[0], :shape[1]]
s = max(float(size), 1.0)
for _ in range(n):
cy, cx = rng.integers(0, shape[0]), rng.integers(0, shape[1])
h = rng.uniform(0.5, 1.0)
aspect = rng.uniform(0.5, 2.0)
angle = rng.uniform(0, np.pi)
cos_a, sin_a = np.cos(angle), np.sin(angle)
dx = ((xx - cx) * cos_a + (yy - cy) * sin_a).astype(np.float64)
dy = (-(xx - cx) * sin_a + (yy - cy) * cos_a).astype(np.float64)
w, ht = s * aspect, s / aspect
r = ((np.abs(dx) / max(w, 1.0)) ** 4 + (np.abs(dy) / max(ht, 1.0)) ** 4) ** 0.25
surface = np.maximum(surface, h * np.maximum(1.0 - r, 0.0))
return surface
def _annealing_surface(shape, rng, n_iterations):
"""Surface relaxation via simulated annealing (terrain smoothing)."""
surface = rng.standard_normal(shape)
for i in range(n_iterations):
t = max(0.01, 1.0 - i / n_iterations)
avg = (np.roll(surface, 1, 0) + np.roll(surface, -1, 0) +
np.roll(surface, 1, 1) + np.roll(surface, -1, 1)) / 4.0
surface += 0.2 * (avg - surface)
surface += rng.standard_normal(shape) * t * 0.02
return surface
def _voronoi_surface(shape, rng, n_sites):
"""Voronoi tessellation with random heights per cell."""
yres, xres = shape
sites_y = rng.uniform(0, yres, size=n_sites)
sites_x = rng.uniform(0, xres, size=n_sites)
heights = rng.uniform(0, 1, size=n_sites)
yy, xx = np.mgrid[:yres, :xres]
surface = np.zeros(shape)
min_dist = np.full(shape, np.inf)
for i in range(n_sites):
dist = (yy - sites_y[i]) ** 2 + (xx - sites_x[i]) ** 2
closer = dist < min_dist
surface = np.where(closer, heights[i], surface)
min_dist = np.where(closer, dist, min_dist)
return surface
def _spinodal_surface(shape, rng, n_iterations):
"""Spinodal decomposition via Cahn-Hilliard equation (FFT-based)."""
yres, xres = shape
c = 0.5 + 0.05 * rng.standard_normal(shape)
kx = np.fft.fftfreq(xres) * 2 * np.pi
ky = np.fft.fftfreq(yres) * 2 * np.pi
KX, KY = np.meshgrid(kx, ky)
K2 = KX ** 2 + KY ** 2
dt, eps2 = 0.5, 0.01
denom = 1.0 + dt * eps2 * K2 ** 2
for _ in range(n_iterations):
mu_hat = np.fft.fft2(c ** 3 - c)
c_hat = np.fft.fft2(c)
c_hat = (c_hat - dt * K2 * mu_hat) / denom
c = np.real(np.fft.ifft2(c_hat))
np.clip(c, -2.0, 2.0, out=c)
return c
def _pde_surface(shape, rng, n_iterations):
"""Gray-Scott reaction-diffusion Turing patterns."""
Du, Dv, F, k = 0.16, 0.08, 0.035, 0.065
u = np.ones(shape)
v = np.zeros(shape)
r = min(shape[0], shape[1]) // 8
cy, cx = shape[0] // 2, shape[1] // 2
y0, y1 = max(0, cy - r), min(shape[0], cy + r)
x0, x1 = max(0, cx - r), min(shape[1], cx + r)
seed_shape = (y1 - y0, x1 - x0)
u[y0:y1, x0:x1] = 0.5 + 0.1 * rng.standard_normal(seed_shape)
v[y0:y1, x0:x1] = 0.25 + 0.1 * rng.standard_normal(seed_shape)
for _ in range(n_iterations):
lu = (np.roll(u, 1, 0) + np.roll(u, -1, 0) +
np.roll(u, 1, 1) + np.roll(u, -1, 1) - 4 * u)
lv = (np.roll(v, 1, 0) + np.roll(v, -1, 0) +
np.roll(v, 1, 1) + np.roll(v, -1, 1) - 4 * v)
uvv = u * v * v
u += Du * lu - uvv + F * (1.0 - u)
v += Dv * lv + uvv - (F + k) * v
return v
def _spectral_surface(shape, rng, exponent):
"""FFT with power-law spectrum: P(k) proportional to k^(-exponent)."""
yres, xres = shape
kx = np.fft.fftfreq(xres)
ky = np.fft.fftfreq(yres)
KX, KY = np.meshgrid(kx, ky)
K = np.sqrt(KX ** 2 + KY ** 2)
K[0, 0] = 1.0
power = K ** (-exponent)
power[0, 0] = 0.0
phases = rng.uniform(0, 2 * np.pi, shape)
magnitudes = rng.standard_normal(shape)
fft_data = magnitudes * np.sqrt(power) * np.exp(1j * phases)
return np.real(np.fft.ifft2(fft_data))
def _residues_surface(shape, rng, n, size):
"""Irregular elliptical deposits with random orientation."""
surface = np.zeros(shape)
yy, xx = np.ogrid[:shape[0], :shape[1]]
for _ in range(n):
cy, cx = rng.integers(0, shape[0]), rng.integers(0, shape[1])
h = rng.uniform(0.3, 1.0)
aspect = rng.uniform(0.3, 3.0)
angle = rng.uniform(0, np.pi)
cos_a, sin_a = np.cos(angle), np.sin(angle)
dx = ((xx - cx) * cos_a + (yy - cy) * sin_a).astype(np.float64)
dy = (-(xx - cx) * sin_a + (yy - cy) * cos_a).astype(np.float64)
sx = max(size * aspect, 1.0)
sy = max(size / aspect, 1.0)
bump = h * np.exp(-2.0 * ((dx / sx) ** 2 + (dy / sy) ** 2))
surface = np.maximum(surface, bump)
return surface
def _noise_surface(shape, rng, noise_type):
"""Various noise distributions."""
if noise_type == "poisson":
return rng.poisson(lam=5.0, size=shape).astype(np.float64)
elif noise_type == "exponential":
return rng.exponential(scale=1.0, size=shape)
elif noise_type == "uniform":
return rng.uniform(0, 1, size=shape)
elif noise_type == "salt_pepper":
base = np.zeros(shape)
base[rng.random(shape) > 0.95] = 1.0
base[rng.random(shape) > 0.95] = -1.0
return base
return rng.standard_normal(shape) # gaussian default
def _periodic_surface(shape, frequency, periodic_type):
"""Repeating tiling patterns."""
yres, xres = shape
xn = np.arange(xres, dtype=np.float64) / max(xres - 1, 1)
yn = np.arange(yres, dtype=np.float64) / max(yres - 1, 1)
X, Y = np.meshgrid(xn, yn)
f = max(frequency, 0.1)
if periodic_type == "hex":
s = 1.0 / f
r = np.sqrt(3) / 2
cy = Y / (s * r)
row = np.floor(cy)
shift = (row % 2) * 0.5
col = np.floor(X / s + shift)
hx = (col - shift) * s
hy = row * s * r
return (np.sqrt((X - hx) ** 2 + (Y - hy) ** 2) < s * 0.35).astype(np.float64)
elif periodic_type == "stripe":
return (np.sin(2 * np.pi * f * X) > 0).astype(np.float64)
elif periodic_type == "diamond":
u = np.floor(f * (X + Y))
v = np.floor(f * (X - Y))
return ((u + v) % 2).astype(np.float64)
elif periodic_type == "staircase":
return np.floor(X * f * 2) / max(f, 0.1)
elif periodic_type == "rings":
r = np.sqrt((X - 0.5) ** 2 + (Y - 0.5) ** 2)
return (np.sin(2 * np.pi * f * r * 4) > 0).astype(np.float64)
# checker (default)
return ((np.floor(X * f * 2) + np.floor(Y * f * 2)) % 2).astype(np.float64)
def _wfr_surface(shape, rng, n_sources, frequency):
"""Concentric wavefronts (ripples) from random sources — no decay."""
yres, xres = shape
xn = np.arange(xres, dtype=np.float64) / max(xres - 1, 1)
yn = np.arange(yres, dtype=np.float64) / max(yres - 1, 1)
X, Y = np.meshgrid(xn, yn)
surface = np.zeros(shape)
for _ in range(n_sources):
sx, sy = rng.random(), rng.random()
r = np.sqrt((X - sx) ** 2 + (Y - sy) ** 2)
surface += np.cos(2 * np.pi * frequency * r)
return surface / max(n_sources, 1)
# ---------------------------------------------------------------------------
# Node class
# ---------------------------------------------------------------------------
@register_node(display_name="Synthetic Surface")
class SyntheticSurface:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"pattern": ([
"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",
], {"default": "fbm"}),
"xres": ("INT", {"default": 256, "min": 16, "max": 2048}),
"yres": ("INT", {"default": 256, "min": 16, "max": 2048}),
"xreal": ("FLOAT", {"default": 1e-6, "min": 1e-9, "max": 1.0, "step": 1e-9}),
"yreal": ("FLOAT", {"default": 1e-6, "min": 1e-9, "max": 1.0, "step": 1e-9}),
"amplitude": ("FLOAT", {"default": 1e-9, "min": 0.0, "max": 1e-3, "step": 1e-10}),
"seed": ("INT", {"default": 42, "min": 0, "max": 999999}),
},
"optional": {
"hurst_exponent": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 1.0, "step": 0.05}),
"lattice_spacing": ("FLOAT", {
"default": 100e-9, "min": 1e-9, "max": 1e-3, "step": 1e-9,
}),
"lattice_angle": ("FLOAT", {"default": 90.0, "min": 0.0, "max": 180.0, "step": 1.0}),
"n_steps": ("INT", {"default": 5, "min": 1, "max": 100}),
"n_particles": ("INT", {"default": 20, "min": 1, "max": 500}),
"particle_radius_px": ("INT", {"default": 10, "min": 2, "max": 100}),
"n_iterations": ("INT", {"default": 200, "min": 10, "max": 5000}),
"direction_deg": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 360.0, "step": 1.0}),
"feature_length_px": ("INT", {"default": 40, "min": 2, "max": 500}),
"object_shape": (["sphere", "pyramid", "box", "cylinder", "cone"],
{"default": "sphere"}),
"noise_type": (["gaussian", "poisson", "exponential", "uniform", "salt_pepper"],
{"default": "gaussian"}),
"periodic_type": (["checker", "hex", "stripe", "diamond", "staircase", "rings"],
{"default": "checker"}),
"spectral_exponent": ("FLOAT", {"default": 2.0, "min": 0.5, "max": 5.0, "step": 0.1}),
"frequency": ("FLOAT", {"default": 5.0, "min": 0.5, "max": 50.0, "step": 0.5}),
}
}
OUTPUTS = (
('DATA_FIELD', 'surface'),
)
FUNCTION = "process"
DESCRIPTION = (
"Generate synthetic test surfaces for development, calibration, and "
"algorithm testing. 28 patterns covering noise, geometry, growth "
"simulations, phase separation, reaction-diffusion, and tiling. "
"Equivalent to Gwyddion's *_synth.c modules."
)
def process(
self,
pattern: str,
xres: int,
yres: int,
xreal: float,
yreal: float,
amplitude: float,
seed: int,
hurst_exponent: float = 0.7,
lattice_spacing: float = 100e-9,
lattice_angle: float = 90.0,
n_steps: int = 5,
n_particles: int = 20,
particle_radius_px: int = 10,
n_iterations: int = 200,
direction_deg: float = 0.0,
feature_length_px: int = 40,
object_shape: str = "sphere",
noise_type: str = "gaussian",
periodic_type: str = "checker",
spectral_exponent: float = 2.0,
frequency: float = 5.0,
) -> tuple:
shape = (yres, xres)
rng = np.random.default_rng(seed)
if pattern == "fbm":
data = _fbm_surface(shape, rng, H=hurst_exponent)
elif pattern == "white_noise":
data = rng.standard_normal(shape)
elif pattern == "lattice":
data = _lattice_surface(shape, xreal, yreal, lattice_spacing, lattice_angle)
elif pattern == "steps":
data = _steps_surface(shape, n_steps)
elif pattern == "particles":
data = _particles_surface(shape, rng, n_particles, particle_radius_px)
elif pattern == "flat":
data = np.zeros(shape)
elif pattern == "columnar":
data = _columnar_surface(shape, rng, n_particles, particle_radius_px)
elif pattern == "objects":
data = _objects_surface(shape, rng, n_particles, particle_radius_px, object_shape)
elif pattern == "fibres":
data = _fibres_surface(shape, rng, n_particles, feature_length_px, particle_radius_px)
elif pattern == "waves":
data = _waves_surface(shape, rng, n_particles, frequency)
elif pattern == "dunes":
data = _dunes_surface(shape, rng, frequency, direction_deg)
elif pattern == "domains":
data = _domains_surface(shape, rng, n_iterations)
elif pattern == "ballistic":
data = _ballistic_surface(shape, rng, n_iterations)
elif pattern == "deposition":
data = _deposition_surface(shape, rng, n_particles, particle_radius_px)
elif pattern == "rods":
data = _rods_surface(shape, rng, n_particles, feature_length_px, particle_radius_px)
elif pattern == "dla":
data = _dla_surface(shape, rng, n_iterations)
elif pattern == "discs":
data = _discs_surface(shape, rng, n_particles, particle_radius_px)
elif pattern == "plateaus":
data = _plateaus_surface(shape, rng, n_particles, particle_radius_px)
elif pattern == "pileups":
data = _pileups_surface(shape, rng, n_particles, particle_radius_px)
elif pattern == "annealing":
data = _annealing_surface(shape, rng, n_iterations)
elif pattern == "voronoi":
data = _voronoi_surface(shape, rng, n_particles)
elif pattern == "spinodal":
data = _spinodal_surface(shape, rng, n_iterations)
elif pattern == "pde":
data = _pde_surface(shape, rng, n_iterations)
elif pattern == "spectral":
data = _spectral_surface(shape, rng, spectral_exponent)
elif pattern == "residues":
data = _residues_surface(shape, rng, n_particles, particle_radius_px)
elif pattern == "noise":
data = _noise_surface(shape, rng, noise_type)
elif pattern == "periodic":
data = _periodic_surface(shape, frequency, periodic_type)
elif pattern == "wfr":
data = _wfr_surface(shape, rng, n_particles, frequency)
else:
raise ValueError(f"Unknown pattern: {pattern!r}")
# Normalise and scale by amplitude
drange = data.max() - data.min()
if drange > 0:
data = (data - data.min()) / drange * amplitude
else:
data = np.zeros(shape)
field = DataField(
data=data,
xreal=xreal,
yreal=yreal,
si_unit_xy="m",
si_unit_z="m",
)
return (field,)