low pri features

This commit is contained in:
2026-04-03 22:09:19 -07:00
parent c24eed104e
commit 5d4c6dfcea
25 changed files with 1707 additions and 117 deletions

View File

@@ -0,0 +1,45 @@
import numpy as np
import pytest
from tests.node_tests._shared import make_field
def test_identity_transform():
"""No shear, unit scale, zero rotation should return near-identical data."""
from backend.nodes.affine_correction import AffineCorrection
node = AffineCorrection()
field = make_field(shape=(32, 32))
result, = node.process(field, shear_x=0.0, shear_y=0.0, scale_x=1.0, scale_y=1.0, angle=0.0)
assert result.data.shape == (32, 32)
assert np.allclose(result.data, field.data, atol=1e-10)
def test_scale_changes_values():
from backend.nodes.affine_correction import AffineCorrection
node = AffineCorrection()
field = make_field(shape=(32, 32))
result, = node.process(field, shear_x=0.0, shear_y=0.0, scale_x=2.0, scale_y=1.0, angle=0.0)
assert result.data.shape == (32, 32)
# Scaled field should differ from original
assert not np.allclose(result.data, field.data)
def test_rotation_preserves_shape():
from backend.nodes.affine_correction import AffineCorrection
node = AffineCorrection()
field = make_field(shape=(48, 64))
result, = node.process(field, shear_x=0.0, shear_y=0.0, scale_x=1.0, scale_y=1.0, angle=10.0)
assert result.data.shape == (48, 64)
def test_shear_changes_values():
from backend.nodes.affine_correction import AffineCorrection
node = AffineCorrection()
# Use a non-symmetric field so shear has a visible effect
data = np.outer(np.arange(32), np.ones(32))
field = make_field(data=data)
result, = node.process(field, shear_x=0.3, shear_y=0.0, scale_x=1.0, scale_y=1.0, angle=0.0)
assert not np.allclose(result.data, field.data)

View File

@@ -0,0 +1,42 @@
import numpy as np
import pytest
from tests.node_tests._shared import make_field
def test_wiener_preserves_shape():
from backend.nodes.deconvolution import Deconvolution
node = Deconvolution()
field = make_field(shape=(32, 32))
result, = node.process(field, "wiener", 2.0, 0.01, 10)
assert result.data.shape == (32, 32)
assert np.isfinite(result.data).all()
def test_richardson_lucy_preserves_shape():
from backend.nodes.deconvolution import Deconvolution
node = Deconvolution()
field = make_field(data=np.abs(np.random.default_rng(42).standard_normal((32, 32))) + 0.1)
result, = node.process(field, "richardson_lucy", 2.0, 0.01, 5)
assert result.data.shape == (32, 32)
assert np.isfinite(result.data).all()
def test_wiener_flat_field():
from backend.nodes.deconvolution import Deconvolution
node = Deconvolution()
field = make_field(data=np.ones((32, 32)))
result, = node.process(field, "wiener", 2.0, 0.01, 10)
# A flat field convolved with anything is still flat; Wiener should preserve it
assert result.data.shape == (32, 32)
def test_unknown_method():
from backend.nodes.deconvolution import Deconvolution
node = Deconvolution()
field = make_field(shape=(32, 32))
with pytest.raises(ValueError):
node.process(field, "unknown", 2.0, 0.01, 10)

View File

@@ -0,0 +1,53 @@
import numpy as np
import pytest
from tests.node_tests._shared import make_field
def test_drift_correction_flat():
from backend.nodes.drift_correction import DriftCorrection
node = DriftCorrection()
field = make_field(data=np.zeros((32, 32)))
result, = node.process(field, "previous_row", "horizontal")
assert result.data.shape == (32, 32)
assert np.allclose(result.data, 0.0, atol=1e-10)
def test_drift_correction_preserves_shape():
from backend.nodes.drift_correction import DriftCorrection
node = DriftCorrection()
field = make_field(shape=(48, 64))
for ref in ("previous_row", "mean_row"):
for direction in ("horizontal", "vertical"):
result, = node.process(field, ref, direction)
assert result.data.shape == (48, 64)
def test_drift_correction_reduces_drift():
"""A field with artificial row-by-row drift should have less variance after correction."""
from backend.nodes.drift_correction import DriftCorrection
node = DriftCorrection()
rng = np.random.default_rng(42)
base = rng.standard_normal((32, 64))
# Add artificial drift: shift each row by cumulative offset
drifted = base.copy()
for i in range(1, 32):
drifted[i] = np.roll(base[i], i)
field = make_field(data=drifted)
result, = node.process(field, "previous_row", "horizontal")
# The corrected field should have lower inter-row variance
row_means_before = np.var(np.diff(drifted, axis=0))
row_means_after = np.var(np.diff(result.data, axis=0))
assert row_means_after <= row_means_before
def test_drift_correction_mean_row_reference():
from backend.nodes.drift_correction import DriftCorrection
node = DriftCorrection()
field = make_field(shape=(32, 32))
result, = node.process(field, "mean_row", "horizontal")
assert result.data.shape == (32, 32)

View File

@@ -0,0 +1,42 @@
import numpy as np
import pytest
from tests.node_tests._shared import make_field
def test_facet_analysis_basic():
from backend.nodes.facet_analysis import FacetAnalysis
node = FacetAnalysis()
field = make_field(shape=(64, 64))
result, = node.process(field, 180, 3)
assert result.data.ndim == 2
assert result.si_unit_xy == "deg"
def test_facet_analysis_flat_field():
from backend.nodes.facet_analysis import FacetAnalysis
node = FacetAnalysis()
field = make_field(data=np.zeros((32, 32)))
result, = node.process(field, 180, 3)
assert result.data.ndim == 2
def test_facet_analysis_density_normalised():
from backend.nodes.facet_analysis import FacetAnalysis
node = FacetAnalysis()
field = make_field(shape=(64, 64))
result, = node.process(field, 180, 3)
# Should be a normalised probability density
assert np.isclose(result.data.sum(), 1.0, atol=1e-10)
def test_facet_analysis_bin_count():
from backend.nodes.facet_analysis import FacetAnalysis
node = FacetAnalysis()
field = make_field(shape=(64, 64))
result, = node.process(field, 360, 3)
# phi bins = n_bins, theta bins = n_bins // 4
assert result.data.shape == (90, 360)

View File

@@ -0,0 +1,50 @@
import numpy as np
import pytest
from tests.node_tests._shared import make_field
def test_canny_edge_detection():
from backend.nodes.feature_detection import FeatureDetection
node = FeatureDetection()
# Create a field with a sharp edge
data = np.zeros((64, 64))
data[:, 32:] = 1.0
field = make_field(data=data)
result, records = node.process(field, "canny", 1.0)
assert result.data.shape == (64, 64)
# Should detect some edge pixels
assert result.data.sum() > 0
assert isinstance(records, list)
def test_harris_corner_detection():
from backend.nodes.feature_detection import FeatureDetection
node = FeatureDetection()
# Create a field with a sharp corner (L-shape)
data = np.zeros((64, 64))
data[16:48, 16:48] = 1.0
field = make_field(data=data)
result, records = node.process(field, "harris", 1.0)
assert result.data.shape == (64, 64)
assert isinstance(records, list)
def test_canny_flat_field():
from backend.nodes.feature_detection import FeatureDetection
node = FeatureDetection()
field = make_field(data=np.ones((32, 32)))
result, records = node.process(field, "canny", 1.0)
# Flat field should have no edges
assert result.data.sum() == 0
def test_unknown_method():
from backend.nodes.feature_detection import FeatureDetection
node = FeatureDetection()
field = make_field(shape=(32, 32))
with pytest.raises(ValueError):
node.process(field, "unknown", 1.0)

View File

@@ -0,0 +1,50 @@
import numpy as np
import pytest
from tests.node_tests._shared import make_field
def test_hough_lines_basic():
from backend.nodes.hough_transform import HoughTransform
node = HoughTransform()
# Create a field with a horizontal line
data = np.zeros((64, 64))
data[32, :] = 1.0
field = make_field(data=data)
accum, records = node.process(field, "lines", 3, 1.0, 10, 30)
assert accum.data.ndim == 2
assert isinstance(records, list)
def test_hough_circles_basic():
from backend.nodes.hough_transform import HoughTransform
node = HoughTransform()
# Create a field with a circle
data = np.zeros((64, 64))
yy, xx = np.ogrid[:64, :64]
r2 = (yy - 32)**2 + (xx - 32)**2
data[(r2 > 144) & (r2 < 196)] = 1.0 # ring at radius ~13
field = make_field(data=data)
accum, records = node.process(field, "circles", 3, 1.0, 8, 20)
assert accum.data.shape == (64, 64)
assert isinstance(records, list)
def test_hough_preserves_output_types():
from backend.nodes.hough_transform import HoughTransform
node = HoughTransform()
field = make_field(shape=(32, 32))
accum, records = node.process(field, "lines", 2, 1.0, 5, 15)
assert hasattr(accum, 'data')
assert isinstance(records, list)
def test_hough_unknown_mode():
from backend.nodes.hough_transform import HoughTransform
node = HoughTransform()
field = make_field(shape=(32, 32))
with pytest.raises(ValueError):
node.process(field, "unknown", 1, 1.0, 5, 15)

View File

@@ -0,0 +1,45 @@
import numpy as np
import pytest
from tests.node_tests._shared import make_field
def test_stitch_right_no_overlap():
from backend.nodes.image_stitch import ImageStitch
node = ImageStitch()
a = make_field(data=np.ones((32, 32)))
b = make_field(data=np.ones((32, 32)) * 2)
result, = node.process(a, b, "right", "none")
assert result.data.shape[0] == 32
assert result.data.shape[1] >= 32
def test_stitch_below():
from backend.nodes.image_stitch import ImageStitch
node = ImageStitch()
a = make_field(data=np.ones((32, 32)))
b = make_field(data=np.ones((32, 32)) * 2)
result, = node.process(a, b, "below", "none")
assert result.data.shape[1] == 32
assert result.data.shape[0] >= 32
def test_stitch_auto_direction():
from backend.nodes.image_stitch import ImageStitch
node = ImageStitch()
a = make_field(data=np.random.default_rng(0).standard_normal((32, 32)))
b = make_field(data=np.random.default_rng(1).standard_normal((32, 32)))
result, = node.process(a, b, "auto", "linear")
assert result.data.ndim == 2
def test_stitch_unknown_direction():
from backend.nodes.image_stitch import ImageStitch
node = ImageStitch()
a = make_field(shape=(16, 16))
b = make_field(shape=(16, 16))
with pytest.raises(ValueError):
node.process(a, b, "unknown", "none")

View File

@@ -0,0 +1,46 @@
import numpy as np
import pytest
from tests.node_tests._shared import make_field
def test_lattice_acf_returns_outputs():
from backend.nodes.lattice_measurement import LatticeMeasurement
node = LatticeMeasurement()
field = make_field(shape=(64, 64))
corr, records = node.process(field, "acf")
assert corr.data.shape == (64, 64)
assert isinstance(records, list)
def test_lattice_fft_returns_outputs():
from backend.nodes.lattice_measurement import LatticeMeasurement
node = LatticeMeasurement()
field = make_field(shape=(64, 64))
corr, records = node.process(field, "fft")
assert corr.data.shape == (64, 64)
def test_lattice_detects_periodic_structure():
"""A simple cosine grid should produce lattice measurements."""
from backend.nodes.lattice_measurement import LatticeMeasurement
node = LatticeMeasurement()
x = np.linspace(0, 4 * np.pi, 64, endpoint=False)
X, Y = np.meshgrid(x, x)
data = np.cos(X) + np.cos(Y)
field = make_field(data=data)
corr, records = node.process(field, "acf")
# Should detect at least one vector
assert len(records) >= 3
def test_lattice_unknown_method():
from backend.nodes.lattice_measurement import LatticeMeasurement
node = LatticeMeasurement()
field = make_field(shape=(32, 32))
with pytest.raises(ValueError):
node.process(field, "unknown")

View File

@@ -0,0 +1,47 @@
import numpy as np
import pytest
from tests.node_tests._shared import make_field
def test_mfm_all_operations():
from backend.nodes.mfm_analysis import MFMAnalysis
node = MFMAnalysis()
field = make_field(shape=(32, 32))
for op in ("phase_to_force_gradient", "force_gradient_to_field",
"charge_density", "magnetisation"):
result, = node.process(field, op, 50e-9)
assert result.data.shape == (32, 32)
assert np.isfinite(result.data).all()
def test_mfm_flat_field():
from backend.nodes.mfm_analysis import MFMAnalysis
node = MFMAnalysis()
field = make_field(data=np.zeros((32, 32)))
result, = node.process(field, "phase_to_force_gradient", 50e-9)
assert np.allclose(result.data, 0.0, atol=1e-10)
def test_mfm_units():
from backend.nodes.mfm_analysis import MFMAnalysis
node = MFMAnalysis()
field = make_field(shape=(32, 32))
result, = node.process(field, "force_gradient_to_field", 50e-9)
assert result.si_unit_z == "A/m"
result, = node.process(field, "charge_density", 50e-9)
assert result.si_unit_z == "A/m²"
def test_mfm_unknown_operation():
from backend.nodes.mfm_analysis import MFMAnalysis
node = MFMAnalysis()
field = make_field(shape=(32, 32))
with pytest.raises(ValueError):
node.process(field, "unknown_op", 50e-9)

View File

@@ -8,8 +8,7 @@ def test_scar_removal():
node = ScarRemoval()
info = get_node_info("ScarRemoval")
assert info["category"] == "Filter"
assert {entry["category"] for entry in info["menu_categories"]} == {"Filter", "Level & Correct"}
assert info["category"] == "Level & Correct"
rows = 96
cols = 128

View File

@@ -0,0 +1,45 @@
import numpy as np
import pytest
from tests.node_tests._shared import make_field
def test_fit_sphere_residual():
from backend.nodes.shape_fitting import ShapeFitting
node = ShapeFitting()
# Create a spherical surface
y, x = np.mgrid[:32, :32]
data = 100.0 - np.sqrt(np.maximum(500**2 - (x - 16)**2 - (y - 16)**2, 0))
field = make_field(data=data)
result, records = node.process(field, "sphere", "residual")
assert result.data.shape == (32, 32)
assert isinstance(records, list)
def test_fit_paraboloid():
from backend.nodes.shape_fitting import ShapeFitting
node = ShapeFitting()
field = make_field(shape=(32, 32))
result, records = node.process(field, "paraboloid", "fitted")
assert result.data.shape == (32, 32)
assert isinstance(records, list)
def test_fit_cylinder():
from backend.nodes.shape_fitting import ShapeFitting
node = ShapeFitting()
field = make_field(shape=(32, 32))
result, records = node.process(field, "cylinder", "residual")
assert result.data.shape == (32, 32)
assert isinstance(records, list)
def test_fit_unknown_shape():
from backend.nodes.shape_fitting import ShapeFitting
node = ShapeFitting()
field = make_field(shape=(32, 32))
with pytest.raises(ValueError):
node.process(field, "cone", "residual")

View File

@@ -0,0 +1,57 @@
import numpy as np
import pytest
def test_all_patterns_produce_correct_shape():
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}"
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,
)
assert np.allclose(result.data, 0.0)
def test_seed_reproducibility():
from backend.nodes.synthetic_surface import SyntheticSurface
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)
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,
)
assert result.data.max() <= 5e-9 + 1e-15
assert result.data.min() >= -1e-15
def test_unknown_pattern():
from backend.nodes.synthetic_surface import SyntheticSurface
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)