synthetic surface and spm specific features
This commit is contained in:
64
tests/node_tests/lateral_force_sim.py
Normal file
64
tests/node_tests/lateral_force_sim.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from tests.node_tests._shared import make_field
|
||||
|
||||
|
||||
def test_output_shapes():
|
||||
"""Both forward and reverse outputs must have the same shape as the input."""
|
||||
from backend.nodes.lateral_force_sim import LateralForceSim
|
||||
|
||||
node = LateralForceSim()
|
||||
field = make_field(shape=(48, 64))
|
||||
|
||||
for direction in ("forward", "reverse", "both"):
|
||||
fwd, rev = node.process(field, direction, 0.3, 1e-9, 10e-9)
|
||||
assert fwd.data.shape == field.data.shape, f"forward shape mismatch for direction={direction}"
|
||||
assert rev.data.shape == field.data.shape, f"reverse shape mismatch for direction={direction}"
|
||||
|
||||
|
||||
def test_flat_surface_uniform():
|
||||
"""A flat (constant) topography has zero slope everywhere, so the lateral
|
||||
force should be spatially uniform (pure friction, no topographic component)."""
|
||||
from backend.nodes.lateral_force_sim import LateralForceSim
|
||||
|
||||
node = LateralForceSim()
|
||||
data = np.full((32, 32), 5e-9, dtype=np.float64)
|
||||
field = make_field(data=data)
|
||||
|
||||
fwd, rev = node.process(field, "both", 0.3, 1e-9, 10e-9)
|
||||
|
||||
# All values in each output should be identical (uniform)
|
||||
assert np.ptp(fwd.data) < 1e-20, "Forward lateral force is not uniform on flat surface"
|
||||
assert np.ptp(rev.data) < 1e-20, "Reverse lateral force is not uniform on flat surface"
|
||||
|
||||
|
||||
def test_forward_reverse_different():
|
||||
"""For a non-flat surface with 'both' direction, forward and reverse
|
||||
lateral force signals should differ (topographic artifact is direction-dependent)."""
|
||||
from backend.nodes.lateral_force_sim import LateralForceSim
|
||||
|
||||
node = LateralForceSim()
|
||||
# Create a steep ramp in x so there is a significant slope
|
||||
ramp = np.tile(np.linspace(0.0, 500e-9, 64), (64, 1))
|
||||
field = make_field(data=ramp)
|
||||
|
||||
fwd, rev = node.process(field, "both", 0.3, 1e-9, 10e-9)
|
||||
|
||||
# Forward and reverse differ due to slope-dependent asymmetry;
|
||||
# use strict tolerance to detect the difference at nanoNewton scale.
|
||||
assert not np.allclose(fwd.data, rev.data, atol=0, rtol=1e-6), (
|
||||
"Forward and reverse should differ on a sloped surface"
|
||||
)
|
||||
|
||||
|
||||
def test_finite_values():
|
||||
"""All output values must be finite (no NaN or inf) for typical inputs."""
|
||||
from backend.nodes.lateral_force_sim import LateralForceSim
|
||||
|
||||
node = LateralForceSim()
|
||||
field = make_field() # random topography
|
||||
|
||||
for direction in ("forward", "reverse", "both"):
|
||||
fwd, rev = node.process(field, direction, 0.3, 1e-9, 10e-9)
|
||||
assert np.isfinite(fwd.data).all(), f"Non-finite values in forward output (direction={direction})"
|
||||
assert np.isfinite(rev.data).all(), f"Non-finite values in reverse output (direction={direction})"
|
||||
70
tests/node_tests/mfm_current.py
Normal file
70
tests/node_tests/mfm_current.py
Normal file
@@ -0,0 +1,70 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from tests.node_tests._shared import make_field
|
||||
|
||||
|
||||
def test_output_shapes():
|
||||
from backend.nodes.mfm_current import MFMCurrentSimulation
|
||||
|
||||
node = MFMCurrentSimulation()
|
||||
field = make_field(shape=(32, 32))
|
||||
|
||||
hx, hz, force = node.process(field, height=100e-9, current=1e-3,
|
||||
width=100e-9, tip_magnetization=1e5)
|
||||
|
||||
assert hx.data.shape == (32, 32)
|
||||
assert hz.data.shape == (32, 32)
|
||||
assert force.data.shape == (32, 32)
|
||||
|
||||
|
||||
def test_finite_values():
|
||||
from backend.nodes.mfm_current import MFMCurrentSimulation
|
||||
|
||||
node = MFMCurrentSimulation()
|
||||
field = make_field(shape=(64, 64))
|
||||
|
||||
hx, hz, force = node.process(field, height=100e-9, current=1e-3,
|
||||
width=100e-9, tip_magnetization=1e5)
|
||||
|
||||
assert np.isfinite(hx.data).all()
|
||||
assert np.isfinite(hz.data).all()
|
||||
assert np.isfinite(force.data).all()
|
||||
|
||||
|
||||
def test_hz_antisymmetric():
|
||||
from backend.nodes.mfm_current import MFMCurrentSimulation
|
||||
|
||||
node = MFMCurrentSimulation()
|
||||
# Use even grid so centre falls between pixels symmetrically
|
||||
field = make_field(shape=(16, 64), xreal=1e-6, yreal=1e-6)
|
||||
|
||||
_, hz, _ = node.process(field, height=100e-9, current=1e-3,
|
||||
width=100e-9, tip_magnetization=1e5)
|
||||
|
||||
hz_row = hz.data[0, :]
|
||||
n = len(hz_row)
|
||||
# The x-grid uses linspace(0, xreal, n, endpoint=False) - xreal/2,
|
||||
# so x[i] + x[n-i] == 0 for i in 1..n-1.
|
||||
# Hz should be antisymmetric about x=0: Hz(x) ≈ -Hz(-x)
|
||||
for i in range(1, n // 2):
|
||||
left = hz_row[i]
|
||||
right = hz_row[n - i]
|
||||
assert np.sign(left) == -np.sign(right), (
|
||||
f"Hz not antisymmetric at positions {i} and {n - i}: "
|
||||
f"{left} vs {right}"
|
||||
)
|
||||
np.testing.assert_allclose(left, -right, rtol=1e-10)
|
||||
|
||||
|
||||
def test_units():
|
||||
from backend.nodes.mfm_current import MFMCurrentSimulation
|
||||
|
||||
node = MFMCurrentSimulation()
|
||||
field = make_field(shape=(32, 32))
|
||||
|
||||
hx, hz, force = node.process(field, height=100e-9, current=1e-3,
|
||||
width=100e-9, tip_magnetization=1e5)
|
||||
|
||||
assert hx.si_unit_z == "A/m"
|
||||
assert hz.si_unit_z == "A/m"
|
||||
assert force.si_unit_z == "N"
|
||||
80
tests/node_tests/mfm_domains.py
Normal file
80
tests/node_tests/mfm_domains.py
Normal file
@@ -0,0 +1,80 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from tests.node_tests._shared import make_field
|
||||
|
||||
|
||||
def test_output_shapes():
|
||||
from backend.nodes.mfm_domains import MFMDomainGeneration
|
||||
|
||||
node = MFMDomainGeneration()
|
||||
field = make_field(shape=(48, 64))
|
||||
|
||||
hz, dhz_dz = node.process(
|
||||
field,
|
||||
height=50e-9,
|
||||
thickness=20e-9,
|
||||
magnetization=1e6,
|
||||
stripe_width_a=200e-9,
|
||||
stripe_width_b=200e-9,
|
||||
gap=0.0,
|
||||
)
|
||||
assert hz.data.shape == (48, 64)
|
||||
assert dhz_dz.data.shape == (48, 64)
|
||||
|
||||
|
||||
def test_finite_values():
|
||||
from backend.nodes.mfm_domains import MFMDomainGeneration
|
||||
|
||||
node = MFMDomainGeneration()
|
||||
field = make_field(shape=(32, 32))
|
||||
|
||||
hz, dhz_dz = node.process(
|
||||
field,
|
||||
height=50e-9,
|
||||
thickness=20e-9,
|
||||
magnetization=1e6,
|
||||
stripe_width_a=200e-9,
|
||||
stripe_width_b=200e-9,
|
||||
gap=0.0,
|
||||
)
|
||||
assert np.isfinite(hz.data).all()
|
||||
assert np.isfinite(dhz_dz.data).all()
|
||||
|
||||
|
||||
def test_uniform_along_y():
|
||||
"""Stripes run along y, so every row of the output should be identical."""
|
||||
from backend.nodes.mfm_domains import MFMDomainGeneration
|
||||
|
||||
node = MFMDomainGeneration()
|
||||
field = make_field(shape=(64, 64))
|
||||
|
||||
hz, dhz_dz = node.process(
|
||||
field,
|
||||
height=50e-9,
|
||||
thickness=20e-9,
|
||||
magnetization=1e6,
|
||||
stripe_width_a=200e-9,
|
||||
stripe_width_b=200e-9,
|
||||
gap=0.0,
|
||||
)
|
||||
assert np.allclose(hz.data[0], hz.data[hz.data.shape[0] // 2])
|
||||
assert np.allclose(dhz_dz.data[0], dhz_dz.data[dhz_dz.data.shape[0] // 2])
|
||||
|
||||
|
||||
def test_units():
|
||||
from backend.nodes.mfm_domains import MFMDomainGeneration
|
||||
|
||||
node = MFMDomainGeneration()
|
||||
field = make_field(shape=(32, 32))
|
||||
|
||||
hz, dhz_dz = node.process(
|
||||
field,
|
||||
height=50e-9,
|
||||
thickness=20e-9,
|
||||
magnetization=1e6,
|
||||
stripe_width_a=200e-9,
|
||||
stripe_width_b=200e-9,
|
||||
gap=0.0,
|
||||
)
|
||||
assert hz.si_unit_z == "A/m"
|
||||
assert dhz_dz.si_unit_z == "A/m²"
|
||||
69
tests/node_tests/pfm_analysis.py
Normal file
69
tests/node_tests/pfm_analysis.py
Normal file
@@ -0,0 +1,69 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from tests.node_tests._shared import make_field
|
||||
|
||||
|
||||
def test_output_shapes():
|
||||
from backend.nodes.pfm_analysis import PFMAnalysis
|
||||
|
||||
node = PFMAnalysis()
|
||||
vpfm_amp = make_field(data=np.abs(make_field(shape=(48, 64)).data), shape=(48, 64))
|
||||
lpfm_amp = make_field(data=np.abs(make_field(shape=(48, 64)).data), shape=(48, 64))
|
||||
vpfm_phase = make_field(shape=(48, 64))
|
||||
lpfm_phase = make_field(shape=(48, 64))
|
||||
|
||||
magnitude, azimuth, inclination = node.process(
|
||||
vpfm_amp, lpfm_amp, vpfm_phase, lpfm_phase, "3d", 1.0,
|
||||
)
|
||||
|
||||
assert magnitude.data.shape == (48, 64)
|
||||
assert azimuth.data.shape == (48, 64)
|
||||
assert inclination.data.shape == (48, 64)
|
||||
|
||||
|
||||
def test_2d_mode_zero_inclination():
|
||||
from backend.nodes.pfm_analysis import PFMAnalysis
|
||||
|
||||
node = PFMAnalysis()
|
||||
vpfm_amp = make_field(data=np.abs(make_field().data))
|
||||
lpfm_amp = make_field(data=np.abs(make_field().data))
|
||||
vpfm_phase = make_field()
|
||||
lpfm_phase = make_field()
|
||||
|
||||
magnitude, azimuth, inclination = node.process(
|
||||
vpfm_amp, lpfm_amp, vpfm_phase, lpfm_phase, "2d", 1.0,
|
||||
)
|
||||
|
||||
assert np.allclose(inclination.data, 0.0)
|
||||
|
||||
|
||||
def test_3d_mode_nonzero_inclination():
|
||||
from backend.nodes.pfm_analysis import PFMAnalysis
|
||||
|
||||
node = PFMAnalysis()
|
||||
vpfm_amp = make_field(data=np.abs(make_field().data))
|
||||
lpfm_amp = make_field(data=np.abs(make_field().data))
|
||||
vpfm_phase = make_field()
|
||||
lpfm_phase = make_field()
|
||||
|
||||
magnitude, azimuth, inclination = node.process(
|
||||
vpfm_amp, lpfm_amp, vpfm_phase, lpfm_phase, "3d", 1.0,
|
||||
)
|
||||
|
||||
assert not np.allclose(inclination.data, 0.0)
|
||||
|
||||
|
||||
def test_magnitude_nonnegative():
|
||||
from backend.nodes.pfm_analysis import PFMAnalysis
|
||||
|
||||
node = PFMAnalysis()
|
||||
vpfm_amp = make_field(data=np.abs(make_field().data))
|
||||
lpfm_amp = make_field(data=np.abs(make_field().data))
|
||||
vpfm_phase = make_field()
|
||||
lpfm_phase = make_field()
|
||||
|
||||
for mode in ("2d", "3d"):
|
||||
magnitude, azimuth, inclination = node.process(
|
||||
vpfm_amp, lpfm_amp, vpfm_phase, lpfm_phase, mode, 1.0,
|
||||
)
|
||||
assert np.all(magnitude.data >= 0.0)
|
||||
46
tests/node_tests/sem_simulation.py
Normal file
46
tests/node_tests/sem_simulation.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from tests.node_tests._shared import make_field
|
||||
|
||||
|
||||
def test_output_shape():
|
||||
from backend.nodes.sem_simulation import SEMSimulation
|
||||
|
||||
node = SEMSimulation()
|
||||
field = make_field(shape=(48, 64))
|
||||
|
||||
for method in ("integration", "monte_carlo"):
|
||||
result, = node.process(field, method, sigma=3.0, n_samples=20)
|
||||
assert result.data.shape == (48, 64)
|
||||
|
||||
|
||||
def test_flat_surface_zero():
|
||||
"""Flat topography (all zeros) should produce all-zero or near-zero SEM signal."""
|
||||
from backend.nodes.sem_simulation import SEMSimulation
|
||||
|
||||
node = SEMSimulation()
|
||||
field = make_field(data=np.zeros((32, 32)))
|
||||
|
||||
for method in ("integration", "monte_carlo"):
|
||||
result, = node.process(field, method, sigma=3.0, n_samples=20)
|
||||
assert np.allclose(result.data, 0.0, atol=1e-10)
|
||||
|
||||
|
||||
def test_integration_method():
|
||||
from backend.nodes.sem_simulation import SEMSimulation
|
||||
|
||||
node = SEMSimulation()
|
||||
field = make_field()
|
||||
|
||||
result, = node.process(field, "integration", sigma=3.0, n_samples=20)
|
||||
assert np.all(np.isfinite(result.data))
|
||||
|
||||
|
||||
def test_monte_carlo_method():
|
||||
from backend.nodes.sem_simulation import SEMSimulation
|
||||
|
||||
node = SEMSimulation()
|
||||
field = make_field()
|
||||
|
||||
result, = node.process(field, "monte_carlo", sigma=3.0, n_samples=20)
|
||||
assert np.all(np.isfinite(result.data))
|
||||
70
tests/node_tests/smm_analysis.py
Normal file
70
tests/node_tests/smm_analysis.py
Normal file
@@ -0,0 +1,70 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from tests.node_tests._shared import make_field
|
||||
|
||||
|
||||
def _make_inputs():
|
||||
"""Return amplitude and phase fields suitable for SMM analysis."""
|
||||
raw = np.abs(make_field().data)
|
||||
amplitude_data = raw / raw.max() * 0.8 + 0.1
|
||||
amplitude = make_field(data=amplitude_data)
|
||||
phase = make_field()
|
||||
return amplitude, phase
|
||||
|
||||
|
||||
def test_output_shapes():
|
||||
from backend.nodes.smm_analysis import SMMAnalysis
|
||||
|
||||
node = SMMAnalysis()
|
||||
amplitude, phase = _make_inputs()
|
||||
cap, imp = node.process(
|
||||
amplitude, phase,
|
||||
frequency=1e9, ref_impedance=50.0,
|
||||
cal_c1=1e-15, cal_c2=10e-15, cal_c3=100e-15,
|
||||
cal_s11_1=0.9, cal_s11_2=0.5, cal_s11_3=0.1,
|
||||
)
|
||||
assert cap.data.shape == amplitude.data.shape
|
||||
assert imp.data.shape == amplitude.data.shape
|
||||
|
||||
|
||||
def test_finite_outputs():
|
||||
from backend.nodes.smm_analysis import SMMAnalysis
|
||||
|
||||
node = SMMAnalysis()
|
||||
amplitude, phase = _make_inputs()
|
||||
cap, imp = node.process(
|
||||
amplitude, phase,
|
||||
frequency=1e9, ref_impedance=50.0,
|
||||
cal_c1=1e-15, cal_c2=10e-15, cal_c3=100e-15,
|
||||
cal_s11_1=0.9, cal_s11_2=0.5, cal_s11_3=0.1,
|
||||
)
|
||||
assert np.isfinite(cap.data).all()
|
||||
assert np.isfinite(imp.data).all()
|
||||
|
||||
|
||||
def test_capacitance_units():
|
||||
from backend.nodes.smm_analysis import SMMAnalysis
|
||||
|
||||
node = SMMAnalysis()
|
||||
amplitude, phase = _make_inputs()
|
||||
cap, _imp = node.process(
|
||||
amplitude, phase,
|
||||
frequency=1e9, ref_impedance=50.0,
|
||||
cal_c1=1e-15, cal_c2=10e-15, cal_c3=100e-15,
|
||||
cal_s11_1=0.9, cal_s11_2=0.5, cal_s11_3=0.1,
|
||||
)
|
||||
assert cap.si_unit_z == "F"
|
||||
|
||||
|
||||
def test_impedance_units():
|
||||
from backend.nodes.smm_analysis import SMMAnalysis
|
||||
|
||||
node = SMMAnalysis()
|
||||
amplitude, phase = _make_inputs()
|
||||
_cap, imp = node.process(
|
||||
amplitude, phase,
|
||||
frequency=1e9, ref_impedance=50.0,
|
||||
cal_c1=1e-15, cal_c2=10e-15, cal_c3=100e-15,
|
||||
cal_s11_1=0.9, cal_s11_2=0.5, cal_s11_3=0.1,
|
||||
)
|
||||
assert imp.si_unit_z == "Ohm"
|
||||
@@ -1,57 +1,232 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from tests.node_tests._shared import make_field
|
||||
|
||||
|
||||
def test_all_patterns_produce_correct_shape():
|
||||
# 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
|
||||
|
||||
node = SyntheticSurface()
|
||||
for pattern in ("fbm", "white_noise", "lattice", "steps", "particles", "flat"):
|
||||
result, = node.process(
|
||||
pattern=pattern, xres=64, yres=48, xreal=1e-6, yreal=1e-6,
|
||||
amplitude=1e-9, seed=42,
|
||||
)
|
||||
assert result.data.shape == (48, 64), f"Failed for {pattern}"
|
||||
return SyntheticSurface()
|
||||
|
||||
|
||||
def test_flat_is_zero():
|
||||
from backend.nodes.synthetic_surface import SyntheticSurface
|
||||
|
||||
node = SyntheticSurface()
|
||||
result, = node.process(
|
||||
pattern="flat", xres=32, yres=32, xreal=1e-6, yreal=1e-6,
|
||||
amplitude=1e-9, seed=0,
|
||||
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,
|
||||
)
|
||||
assert np.allclose(result.data, 0.0)
|
||||
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():
|
||||
from backend.nodes.synthetic_surface import SyntheticSurface
|
||||
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"
|
||||
|
||||
node = SyntheticSurface()
|
||||
kwargs = dict(pattern="fbm", xres=32, yres=32, 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)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. Amplitude scaling
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_amplitude_scaling():
|
||||
from backend.nodes.synthetic_surface import SyntheticSurface
|
||||
|
||||
node = SyntheticSurface()
|
||||
result, = node.process(
|
||||
pattern="white_noise", xres=64, yres=64, xreal=1e-6, yreal=1e-6,
|
||||
amplitude=5e-9, seed=42,
|
||||
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,
|
||||
)
|
||||
assert result.data.max() <= 5e-9 + 1e-15
|
||||
assert result.data.min() >= -1e-15
|
||||
# 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)
|
||||
|
||||
|
||||
def test_unknown_pattern():
|
||||
from backend.nodes.synthetic_surface import SyntheticSurface
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. Flat pattern produces all zeros
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
node = SyntheticSurface()
|
||||
with pytest.raises(ValueError):
|
||||
node.process(pattern="unknown", xres=32, yres=32, xreal=1e-6, yreal=1e-6,
|
||||
amplitude=1e-9, seed=0)
|
||||
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)}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user