low pri features
This commit is contained in:
152
backend/nodes/synthetic_surface.py
Normal file
152
backend/nodes/synthetic_surface.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""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
|
||||
|
||||
|
||||
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 # avoid division by zero
|
||||
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)
|
||||
surface = np.real(np.fft.ifft2(fft_data))
|
||||
return surface
|
||||
|
||||
|
||||
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
|
||||
surface = np.cos(k * X) + np.cos(k * (X * np.cos(theta) + Y * np.sin(theta)))
|
||||
return surface
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
@register_node(display_name="Synthetic Surface")
|
||||
class SyntheticSurface:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"pattern": (["fbm", "white_noise", "lattice", "steps", "particles", "flat"],
|
||||
{"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}),
|
||||
}
|
||||
}
|
||||
|
||||
OUTPUTS = (
|
||||
('DATA_FIELD', 'surface'),
|
||||
)
|
||||
FUNCTION = "process"
|
||||
|
||||
DESCRIPTION = (
|
||||
"Generate synthetic test surfaces for development, calibration, and "
|
||||
"algorithm testing. Patterns: fbm (fractional Brownian motion), "
|
||||
"white_noise, lattice (periodic grid), steps (terraced), "
|
||||
"particles (spherical bumps on flat), flat (zero surface). "
|
||||
"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,
|
||||
) -> 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)
|
||||
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,)
|
||||
Reference in New Issue
Block a user